djot 0.0.1

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