djot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ ]]