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("#(.+)$") 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() end end end local function make_toc_selector(toc_levels) local level_classes = {} for _, l in ipairs(toc_levels) do level_classes[#level_classes+1] = "." .. l end return table.concat(level_classes, ", ") 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 end 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 end matched_levels[name] = matched if remove then el:remove_node() --print(name,id, level_id, matched) end end end -- local process = filter{ function(s) -- local dom = domobject.parse(s) local function collapse_toc(dom, par) -- set options local options = get_filter_settings "collapsetoc" 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 end return dom end return collapse_toc -- Make:match("html$", process)