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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +52 -0
- data/docs/adr/001-byte-range-edits.md +16 -0
- data/docs/adr/002-html-comment-directives.md +16 -0
- data/lib/beid/directive.rb +29 -0
- data/lib/beid/document.rb +112 -0
- data/lib/beid/editing.rb +173 -0
- data/lib/beid/inline_parser.rb +704 -0
- data/lib/beid/node.rb +27 -0
- data/lib/beid/parser.rb +851 -0
- data/lib/beid/version.rb +5 -0
- data/lib/beid.rb +13 -0
- data/sig/beid.rbs +51 -0
- metadata +60 -0
|
@@ -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
|