From cef58753eba8ad7e2071195dbd92454febe82d32 Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Tue, 27 Jul 2021 03:01:57 +0000 Subject: CTAN sync 202107270301 --- support/make4ht/domfilters/make4ht-collapsetoc.lua | 387 ++++++++++++--------- 1 file changed, 219 insertions(+), 168 deletions(-) (limited to 'support/make4ht/domfilters/make4ht-collapsetoc.lua') 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 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 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 , 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 -- cgit v1.2.3