diff options
author | Norbert Preining <norbert@preining.info> | 2023-02-25 03:01:09 +0000 |
---|---|---|
committer | Norbert Preining <norbert@preining.info> | 2023-02-25 03:01:09 +0000 |
commit | 81a9d839224eef4c2bd9bf68410b4049c61cdb14 (patch) | |
tree | 813b5a34a2548b16dbbcd271697c7bcf29342bde /support/make4ht/domfilters | |
parent | c283f3a6970d2bc9de3501415f5701277da68693 (diff) |
CTAN sync 202302250301
Diffstat (limited to 'support/make4ht/domfilters')
-rw-r--r-- | support/make4ht/domfilters/make4ht-inlinecss.lua | 83 | ||||
-rw-r--r-- | support/make4ht/domfilters/make4ht-joincharacters.lua | 2 | ||||
-rw-r--r-- | support/make4ht/domfilters/make4ht-mathmlfixes.lua | 221 |
3 files changed, 275 insertions, 31 deletions
diff --git a/support/make4ht/domfilters/make4ht-inlinecss.lua b/support/make4ht/domfilters/make4ht-inlinecss.lua new file mode 100644 index 0000000000..b086a2fc42 --- /dev/null +++ b/support/make4ht/domfilters/make4ht-inlinecss.lua @@ -0,0 +1,83 @@ +local cssquery = require "luaxml-cssquery" + +local log = logging.new("inlinecss") + +local cssrules = {} +local cssobj = cssquery() + +local function parse_rule(line) + -- parse CSS selector and attributes + -- they are always on one line in the CSS file produced by TeX4ht + local selector, values = line:match("%s*(.-)%s*(%b{})") + if values then + values = values:sub(2,-2) + end + return selector, values +end + +local function join_values(old, new) + -- correctly joins two attribute lists, depending on the ending + local separator = ";" + if not old then return new end + -- if old already ends with ;, then don't use semicolon as a separator + if old:match(";%s*$") then separator = "" end + return old .. separator .. new +end + +local function parse_css(filename) + local css_file = io.open(filename, "r") + if not css_file then return nil, "cannot load css file: " .. (filename or "") end + local newlines = {} + for line in css_file:lines() do + -- match lines that contain # or =, as these can be id or attribute selectors + if line:match("[%#%=].-{") then + -- update attributes for the current selector + local selector, value = parse_rule(line) + local oldvalue = cssrules[selector] + cssrules[selector] = join_values(oldvalue, value) + else + newlines[#newlines+1] = line + end + end + -- we need to add css rules + for selector, value in pairs(cssrules) do + cssobj:add_selector(selector, function(dom) end, {value=value}) + end + css_file:close() + -- write new version of the CSS file, without rules for ids and attributes + local css_file = io.open(filename, "w") + css_file:write(table.concat(newlines, "\n")) + css_file:close() + return true +end + +local processed = false + +-- process the HTML file and insert inline CSS for id and attribute selectors +return function(dom, par) + if not processed then + -- process the CSS file before everything else, but only once + processed = true + local css_file = par.input .. ".css" + local status, msg = parse_css(css_file) + if not status then log:warning(msg) end + end + -- loop over all elements in the current page + dom:traverse_elements(function(curr) + -- use CSS object to match if the current element + -- is matched by id attribute selector + local matched = cssobj:match_querylist(curr) + if #matched > 0 then + -- join possible already existing style attribute with values from the CSS file + local values = curr:get_attribute("style") + -- join values of all matched rules + for _,rule in ipairs(matched) do + values = join_values(values, rule.params.value) + end + curr:set_attribute("style", values) + end + + end) + return dom +end + diff --git a/support/make4ht/domfilters/make4ht-joincharacters.lua b/support/make4ht/domfilters/make4ht-joincharacters.lua index 34737f921a..7f70cca3db 100644 --- a/support/make4ht/domfilters/make4ht-joincharacters.lua +++ b/support/make4ht/domfilters/make4ht-joincharacters.lua @@ -107,7 +107,7 @@ local function join_characters(obj,par) -- we must create a new node el:add_child_node(el:create_text_node(s)) next_el:remove_node() - real_next = nil + -- real_next = nil else real_next = nil end diff --git a/support/make4ht/domfilters/make4ht-mathmlfixes.lua b/support/make4ht/domfilters/make4ht-mathmlfixes.lua index 12754bedb9..7fa74f4632 100644 --- a/support/make4ht/domfilters/make4ht-mathmlfixes.lua +++ b/support/make4ht/domfilters/make4ht-mathmlfixes.lua @@ -5,17 +5,53 @@ local token = {"mi", "mn", "mo", "mtext", "mspace", "ms"} local token_elements = {} for _, tok in ipairs(token) do token_elements[tok] = true end +-- helper functions to support MathML elements with prefixes (<mml:mi> etc). +-- +local function get_element_name(el) + -- return element name and xmlns prefix + local name = el:get_element_name() + if name:match(":") then + local prefix, real_name = name:match("([^%:]+):?(.+)") + return real_name, prefix + else + return name + end +end + +local function get_attribute(el, attr_name) + -- attributes can have the prefix, but sometimes they don't have it + -- so we need to catch both cases + local _, prefix = get_element_name(el) + prefix = prefix or "" + return el:get_attribute(attr_name) or el:get_attribute(prefix .. ":" .. attr_name) +end + +local function get_new_element_name(name, prefix) + return prefix and prefix .. ":" .. name or name +end + +local function update_element_name(el, name, prefix) + local newname = get_new_element_name(name, prefix) + el._name = newname +end + +local function create_element(el, name, prefix) + return el:create_element(newname) +end + local function is_token_element(el) - return token_elements[el:get_element_name()] + local name, prefix = get_element_name(el) + return token_elements[name], prefix end local function fix_token_elements(el) -- find token elements that are children of other token elements if is_token_element(el) then local parent = el:get_parent() - if is_token_element(parent) then + local is_parent_token, prefix = is_token_element(parent) + if is_parent_token then -- change top element in nested token elements to mstyle - parent._name = "mstyle" + update_element_name(parent, "mstyle", prefix) end end end @@ -23,7 +59,8 @@ end local function fix_nested_mstyle(el) -- the <mstyle> element can be child of token elements -- we must exterminate it - if el:get_element_name() == "mstyle" then + local el_name = get_element_name(el) + if el_name == "mstyle" then local parent = el:get_parent() if is_token_element(parent) then -- if parent doesn't have the mathvariant attribute copy it from <mstyle> @@ -44,13 +81,13 @@ local function fix_mathvariant(el) -- find if element has <mstyle> parent, and its value of mathvariant if not x:is_element() then return nil - elseif x:get_element_name() == "mstyle" then + elseif get_element_name(x) == "mstyle" then return x:get_attribute("mathvariant") else return find_mstyle(x:get_parent()) end end - if el:get_element_name() == "mi" then + if get_element_name(el) == "mi" then -- process only <mi> that have mathvariant set local oldmathvariant = el:get_attribute("mathvariant") if oldmathvariant then @@ -62,6 +99,74 @@ local function fix_mathvariant(el) end end +local function contains_only_text(el) + -- detect if element contains only text + local elements = 0 + local text = 0 + local children = el:get_children() or {} + for _ , child in ipairs(children) do + if child:is_text() then text = text + 1 + elseif child:is_element() then elements = elements + 1 + end + end + return text > 0 and elements == 0 +end + +-- check if <mstyle> element contains direct text. in that case, add +-- <mtext> +local function fix_missing_mtext(el) + if el:get_element_name() == "mstyle" and contains_only_text(el) then + -- add child <mtext> + log:debug("mstyle contains only text: " .. el:get_text()) + -- copy the current mode, change it's element name to mtext and add it as a child of <mstyle> + local copy = el:copy_node() + copy._name = "mtext" + copy._parent = el + el._children = {copy} + end +end + +local function is_radical(el) + local radicals = {msup=true, msub=true, msubsup=true} + return radicals[el:get_element_name()] +end + +local function get_mrow_child(el) + local get_first = function(x) + local children = x:get_children() + return children[1] + end + local first = get_first(el) + -- either return first child, and if the child is <mrow>, return it's first child + if first and first:is_element() then + if first:get_element_name() == "mrow" then + return get_first(first), first + else + return first + end + end +end + +local function fix_radicals(el) + if is_radical(el) then + local first_child, mrow = get_mrow_child(el) + -- if the first child is only one character long, it is possible that there is a problem + if first_child and string.len(first_child:get_text()) == 1 then + local name = first_child:get_element_name() + local siblings = el:get_siblings() + local pos = el:find_element_pos() + -- it doesn't make sense to do any further processing if the element is at the beginning + if pos == 1 then return end + if name == "mo" then + for i = pos, 1,-1 do + end + + end + end + + end +end + -- put <mrow> as child of <math> if it already isn't here local allowed_top_mrow = { math=true @@ -73,24 +178,25 @@ local function top_mrow(math) -- don't process elements that already are mrow local parent = math:get_parent() local parent_name - if parent then parent_name = parent:get_element_name() end - local current_name = math:get_element_name() + if parent then parent_name = get_element_name(parent) end + local current_name, prefix = get_element_name(math) if #children < 2 or not allowed_top_mrow[current_name] or current_name == "mrow" or parent_name == "mrow" then return nil end local mrow_count = 0 for _,v in ipairs(children) do if v:is_element() and is_token_element(v) then put_mrow = true -- break - elseif v:is_element() and v:get_element_name() == "mrow" then + elseif v:is_element() and get_element_name(v) == "mrow" then mrow_count = mrow_count + 1 end end - if not put_mrow and math:get_element_name() == "math" and mrow_count == 0 then + if not put_mrow and get_element_name(math) == "math" and mrow_count == 0 then -- put at least one <mrow> to each <math> put_mrow = true end if put_mrow then - local mrow = math:create_element("mrow") + local newname = get_new_element_name("mrow", prefix) + local mrow = math:create_element(newname) for _, el in ipairs(children) do mrow:add_child_node(el) end @@ -106,7 +212,9 @@ local function get_fence(el, attr, form) local char = el:get_attribute(attr) local mo if char then - mo = el:create_element("mo", {fence="true", form = form}) + local name, prefix = get_element_name(el) + local newname = get_new_element_name("mo", prefix) + mo = el:create_element(newname, {fence="true", form = form}) mo:add_child_node(mo:create_text_node(char)) end return mo @@ -116,13 +224,15 @@ end local function fix_mfenced(el) -- TeX4ht uses in some cases <mfenced> element which is deprecated in MathML. -- Firefox doesn't support it already. - if el:get_element_name() == "mfenced" then + local name, prefix = get_element_name(el) + if name == "mfenced" then -- we must replace it by <mrow><mo>start</mo><mfenced children...><mo>end</mo></mrow> local open = get_fence(el, "open", "prefix") local close = get_fence(el, "close", "postfix") -- there can be also separator attribute, but it is not used in TeX4ht -- change <mfenced> to <mrow> and remove all attributes - el._name = "mrow" + local newname = get_new_element_name("mrow", prefix) + el._name = newname el._attr = {} -- open must be first child, close needs to be last if open then el:add_child_node(open, 1) end @@ -131,7 +241,7 @@ local function fix_mfenced(el) end local function is_fence(el) - return el:get_element_name() == "mo" and el:get_attribute("fence") == "true" + return get_element_name(el) == "mo" and el:get_attribute("fence") == "true" end local function fix_mo_to_mfenced(el) @@ -155,25 +265,27 @@ local function fix_mo_to_mfenced(el) end end -- convert parent <mrow> to <mfenced> - parent._name = "mfenced" + local _, prefix = get_element_name(parent) + local newname = get_new_element_name("mfenced", prefix) + parent._name = newname parent._attr = {open = open, close = close} end end local function fix_numbers(el) -- convert <mn>1</mn><mo>.</mo><mn>3</mn> to <mn>1.3</mn> - if el:get_element_name() == "mn" then + if get_element_name(el) == "mn" then local n = el:get_sibling_node(1) -- test if next element is <mo class="MathClass-punc">.</mo> if n and n:is_element() - and n:get_element_name() == "mo" - and n:get_attribute("class") == "MathClass-punc" + and get_element_name(n) == "mo" + and get_attribute(n, "class") == "MathClass-punc" and n:get_text() == "." then -- get next element and test if it is <mn> local x = el:get_sibling_node(2) if x and x:is_element() - and x:get_element_name() == "mn" + and get_element_name(x) == "mn" then -- join numbers and set it as text content of the current element local newnumber = el:get_text() .. "." .. x:get_text() @@ -194,7 +306,7 @@ local function just_operators(list) -- count <mo> and return true if list contains just them local mo = 0 for _, x in ipairs(list) do - if x:get_element_name() == "mo" then mo = mo + 1 end + if get_element_name(x) == "mo" then mo = mo + 1 end end return mo end @@ -205,18 +317,22 @@ local function fix_operators(x) -- this fixes issues in LibreOffice with a^{*} -- I hope it doesn't introduce different issues -- process only <mo> - if x:get_element_name() ~= "mo" then return nil end + local el_name, prefix = get_element_name(x) + if el_name ~= "mo" then return nil end local siblings = x:get_siblings() -- test if current element list contains only <mo> if just_operators(siblings) == #siblings then if #siblings == 1 then - -- one <mo> translates to <mtext> - x._name = "mtext" - log:debug("changing one <mo> to <mtext>: " .. x:get_text()) - -- I think we should use <mi>, but LO incorrectly renders it in <msubsup>, - -- even if we use the mathvariant="normal" attribute. <mtext> works, so - -- we use that instead. - -- x:set_attribute("mathvariant", "normal") + if not x:get_attribute("stretchy") then + -- one <mo> translates to <mtext> + local newname = get_new_element_name("mtext", prefix) + x._name = newname + log:debug("changing one <mo> to <mtext>: " .. x:get_text()) + -- I think we should use <mi>, but LO incorrectly renders it in <msubsup>, + -- even if we use the mathvariant="normal" attribute. <mtext> works, so + -- we use that instead. + -- x:set_attribute("mathvariant", "normal") + end else -- multiple <mo> translate to <mtext> local text = {} @@ -231,7 +347,8 @@ local function fix_operators(x) log:debug("changing <mo> to <mtext>: " .. newtext) x:add_child_node(text_el) -- change <mo> to <mtext> - x._name = "mtext" + local newname = get_new_element_name("mtext", prefix) + x._name = newname -- remove subsequent <mo> for i = 2, #siblings do siblings[i]:remove_node() @@ -240,6 +357,47 @@ local function fix_operators(x) end end +local function is_last_element(el) + local siblings = el:get_siblings() + -- return true only if the current element is the last in the parent's children + for i = #siblings, 1, -1 do + local curr = siblings[i] + if curr == el then + return true + elseif curr:is_element() then + return false + end + end + return false +end + +local function is_empty_row(el) + -- empty row should contain only one <mtd> + local count = 0 + if el:get_text():match("^%s*$") then + for _, child in ipairs(el:get_children()) do + if child:is_element() then count = count + 1 end + end + end + -- if there is one or zero childrens, then it is empty row + return count < 2 +end + + +local function delete_last_empty_mtr(el) + -- arrays sometimes contain last empty row, which causes rendering issues, + -- so we should remove them + local el_name, prefix = get_element_name(el) + if el_name == "mtr" + and get_attribute(el, "class") == "array-row" + and is_last_element(el) + and is_empty_row(el) + then + el:remove_node() + end + +end + return function(dom) dom:traverse_elements(function(el) if settings.output_format ~= "odt" then @@ -248,12 +406,15 @@ return function(dom) else fix_mo_to_mfenced(el) end + fix_radicals(el) fix_token_elements(el) fix_nested_mstyle(el) + fix_missing_mtext(el) fix_numbers(el) fix_operators(el) fix_mathvariant(el) top_mrow(el) + delete_last_empty_mtr(el) end) return dom end |