size: 10 KiB

1local djot = require("djot")
2local ast = require("djot.ast")
3local insert_attribute, copy_attributes =
4 ast.insert_attribute, ast.copy_attributes
5local emoji -- require this later, only if emoji encountered
6local format = string.format
7local find, gsub = string.find, string.gsub
8
9-- Produce a copy of a table.
10local function copy(tbl)
11 local result = {}
12 if tbl then
13 for k,v in pairs(tbl) do
14 local newv = v
15 if type(v) == "table" then
16 newv = copy(v)
17 end
18 result[k] = newv
19 end
20 end
21 return result
22end
23
24local Renderer = {}
25
26function Renderer:new()
27 local state = {
28 tight = false,
29 footnotes = nil,
30 references = nil
31 }
32 setmetatable(state, self)
33 self.__index = self
34 return state
35end
36
37local function words(s)
38 if s then
39 local res = {}
40 string.gsub(s, "(%S+)", function(x) table.insert(res, x) end)
41 return res
42 else
43 return {}
44 end
45end
46
47local function to_attr(attr)
48 if not attr then
49 return nil
50 end
51 local result = copy(attr)
52 result.id = nil
53 result.class = nil
54 return pandoc.Attr(attr.id or "", words(attr.class), result)
55end
56
57function Renderer:with_optional_span(node, f)
58 local base = f(self:render_children(node))
59 if node.attr then
60 return pandoc.Span(base, to_attr(node.attr))
61 else
62 return base
63 end
64end
65
66function Renderer:with_optional_div(node, f)
67 local base = f(self:render_children(node))
68 if node.attr then
69 return pandoc.Div(base, to_attr(node.attr))
70 else
71 return base
72 end
73end
74
75function Renderer:render_node(node)
76 return self[node.tag](self, node)
77end
78
79function Renderer:render_children(node)
80 local buff = {}
81 local inline = false
82 if node.children and #node.children > 0 then
83 local oldtight
84 if node.tight ~= nil then
85 oldtight = self.tight
86 self.tight = node.tight
87 end
88 local function integrate_elt(elt)
89 if elt.__name == "Inlines" or elt.__name == "Blocks" then
90 for i=1,#elt do
91 integrate_elt(elt[i])
92 end
93 else
94 buff[#buff + 1] = elt
95 end
96 end
97 for _,child in ipairs(node.children) do
98 local elt = self:render_node(child)
99 integrate_elt(elt)
100 end
101 if node.tight ~= nil then
102 self.tight = oldtight
103 end
104 end
105 return buff
106end
107
108function Renderer:doc(node)
109 self.footnotes = node.footnotes
110 self.references = node.references
111 return pandoc.Pandoc(self:render_children(node))
112end
113
114function Renderer:section(node)
115 local attrs = to_attr(node.attr)
116 table.insert(attrs.classes, 1, "section")
117 return pandoc.Div(self:render_children(node), attrs)
118end
119
120function Renderer:raw_block(node)
121 return pandoc.RawBlock(node.format, node.text)
122end
123
124function Renderer:para(node)
125 local constructor = pandoc.Para
126 if self.tight then
127 constructor = pandoc.Plain
128 end
129 return self:with_optional_div(node, constructor)
130end
131
132function Renderer:blockquote(node)
133 return self:with_optional_div(node, pandoc.BlockQuote)
134end
135
136function Renderer:div(node)
137 return pandoc.Div(self:render_children(node), to_attr(node.attr))
138end
139
140function Renderer:heading(node)
141 return pandoc.Header(node.level,
142 self:render_children(node),
143 to_attr(node.attr))
144end
145
146function Renderer:thematic_break(node)
147 if node.attr then
148 return pandoc.Div(pandoc.HorizontalRule(), to_attr(node.attr))
149 else
150 return pandoc.HorizontalRule()
151 end
152end
153
154function Renderer:code_block(node)
155 local attr = copy(to_attr(node.attr))
156 if not attr.class then
157 attr.class = node.lang
158 else
159 attr.class = node.lang .. " " .. attr.class
160 end
161 return pandoc.CodeBlock(node.text:gsub("\n$",""), attr)
162end
163
164function Renderer:table(node)
165 local rows = {}
166 local headers = {}
167 local caption = {}
168 local aligns = {}
169 local widths = {}
170 local content = node.c
171 for i=1,#content do
172 local row = content[i]
173 if row.t == "caption" then
174 caption = self:render_children(row)
175 elseif row.t == "row" then
176 local cells = {}
177 for j=1,#row.c do
178 cells[j] = self:render_node(row.c[j])
179 if not aligns[j] then
180 local align = row.c[j].align
181 if not align then
182 aligns[j] = "AlignDefault"
183 elseif align == "center" then
184 aligns[j] = "AlignCenter"
185 elseif align == "left" then
186 aligns[j] = "AlignLeft"
187 elseif align == "right" then
188 aligns[j] = "AlignRight"
189 end
190 widths[j] = 0
191 end
192 end
193 if row.head then
194 headers = cells
195 else
196 rows[#rows + 1] = cells
197 end
198 end
199 end
200 return pandoc.utils.from_simple_table(
201 pandoc.SimpleTable(caption, aligns, widths, headers, rows))
202end
203
204function Renderer:cell(node)
205 return { pandoc.Plain(self:render_children(node)) }
206end
207
208function Renderer:list(node)
209 local sty = node.style
210 if sty == "*" or sty == "+" or sty == "-" then
211 return self:with_optional_div(node, pandoc.BulletList)
212 elseif sty == "X" then
213 return self:with_optional_div(node, pandoc.BulletList)
214 elseif sty == ":" then
215 return self:with_optional_div(node, pandoc.DefinitionList)
216 else
217 local start = 1
218 local sty = "DefaultStyle"
219 local delim = "DefaultDelim"
220 if node.start and node.start > 1 then
221 start = node.start
222 end
223 local list_type = gsub(node.style, "%p", "")
224 if list_type == "a" then
225 sty = "LowerAlpha"
226 elseif list_type == "A" then
227 sty = "UpperAlpha"
228 elseif list_type == "i" then
229 sty = "LowerRoman"
230 elseif list_type == "I" then
231 sty = "UpperRoman"
232 end
233 local list_delim = gsub(node.style, "%P", "")
234 if list_delim == ")" then
235 delim = "OneParen"
236 elseif list_delim == "()" then
237 delim = "TwoParens"
238 end
239 return self:with_optional_div(node, function(x)
240 return pandoc.OrderedList(x,
241 pandoc.ListAttributes(start, sty, delim))
242 end)
243 end
244end
245
246function Renderer:list_item(node)
247 local children = self:render_children(node)
248 if node.checkbox then
249 local box = (node.checkbox == "checked" and "☒") or "☐"
250 local tag = children[1].tag
251 if tag == "Para" or tag == "Plain" then
252 children[1].content:insert(1, pandoc.Space())
253 children[1].content:insert(1, pandoc.Str(box))
254 else
255 children:insert(1, pandoc.Para{pandoc.Str(box), pandoc.Space()})
256 end
257 end
258 return children
259end
260
261function Renderer:definition_list_item(node)
262 local term = self:render_node(node.children[1])
263 local defn = self:render_node(node.children[2])
264 return { term, defn }
265end
266
267function Renderer:term(node)
268 return self:render_children(node)
269end
270
271function Renderer:definition(node)
272 return self:render_children(node)
273end
274
275function Renderer:reference_definition()
276 return ""
277end
278
279function Renderer:footnote_reference(node)
280 local label = node.text
281 local note = self.footnotes[label]
282 if note then
283 return pandoc.Note(self:render_children(note))
284 else
285 io.stderr:write("Note " .. label .. " not found.")
286 return pandoc.Str("[^" .. label .. "]")
287 end
288end
289
290function Renderer:raw_inline(node)
291 return pandoc.RawInline(node.format, node.text)
292end
293
294function Renderer:str(node)
295 -- add a span, if needed, to contain attribute on a bare string:
296 if node.attr then
297 return pandoc.Span(pandoc.Inlines(node.text), to_attr(node.attr))
298 else
299 return pandoc.Inlines(node.text)
300 end
301end
302
303function Renderer:softbreak()
304 return pandoc.SoftBreak()
305end
306
307function Renderer:hardbreak()
308 return pandoc.LineBreak()
309end
310
311function Renderer:nbsp()
312 return pandoc.Str(" ")
313end
314
315function Renderer:verbatim(node)
316 return pandoc.Code(node.text, to_attr(node.attr))
317end
318
319function Renderer:link(node)
320 local attrs = {}
321 local dest = node.destination
322 if node.reference then
323 local ref = self.references[node.reference]
324 if ref then
325 if ref.attributes then
326 attrs = copy(ref.attributes)
327 end
328 dest = ref.destination
329 else
330 dest = "#" -- empty href is illegal
331 end
332 end
333 -- link's attributes override reference's:
334 copy_attributes(attrs, node.attr)
335 local title = attrs.title
336 attrs.title = nil
337 return pandoc.Link(self:render_children(node), dest,
338 title, to_attr(attrs))
339end
340
341function Renderer:image(node)
342 local attrs = {}
343 local dest = node.destination
344 if node.reference then
345 local ref = self.references[node.reference]
346 if ref then
347 if ref.attributes then
348 attrs = copy(ref.attributes)
349 end
350 dest = ref.destination
351 else
352 dest = "#" -- empty href is illegal
353 end
354 end
355 -- image's attributes override reference's:
356 copy_attributes(attrs, node.attr)
357 return pandoc.Image(self:render_children(node), dest,
358 title, to_attr(attrs))
359end
360
361function Renderer:span(node)
362 return pandoc.Span(self:render_children(node), to_attr(node.attr))
363end
364
365function Renderer:mark(node)
366 local attr = copy(node.attr)
367 if attr.class then
368 attr.class = "mark " .. attr.class
369 else
370 attr = { class = "mark" }
371 end
372 return pandoc.Span(self:render_children(node), to_attr(attr))
373end
374
375function Renderer:insert(node)
376 local attr = copy(node.attr)
377 if attr.class then
378 attr.class = "insert " .. attr.class
379 else
380 attr = { class = "insert" }
381 end
382 return pandoc.Span(self:render_children(node), to_attr(attr))
383end
384
385function Renderer:delete(node)
386 return self:with_optional_span(node, pandoc.Strikeout)
387end
388
389function Renderer:subscript(node)
390 return self:with_optional_span(node, pandoc.Subscript)
391end
392
393function Renderer:superscript(node)
394 return self:with_optional_span(node, pandoc.Superscript)
395end
396
397function Renderer:emph(node)
398 return self:with_optional_span(node, pandoc.Emph)
399end
400
401function Renderer:strong(node)
402 return self:with_optional_span(node, pandoc.Strong)
403end
404
405function Renderer:double_quoted(node)
406 return self:with_optional_span(node,
407 function(x) return pandoc.Quoted("DoubleQuote", x) end)
408end
409
410function Renderer:single_quoted(node)
411 return self:with_optional_span(node,
412 function(x) return pandoc.Quoted("SingleQuote", x) end)
413end
414
415function Renderer:left_double_quote()
416 return "“"
417end
418
419function Renderer:right_double_quote()
420 return "”"
421end
422
423function Renderer:left_single_quote()
424 return "‘"
425end
426
427function Renderer:right_single_quote()
428 return "’"
429end
430
431function Renderer:ellipses()
432 return "…"
433end
434
435function Renderer:em_dash()
436 return "—"
437end
438
439function Renderer:en_dash()
440 return "–"
441end
442
443function Renderer:symbol(node)
444 return pandoc.Span(":" .. node.alias .. ":",
445 pandoc.Attr("",{"symbol"},{["alias"] = node.alias}))
446end
447
448function Renderer:math(node)
449 local math_type = "InlineMath"
450 if find(node.attr.class, "display") then
451 math_type = "DisplayMath"
452 end
453 return pandoc.Math(math_type, node.text)
454end
455
456function Reader(input)
457 local doc = djot.parse(tostring(input))
458 return Renderer:render_node(doc)
459end