beid 0.1.0

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,851 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Beid
4
+ class Parser
5
+ LINK_DEFINITION_START = /\A {0,3}\[((?:\\[[:punct:]]|[^\[\]\n])+)\]:[ \t]*(.*)\z/
6
+ LINK_DESTINATION = /\A(<[^>\n]*>|(?:\\.|[^\s])+)(.*)\z/
7
+
8
+ Line = Struct.new(:text, :ending, :start, :finish, keyword_init: true) do
9
+ def blank?
10
+ text.match?(/\A[ \t]*\z/)
11
+ end
12
+ end
13
+ FragmentLine = Struct.new(:text, :ending, :source_start, :range_start, :literal_setext, keyword_init: true)
14
+
15
+ def initialize(source, gfm: true, front_matter: true, inherited_link_definitions: {}, literal_setext_lines: [])
16
+ @source, @gfm, @front_matter = source, gfm, front_matter
17
+ @literal_setext_lines = literal_setext_lines
18
+ @lines = source.valid_encoding? ? lines_for(source) : []
19
+ local_definitions, @link_definition_lines = collect_link_definitions
20
+ @link_definitions = inherited_link_definitions.merge(local_definitions).freeze
21
+ end
22
+
23
+ def parse
24
+ unless @source.valid_encoding?
25
+ return [Node.new(type: :document, range: 0...@source.bytesize,
26
+ children: [Node.new(type: :text, range: 0...@source.bytesize,
27
+ attributes: { text: @source })]), nil, ["Source is not valid UTF-8"]]
28
+ end
29
+
30
+ children = []
31
+ front_matter = nil
32
+ index = 0
33
+ if @front_matter && @lines.first&.text == "---"
34
+ finish = (1...@lines.length).find { |i| ["---", "..."].include?(@lines[i].text) }
35
+ if finish
36
+ first, last = @lines.first, @lines[finish]
37
+ front_matter = @source.byteslice(first.finish...last.start)
38
+ inner_start = first.finish
39
+ children << make(:front_matter, first.start, last.finish,
40
+ marker: first.text, attributes: { text: front_matter,
41
+ content_range: inner_start...last.start })
42
+ index = finish + 1
43
+ end
44
+ end
45
+
46
+ while index < @lines.length
47
+ index += 1 while index < @lines.length && @lines[index].blank?
48
+ break if index >= @lines.length
49
+
50
+ node, index = parse_block(index)
51
+ children << node
52
+ end
53
+
54
+ root = Node.new(type: :document, range: 0...@source.bytesize, children: children)
55
+ [root, front_matter, []]
56
+ end
57
+
58
+ private
59
+
60
+ def lines_for(source)
61
+ lines = []
62
+ offset = 0
63
+ source.each_line do |raw|
64
+ ending = raw[/\r\n|\n|\r\z/] || ""
65
+ text = ending.empty? ? raw : raw[0...-ending.length]
66
+ lines << Line.new(text: text, ending: ending, start: offset, finish: offset + raw.bytesize)
67
+ offset += raw.bytesize
68
+ end
69
+ lines
70
+ end
71
+
72
+ def parse_block(index)
73
+ line = @lines[index]
74
+ text = line.text
75
+
76
+ return parse_fence(index) if fence_opening(text)
77
+ return parse_div(index) if (match = /\A {0,3}:::\s*([^\s]*)\s*\z/.match(text))
78
+ return parse_quote(index) if text.match?(/\A {0,3}>/)
79
+ return [make(:thematic_break, line.start, line.finish, marker: text.strip), index + 1] if thematic_break?(text)
80
+ return parse_list(index) if list_marker(text)
81
+ return parse_indented_code(index) if indented_code_start?(text)
82
+ return [parse_comment_directive(index), index + 1] if directive_comment(text)
83
+ return parse_html(index) if html_block_start?(text)
84
+ return parse_link_definition(index) if @link_definition_lines.key?(index)
85
+ return parse_table(index) if @gfm && table_separator?(@lines[index + 1]&.text) && text.include?("|")
86
+ return parse_footnote(index) if @gfm && text.match?(/\A {0,3}\[\^[^\]]+\]:/)
87
+ return [parse_heading(index), index + 1] if (match = /\A {0,3}(\#{1,6})(?:[ \t]+|$)(.*)\z/.match(text))
88
+ parse_paragraph(index)
89
+ end
90
+
91
+ def parse_fence(index)
92
+ opener = @lines[index]
93
+ match = fence_opening(opener.text)
94
+ fence, info = match[1], match[2].strip
95
+ indent = match.begin(1)
96
+ close = (index + 1...@lines.length).find do |i|
97
+ @lines[i].text.match?(/\A {0,3}#{Regexp.escape(fence[0])}{#{fence.length},}[ \t]*\z/)
98
+ end
99
+ last = close ? @lines[close] : @lines[-1]
100
+ finish_index = close ? close + 1 : @lines.length
101
+ content_end = close ? @lines[close].start : @source.bytesize
102
+ content_lines = @lines[(index + 1)...(close || @lines.length)]
103
+ content = content_lines.map { |line| strip_columns(line.text, indent) + line.ending }.join
104
+ [make(:code_block, opener.start, last.finish, marker: fence,
105
+ attributes: { info: info, fence: fence, closed: !close.nil?,
106
+ text: content, content_range: opener.finish...content_end }), finish_index]
107
+ end
108
+
109
+ def fence_opening(text)
110
+ match = /\A {0,3}(`{3,}|~{3,})(.*)\z/.match(text)
111
+ return if match && match[1].start_with?("`") && match[2].include?("`")
112
+
113
+ match
114
+ end
115
+
116
+ def parse_div(index)
117
+ opener = @lines[index]
118
+ name = /\A {0,3}:::\s*([^\s]*)/.match(opener.text)[1]
119
+ depth = 1
120
+ finish_index = index + 1
121
+ while finish_index < @lines.length
122
+ text = @lines[finish_index].text
123
+ depth += 1 if text.match?(/\A {0,3}:::\s*\S+/)
124
+ if text.match?(/\A {0,3}:::\s*\z/)
125
+ depth -= 1
126
+ if depth.zero?
127
+ finish_index += 1
128
+ break
129
+ end
130
+ end
131
+ finish_index += 1
132
+ end
133
+ last = @lines[finish_index - 1]
134
+ closing_index = depth.zero? ? finish_index - 1 : finish_index
135
+ children = []
136
+ child_index = index + 1
137
+ while child_index < closing_index
138
+ child_index += 1 while child_index < closing_index && @lines[child_index].blank?
139
+ break if child_index >= closing_index
140
+
141
+ child, child_index = parse_block(child_index)
142
+ children << child
143
+ end
144
+ [make(:directive, opener.start, last.finish, marker: ":::",
145
+ attributes: { kind: :div, name: name, closed: depth.zero? }, children: children), finish_index]
146
+ end
147
+
148
+ def parse_quote(index)
149
+ first = index
150
+ fragments = []
151
+ paragraph_open = false
152
+ while index < @lines.length
153
+ line = @lines[index]
154
+ match = /\A {0,3}> ?(.*)\z/.match(line.text)
155
+ if match
156
+ prefix_end = match.begin(1)
157
+ content = match[1]
158
+ content_start = line.start + byte_length(line.text[0...prefix_end])
159
+ fragments << FragmentLine.new(text: content, ending: line.ending,
160
+ source_start: content_start, range_start: line.start)
161
+ paragraph_open = quote_paragraph_open?(content)
162
+ index += 1
163
+ elsif paragraph_open && !line.blank? && !interrupting?(line.text)
164
+ fragments << FragmentLine.new(text: line.text, ending: line.ending,
165
+ source_start: line.start, range_start: line.start,
166
+ literal_setext: !setext_level(line.text).nil?)
167
+ index += 1
168
+ else
169
+ break
170
+ end
171
+ end
172
+ children = parse_fragment(fragments)
173
+ [make(:block_quote, @lines[first].start, @lines[index - 1].finish, marker: ">", children: children), index]
174
+ end
175
+
176
+ def parse_list(index)
177
+ first = index
178
+ initial = list_marker(@lines[index].text)
179
+ base_indent, list_marker_text, ordered = initial
180
+ items = []
181
+ while index < @lines.length
182
+ break if thematic_break?(@lines[index].text)
183
+
184
+ current = list_marker(@lines[index].text)
185
+ break unless same_list?(current, initial)
186
+
187
+ line = @lines[index]
188
+ marker_match = /\A[ \t]*(?:([-+*])|(\d{1,9}[.)]))(?:([ \t]+)(.*)|\z)/.match(line.text)
189
+ marker = marker_match[1] || marker_match[2]
190
+ content = marker_match[4].to_s
191
+ marker_start = marker_match.begin(1) || marker_match.begin(2)
192
+ marker_finish = marker_start + marker.length
193
+ marker_indent = indentation(line.text[0...marker_start])
194
+ spacing = marker_match[3].to_s
195
+ spacing_columns = indentation(spacing)
196
+ content_column = marker_indent + marker.length + [spacing_columns, 1].max
197
+ content_character = marker_match.begin(4)
198
+ if content_character && spacing_columns > 4
199
+ content_character = marker_finish + 1
200
+ content = line.text[content_character..].to_s
201
+ end
202
+ content_prefix = line.text[0...(content_character || line.text.length)]
203
+ content_start = line.start + byte_length(content_prefix)
204
+ content_indent = content_column
205
+ if content.empty?
206
+ content_indent = marker_indent + marker.length + 1
207
+ end
208
+ content_finish = content_start + byte_length(content)
209
+ task = @gfm && /\A\[([ xX])\](?:[ \t]+|$)(.*)\z/.match(content)
210
+ if task
211
+ content_start += byte_length(content[0...task.begin(2)])
212
+ content, content_finish = task[2], content_start + byte_length(task[2])
213
+ end
214
+ attrs = { content_range: content_start...content_finish, ordered: ordered,
215
+ start: marker[/\A\d+/]&.to_i, task: !!task, checked: task && task[1].downcase == "x" }
216
+ fragments = [FragmentLine.new(text: content, ending: line.ending,
217
+ source_start: content_start, range_start: content_start)]
218
+ item_end = line.finish
219
+ has_item_content = !content.empty?
220
+ paragraph_open = paragraph_continuation?(content)
221
+ index += 1
222
+ while index < @lines.length
223
+ if @lines[index].blank?
224
+ next_index = index + 1
225
+ next_index += 1 while next_index < @lines.length && @lines[next_index].blank?
226
+ next_line = @lines[next_index]
227
+ next_marker = list_marker(next_line&.text.to_s, max_indent: nil)
228
+ if next_marker && same_list?(initial, next_marker) && next_marker[0] < content_indent
229
+ index = next_index
230
+ break
231
+ end
232
+ if has_item_content && next_line && indentation(next_line.text[/\A[ \t]*/].to_s) >= content_indent
233
+ while index < next_index
234
+ blank = @lines[index]
235
+ blank_text, blank_start = strip_indent(blank, content_indent)
236
+ fragments << FragmentLine.new(text: blank_text, ending: blank.ending,
237
+ source_start: blank_start, range_start: blank.start)
238
+ item_end = blank.finish
239
+ index += 1
240
+ end
241
+ next
242
+ end
243
+ break
244
+ end
245
+
246
+ continuation = @lines[index]
247
+ continuation_marker = list_marker(continuation.text, max_indent: nil)
248
+ continuation_indent = indentation(continuation.text[/\A[ \t]*/].to_s)
249
+ break if continuation_marker && continuation_marker[0] == base_indent
250
+
251
+ if continuation_indent >= content_indent
252
+ text, source_start = strip_indent(continuation, content_indent)
253
+ fragments << FragmentLine.new(text: text, ending: continuation.ending,
254
+ source_start: source_start, range_start: continuation.start)
255
+ has_item_content ||= !text.empty?
256
+ paragraph_open = paragraph_continuation?(text)
257
+ item_end = continuation.finish
258
+ index += 1
259
+ elsif paragraph_open && continuation_indent < content_indent && !interrupting?(continuation.text)
260
+ text, source_start = strip_indent(continuation, base_indent)
261
+ fragments << FragmentLine.new(text: text, ending: continuation.ending,
262
+ source_start: source_start, range_start: continuation.start)
263
+ has_item_content ||= !continuation.text.empty?
264
+ item_end = continuation.finish
265
+ index += 1
266
+ else
267
+ break
268
+ end
269
+ end
270
+
271
+ item_children = parse_fragment(fragments, preserve_list_indent: true)
272
+ if task
273
+ checkbox_start = content_start - (task[0].bytesize - task[2].bytesize)
274
+ item_children.unshift(Node.new(type: :task_checkbox,
275
+ attributes: { checked: attrs[:checked] }, children: [],
276
+ range: checkbox_start...content_start, marker: task[1]))
277
+ end
278
+ items << make(:list_item, line.start, item_end, marker: marker, attributes: attrs, children: item_children)
279
+ end
280
+ list_type = ordered ? :ordered_list : :list
281
+ [make(list_type, @lines[first].start, items.last.range.end, marker: list_marker_text,
282
+ attributes: { ordered: ordered, start: initial[3], tight: !loose_list?(items) }, children: items), index]
283
+ end
284
+
285
+ def parse_link_definition(index)
286
+ line = @lines[index]
287
+ definition = @link_definition_lines.fetch(index)
288
+ node = make(:link_definition, line.start, definition[:range].end, marker: "[", attributes: {
289
+ label: definition[:label],
290
+ normalized_label: definition[:normalized_label],
291
+ destination: definition[:destination],
292
+ title: definition[:title],
293
+ destination_range: definition[:destination_range],
294
+ title_range: definition[:title_range],
295
+ effective: definition[:effective]
296
+ })
297
+ [node, definition[:last_index] + 1]
298
+ end
299
+
300
+ def parse_indented_code(index)
301
+ first = index
302
+ index += 1
303
+ index += 1 while index < @lines.length && (indented_code_start?(@lines[index].text) || @lines[index].blank?)
304
+ index -= 1 while index > first && @lines[index - 1].blank?
305
+ last = @lines[index - 1]
306
+ content = @lines[first...index].map { |line| strip_columns(line.text, 4) + line.ending }.join
307
+ [make(:code_block, @lines[first].start, last.finish, marker: " ",
308
+ attributes: { info: "", fence: nil, text: content }), index]
309
+ end
310
+
311
+ def parse_html(index)
312
+ first = index
313
+ terminator = html_block_terminator(@lines[index].text)
314
+ index += 1
315
+ if terminator
316
+ index += 1 while index < @lines.length && !@lines[index - 1].text.match?(terminator)
317
+ else
318
+ index += 1 while index < @lines.length && !@lines[index].blank?
319
+ end
320
+ last = @lines[index - 1]
321
+ [make(:html_block, @lines[first].start, last.finish, marker: nil,
322
+ attributes: { text: @source.byteslice(@lines[first].start...last.finish) }), index]
323
+ end
324
+
325
+ def parse_comment_directive(index)
326
+ line = @lines[index]
327
+ match = /\A[ \t]*(<!--.*-->)[ \t]*\z/.match(line.text)
328
+ raw = match[1]
329
+ comment_start = line.start + byte_length(line.text[0...match.begin(1)])
330
+ comment_range = comment_start...(comment_start + byte_length(raw))
331
+ make(:directive, line.start, line.finish, marker: raw,
332
+ attributes: { kind: :html_comment, values: Directive.parse(raw), comment_range: comment_range })
333
+ end
334
+
335
+ def parse_table(index)
336
+ first = index
337
+ head = table_cells(@lines[index])
338
+ separator = table_cells(@lines[index + 1])
339
+ aligns = separator.map do |cell|
340
+ left, right = cell[:text].strip.start_with?(":"), cell[:text].strip.end_with?(":")
341
+ left && right ? :center : (left ? :left : (right ? :right : nil))
342
+ end
343
+ separator_line = @lines[index + 1]
344
+ rows = [table_row(@lines[index], head, :header, aligns),
345
+ make(:table_separator, separator_line.start, separator_line.finish, marker: separator_line.text)]
346
+ index += 2
347
+ while index < @lines.length && @lines[index].text.include?("|") && !@lines[index].blank?
348
+ rows << table_row(@lines[index], table_cells(@lines[index]), :body, aligns)
349
+ index += 1
350
+ end
351
+ [make(:table, @lines[first].start, rows.last.range.end, marker: "|",
352
+ attributes: { alignments: aligns }, children: rows), index]
353
+ end
354
+
355
+ def table_cells(line)
356
+ text = line.text
357
+ first = text.start_with?("|") ? 1 : 0
358
+ last = text.end_with?("|") ? text.length - 1 : text.length
359
+ cells = []
360
+ start = first
361
+ (first...last).each do |index|
362
+ next unless text[index] == "|"
363
+
364
+ cells << cell_data(text, start, index)
365
+ start = index + 1
366
+ end
367
+ cells << cell_data(text, start, last)
368
+ cells
369
+ end
370
+
371
+ def cell_data(text, first, last)
372
+ raw = text[first...last]
373
+ leading = raw[/\A[ \t]*/].to_s.length
374
+ trailing = raw[/[ \t]*\z/].to_s.length
375
+ from = first + leading
376
+ to = [from, last - trailing].max
377
+ { text: text[from...to], first: from, last: to }
378
+ end
379
+
380
+ def table_row(line, cells, kind, aligns)
381
+ children = cells.each_with_index.map do |cell, index|
382
+ start = line.start + byte_length(line.text[0...cell[:first]])
383
+ finish = line.start + byte_length(line.text[0...cell[:last]])
384
+ make(:table_cell, start, finish, marker: "|", attributes: { alignment: aligns[index] },
385
+ children: inline_nodes(cell[:text], start))
386
+ end
387
+ make(:table_row, line.start, line.finish, marker: "|", attributes: { kind: kind }, children: children)
388
+ end
389
+
390
+ def parse_footnote(index)
391
+ line = @lines[index]
392
+ match = /\A {0,3}\[\^([^\]]+)\]:[ \t]*(.*)\z/.match(line.text)
393
+ start = line.start + byte_length(line.text[0...match.begin(2)])
394
+ finish = start + byte_length(match[2])
395
+ [make(:footnote_definition, line.start, line.finish, marker: "[^#{match[1]}]:",
396
+ attributes: { identifier: match[1], content_range: start...finish },
397
+ children: inline_nodes(match[2], start)), index + 1]
398
+ end
399
+
400
+ def parse_heading(index)
401
+ line = @lines[index]
402
+ match = /\A {0,3}(\#{1,6})(?:[ \t]+|$)(.*)\z/.match(line.text)
403
+ content = match[2].sub(/[ \t]+#+[ \t]*\z/, "")
404
+ content = "" if content.match?(/\A#+[ \t]*\z/)
405
+ content = content.sub(/[ \t]+\z/, "")
406
+ marker_start = line.start + byte_length(line.text[0...match.begin(1)])
407
+ char_offset = match.begin(2)
408
+ start = line.start + byte_length(line.text[0...char_offset])
409
+ finish = start + byte_length(content)
410
+ make(:heading, line.start, line.finish, marker: match[1],
411
+ attributes: { level: match[1].length, content_range: start...finish,
412
+ marker_range: marker_start...(marker_start + byte_length(match[1])), style: :atx },
413
+ children: inline_nodes(content, start))
414
+ end
415
+
416
+ def parse_paragraph(index)
417
+ first = index
418
+ index += 1
419
+ index += 1 while index < @lines.length && !@lines[index].blank? &&
420
+ !setext_underline?(index) && !interrupting?(@lines[index].text)
421
+ if index < @lines.length && setext_underline?(index)
422
+ last = @lines[index]
423
+ content_line = @lines[index - 1]
424
+ content = @source.byteslice(@lines[first].start...content_line.finish).sub(/(?:\r\n|\r|\n)\z/, "")
425
+ indentation = content[/\A {0,3}/].to_s
426
+ content = content.byteslice(indentation.bytesize..).to_s
427
+ content = content.sub(/[ \t]+\z/, "")
428
+ start = @lines[first].start + indentation.bytesize
429
+ finish = start + byte_length(content)
430
+ level = setext_level(last.text)
431
+ marker_start = last.start + byte_length(last.text[/\A */].to_s)
432
+ marker_end = last.start + byte_length(last.text.rstrip)
433
+ node = make(:heading, @lines[first].start, last.finish, marker: last.text.strip,
434
+ attributes: { level: level, content_range: start...finish,
435
+ marker_range: marker_start...marker_end, style: :setext },
436
+ children: inline_nodes(content, start))
437
+ return [node, index + 1]
438
+ end
439
+ last = @lines[index - 1]
440
+ content = @source.byteslice(@lines[first].start...last.finish).sub(/(?:\r\n|\r|\n)\z/, "")
441
+ indentation = content[/\A {0,3}/].to_s
442
+ content = content.byteslice(indentation.bytesize..).to_s.sub(/[ \t]+\z/, "")
443
+ start = @lines[first].start + indentation.bytesize
444
+ [make(:paragraph, start, last.finish,
445
+ attributes: { content_range: start...(start + byte_length(content)) },
446
+ children: inline_nodes(content, start)), index]
447
+ end
448
+
449
+ def make(type, first, last, marker: nil, attributes: {}, children: [])
450
+ Node.new(type: type, range: first...last, marker: marker, attributes: attributes, children: children)
451
+ end
452
+
453
+ def inline_nodes(text, offset)
454
+ InlineParser.new(text, offset, gfm: @gfm, references: @link_definitions).parse
455
+ end
456
+
457
+ def parse_fragment(lines, preserve_list_indent: false)
458
+ source = +""
459
+ starts = []
460
+ ends = []
461
+ list_starts = {}
462
+ literal_setext_lines = []
463
+ lines.each do |line|
464
+ offset = source.bytesize
465
+ literal_setext_lines << offset if line.literal_setext
466
+ starts[offset] = line.source_start
467
+ ends[offset] ||= offset.zero? ? line.source_start : ends[offset - 1]
468
+ list_starts[offset] = line.range_start
469
+ append_fragment_text(source, line.text, line.source_start, starts, ends)
470
+ append_fragment_text(source, line.ending, line.source_start + line.text.bytesize, starts, ends)
471
+ end
472
+ starts[0] ||= lines.first&.source_start || 0
473
+ ends[0] ||= starts[0]
474
+
475
+ parser = self.class.new(source, gfm: @gfm, front_matter: false,
476
+ inherited_link_definitions: @link_definitions,
477
+ literal_setext_lines: literal_setext_lines)
478
+ root, = parser.parse
479
+ external_range_ids = @link_definitions.values.flat_map do |definition|
480
+ definition.values_at(:range, :destination_range, :title_range)
481
+ end.compact.map(&:object_id)
482
+ root.children.map do |node|
483
+ remap_fragment_node(node, starts, ends, list_starts, external_range_ids, preserve_list_indent)
484
+ end
485
+ end
486
+
487
+ def append_fragment_text(source, text, source_start, starts, ends)
488
+ offset = source.bytesize
489
+ starts[offset] ||= source_start
490
+ ends[offset] ||= source_start
491
+ source << text
492
+ (1..text.bytesize).each do |length|
493
+ starts[offset + length] = source_start + length
494
+ ends[offset + length] = source_start + length
495
+ end
496
+ end
497
+
498
+ def remap_fragment_node(node, starts, ends, list_starts, external_range_ids, preserve_list_indent)
499
+ children = node.children.map do |child|
500
+ remap_fragment_node(child, starts, ends, list_starts, external_range_ids, preserve_list_indent)
501
+ end
502
+ range = remap_fragment_range(node.range, starts, ends)
503
+ if preserve_list_indent && %i[list ordered_list].include?(node.type) && list_starts.key?(node.range.begin)
504
+ range = list_starts.fetch(node.range.begin)...range.end
505
+ end
506
+ attributes = remap_fragment_value(node.attributes, starts, ends, external_range_ids)
507
+ Node.new(type: node.type, attributes: attributes, children: children, range: range, marker: node.marker)
508
+ end
509
+
510
+ def remap_fragment_value(value, starts, ends, external_range_ids)
511
+ case value
512
+ when Range
513
+ external_range_ids.include?(value.object_id) ? value : remap_fragment_range(value, starts, ends)
514
+ when Array
515
+ value.map { |item| remap_fragment_value(item, starts, ends, external_range_ids) }
516
+ when Hash
517
+ value.to_h do |key, item|
518
+ [key, remap_fragment_value(item, starts, ends, external_range_ids)]
519
+ end
520
+ else
521
+ value
522
+ end
523
+ end
524
+
525
+ def remap_fragment_range(range, starts, ends)
526
+ (starts[range.begin] || range.begin)...(ends[range.end] || range.end)
527
+ end
528
+
529
+ def paragraph_continuation?(text)
530
+ !text.match?(/\A[ \t]*\z/) && !text.start_with?(" ", "\t") && !interrupting?(text)
531
+ end
532
+
533
+ def quote_paragraph_open?(text)
534
+ content = text
535
+ loop do
536
+ if (quote = /\A {0,3}>(?: ?)(.*)\z/.match(content))
537
+ content = quote[1]
538
+ elsif (list = /\A {0,3}(?:[-+*]|\d{1,9}[.)])(?:[ \t]+|$)(.*)\z/.match(content))
539
+ content = list[1]
540
+ else
541
+ break
542
+ end
543
+ end
544
+ paragraph_continuation?(content)
545
+ end
546
+
547
+ def indented_code_start?(text)
548
+ indentation(text[/\A[ \t]*/].to_s) >= 4
549
+ end
550
+
551
+ def strip_columns(text, columns)
552
+ index = 0
553
+ column = 0
554
+ while index < text.length && column < columns && text[index].match?(/[ \t]/)
555
+ if text[index] == "\t"
556
+ next_column = (column / 4 + 1) * 4
557
+ return (" " * (next_column - columns)) + text[(index + 1)..].to_s if next_column > columns
558
+
559
+ column = next_column
560
+ else
561
+ column += 1
562
+ end
563
+ index += 1
564
+ end
565
+ text[index..].to_s
566
+ end
567
+
568
+ def strip_indent(line, columns)
569
+ index = 0
570
+ column = 0
571
+ while index < line.text.length && column < columns && line.text[index].match?(/[ \t]/)
572
+ column = line.text[index] == "\t" ? (column / 4 + 1) * 4 : column + 1
573
+ index += 1
574
+ end
575
+ [line.text[index..].to_s, line.start + byte_length(line.text[0...index])]
576
+ end
577
+
578
+ def collect_link_definitions
579
+ definitions = {}
580
+ definition_lines = {}
581
+ fence = nil
582
+ html_block = nil
583
+ previous_definition_end = nil
584
+ front_matter_end = if @front_matter && @lines.first&.text == "---"
585
+ (1...@lines.length).find { |index| ["---", "..."].include?(@lines[index].text) }
586
+ end
587
+ index = 0
588
+ while index < @lines.length
589
+ if front_matter_end && index <= front_matter_end
590
+ index += 1
591
+ next
592
+ end
593
+ line = @lines[index]
594
+ if fence
595
+ fence = nil if line.text.match?(/\A {0,3}#{Regexp.escape(fence[0])}{#{fence.length},}[ \t]*\z/)
596
+ index += 1
597
+ next
598
+ end
599
+
600
+ if (opening = fence_opening(line.text))
601
+ fence = opening[1]
602
+ index += 1
603
+ next
604
+ end
605
+ if html_block
606
+ html_block = nil if html_block == :blank_line ? line.blank? : line.text.match?(html_block)
607
+ index += 1
608
+ next
609
+ end
610
+ if !directive_comment(line.text) && html_block_start?(line.text)
611
+ terminator = html_block_terminator(line.text)
612
+ html_block = terminator ? (line.text.match?(terminator) ? nil : terminator) : :blank_line
613
+ index += 1
614
+ next
615
+ end
616
+ if indented_code_start?(line.text) || (@gfm && line.text.match?(/\A {0,3}\[\^[^\]]+\]:/))
617
+ index += 1
618
+ next
619
+ end
620
+ if index.positive? && !@lines[index - 1].blank? && previous_definition_end != index - 1 &&
621
+ !link_definition_block_boundary?(@lines[index - 1].text)
622
+ index += 1
623
+ next
624
+ end
625
+
626
+ definition = link_definition_at(index)
627
+ unless definition
628
+ index += 1
629
+ next
630
+ end
631
+
632
+ normalized = normalize_reference_label(definition[:label])
633
+ definition[:normalized_label] = normalized
634
+ definition[:effective] = !definitions.key?(normalized)
635
+ definitions[normalized] ||= definition
636
+ definition_lines[index] = definition
637
+ previous_definition_end = definition[:last_index]
638
+ index = definition[:last_index] + 1
639
+ end
640
+ [definitions.freeze, definition_lines.freeze]
641
+ end
642
+
643
+ def link_definition_at(index)
644
+ line = @lines[index]
645
+ prefix = LINK_DEFINITION_START.match(line.text)
646
+ if prefix
647
+ label = prefix[1]
648
+ content = prefix[2]
649
+ content_index = prefix.begin(2)
650
+ destination_line = index
651
+ else
652
+ first_label = /\A {0,3}\[([^\]\n]+)\z/.match(line.text)
653
+ continuation = @lines[index + 1]
654
+ continuation_prefix = continuation && /\A {0,3}([^\[\]\n]+)\]:[ \t]*(.*)\z/.match(continuation.text)
655
+ return unless first_label && continuation_prefix
656
+
657
+ label = "#{first_label[1]} #{continuation_prefix[1]}"
658
+ content = continuation_prefix[2]
659
+ content_index = continuation_prefix.begin(2)
660
+ destination_line = index + 1
661
+ end
662
+ if content.strip.empty?
663
+ destination_line += 1
664
+ return if destination_line >= @lines.length || @lines[destination_line].blank?
665
+
666
+ content = @lines[destination_line].text
667
+ content_index = 0
668
+ content_index += 1 while content[content_index]&.match?(/[ \t]/)
669
+ content = content[content_index..]
670
+ end
671
+
672
+ destination_match = LINK_DESTINATION.match(content)
673
+ return unless destination_match
674
+
675
+ raw_destination = destination_match[1]
676
+ raw_destination_value = raw_destination.start_with?("<") ? raw_destination[1...-1] : raw_destination
677
+ destination = unescape_punctuation(raw_destination_value)
678
+ destination_start_index = content_index + destination_match.begin(1)
679
+ destination_start = @lines[destination_line].start + byte_length(@lines[destination_line].text[0...destination_start_index])
680
+ destination_start += 1 if raw_destination.start_with?("<")
681
+ destination_range = destination_start...(destination_start + byte_length(raw_destination_value))
682
+ tail_start = content_index + destination_match.end(1)
683
+ tail = destination_match[2]
684
+ last_index = destination_line
685
+ title = nil
686
+ title_range = nil
687
+
688
+ unless tail.strip.empty?
689
+ spacing = tail[/\A[ \t]*/].to_s.length
690
+ return if spacing.zero?
691
+
692
+ title_start = tail_start + spacing
693
+ title_data = link_title_at(destination_line, title_start)
694
+ return unless title_data
695
+
696
+ title, title_range, last_index = title_data
697
+ else
698
+ next_index = destination_line + 1
699
+ if next_index < @lines.length && !@lines[next_index].blank?
700
+ leading = @lines[next_index].text[/\A[ \t]*/].to_s.length
701
+ title_data = link_title_at(next_index, leading)
702
+ if title_data
703
+ title, title_range, last_index = title_data
704
+ end
705
+ end
706
+ end
707
+
708
+ {
709
+ label: label, destination: destination, title: title,
710
+ range: line.start...@lines[last_index].finish,
711
+ destination_range: destination_range, title_range: title_range,
712
+ last_index: last_index
713
+ }
714
+ end
715
+
716
+ def link_title_at(line_index, character_index)
717
+ opener = @lines[line_index].text[character_index]
718
+ closer = { "\"" => "\"", "'" => "'", "(" => ")" }[opener]
719
+ return unless closer
720
+
721
+ title_start = @lines[line_index].start + byte_length(@lines[line_index].text[0...(character_index + 1)])
722
+ index = line_index
723
+ position = character_index + 1
724
+ loop do
725
+ line = @lines[index]
726
+ while position < line.text.length
727
+ if line.text[position] == closer && !escaped_in_line?(line.text, position)
728
+ return unless line.text[(position + 1)..].match?(/\A[ \t]*\z/)
729
+
730
+ title_end = line.start + byte_length(line.text[0...position])
731
+ raw_title = @source.byteslice(title_start...title_end)
732
+ return [unescape_punctuation(raw_title), title_start...title_end, index]
733
+ end
734
+ position += 1
735
+ end
736
+ index += 1
737
+ return if index >= @lines.length || @lines[index].blank?
738
+
739
+ position = 0
740
+ end
741
+ end
742
+
743
+ def escaped_in_line?(text, index)
744
+ slashes = 0
745
+ index -= 1
746
+ while index >= 0 && text[index] == "\\"
747
+ slashes += 1
748
+ index -= 1
749
+ end
750
+ slashes.odd?
751
+ end
752
+
753
+ def link_definition_block_boundary?(text)
754
+ text.match?(/\A {0,3}(?:\#{1,6}(?:[ \t]|$)|>|`{3,}|~{3,}|(?:[-+*]|\d{1,9}[.)])[ \t]+|(?:[-*_][ \t]*){3,}|<\/?(?:address|article|aside|blockquote|div|h[1-6]|hr|ol|p|pre|section|table|ul)(?:\s|\x2f?>))/i) ||
755
+ text.match?(/\A {0,3}(?:=+|\*{3,}|-{3,})[ \t]*\z/)
756
+ end
757
+
758
+ def normalize_reference_label(label)
759
+ label.gsub(/[[:space:]]+/, " ").strip.downcase(:fold)
760
+ end
761
+
762
+ def unescape_punctuation(text)
763
+ text&.gsub(/\\([[:punct:]])/, "\\1")
764
+ end
765
+
766
+ def list_marker(text, max_indent: 3)
767
+ match = /\A([ \t]*)([-+*]|(\d{1,9}[.)]))(?=[ \t]|\z)[ \t]*/.match(text)
768
+ return nil unless match
769
+ indent = indentation(match[1])
770
+ return nil if max_indent && indent > max_indent
771
+
772
+ [indent, match[2], !match[3].nil?, match[3]&.to_i || 0]
773
+ end
774
+
775
+ def same_list?(left, right)
776
+ return false unless left && right && left[0] <= 3 && right[0] <= 3 && left[2] == right[2]
777
+
778
+ left[2] ? left[1][-1] == right[1][-1] : left[1] == right[1]
779
+ end
780
+
781
+ def loose_list?(items)
782
+ items.each do |item|
783
+ blocks = item.children.reject { |child| child.type == :task_checkbox }
784
+ return true if blocks.each_cons(2).any? { |left, right| blank_line_between?(left.range.end, right.range.begin) }
785
+ end
786
+ items.each_cons(2).any? { |left, right| blank_line_between?(left.range.end, right.range.begin) }
787
+ end
788
+
789
+ def blank_line_between?(first, last)
790
+ first < last && @source.byteslice(first...last).to_s.match?(/[\r\n]/)
791
+ end
792
+
793
+ def indentation(whitespace)
794
+ whitespace.each_char.reduce(0) { |column, char| char == "\t" ? (column / 4 + 1) * 4 : column + 1 }
795
+ end
796
+
797
+ def interrupting?(text)
798
+ text.match?(/\A {0,3}(?:\#{1,6}(?:[ \t]|$)|>|`{3,}|~{3,}|:::\s*(?:\S|$)|(?:[-+*]|1[.)])[ \t]+|(?:[-*_][ \t]*){3,}|<\/?(?:address|article|aside|base|blockquote|body|caption|center|col|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|ol|optgroup|option|p|pre|script|section|source|style|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?:\s|\x2f?>))/i)
799
+ end
800
+
801
+ def thematic_break?(text)
802
+ text.match?(/\A {0,3}(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})\z/)
803
+ end
804
+
805
+ def setext_level(text)
806
+ return 1 if text.match?(/\A {0,3}=+[ \t]*\z/)
807
+ return 2 if text.match?(/\A {0,3}-+[ \t]*\z/)
808
+
809
+ nil
810
+ end
811
+
812
+ def setext_underline?(index)
813
+ !@literal_setext_lines.include?(@lines[index].start) && !setext_level(@lines[index].text).nil?
814
+ end
815
+
816
+ def table_separator?(text)
817
+ text && text.include?("|") && table_cells(Line.new(text: text)).all? { |cell| cell[:text].strip.match?(/\A:?-{3,}:?\z/) }
818
+ end
819
+
820
+ def html_block_start?(text)
821
+ text.match?(/\A {0,3}(?:<!--|<\?|<!\[CDATA\[|<![A-Z]|<(?:script|pre|style|textarea)(?:[ \t]|$)|<\/?(?:address|article|aside|base|blockquote|body|caption|center|col|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|ol|optgroup|option|p|pre|script|section|source|style|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?:\s|\x2f?>))/i) ||
822
+ valid_html_tag_line?(text)
823
+ end
824
+
825
+ def valid_html_tag_line?(text)
826
+ line = text.sub(/\A {0,3}/, "")
827
+ match = InlineParser::HTML_INLINE.match(line)
828
+ match && match.begin(0).zero? && match.end(0) == line.rstrip.length
829
+ end
830
+
831
+ def html_block_terminator(text)
832
+ tag = /\A {0,3}<(script|pre|style|textarea)(?=[ \t\n>]|$)/i.match(text)
833
+ return %r{</#{Regexp.escape(tag[1])}[ \t]*>}i if tag
834
+ return /-->/ if text.match?(/\A {0,3}<!--/)
835
+ return /\?>/ if text.match?(/\A {0,3}<\?/)
836
+ return /\]\]>/ if text.match?(/\A {0,3}<!\[CDATA\[/)
837
+ return />/ if text.match?(/\A {0,3}<![A-Z]/)
838
+
839
+ nil
840
+ end
841
+
842
+ def directive_comment(text)
843
+ match = /\A[ \t]*(<!--.*-->)[ \t]*\z/.match(text)
844
+ match && Directive.parse(match[1])
845
+ end
846
+
847
+ def byte_length(text)
848
+ text.to_s.bytesize
849
+ end
850
+ end
851
+ end