summaryrefslogtreecommitdiff
path: root/support/make4ht/domfilters/make4ht-aeneas.lua
blob: 4a21c893323e3356d52a1c393c8da801fc16cfae (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
-- 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"
local log = logging.new "aeneas"

-- 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)
      log:debug(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