summaryrefslogtreecommitdiff
path: root/support/make4ht/domfilters
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
committerNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
commite0c6872cf40896c7be36b11dcc744620f10adf1d (patch)
tree60335e10d2f4354b0674ec22d7b53f0f8abee672 /support/make4ht/domfilters
Initial commit
Diffstat (limited to 'support/make4ht/domfilters')
-rw-r--r--support/make4ht/domfilters/make4ht-aeneas.lua144
-rw-r--r--support/make4ht/domfilters/make4ht-fixinlines.lua57
-rw-r--r--support/make4ht/domfilters/make4ht-idcolons.lua20
-rw-r--r--support/make4ht/domfilters/make4ht-joincharacters.lua85
-rw-r--r--support/make4ht/domfilters/make4ht-joincolors.lua75
-rw-r--r--support/make4ht/domfilters/make4ht-odtimagesize.lua16
-rw-r--r--support/make4ht/domfilters/make4ht-t4htlinks.lua27
7 files changed, 424 insertions, 0 deletions
diff --git a/support/make4ht/domfilters/make4ht-aeneas.lua b/support/make4ht/domfilters/make4ht-aeneas.lua
new file mode 100644
index 0000000000..0b93c95840
--- /dev/null
+++ b/support/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/support/make4ht/domfilters/make4ht-fixinlines.lua b/support/make4ht/domfilters/make4ht-fixinlines.lua
new file mode 100644
index 0000000000..25489f59ca
--- /dev/null
+++ b/support/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/support/make4ht/domfilters/make4ht-idcolons.lua b/support/make4ht/domfilters/make4ht-idcolons.lua
new file mode 100644
index 0000000000..ccdafbd6d4
--- /dev/null
+++ b/support/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/support/make4ht/domfilters/make4ht-joincharacters.lua b/support/make4ht/domfilters/make4ht-joincharacters.lua
new file mode 100644
index 0000000000..99d3058d7d
--- /dev/null
+++ b/support/make4ht/domfilters/make4ht-joincharacters.lua
@@ -0,0 +1,85 @@
+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
+ -- 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/support/make4ht/domfilters/make4ht-joincolors.lua b/support/make4ht/domfilters/make4ht-joincolors.lua
new file mode 100644
index 0000000000..94a3a054e6
--- /dev/null
+++ b/support/make4ht/domfilters/make4ht-joincolors.lua
@@ -0,0 +1,75 @@
+local cssfiles = {}
+
+
+-- keep mapping between span ids and colors
+local colors = {}
+
+local function extract_colors(csscontent)
+ local used_colors = {}
+ -- delete the color ids and save the used colors
+ csscontent = csscontent:gsub("[%a]*%#(textcolor.-)%s*{%s*color%s*%:%s*(.-)%s*%}%s", function(id, color)
+ -- convert rgb() function to hex value and generate the span name
+ local converted = "textcolor-" .. color:gsub("rgb%((.-),(.-),(.-)%)", function(r,g,b)
+ return string.format("%02x%02x%02x", tonumber(r), tonumber(g), tonumber(b))
+ end)
+ -- remove the # characters from the converted color name
+ converted = converted:gsub("%#", "")
+ -- save the id and used color
+ colors[id] = converted
+ used_colors[converted] = color
+ return ""
+ end)
+ -- add the used colors to css
+ local t = {}
+ for class, color in pairs(used_colors) do
+ t[#t+1] = string.format(".%s{color:%s;}", class, color)
+ end
+ return csscontent .. table.concat(t, "\n")
+end
+
+local function process_css(cssfile)
+ local f = io.open(cssfile,"r")
+ if not f then return nil, "Cannot open the CSS file: ".. cssfile end
+ local content = f:read("*all")
+ f:close()
+ -- delete color ids and replace them with joined spans
+ local newcontent = extract_colors(content)
+ -- save the updated css file
+ local f=io.open(cssfile, "w")
+ f:write(newcontent)
+ f:close()
+end
+
+
+local function process_css_files(dom)
+ for _, el in ipairs(dom:query_selector("link")) do
+ local href = el:get_attribute("href") or ""
+ if not cssfiles[href] and href:match("css$") then
+ print("Load CSS file ", href)
+ cssfiles[href] = true
+ process_css(href)
+ end
+ end
+
+end
+
+local function join_colors(dom)
+ -- find css files in the current HTML file and join the colors
+ process_css_files(dom)
+ for _, span in ipairs(dom:query_selector("span")) do
+ local id = span:get_attribute("id")
+ if id then
+ -- test if the id is in the saved colors
+ local class = colors[id]
+ if class then
+ -- remove the id
+ span:set_attribute("id", nil)
+ span:set_attribute("class", class)
+ end
+ end
+ end
+
+ return dom
+end
+
+return join_colors
diff --git a/support/make4ht/domfilters/make4ht-odtimagesize.lua b/support/make4ht/domfilters/make4ht-odtimagesize.lua
new file mode 100644
index 0000000000..4ce8f0d92e
--- /dev/null
+++ b/support/make4ht/domfilters/make4ht-odtimagesize.lua
@@ -0,0 +1,16 @@
+-- set correct dimensions to frames around images
+return function(dom)
+ local frames = dom:query_selector("draw|frame")
+ for _, frame in ipairs(frames) do
+ local images = frame:query_selector("draw|image")
+ if #images > 0 then
+ local image = images[1]
+ local width = image:get_attribute("svg:width")
+ local height = image:get_attribute("svg:height")
+ if widht then frame:set_attribute("svg:width", width) end
+ if height then frame:set_attribute("svg:height", height) end
+ print("image dimensions", width, height)
+ end
+ end
+ return dom
+end
diff --git a/support/make4ht/domfilters/make4ht-t4htlinks.lua b/support/make4ht/domfilters/make4ht-t4htlinks.lua
new file mode 100644
index 0000000000..9a4efb9739
--- /dev/null
+++ b/support/make4ht/domfilters/make4ht-t4htlinks.lua
@@ -0,0 +1,27 @@
+-- This filter is used by the ODT output format to fix links
+return function(dom)
+ for _, link in ipairs(dom:query_selector("t4htlink")) do
+ local name = link:get_attribute("name")
+ local href = link:get_attribute("href")
+ local children = link:get_children()
+ -- print("link", name, href, #link._children, link:get_text())
+ -- add a link if it contains any subnodes and has href attribute
+ if #children > 0 and href then
+ link._name = "text:a"
+ link._attr = {["xlink:type"]="simple", ["xlink:href"]=href}
+ -- if the link is named, add a bookmark
+ if name then
+ local bookmark = link:create_element("text:bookmark", {["text:name"] = name})
+ link:add_child_node(bookmark)
+ end
+ -- add bookmark if element has name
+ elseif name then
+ link._name = "text:bookmark"
+ link._attr = {["text:name"] = name}
+ else
+ -- just remove the link in other cases
+ link:remove_node()
+ end
+ end
+ return dom
+end