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,704 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Beid
4
+ class InlineParser
5
+ TOKEN = /(`+)(.+?)\1|(!?)\[([^\]]*)\]\(([^\s()]*(?:\([^()]*\)[^\s()]*)*)(?:[ \t]+(?:"([^"]*)"|'([^']*)'))?\)|(\*\*|__|\*|_|~~)(?=\S)(.+?)\8/m
6
+ AUTOLINK = /<([A-Za-z][A-Za-z0-9.+-]{1,31}:[^ <>]*)>|<([A-Za-z0-9.!#$%&'*+\/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?)>/
7
+ HTML_INLINE = /<!--[\s\S]*?-->|<\?[\s\S]*?\?>|<!\[CDATA\[[\s\S]*?\]\]>|<![A-Z][^>]*>|<\/[A-Za-z][A-Za-z0-9-]*[ \t\n]*>|<[A-Za-z][A-Za-z0-9-]*(?:[ \t\n]+[A-Za-z_:][A-Za-z0-9_.:-]*(?:[ \t\n]*=[ \t\n]*(?:[^ \t\n\"'=<>`]+|'[^']*'|\"[^\"]*\"))?)*[ \t\n]*\/?>/
8
+ REFERENCE_START = /!?\[/
9
+ Reference = Struct.new(:start, :finish, :image, :label, :label_start, :label_end,
10
+ :reference_label, :definition, keyword_init: true)
11
+ CodeRun = Struct.new(:start, :finish, :length, keyword_init: true)
12
+ CodeSpan = Struct.new(:start, :finish, :marker, :text, keyword_init: true)
13
+ Break = Struct.new(:start, :finish, :type, :marker, keyword_init: true)
14
+ DelimiterMatch = Struct.new(:start, :finish, :marker, :inner_start, :inner_finish, keyword_init: true)
15
+ InlineLink = Struct.new(:start, :finish, :image, :label, :label_start, :label_finish,
16
+ :destination, :destination_range, :title, :title_range, keyword_init: true)
17
+
18
+ def initialize(source, base_offset, gfm: true, references: {})
19
+ @source, @base_offset, @gfm, @references = source, base_offset, gfm, references
20
+ @code_runs = code_runs
21
+ @code_closers = {}
22
+ next_run_by_length = {}
23
+ @code_runs.reverse_each do |run|
24
+ @code_closers[run.start] = next_run_by_length[run.length]
25
+ next_run_by_length[run.length] = run
26
+ end
27
+ end
28
+
29
+ def parse
30
+ return [] unless @source && !@source.empty? && @source.valid_encoding?
31
+
32
+ nodes = []
33
+ cursor = 0
34
+ while cursor < @source.length
35
+ match = next_inline_match(cursor)
36
+ inline_link = next_inline_link(cursor)
37
+ code_span = next_code_span(cursor)
38
+ line_break = next_line_break(cursor)
39
+ html_inline = next_html_inline(cursor)
40
+ footnote = /\[\^([^\]]+)\]/.match(@source, cursor) if @gfm
41
+ autolink = next_autolink(cursor)
42
+ if match.is_a?(MatchData) && autolink && ((match[3] && autolink.begin(0) < match.end(0) && autolink.end(0) > match.end(0)) ||
43
+ (match[8] && autolink.begin(0) <= match.end(9) && match.end(9) < autolink.end(0)))
44
+ match = nil
45
+ elsif match.is_a?(DelimiterMatch) && autolink &&
46
+ autolink.begin(0) <= match.inner_finish && match.inner_finish < autolink.end(0)
47
+ match = nil
48
+ end
49
+ if inline_link && autolink && inline_link.start < autolink.begin(0) &&
50
+ autolink.begin(0) < inline_link.finish && inline_link.finish < autolink.end(0)
51
+ inline_link = nil
52
+ end
53
+ if inline_link && code_span && inline_link.start < code_span.start &&
54
+ code_span.start <= inline_link.label_finish && inline_link.label_finish < code_span.finish
55
+ inline_link = nil
56
+ end
57
+ if inline_link && html_inline && inline_link.start < html_inline.begin(0) &&
58
+ html_inline.begin(0) < inline_link.finish && inline_link.finish < html_inline.end(0)
59
+ inline_link = nil
60
+ end
61
+ if inline_link && match.is_a?(DelimiterMatch) && match.start < inline_link.start &&
62
+ inline_link.start < match.inner_finish && match.inner_finish <= inline_link.label_finish
63
+ match = nil
64
+ end
65
+ reference = next_reference(cursor)
66
+ if match.is_a?(MatchData) && match[8] && reference && match.begin(0) < reference.start &&
67
+ match.end(9) > reference.start && match.end(9) < reference.finish
68
+ match = nil
69
+ elsif match.is_a?(DelimiterMatch) && reference && match.start < reference.start &&
70
+ match.inner_finish > reference.start && match.inner_finish < reference.finish
71
+ match = nil
72
+ end
73
+ if reference && code_span && reference.start < code_span.start &&
74
+ code_span.start < reference.finish && reference.finish < code_span.finish
75
+ reference = nil
76
+ end
77
+ if reference && html_inline && reference.start < html_inline.begin(0) &&
78
+ html_inline.begin(0) < reference.finish && reference.finish < html_inline.end(0)
79
+ reference = nil
80
+ end
81
+ candidates = [[:inline_link, inline_link], [:inline, match], [:code, code_span], [:break, line_break],
82
+ [:html, html_inline], [:footnote, footnote],
83
+ [:autolink, autolink], [:reference, reference]]
84
+ .compact.reject { |_kind, candidate| candidate.nil? }
85
+ kind, candidate = candidates.min_by { |candidate_kind, value| [candidate_start(candidate_kind, value), candidate_priority(candidate_kind)] }
86
+ start = candidate && candidate_start(kind, candidate)
87
+ unless candidate && start == cursor
88
+ next_cursor = start || @source.length
89
+ nodes << text_node(cursor, next_cursor)
90
+ cursor = next_cursor
91
+ next
92
+ end
93
+
94
+ if kind == :footnote
95
+ match = candidate
96
+ nodes << node(:footnote_reference, cursor, match.end(0), "[^",
97
+ { identifier: match[1] })
98
+ elsif kind == :break
99
+ nodes << node(candidate.type, candidate.start, candidate.finish, candidate.marker)
100
+ elsif kind == :html
101
+ nodes << node(:html_inline, candidate.begin(0), candidate.end(0), nil, text: candidate[0])
102
+ elsif kind == :inline_link
103
+ image = candidate.image
104
+ children = if image
105
+ []
106
+ else
107
+ InlineParser.new(candidate.label, @base_offset + byte_offset(candidate.label_start),
108
+ gfm: @gfm, references: @references).parse
109
+ end
110
+ nodes << node(image ? :image : :link, candidate.start, candidate.finish, image ? "![" : "[", {
111
+ label: candidate.label,
112
+ destination: candidate.destination,
113
+ title: candidate.title,
114
+ destination_range: range_for_offsets(*candidate.destination_range),
115
+ title_range: candidate.title_range && range_for_offsets(*candidate.title_range),
116
+ label_range: range_for_offsets(candidate.label_start, candidate.label_finish)
117
+ }, children)
118
+ elsif kind == :code
119
+ nodes << node(:code_span, cursor, candidate.finish, candidate.marker, text: candidate.text)
120
+ elsif candidate.is_a?(DelimiterMatch)
121
+ body = @source[candidate.inner_start...candidate.inner_finish]
122
+ type = case candidate.marker
123
+ when "**", "__" then :strong
124
+ when "~~" then @gfm ? :strikethrough : nil
125
+ else :emphasis
126
+ end
127
+ if type
128
+ children = InlineParser.new(body, @base_offset + byte_offset(candidate.inner_start),
129
+ gfm: @gfm, references: @references).parse
130
+ nodes << node(type, candidate.start, candidate.finish, candidate.marker, { text: body }, children)
131
+ else
132
+ nodes << text_node(candidate.start, candidate.finish)
133
+ end
134
+ elsif kind == :autolink
135
+ nodes << parse_autolink(candidate)
136
+ elsif kind == :reference
137
+ nodes << parse_reference(candidate)
138
+ elsif match[3]
139
+ match = candidate
140
+ image = match[3] == "!"
141
+ label = match[4]
142
+ destination = unescape_punctuation(match[5])
143
+ title = unescape_punctuation(match[6] || match[7])
144
+ title_range = if match[6]
145
+ range_for_capture(6, match)
146
+ elsif match[7]
147
+ range_for_capture(7, match)
148
+ end
149
+ type = image ? :image : :link
150
+ children = image ? [] : InlineParser.new(label, @base_offset + byte_offset(cursor) + 1, gfm: @gfm).parse
151
+ nodes << node(type, cursor, match.end(0), image ? "![" : "[", {
152
+ label: label, destination: destination, title: title,
153
+ destination_range: range_for_capture(5, match),
154
+ title_range: title_range,
155
+ label_range: range_for_capture(4, match)
156
+ }, children)
157
+ else
158
+ match = candidate
159
+ marker = match[8]
160
+ next_cursor = match.end(0)
161
+ type = case marker
162
+ when "**", "__" then :strong
163
+ when "~~" then @gfm ? :strikethrough : nil
164
+ else :emphasis
165
+ end
166
+ if type
167
+ inner_begin = match.begin(9)
168
+ inner_end = match.end(9)
169
+ children = InlineParser.new(@source[inner_begin...inner_end], @base_offset + byte_offset(inner_begin), gfm: @gfm).parse
170
+ nodes << node(type, cursor, next_cursor, marker, { text: @source[inner_begin...inner_end] }, children)
171
+ else
172
+ nodes << text_node(cursor, next_cursor)
173
+ end
174
+ end
175
+ cursor = %i[inline_link reference code break].include?(kind) || candidate.is_a?(DelimiterMatch) ? candidate.finish : candidate.end(0)
176
+ end
177
+ nodes
178
+ end
179
+
180
+ private
181
+
182
+ def candidate_start(kind, candidate)
183
+ return candidate.start if %i[inline_link reference code break].include?(kind) || candidate.is_a?(DelimiterMatch)
184
+
185
+ candidate.begin(0)
186
+ end
187
+
188
+ def candidate_priority(kind)
189
+ { inline_link: 0, inline: 1, code: 2, break: 3, html: 4, footnote: 5, autolink: 6, reference: 7 }.fetch(kind)
190
+ end
191
+
192
+ def next_inline_match(cursor)
193
+ match = TOKEN.match(@source, cursor)
194
+ while match
195
+ if match[1]
196
+ next_start = match.begin(1)
197
+ next_start += 1 while next_start < @source.length && @source[next_start] == "`"
198
+ match = TOKEN.match(@source, next_start)
199
+ elsif match[8]
200
+ delimiter_match = find_delimiter_match(match)
201
+ return delimiter_match if delimiter_match
202
+
203
+ match = TOKEN.match(@source, match.begin(8) + match[8].length)
204
+ elsif match[3]
205
+ # Direct links are parsed by inline_link_at so malformed link syntax
206
+ # cannot be accepted by the older, more permissive token expression.
207
+ match = TOKEN.match(@source, match.begin(0) + 1)
208
+ elsif escaped?(match.begin(0))
209
+ match = TOKEN.match(@source, match.end(0))
210
+ else
211
+ break
212
+ end
213
+ end
214
+ match
215
+ end
216
+
217
+ def next_inline_link(cursor)
218
+ search = cursor
219
+ while (start = @source.index(REFERENCE_START, search))
220
+ image = @source[start] == "!"
221
+ bracket = start + (image ? 1 : 0)
222
+ closing = closing_bracket(bracket)
223
+ if closing
224
+ link = inline_link_at(start, image, bracket, closing)
225
+ return link if link && (image || !nested_inline_link?(bracket + 1, closing))
226
+ end
227
+ search = start + 1
228
+ end
229
+ nil
230
+ end
231
+
232
+ def inline_link_at(start, image, bracket, closing)
233
+ opening_paren = closing + 1
234
+ return unless @source[opening_paren] == "("
235
+
236
+ cursor = skip_link_whitespace(opening_paren + 1)
237
+ destination, destination_range, cursor = link_destination(cursor)
238
+ return unless destination_range
239
+
240
+ whitespace_end = skip_link_whitespace(cursor)
241
+ title = nil
242
+ title_range = nil
243
+ if whitespace_end > cursor && ["\"", "'", "("].include?(@source[whitespace_end])
244
+ title_data = link_title(whitespace_end)
245
+ return unless title_data
246
+
247
+ title, title_range, cursor = title_data
248
+ cursor = skip_link_whitespace(cursor)
249
+ else
250
+ cursor = whitespace_end
251
+ end
252
+ return unless @source[cursor] == ")"
253
+
254
+ InlineLink.new(start: start, finish: cursor + 1, image: image,
255
+ label: @source[(bracket + 1)...closing], label_start: bracket + 1,
256
+ label_finish: closing, destination: unescape_punctuation(destination),
257
+ destination_range: destination_range, title: title && unescape_punctuation(title),
258
+ title_range: title_range)
259
+ end
260
+
261
+ def link_destination(cursor)
262
+ if @source[cursor] == "<"
263
+ destination_start = cursor + 1
264
+ index = destination_start
265
+ while index < @source.length
266
+ character = @source[index]
267
+ return unless character && !character.match?(/[\r\n]/)
268
+ return if character == "<" && !escaped?(index)
269
+ return if character == ">" && escaped?(index)
270
+ break if character == ">"
271
+
272
+ index += 1
273
+ end
274
+ return unless @source[index] == ">"
275
+
276
+ return [@source[destination_start...index], [destination_start, index], index + 1]
277
+ end
278
+
279
+ start = cursor
280
+ index = cursor
281
+ depth = 0
282
+ while index < @source.length
283
+ character = @source[index]
284
+ break if character.match?(/[ \t\r\n]/)
285
+ if character == "\\" && index + 1 < @source.length
286
+ index += 2
287
+ next
288
+ elsif character == "<"
289
+ return
290
+ elsif character == "("
291
+ depth += 1
292
+ return if depth > 32
293
+ elsif character == ")"
294
+ break if depth.zero?
295
+
296
+ depth -= 1
297
+ end
298
+ index += 1
299
+ end
300
+ return unless depth.zero?
301
+
302
+ [@source[start...index], [start, index], index]
303
+ end
304
+
305
+ def link_title(cursor)
306
+ opening = @source[cursor]
307
+ closer = { "\"" => "\"", "'" => "'", "(" => ")" }[opening]
308
+ return unless closer
309
+
310
+ start = cursor + 1
311
+ index = start
312
+ depth = 1
313
+ while index < @source.length
314
+ character = @source[index]
315
+ if character == "\\" && index + 1 < @source.length
316
+ index += 2
317
+ next
318
+ elsif character == closer
319
+ if opening != "(" || (depth -= 1).zero?
320
+ return [@source[start...index], [start, index], index + 1]
321
+ end
322
+ elsif opening == "(" && character == "("
323
+ depth += 1
324
+ end
325
+ index += 1
326
+ end
327
+ nil
328
+ end
329
+
330
+ def nested_inline_link?(start, finish)
331
+ search = start
332
+ while (nested_start = @source.index(REFERENCE_START, search))
333
+ break if nested_start >= finish
334
+
335
+ next_search = nested_start + 1
336
+ if @source[nested_start] != "!" && (nested_start.zero? || @source[nested_start - 1] != "!")
337
+ closing = closing_bracket(nested_start)
338
+ if closing && closing < finish
339
+ return true if inline_link_at(nested_start, false, nested_start, closing)
340
+
341
+ suffix = closing + 1
342
+ if @source[suffix] == "["
343
+ reference_end = closing_bracket(suffix)
344
+ if reference_end && reference_end < finish
345
+ label = @source[(suffix + 1)...reference_end]
346
+ label = @source[(nested_start + 1)...closing] if label.empty?
347
+ return true if @references.key?(normalize_reference_label(label))
348
+ end
349
+ else
350
+ label = @source[(nested_start + 1)...closing]
351
+ return true if @references.key?(normalize_reference_label(label))
352
+ end
353
+ end
354
+ end
355
+ search = next_search
356
+ end
357
+ false
358
+ end
359
+
360
+ def skip_link_whitespace(index)
361
+ index += 1 while index < @source.length && @source[index].match?(/[ \t\r\n]/)
362
+ index
363
+ end
364
+
365
+ def find_delimiter_match(match)
366
+ marker = match[8]
367
+ opening_start = match.begin(8)
368
+ return if escaped?(opening_start)
369
+ opening_length = delimiter_run_length(opening_start, marker[0])
370
+ opening_marker = marker[0] * opening_length
371
+ return unless can_open_delimiter?(opening_start, opening_marker)
372
+
373
+ search = opening_start + opening_length
374
+ ignored = [next_code_span(search), next_html_inline(search)].compact.map do |span|
375
+ span.respond_to?(:start) ? (span.start...span.finish) : (span.begin(0)...span.end(0))
376
+ end
377
+ nested_openers = 0
378
+ while (closing_start = @source.index(marker[0], search))
379
+ closing_length = delimiter_run_length(closing_start, marker[0])
380
+ if ignored.any? { |range| range.cover?(closing_start) }
381
+ span = ignored.find { |range| range.cover?(closing_start) }
382
+ search = span.end
383
+ next
384
+ end
385
+ if !escaped?(closing_start)
386
+ closing_marker = marker[0] * closing_length
387
+ can_open = can_open_delimiter?(closing_start, closing_marker)
388
+ can_close = can_close_delimiter?(closing_start, closing_marker)
389
+ if can_open && !can_close
390
+ nested_openers += 1
391
+ elsif can_close && nested_openers.positive?
392
+ nested_openers -= 1
393
+ elsif can_close && closing_start > opening_start + opening_length - 1 &&
394
+ !rule_of_three?(opening_start, closing_start, marker[0])
395
+ match_data = delimiter_pair(opening_start, opening_length, closing_start, closing_length, marker)
396
+ return match_data if match_data
397
+ end
398
+ end
399
+ search = closing_start + [closing_length, 1].max
400
+ end
401
+ nil
402
+ end
403
+
404
+ def delimiter_pair(opening_start, opening_length, closing_start, closing_length, marker)
405
+ if marker == "~~"
406
+ return unless opening_length == 2 && closing_length == 2
407
+
408
+ return DelimiterMatch.new(start: opening_start, finish: closing_start + 2,
409
+ marker: marker, inner_start: opening_start + 2,
410
+ inner_finish: closing_start)
411
+ end
412
+
413
+ width = if opening_length >= 2 && closing_length >= 2
414
+ opening_length.odd? && closing_length.odd? ? 1 : 2
415
+ elsif opening_length >= 2
416
+ 1
417
+ else
418
+ 1
419
+ end
420
+ start = opening_start
421
+ if width == 1 && opening_length >= 3 && closing_length >= 2 && closing_length.even?
422
+ start += 1
423
+ width = 2
424
+ end
425
+ return if opening_length - (start - opening_start) < width || closing_length < width
426
+
427
+ close = closing_start
428
+ close += closing_length - width if width == 1 && opening_length >= 3 && closing_length >= 3
429
+ tag = marker[0] * width
430
+ DelimiterMatch.new(start: start, finish: close + width, marker: tag,
431
+ inner_start: start + width, inner_finish: close)
432
+ end
433
+
434
+ def can_open_delimiter?(start, marker)
435
+ before = start.positive? ? @source[start - 1] : nil
436
+ after = @source[start + marker.length]
437
+ left, right = flanking?(before, after)
438
+ left && (marker[0] != "_" || !right || punctuation?(before))
439
+ end
440
+
441
+ def can_close_delimiter?(start, marker)
442
+ before = start.positive? ? @source[start - 1] : nil
443
+ after = @source[start + marker.length]
444
+ left, right = flanking?(before, after)
445
+ right && (marker[0] != "_" || !left || punctuation?(after))
446
+ end
447
+
448
+ def flanking?(before, after)
449
+ before_space = before.nil? || before.match?(/\p{Space}/)
450
+ after_space = after.nil? || after.match?(/\p{Space}/)
451
+ before_punctuation = punctuation?(before)
452
+ after_punctuation = punctuation?(after)
453
+ left = !after_space && (!after_punctuation || before_space || before_punctuation)
454
+ right = !before_space && (!before_punctuation || after_space || after_punctuation)
455
+ [left, right]
456
+ end
457
+
458
+ def punctuation?(character)
459
+ character && character.match?(/[\p{P}\p{S}]/)
460
+ end
461
+
462
+ def rule_of_three?(opening_start, closing_start, marker)
463
+ opening_length = delimiter_run_length(opening_start, marker[0])
464
+ closing_length = delimiter_run_length(closing_start, marker[0])
465
+ opening_can_close = can_close_delimiter?(opening_start, marker[0] * opening_length)
466
+ closing_can_open = can_open_delimiter?(closing_start, marker[0] * closing_length)
467
+ (opening_can_close || closing_can_open) && ((opening_length + closing_length) % 3).zero? &&
468
+ (opening_length % 3 != 0 || closing_length % 3 != 0)
469
+ end
470
+
471
+ def delimiter_run_length(start, character)
472
+ finish = start
473
+ finish += 1 while finish < @source.length && @source[finish] == character && !escaped?(finish)
474
+ finish - start
475
+ end
476
+
477
+ def next_autolink(cursor)
478
+ match = AUTOLINK.match(@source, cursor)
479
+ while match && escaped?(match.begin(0))
480
+ match = AUTOLINK.match(@source, match.end(0))
481
+ end
482
+ match
483
+ end
484
+
485
+ def next_html_inline(cursor)
486
+ match = HTML_INLINE.match(@source, cursor)
487
+ while match && escaped?(match.begin(0))
488
+ match = HTML_INLINE.match(@source, match.end(0))
489
+ end
490
+ match
491
+ end
492
+
493
+ def next_line_break(cursor)
494
+ search = cursor
495
+ while (newline = /\r\n|\r|\n/.match(@source, search))
496
+ newline_start = newline.begin(0)
497
+ slash_count = 0
498
+ slash_index = newline_start - 1
499
+ while slash_index >= cursor && @source[slash_index] == "\\"
500
+ slash_count += 1
501
+ slash_index -= 1
502
+ end
503
+ if slash_count.odd?
504
+ start = newline_start - 1
505
+ marker = "\\"
506
+ type = :linebreak
507
+ else
508
+ space_index = newline_start - 1
509
+ space_index -= 1 while space_index >= cursor && @source[space_index].match?(/[ \t]/)
510
+ spaces = newline_start - space_index - 1
511
+ start = spaces >= 2 ? newline_start - spaces : newline_start
512
+ marker = spaces >= 2 ? @source[start...newline_start] : nil
513
+ type = spaces >= 2 ? :linebreak : :softbreak
514
+ end
515
+ return Break.new(start: start, finish: newline.end(0), type: type, marker: marker) if start >= cursor
516
+
517
+ search = newline.end(0)
518
+ end
519
+ nil
520
+ end
521
+
522
+ def next_code_span(cursor)
523
+ index = @code_runs.bsearch_index { |run| run.start >= cursor } || @code_runs.length
524
+ while index < @code_runs.length
525
+ opening = @code_runs[index]
526
+ if escaped?(opening.start)
527
+ index += 1
528
+ next
529
+ end
530
+ closing = @code_closers[opening.start]
531
+ if closing
532
+ text = @source[opening.finish...closing.start].gsub(/\r\n|\r|\n/, " ")
533
+ text = text[1...-1] if text.start_with?(" ") && text.end_with?(" ") && !text.match?(/\A +\z/)
534
+ marker = @source[opening.start...opening.finish]
535
+ return CodeSpan.new(start: opening.start, finish: closing.finish, marker: marker, text: text)
536
+ end
537
+ index += 1
538
+ end
539
+ nil
540
+ end
541
+
542
+ def code_runs
543
+ runs = []
544
+ index = 0
545
+ while (start = @source.index("`", index))
546
+ finish = start + 1
547
+ finish += 1 while finish < @source.length && @source[finish] == "`"
548
+ runs << CodeRun.new(start: start, finish: finish, length: finish - start)
549
+ index = finish
550
+ end
551
+ runs
552
+ end
553
+
554
+ def parse_autolink(match)
555
+ label = match[1] || match[2]
556
+ first, last = match.begin(0), match.end(0)
557
+ label_first, label_last = match.begin(0) + 1, match.end(0) - 1
558
+ destination = match[2] ? "mailto:#{label}" : label
559
+ child = node(:text, label_first, label_last, nil, text: label, literal: true)
560
+ node(:link, first, last, "<", {
561
+ label: label, destination: destination, title: nil, autolink: true,
562
+ destination_range: range_for_offsets(label_first, label_last)
563
+ }, [child])
564
+ end
565
+
566
+ def parse_reference(reference)
567
+ definition = reference.definition
568
+ type = reference.image ? :image : :link
569
+ children = if reference.image
570
+ []
571
+ else
572
+ InlineParser.new(reference.label, @base_offset + byte_offset(reference.label_start),
573
+ gfm: @gfm, references: @references).parse
574
+ end
575
+ node(type, reference.start, reference.finish, reference.image ? "![" : "[", {
576
+ label: reference.label,
577
+ destination: definition[:destination],
578
+ title: definition[:title],
579
+ reference_label: reference.reference_label,
580
+ definition_range: definition[:range],
581
+ destination_range: definition[:destination_range],
582
+ label_range: range_for_offsets(reference.label_start, reference.label_end)
583
+ }, children)
584
+ end
585
+
586
+ def next_reference(cursor)
587
+ search = cursor
588
+ while (start = @source.index(REFERENCE_START, search))
589
+ image = @source[start] == "!"
590
+ bracket = start + (image ? 1 : 0)
591
+ unless @source[bracket] == "[" && !escaped?(start)
592
+ search = start + 1
593
+ next
594
+ end
595
+
596
+ label_start = bracket + 1
597
+ label_end = closing_bracket(bracket)
598
+ unless label_end && label_end > label_start
599
+ search = start + 1
600
+ next
601
+ end
602
+
603
+ label = @source[label_start...label_end]
604
+ suffix = reference_suffix_start(label_end + 1)
605
+ if @source[suffix] == "["
606
+ reference_end = closing_bracket(suffix)
607
+ unless reference_end
608
+ search = start + 1
609
+ next
610
+ end
611
+ reference_label = @source[(suffix + 1)...reference_end]
612
+ reference_label = label if reference_label.empty?
613
+ finish = reference_end + 1
614
+ else
615
+ reference_label = label
616
+ finish = label_end + 1
617
+ end
618
+
619
+ autolink = AUTOLINK.match(@source, start)
620
+ if autolink && autolink.begin(0) < finish && autolink.end(0) > finish
621
+ search = start + 1
622
+ next
623
+ end
624
+
625
+ if !image && nested_inline_link?(label_start, label_end)
626
+ search = start + 1
627
+ next
628
+ end
629
+
630
+ definition = @references[normalize_reference_label(reference_label)]
631
+ return Reference.new(start: start, finish: finish, image: image, label: label,
632
+ label_start: label_start, label_end: label_end,
633
+ reference_label: reference_label, definition: definition) if definition
634
+
635
+ search = start + 1
636
+ end
637
+ nil
638
+ end
639
+
640
+ def closing_bracket(opening)
641
+ depth = 0
642
+ index = opening
643
+ while index < @source.length
644
+ if escaped?(index)
645
+ index += 1
646
+ next
647
+ end
648
+ depth += 1 if @source[index] == "["
649
+ if @source[index] == "]"
650
+ depth -= 1
651
+ return index if depth.zero?
652
+ end
653
+ index += 1
654
+ end
655
+ nil
656
+ end
657
+
658
+ def escaped?(index)
659
+ slashes = 0
660
+ index -= 1
661
+ while index >= 0 && @source[index] == "\\"
662
+ slashes += 1
663
+ index -= 1
664
+ end
665
+ slashes.odd?
666
+ end
667
+
668
+ def reference_suffix_start(index)
669
+ index
670
+ end
671
+
672
+ def normalize_reference_label(label)
673
+ label.gsub(/[[:space:]]+/, " ").strip.downcase(:fold)
674
+ end
675
+
676
+ def unescape_punctuation(text)
677
+ text&.gsub(/\\([[:punct:]])/, "\\1")
678
+ end
679
+
680
+ def text_node(first, last)
681
+ text = unescape_punctuation(@source[first...last])
682
+ node(:text, first, last, nil, text: text)
683
+ end
684
+
685
+ def node(type, first, last, marker, attributes = {}, children = [])
686
+ Node.new(type: type, attributes: attributes, children: children,
687
+ range: (@base_offset + byte_offset(first))...(@base_offset + byte_offset(last)), marker: marker)
688
+ end
689
+
690
+ def byte_offset(character_offset)
691
+ @source[0...character_offset].bytesize
692
+ end
693
+
694
+ def range_for_capture(index, match)
695
+ range_for_offsets(match.begin(index), match.end(index))
696
+ end
697
+
698
+ def range_for_offsets(first, last)
699
+ first = @base_offset + byte_offset(first)
700
+ last = @base_offset + byte_offset(last)
701
+ first...last
702
+ end
703
+ end
704
+ end