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
data/lib/beid/parser.rb
ADDED
|
@@ -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
|