Differences From
Artifact [ed743c91e7]:
1 1 local ct = require 'cortav'
2 2 local ss = require 'sirsem'
3 +
4 +local css_toc = [[
5 +
6 +]]
7 +
8 +local css_toc_fixed = [[
9 + @media (min-width: calc(@[width]:[100vw] + 20em)) {
10 + ol.toc {
11 + position: fixed;
12 + padding-top: 1em; padding-bottom: 1em;
13 + padding-right: 1em;
14 + margin-top: 0; margin-bottom: 0;
15 + right: 0; top: 0; bottom: 0;
16 + max-width: calc(50vw - ((@[width]:[0]) / 2) - 3.5em);
17 + overflow-y: auto;
18 + }
19 + @media (max-width: calc(@[width]:[100vw] + 30em)) {
20 + ol.toc {
21 + max-width: calc(100vw - ((@[width]:[0])) - 9.5em);
22 + }
23 + body {
24 + margin-left: 5em;
25 + }
26 + }
27 + }
28 +]]
3 29
4 30 ct.ext.install {
5 31 id = 'toc';
6 32 desc = 'provides a table of contents for HTML renderer plus generic fallback';
7 33 version = ss.version {0,1; 'devel'};
8 34 contributors = {{name='lexi hale', handle='velartrill', mail='lexi@hale.su', homepage='https://hale.su'}};
9 - directive = function(words)
35 + default = true; -- on unless inhibited
36 + hook = {
37 + doc_init = function(job)
38 + print('initing doc:toc',job.doc)
39 + job.state.toc_custom_position = false
40 + end;
41 +
42 + render_html_init = function(job, render)
43 + render.stylesets.toc = css_toc
44 + render.stylesets.tocFixed = css_toc_fixed
45 + end;
46 +
47 + render_html_ir_assemble = function(job, render, ir)
48 + -- the custom position state is part of the document job,
49 + -- but rendering is a separate job, so we need to get the
50 + -- state of this extension in the parent job, which is
51 + -- done with the job:unwind(depth) call. unwind is a method
52 + -- of the delegate we access the job through which gives us
53 + -- direct access to the job state of this extension; unwind
54 + -- climbs the jobtree and constructs a similar delegate for
55 + -- the nth parent. note that this will only work if the
56 + -- current extension hasn't been excluded by predicate from
57 + -- the nth parent!
58 + if not job:unwind(1).state.toc_custom_position then
59 + -- TODO insert %toc end of first section
60 + end
61 + end;
62 + };
63 + directives = {
64 + mark = function (job, ctx, words)
65 + local _, _, text = words(2)
66 + ctx:insert {kind = 'anchor', _toc_label = ct.parse_span(text,ctx)}
67 + end;
68 + name = function (job, ctx, words)
69 + local _, _, id, text = words(3)
70 + ctx:insert {kind = 'anchor', id=id, _toc_label = ct.parse_span(text,ctx)}
71 + end;
72 + [true] = function (job, ctx, words)
73 + local _, op, val = words(2)
74 + if op == nil then
75 + local toc = {kind='toc'}
76 + ctx:insert(toc)
77 + -- same deal here -- directives are processed as part of
78 + -- the parse job, which is forked off the document job,
79 + -- so we need to climb the jobstack
80 + job:unwind(1).state.toc_custom_position = true
81 + job:hook('ext_toc_position', ctx, toc)
82 + else
83 + ctx:fail 'bad %toc directive'
84 + end
85 + end;
86 + };
87 + render = {
88 + toc = {
89 + html = function(job, renderer, block, section)
90 + -- “tagproc” contains the functions that determine what kind
91 + -- of data our abstract tags will be transformed into. this
92 + -- is needed to that plain text, HTML, and HTML IR can be
93 + -- produced from the same functions just by varying the
94 + -- proc set.
95 + --
96 + -- “astproc” contains the functions that determine what form
97 + -- our span arrays (and blocks, but not relevant here) will
98 + -- be transformed into, and is analogous to “tagproc”
99 + local tag = renderer.tagproc.tag;
100 + local elt = renderer.tagproc.elt;
101 + local catenate = renderer.tagproc.catenate;
102 + local sr = renderer.astproc.span_renderers;
103 + local getSafeID = renderer.state.obj_htmlid;
104 +
105 + -- toplevel HTML IR
106 + local lst = {tag = 'ol', attrs={class='toc'}, nodes={}}
107 +
108 + -- "renderer.state" contains the stateglob of the renderer
109 + -- itself, not to be confused with the "state" parameter
110 + -- which contains this extension's share of the job state
111 + -- we use it to activate the stylesets we injected earlier
112 + renderer.state.stylesets_active.toc = true
113 + if renderer.state.opts['width'] then
114 + renderer.state.stylesets_active.tocFixed = true
115 + end
116 +
117 + -- assemble a tree of links from the document section
118 + -- structure. this is tricky, because we need a tree, but
119 + -- all we have is a flat list with depth values attached to
120 + -- each node.
121 + local stack = {lst}
122 + local top = function() return stack[#stack] end
123 + -- job.doc is the document the render job is bound to, and
124 + -- its secorder field is a list of all the doc's sections in
125 + -- the order they occur ("doc.sections" is a hashmap from name
126 + -- to section object)
127 + local all = job.doc.secorder
128 +
129 + for i, sec in ipairs(all) do
130 + if sec.heading_node then -- does this section have a label?
131 + local ent = tag('li',nil,
132 + catenate{tag('a', {href='#'..getSafeID(sec)},
133 + sr.htmlSpan(sec.heading_node.spans, sec.heading_node, sec))})
134 + if sec.depth > #stack then
135 + local n = {tag = 'ol', attrs={}, nodes={ent}}
136 + table.insert(top().nodes[#top().nodes].nodes, n)
137 + table.insert(stack, n)
138 + else
139 + if sec.depth < #stack then
140 + for j=#stack,sec.depth+1,-1 do stack[j] = nil end
141 + end
142 + table.insert(top().nodes, ent)
143 + end
144 +
145 + -- now we need to assemble a list of items within the
146 + -- section worthy of an entry on their own. currently
147 + -- this is only anchors created with %toc mark|name
148 + local innerlinks = {}
149 + local noteworthy = { anchor = true }
150 + for j, block in pairs(sec.blocks) do
151 + if noteworthy[block.kind] then
152 + local label = ss.coalesce(block._toc_label, block.label, block.spans)
153 + if label then
154 + table.insert(innerlinks, {
155 + id = renderer.state.obj_htmlid(block);
156 + label = label;
157 + block = block;
158 + })
159 + end
160 + end
161 + end
162 +
163 + if next(innerlinks) then
164 + local n = {tag = 'ol', attrs = {}, nodes = {}}
165 + for i, l in ipairs(innerlinks) do
166 + local nn = {
167 + tag = 'a';
168 + attrs = {href = '#' .. l.id};
169 + nodes = {sr.htmlSpan(l.label, l.block, sec)};
170 + }
171 + table.insert(n.nodes, {tag = 'li', attrs = {}, nodes={nn}})
172 + end
173 + table.insert(ent.nodes, n)
174 + end
175 + print(ss.dump(ent))
176 + end
177 + end
178 + return lst
179 + end;
10 180
11 - end;
181 + [true] = function() end; -- fallback // convert to different node types
182 + };
183 + };
12 184 }