summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/scripts/make4ht
diff options
context:
space:
mode:
authorKarl Berry <karl@freefriends.org>2018-04-09 21:09:51 +0000
committerKarl Berry <karl@freefriends.org>2018-04-09 21:09:51 +0000
commit4d2d4764dde3caf40817a1ef84719e1319b10d2f (patch)
tree096967b6679e1e110b75e42e1747936323508fc4 /Master/texmf-dist/scripts/make4ht
parent1e0147a88c7bc3a3e2986968f697ef5fbcef2bcb (diff)
make4ht (9apr18)
git-svn-id: svn://tug.org/texlive/trunk@47398 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/scripts/make4ht')
-rw-r--r--Master/texmf-dist/scripts/make4ht/domfilters/make4ht-aeneas.lua144
-rw-r--r--Master/texmf-dist/scripts/make4ht/domfilters/make4ht-fixinlines.lua57
-rw-r--r--Master/texmf-dist/scripts/make4ht/domfilters/make4ht-idcolons.lua20
-rw-r--r--Master/texmf-dist/scripts/make4ht/domfilters/make4ht-joincharacters.lua87
-rw-r--r--Master/texmf-dist/scripts/make4ht/filters/make4ht-domfilter.lua63
-rw-r--r--Master/texmf-dist/scripts/make4ht/filters/make4ht-filter.lua36
-rw-r--r--Master/texmf-dist/scripts/make4ht/filters/make4ht-mathjaxnode.lua94
-rw-r--r--Master/texmf-dist/scripts/make4ht/filters/make4ht-svg-height.lua43
-rwxr-xr-xMaster/texmf-dist/scripts/make4ht/make4ht37
-rwxr-xr-xMaster/texmf-dist/scripts/make4ht/make4ht-aeneas-config.lua215
-rwxr-xr-xMaster/texmf-dist/scripts/make4ht/make4ht-config.lua92
-rwxr-xr-xMaster/texmf-dist/scripts/make4ht/make4ht-dom.lua448
-rwxr-xr-xMaster/texmf-dist/scripts/make4ht/make4ht-filterlib.lua42
-rwxr-xr-xMaster/texmf-dist/scripts/make4ht/mkparams.lua55
-rwxr-xr-xMaster/texmf-dist/scripts/make4ht/mkutils.lua249
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