summaryrefslogtreecommitdiff
path: root/support/make4ht/domfilters/make4ht-collapsetoc.lua
blob: 0b77b9dbbba35cb04f8de4e861b94286129d4e2c (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
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)