djot 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,807 @@
|
|
1
|
+
local inline = require("djot.inline")
|
2
|
+
local attributes = require("djot.attributes")
|
3
|
+
local match = require("djot.match")
|
4
|
+
local make_match, unpack_match = match.make_match, match.unpack_match
|
5
|
+
local unpack = unpack or table.unpack
|
6
|
+
local find, sub, byte = string.find, string.sub, string.byte
|
7
|
+
|
8
|
+
local Container = {}
|
9
|
+
|
10
|
+
function Container:new(spec, data)
|
11
|
+
local contents = {}
|
12
|
+
setmetatable(contents, spec)
|
13
|
+
spec.__index = spec
|
14
|
+
if data then
|
15
|
+
for k,v in pairs(data) do
|
16
|
+
contents[k] = v
|
17
|
+
end
|
18
|
+
end
|
19
|
+
return contents
|
20
|
+
end
|
21
|
+
|
22
|
+
local function get_list_style(marker)
|
23
|
+
if marker == "+" or marker == "-" or marker == "*" or marker == ":" then
|
24
|
+
return marker
|
25
|
+
elseif find(marker, "^[+*-] %[[Xx ]%]") then
|
26
|
+
return "X" -- task list
|
27
|
+
elseif find(marker, "^%[[Xx ]%]") then
|
28
|
+
return "X"
|
29
|
+
elseif find(marker, "^[(]?%d+[).]") then
|
30
|
+
return (marker:gsub("%d+","1"))
|
31
|
+
-- in ambiguous cases we return two values
|
32
|
+
elseif find(marker, "^[(]?[ivxlcdm][).]") then
|
33
|
+
return (marker:gsub("%a+", "a")), (marker:gsub("%a+", "i"))
|
34
|
+
elseif find(marker, "^[(]?[IVXLCDM][).]") then
|
35
|
+
return (marker:gsub("%a+", "A")), (marker:gsub("%a+", "I"))
|
36
|
+
elseif find(marker, "^[(]?%l[).]") then
|
37
|
+
return (marker:gsub("%l", "a"))
|
38
|
+
elseif find(marker, "^[(]?%u[).]") then
|
39
|
+
return (marker:gsub("%u", "A"))
|
40
|
+
elseif find(marker, "^[(]?[ivxlcdm]+[).]") then
|
41
|
+
return (marker:gsub("%a+", "i"))
|
42
|
+
elseif find(marker, "^[(]?[IVXLCDM]+[).]") then
|
43
|
+
return (marker:gsub("%a+", "I"))
|
44
|
+
else
|
45
|
+
assert(false, "Could not identify list style for " .. marker)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
local Parser = {}
|
50
|
+
|
51
|
+
function Parser:new(subject, opts)
|
52
|
+
-- ensure the subject ends with a newline character
|
53
|
+
if not subject:find("[\r\n]$") then
|
54
|
+
subject = subject .. "\n"
|
55
|
+
end
|
56
|
+
local state = {
|
57
|
+
subject = subject,
|
58
|
+
indent = 0,
|
59
|
+
startline = nil,
|
60
|
+
starteol = nil,
|
61
|
+
endeol = nil,
|
62
|
+
matches = {},
|
63
|
+
containers = {},
|
64
|
+
pos = 1,
|
65
|
+
last_matched_container = 0,
|
66
|
+
timer = {},
|
67
|
+
warnings = {},
|
68
|
+
opts = opts or {},
|
69
|
+
finished_line = false }
|
70
|
+
setmetatable(state, self)
|
71
|
+
self.__index = self
|
72
|
+
return state
|
73
|
+
end
|
74
|
+
|
75
|
+
-- parameters are start and end position
|
76
|
+
function Parser:parse_table_row(sp, ep)
|
77
|
+
local orig_matches = #self.matches -- so we can rewind
|
78
|
+
local startpos = self.pos
|
79
|
+
self:add_match(sp, sp, "+row")
|
80
|
+
-- skip | and any initial space in the cell:
|
81
|
+
self.pos = find(self.subject, "%S", sp + 1)
|
82
|
+
-- check to see if we have a separator line
|
83
|
+
local seps = {}
|
84
|
+
local p = self.pos
|
85
|
+
local sepfound = false
|
86
|
+
while not sepfound do
|
87
|
+
local sepsp, sepep, left, right, trailing =
|
88
|
+
find(self.subject, "^(%:?)%-%-*(%:?)([ \t]*%|[ \t]*)", p)
|
89
|
+
if sepep then
|
90
|
+
local st = "separator_default"
|
91
|
+
if #left > 0 and #right > 0 then
|
92
|
+
st = "separator_center"
|
93
|
+
elseif #right > 0 then
|
94
|
+
st = "separator_right"
|
95
|
+
elseif #left > 0 then
|
96
|
+
st = "separator_left"
|
97
|
+
end
|
98
|
+
seps[#seps + 1] = {sepsp, sepep - #trailing, st}
|
99
|
+
p = sepep + 1
|
100
|
+
if p == self.starteol then
|
101
|
+
sepfound = true
|
102
|
+
break
|
103
|
+
end
|
104
|
+
else
|
105
|
+
break
|
106
|
+
end
|
107
|
+
end
|
108
|
+
if sepfound then
|
109
|
+
for i=1,#seps do
|
110
|
+
self:add_match(unpack(seps[i]))
|
111
|
+
end
|
112
|
+
self:add_match(self.starteol - 1, self.starteol - 1, "-row")
|
113
|
+
self.pos = self.starteol
|
114
|
+
self.finished_line = true
|
115
|
+
return true
|
116
|
+
end
|
117
|
+
local inline_parser = inline.Parser:new(self.subject, self.opts)
|
118
|
+
self:add_match(sp, sp, "+cell")
|
119
|
+
while self.pos < ep do
|
120
|
+
-- parse a chunk as inline content
|
121
|
+
local _,nextbar = self:find("^[^|\r\n]*|")
|
122
|
+
inline_parser:feed(self.pos, nextbar - 1)
|
123
|
+
if inline_parser:in_verbatim() then
|
124
|
+
-- read the next | as part of verbatim
|
125
|
+
inline_parser:feed(nextbar, nextbar)
|
126
|
+
self.pos = nextbar + 1
|
127
|
+
else
|
128
|
+
self.pos = nextbar + 1 -- skip past the next |
|
129
|
+
-- add a table cell
|
130
|
+
local cell_matches = inline_parser:get_matches()
|
131
|
+
for i=1,#cell_matches do
|
132
|
+
local s,e,ann = unpack_match(cell_matches[i])
|
133
|
+
if i == #cell_matches and ann == "str" then
|
134
|
+
-- strip trailing space
|
135
|
+
while byte(self.subject, e) == 32 and e >= s do
|
136
|
+
e = e - 1
|
137
|
+
end
|
138
|
+
end
|
139
|
+
self:add_match(s,e,ann)
|
140
|
+
end
|
141
|
+
self:add_match(nextbar, nextbar, "-cell")
|
142
|
+
if nextbar < ep then
|
143
|
+
-- reset inline parser state
|
144
|
+
inline_parser = inline.Parser:new(self.subject, self.opts)
|
145
|
+
self:add_match(nextbar, nextbar, "+cell")
|
146
|
+
self.pos = find(self.subject, "%S", self.pos)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
if inline_parser:in_verbatim() then
|
151
|
+
-- rewind, this is not a valid table row
|
152
|
+
self.pos = startpos
|
153
|
+
for i = orig_matches,#self.matches do
|
154
|
+
self.matches[i] = nil
|
155
|
+
end
|
156
|
+
return false
|
157
|
+
else
|
158
|
+
self:add_match(self.pos, self.pos, "-row")
|
159
|
+
self.pos = self.starteol
|
160
|
+
self.finished_line = true
|
161
|
+
return true
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
function Parser:specs()
|
166
|
+
return {
|
167
|
+
{ name = "para",
|
168
|
+
is_para = true,
|
169
|
+
content = "inline",
|
170
|
+
continue = function()
|
171
|
+
return self:find("^%S")
|
172
|
+
end,
|
173
|
+
open = function(spec)
|
174
|
+
self:add_container(Container:new(spec,
|
175
|
+
{ inline_parser = inline.Parser:new(self.subject, self.opts) }))
|
176
|
+
self:add_match(self.pos, self.pos, "+para")
|
177
|
+
return true
|
178
|
+
end,
|
179
|
+
close = function()
|
180
|
+
self:get_inline_matches()
|
181
|
+
self:add_match(self.pos - 1, self.pos - 1, "-para")
|
182
|
+
self.containers[#self.containers] = nil
|
183
|
+
end
|
184
|
+
},
|
185
|
+
|
186
|
+
{ name = "caption",
|
187
|
+
is_para = false,
|
188
|
+
content = "inline",
|
189
|
+
continue = function()
|
190
|
+
return self:find("^%S")
|
191
|
+
end,
|
192
|
+
open = function(spec)
|
193
|
+
local _, ep = self:find("^%^[ \t]+")
|
194
|
+
if ep then
|
195
|
+
self.pos = ep + 1
|
196
|
+
self:add_container(Container:new(spec,
|
197
|
+
{ inline_parser = inline.Parser:new(self.subject, self.opts) }))
|
198
|
+
self:add_match(self.pos, self.pos, "+caption")
|
199
|
+
return true
|
200
|
+
end
|
201
|
+
end,
|
202
|
+
close = function()
|
203
|
+
self:get_inline_matches()
|
204
|
+
self:add_match(self.pos - 1, self.pos - 1, "-caption")
|
205
|
+
self.containers[#self.containers] = nil
|
206
|
+
end
|
207
|
+
},
|
208
|
+
|
209
|
+
{ name = "blockquote",
|
210
|
+
content = "block",
|
211
|
+
continue = function()
|
212
|
+
if self:find("^%>%s") then
|
213
|
+
self.pos = self.pos + 1
|
214
|
+
return true
|
215
|
+
else
|
216
|
+
return false
|
217
|
+
end
|
218
|
+
end,
|
219
|
+
open = function(spec)
|
220
|
+
if self:find("^%>%s") then
|
221
|
+
self:add_container(Container:new(spec))
|
222
|
+
self:add_match(self.pos, self.pos, "+blockquote")
|
223
|
+
self.pos = self.pos + 1
|
224
|
+
return true
|
225
|
+
end
|
226
|
+
end,
|
227
|
+
close = function()
|
228
|
+
self:add_match(self.pos, self.pos, "-blockquote")
|
229
|
+
self.containers[#self.containers] = nil
|
230
|
+
end
|
231
|
+
},
|
232
|
+
|
233
|
+
-- should go before reference definitions
|
234
|
+
{ name = "footnote",
|
235
|
+
content = "block",
|
236
|
+
continue = function(container)
|
237
|
+
if self.indent > container.indent or self:find("^[\r\n]") then
|
238
|
+
return true
|
239
|
+
else
|
240
|
+
return false
|
241
|
+
end
|
242
|
+
end,
|
243
|
+
open = function(spec)
|
244
|
+
local sp, ep, label = self:find("^%[%^([^]]+)%]:%s")
|
245
|
+
if not sp then
|
246
|
+
return nil
|
247
|
+
end
|
248
|
+
-- adding container will close others
|
249
|
+
self:add_container(Container:new(spec, {note_label = label,
|
250
|
+
indent = self.indent}))
|
251
|
+
self:add_match(sp, sp, "+footnote")
|
252
|
+
self:add_match(sp + 2, ep - 3, "note_label")
|
253
|
+
self.pos = ep
|
254
|
+
return true
|
255
|
+
end,
|
256
|
+
close = function(_container)
|
257
|
+
self:add_match(self.pos, self.pos, "-footnote")
|
258
|
+
self.containers[#self.containers] = nil
|
259
|
+
end
|
260
|
+
},
|
261
|
+
|
262
|
+
-- should go before list_item_spec
|
263
|
+
{ name = "thematic_break",
|
264
|
+
content = nil,
|
265
|
+
continue = function()
|
266
|
+
return false
|
267
|
+
end,
|
268
|
+
open = function(spec)
|
269
|
+
local sp, ep = self:find("^[-*][ \t]*[-*][ \t]*[-*][-* \t]*[\r\n]")
|
270
|
+
if ep then
|
271
|
+
self:add_container(Container:new(spec))
|
272
|
+
self:add_match(sp, ep, "thematic_break")
|
273
|
+
self.pos = ep
|
274
|
+
return true
|
275
|
+
end
|
276
|
+
end,
|
277
|
+
close = function(_container)
|
278
|
+
self.containers[#self.containers] = nil
|
279
|
+
end
|
280
|
+
},
|
281
|
+
|
282
|
+
{ name = "list_item",
|
283
|
+
content = "block",
|
284
|
+
continue = function(container)
|
285
|
+
if self.indent > container.indent or self:find("^[\r\n]") then
|
286
|
+
return true
|
287
|
+
else
|
288
|
+
return false
|
289
|
+
end
|
290
|
+
end,
|
291
|
+
open = function(spec)
|
292
|
+
local sp, ep = self:find("^[-*+:]%s")
|
293
|
+
if not sp then
|
294
|
+
sp, ep = self:find("^%d+[.)]%s")
|
295
|
+
end
|
296
|
+
if not sp then
|
297
|
+
sp, ep = self:find("^%(%d+%)%s")
|
298
|
+
end
|
299
|
+
if not sp then
|
300
|
+
sp, ep = self:find("^[ivxlcdmIVXLCDM]+[.)]%s")
|
301
|
+
end
|
302
|
+
if not sp then
|
303
|
+
sp, ep = self:find("^%([ivxlcdmIVXLCDM]+%)%s")
|
304
|
+
end
|
305
|
+
if not sp then
|
306
|
+
sp, ep = self:find("^%a[.)]%s")
|
307
|
+
end
|
308
|
+
if not sp then
|
309
|
+
sp, ep = self:find("^%(%a%)%s")
|
310
|
+
end
|
311
|
+
if not sp then
|
312
|
+
return nil
|
313
|
+
end
|
314
|
+
local marker = sub(self.subject, sp, ep - 1)
|
315
|
+
local checkbox = nil
|
316
|
+
if self:find("^[*+-] %[[Xx ]%]%s", sp + 1) then -- task list
|
317
|
+
marker = sub(self.subject, sp, sp + 4)
|
318
|
+
checkbox = sub(self.subject, sp + 3, sp + 3)
|
319
|
+
end
|
320
|
+
-- some items have ambiguous style
|
321
|
+
local styles = {get_list_style(marker)}
|
322
|
+
local data = { styles = styles,
|
323
|
+
indent = self.indent }
|
324
|
+
-- adding container will close others
|
325
|
+
self:add_container(Container:new(spec, data))
|
326
|
+
local annot = "+list_item"
|
327
|
+
for i=1,#styles do
|
328
|
+
annot = annot .. "[" .. styles[i] .. "]"
|
329
|
+
end
|
330
|
+
self:add_match(sp, ep - 1, annot)
|
331
|
+
self.pos = ep
|
332
|
+
if checkbox then
|
333
|
+
if checkbox == " " then
|
334
|
+
self:add_match(sp + 2, sp + 4, "checkbox_unchecked")
|
335
|
+
else
|
336
|
+
self:add_match(sp + 2, sp + 4, "checkbox_checked")
|
337
|
+
end
|
338
|
+
self.pos = sp + 5
|
339
|
+
end
|
340
|
+
return true
|
341
|
+
end,
|
342
|
+
close = function(_container)
|
343
|
+
self:add_match(self.pos, self.pos, "-list_item")
|
344
|
+
self.containers[#self.containers] = nil
|
345
|
+
end
|
346
|
+
},
|
347
|
+
|
348
|
+
{ name = "reference_definition",
|
349
|
+
content = nil,
|
350
|
+
continue = function(container)
|
351
|
+
if container.indent >= self.indent then
|
352
|
+
return false
|
353
|
+
end
|
354
|
+
local _, ep, rest = self:find("^(%S+)")
|
355
|
+
if ep then
|
356
|
+
self:add_match(ep - #rest + 1, ep, "reference_value")
|
357
|
+
container.value = rest
|
358
|
+
self.pos = ep + 1
|
359
|
+
end
|
360
|
+
return true
|
361
|
+
end,
|
362
|
+
open = function(spec)
|
363
|
+
local sp, ep, label, rest = self:find("^%[([^]\r\n]*)%]:[ \t]*(%S*)")
|
364
|
+
if sp then
|
365
|
+
self:add_container(Container:new(spec,
|
366
|
+
{ key = label,
|
367
|
+
value = rest,
|
368
|
+
indent = self.indent }))
|
369
|
+
self:add_match(sp, sp, "+reference_definition")
|
370
|
+
self:add_match(sp, sp + #label + 1, "reference_key")
|
371
|
+
if #rest > 0 then
|
372
|
+
self:add_match(ep - #rest + 1, ep, "reference_value")
|
373
|
+
end
|
374
|
+
self.pos = ep + 1
|
375
|
+
return true
|
376
|
+
end
|
377
|
+
end,
|
378
|
+
close = function(_container)
|
379
|
+
self:add_match(self.pos, self.pos, "-reference_definition")
|
380
|
+
self.containers[#self.containers] = nil
|
381
|
+
end
|
382
|
+
},
|
383
|
+
|
384
|
+
{ name = "heading",
|
385
|
+
content = "inline",
|
386
|
+
continue = function(_container)
|
387
|
+
return false
|
388
|
+
end,
|
389
|
+
open = function(spec)
|
390
|
+
local sp, ep = self:find("^#+")
|
391
|
+
if ep and find(self.subject, "^%s", ep + 1) then
|
392
|
+
local level = ep - sp + 1
|
393
|
+
self:add_container(Container:new(spec, {level = level,
|
394
|
+
inline_parser = inline.Parser:new(self.subject, self.opts) }))
|
395
|
+
self:add_match(sp, ep, "+heading")
|
396
|
+
self.pos = ep + 1
|
397
|
+
return true
|
398
|
+
end
|
399
|
+
end,
|
400
|
+
close = function(_container)
|
401
|
+
self:get_inline_matches()
|
402
|
+
local last = self.matches[#self.matches] or self.pos - 1
|
403
|
+
local sp, ep, annot = unpack_match(last)
|
404
|
+
-- handle final ###
|
405
|
+
local endheadingpos = ep
|
406
|
+
local endheadingendpos = ep
|
407
|
+
if annot == "str" then
|
408
|
+
local endheadingstart, _, hashes =
|
409
|
+
find(sub(self.subject, sp, ep), "%s+(#+)$")
|
410
|
+
if hashes then
|
411
|
+
endheadingpos = endheadingpos - #hashes
|
412
|
+
if endheadingstart == 1 then
|
413
|
+
-- remove final str match
|
414
|
+
self.matches[#self.matches] = nil
|
415
|
+
else
|
416
|
+
self.matches[#self.matches] =
|
417
|
+
make_match(sp, sp + (endheadingstart - 2), "str")
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
self:add_match(endheadingpos, endheadingendpos, "-heading")
|
422
|
+
self.containers[#self.containers] = nil
|
423
|
+
end
|
424
|
+
},
|
425
|
+
|
426
|
+
{ name = "code_block",
|
427
|
+
content = "text",
|
428
|
+
continue = function(container)
|
429
|
+
local char = sub(container.border, 1, 1)
|
430
|
+
local sp, ep, border = self:find("^(" .. container.border ..
|
431
|
+
char .. "*)[ \t]*[\r\n]")
|
432
|
+
if ep then
|
433
|
+
container.end_fence_sp = sp
|
434
|
+
container.end_fence_ep = sp + #border - 1
|
435
|
+
self.pos = ep -- before newline
|
436
|
+
self.finished_line = true
|
437
|
+
return false
|
438
|
+
else
|
439
|
+
return true
|
440
|
+
end
|
441
|
+
end,
|
442
|
+
open = function(spec)
|
443
|
+
local sp, ep, border, ws, lang =
|
444
|
+
self:find("^(~~~~*)([ \t]*)(%S*)[ \t]*[\r\n]")
|
445
|
+
if not ep then
|
446
|
+
sp, ep, border, ws, lang =
|
447
|
+
self:find("^(````*)([ \t]*)([^%s`]*)[ \t]*[\r\n]")
|
448
|
+
end
|
449
|
+
if border then
|
450
|
+
local is_raw = find(lang, "^=") and true or false
|
451
|
+
self:add_container(Container:new(spec, {border = border,
|
452
|
+
indent = self.indent }))
|
453
|
+
self:add_match(sp, sp + #border - 1, "+code_block")
|
454
|
+
if #lang > 0 then
|
455
|
+
local langstart = sp + #border + #ws
|
456
|
+
if is_raw then
|
457
|
+
self:add_match(langstart, langstart + #lang - 1, "raw_format")
|
458
|
+
else
|
459
|
+
self:add_match(langstart, langstart + #lang - 1, "code_language")
|
460
|
+
end
|
461
|
+
end
|
462
|
+
self.pos = ep -- before newline
|
463
|
+
self.finished_line = true
|
464
|
+
return true
|
465
|
+
end
|
466
|
+
end,
|
467
|
+
close = function(container)
|
468
|
+
local sp = container.end_fence_sp or self.pos
|
469
|
+
local ep = container.end_fence_ep or self.pos
|
470
|
+
self:add_match(sp, ep, "-code_block")
|
471
|
+
if sp == ep then
|
472
|
+
self.warnings[#self.warnings + 1] = {self.pos, "Unclosed code block"}
|
473
|
+
end
|
474
|
+
self.containers[#self.containers] = nil
|
475
|
+
end
|
476
|
+
},
|
477
|
+
|
478
|
+
{ name = "fenced_div",
|
479
|
+
content = "block",
|
480
|
+
continue = function(container)
|
481
|
+
local sp, ep, equals = self:find("^(::::*)[ \t]*[r\n]")
|
482
|
+
if ep and #equals >= container.equals then
|
483
|
+
container.end_fence_sp = sp
|
484
|
+
container.end_fence_ep = sp + #equals - 1
|
485
|
+
self.pos = ep -- before newline
|
486
|
+
return false
|
487
|
+
else
|
488
|
+
return true
|
489
|
+
end
|
490
|
+
end,
|
491
|
+
open = function(spec)
|
492
|
+
local sp, ep1, equals = self:find("^(::::*)[ \t]*")
|
493
|
+
if not ep1 then
|
494
|
+
return false
|
495
|
+
end
|
496
|
+
local clsp, ep = find(self.subject, "^%w*", ep1 + 1)
|
497
|
+
local _, eol = find(self.subject, "^[ \t]*[\r\n]", ep + 1)
|
498
|
+
if eol then
|
499
|
+
self:add_container(Container:new(spec, {equals = #equals}))
|
500
|
+
self:add_match(sp, ep, "+div")
|
501
|
+
if ep > clsp then
|
502
|
+
self:add_match(clsp, ep, "class")
|
503
|
+
end
|
504
|
+
self.pos = eol + 1
|
505
|
+
self.finished_line = true
|
506
|
+
return true
|
507
|
+
end
|
508
|
+
end,
|
509
|
+
close = function(container)
|
510
|
+
local sp = container.end_fence_sp or self.pos
|
511
|
+
local ep = container.end_fence_ep or self.pos
|
512
|
+
-- check to make sure the match is in order
|
513
|
+
self:add_match(sp, ep, "-div")
|
514
|
+
if sp == ep then
|
515
|
+
self.warnings[#self.warnings + 1] = {self.pos, "Unclosed div"}
|
516
|
+
end
|
517
|
+
self.containers[#self.containers] = nil
|
518
|
+
end
|
519
|
+
},
|
520
|
+
|
521
|
+
{ name = "table",
|
522
|
+
content = "cells",
|
523
|
+
continue = function(_container)
|
524
|
+
local sp, ep = self:find("^|[^\r\n]*|")
|
525
|
+
local eolsp = " *[\r\n]" -- make sure at end of line
|
526
|
+
if sp and eolsp then
|
527
|
+
return self:parse_table_row(sp, ep)
|
528
|
+
end
|
529
|
+
end,
|
530
|
+
open = function(spec)
|
531
|
+
local sp, ep = self:find("^|[^\r\n]*|")
|
532
|
+
local eolsp = " *[\r\n]" -- make sure at end of line
|
533
|
+
if sp and eolsp then
|
534
|
+
self:add_container(Container:new(spec, { columns = 0 }))
|
535
|
+
self:add_match(sp, sp, "+table")
|
536
|
+
if self:parse_table_row(sp, ep) then
|
537
|
+
return true
|
538
|
+
else
|
539
|
+
self.containers[#self.containers] = nil
|
540
|
+
self.matches[#self.matches] = nil -- remove +table match
|
541
|
+
return false
|
542
|
+
end
|
543
|
+
end
|
544
|
+
end,
|
545
|
+
close = function(_container)
|
546
|
+
self:add_match(self.pos, self.pos, "-table")
|
547
|
+
self.containers[#self.containers] = nil
|
548
|
+
end
|
549
|
+
},
|
550
|
+
|
551
|
+
{ name = "attributes",
|
552
|
+
content = "attributes",
|
553
|
+
continue = function(container)
|
554
|
+
if self.indent > container.indent then
|
555
|
+
container.slices[#container.slices + 1] =
|
556
|
+
{self.pos, self.endeol}
|
557
|
+
self.pos = self.starteol
|
558
|
+
return true
|
559
|
+
else
|
560
|
+
return false
|
561
|
+
end
|
562
|
+
end,
|
563
|
+
open = function(spec)
|
564
|
+
if self:find("^%{") then
|
565
|
+
self:add_container(Container:new(spec,
|
566
|
+
{ slices = {{self.pos, self.endeol}},
|
567
|
+
indent = self.indent }))
|
568
|
+
self.pos = self.starteol
|
569
|
+
return true
|
570
|
+
end
|
571
|
+
end,
|
572
|
+
close = function(container)
|
573
|
+
local attribute_parser = attributes.AttributeParser:new(self.subject)
|
574
|
+
local slices = container.slices
|
575
|
+
local status, finalpos
|
576
|
+
for i=1,#slices do
|
577
|
+
status, finalpos = attribute_parser:feed(unpack(slices[i]))
|
578
|
+
if status ~= 'continue' then
|
579
|
+
break
|
580
|
+
end
|
581
|
+
end
|
582
|
+
-- make sure there's no extra content after the }
|
583
|
+
if status == 'done' and find(self.subject, "^[ \t]*[\r\n]", finalpos + 1) then
|
584
|
+
local attr_matches = attribute_parser:get_matches()
|
585
|
+
self:add_match(slices[1][1], slices[1][1], "+block_attributes")
|
586
|
+
for i=1,#attr_matches do
|
587
|
+
self:add_match(unpack_match(attr_matches[i]))
|
588
|
+
end
|
589
|
+
self:add_match(slices[#slices][2], slices[#slices][2], "-block_attributes")
|
590
|
+
else -- If not, parse it as inlines and add paragraph match
|
591
|
+
container.inline_parser = inline.Parser:new(self.subject, self.opts)
|
592
|
+
self:add_match(slices[1][1], slices[1][1], "+para")
|
593
|
+
for i=1,#slices do
|
594
|
+
container.inline_parser:feed(unpack(slices[i]))
|
595
|
+
end
|
596
|
+
self:get_inline_matches()
|
597
|
+
self:add_match(slices[#slices][2], slices[#slices][2], "-para")
|
598
|
+
end
|
599
|
+
self.containers[#self.containers] = nil
|
600
|
+
end
|
601
|
+
}
|
602
|
+
}
|
603
|
+
end
|
604
|
+
|
605
|
+
function Parser:get_inline_matches()
|
606
|
+
local matches, warnings =
|
607
|
+
self.containers[#self.containers].inline_parser:get_matches()
|
608
|
+
for i=1,#matches do
|
609
|
+
self.matches[#self.matches + 1] = matches[i]
|
610
|
+
end
|
611
|
+
for i=1,#warnings do
|
612
|
+
self.warnings[#self.warnings + 1] = warnings[i]
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
function Parser:find(patt)
|
617
|
+
return find(self.subject, patt, self.pos)
|
618
|
+
end
|
619
|
+
|
620
|
+
function Parser:add_match(startpos, endpos, annotation)
|
621
|
+
self.matches[#self.matches + 1] = make_match(startpos, endpos, annotation)
|
622
|
+
end
|
623
|
+
|
624
|
+
function Parser:add_container(container)
|
625
|
+
local last_matched = self.last_matched_container
|
626
|
+
while #self.containers > last_matched or
|
627
|
+
(#self.containers > 0 and
|
628
|
+
self.containers[#self.containers].content ~= "block") do
|
629
|
+
self.containers[#self.containers]:close()
|
630
|
+
end
|
631
|
+
self.containers[#self.containers + 1] = container
|
632
|
+
end
|
633
|
+
|
634
|
+
function Parser:skip_space()
|
635
|
+
local newpos, _ = find(self.subject, "[^ \t]", self.pos)
|
636
|
+
if newpos then
|
637
|
+
self.indent = newpos - self.startline
|
638
|
+
self.pos = newpos
|
639
|
+
end
|
640
|
+
end
|
641
|
+
|
642
|
+
function Parser:get_eol()
|
643
|
+
local starteol, endeol = find(self.subject, "[\r]?[\n]", self.pos)
|
644
|
+
if not endeol then
|
645
|
+
starteol, endeol = #self.subject, #self.subject
|
646
|
+
end
|
647
|
+
self.starteol = starteol
|
648
|
+
self.endeol = endeol
|
649
|
+
end
|
650
|
+
|
651
|
+
function Parser:parse()
|
652
|
+
local specs = self:specs()
|
653
|
+
local para_spec = specs[1]
|
654
|
+
local subjectlen = #self.subject
|
655
|
+
while self.pos <= subjectlen do
|
656
|
+
|
657
|
+
self.indent = 0
|
658
|
+
self.startline = self.pos
|
659
|
+
self.finished_line = false
|
660
|
+
self:get_eol()
|
661
|
+
|
662
|
+
-- check open containers for continuation
|
663
|
+
self.last_matched_container = 0
|
664
|
+
local idx = 0
|
665
|
+
while idx < #self.containers do
|
666
|
+
idx = idx + 1
|
667
|
+
local container = self.containers[idx]
|
668
|
+
-- skip any indentation
|
669
|
+
self:skip_space()
|
670
|
+
if container:continue() then
|
671
|
+
self.last_matched_container = idx
|
672
|
+
else
|
673
|
+
break
|
674
|
+
end
|
675
|
+
end
|
676
|
+
|
677
|
+
-- if we hit a close fence, we can move to next line
|
678
|
+
if self.finished_line then
|
679
|
+
while #self.containers > self.last_matched_container do
|
680
|
+
self.containers[#self.containers]:close()
|
681
|
+
end
|
682
|
+
end
|
683
|
+
|
684
|
+
if not self.finished_line then
|
685
|
+
-- check for new containers
|
686
|
+
self:skip_space()
|
687
|
+
local is_blank = (self.pos == self.starteol)
|
688
|
+
|
689
|
+
local new_starts = false
|
690
|
+
local last_match = self.containers[self.last_matched_container]
|
691
|
+
local check_starts = not is_blank and
|
692
|
+
(not last_match or last_match.content == "block") and
|
693
|
+
not self:find("^%a+%s") -- optimization
|
694
|
+
while check_starts do
|
695
|
+
check_starts = false
|
696
|
+
for i=1,#specs do
|
697
|
+
local spec = specs[i]
|
698
|
+
if not spec.is_para then
|
699
|
+
if spec:open() then
|
700
|
+
self.last_matched_container = #self.containers
|
701
|
+
if self.finished_line then
|
702
|
+
check_starts = false
|
703
|
+
else
|
704
|
+
self:skip_space()
|
705
|
+
new_starts = true
|
706
|
+
check_starts = spec.content ~= "text"
|
707
|
+
end
|
708
|
+
break
|
709
|
+
end
|
710
|
+
end
|
711
|
+
end
|
712
|
+
end
|
713
|
+
|
714
|
+
if not self.finished_line then
|
715
|
+
-- handle remaining content
|
716
|
+
self:skip_space()
|
717
|
+
|
718
|
+
is_blank = (self.pos == self.starteol)
|
719
|
+
|
720
|
+
local is_lazy = not is_blank and
|
721
|
+
not new_starts and
|
722
|
+
self.last_matched_container < #self.containers and
|
723
|
+
self.containers[#self.containers].content == 'inline'
|
724
|
+
|
725
|
+
if not is_lazy and
|
726
|
+
self.last_matched_container < #self.containers then
|
727
|
+
while #self.containers > self.last_matched_container do
|
728
|
+
self.containers[#self.containers]:close()
|
729
|
+
end
|
730
|
+
end
|
731
|
+
|
732
|
+
local tip = self.containers[#self.containers]
|
733
|
+
|
734
|
+
-- add para by default if there's text
|
735
|
+
if not tip or tip.content == 'block' then
|
736
|
+
if is_blank then
|
737
|
+
if not new_starts then
|
738
|
+
-- need to track these for tight/loose lists
|
739
|
+
self:add_match(self.pos, self.endeol, "blankline")
|
740
|
+
end
|
741
|
+
else
|
742
|
+
para_spec:open()
|
743
|
+
end
|
744
|
+
tip = self.containers[#self.containers]
|
745
|
+
end
|
746
|
+
|
747
|
+
if tip then
|
748
|
+
if tip.content == "text" then
|
749
|
+
local startpos = self.pos
|
750
|
+
if tip.indent and self.indent > tip.indent then
|
751
|
+
-- get back the leading spaces we gobbled
|
752
|
+
startpos = startpos - (self.indent - tip.indent)
|
753
|
+
end
|
754
|
+
self:add_match(startpos, self.endeol, "str")
|
755
|
+
elseif tip.content == "inline" then
|
756
|
+
if not is_blank then
|
757
|
+
tip.inline_parser:feed(self.pos, self.endeol)
|
758
|
+
end
|
759
|
+
end
|
760
|
+
end
|
761
|
+
end
|
762
|
+
end
|
763
|
+
|
764
|
+
self.pos = self.endeol + 1
|
765
|
+
end
|
766
|
+
self:finish()
|
767
|
+
|
768
|
+
end
|
769
|
+
|
770
|
+
function Parser:finish()
|
771
|
+
-- close unmatched containers
|
772
|
+
while #self.containers > 0 do
|
773
|
+
self.containers[#self.containers]:close()
|
774
|
+
end
|
775
|
+
end
|
776
|
+
|
777
|
+
function Parser:get_matches()
|
778
|
+
return self.matches
|
779
|
+
end
|
780
|
+
|
781
|
+
return { Parser = Parser,
|
782
|
+
Container = Container }
|
783
|
+
|
784
|
+
|
785
|
+
--[[
|
786
|
+
Copyright (C) 2022 John MacFarlane
|
787
|
+
|
788
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
789
|
+
a copy of this software and associated documentation files (the
|
790
|
+
"Software"), to deal in the Software without restriction, including
|
791
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
792
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
793
|
+
permit persons to whom the Software is furnished to do so, subject to
|
794
|
+
the following conditions:
|
795
|
+
|
796
|
+
The above copyright notice and this permission notice shall be included
|
797
|
+
in all copies or substantial portions of the Software.
|
798
|
+
|
799
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
800
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
801
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
802
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
803
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
804
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
805
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
806
|
+
|
807
|
+
]]
|