summaryrefslogtreecommitdiff
path: root/support/make4ht/domfilters
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2023-02-25 03:01:09 +0000
committerNorbert Preining <norbert@preining.info>2023-02-25 03:01:09 +0000
commit81a9d839224eef4c2bd9bf68410b4049c61cdb14 (patch)
tree813b5a34a2548b16dbbcd271697c7bcf29342bde /support/make4ht/domfilters
parentc283f3a6970d2bc9de3501415f5701277da68693 (diff)
CTAN sync 202302250301
Diffstat (limited to 'support/make4ht/domfilters')
-rw-r--r--support/make4ht/domfilters/make4ht-inlinecss.lua83
-rw-r--r--support/make4ht/domfilters/make4ht-joincharacters.lua2
-rw-r--r--support/make4ht/domfilters/make4ht-mathmlfixes.lua221
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