size: 11 KiB

1local ast = require("djot.ast")
2local new_node = ast.new_node
3local new_attributes = ast.new_attributes
4local add_child = ast.add_child
5local unpack = unpack or table.unpack
6local insert_attribute, copy_attributes =
7 ast.insert_attribute, ast.copy_attributes
8local format = string.format
9local find, gsub = string.find, string.gsub
10
11-- Produce a copy of a table.
12local function copy(tbl)
13 local result = {}
14 if tbl then
15 for k,v in pairs(tbl) do
16 local newv = v
17 if type(v) == "table" then
18 newv = copy(v)
19 end
20 result[k] = newv
21 end
22 end
23 return result
24end
25
26local function to_text(node)
27 local buffer = {}
28 if node.t == "str" then
29 buffer[#buffer + 1] = node.s
30 elseif node.t == "nbsp" then
31 buffer[#buffer + 1] = "\160"
32 elseif node.t == "softbreak" then
33 buffer[#buffer + 1] = " "
34 elseif node.c and #node.c > 0 then
35 for i=1,#node.c do
36 buffer[#buffer + 1] = to_text(node.c[i])
37 end
38 end
39 return table.concat(buffer)
40end
41
42local Renderer = {}
43
44function Renderer:new()
45 local state = {
46 out = function(s)
47 io.stdout:write(s)
48 end,
49 tight = false,
50 footnote_index = {},
51 next_footnote_index = 1,
52 references = nil,
53 footnotes = nil }
54 setmetatable(state, self)
55 self.__index = self
56 return state
57end
58
59Renderer.html_escapes =
60 { ["<"] = "&lt;",
61 [">"] = "&gt;",
62 ["&"] = "&amp;",
63 ['"'] = "&quot;" }
64
65function Renderer:escape_html(s)
66 if find(s, '[<>&]') then
67 return (gsub(s, '[<>&]', self.html_escapes))
68 else
69 return s
70 end
71end
72
73function Renderer:escape_html_attribute(s)
74 if find(s, '[<>&"]') then
75 return (gsub(s, '[<>&"]', self.html_escapes))
76 else
77 return s
78 end
79end
80
81function Renderer:render(doc, handle)
82 self.references = doc.references
83 self.footnotes = doc.footnotes
84 if handle then
85 self.out = function(s)
86 handle:write(s)
87 end
88 end
89 self[doc.t](self, doc)
90end
91
92
93function Renderer:render_children(node)
94 -- trap stack overflow
95 local ok, err = pcall(function ()
96 if node.c and #node.c > 0 then
97 local oldtight
98 if node.tight ~= nil then
99 oldtight = self.tight
100 self.tight = node.tight
101 end
102 for i=1,#node.c do
103 self[node.c[i].t](self, node.c[i])
104 end
105 if node.tight ~= nil then
106 self.tight = oldtight
107 end
108 end
109 end)
110 if not ok and err:find("stack overflow") then
111 self.out("(((DEEPLY NESTED CONTENT OMITTED)))\n")
112 end
113end
114
115function Renderer:render_attrs(node)
116 if node.attr then
117 for k,v in pairs(node.attr) do
118 self.out(" " .. k .. "=" .. '"' ..
119 self:escape_html_attribute(v) .. '"')
120 end
121 end
122 if node.pos then
123 local sp, ep = unpack(node.pos)
124 self.out(' data-startpos="' .. tostring(sp) ..
125 '" data-endpos="' .. tostring(ep) .. '"')
126 end
127end
128
129function Renderer:render_tag(tag, node)
130 self.out("<" .. tag)
131 self:render_attrs(node)
132 self.out(">")
133end
134
135function Renderer:add_backlink(nodes, i)
136 local backlink = new_node("link")
137 backlink.destination = "#fnref" .. tostring(i)
138 backlink.attr = ast.new_attributes({role = "doc-backlink"})
139 local arrow = new_node("str")
140 arrow.s = "↩︎︎"
141 add_child(backlink, arrow)
142 if nodes.c[#nodes.c].t == "para" then
143 add_child(nodes.c[#nodes.c], backlink)
144 else
145 local para = new_node("para")
146 add_child(para, backlink)
147 add_child(nodes, para)
148 end
149end
150
151function Renderer:doc(node)
152 self:render_children(node)
153 -- render notes
154 if self.next_footnote_index > 1 then
155 local ordered_footnotes = {}
156 for k,v in pairs(self.footnotes) do
157 if self.footnote_index[k] then
158 ordered_footnotes[self.footnote_index[k]] = v
159 end
160 end
161 self.out('<section role="doc-endnotes">\n<hr>\n<ol>\n')
162 for i=1,#ordered_footnotes do
163 local note = ordered_footnotes[i]
164 if note then
165 self.out(format('<li id="fn%d">\n', i))
166 self:add_backlink(note,i)
167 self:render_children(note)
168 self.out('</li>\n')
169 end
170 end
171 self.out('</ol>\n</section>\n')
172 end
173end
174
175function Renderer:raw_block(node)
176 if node.format == "html" then
177 self.out(node.s) -- no escaping
178 end
179end
180
181function Renderer:para(node)
182 if not self.tight then
183 self:render_tag("p", node)
184 end
185 self:render_children(node)
186 if not self.tight then
187 self.out("</p>")
188 end
189 self.out("\n")
190end
191
192function Renderer:blockquote(node)
193 self:render_tag("blockquote", node)
194 self.out("\n")
195 self:render_children(node)
196 self.out("</blockquote>\n")
197end
198
199function Renderer:div(node)
200 self:render_tag("div", node)
201 self.out("\n")
202 self:render_children(node)
203 self.out("</div>\n")
204end
205
206function Renderer:section(node)
207 self:render_tag("section", node)
208 self.out("\n")
209 self:render_children(node)
210 self.out("</section>\n")
211end
212
213function Renderer:heading(node)
214 self:render_tag("h" .. node.level , node)
215 self:render_children(node)
216 self.out("</h" .. node.level .. ">\n")
217end
218
219function Renderer:thematic_break(node)
220 self:render_tag("hr", node)
221 self.out("\n")
222end
223
224function Renderer:code_block(node)
225 self:render_tag("pre", node)
226 self.out("<code")
227 if node.lang and #node.lang > 0 then
228 self.out(" class=\"language-" .. node.lang .. "\"")
229 end
230 self.out(">")
231 self.out(self:escape_html(node.s))
232 self.out("</code></pre>\n")
233end
234
235function Renderer:table(node)
236 self:render_tag("table", node)
237 self.out("\n")
238 self:render_children(node)
239 self.out("</table>\n")
240end
241
242function Renderer:row(node)
243 self:render_tag("tr", node)
244 self.out("\n")
245 self:render_children(node)
246 self.out("</tr>\n")
247end
248
249function Renderer:cell(node)
250 local tag
251 if node.head then
252 tag = "th"
253 else
254 tag = "td"
255 end
256 local attr = copy(node.attr)
257 if node.align then
258 insert_attribute(attr, "style", "text-align: " .. node.align .. ";")
259 end
260 self:render_tag(tag, {attr = attr})
261 self:render_children(node)
262 self.out("</" .. tag .. ">\n")
263end
264
265function Renderer:caption(node)
266 self:render_tag("caption", node)
267 self:render_children(node)
268 self.out("</caption>\n")
269end
270
271function Renderer:list(node)
272 local sty = node.style
273 if sty == "*" or sty == "+" or sty == "-" then
274 self:render_tag("ul", node)
275 self.out("\n")
276 self:render_children(node)
277 self.out("</ul>\n")
278 elseif sty == "X" then
279 local attr = copy(node.attr)
280 if attr.class then
281 attr.class = "task-list " .. attr.class
282 else
283 insert_attribute(attr, "class", "task-list")
284 end
285 self:render_tag("ul", {attr = attr})
286 self.out("\n")
287 self:render_children(node)
288 self.out("</ul>\n")
289 elseif sty == ":" then
290 self:render_tag("dl", node)
291 self.out("\n")
292 self:render_children(node)
293 self.out("</dl>\n")
294 else
295 self.out("<ol")
296 if node.start and node.start > 1 then
297 self.out(" start=\"" .. node.start .. "\"")
298 end
299 local list_type = gsub(node.style, "%p", "")
300 if list_type ~= "1" then
301 self.out(" type=\"" .. list_type .. "\"")
302 end
303 self:render_attrs(node)
304 self.out(">\n")
305 self:render_children(node)
306 self.out("</ol>\n")
307 end
308end
309
310function Renderer:list_item(node)
311 if node.checkbox then
312 if node.checkbox == "checked" then
313 self.out('<li class="checked">')
314 elseif node.checkbox == "unchecked" then
315 self.out('<li class="unchecked">')
316 end
317 else
318 self:render_tag("li", node)
319 end
320 self.out("\n")
321 self:render_children(node)
322 self.out("</li>\n")
323end
324
325function Renderer:term(node)
326 self:render_tag("dt", node)
327 self:render_children(node)
328 self.out("</dt>\n")
329end
330
331function Renderer:definition(node)
332 self:render_tag("dd", node)
333 self.out("\n")
334 self:render_children(node)
335 self.out("</dd>\n")
336end
337
338function Renderer:definition_list_item(node)
339 self:render_children(node)
340end
341
342function Renderer:reference_definition()
343end
344
345function Renderer:footnote_reference(node)
346 local label = node.s
347 local index = self.footnote_index[label]
348 if not index then
349 index = self.next_footnote_index
350 self.footnote_index[label] = index
351 self.next_footnote_index = self.next_footnote_index + 1
352 end
353 self.out(format('<a id="fnref%d" href="#fn%d" role="doc-noteref"><sup>%d</sup></a>', index, index, index))
354end
355
356function Renderer:raw_inline(node)
357 if node.format == "html" then
358 self.out(node.s) -- no escaping
359 end
360end
361
362function Renderer:str(node)
363 -- add a span, if needed, to contain attribute on a bare string:
364 if node.attr then
365 self:render_tag("span", node)
366 self.out(self:escape_html(node.s))
367 self.out("</span>")
368 else
369 self.out(self:escape_html(node.s))
370 end
371end
372
373function Renderer:softbreak()
374 self.out("\n")
375end
376
377function Renderer:hardbreak()
378 self.out("<br>\n")
379end
380
381function Renderer:nbsp()
382 self.out("&nbsp;")
383end
384
385function Renderer:verbatim(node)
386 self:render_tag("code", node)
387 self.out(self:escape_html(node.s))
388 self.out("</code>")
389end
390
391function Renderer:link(node)
392 local attrs = new_attributes{}
393 if node.reference then
394 local ref = self.references[node.reference]
395 if ref then
396 if ref.attr then
397 copy_attributes(attrs, ref.attr)
398 end
399 insert_attribute(attrs, "href", ref.destination)
400 end
401 elseif node.destination then
402 insert_attribute(attrs, "href", node.destination)
403 end
404 -- link's attributes override reference's:
405 copy_attributes(attrs, node.attr)
406 self:render_tag("a", {attr = attrs})
407 self:render_children(node)
408 self.out("</a>")
409end
410
411Renderer.url = Renderer.link
412
413Renderer.email = Renderer.link
414
415function Renderer:image(node)
416 local attrs = new_attributes{}
417 local alt_text = to_text(node)
418 if #alt_text > 0 then
419 insert_attribute(attrs, "alt", to_text(node))
420 end
421 if node.reference then
422 local ref = self.references[node.reference]
423 if ref then
424 if ref.attr then
425 copy_attributes(attrs, ref.attr)
426 end
427 insert_attribute(attrs, "src", ref.destination)
428 end
429 elseif node.destination then
430 insert_attribute(attrs, "src", node.destination)
431 end
432 -- image's attributes override reference's:
433 copy_attributes(attrs, node.attr)
434 self:render_tag("img", {attr = attrs})
435end
436
437function Renderer:span(node)
438 self:render_tag("span", node)
439 self:render_children(node)
440 self.out("</span>")
441end
442
443function Renderer:mark(node)
444 self:render_tag("mark", node)
445 self:render_children(node)
446 self.out("</mark>")
447end
448
449function Renderer:insert(node)
450 self:render_tag("ins", node)
451 self:render_children(node)
452 self.out("</ins>")
453end
454
455function Renderer:delete(node)
456 self:render_tag("del", node)
457 self:render_children(node)
458 self.out("</del>")
459end
460
461function Renderer:subscript(node)
462 self:render_tag("sub", node)
463 self:render_children(node)
464 self.out("</sub>")
465end
466
467function Renderer:superscript(node)
468 self:render_tag("sup", node)
469 self:render_children(node)
470 self.out("</sup>")
471end
472
473function Renderer:emph(node)
474 self:render_tag("em", node)
475 self:render_children(node)
476 self.out("</em>")
477end
478
479function Renderer:strong(node)
480 self:render_tag("strong", node)
481 self:render_children(node)
482 self.out("</strong>")
483end
484
485function Renderer:double_quoted(node)
486 self.out("&ldquo;")
487 self:render_children(node)
488 self.out("&rdquo;")
489end
490
491function Renderer:single_quoted(node)
492 self.out("&lsquo;")
493 self:render_children(node)
494 self.out("&rsquo;")
495end
496
497function Renderer:left_double_quote()
498 self.out("&ldquo;")
499end
500
501function Renderer:right_double_quote()
502 self.out("&rdquo;")
503end
504
505function Renderer:left_single_quote()
506 self.out("&lsquo;")
507end
508
509function Renderer:right_single_quote()
510 self.out("&rsquo;")
511end
512
513function Renderer:ellipses()
514 self.out("&hellip;")
515end
516
517function Renderer:em_dash()
518 self.out("&mdash;")
519end
520
521function Renderer:en_dash()
522 self.out("&ndash;")
523end
524
525function Renderer:symbol(node)
526 self.out(":" .. node.alias .. ":")
527end
528
529function Renderer:math(node)
530 local math_t = "inline"
531 if find(node.attr.class, "display") then
532 math_t = "display"
533 end
534 self:render_tag("span", node)
535 if math_t == "inline" then
536 self.out("\\(")
537 else
538 self.out("\\[")
539 end
540 self.out(self:escape_html(node.s))
541 if math_t == "inline" then
542 self.out("\\)")
543 else
544 self.out("\\]")
545 end
546 self.out("</span>")
547end
548
549return { Renderer = Renderer }