summaryrefslogtreecommitdiff
path: root/support/make4ht/domfilters/make4ht-collapsetoc.lua
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2021-07-27 03:01:57 +0000
committerNorbert Preining <norbert@preining.info>2021-07-27 03:01:57 +0000
commitcef58753eba8ad7e2071195dbd92454febe82d32 (patch)
treedf909696e42fb25ce108e30258c7f935ded86079 /support/make4ht/domfilters/make4ht-collapsetoc.lua
parent84465d50d746dfed1bfbe7bb5065279665fa87de (diff)
CTAN sync 202107270301
Diffstat (limited to 'support/make4ht/domfilters/make4ht-collapsetoc.lua')
-rw-r--r--support/make4ht/domfilters/make4ht-collapsetoc.lua387
1 files changed, 219 insertions, 168 deletions
diff --git a/support/make4ht/domfilters/make4ht-collapsetoc.lua b/support/make4ht/domfilters/make4ht-collapsetoc.lua
index 0b77b9dbbb..ede6e1f908 100644
--- a/support/make4ht/domfilters/make4ht-collapsetoc.lua
+++ b/support/make4ht/domfilters/make4ht-collapsetoc.lua
@@ -1,198 +1,249 @@
+-- mini TOC support for make4ht
local domobject = require "luaxml-domobject"
local filter = require "make4ht-filter"
local log = logging.new "collapsetoc"
-
-
-local toc_levels = {"partToc", "chapterToc", "sectionToc", "subsectionToc", "subsubsectionToc"}
-
-local debug_print = function(...) log:debug(...) end
-
-
--- return toc element type and it's id
-local function get_id(el)
- local name = el:get_attribute "class"
- local id
- local a = el:query_selector "a" or {}
- local first = a[1]
- if first then
- local href = first:get_attribute "href"
- id = href:match("#(.+)$")
+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
- return name, id
end
-local function remove_sections(part_elements, currentpart)
- -- we need to remove toc entries from the previous part if the
- -- current document isn't part of it
- if currentpart == false then
- for _, part in ipairs(part_elements) do
- part:remove_node()
+-- 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
-local function make_toc_selector(toc_levels)
- local level_classes = {}
- for _, l in ipairs(toc_levels) do
- level_classes[#level_classes+1] = "." .. l
+-- 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 table.concat(level_classes, ", ")
+ return ids
end
-local function find_toc_levels(toc)
- -- find toc levels used in the document
- -- it ecpects that sectioning levels appears in the TOC in the descending order
- local levels, used = {}, {}
- local level = 1
- -- we still expect the standard class names
- local toc_selector = make_toc_selector(toc_levels)
- for _, el in ipairs(toc:query_selector(toc_selector)) do
- local class = el:get_attribute("class")
- if not used[class] then
- table.insert(levels, class)
- used[class] = level
- level = level + 1
+
+-- 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
- return levels, used
-end
-
-local function remove_levels(toc, matched_ids)
- -- sort the matched sections according to their levels
- local levels, level_numbers = find_toc_levels(toc)
- debug_print("remove levels", #levels)
- -- for _, level in ipairs(levels) do
- -- print(level, level_numbers[level], matched_ids[level])
- -- end
- local keep_branch = false
- local matched_levels = {}
- local toc_selector = make_toc_selector(toc_levels)
- for _, el in ipairs(toc:query_selector(toc_selector)) do
- local name, id = get_id(el)
- -- get the current toc hiearchy level
- local level = level_numbers[name]
- -- get the matched id for the current level
- local level_id = matched_ids[name]
- local matched = level_id == id
- local remove = true
- -- we will use this for toc elements at lower hiearchy than is the top sectioning level on the page
- if matched then keep_branch = true end
- -- find the parent level to the current section level
- local parent_level = toc_levels[level - 1]
- local parent_matched = matched_levels[parent_level]
- if matched then
- debug_print("match",name, id, level_id, level, #levels)
- keep_branch = true
- remove = false
- elseif level==1 then
- -- don't remove the top level
- debug_print("part",name, id, level_id, level)
- remove = false
- matched_levels = {}
- if not matched then keep_branch = false end
- elseif keep_branch then
- -- if level >= (#levels - 1) then
- if level > matched_ids._levels then
- debug_print("level",name, id, level_id, level, parent_level, parent_matched)
- remove = false
- elseif matched_ids.ids[id] then
- debug_print("matched id",name, id, level_id, level, parent_level, parent_matched)
- remove = false
- elseif parent_matched then
- debug_print("parent_matched",name, id, level_id, level, parent_level, parent_matched)
- keep_branch = false
- remove = false
- end
- -- else
- -- print("remove", name, id, level_id, level, #matched_ids)
- -- end
- elseif parent_matched then
- debug_print("parent_matched alternative",name, id, level_id, level, parent_level, parent_matched)
- remove = false
- else
- debug_print("else",name, id, level_id, level, keep_branch)
- keep_branch = false
+ -- 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
- matched_levels[name] = matched
- if remove then
- el:remove_node()
- --print(name,id, level_id, matched)
+ 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 process = filter{ function(s)
- -- local dom = domobject.parse(s)
-local function collapse_toc(dom, par)
+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"
- local title_query = par.title_query or options.title_query or ".partHead a, .chapterHead a, .sectionHead a, .subsectionHead a"
- toc_levels = par.toc_levels or options.toc_levels or toc_levels
- -- keep track of current id of each sectioning level
- local current_ids, matched_ids = {}, {_levels = 0, ids = {}}
- -- search sectioning elements
- local titles = dom:query_selector(title_query)
- local section_ids = {}
- for _, x in ipairs(titles) do
- -- get their id attributes and save them in a table
- section_ids[#section_ids+1] = x:get_attribute("id")
- end
-
- -- we need to retrieve the first table of contents
- local toctables = dom:query_selector(toc_query) or {}
- -- process only when we got a TOC
- debug_print("toc query", toc_query, #toctables)
- if #toctables > 0 then
- local tableofcontents = toctables[1]
- -- all toc entries are in span elements
- local toc = tableofcontents:query_selector("span")
- local currentpart = false
- local part_elements = {}
- for _, el in ipairs(toc) do
- -- get sectioning level and id of the current TOC entry
- local name, id = get_id(el)
- -- set the id of the current sectioning level
- current_ids[name] = id
- for _, sectid in ipairs(section_ids) do
- -- detect if the current TOC entry match some sectioning element in the current document
- if id == sectid then
- currentpart = true
- -- save the current id as a matched id
- matched_ids.ids[id] = true
- -- copy the TOC hiearchy for the current toc level
- for i, level in ipairs(toc_levels) do
- -- print("xxx",i, level, current_ids[level])
- matched_ids[level] = current_ids[level]
- -- set the maximum matched level
- if i > matched_ids._levels then matched_ids._levels = i end
- if level == name then break end
- end
- debug_print("match", id)
- end
- end
- end
- remove_levels(tableofcontents, matched_ids)
-
- -- remove sections from the last part
- -- remove_sections(part_elements,currentpart)
- -- remove unneeded br elements
- local br = tableofcontents:query_selector("br")
- for _, el in ipairs(br) do el:remove_node() end
- -- remove unneded whitespace
- for _, el in ipairs(tableofcontents:get_children()) do
- if el:is_text() then el:remove_node() end
- end
+ -- 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 collapse_toc
+end
--- Make:match("html$", process)
+return collapsetoc