summaryrefslogtreecommitdiff
path: root/support/make4ht/domfilters/make4ht-collapsetoc.lua
blob: ede6e1f908e5936e59e0edfdb4102527762c22c6 (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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
-- mini TOC support for make4ht
local domobject = require "luaxml-domobject"

local filter = require "make4ht-filter"
local log = logging.new "collapsetoc"
local mktuils = require "mkutils"


-- assign levels to entries in the .4tc file
local toc_levels = {
  tocpart = 1,
  toclikepart = 1,
  tocappendix = 2,
  toclikechapter = 2,
  tocchapter = 2,
  tocsection = 3,
  toclikesection = 3,
  tocsubsection = 4,
  toclikesubsection = 4,
  tocsubsubsection = 5,
  toclikesubsubsection = 5,
  tocparagraph = 6,
  toclikeparagraph = 6,
  tocsubparagraph = 7,
  toclikesubparagraph = 7,
}

-- number of child levels to be kept
-- the depth of 1 ensures that only direct children of the current sectioning command 
-- will be kept in TOC
local max_depth = 1


-- debugging function to test correct structure of the TOC tree
local function print_tree(tree, level) 
  local level = level or 0
  log:debug(string.rep(" ", level) .. (tree.type or "root"), tree.id)
  for k, v in pairs(tree.children) do
    print_tree(v, level + 2)
  end
end

-- convert the parsed toc entries to a tree structure
local function make_toc_tree(tocentries, lowestlevel, position, tree)
  local position = position or 1
  local tree = tree or {
    level = lowestlevel - 1,
    children = {}
  }
  local stack = {tree}
  if position > #tocentries then return tree, position end
  -- loop over TOC entries and make a tree
  for i = 1, #tocentries do
    -- initialize new child
    local element = tocentries[i]
    element.children = element.children or {}
    local parent = stack[#stack]
    local level_diff = element.level - parent.level
    if level_diff == 0 then -- entry is sibling of parent
      -- current parent is sibling of the current elemetn, true parent is 
      -- sibling's parent
      parent = parent.parent
      -- we must replace sibling element with the current element in stact
      -- so the child elements get correct parent
      table.remove(stack)
      table.insert(stack, element)
    elseif level_diff > 0 then -- entry is child of parent
      for x = 1, level_diff do
        table.insert(stack, element)
      end
    else
      -- we must remove levels from the stack to get the correct parent
      for x =1 , level_diff, -1 do
        if #stack > 0 then
          parent = table.remove(stack)
        end
      end
      -- we must reinsert parent back to stack, place the current element to stact too
      table.insert(stack, parent)
      table.insert(stack, element)
    end
    table.insert(parent.children, element)
    element.parent = parent
  end
  print_tree(tree)
  return tree
end

-- find first sectioning element in the current page
local function find_headers(dom, header_levels)
  -- we need to find id attributes in <a> elements that are children of sectioning elements
  local ids = {}
  for _, header in ipairs(dom:query_selector(header_levels)) do
    local id = header:get_attribute "id"
    if id then ids[#ids+1] = id end
  end
  return ids
end


-- process list of ids and find those that should be kept:
-- siblings, children, parents and top level
local function find_toc_entries_to_keep(ids, tree)
  local tree = tree or {}
  -- all id in TOC tree that we want to kepp are saved in this table
  local ids_to_keep = {}
  -- find current id in the TOC tree
  local function find_id(id, tree)
    if tree.id == id then return tree end
    if not tree.children or #tree.children == 0 then return false end
    for k,v in pairs(tree.children) do
      local found_id = find_id(id, v)
      if found_id then return found_id end
    end
    return false
  end
  -- always keep top level of the hiearchy
  local function keep_toplevel(tree)
    for _, el in ipairs(tree.children) do
      ids_to_keep[el.id] = true
    end
  end
  -- we want to keep all children in TOC hiearchy
  local function keep_children(element, depth)
    local depth = depth or 1
    local max_depth = max_depth or 1
    -- stop processing when there are no children
    for _, el in pairs(element.children or {}) do
      if el.id then ids_to_keep[el.id] = true end
      -- by default, we keep just direct children of the current sectioning element
      if depth < max_depth then
        keep_children(el, depth + 1)
      end
    end
  end
  -- also keep all siblings
  local function keep_siblings(element)
    local parent = element.parent
    for k, v in pairs(parent.children or {}) do
      ids_to_keep[v.id] = true
    end
  end
  -- and of course, keep all parents
  local function keep_parents(element)
    local parent = element.parent
    if parent and parent.id then
      ids_to_keep[parent.id] = true
      -- we should keep siblings of all parents as well
      keep_siblings(parent)
      keep_parents(parent)
    end
  end
  -- always keep the top-level TOC hiearchy, even if we cannot find any sectioning element on the page
  keep_toplevel(tree)
  for _, id in ipairs(ids) do
    -- keep the current id
    ids_to_keep[id] = true
    local found_element = find_id(id, tree)
    if found_element then
      keep_children(found_element)
      keep_siblings(found_element)
      keep_parents(found_element)
    end
  end
  return ids_to_keep
end

-- process the .4tc file and convert entries to a tree structure
-- based on the sectioning level
local function parse_4tc(parameters, toc_levels)
  local tcfilename = parameters.input .. ".4tc"
  if not mkutils.file_exists(tcfilename) then 
    log:warning("Cannot find TOC: " .. tcfilename)
    return {}
  end
  local tocentries = {}
  local f = io.open(tcfilename, "r")
  -- we need to find the lowest level used in the TOC
  local lowestlevel = 999
  for line in f:lines() do
    -- entries looks like: \doTocEntry\tocsubsection{1.2.2}{\csname a:TocLink\endcsname{5}{x5-60001.2.2}{QQ2-5-6}{aaaa}}{7}\relax 
    -- we want do extract tocsubsection and x5-60001.2.2
    local toctype, id = line:match("\\doTocEntry\\(.-){.-}{.-{.-}{(.-)}")
    if toctype then
      local level = toc_levels[toctype]
      if not level then 
        log:warning("Cannot find TOC level for: " .. toctype)
      else
        lowestlevel = level < lowestlevel and level or lowestlevel
        table.insert(tocentries, {type = toctype, id = id, level = level})
      end
    end
  end
  f:close()
  local toc =  make_toc_tree(tocentries, lowestlevel)
  return toc
end

local function remove_levels(toc, matched_ids)
  -- remove links that aren't in the TOC hiearchy that should be kept
  for _, link in ipairs(toc:query_selector("a")) do
    local href = link:get_attribute("href")
    -- find id in the href
    local id = href:match("#(.+)")
    if id and not matched_ids[id] then
      -- toc links are in <span> elements that can contain the section number
      -- we must remove them too
      local parent = link:get_parent()
      if parent:get_element_name() == "span" then
        parent:remove_node()
      else
        -- if the parent node isn't <span>, remove at least the link itself
        link:remove_node()
      end
    end
  end
end


local function collapsetoc(dom, parameters)
  -- set options
  local par = parameters
  local options = get_filter_settings "collapsetoc"
  -- query to find the TOC element in DOM
  local toc_query = par.toc_query or options.toc_query or ".tableofcontents"
  -- query to select sectioning elements with id's
  local title_query = par.title_query or options.title_query or "h1 a, h2 a, h3 a, h4 a, h5 a, h6 a" 
  -- level of child levels to be kept in TOC
  max_depth = par.max_depth or options.max_depth or max_depth
  -- set level numbers for particular TOC entry types
  local user_toc_levels = par.toc_levels or options.toc_levels or {}
  -- join user's levels with default
  for k,v in pairs(user_toc_levels) do toc_levels[k] = v end
  -- parse the .4tc file to get TOC tree
  toc = toc or parse_4tc(parameters, toc_levels)
  -- find sections in the current html file
  local ids = find_headers(dom, title_query)
  log:debug("Ids", table.concat(ids, ","))
  local ids_to_keep = find_toc_entries_to_keep(ids, toc)
  local toc_dom = dom:query_selector(toc_query)[1]
  if toc_dom then
    remove_levels(toc_dom, ids_to_keep)
  else
    log:warning("Cannot find TOC element using query: " .. toc_query)
  end
  return dom
end

return collapsetoc