djot 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +20 -0
- data/CHANGELOG.md +7 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +54 -0
- data/Rakefile +40 -0
- data/Steepfile +10 -0
- data/djot.gemspec +33 -0
- data/lib/djot/version.rb +5 -0
- data/lib/djot.rb +42 -0
- data/lib/lua/djot/ast.lua +642 -0
- data/lib/lua/djot/attributes.lua +273 -0
- data/lib/lua/djot/block.lua +807 -0
- data/lib/lua/djot/emoji.lua +1880 -0
- data/lib/lua/djot/html.lua +557 -0
- data/lib/lua/djot/inline.lua +641 -0
- data/lib/lua/djot/match.lua +75 -0
- data/lib/lua/djot.lua +107 -0
- data/sig/djot.rbs +6 -0
- metadata +81 -0
@@ -0,0 +1,642 @@
|
|
1
|
+
local match = require("djot.match")
|
2
|
+
local unpack = unpack or table.unpack
|
3
|
+
|
4
|
+
local find, lower, sub, gsub, rep, format =
|
5
|
+
string.find, string.lower, string.sub, string.gsub, string.rep, string.format
|
6
|
+
|
7
|
+
local unpack_match, get_length, matches_pattern =
|
8
|
+
match.unpack_match, match.get_length, match.matches_pattern
|
9
|
+
|
10
|
+
local function get_string_content(node)
|
11
|
+
local buffer = {}
|
12
|
+
for i=2,#node do
|
13
|
+
local n = node[i]
|
14
|
+
if type(n) ~= "table" then
|
15
|
+
break
|
16
|
+
elseif n[1] == "str" or n[1] == "nbsp" then
|
17
|
+
buffer[#buffer + 1] = n[2]
|
18
|
+
elseif n[1] == "softbreak" then
|
19
|
+
buffer[#buffer + 1] = "\n"
|
20
|
+
else
|
21
|
+
buffer[#buffer + 1] = get_string_content(n)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
return table.concat(buffer)
|
25
|
+
end
|
26
|
+
|
27
|
+
local roman_digits = {
|
28
|
+
i = 1,
|
29
|
+
v = 5,
|
30
|
+
x = 10,
|
31
|
+
l = 50,
|
32
|
+
c = 100,
|
33
|
+
d = 500,
|
34
|
+
m = 1000 }
|
35
|
+
|
36
|
+
local function roman_to_number(s)
|
37
|
+
-- go backwards through the digits
|
38
|
+
local total = 0
|
39
|
+
local prevdigit = 0
|
40
|
+
local i=#s
|
41
|
+
while i > 0 do
|
42
|
+
local c = lower(sub(s,i,i))
|
43
|
+
local n = roman_digits[c]
|
44
|
+
if n < prevdigit then -- e.g. ix
|
45
|
+
total = total - n
|
46
|
+
else
|
47
|
+
total = total + n
|
48
|
+
end
|
49
|
+
assert(n ~= nil, "Encountered bad character in roman numeral " .. s)
|
50
|
+
prevdigit = n
|
51
|
+
i = i - 1
|
52
|
+
end
|
53
|
+
return total
|
54
|
+
end
|
55
|
+
|
56
|
+
local function get_list_start(marker, style)
|
57
|
+
local numtype = string.gsub(style, "%p", "")
|
58
|
+
local s = string.gsub(marker, "%p", "")
|
59
|
+
if numtype == "1" then
|
60
|
+
return tonumber(s)
|
61
|
+
elseif numtype == "A" then
|
62
|
+
return (string.byte(s) - string.byte("A") + 1)
|
63
|
+
elseif numtype == "a" then
|
64
|
+
return (string.byte(s) - string.byte("a") + 1)
|
65
|
+
elseif numtype == "I" then
|
66
|
+
return roman_to_number(s)
|
67
|
+
elseif numtype == "i" then
|
68
|
+
return roman_to_number(s)
|
69
|
+
elseif numtype == "" then
|
70
|
+
return nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
local ignorable = {
|
75
|
+
image_marker = true,
|
76
|
+
escape = true,
|
77
|
+
blankline = true,
|
78
|
+
checkbox_checked = true,
|
79
|
+
checkbox_unchecked = true
|
80
|
+
}
|
81
|
+
|
82
|
+
local function is_tight(matches, startidx, endidx, is_last_item)
|
83
|
+
-- see if there are any blank lines between blocks in a list item.
|
84
|
+
local blanklines = 0
|
85
|
+
-- we don't care about blank lines at very end of list
|
86
|
+
for i=startidx, endidx do
|
87
|
+
local _, _, x = unpack_match(matches[i])
|
88
|
+
if x == "blankline" then
|
89
|
+
if matches_pattern(matches[i+1], "%+list_item") then
|
90
|
+
-- don't count blank lines before list starts
|
91
|
+
elseif matches_pattern(matches[i+1], "%-list_item") and
|
92
|
+
(is_last_item or matches_pattern(matches[i+2], "%-list_item")) then
|
93
|
+
-- don't count blank lines at end of nested lists
|
94
|
+
-- or end of last item
|
95
|
+
else
|
96
|
+
blanklines = blanklines + 1
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
return (blanklines == 0)
|
101
|
+
end
|
102
|
+
|
103
|
+
local function insert_attribute(attr, key, val)
|
104
|
+
if not attr._keys then
|
105
|
+
attr._keys = {}
|
106
|
+
end
|
107
|
+
local function add_key(k)
|
108
|
+
local keys = attr._keys
|
109
|
+
for i=1,#keys do
|
110
|
+
if keys[i] == k then
|
111
|
+
return
|
112
|
+
end
|
113
|
+
end
|
114
|
+
keys[#keys + 1] = k
|
115
|
+
end
|
116
|
+
-- _keys records order of key insertion for deterministic output
|
117
|
+
if key == "id" then
|
118
|
+
attr.id = val
|
119
|
+
add_key("id")
|
120
|
+
elseif key == "class" then
|
121
|
+
if attr.class then
|
122
|
+
attr.class =
|
123
|
+
attr.class .. " " .. val
|
124
|
+
else
|
125
|
+
attr.class = val
|
126
|
+
add_key("class")
|
127
|
+
end
|
128
|
+
else
|
129
|
+
attr[key] = val
|
130
|
+
add_key(key)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
local function copy_attributes(target, source)
|
135
|
+
if source then
|
136
|
+
for k,v in pairs(source) do
|
137
|
+
if k ~= "_keys" then
|
138
|
+
insert_attribute(target, k, v)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
local function insert_attributes(targetnode, attrnode)
|
145
|
+
targetnode.attr = targetnode.attr or {_keys = {}}
|
146
|
+
local i=2
|
147
|
+
while i <= #attrnode do
|
148
|
+
local x,y = unpack(attrnode[i])
|
149
|
+
if x == "id" or x == "class" then
|
150
|
+
insert_attribute(targetnode.attr, x, y)
|
151
|
+
elseif x == "key" then
|
152
|
+
local valnode = attrnode[i + 1]
|
153
|
+
if valnode[1] == "value" then
|
154
|
+
-- resolve backslash escapes
|
155
|
+
insert_attribute(targetnode.attr, y, valnode[2]:gsub("\\(%p)", "%1"))
|
156
|
+
end
|
157
|
+
i = i + 1
|
158
|
+
end
|
159
|
+
i = i + 1
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
local function make_definition_list_item(result)
|
164
|
+
assert(result[1] and result[1][1] ~= "list_item", "sanity check")
|
165
|
+
result[1] = "definition_list_item"
|
166
|
+
if result[2] and result[2][1] == "para" then
|
167
|
+
result[2][1] = "term"
|
168
|
+
else
|
169
|
+
table.insert(result, 2, {"term"})
|
170
|
+
end
|
171
|
+
if result[3] then
|
172
|
+
local defn = {"definition"}
|
173
|
+
for i=3,#result do
|
174
|
+
defn[#defn + 1] = result[i]
|
175
|
+
result[i] = nil
|
176
|
+
end
|
177
|
+
result[3] = defn
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
-- create an abstract syntax tree based on an event
|
182
|
+
-- stream and references
|
183
|
+
local function to_ast(subject, matches, options)
|
184
|
+
if not options then
|
185
|
+
options = {}
|
186
|
+
end
|
187
|
+
local idx = 1
|
188
|
+
local matcheslen = #matches
|
189
|
+
local sourcepos = options.sourcepos
|
190
|
+
local references = {}
|
191
|
+
local footnotes = {}
|
192
|
+
local identifiers = {} -- identifiers used (to ensure uniqueness)
|
193
|
+
|
194
|
+
-- generate auto identifier for heading
|
195
|
+
local function get_identifier(s)
|
196
|
+
local base = s:gsub("[][~!@#$%^&*(){}`,.<>\\|=+/?]","")
|
197
|
+
:gsub("^%s+",""):gsub("%s+$","")
|
198
|
+
:gsub("%s+","-")
|
199
|
+
local suffix = ""
|
200
|
+
local i = 0
|
201
|
+
local ident = base
|
202
|
+
-- generate unique id
|
203
|
+
while identifiers[ident] do
|
204
|
+
i = i + 1
|
205
|
+
ident = base .. tostring(i)
|
206
|
+
end
|
207
|
+
identifiers[ident] = true
|
208
|
+
return ident
|
209
|
+
end
|
210
|
+
|
211
|
+
local function set_checkbox(node, startidx)
|
212
|
+
-- determine if checked or unchecked
|
213
|
+
local _,_,ann = unpack_match(matches[startidx + 1])
|
214
|
+
if ann == "checkbox_checked" then
|
215
|
+
node.checkbox = "checked"
|
216
|
+
elseif ann == "checkbox_unchecked" then
|
217
|
+
node.checkbox = "unchecked"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
local function get_node(maintag)
|
222
|
+
local nodes = {maintag}
|
223
|
+
local stopper
|
224
|
+
local block_attributes = nil
|
225
|
+
if maintag then
|
226
|
+
-- strip off data (e.g. for list_items)
|
227
|
+
stopper = "^%-" .. gsub(maintag, "%[.*$", "")
|
228
|
+
end
|
229
|
+
while idx <= matcheslen do
|
230
|
+
local match = matches[idx]
|
231
|
+
local startpos, endpos, annot = unpack_match(match)
|
232
|
+
if stopper and find(annot, stopper) then
|
233
|
+
idx = idx + 1
|
234
|
+
return nodes
|
235
|
+
else
|
236
|
+
local mod, tag = string.match(annot, "^([-+]?)(.*)")
|
237
|
+
if ignorable[tag] then
|
238
|
+
idx = idx + 1 -- skip
|
239
|
+
elseif mod == "+" then -- open
|
240
|
+
local startidx = idx
|
241
|
+
idx = idx + 1
|
242
|
+
local result = get_node(tag)
|
243
|
+
if tag == "list_item[X]" then
|
244
|
+
set_checkbox(result, startidx)
|
245
|
+
end
|
246
|
+
local _, finalpos = unpack_match(matches[idx - 1])
|
247
|
+
if sourcepos then
|
248
|
+
result.pos = {startpos, finalpos}
|
249
|
+
end
|
250
|
+
if block_attributes and tag ~= "block_attributes" then
|
251
|
+
for i=1,#block_attributes do
|
252
|
+
insert_attributes(result, block_attributes[i])
|
253
|
+
end
|
254
|
+
if result.attr and result.attr.id then
|
255
|
+
identifiers[result.attr.id] = true
|
256
|
+
end
|
257
|
+
block_attributes = nil
|
258
|
+
end
|
259
|
+
if tag == "verbatim" then
|
260
|
+
local s = get_string_content(result)
|
261
|
+
-- trim space next to ` at beginning or end
|
262
|
+
if find(s, "^ +`") then
|
263
|
+
s = s:sub(2)
|
264
|
+
end
|
265
|
+
if find(s, "` +$") then
|
266
|
+
s = s:sub(1, #s - 1)
|
267
|
+
end
|
268
|
+
result = {"verbatim", {"str", s}}
|
269
|
+
-- check for raw_format, which makes this a raw node
|
270
|
+
local sp,ep,ann = unpack_match(matches[idx])
|
271
|
+
if ann == "raw_format" then
|
272
|
+
local s = get_string_content(result)
|
273
|
+
result = {"raw_inline", s}
|
274
|
+
result.format = sub(subject, sp + 2, ep - 1)
|
275
|
+
idx = idx + 1 -- skip the raw_format
|
276
|
+
end
|
277
|
+
elseif tag == "caption" then
|
278
|
+
if nodes[#nodes][1] == "table" then
|
279
|
+
-- move caption in table node
|
280
|
+
table.insert(nodes[#nodes], 2, result)
|
281
|
+
result = nil
|
282
|
+
end
|
283
|
+
elseif tag == "reference_definition" then
|
284
|
+
local dest = ""
|
285
|
+
local key
|
286
|
+
for i=2,#result do
|
287
|
+
if result[i][1] == "reference_key" then
|
288
|
+
key = result[i][2]
|
289
|
+
end
|
290
|
+
if result[i][1] == "reference_value" then
|
291
|
+
dest = dest .. result[i][2]
|
292
|
+
end
|
293
|
+
end
|
294
|
+
references[key] = { destination = dest,
|
295
|
+
attributes = result.attr }
|
296
|
+
elseif tag == "footnote" then
|
297
|
+
local label
|
298
|
+
if result[2][1] == "note_label" then
|
299
|
+
label = result[2][2]
|
300
|
+
end
|
301
|
+
if label then
|
302
|
+
table.remove(result,2)
|
303
|
+
footnotes[label] = result
|
304
|
+
end
|
305
|
+
result = nil
|
306
|
+
elseif tag == "inline_math" then
|
307
|
+
result[1] = "math"
|
308
|
+
result.attr = {class = "math inline", _keys={"class"}}
|
309
|
+
elseif tag == "display_math" then
|
310
|
+
result[1] = "math"
|
311
|
+
result.attr = {class = "math display", _keys={"class"}}
|
312
|
+
elseif tag == "url" then
|
313
|
+
result[1] = "link"
|
314
|
+
result.destination = get_string_content(result)
|
315
|
+
elseif tag == "email" then
|
316
|
+
result[1] = "link"
|
317
|
+
result.destination = "mailto:" .. get_string_content(result)
|
318
|
+
elseif tag == "imagetext" or tag == "linktext" then
|
319
|
+
-- gobble destination or reference
|
320
|
+
local nextmatch = matches[idx]
|
321
|
+
local _, _, nextannot = unpack_match(nextmatch)
|
322
|
+
if nextannot == "+destination" then
|
323
|
+
idx = idx + 1
|
324
|
+
local dest = get_node("destination")
|
325
|
+
result.destination = get_string_content(dest):gsub("\r?\n", "")
|
326
|
+
elseif nextannot == "+reference" then
|
327
|
+
idx = idx + 1
|
328
|
+
local ref = get_node("reference")
|
329
|
+
if #ref == 1 then -- []
|
330
|
+
result.reference = get_string_content(result):gsub("\r?\n", " ")
|
331
|
+
else
|
332
|
+
result.reference = get_string_content(ref):gsub("\r?\n", " ")
|
333
|
+
end
|
334
|
+
end
|
335
|
+
result[1] = result[1]:gsub("text","")
|
336
|
+
elseif tag == "heading" then
|
337
|
+
result.level = get_length(match)
|
338
|
+
local heading_str = get_string_content(result)
|
339
|
+
:gsub("^%s+",""):gsub("%s+$","")
|
340
|
+
if not (result.attr and result.attr.id) then
|
341
|
+
local ident = get_identifier(heading_str)
|
342
|
+
insert_attributes(result, {nil,{"id", ident}})
|
343
|
+
end
|
344
|
+
-- insert into references unless there's a same-named one already:
|
345
|
+
if not references[heading_str] then
|
346
|
+
references[heading_str] =
|
347
|
+
{destination = "#" .. result.attr.id, attributes = {_keys={}}}
|
348
|
+
end
|
349
|
+
elseif tag == "table" then
|
350
|
+
-- look for a separator line
|
351
|
+
-- if found, make the preceding rows headings
|
352
|
+
-- and set attributes for column alignments on the table
|
353
|
+
local i=2
|
354
|
+
local aligns = {}
|
355
|
+
while i <= #result do
|
356
|
+
local found, align
|
357
|
+
if result[i][1] == "row" then
|
358
|
+
local row = result[i]
|
359
|
+
for j=2,#row do
|
360
|
+
found, _, align = find(row[j][1], "^separator_(.*)")
|
361
|
+
if not found then
|
362
|
+
break
|
363
|
+
end
|
364
|
+
aligns[j - 1] = align
|
365
|
+
end
|
366
|
+
if found and #aligns > 0 then
|
367
|
+
-- set previous row to head and adjust aligns
|
368
|
+
local prevrow = result[i - 1]
|
369
|
+
if prevrow[1] == "row" then
|
370
|
+
prevrow.head = true
|
371
|
+
for k=2,#prevrow do
|
372
|
+
-- set head on cells too
|
373
|
+
prevrow[k].head = true
|
374
|
+
if aligns[k - 1] ~= "default" then
|
375
|
+
prevrow[k].align = aligns[k - 1]
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
table.remove(result,i) -- remove sep line
|
380
|
+
-- we don't need to increment i because we removed ith elt
|
381
|
+
else
|
382
|
+
if #aligns > 0 then
|
383
|
+
for l=2,#result[i] do
|
384
|
+
if aligns[l - 1] ~= "default" then
|
385
|
+
result[i][l].align = aligns[l - 1]
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
i = i + 1
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
result.level = get_length(match)
|
394
|
+
elseif tag == "div" then
|
395
|
+
if result[2] and result[2][1] == "class" then
|
396
|
+
result.attr = result.attr or {_keys = {}}
|
397
|
+
insert_attribute(result.attr, "class", result[2][2])
|
398
|
+
table.remove(result, 2)
|
399
|
+
end
|
400
|
+
elseif tag == "code_block" then
|
401
|
+
if result[2] then
|
402
|
+
if result[2][1] == "code_language" then
|
403
|
+
result.lang = result[2][2]
|
404
|
+
table.remove(result, 2)
|
405
|
+
elseif result[2][1] == "raw_format" then
|
406
|
+
local fmt = result[2][2]:sub(2)
|
407
|
+
local s = get_string_content(result)
|
408
|
+
result = {"raw_block", s}
|
409
|
+
result.format = fmt
|
410
|
+
end
|
411
|
+
end
|
412
|
+
elseif tag == "block_attributes" then
|
413
|
+
if block_attributes then
|
414
|
+
block_attributes[#block_attributes + 1] = result
|
415
|
+
else
|
416
|
+
block_attributes = {result}
|
417
|
+
end
|
418
|
+
result = nil
|
419
|
+
elseif tag == "attributes" then
|
420
|
+
-- parse attributes, add to last node
|
421
|
+
local prevnode = nodes[#nodes]
|
422
|
+
local endswithspace = false
|
423
|
+
if type(prevnode) == "table" then
|
424
|
+
if prevnode[1] == "str" then
|
425
|
+
-- split off last consecutive word of string
|
426
|
+
-- to which to attach attributes
|
427
|
+
local lastwordpos = string.find(prevnode[2], "%w+$")
|
428
|
+
if not lastwordpos then
|
429
|
+
endswithspace = true
|
430
|
+
elseif lastwordpos > 1 then
|
431
|
+
local newnode = {"str", sub(prevnode[2], lastwordpos, -1)}
|
432
|
+
prevnode[2] = sub(prevnode[2], 1, lastwordpos - 1)
|
433
|
+
nodes[#nodes + 1] = newnode
|
434
|
+
prevnode = newnode
|
435
|
+
end
|
436
|
+
end
|
437
|
+
if not endswithspace then
|
438
|
+
insert_attributes(prevnode, result)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
result = nil
|
442
|
+
elseif find(tag, "^list_item") then
|
443
|
+
local marker = string.match(subject, "^%S+", startpos)
|
444
|
+
local styles = {}
|
445
|
+
gsub(tag, "%[([^]]*)%]", function(x) styles[#styles + 1] = x end)
|
446
|
+
-- create a list node with the consecutive list items
|
447
|
+
-- of the same kind
|
448
|
+
local list = {"list", result}
|
449
|
+
-- put the attributes from the first item on the list itself:
|
450
|
+
list.attr = result.attr
|
451
|
+
result.attr = nil
|
452
|
+
result[1] = "list_item"
|
453
|
+
if marker == ":" then
|
454
|
+
make_definition_list_item(result)
|
455
|
+
end
|
456
|
+
if sourcepos then
|
457
|
+
list.pos = {result.pos[1], result.pos[2]}
|
458
|
+
end
|
459
|
+
-- now get remaining items
|
460
|
+
local nextitem = matches[idx]
|
461
|
+
while nextitem do
|
462
|
+
local sp, _, ann = unpack_match(nextitem)
|
463
|
+
if not find(ann, "^%+list_item") then
|
464
|
+
break
|
465
|
+
end
|
466
|
+
-- check which of the styles this item matches
|
467
|
+
local newstyles = {}
|
468
|
+
gsub(ann, "%[([^]]*)%]",
|
469
|
+
function(x) newstyles[x] = true end)
|
470
|
+
local matched_styles = {}
|
471
|
+
for _,x in ipairs(styles) do
|
472
|
+
if newstyles[x] then
|
473
|
+
matched_styles[#matched_styles + 1] = x
|
474
|
+
end
|
475
|
+
end
|
476
|
+
if #styles > 0 and #matched_styles == 0 then
|
477
|
+
break -- does not match any styles
|
478
|
+
end
|
479
|
+
styles = matched_styles
|
480
|
+
-- at this point styles contains the styles that match all items
|
481
|
+
-- in the list so far...
|
482
|
+
|
483
|
+
list[#list].tight = is_tight(matches, startidx, idx - 1, false)
|
484
|
+
startidx = idx
|
485
|
+
idx = idx + 1
|
486
|
+
local item = get_node(tag)
|
487
|
+
if tag == "list_item[X]" then
|
488
|
+
set_checkbox(item, startidx)
|
489
|
+
end
|
490
|
+
item[1] = "list_item"
|
491
|
+
if sourcepos then
|
492
|
+
item.pos = {sp, finalpos}
|
493
|
+
list.pos[2] = item.pos[2]
|
494
|
+
end
|
495
|
+
if marker == ":" then
|
496
|
+
make_definition_list_item(item)
|
497
|
+
end
|
498
|
+
list[#list + 1] = item
|
499
|
+
nextitem = matches[idx]
|
500
|
+
end
|
501
|
+
list[#list].tight = is_tight(matches, startidx, idx - 1, true)
|
502
|
+
local tight = true
|
503
|
+
for i=2,#list do
|
504
|
+
tight = tight and list[i].tight
|
505
|
+
list[i].tight = nil
|
506
|
+
end
|
507
|
+
list.list_style = styles[1] -- resolve, if still ambiguous
|
508
|
+
list.tight = tight
|
509
|
+
list.start = get_list_start(marker, list.list_style)
|
510
|
+
result = list
|
511
|
+
end
|
512
|
+
nodes[#nodes + 1] = result
|
513
|
+
elseif mod == "-" then -- close
|
514
|
+
assert(false, "unmatched " .. annot .. " encountered at byte " ..
|
515
|
+
startpos)
|
516
|
+
idx = idx + 1
|
517
|
+
return nil
|
518
|
+
elseif tag == "reference_key" then
|
519
|
+
local key = sub(subject, startpos + 1, endpos - 1)
|
520
|
+
local result = {tag, key}
|
521
|
+
idx = idx + 1
|
522
|
+
nodes[#nodes + 1] = result
|
523
|
+
elseif tag == "reference_value" then
|
524
|
+
local val = sub(subject, startpos, endpos)
|
525
|
+
local result = {tag, val}
|
526
|
+
idx = idx + 1
|
527
|
+
nodes[#nodes + 1] = result
|
528
|
+
else -- leaf
|
529
|
+
local result
|
530
|
+
if tag == "softbreak" then
|
531
|
+
result = {tag}
|
532
|
+
elseif tag == "footnote_reference" then
|
533
|
+
result = {tag, sub(subject, startpos + 2, endpos - 1)}
|
534
|
+
else
|
535
|
+
result = {tag, sub(subject, startpos, endpos)}
|
536
|
+
end
|
537
|
+
if sourcepos then
|
538
|
+
result.pos = {startpos, endpos}
|
539
|
+
end
|
540
|
+
if block_attributes then
|
541
|
+
for i=1,#block_attributes do
|
542
|
+
insert_attributes(result, block_attributes[i])
|
543
|
+
end
|
544
|
+
block_attributes = nil
|
545
|
+
end
|
546
|
+
idx = idx + 1
|
547
|
+
if result then
|
548
|
+
nodes[#nodes + 1] = result
|
549
|
+
end
|
550
|
+
end
|
551
|
+
end
|
552
|
+
end
|
553
|
+
return nodes
|
554
|
+
end
|
555
|
+
|
556
|
+
local doc = get_node("doc")
|
557
|
+
doc.references = references
|
558
|
+
doc.footnotes = footnotes
|
559
|
+
return doc
|
560
|
+
end
|
561
|
+
|
562
|
+
local function render_nodes(nodes, handle, init, indent)
|
563
|
+
indent = indent or 0
|
564
|
+
init = init or 1
|
565
|
+
for i=init,#nodes do
|
566
|
+
local node = nodes[i]
|
567
|
+
handle:write(rep(" ", indent))
|
568
|
+
if type(node) == "string" then
|
569
|
+
handle:write(format("%q",node))
|
570
|
+
else
|
571
|
+
handle:write(node[1])
|
572
|
+
if node.pos then
|
573
|
+
handle:write(format(" (%d-%d)", node.pos[1], node.pos[2]))
|
574
|
+
end
|
575
|
+
for k,v in pairs(node) do
|
576
|
+
if type(k) == "string" and k ~= "pos" and k ~= "attr" then
|
577
|
+
handle:write(format(" %s=%q", k, tostring(v)))
|
578
|
+
end
|
579
|
+
end
|
580
|
+
if node.attr then
|
581
|
+
local keys = node.attr._keys
|
582
|
+
for j=1,#keys do
|
583
|
+
local k = keys[j]
|
584
|
+
handle:write(format(" %s=%q", k, node.attr[k]))
|
585
|
+
end
|
586
|
+
end
|
587
|
+
end
|
588
|
+
handle:write("\n")
|
589
|
+
if node[2] then -- children
|
590
|
+
render_nodes(node, handle, 2, indent + 2)
|
591
|
+
end
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
local function render(doc, handle)
|
596
|
+
render_nodes(doc, handle, 2, 0)
|
597
|
+
if doc.references then
|
598
|
+
handle:write("references = {\n")
|
599
|
+
for k,v in pairs(doc.references) do
|
600
|
+
handle:write(format(" [%q] = %q,\n", k, v.destination))
|
601
|
+
end
|
602
|
+
handle:write("}\n")
|
603
|
+
end
|
604
|
+
if doc.footnotes then
|
605
|
+
handle:write("footnotes = {\n")
|
606
|
+
for k,v in pairs(doc.footnotes) do
|
607
|
+
handle:write(format(" [%q] =\n", k))
|
608
|
+
render_nodes(v, handle, 2, 4)
|
609
|
+
end
|
610
|
+
handle:write("}\n")
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
return { to_ast = to_ast,
|
615
|
+
render = render,
|
616
|
+
insert_attribute = insert_attribute,
|
617
|
+
copy_attributes = copy_attributes }
|
618
|
+
|
619
|
+
|
620
|
+
--[[
|
621
|
+
Copyright (C) 2022 John MacFarlane
|
622
|
+
|
623
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
624
|
+
a copy of this software and associated documentation files (the
|
625
|
+
"Software"), to deal in the Software without restriction, including
|
626
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
627
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
628
|
+
permit persons to whom the Software is furnished to do so, subject to
|
629
|
+
the following conditions:
|
630
|
+
|
631
|
+
The above copyright notice and this permission notice shall be included
|
632
|
+
in all copies or substantial portions of the Software.
|
633
|
+
|
634
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
635
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
636
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
637
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
638
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
639
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
640
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
641
|
+
|
642
|
+
]]
|