diff options
author | Karl Berry <karl@freefriends.org> | 2018-04-09 21:09:51 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2018-04-09 21:09:51 +0000 |
commit | 4d2d4764dde3caf40817a1ef84719e1319b10d2f (patch) | |
tree | 096967b6679e1e110b75e42e1747936323508fc4 /Master/texmf-dist/scripts | |
parent | 1e0147a88c7bc3a3e2986968f697ef5fbcef2bcb (diff) |
make4ht (9apr18)
git-svn-id: svn://tug.org/texlive/trunk@47398 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/scripts')
15 files changed, 1174 insertions, 508 deletions
diff --git a/Master/texmf-dist/scripts/make4ht/domfilters/make4ht-aeneas.lua b/Master/texmf-dist/scripts/make4ht/domfilters/make4ht-aeneas.lua new file mode 100644 index 00000000000..0b93c958404 --- /dev/null +++ b/Master/texmf-dist/scripts/make4ht/domfilters/make4ht-aeneas.lua @@ -0,0 +1,144 @@ +-- DOM filter for Aeneas, tool for automatical text and audio synchronization +-- https://github.com/readbeyond/aeneas +-- It adds elements with id attributes for text chunks, in sentence length. +-- +-- +local cssquery = require "luaxml-cssquery" +local mkutils = require "mkutils" + +-- Table of CSS selectors to be skipped. +local skip_elements = { "math", "svg"} + +-- The id attribute format is configurable +-- Aeneas must be told to search for the ID pattern using is_text_unparsed_id_regex +-- option in Aneas configuration file +local id_prefix = "ast" + +-- Pattern to mach a sentence. It should match two groups, first is actual +-- sentence, the second optional interpunction mark. +local sentence_match = "([^%.^%?^!]*)([%.%?!]?)" + +-- convert table with selectors to a query list +local function prepare_selectors(skips) + local css = cssquery() + for _, selector in ipairs(skips) do + css:add_selector(selector) + end + return css +end + +-- save the HTML language +local function save_config(dom, saves) + local get_lang = function(d) + local html = d:query_selector("html")[1] or {} + return html:get_attribute("lang") + end + local saves = saves or {} + local config = get_filter_settings "aeneas_config" + if config.language then return end + saves.lang = get_lang(dom) + filter_settings "aeneas-config" (saves) +end +-- make span element with unique id for a sentence +local function make_span(id,parent, text) + local newobj = parent:create_element("span", {id=id }) + newobj.processed = true -- to disable multiple processing of the node + local text_node = newobj:create_text_node(text) + newobj:add_child_node(text_node) + return newobj +end + +-- make the id attribute and update the id value +local function make_id(lastid, id_prefix) + local id = id_prefix .. lastid + lastid = lastid + 1 + return id, lastid +end + +-- parse text for sentences and add spans +local function make_ids(parent, text, lastid, id_prefix) + local t = {} + local id + for chunk, punct in text:gmatch(sentence_match) do + id, lastid = make_id(lastid, id_prefix) + local newtext = chunk..punct + -- the newtext is empty string sometimes. we can skipt it then. + if newtext~="" then + table.insert(t, make_span(id, parent, newtext)) + end + end + return t, lastid +end + + + +-- test if the DOM element is in list of skipped CSS selectors +local function is_skipped(el, css) + local matched = css:match_querylist(el) + return #matched > 0 +end + +local function aeneas(dom, par) + local par = par or {} + local id = 1 + local options = get_filter_settings "aeneas" + local skip_elements = options.skip_elements or par.skip_elements or skip_elements + local id_prefix = options.id_prefix or par.id_prefix or id_prefix + local skip_object = prepare_selectors(skip_elements) + sentence_match = options.sentence_match or par.sentence_match or sentence_match + + local body = dom:query_selector("body")[1] + -- process only the document body + if not body then return dom end + -- save information for aeneas_config + save_config(dom, {id_prefix = id_prefix}) + body:traverse_elements(function(el) + -- skip disabled elements + if(is_skipped(el, skip_object)) then return false end + -- skip already processed elements + if el.processed then return false end + local newchildren = {} -- this will contain the new elements + local children = el:get_children() + local first_child = children[1] + + -- if the element contains only text, doesn't already have an id attribute and the text is short, + -- the id is set directly on that element. + if #children == 1 + and first_child:is_text() + and not el:get_attribute("id") + and string.len(first_child._text) < 20 + and el._attr + then + local idtitle + idtitle, id = make_id(id, id_prefix) + print(el._name, first_child._text) + el:set_attribute("id", idtitle) + return el + end + + for _, child in ipairs(children) do + -- process only non-empty text + if child:is_text() and child._text:match("%a+") then + local newnodes + newnodes, id = make_ids(child, child._text, id, id_prefix) + for _, node in ipairs(newnodes) do + table.insert(newchildren, node or {}) + end + else + -- insert the current processing element to the new element list + -- if it isn't only text + table.insert(newchildren, child or {}) + end + end + -- replace element children with the new ones + if #newchildren > 0 then + el._children = {} + for _, c in ipairs(newchildren) do + el:add_child_node(c) + end + end + end) + return dom +end + +return aeneas diff --git a/Master/texmf-dist/scripts/make4ht/domfilters/make4ht-fixinlines.lua b/Master/texmf-dist/scripts/make4ht/domfilters/make4ht-fixinlines.lua new file mode 100644 index 00000000000..25489f59ca5 --- /dev/null +++ b/Master/texmf-dist/scripts/make4ht/domfilters/make4ht-fixinlines.lua @@ -0,0 +1,57 @@ +local inline_elements = { + b=true, + big=true, + i=true, + small=true, + tt=true, + abbr=true, + acronym=true, + cite=true, + code=true, + dfn=true, + em=true, + kbd=true, + strong=true, + samp=true, + time=true, + var=true, + a=true, + bdo=true, + br=true, + img=true, + map=true, + object=true, + q=true, + script=true, + span=true, + sub=true, + sup=true, + button=true, + input=true, + label=true, + select=true, + textarea=true, + mn=true, + mi=true +} + + +local function fix_inlines(obj) + local settings = get_filter_settings "fixinlines" + local inline_elements = settings.inline_elements or inline_elements + local nodes = obj:get_path("html body") + obj:traverse_node_list(nodes, function(jej) + if jej._type == "ELEMENT" then + local name = string.lower(jej._name) + -- local parent = jej:get_parent_node() + if inline_elements[name] then + local new = obj:create_element("p" ) + new:add_child_node(obj:copy_node(jej)) + jej:replace_node(new) + end + end + end) + return obj +end + +return fix_inlines diff --git a/Master/texmf-dist/scripts/make4ht/domfilters/make4ht-idcolons.lua b/Master/texmf-dist/scripts/make4ht/domfilters/make4ht-idcolons.lua new file mode 100644 index 00000000000..ccdafbd6d41 --- /dev/null +++ b/Master/texmf-dist/scripts/make4ht/domfilters/make4ht-idcolons.lua @@ -0,0 +1,20 @@ +local function id_colons(obj) + -- replace : characters in links and ids with unserscores + obj:traverse_elements(function(el) + local name = string.lower(obj:get_element_name(el)) + if name == "a" then + local href = obj:get_attribute(el, "href") + if href and not href:match("[a-z]%://") then + obj:set_attribute(el, "href", href:gsub(":", "_")) + end + end + local id = obj:get_attribute( el , "id") + if id then + obj:set_attribute(el, "id", id:gsub(":", "_")) + end + -- local id = obj:get_attribute(el, "id") + end) + return obj +end + +return id_colons diff --git a/Master/texmf-dist/scripts/make4ht/domfilters/make4ht-joincharacters.lua b/Master/texmf-dist/scripts/make4ht/domfilters/make4ht-joincharacters.lua new file mode 100644 index 00000000000..8116a970da0 --- /dev/null +++ b/Master/texmf-dist/scripts/make4ht/domfilters/make4ht-joincharacters.lua @@ -0,0 +1,87 @@ +local charclases = { + span=true, + mn = true, +} + +local function join_characters(obj,par) + -- join adjanced span and similar elements inserted by + -- tex4ht to just one object. + local par = par or {} + local options = get_filter_settings "joincharacters" + local charclases = options.charclases or par.charclases or charclases + + obj:traverse_elements(function(el) + local get_name = function(curr) + return string.lower(curr:get_element_name()) + end + local get_class = function(next_el) + return next_el:get_attribute("class") + end + local is_span = function(next_el) + return charclases[get_name(next_el)] + end + + local function get_next(curr, class) + local next_el = curr:get_next_node() + if next_el and next_el:is_element() and is_span(next_el) then + return next_el + -- if the next node is space followed by a matching element, we should add this space + elseif next_el and next_el:is_text() and get_next(next_el, class) then + local text = next_el._text + -- match only text containing just whitespace + if text:match("^%s+$") then return next_el end + end + end + -- loop over all elements and test if the current element is in a list of + -- processed elements (charclases) + if is_span(el) then + local next_el = get_next(el) + -- loop over the following elements and test whether they are of the same type + -- as the current one + while next_el do + -- save the next element because we will remove it later + local real_next = get_next(next_el) + if get_name(el) == get_name(next_el) and get_class(el) == get_class(next_el) and not el:get_attribute("id") then + -- it the following element match, copy it's children to the current element + for _, child in ipairs(next_el:get_children()) do + el:add_child_node(child) + end + -- remove the next element + next_el:remove_node() + -- add the whitespace + elseif next_el:is_text() then + local s = next_el._text + -- this is needed to fix newlines inserted by Tidy + s = s:gsub("\n", "") + -- we must create a new node + el:add_child_node(el:create_text_node(s)) + next_el:remove_node() + real_next = nil + else + real_next = nil + end + -- use the saved element as a next object + next_el = real_next + end + end + + end) + -- join text nodes in an element into one + obj:traverse_elements(function(el) + -- save the text + local t = {} + local children = el:get_children() + for _, x in ipairs(children) do + if x:is_text() then + t[#t+1] = x._text + else + return nil + end + end + el._text = table.concat(t) + return el + end) + return obj +end + +return join_characters diff --git a/Master/texmf-dist/scripts/make4ht/filters/make4ht-domfilter.lua b/Master/texmf-dist/scripts/make4ht/filters/make4ht-domfilter.lua new file mode 100644 index 00000000000..27d9ed871d1 --- /dev/null +++ b/Master/texmf-dist/scripts/make4ht/filters/make4ht-domfilter.lua @@ -0,0 +1,63 @@ +local filter_lib = require "make4ht-filterlib" +local dom = require "luaxml-domobject" +local mkutils = require "mkutils" + +local function load_filter(filtername) + return require("domfilters.make4ht-"..filtername) +end + +-- save processed names, in order to block multiple executions of the filter +-- sequence on a same file +local processed = {} + +local function filter(filters, name) + -- because XML parsing to DOM is potentially expensive operation + -- this filter will use cache for it's sequence + -- all requests to the domfilter will add new filters to the + -- one sequence, which will be executed on one DOM object. + -- it is possible to request a different sequence using + -- unique name parameter + local name = name or "domfilter" + local settings = mkutils.get_filter_settings(name) or {} + local sequence = settings.sequence or {} + local local_sequence = filter_lib.load_filters(filters, load_filter) + for _, filter in ipairs(local_sequence) do + table.insert(sequence, filter) + end + settings.sequence = sequence + mkutils.filter_settings (name) (settings) + + return function(filename, parameters) + -- load processed files for the current filter name + local processed_files = processed[name] or {} + -- don't process the file again + if processed_files[filename] then + return nil + end + local input = filter_lib.load_input_file(filename) + if not input then return nil, "Cannot load the input file" end + -- we need to use pcall, because XML error would break the whole build process + -- domobject will be error object if DOM parsing failed + local status, domobject = pcall(function() + return dom.parse(input) + end) + if not status then + print("DOM parsing of " .. filename .. " failed:") + print(domobject) + return nil, "DOM parsing failed" + end + for _,f in pairs(sequence) do + domobject = f(domobject,parameters) + end + local output = domobject:serialize() + if output then + filter_lib.save_input_file(filename, output) + else + print("DOM filter failed on ".. filename) + end + -- mark the filename as processed + processed_files[filename] = true + processed[name] = processed_files + end +end +return filter diff --git a/Master/texmf-dist/scripts/make4ht/filters/make4ht-filter.lua b/Master/texmf-dist/scripts/make4ht/filters/make4ht-filter.lua index 4c7ce409a07..bef44f585b5 100644 --- a/Master/texmf-dist/scripts/make4ht/filters/make4ht-filter.lua +++ b/Master/texmf-dist/scripts/make4ht/filters/make4ht-filter.lua @@ -1,41 +1,19 @@ -local filters = {} +local filter_lib = require "make4ht-filterlib" local function load_filter(filtername) return require("filters.make4ht-"..filtername) end -function filter(filters) - local sequence = {} - if type(filters) == "string" then - table.insert(sequence,load_filter(filters)) - elseif type(filters) == "table" then - for _,n in pairs(filters) do - if type(n) == "string" then - table.insert(sequence,load_filter(n)) - elseif type(n) == "function" then - table.insert(sequence, n) - end - end - elseif type(filters) == "function" then - table.insert(sequence, filters) - else - return false, "Argument to filter must be either\ntable with filter names, or single filter name" - end +function filter(filters,parameters) + local sequence = filter_lib.load_filters(filters, load_filter) return function(filename) if not filename then return false, "filters: no filename" end - local input = nil - - if filename then - local file = io.open(filename,"r") - input = file:read("*all") - file:close() - end + local input = filter_lib.load_input_file(filename) + if not input then return nil, "Cannot load the input file" end for _,f in pairs(sequence) do - input = f(input) + input = f(input,parameters) end - local file = io.open(filename,"w") - file:write(input) - file:close() + filter_lib.save_input_file(filename, input) end end return filter diff --git a/Master/texmf-dist/scripts/make4ht/filters/make4ht-mathjaxnode.lua b/Master/texmf-dist/scripts/make4ht/filters/make4ht-mathjaxnode.lua new file mode 100644 index 00000000000..2b453e4255b --- /dev/null +++ b/Master/texmf-dist/scripts/make4ht/filters/make4ht-mathjaxnode.lua @@ -0,0 +1,94 @@ +local mkutils = require "mkutils" +-- other possible value is page2svg +local mathnodepath = "mjpage" +-- options for MathJax command +local options = "--output CommonHTML" +-- math fonts position +-- don't alter fonts if not set +local fontdir = nil +-- if we copy fonts +local fontdest = nil +local fontformat = "woff" +local cssfilename = "mathjax-chtml.css" + +local function compile(text) + local tmpfile = os.tmpname() + print("Compile using MathJax") + local command = mathnodepath .. " ".. options .. " > " .. tmpfile + print(command) + local commandhandle = io.popen(command,"w") + commandhandle:write(text) + commandhandle:close() + print("Result written to: ".. tmpfile) + local f = io.open(tmpfile) + local content = f:read("*all") + f:close() + os.remove(tmpfile) + return content +end + +-- save the css code from the html page generated by MathJax +local function extract_css(contents) + local css = "" + local filename = cssfilename + contents = contents:gsub('<style [^>]+>(.-)</style>', function(style) + -- replace only the style for mathjax + if style:match "%.mjx%-math" then + css = style + return '<link rel="stylesheet" type="text/css" href="'..filename ..'" />' + end + end) + -- local x = assert(io.open(file, "w")) + -- x:write(contents) + -- x:close() + return filename, contents, css +end + +-- Update the paths to fonts to use the local versions +local function use_fonts(css) + local family_pattern = "font%-family:%s*(.-);.-%/([^%/]+)%.".. fontformat + local family_build = "@font-face {font-family: %s; src: url('%s/%s.%s') format('%s')}" + local fontdir = fontdir:gsub("/$","") + css = css:gsub("(@font%-face%s*{.-})", function(face) + if not face:match("url%(") then return face end + -- print(face) + local family, filename = face:match(family_pattern) + print(family, filename) + local newfile = string.format("%s/%s.%s", fontdir, filename, fontformat) + Make:add_file(newfile) + return family_build:format(family, fontdir, filename, fontformat, fontformat) + -- return face + end) + return css +end + + +local function save_css(filename, css) + local f = io.open(filename, "w") + f:write(css) + f:close() +end + +return function(text, arguments) + -- if arguments.prg then mathnodepath = arguments.prg end + local extoptions = mkutils.get_filter_settings "mathjaxnode" or {} + local arguments = arguments or {} + mathnodepath = arguments.prg or extoptions.prg or mathnodepath + options = arguments.options or extoptions.options or options + fontdir = arguments.fontdir or extoptions.fontdir or fontdir + -- the following ne is unused ATM + fontdest = arguments.fontdest or extoptions.fontdest or fontdest + fontformat = arguments.fontformat or extoptions.fontformat or fontformat + cssfilename = arguments.cssfilename or extoptions.cssfilename or cssfilename + local newtext = compile(text) + local cssfile, newtext, css = extract_css(newtext) + -- use local font files if fontdir is present + if fontdir then + css = use_fonts(css) + end + save_css(cssfile, css) + Make:add_file(cssfile) + -- print(css) + print(cssfile) + return newtext +end diff --git a/Master/texmf-dist/scripts/make4ht/filters/make4ht-svg-height.lua b/Master/texmf-dist/scripts/make4ht/filters/make4ht-svg-height.lua new file mode 100644 index 00000000000..44e98748ea9 --- /dev/null +++ b/Master/texmf-dist/scripts/make4ht/filters/make4ht-svg-height.lua @@ -0,0 +1,43 @@ + +-- Make:image("svg$", "dvisvgm -n -a -p ${page} -b preview -c 1.4,1.4 -s ${source} > ${output}") + +local max = function(a,b) + return a > b and a or b +end + +local function get_height(svg) + local height = svg:match("height='([0-9%.]+)pt'") + return tonumber(height) +end + +local function get_max_height(path,max_number) + local coordinates = {} + for number in path:gmatch("(%-?[0-9%.]+)") do + table.insert(coordinates, tonumber(number)) + end + for i = 2, #coordinates, 2 do + max_number = max(max_number, coordinates[i]) + end + return max_number +end + +local function update_height(svg, height) + return svg:gsub("height='.-pt'", "height='"..height .."pt'") +end + +-- we need to fix the svg height +return function(svg) + local max_height = 0 + local height = get_height(svg) + for path in svg:gmatch("path d='([^']+)'") do + -- find highest height in all paths in the svg file + max_height = get_max_height(path, max_height) + end + -- update the height only if the max_height is larger than height set in the SVG file + print(max_height, height) + if max_height > height then + svg = update_height(svg, max_height) + end + return svg +end + diff --git a/Master/texmf-dist/scripts/make4ht/make4ht b/Master/texmf-dist/scripts/make4ht/make4ht index 15bda54aca8..0da3c30549c 100755 --- a/Master/texmf-dist/scripts/make4ht/make4ht +++ b/Master/texmf-dist/scripts/make4ht/make4ht @@ -9,6 +9,7 @@ local make4ht = require("make4ht-lib") local lapp = require("lapp-mk4") local mkutils = require("mkutils") local mkparams = require("mkparams") +local mk_config = require("make4ht-config") -- args string is here just as sample, we dont pass it it to -- mkparams.get_args() so default args string is used local args = [[ @@ -26,7 +27,7 @@ make4ht [options] filename ["tex4ht.sty op." "tex4ht op." "t4ht op" "latex op"] -- set version number. the template should be replaced by the -- actual version number by the build script -local version = "v0.1c" +local version = "v0.2" mkparams.version_number = version local args = mkparams.get_args() @@ -36,6 +37,35 @@ local parameters = mkparams.process_args(args) local mode = parameters.mode local build_file = parameters.build_file +-- handle output formats +local allowed_output_formats = {xhtml = true, html5=true, odt = true} +-- formatter is Lua library which must provide at least prepare_parameters +-- and process_build_sequence functions +local formatter +local output_format = parameters.output_format +if allowed_output_formats[ output_format ] then + formatter = mkutils.load_output_format(output_format) +else + -- load html5 as default output format + if output_format then + print("Cannot load output format: ".. output_format) + end + formatter = mkutils.load_output_format("html5") +end +-- find make4ht configuration file +local configname = "make4ht" +local conffile = mk_config.find_config(configname) or mk_config.find_xdg_config(configname) +if conffile then + print("Using configuration file: " .. conffile) + mkutils.load_config(parameters, conffile) +end +local extensions = formatter.prepare_extensions(parameters.extensions) +extensions = mkutils.load_extensions(extensions, output_format) + + + +-- run extensions with prepare_parameters function +parameters = formatter.prepare_parameters(parameters,extensions) local make = mkutils.load_config(parameters, build_file)["Make"] make.params = parameters if make:length() < 1 then @@ -58,6 +88,10 @@ if #make.image_patterns > 0 then make.params.t4ht_par = make.params.t4ht_par .. " -p" end make:t4ht {ext = ext} +-- run extensions which modify the build sequence +if #extensions > 0 then + make = mkutils.extensions_modify_build(extensions, make) +end make:match("tmp$", function() return false,"tmp file" end) make:match(".*",function(filename,par) local outdir = '' --par["outdir"] and par["outdir"] .."/" or '' @@ -67,4 +101,5 @@ make:match(".*",function(filename,par) mkutils.copy(filename,outfilename) return true end) + make:run() diff --git a/Master/texmf-dist/scripts/make4ht/make4ht-aeneas-config.lua b/Master/texmf-dist/scripts/make4ht/make4ht-aeneas-config.lua new file mode 100755 index 00000000000..d0b4a1360d5 --- /dev/null +++ b/Master/texmf-dist/scripts/make4ht/make4ht-aeneas-config.lua @@ -0,0 +1,215 @@ +local M = {} + +local mkutils = require "mkutils" + +local task_template = [[ +<task> + <task_language>${lang}</task_language> + <task_description>${file_desc}</task_description> + <task_custom_id>${file_id}</task_custom_id> + <is_text_file>${prefix}${html_file}</is_text_file> + <is_text_type>${text_type}</is_text_type> + <is_audio_file>${prefix}${audio_file}</is_audio_file> + <is_text_unparsed_id_sort>${id_sort}</is_text_unparsed_id_sort> + <is_text_unparsed_id_regex>${id_regex}</is_text_unparsed_id_regex> + <os_task_file_name>${sub_file}</os_task_file_name> + <os_task_file_format>${sub_format}</os_task_file_format> + <os_task_file_smil_page_ref>${html_file}</os_task_file_smil_page_ref> + <os_task_file_smil_audio_ref>${audio_file}</os_task_file_smil_audio_ref> +</task> +]] + +-- get html files +local function get_html_files(config) + local config = config or {} + local files = {} + local filematch = config.file_match or "html$" + -- this is a trick to get list of files from the LG file + for _, file in ipairs(Make.lgfile.files) do + if file:match(filematch) then table.insert(files, file) end + end + return files +end + +-- prepare filename for the audio +local function get_audio_file(filename, config) + local extension = config.audio_extension or "mp3" + local base = mkutils.remove_extension(filename) + return base .. "." .. extension +end + +local function get_sub_file(filename, config) + local extension = config.sub_format or "smil" + local base = mkutils.remove_extension(filename) + return base .. "." .. extension +end + + +-- create task record for each HTML file +local function prepare_tasks(files, configuration) + local tasks = {} + -- the map can contain info for particular files, otherwise we will interfere default values + local map = configuration.map or {} + -- task_template should be configurable + local task_template = configuration.task_template or task_template + for i, filename in ipairs(files) do + local filemap = map[filename] + if filemap ~= false then + filemap = filemap or {} + local taskconfig = configuration + taskconfig.html_file = filename + taskconfig.prefix = filemap.prefix or configuration.prefix + taskconfig.file_desc = filemap.description or configuration.description .. " " .. i + taskconfig.file_id = filemap.id or filename:gsub("[%/%.]", "_") + taskconfig.text_type = filemap.text_type or configuration.text_type + taskconfig.audio_file = filemap.audio_file or get_audio_file(filename, configuration) + taskconfig.sub_file = filemap.sub_file or get_sub_file(filename, configuration) + taskconfig.id_sort= filemap.id_sort or configuration.id_sort + taskconfig.id_prefix = filemap.id_regex or configuration.id_regex + taskconfig.sub_format = filemap.sub_format or configuration.sub_format + tasks[#tasks+1] = task_template % taskconfig + Make:add_file(taskconfig.audio_file) + Make:add_file(taskconfig.sub_file) + end + end + return tasks --table.concat(tasks, "\n") +end +-- from https://www.readbeyond.it/aeneas/docs/clitutorial.html#xml-config-file-config-xml +local config_template = [[ +<job> + <job_language>${lang}</job_language> + <job_description>${description}</job_description> + <tasks> + ${tasks} + </tasks> + <os_job_file_name>output_example4</os_job_file_name> + <os_job_file_container>zip</os_job_file_container> + <os_job_file_hierarchy_type>flat</os_job_file_hierarchy_type> + <os_job_file_hierarchy_prefix>${prefix}</os_job_file_hierarchy_prefix> +</job> +]] + +-- check if the config file exists +local function is_config(filename) + return mkutils.file_exists(filename) +end + +-- prepare Aeneas configuration +local function prepare_configuration(parameters) + local config = parameters or {} + config.lang = parameters.lang + config.tasks = table.concat(prepare_tasks(parameters.files, config), "\n") + return config +end + +-- write Aeneeas configuration file in the XML format +local function write_config(filename, configuration) + local cfg = config_template % configuration + print(cfg) + local f = io.open(filename, "w") + f:write(cfg) + f:close() +end + + +local function make_default_options(options) + local configuration = {} + local par = get_filter_settings "aeneas-config" + configuration.lang = options.lang or par.lang or "en" + configuration.description = options.description or par.description or "Aeneas job" + configuration.map = options.map or par.map or {} + configuration.text_type = options.text_type or par.text_type or "unparsed" + configuration.id_sort = options.id_sort or par.id_sort or "numeric" + configuration.id_regex = options.id_regex or par.id_regex or par.id_prefix .. "[0-9]+" + configuration.sub_format = options.sub_format or par.sub_format or "smil" + configuration.prefix = options.prefix or par.prefix or "./" + configuration.config_name = options.config_name or par.config_name or "config.xml" + configuration.keep_config = options.keep_config or par.keep_config + return configuration +end + + +local function configure_job(options) + local configuration = make_default_options(options) + local config_name = configuration.config_name + -- prepare the configuration in every case + configuration.files = get_html_files() + local configuration = prepare_configuration(configuration) + -- write the configuration only if the config file doesn't exist + -- and keep_config option is set to true + if is_config(config_name) and configuration.keep_config==true then + else + write_config(config_name, configuration) + end +end + +local function execute_job(options) + local par = get_filter_settings "aeneas-config" + local configuration = make_default_options(options) + configuration.files = get_html_files() + -- we need to configure prepare_tasks to return calls to aeneas task convertor + configuration.python = options.python or par.python or "python3" + configuration.module = options.module or par.module or "aeneas.tools.execute_task" + configuration.task_template = '${python} -m "${module}" "${audio_file}" "${html_file}" "is_text_type=${text_type}|os_task_file_smil_audio_ref=${audio_file}|os_task_file_smil_page_ref=${html_file}|task_language=${lang}|is_text_unparsed_id_sort=${id_sort}|is_text_unparsed_id_regex=${id_regex}|os_task_file_format=${sub_format}" "${sub_file}"' + local tasks = prepare_tasks(configuration.files, configuration) + -- execute the tasks + for _, v in ipairs(tasks) do + print("task", v) + local proc = io.popen(v, "r") + local result = proc:read("*all") + proc:close() + print(result) + end +end + +-- the aeneas configuration must be executed at last processed file, after all filters +-- have been executed +local function get_last_lg_file() + local t = Make.lgfile.files + for i = #t, 1, -1 do + -- find last html file or the tmp file + local x = t[i] + if x:match "html$" or x:match "tmp$" then + return x + end + end + return t[#t] +end + +-- write Aeneas job configuration file +-- it doesn't execute Aeneas +function M.write_job(par) + -- configuration table for Aeneas job + Make:match("tmp$", function() + configure_job(par) + end) +end + +-- execute Aeneas directly +function M.execute(par) + Make:match("tmp$", function(current_name) + -- there may be html files after the .tmp file + -- the aeneas must be executed after the Aeneas filter inserts the id + -- attributes, so it is necessary to execute this code as very last one + local last = get_last_lg_file() + -- execute the job if there are no HTML files after the tmp file + if current_name == last then + execute_job(par) + end + Make:match(last, function() + execute_job(par) + end) + end) +end + +-- only register the audio and smil files as processed files +function M.process_files(par) + Make:match("tmp$", function() + local configuration = make_default_options(par) + local files = get_html_files() + prepare_tasks(files, configuration) + end) +end + + +return M diff --git a/Master/texmf-dist/scripts/make4ht/make4ht-config.lua b/Master/texmf-dist/scripts/make4ht/make4ht-config.lua new file mode 100755 index 00000000000..bd83632a574 --- /dev/null +++ b/Master/texmf-dist/scripts/make4ht/make4ht-config.lua @@ -0,0 +1,92 @@ +local m = {} + +local mkutils = require "mkutils" + +local file_exists = mkutils.file_exists +-- function file_exists(name) +-- local f=io.open(name,"r") +-- if f~=nil then io.close(f) return true else return false end +-- end + + +local make_name = function(name) + return table.concat(name, "/") + -- return name:gsub("//","/") +end + +-- find the config file in XDG_CONFIG_HOME or in the HOME directry +-- the XDG tree is looked up first, the $HOME is used only when it cannot be +-- find in the former +local xdg_config = function(filename, xdg_config_name) + local dotfilename = "." .. filename + local xdg_config_name = xdg_config_name or "config.lua" + local xdg = os.getenv("XDG_CONFIG_HOME") or ((os.getenv("HOME") or "") .. "/.config") + local home = os.getenv("HOME") + if xdg then + -- filename like ~/.config/make4ht/config.lua + local fn = make_name{ xdg ,filename , xdg_config_name } + if file_exists(fn) then + return fn + end + end + if home then + -- ~/.make4ht + local fn = make_name{ home, dotfilename } + if file_exists(fn) then + return fn + end + end +end + +local find_config = function(filename) + local filename = "." .. filename + local current = lfs.currentdir() + local path = {} + current:gsub("([^/]+)", function(s) table.insert(path,s) end) + local begin = os.type == "windows" and "" or "/" + for i = #path, 1, -1 do + local fn =begin .. table.concat(path,"/") .. "/".. filename + -- print("testing",fn) + if file_exists(fn) then return fn end + table.remove(path) + end + return false +end + +local function load_config(filename, default) + local default = default or {} + default.table = table + default.string = string + default.io = io + default.os = os + default.math = math + default.print = print + default.ipairs = ipairs + default.pairs = pairs + local f = io.open(filename, "r") + local contents = f:read("*all") + f:close() + load(contents,"sandbox config","bt", default)() + return default +end + +--[[ +local function load_config(filename, default) + local default = default or {} + if ~file_exists(filename) then + return nil, "Cannot load config file "..filename + end + local section = "default" + local file = io.open(filename, "r") + if ~file then return nil, "Error opening config file"..filename end + for line in file:lines() do + local ts = line:match("") + end + file:close() +end +--]] + +m.find_config = find_config +m.find_xdg_config = xdg_config +m.load_config = load_config +return m diff --git a/Master/texmf-dist/scripts/make4ht/make4ht-dom.lua b/Master/texmf-dist/scripts/make4ht/make4ht-dom.lua deleted file mode 100755 index fc90bf67e7c..00000000000 --- a/Master/texmf-dist/scripts/make4ht/make4ht-dom.lua +++ /dev/null @@ -1,448 +0,0 @@ ---- DOM module for LuaXML --- @module make4ht-dom -local dom = {} -local xml = require("luaxml-mod-xml") -local handler = require("luaxml-mod-handler") -local query = require("make4ht-parse-query") - - -local void = {area = true, base = true, br = true, col = true, hr = true, img = true, input = true, link = true, meta = true, param = true} - -local actions = { - TEXT = {text = "%s"}, - COMMENT = {start = "<!-- ", text = "%s", stop = " -->"}, - ELEMENT = {start = "<%s%s>", stop = "</%s>", void = "<%s%s />"}, - DECL = {start = "<?%s %s?>"}, - DTD = {start = "<!DOCTYPE ", text = "%s" , stop=">"} -} - ---- It serializes the DOM object back to XML --- @function serialize_dom --- @param parser DOM object --- @param current --- @param level --- @param output --- @return table -function serialize_dom(parser, current,level, output) - local output = output or {} - local function get_action(typ, action) - local ac = actions[typ] or {} - local format = ac[action] or "" - return format - end - local function insert(format, ...) - table.insert(output, string.format(format, ...)) - end - local function prepare_attributes(attr) - local t = {} - local attr = attr or {} - for k, v in pairs(attr) do - t[#t+1] = string.format("%s='%s'", k, v) - end - if #t == 0 then return "" end - -- add space before attributes - return " " .. table.concat(t, " ") - end - local function start(typ, el, attr) - local format = get_action(typ, "start") - insert(format, el, prepare_attributes(attr)) - end - local function text(typ, text) - local format = get_action(typ, "text") - insert(format, text) - end - local function stop(typ, el) - local format = get_action(typ, "stop") - insert(format,el) - end - local level = level or 0 - local spaces = string.rep(" ",level) - local root= current or parser._handler.root - local name = root._name or "unnamed" - local xtype = root._type or "untyped" - local text_content = root._text or "" - local attributes = root._attr or {} - -- if xtype == "TEXT" then - -- print(spaces .."TEXT : " .. root._text) - -- elseif xtype == "COMMENT" then - -- print(spaces .. "Comment : ".. root._text) - -- else - -- print(spaces .. xtype .. " : " .. name) - -- end - -- for k, v in pairs(attributes) do - -- print(spaces .. " ".. k.."="..v) - -- end - if xtype == "DTD" then - text_content = string.format('%s %s "%s" "%s"', name, attributes["_type"], attributes._name, attributes._uri ) - attributes = {} - elseif xtype == "ELEMENT" and void[name] then - local format = get_action(xtype, "void") - insert(format, name, prepare_attributes(attributes)) - return output - end - - start(xtype, name, attributes) - text(xtype,text_content) - local children = root._children or {} - for _, child in ipairs(children) do - output = serialize_dom(parser,child, level + 1, output) - end - stop(xtype, name) - return output -end - -local parse = function(x) - local domHandler = handler.domHandler() - local Parser = xml.xmlParser(domHandler) - -- preserve whitespace - Parser.options.stripWS = nil - Parser:parse(x) - Parser.current = Parser._handler.root - Parser.__index = Parser - - local function save_methods(element) - setmetatable(element,Parser) - local children = element._children or {} - for _, x in ipairs(children) do - save_methods(x) - end - end - local parser = setmetatable({}, Parser) - - --- @class Parser - function Parser.root_node(self) - return self._handler.root - end - - - function Parser.get_element_type(self, el) - local el = el or self - return el._type - end - function Parser.is_element(self, el) - local el = el or self - return self:get_element_type(el) == "ELEMENT" - end - - function Parser.is_text(self, el) - local el = el or self - return self:get_element_type(el) == "TEXT" - end - - local lower = string.lower - - function Parser.get_element_name(self, el) - local el = el or self - return el._name or "unnamed" - end - - function Parser.get_attribute(self, name) - local el = self - if self:is_element(el) then - local attr = el._attr or {} - return attr[name] - end - end - - function Parser.set_attribute(self, name, value) - local el = self - if self:is_element(el) then - el._attr[name] = value - return true - end - end - - - function Parser.serialize(self, current) - local current = current - -- if no current element is added and self is not plain parser object - -- (_type is then nil), use the current object as serialized root - if not current and self._type then - current = self - end - return table.concat(serialize_dom(self, current)) - end - - function Parser.get_path(self,path, current) - local function traverse_path(path_elements, current, t) - local t = t or {} - if #path_elements == 0 then - -- for _, x in ipairs(current._children or {}) do - -- table.insert(t,x) - -- end - table.insert(t,current) - return t - end - local current_path = table.remove(path_elements, 1) - for _, x in ipairs(self:get_children(current)) do - if self:is_element(x) then - local name = string.lower(self:get_element_name(x)) - if name == current_path then - t = traverse_path(path_elements, x, t) - end - end - end - return t - end - local current = current or self:root_node() -- self._handler.root - local path_elements = {} - local path = string.lower(path) - for el in path:gmatch("([^%s]+)") do table.insert(path_elements, el) end - return traverse_path(path_elements, current) - end - - function Parser.calculate_specificity(self, query) - local query = query or {} - local specificity = 0 - for _, item in ipairs(query.query or {}) do - for key, value in pairs(item) do - if key == "id" then - specificity = specificity + 100 - elseif key == "tag" then - specificity = specificity + 1 - else - specificity = specificity + 10 - end - end - end - return specificity - end - - function Parser.match_querylist(self, querylist) - local matches = {} - local querylist = querylist - - local function test_part(key, value, el) - -- print("testing", key, value, el:get_element_name()) - if key == "tag" then - return el:get_element_name() == value - elseif key == "id" then - local id = el:get_attribute "id" - return id and id == value - elseif key == "class" then - local class = el:get_attribute "class" - if not class then return false end - local c = {} - for part in class:gmatch "([^%s]+)" do - c[part] = true - end - return c[value] == true - end - -- TODO: Add more cases - -- just return true for not supported selectors - return true - end - - local function test_object(query, el) - -- test one object in CSS selector - local matched = {} - for key, value in pairs(query) do - matched[#matched+1] = test_part(key, value, el) - end - if #matched == 0 then return false end - for k, v in ipairs(matched) do - if v ~= true then return false end - end - return true - end - - local function match_query(query, el) - local query = query or {} - local object = table.remove(query) -- get current object from the query stack - if not object then return true end -- if the query stack is empty, then we can be sure that it matched previous items - if not el:is_element() then return false end -- if there is object to test, but current node isn't element, test failed - local result = test_object(object, el) - if result then - return match_query(query, el:get_parent()) - end - return false - end - for _,element in ipairs(querylist) do - local query = {} - for k,v in ipairs(element.query) do query[k] = v end - if #query > 0 then -- don't try to match empty query - local result = match_query(query, self) - if result then matches[#matches+1] = element end - end - end - return matches - end - - function Parser.get_selector_path(self, selectorlist) - local nodelist = {} - self:traverse_elements(function(el) - local matches = el:match_querylist(selectorlist) - print("Matching", el:get_element_name(), #matches) - if #matches > 0 then nodelist[#nodelist+1] = el - end - end) - return nodelist - end - - --- Parse CSS selector to match table - function Parser.prepare_selector(self, selector) - local querylist = {} - local function parse_selector(item) - local query = {} - -- for i = #item, 1, -1 do - -- local part = item[i] - for _, part in ipairs(item) do - local t = {} - for _, atom in ipairs(part) do - local key = atom[1] - local value = atom[2] - t[key] = value - end - query[#query + 1] = t - end - return query - end - -- for item in selector:gmatch("([^%s]+)") do - -- elements[#elements+1] = parse_selector(item) - -- end - local parts = query.parse_query(selector) or {} - -- several selectors may be separated using ",", we must process them separately - for _, part in ipairs(parts) do - querylist[#querylist+1] = {query = parse_selector(part)} - end - return querylist - end - - function Parser.get_children(self, el) - local el = el or self - local children = el._children or {} - return children - end - - function Parser.get_parent(self, el) - local el = el or self - return el._parent - end - - function Parser.traverse_elements(self, fn, current) - local current = current or self:root_node() - local status = true - if self:is_element(current) or self:get_element_type(current) == "ROOT"then - local status = fn(current) - -- don't traverse child nodes when the user function return false - if status ~= false then - for _, child in ipairs(self:get_children(current)) do - self:traverse_elements(fn, child) - end - end - end - end - - function Parser.traverse_node_list(self, nodelist, fn) - local nodelist = nodelist or {} - for _, node in ipairs(nodelist) do - for _, element in ipairs(node._children) do - fn(element) - end - end - end - - function Parser.replace_node(self, new) - local old = self - local parent = self:get_parent(old) - local id,msg = self:find_element_pos( old) - if id then - parent._children[id] = new - return true - end - return false, msg - end - - function Parser.add_child_node(self, child) - local parent = self - child._parent = parent - table.insert(parent._children, child) - end - - - function Parser.copy_node(self, element) - local element = element or self - local t = {} - for k, v in pairs(element) do - if type(v) == "table" and k~="_parent" then - t[k] = self:copy_node(v) - else - t[k] = v - end - end - save_methods(t) - return t - end - - function Parser.create_element(self, name, attributes, parent) - local parent = parent or self - local new = {} - new._type = "ELEMENT" - new._name = name - new._attr = attributes or {} - new._children = {} - new._parent = parent - save_methods(new) - return new - end - - function Parser.remove_node(self, element) - local element = element or self - local parent = self:get_parent(element) - local pos = self:find_element_pos(element) - -- if pos then table.remove(parent._children, pos) end - if pos then - -- table.remove(parent._children, pos) - parent._children[pos] = setmetatable({_type = "removed"}, Parser) - end - end - - function Parser.find_element_pos(self, el) - local el = el or self - local parent = self:get_parent(el) - if not self:is_element(parent) and self:get_element_type(parent) ~= "ROOT" then return nil, "The parent isn't element" end - for i, x in ipairs(parent._children) do - if x == el then return i end - end - return false, "Cannot find element" - end - - function Parser.get_siblibgs(self, el) - local el = el or self - local parent = el:get_parent() - if parent:is_element() then - return parent:get_children() - end - end - - function Parser.get_sibling_node(self, change) - local el = self - local pos = el:find_element_pos() - local siblings = el:get_siblibgs() - if pos and siblings then - return siblings[pos + change] - end - end - - function Parser.get_next_node(self, el) - local el = el or self - return el:get_sibling_node(1) - end - - function Parser.get_prev_node(self, el) - local el = el or self - return el:get_sibling_node(-1) - end - - - -- include the methods to all xml nodes - save_methods(parser._handler.root) - -- parser: - return parser -end - - -local M = {} -M.parse = parse -M.serialize= serialize_dom -return M diff --git a/Master/texmf-dist/scripts/make4ht/make4ht-filterlib.lua b/Master/texmf-dist/scripts/make4ht/make4ht-filterlib.lua new file mode 100755 index 00000000000..33cb3530a85 --- /dev/null +++ b/Master/texmf-dist/scripts/make4ht/make4ht-filterlib.lua @@ -0,0 +1,42 @@ +local M = {} + +-- the filter module must implement the load_filter function +function M.load_filters(filters, load_filter) + local sequence = {} + if type(filters) == "string" then + table.insert(sequence,load_filter(filters)) + elseif type(filters) == "table" then + for _,n in pairs(filters) do + if type(n) == "string" then + table.insert(sequence,load_filter(n)) + elseif type(n) == "function" then + table.insert(sequence, n) + end + end + elseif type(filters) == "function" then + table.insert(sequence, filters) + else + return false, "Argument to filter must be either\ntable with filter names, or single filter name" + end + return sequence +end + +function M.load_input_file(filename) + if not filename then return false, "filters: no filename" end + local input = nil + + if filename then + local file = io.open(filename,"r") + input = file:read("*all") + file:close() + end + return input +end + +function M.save_input_file(filename, input) + local file = io.open(filename,"w") + file:write(input) + file:close() +end + +return M diff --git a/Master/texmf-dist/scripts/make4ht/mkparams.lua b/Master/texmf-dist/scripts/make4ht/mkparams.lua index 61c522a2406..f5df4849966 100755 --- a/Master/texmf-dist/scripts/make4ht/mkparams.lua +++ b/Master/texmf-dist/scripts/make4ht/mkparams.lua @@ -17,6 +17,7 @@ ${progname} [options] filename ["tex4ht.sty op." "tex4ht op." "t4ht op" "latex o -c,--config (default xhtml) Custom config file -d,--output-dir (default nil) Output directory -e,--build-file (default nil) If build file is different than `filename`.mk4 +-f,--format (default html5) Output file format -h,-- help Display this message -l,--lua Use lualatex for document compilation -m,--mode (default default) Switch which can be used in the makefile @@ -26,6 +27,15 @@ ${progname} [options] filename ["tex4ht.sty op." "tex4ht op." "t4ht op" "latex o -x,--xetex Use xelatex for document compilation -v,--version Display version number ]] + +-- test if the current command line argument should be passed to tex4ht, t4ht or latex +local function is_escapedargument(arg) + -- we need to ignore make4ht options which can be used without filename, ie --version and --help + local ignored_options = {["-h"]=true, ["--help"]=true, ["-v"] = true, ["--version"]=true} + if ignored_options[arg] then return false end + -- in other cases, match if the argument starts with "-" character + return arg:match("^%-") +end local function get_args(parameters, optiontext) local parameters = parameters or {} parameters.progname = parameters.progname or "make4ht" @@ -33,10 +43,37 @@ local function get_args(parameters, optiontext) local optiontext = optiontext or m.optiontext parameters.postfile = parameters.postfile or "" optiontext = optiontext .. parameters.postparams .."<filename> (string) Input file name\n" .. parameters.postfile + -- we can pass arguments for tex4ht and t4ht after filename, but it will confuse lapp, thinking that these + -- options are for make4ht. this may result in execution error or wrong option parsing + -- as fix, add a space before options at the end (we need to stop to add spaces as soon as we find + -- nonempty string which doesn't start with - it will be filename or tex4ht.sty options + if #arg > 1 then -- do this only if more than one argument is used + for i=#arg,1,-1 do + local current = arg[i] + if is_escapedargument(arg[i]) then + arg[i] = " ".. arg[i] + -- empty parameter + elseif current == "" then + else + break + end + end + end --print("--------------\n" .. optiontext .."--------------\n") return lapp(optiontext % parameters) end +--- get outptut file format and list of extensions from --format option string +local function get_format_extensions(format_string) + local format, rest = format_string:match("^([a-zA-Z0-9]+)(.*)") + local extensions = {} + -- it is possible to pass only the extensions + rest = rest or format_string + rest:gsub("([%+%-])([^%+^%-]+)",function(typ, name) + table.insert(extensions, {type = typ, name = name}) + end) + return format, extensions +end local function process_args(args) local function get_inserter(args,tb) @@ -80,7 +117,7 @@ local function process_args(args) local latex_cli_params = args[4] or "" if not latex_cli_params:match("%-jobname") then -- we must strip out directories from jobname when full path to document is given - input = input:match("([^%/]+)$") + input = input:match("([^%/^%\\]+)$") table.insert(latex_params,"-jobname="..input) else -- when user specifies -jobname, we must change name of the input file, @@ -140,9 +177,14 @@ local function process_args(args) local build_file = input.. ".mk4" if args["build-file"] and args["build-file"] ~= "nil" then - build_file = args["build-file"] + build_file = args["build-file"] end + local outformat, extensions + if args["format"] and arg["format"] ~= "nil" then + outformat, extensions = get_format_extensions(args["format"]) + end + local parameters = { htlatex = compiler ,input=input @@ -156,6 +198,8 @@ local function process_args(args) ,mode = mode ,dvi = dvi ,build_file = build_file + ,output_format = outformat + ,extensions = extensions --,t4ht_dir_format=t4ht_dir_format } if outdir then parameters.outdir = outdir end @@ -165,8 +209,15 @@ local function process_args(args) print("tex4ht.sty :",t4sty) print("tex4ht",tex4ht) print("build_file", build_file) + if outformat~="nil" then + print("Output format", outformat) + for _, ex in ipairs(extensions) do + print("Extension", ex.type .. ex.name) + end + end return parameters end m.get_args = get_args +m.get_format_extensions = get_format_extensions m.process_args = process_args return m diff --git a/Master/texmf-dist/scripts/make4ht/mkutils.lua b/Master/texmf-dist/scripts/make4ht/mkutils.lua index 56c413ffb43..88b456fe9da 100755 --- a/Master/texmf-dist/scripts/make4ht/mkutils.lua +++ b/Master/texmf-dist/scripts/make4ht/mkutils.lua @@ -1,6 +1,7 @@ module(...,package.seeall) local make4ht = require("make4ht-lib") +local mkparams = require("mkparams") --template engine function interp(s, tab) local tab = tab or {} @@ -223,6 +224,8 @@ env.split = split env.string = string env.table = table env.copy = copy +env.tonumber = tonumber +env.tostring = tostring env.mkdirectories = mkdirectories env.require = require env.texio = texio @@ -230,7 +233,49 @@ env.type = type env.lfs = lfs env.os = os env.io = io +env.math = math env.unicode = unicode + + +-- it is necessary to use the settings table +-- set in the Make environment by mkutils +function env.set_settings(par) + local settings = env.settings + for k,v in pairs(par) do + settings[k] = v + end +end + +-- Add a value to the current settings +function env.settings_add(par) + local settings = env.settings + for k,v in pairs(par) do + local oldval = settings[k] or "" + settings[k] = oldval .. v + end +end + +function env.get_filter_settings(name) + local settings = env.settings + -- local settings = self.params + local filters = settings.filter or {} + local filter_options = filters[name] or {} + return filter_options +end + +function env.filter_settings(name) + -- local settings = Make.params + local settings = env.settings + local filters = settings.filter or {} + local filter_options = filters[name] or {} + return function(par) + for k,v in pairs(par) do + filter_options[k] = v + end + filters[name] = filter_options + settings.filter = filters + end +end env.Font = function(s) local font_name = s["name"] if not font_name then return nil, "Cannot find font name" end @@ -239,11 +284,35 @@ end env.Make = make4ht.Make env.Make.params = env.settings -env.Make:add("test","no takže ${tex4ht_sty_par} ${htlatex} ${input} ${config}") +env.Make:add("test","test the variables: ${tex4ht_sty_par} ${htlatex} ${input} ${config}") + +-- this function reads the LaTeX log file and tries to detect fatal errors in the compilation +local function testlogfile(par) + local logfile = par.input .. ".log" + local f = io.open(logfile,"r") + if not f then + print("Make4ht: cannot open log file "..logfile) + return 1 + end + local len = f:seek("end") + -- test only the end of the log file, no need to run search functions on everything + local newlen = len - 1256 + -- but the value to seek must be greater than 0 + newlen = (newlen > 0) and newlen or 0 + f:seek("set", newlen) + local text = f:read("*a") + f:close() + if text:match("No pages of output") or text:match("TeX capacity exceeded, sorry") then return 1 end + return 0 +end + + +-- Make this function available in the build files +Make.testlogfile = testlogfile --env.Make:add("htlatex", "${htlatex} ${latex_par} '\\\makeatletter\\def\\HCode{\\futurelet\\HCode\\HChar}\\def\\HChar{\\ifx\"\\HCode\\def\\HCode\"##1\"{\\Link##1}\\expandafter\\HCode\\else\\expandafter\\Link\\fi}\\def\\Link#1.a.b.c.{\\g@addto@macro\\@documentclasshook{\\RequirePackage[#1,html]{tex4ht}\\let\\HCode\\documentstyle\\def\\documentstyle{\\let\\documentstyle\\HCode\\expandafter\\def\\csname tex4ht\\endcsname{#1,html}\\def\\HCode####1{\\documentstyle[tex4ht,}\\@ifnextchar[{\\HCode}{\\documentstyle[tex4ht]}}}\\makeatother\\HCode '${config}${tex4ht_sty_par}'.a.b.c.\\input ' ${input}") -env.Make:add("htlatex",function(par) - local command = -"${htlatex} ${latex_par} '\\makeatletter".. + +-- template for calling LaTeX with tex4ht loaded +Make.latex_command = "${htlatex} ${latex_par} '\\makeatletter".. "\\def\\HCode{\\futurelet\\HCode\\HChar}\\def\\HChar{\\ifx\"\\HCode".. "\\def\\HCode\"##1\"{\\Link##1}\\expandafter\\HCode\\else".. "\\expandafter\\Link\\fi}\\def\\Link#1.a.b.c.{\\g@addto@macro".. @@ -252,46 +321,170 @@ env.Make:add("htlatex",function(par) "\\HCode\\expandafter\\def\\csname tex4ht\\endcsname{#1,html}\\def".. "\\HCode####1{\\documentstyle[tex4ht,}\\@ifnextchar[{\\HCode}{".. "\\documentstyle[tex4ht]}}}\\makeatother\\HCode ${tex4ht_sty_par}.a.b.c.".. -"\\input ${tex_file}'" +"\\input ${tex_file}'" + +env.Make:add("htlatex",function(par) + local command = Make.latex_command if os.type == "windows" then command = command:gsub("'",'') end command = command % par print("LaTeX call: "..command) os.execute(command) - local logfile = par.input .. ".log" - local f = io.open(logfile,"r") - if not f then - print("Make4ht: cannot open log file "..logfile) - return 1 - end - local len = f:seek("end") - f:seek("set", len - 1256) - local text = f:read("*a") - f:close() - if text:match("No pages of output") or text:match("TeX capacity exceeded, sorry") then return 1 end - return 0 + return testlogfile(par) end ,{correct_exit=0}) + + +env.Make:add("latexmk", function(par) + local command = Make.latex_command + par.expanded = command % par + -- quotes in latex_command must be escaped, they cause Latexmk error + par.expanded = par.expanded:gsub('"', '\\"') + local newcommand = 'latexmk -latex="${expanded}" ${tex_file}' % par + os.execute(newcommand) + return Make.testlogfile(par) +end, {correct_exit= 0}) + + + env.Make:add("tex4ht","tex4ht ${tex4ht_par} \"${input}.${dvi}\"", nil, 1) env.Make:add("t4ht","t4ht ${t4ht_par} \"${input}.${ext}\"",{ext="dvi"},1) +-- enable extension in the config file +-- the following two functions must be here and not in make4ht-lib.lua +-- because of the access to env.settings +env.Make.enable_extension = function(self,name) + table.insert(env.settings.extensions, {type="+", name=name}) +end + +-- disable extension in the config file +env.Make.disable_extension = function(self,name) + table.insert(env.settings.extensions, {type="-", name=name}) +end + function load_config(settings, config_name) - local settings = settings or main_settings - env.settings = settings - env.mode = settings.mode - local config_name = kpse.find_file(config_name, 'texmfscripts') or config_name - local f = io.open(config_name,"r") - if not f then + local settings = settings or main_settings + env.settings = settings + env.mode = settings.mode + if config_name and not file_exists(config_name) then + config_name = kpse.find_file(config_name, 'texmfscripts') or config_name + end + local f = io.open(config_name,"r") + if not f then print("Cannot open config file", config_name) return env end print("Using build file", config_name) - local code = f:read("*all") - local fn, msg = run(code,env) - if not fn then print(msg) end - assert(fn) - return env + local code = f:read("*all") + local fn, msg = run(code,env) + if not fn then print(msg) end + assert(fn) + return env +end + +local function find_lua_file(name) + local extension_path = name:gsub("%.", "/") .. ".lua" + return kpse.find_file(extension_path, "lua") +end + +--- load the output format plugins +function load_output_format(format_name) + local format_library = "make4ht.formats."..format_name + local is_format_file = find_lua_file(format_library) + if is_format_file then + local format = assert(require(format_library)) + if format then + format.prepare_extensions = format.prepare_extensions or function(extensions) return extensions end + end + return format + end +end + +--- Execute the prepare_parameters function in list of extensions +function extensions_prepare_parameters(extensions, parameters) + for _, ext in ipairs(extensions) do + -- execute the extension only if it contains prepare_parameters function + local fn = ext.prepare_parameters + if fn then + parameters = fn(parameters) + end + end + return parameters +end + +--- Modify the build sequence using extensions +-- @param extensions list of extensions +-- @make Make object +function extensions_modify_build(extensions, make) + for _, ext in ipairs(extensions) do + local fn = ext.modify_build + if fn then + make = fn(make) + end + end + return make +end + + +--- load one extension +-- @param name extension name +-- @param format current output format +function load_extension(name,format) + -- first test if the extension exists + local extension_library = "make4ht.extensions." .. name + local is_extension_file = find_lua_file(extension_library) + -- don't try to load the extension if it doesn't exist + if not is_extension_file then return nil end + local extension = require("make4ht.extensions.".. name) + -- extensions can test if the current output format is supported + local test = extension.test + if test then + if test(format) then + return extension + end + -- if the test fail return nil + return nil + end + -- if the extension doesn't provide the test function, we will assume that + -- it supports every output format + return extension end +--- load extensions +-- @param extensions table created by mkparams.get_format_extensions function +-- @param format output type format. extensions may support only certain file +-- formats +function load_extensions(extensions, format) + local module_names = {} + local extension_table = {} + -- process the extension table. it contains type field, which can enable or + -- diable the extension + for _, v in ipairs(extensions) do + local enable = v.type == "+" and true or nil + module_names[v.name] = enable + end + for name, _ in pairs(module_names) do + local extension = load_extension(name,format) + if extension then + table.insert(extension_table, extension) + else + print("Cannot load extension: ".. name) + end + end + return extension_table +end + +--- add new extensions to a list of loaded extensions +-- @param added string with extensions to be added in the form +ext1+ext2 +function add_extensions(added, extensions) + local _, newextensions = mkparams.get_format_extensions("dummyfmt" .. added) + -- insert new extension at the beginning, in order to support disabling using + -- the -f option + for _, x in ipairs(extensions or {}) do table.insert(newextensions, x) end + return newextensions +end +-- I don't know if this is clean, but settings functions won't be available +-- for filters and extensions otherwise +for k,v in pairs(env) do _G[k] = v end |