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.
@@ -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
+ ]]