mdlint 0.1.0 → 0.2.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 +4 -4
- data/.pre-commit-hooks.yaml +6 -0
- data/CHANGELOG.md +33 -0
- data/README.md +136 -4
- data/Rakefile +14 -0
- data/Steepfile +17 -0
- data/action.yml +54 -0
- data/benchmark/compare.rb +49 -0
- data/benchmark/format.rb +29 -0
- data/lib/mdlint/cache_store.rb +85 -0
- data/lib/mdlint/cli/output_formatter.rb +150 -0
- data/lib/mdlint/cli.rb +268 -106
- data/lib/mdlint/config.rb +87 -1
- data/lib/mdlint/dialect.rb +53 -0
- data/lib/mdlint/linter/directive_filter.rb +91 -0
- data/lib/mdlint/linter/rule.rb +25 -5
- data/lib/mdlint/linter/rule_engine.rb +44 -7
- data/lib/mdlint/linter/rules/code_block_syntax.rb +128 -0
- data/lib/mdlint/linter/rules/first_line_heading.rb +10 -3
- data/lib/mdlint/linter/rules/heading_increment.rb +4 -3
- data/lib/mdlint/linter/rules/heading_style.rb +24 -2
- data/lib/mdlint/linter/rules/japanese.rb +201 -0
- data/lib/mdlint/linter/rules/line_length.rb +37 -0
- data/lib/mdlint/linter/rules/link_check.rb +151 -0
- data/lib/mdlint/linter/rules/no_multiple_blanks.rb +1 -0
- data/lib/mdlint/linter/rules/no_trailing_spaces.rb +1 -0
- data/lib/mdlint/linter/rules/source_style.rb +317 -0
- data/lib/mdlint/linter/violation.rb +16 -1
- data/lib/mdlint/linter.rb +9 -3
- data/lib/mdlint/lsp.rb +176 -0
- data/lib/mdlint/parallel_runner.rb +42 -0
- data/lib/mdlint/parser/block_parser.rb +627 -50
- data/lib/mdlint/parser/inline_parser.rb +259 -27
- data/lib/mdlint/parser/state.rb +21 -2
- data/lib/mdlint/parser.rb +5 -5
- data/lib/mdlint/plugin.rb +31 -0
- data/lib/mdlint/renderer/html_renderer.rb +346 -0
- data/lib/mdlint/renderer/md_renderer.rb +147 -11
- data/lib/mdlint/renderer.rb +5 -0
- data/lib/mdlint/text_width.rb +39 -0
- data/lib/mdlint/toc.rb +80 -0
- data/lib/mdlint/token.rb +3 -1
- data/lib/mdlint/version.rb +1 -1
- data/lib/mdlint.rb +25 -5
- data/script/commonmark_compatibility.rb +24 -0
- data/script/fetch_commonmark_spec.rb +14 -0
- data/sig/internal.rbs +405 -0
- data/sig/mdlint.rbs +107 -0
- metadata +26 -2
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "state"
|
|
4
|
+
require_relative "../dialect"
|
|
4
5
|
|
|
5
6
|
module Mdlint
|
|
6
7
|
module Parser
|
|
7
8
|
class BlockParser
|
|
8
9
|
ATX_HEADING_REGEXP = /\A {0,3}(\#{1,6})(?:\s+(.*))?$/
|
|
9
10
|
SETEXT_HEADING_REGEXP = /\A {0,3}(=+|-+)\s*\z/
|
|
10
|
-
FENCE_OPEN_REGEXP = /\A {0,3}(`{3,}|~{3,})(
|
|
11
|
+
FENCE_OPEN_REGEXP = /\A {0,3}(`{3,}|~{3,})(.*)\z/
|
|
11
12
|
BLOCKQUOTE_REGEXP = /\A {0,3}> ?/
|
|
12
13
|
HR_REGEXP = /\A {0,3}([-*_])(?:\s*\1){2,}\s*\z/
|
|
13
14
|
BULLET_LIST_REGEXP = /\A( {0,3})([-*+])\s+/
|
|
14
15
|
ORDERED_LIST_REGEXP = /\A( {0,3})(\d{1,9})([.)])\s+/
|
|
15
16
|
CODE_BLOCK_INDENT = /\A {4}/
|
|
16
|
-
HTML_BLOCK_START_1 = /\A {0,3}<(script|pre|style|textarea)[\s>]/i
|
|
17
|
+
HTML_BLOCK_START_1 = /\A {0,3}<(script|pre|style|textarea)(?:[\s>]|\z)/i
|
|
17
18
|
HTML_BLOCK_START_2 = /\A {0,3}<!--/
|
|
18
19
|
HTML_BLOCK_START_3 = /\A {0,3}<\?/
|
|
19
20
|
HTML_BLOCK_START_4 = /\A {0,3}<![A-Z]/
|
|
20
21
|
HTML_BLOCK_START_5 = /\A {0,3}<!\[CDATA\[/
|
|
21
22
|
HTML_BLOCK_START_6 = /\A {0,3}<\/?(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?:\s|\/?>|$)/i
|
|
23
|
+
HTML_BLOCK_START_7 = /\A {0,3}<\/?[A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z0-9_-]+)?(?:\s[^>]*>|\/?>)\s*\z/
|
|
22
24
|
# Reference definition: [label]: url "title"
|
|
23
|
-
REFERENCE_DEF_REGEXP = /\A {0,3}\[([^\]]+)\]
|
|
25
|
+
REFERENCE_DEF_REGEXP = /\A {0,3}\[((?:\\.|[^\]])+)\]:/
|
|
26
|
+
FOOTNOTE_DEF_REGEXP = /\A {0,3}\[\^([^\]]+)\]:\s*(.*?)\s*$/
|
|
27
|
+
MATH_BLOCK_REGEXP = /\A {0,3}\$\$\s*$/
|
|
24
28
|
|
|
25
29
|
def initialize(options = {})
|
|
26
30
|
@options = options
|
|
31
|
+
@dialect = Dialect.resolve(options[:dialect])
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
def parse(src)
|
|
@@ -41,20 +46,176 @@ module Mdlint
|
|
|
41
46
|
def parse_block(state)
|
|
42
47
|
return if state.eof?
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
parse_front_matter(state) ||
|
|
50
|
+
parse_blank_line(state) ||
|
|
45
51
|
parse_atx_heading(state) ||
|
|
52
|
+
parse_directive(state) ||
|
|
53
|
+
parse_math_block(state) ||
|
|
46
54
|
parse_fence(state) ||
|
|
47
55
|
parse_hr(state) ||
|
|
48
56
|
parse_blockquote(state) ||
|
|
49
57
|
parse_bullet_list(state) ||
|
|
50
58
|
parse_ordered_list(state) ||
|
|
59
|
+
parse_table(state) ||
|
|
51
60
|
parse_html_block(state) ||
|
|
61
|
+
parse_footnote_definition(state) ||
|
|
52
62
|
parse_reference_definition(state) ||
|
|
53
63
|
parse_code_block(state) ||
|
|
54
64
|
parse_setext_heading(state) ||
|
|
55
65
|
parse_paragraph(state)
|
|
56
66
|
end
|
|
57
67
|
|
|
68
|
+
def parse_directive(state)
|
|
69
|
+
line = state.current_line
|
|
70
|
+
match = line.match(/\A {0,3}:::([A-Za-z][\w-]*)(?:\s+(.*?))?\s*\z/)
|
|
71
|
+
return false unless match
|
|
72
|
+
|
|
73
|
+
closing_line = find_directive_closing_line(state, match[1])
|
|
74
|
+
return false unless closing_line
|
|
75
|
+
|
|
76
|
+
start_line = state.line
|
|
77
|
+
content = state.raw_lines[start_line..closing_line].join("\n")
|
|
78
|
+
content += "\n" if closing_line < state.lines.length - 1 || state.src.end_with?("\n")
|
|
79
|
+
state.tokens << Token.new(
|
|
80
|
+
type: :directive,
|
|
81
|
+
content: content,
|
|
82
|
+
meta: { name: match[1], title: match[2] },
|
|
83
|
+
map: [start_line, closing_line + 1]
|
|
84
|
+
)
|
|
85
|
+
state.line = closing_line + 1
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_math_block(state)
|
|
90
|
+
return false unless state.current_line.match?(MATH_BLOCK_REGEXP)
|
|
91
|
+
|
|
92
|
+
start_line = state.line
|
|
93
|
+
content_lines = [state.current_line]
|
|
94
|
+
state.next_line
|
|
95
|
+
until state.eof?
|
|
96
|
+
content_lines << state.raw_line
|
|
97
|
+
state.next_line
|
|
98
|
+
break if content_lines.last.match?(MATH_BLOCK_REGEXP)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
state.tokens << Token.new(
|
|
102
|
+
type: :math_block,
|
|
103
|
+
content: content_lines.join("\n") + "\n",
|
|
104
|
+
map: [start_line, state.line]
|
|
105
|
+
)
|
|
106
|
+
true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def parse_table(state)
|
|
110
|
+
return false unless @dialect.feature?(:tables)
|
|
111
|
+
return false unless table_delimiter?(state.peek_line)
|
|
112
|
+
return false unless state.current_line.include?("|")
|
|
113
|
+
|
|
114
|
+
start_line = state.line
|
|
115
|
+
header = split_table_row(state.current_line)
|
|
116
|
+
alignments = split_table_row(state.peek_line).map { |cell| table_alignment(cell) }
|
|
117
|
+
rows = [header]
|
|
118
|
+
state.next_line
|
|
119
|
+
state.next_line
|
|
120
|
+
|
|
121
|
+
while !state.eof? && table_row?(state.current_line)
|
|
122
|
+
rows << split_table_row(state.current_line)
|
|
123
|
+
state.next_line
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
state.tokens << Token.new(
|
|
127
|
+
type: :table,
|
|
128
|
+
meta: { rows: rows, alignments: alignments },
|
|
129
|
+
map: [start_line, state.line]
|
|
130
|
+
)
|
|
131
|
+
true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def table_delimiter?(line)
|
|
135
|
+
return false unless line
|
|
136
|
+
|
|
137
|
+
cells = split_table_row(line)
|
|
138
|
+
cells.length > 0 && cells.all? { |cell| cell.match?(/\A\s*:?-{3,}:?\s*\z/) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def table_row?(line)
|
|
142
|
+
line && line.include?("|") && !line.strip.empty?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def split_table_row(line)
|
|
146
|
+
value = line.to_s.strip
|
|
147
|
+
value = value[1..] if value.start_with?("|")
|
|
148
|
+
value = value[0...-1] if value.end_with?("|") && !value.end_with?("\\|")
|
|
149
|
+
cells = []
|
|
150
|
+
current = +""
|
|
151
|
+
escaped = false
|
|
152
|
+
value.each_char do |character|
|
|
153
|
+
if character == "|" && !escaped
|
|
154
|
+
cells << current.strip
|
|
155
|
+
current = +""
|
|
156
|
+
else
|
|
157
|
+
current << character
|
|
158
|
+
end
|
|
159
|
+
escaped = character == "\\" && !escaped
|
|
160
|
+
escaped = false if character != "\\"
|
|
161
|
+
end
|
|
162
|
+
cells << current.strip
|
|
163
|
+
cells
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def table_alignment(cell)
|
|
167
|
+
value = cell.strip
|
|
168
|
+
return :center if value.start_with?(":") && value.end_with?(":")
|
|
169
|
+
return :left if value.start_with?(":")
|
|
170
|
+
return :right if value.end_with?(":")
|
|
171
|
+
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def parse_front_matter(state)
|
|
176
|
+
return false unless state.line.zero?
|
|
177
|
+
|
|
178
|
+
opener = state.current_line
|
|
179
|
+
format, closing_marker = front_matter_markers(opener)
|
|
180
|
+
return false unless format
|
|
181
|
+
|
|
182
|
+
closing_line = state.lines[(state.line + 1)..]&.index do |line|
|
|
183
|
+
line.strip == closing_marker
|
|
184
|
+
end
|
|
185
|
+
return false unless closing_line
|
|
186
|
+
|
|
187
|
+
closing_line += state.line + 1
|
|
188
|
+
start_line = state.line
|
|
189
|
+
raw_lines = state.raw_lines[start_line..closing_line]
|
|
190
|
+
payload = state.lines[(start_line + 1)...closing_line].to_a
|
|
191
|
+
return false unless front_matter_payload?(format, payload)
|
|
192
|
+
|
|
193
|
+
content = raw_lines.join("\n")
|
|
194
|
+
content += "\n" if closing_line < state.lines.length - 1 || state.src.end_with?("\n")
|
|
195
|
+
|
|
196
|
+
state.tokens << Token.new(
|
|
197
|
+
type: :front_matter,
|
|
198
|
+
content: content,
|
|
199
|
+
meta: { format: format, delimiter: closing_marker },
|
|
200
|
+
map: [start_line, closing_line + 1]
|
|
201
|
+
)
|
|
202
|
+
state.line = closing_line + 1
|
|
203
|
+
true
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def front_matter_markers(opener)
|
|
207
|
+
case opener.strip
|
|
208
|
+
when "---"
|
|
209
|
+
[:yaml, "---"]
|
|
210
|
+
when "+++"
|
|
211
|
+
[:toml, "+++"]
|
|
212
|
+
when ";;;"
|
|
213
|
+
[:json, ";;;"]
|
|
214
|
+
when "{"
|
|
215
|
+
[:json, "}"]
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
58
219
|
def parse_blank_line(state)
|
|
59
220
|
return false unless state.blank_line?
|
|
60
221
|
|
|
@@ -68,7 +229,8 @@ module Mdlint
|
|
|
68
229
|
return false unless match
|
|
69
230
|
|
|
70
231
|
level = match[1].length
|
|
71
|
-
content = match[2]
|
|
232
|
+
content = match[2].to_s.gsub(/\s+#+\s*\z/, "").strip
|
|
233
|
+
content = "" if content.match?(/\A#+\s*\z/)
|
|
72
234
|
|
|
73
235
|
start_line = state.line
|
|
74
236
|
|
|
@@ -102,19 +264,23 @@ module Mdlint
|
|
|
102
264
|
end
|
|
103
265
|
|
|
104
266
|
def parse_setext_heading(state)
|
|
105
|
-
return false if state.line.zero?
|
|
106
|
-
|
|
107
267
|
line = state.current_line
|
|
108
268
|
return false if line.match?(/\A\s*\z/)
|
|
109
269
|
|
|
110
|
-
|
|
111
|
-
|
|
270
|
+
underline_index = state.line + 1
|
|
271
|
+
while underline_index < state.lines.length && !state.blank_line?(underline_index)
|
|
272
|
+
break if state.lines[underline_index].match?(SETEXT_HEADING_REGEXP)
|
|
273
|
+
break if block_boundary?(state.lines[underline_index])
|
|
274
|
+
|
|
275
|
+
underline_index += 1
|
|
276
|
+
end
|
|
277
|
+
return false if underline_index >= state.lines.length
|
|
112
278
|
|
|
113
|
-
match =
|
|
279
|
+
match = state.lines[underline_index].match(SETEXT_HEADING_REGEXP)
|
|
114
280
|
return false unless match
|
|
115
281
|
|
|
116
282
|
level = match[1][0] == "=" ? 1 : 2
|
|
117
|
-
content = line.strip
|
|
283
|
+
content = state.lines[state.line...underline_index].join("\n").strip
|
|
118
284
|
start_line = state.line
|
|
119
285
|
|
|
120
286
|
state.tokens << Token.new(
|
|
@@ -123,14 +289,14 @@ module Mdlint
|
|
|
123
289
|
nesting: 1,
|
|
124
290
|
level: state.level,
|
|
125
291
|
markup: match[1][0],
|
|
126
|
-
map: [start_line,
|
|
292
|
+
map: [start_line, underline_index + 1]
|
|
127
293
|
)
|
|
128
294
|
|
|
129
295
|
state.tokens << Token.new(
|
|
130
296
|
type: :inline,
|
|
131
297
|
content: content,
|
|
132
298
|
level: state.level + 1,
|
|
133
|
-
map: [start_line,
|
|
299
|
+
map: [start_line, underline_index]
|
|
134
300
|
)
|
|
135
301
|
|
|
136
302
|
state.tokens << Token.new(
|
|
@@ -141,32 +307,35 @@ module Mdlint
|
|
|
141
307
|
markup: match[1][0]
|
|
142
308
|
)
|
|
143
309
|
|
|
144
|
-
state.
|
|
145
|
-
state.next_line
|
|
310
|
+
state.line = underline_index + 1
|
|
146
311
|
true
|
|
147
312
|
end
|
|
148
313
|
|
|
149
314
|
def parse_fence(state)
|
|
150
315
|
line = state.current_line
|
|
151
|
-
match = line
|
|
316
|
+
match = fence_match(line)
|
|
152
317
|
return false unless match
|
|
153
318
|
|
|
154
319
|
marker = match[1]
|
|
155
320
|
info = match[2].strip
|
|
321
|
+
|
|
156
322
|
fence_char = marker[0]
|
|
157
323
|
fence_length = marker.length
|
|
324
|
+
opening_indent = line[/\A */].length
|
|
158
325
|
start_line = state.line
|
|
159
326
|
state.next_line
|
|
160
327
|
|
|
161
328
|
content_lines = []
|
|
162
329
|
until state.eof?
|
|
163
330
|
current = state.current_line
|
|
331
|
+
break if state.line == state.lines.length - 1 && current.empty?
|
|
332
|
+
|
|
164
333
|
close_match = current.match(/\A {0,3}#{fence_char}{#{fence_length},}\s*\z/)
|
|
165
334
|
if close_match
|
|
166
335
|
state.next_line
|
|
167
336
|
break
|
|
168
337
|
end
|
|
169
|
-
content_lines <<
|
|
338
|
+
content_lines << strip_fence_indent(state.raw_line, opening_indent)
|
|
170
339
|
state.next_line
|
|
171
340
|
end
|
|
172
341
|
|
|
@@ -204,9 +373,16 @@ module Mdlint
|
|
|
204
373
|
start_line = state.line
|
|
205
374
|
content_lines = []
|
|
206
375
|
|
|
207
|
-
while !state.eof?
|
|
208
|
-
|
|
209
|
-
|
|
376
|
+
while !state.eof?
|
|
377
|
+
if state.current_line.match?(BLOCKQUOTE_REGEXP)
|
|
378
|
+
content_lines << state.raw_line.sub(BLOCKQUOTE_REGEXP, "")
|
|
379
|
+
state.next_line
|
|
380
|
+
elsif !state.blank_line? && lazy_blockquote_continuation?(state.current_line)
|
|
381
|
+
content_lines << state.raw_line.sub(/\A\s+/, "")
|
|
382
|
+
state.next_line
|
|
383
|
+
else
|
|
384
|
+
break
|
|
385
|
+
end
|
|
210
386
|
end
|
|
211
387
|
|
|
212
388
|
state.tokens << Token.new(
|
|
@@ -215,9 +391,12 @@ module Mdlint
|
|
|
215
391
|
nesting: 1,
|
|
216
392
|
level: state.level,
|
|
217
393
|
markup: ">",
|
|
394
|
+
attrs: alert_attributes(content_lines),
|
|
218
395
|
map: [start_line, state.line]
|
|
219
396
|
)
|
|
220
397
|
|
|
398
|
+
content_lines[0] = content_lines[0].sub(/\A\[!(?:NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/i, "") if alert_attributes(content_lines)[:alert]
|
|
399
|
+
|
|
221
400
|
state.level += 1
|
|
222
401
|
inner_content = content_lines.join("\n")
|
|
223
402
|
inner_parser = BlockParser.new(@options)
|
|
@@ -258,12 +437,13 @@ module Mdlint
|
|
|
258
437
|
nesting: 1,
|
|
259
438
|
level: state.level,
|
|
260
439
|
markup: marker,
|
|
440
|
+
attrs: { tight: true },
|
|
261
441
|
map: [start_line, nil]
|
|
262
442
|
)
|
|
263
443
|
list_token_index = state.tokens.length - 1
|
|
264
444
|
|
|
265
445
|
state.level += 1
|
|
266
|
-
parse_list_items(state, BULLET_LIST_REGEXP, marker)
|
|
446
|
+
parse_list_items(state, BULLET_LIST_REGEXP, marker, match[1].length)
|
|
267
447
|
state.level -= 1
|
|
268
448
|
|
|
269
449
|
state.tokens[list_token_index].map[1] = state.line
|
|
@@ -294,13 +474,13 @@ module Mdlint
|
|
|
294
474
|
nesting: 1,
|
|
295
475
|
level: state.level,
|
|
296
476
|
markup: delimiter,
|
|
297
|
-
attrs: { start: start_num },
|
|
477
|
+
attrs: { start: start_num, tight: true },
|
|
298
478
|
map: [start_line, nil]
|
|
299
479
|
)
|
|
300
480
|
list_token_index = state.tokens.length - 1
|
|
301
481
|
|
|
302
482
|
state.level += 1
|
|
303
|
-
parse_ordered_list_items(state, delimiter)
|
|
483
|
+
parse_ordered_list_items(state, delimiter, match[1].length)
|
|
304
484
|
state.level -= 1
|
|
305
485
|
|
|
306
486
|
state.tokens[list_token_index].map[1] = state.line
|
|
@@ -316,11 +496,12 @@ module Mdlint
|
|
|
316
496
|
true
|
|
317
497
|
end
|
|
318
498
|
|
|
319
|
-
def parse_list_items(state, pattern, _marker)
|
|
499
|
+
def parse_list_items(state, pattern, _marker, base_indent = 0)
|
|
320
500
|
while !state.eof?
|
|
321
501
|
line = state.current_line
|
|
322
502
|
match = line.match(pattern)
|
|
323
503
|
break unless match
|
|
504
|
+
break if line.match?(HR_REGEXP) && match[1].length <= base_indent
|
|
324
505
|
|
|
325
506
|
item_start = state.line
|
|
326
507
|
content = line.sub(pattern, "")
|
|
@@ -330,6 +511,7 @@ module Mdlint
|
|
|
330
511
|
tag: "li",
|
|
331
512
|
nesting: 1,
|
|
332
513
|
level: state.level,
|
|
514
|
+
attrs: task_attributes(content).merge(tight: true),
|
|
333
515
|
map: [item_start, nil]
|
|
334
516
|
)
|
|
335
517
|
item_token_index = state.tokens.length - 1
|
|
@@ -337,17 +519,44 @@ module Mdlint
|
|
|
337
519
|
state.level += 1
|
|
338
520
|
state.next_line
|
|
339
521
|
|
|
522
|
+
content = strip_task_marker(content)
|
|
523
|
+
|
|
340
524
|
item_content_lines = [content]
|
|
525
|
+
nested_lines = []
|
|
526
|
+
nested_start_line = state.line
|
|
341
527
|
while !state.eof? && !state.blank_line? && !state.current_line.match?(pattern)
|
|
342
|
-
if state.current_line.match?(/\A\s+/)
|
|
343
|
-
|
|
528
|
+
if nested_lines.any? && state.current_line.match?(/\A\s+/)
|
|
529
|
+
nested_lines << dedent_list_line(state.current_line, base_indent)
|
|
530
|
+
state.next_line
|
|
531
|
+
elsif state.current_line.match?(/\A\s+/)
|
|
532
|
+
item_content_lines << state.raw_line.sub(/\A\s+/, "")
|
|
344
533
|
state.next_line
|
|
345
534
|
else
|
|
346
535
|
break
|
|
347
536
|
end
|
|
348
537
|
end
|
|
349
538
|
|
|
539
|
+
while !state.eof? && !state.blank_line?
|
|
540
|
+
nested_match = state.current_line.match(pattern)
|
|
541
|
+
break unless nested_match && nested_match[1].length > base_indent
|
|
542
|
+
|
|
543
|
+
nested_lines << dedent_list_line(state.current_line, base_indent)
|
|
544
|
+
state.next_line
|
|
545
|
+
while !state.eof? && !state.blank_line? && !state.current_line.match?(pattern)
|
|
546
|
+
break unless state.current_line.match?(/\A\s+/)
|
|
547
|
+
|
|
548
|
+
nested_lines << dedent_list_line(state.current_line, base_indent)
|
|
549
|
+
state.next_line
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
loose_content = collect_loose_item_content(state, pattern, base_indent, nested_lines, match[0].length)
|
|
554
|
+
|
|
350
555
|
paragraph_content = item_content_lines.join("\n").strip
|
|
556
|
+
if paragraph_content.match?(HR_REGEXP)
|
|
557
|
+
state.tokens << Token.new(type: :hr, tag: "hr", markup: paragraph_content.strip[0], map: [item_start, state.line])
|
|
558
|
+
paragraph_content = ""
|
|
559
|
+
end
|
|
351
560
|
unless paragraph_content.empty?
|
|
352
561
|
state.tokens << Token.new(
|
|
353
562
|
type: :paragraph_open,
|
|
@@ -372,6 +581,9 @@ module Mdlint
|
|
|
372
581
|
)
|
|
373
582
|
end
|
|
374
583
|
|
|
584
|
+
append_nested_tokens(state, nested_lines, nested_start_line)
|
|
585
|
+
append_loose_paragraph(state, loose_content, item_start) if loose_content
|
|
586
|
+
|
|
375
587
|
state.level -= 1
|
|
376
588
|
state.tokens[item_token_index].map[1] = state.line
|
|
377
589
|
|
|
@@ -386,13 +598,14 @@ module Mdlint
|
|
|
386
598
|
end
|
|
387
599
|
end
|
|
388
600
|
|
|
389
|
-
def parse_ordered_list_items(state, delimiter)
|
|
601
|
+
def parse_ordered_list_items(state, delimiter, base_indent = 0)
|
|
390
602
|
pattern = /\A( {0,3})(\d{1,9})([#{Regexp.escape(delimiter)}])\s+/
|
|
391
603
|
|
|
392
604
|
while !state.eof?
|
|
393
605
|
line = state.current_line
|
|
394
606
|
match = line.match(pattern)
|
|
395
607
|
break unless match
|
|
608
|
+
break if line.match?(HR_REGEXP) && match[1].length <= base_indent
|
|
396
609
|
|
|
397
610
|
item_start = state.line
|
|
398
611
|
content = line.sub(pattern, "")
|
|
@@ -402,6 +615,7 @@ module Mdlint
|
|
|
402
615
|
tag: "li",
|
|
403
616
|
nesting: 1,
|
|
404
617
|
level: state.level,
|
|
618
|
+
attrs: task_attributes(content).merge(tight: true),
|
|
405
619
|
map: [item_start, nil]
|
|
406
620
|
)
|
|
407
621
|
item_token_index = state.tokens.length - 1
|
|
@@ -409,17 +623,44 @@ module Mdlint
|
|
|
409
623
|
state.level += 1
|
|
410
624
|
state.next_line
|
|
411
625
|
|
|
626
|
+
content = strip_task_marker(content)
|
|
627
|
+
|
|
412
628
|
item_content_lines = [content]
|
|
629
|
+
nested_lines = []
|
|
630
|
+
nested_start_line = state.line
|
|
413
631
|
while !state.eof? && !state.blank_line? && !state.current_line.match?(pattern)
|
|
414
|
-
if state.current_line.match?(/\A\s+/)
|
|
415
|
-
|
|
632
|
+
if nested_lines.any? && state.current_line.match?(/\A\s+/)
|
|
633
|
+
nested_lines << dedent_list_line(state.current_line, base_indent)
|
|
634
|
+
state.next_line
|
|
635
|
+
elsif state.current_line.match?(/\A\s+/)
|
|
636
|
+
item_content_lines << state.raw_line.sub(/\A\s+/, "")
|
|
416
637
|
state.next_line
|
|
417
638
|
else
|
|
418
639
|
break
|
|
419
640
|
end
|
|
420
641
|
end
|
|
421
642
|
|
|
643
|
+
while !state.eof? && !state.blank_line?
|
|
644
|
+
nested_match = state.current_line.match(pattern)
|
|
645
|
+
break unless nested_match && nested_match[1].length > base_indent
|
|
646
|
+
|
|
647
|
+
nested_lines << dedent_list_line(state.current_line, base_indent)
|
|
648
|
+
state.next_line
|
|
649
|
+
while !state.eof? && !state.blank_line? && !state.current_line.match?(pattern)
|
|
650
|
+
break unless state.current_line.match?(/\A\s+/)
|
|
651
|
+
|
|
652
|
+
nested_lines << dedent_list_line(state.current_line, base_indent)
|
|
653
|
+
state.next_line
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
loose_content = collect_loose_item_content(state, pattern, base_indent, nested_lines, match[0].length)
|
|
658
|
+
|
|
422
659
|
paragraph_content = item_content_lines.join("\n").strip
|
|
660
|
+
if paragraph_content.match?(HR_REGEXP)
|
|
661
|
+
state.tokens << Token.new(type: :hr, tag: "hr", markup: paragraph_content.strip[0], map: [item_start, state.line])
|
|
662
|
+
paragraph_content = ""
|
|
663
|
+
end
|
|
423
664
|
unless paragraph_content.empty?
|
|
424
665
|
state.tokens << Token.new(
|
|
425
666
|
type: :paragraph_open,
|
|
@@ -444,6 +685,9 @@ module Mdlint
|
|
|
444
685
|
)
|
|
445
686
|
end
|
|
446
687
|
|
|
688
|
+
append_nested_tokens(state, nested_lines, nested_start_line)
|
|
689
|
+
append_loose_paragraph(state, loose_content, item_start) if loose_content
|
|
690
|
+
|
|
447
691
|
state.level -= 1
|
|
448
692
|
state.tokens[item_token_index].map[1] = state.line
|
|
449
693
|
|
|
@@ -458,23 +702,128 @@ module Mdlint
|
|
|
458
702
|
end
|
|
459
703
|
end
|
|
460
704
|
|
|
705
|
+
def dedent_list_line(line, base_indent)
|
|
706
|
+
line[(base_indent + 2)..] || line.lstrip
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def append_nested_tokens(state, nested_lines, start_line)
|
|
710
|
+
return if nested_lines.empty?
|
|
711
|
+
|
|
712
|
+
nested_tokens = BlockParser.new(@options).parse(nested_lines.join("\n"))
|
|
713
|
+
nested_tokens.each do |token|
|
|
714
|
+
token.level += state.level
|
|
715
|
+
token.map = token.map.map { |line| line + start_line } if token.map
|
|
716
|
+
state.tokens << token
|
|
717
|
+
end
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def collect_loose_item_content(state, pattern, base_indent, nested_lines = nil, content_indent = base_indent + 2)
|
|
721
|
+
return unless state.blank_line?
|
|
722
|
+
|
|
723
|
+
state.skip_blank_lines
|
|
724
|
+
return if state.eof?
|
|
725
|
+
|
|
726
|
+
next_match = state.current_line.match(pattern)
|
|
727
|
+
if next_match && next_match[1].length <= base_indent
|
|
728
|
+
mark_current_list_loose(state)
|
|
729
|
+
return
|
|
730
|
+
end
|
|
731
|
+
return unless state.current_line.match?(/\A {#{content_indent},}/)
|
|
732
|
+
|
|
733
|
+
mark_current_list_loose(state)
|
|
734
|
+
if state.current_line.match?(/\A {#{base_indent + 4},}/) && nested_lines
|
|
735
|
+
while !state.eof?
|
|
736
|
+
if state.blank_line?
|
|
737
|
+
index = state.line
|
|
738
|
+
index += 1 while index < state.lines.length && state.blank_line?(index)
|
|
739
|
+
break if index >= state.lines.length || !state.lines[index].match?(/\A\s+/)
|
|
740
|
+
|
|
741
|
+
nested_lines << ""
|
|
742
|
+
state.next_line
|
|
743
|
+
elsif state.current_line.match?(/\A\s+/)
|
|
744
|
+
nested_lines << dedent_list_line(state.current_line, base_indent)
|
|
745
|
+
state.next_line
|
|
746
|
+
else
|
|
747
|
+
break
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
return nil
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
content_lines = []
|
|
754
|
+
while !state.eof? && !state.blank_line? && !state.current_line.match?(pattern)
|
|
755
|
+
break unless state.current_line.match?(/\A {#{content_indent},}/)
|
|
756
|
+
|
|
757
|
+
content_lines << state.raw_line.sub(/\A\s+/, "")
|
|
758
|
+
state.next_line
|
|
759
|
+
end
|
|
760
|
+
content_lines.join("\n").strip
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def append_loose_paragraph(state, content, start_line)
|
|
764
|
+
return if content.to_s.empty?
|
|
765
|
+
|
|
766
|
+
state.tokens << Token.new(type: :paragraph_open, tag: "p", nesting: 1, level: state.level, map: [start_line, state.line])
|
|
767
|
+
state.tokens << Token.new(type: :inline, content: content, level: state.level + 1, map: [start_line, state.line])
|
|
768
|
+
state.tokens << Token.new(type: :paragraph_close, tag: "p", nesting: -1, level: state.level)
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def mark_current_list_loose(state)
|
|
772
|
+
list_index = state.tokens.rindex { |token| %i[bullet_list_open ordered_list_open].include?(token.type) }
|
|
773
|
+
return unless list_index
|
|
774
|
+
|
|
775
|
+
state.tokens[list_index].attrs[:tight] = false
|
|
776
|
+
state.tokens[(list_index + 1)..].to_a.reverse_each do |token|
|
|
777
|
+
break if %i[bullet_list_open ordered_list_open].include?(token.type)
|
|
778
|
+
token.attrs[:tight] = false if token.type == :list_item_open
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
|
|
461
782
|
def parse_html_block(state)
|
|
462
783
|
line = state.current_line
|
|
463
784
|
|
|
464
|
-
return false unless
|
|
465
|
-
line.match?(HTML_BLOCK_START_2) ||
|
|
466
|
-
line.match?(HTML_BLOCK_START_3) ||
|
|
467
|
-
line.match?(HTML_BLOCK_START_4) ||
|
|
468
|
-
line.match?(HTML_BLOCK_START_5) ||
|
|
469
|
-
line.match?(HTML_BLOCK_START_6)
|
|
785
|
+
return false unless html_block_start?(line)
|
|
470
786
|
|
|
471
787
|
start_line = state.line
|
|
472
788
|
content_lines = []
|
|
473
789
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
790
|
+
if line.match?(HTML_BLOCK_START_2)
|
|
791
|
+
until state.eof?
|
|
792
|
+
current_line = state.current_line
|
|
793
|
+
content_lines << current_line
|
|
794
|
+
state.next_line
|
|
795
|
+
break if current_line.include?("-->")
|
|
796
|
+
end
|
|
797
|
+
elsif line.match?(HTML_BLOCK_START_1)
|
|
798
|
+
tag = line[/\A {0,3}<([A-Za-z][A-Za-z0-9]*)/i, 1]
|
|
799
|
+
until state.eof?
|
|
800
|
+
current_line = state.current_line
|
|
801
|
+
content_lines << state.raw_line
|
|
802
|
+
state.next_line
|
|
803
|
+
break if current_line.match?(%r{</#{Regexp.escape(tag)}\s*>}i)
|
|
804
|
+
end
|
|
805
|
+
elsif line.match?(HTML_BLOCK_START_3)
|
|
806
|
+
until state.eof?
|
|
807
|
+
current_line = state.current_line
|
|
808
|
+
content_lines << state.raw_line
|
|
809
|
+
state.next_line
|
|
810
|
+
break if current_line.include?("?>")
|
|
811
|
+
end
|
|
812
|
+
elsif line.match?(HTML_BLOCK_START_5)
|
|
813
|
+
until state.eof?
|
|
814
|
+
current_line = state.current_line
|
|
815
|
+
content_lines << state.raw_line
|
|
816
|
+
state.next_line
|
|
817
|
+
break if current_line.include?("]]>")
|
|
818
|
+
end
|
|
819
|
+
else
|
|
820
|
+
until state.eof?
|
|
821
|
+
current_line = state.current_line
|
|
822
|
+
content_lines << state.raw_line
|
|
823
|
+
state.next_line
|
|
824
|
+
break if line.match?(HTML_BLOCK_START_7) && current_line.match?(%r{</[A-Z][A-Za-z0-9]*(?:\.[A-Za-z0-9_-]+)?>})
|
|
825
|
+
break if state.blank_line?
|
|
826
|
+
end
|
|
478
827
|
end
|
|
479
828
|
|
|
480
829
|
state.tokens << Token.new(
|
|
@@ -486,16 +835,36 @@ module Mdlint
|
|
|
486
835
|
true
|
|
487
836
|
end
|
|
488
837
|
|
|
838
|
+
def html_block_start?(line)
|
|
839
|
+
[HTML_BLOCK_START_1, HTML_BLOCK_START_2, HTML_BLOCK_START_3, HTML_BLOCK_START_4,
|
|
840
|
+
HTML_BLOCK_START_5, HTML_BLOCK_START_6, HTML_BLOCK_START_7].any? { |pattern| line.match?(pattern) }
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def lazy_blockquote_continuation?(line)
|
|
844
|
+
!line.match?(ATX_HEADING_REGEXP) && !fence_match(line) && !line.match?(HR_REGEXP) &&
|
|
845
|
+
!line.match?(BLOCKQUOTE_REGEXP) && !line.match?(BULLET_LIST_REGEXP) &&
|
|
846
|
+
!line.match?(ORDERED_LIST_REGEXP) && !html_block_start?(line) &&
|
|
847
|
+
!line.match?(REFERENCE_DEF_REGEXP) && !line.match?(FOOTNOTE_DEF_REGEXP)
|
|
848
|
+
end
|
|
849
|
+
|
|
489
850
|
def parse_code_block(state)
|
|
490
851
|
return false unless state.current_line.match?(CODE_BLOCK_INDENT)
|
|
491
852
|
|
|
492
853
|
start_line = state.line
|
|
493
854
|
content_lines = []
|
|
494
855
|
|
|
495
|
-
while !state.eof?
|
|
496
|
-
|
|
497
|
-
|
|
856
|
+
while !state.eof?
|
|
857
|
+
if state.current_line.match?(CODE_BLOCK_INDENT)
|
|
858
|
+
content_lines << strip_code_indent(state.raw_line, state.current_line)
|
|
859
|
+
state.next_line
|
|
860
|
+
elsif state.blank_line? && state.line < state.lines.length - 1
|
|
861
|
+
content_lines << ""
|
|
862
|
+
state.next_line
|
|
863
|
+
else
|
|
864
|
+
break
|
|
865
|
+
end
|
|
498
866
|
end
|
|
867
|
+
content_lines.pop while content_lines.last == ""
|
|
499
868
|
|
|
500
869
|
state.tokens << Token.new(
|
|
501
870
|
type: :code_block,
|
|
@@ -508,13 +877,33 @@ module Mdlint
|
|
|
508
877
|
end
|
|
509
878
|
|
|
510
879
|
def parse_reference_definition(state)
|
|
511
|
-
line = state.
|
|
880
|
+
line = state.raw_line
|
|
512
881
|
match = line.match(REFERENCE_DEF_REGEXP)
|
|
513
882
|
return false unless match
|
|
514
883
|
|
|
515
|
-
label = match[1]
|
|
516
|
-
|
|
517
|
-
|
|
884
|
+
label = normalize_reference_label(match[1])
|
|
885
|
+
tail = line[match[0].length..].to_s.strip
|
|
886
|
+
parsed = parse_reference_tail(tail)
|
|
887
|
+
consumed = 1
|
|
888
|
+
|
|
889
|
+
if parsed.nil? && tail.empty?
|
|
890
|
+
url_line = state.peek_line
|
|
891
|
+
return false if url_line.nil? || state.blank_line?(state.line + 1)
|
|
892
|
+
|
|
893
|
+
parsed = parse_reference_tail(url_line.to_s.strip)
|
|
894
|
+
consumed += 1 if parsed
|
|
895
|
+
end
|
|
896
|
+
if parsed && parsed[1].nil?
|
|
897
|
+
title_line = state.peek_line(consumed)
|
|
898
|
+
if title_line && !state.blank_line?(state.line + consumed) && title_line.match?(/\A\s+/)
|
|
899
|
+
title = parse_reference_title(title_line.to_s.strip)
|
|
900
|
+
parsed = [parsed[0], title] if title
|
|
901
|
+
consumed += 1 if title
|
|
902
|
+
end
|
|
903
|
+
end
|
|
904
|
+
return false unless parsed
|
|
905
|
+
|
|
906
|
+
url, title = parsed
|
|
518
907
|
|
|
519
908
|
state.tokens << Token.new(
|
|
520
909
|
type: :reference_definition,
|
|
@@ -526,7 +915,101 @@ module Mdlint
|
|
|
526
915
|
map: [state.line, state.line + 1]
|
|
527
916
|
)
|
|
528
917
|
|
|
918
|
+
state.line += consumed
|
|
919
|
+
true
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
def reference_definition_candidate?(state)
|
|
923
|
+
match = state.raw_line.match(REFERENCE_DEF_REGEXP)
|
|
924
|
+
return false unless match
|
|
925
|
+
|
|
926
|
+
tail = state.raw_line[match[0].length..].to_s.strip
|
|
927
|
+
return true if parse_reference_tail(tail)
|
|
928
|
+
return false unless tail.empty?
|
|
929
|
+
|
|
930
|
+
next_line = state.peek_line
|
|
931
|
+
next_line && !state.blank_line?(state.line + 1) && parse_reference_tail(next_line.to_s.strip)
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
def normalize_reference_label(label)
|
|
935
|
+
label.to_s.gsub(/\\([!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~])/, '\\1').gsub(/\s+/, " ").strip.downcase
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
def parse_reference_tail(tail)
|
|
939
|
+
value = tail.to_s
|
|
940
|
+
return nil if value.empty?
|
|
941
|
+
|
|
942
|
+
if value.start_with?("<")
|
|
943
|
+
closing = value.index(">", 1)
|
|
944
|
+
return nil unless closing
|
|
945
|
+
|
|
946
|
+
url = value[1...closing]
|
|
947
|
+
raw_remainder = value[(closing + 1)..].to_s
|
|
948
|
+
return nil unless raw_remainder.empty? || raw_remainder.match?(/\A\s/)
|
|
949
|
+
|
|
950
|
+
remainder = raw_remainder.strip
|
|
951
|
+
else
|
|
952
|
+
index = 0
|
|
953
|
+
depth = 0
|
|
954
|
+
while index < value.length
|
|
955
|
+
character = value[index]
|
|
956
|
+
if character == "\\"
|
|
957
|
+
index += 2
|
|
958
|
+
next
|
|
959
|
+
end
|
|
960
|
+
if character == "("
|
|
961
|
+
depth += 1
|
|
962
|
+
elsif character == ")"
|
|
963
|
+
break if depth.zero?
|
|
964
|
+
|
|
965
|
+
depth -= 1
|
|
966
|
+
elsif character.match?( /\s/ ) && depth.zero?
|
|
967
|
+
break
|
|
968
|
+
end
|
|
969
|
+
index += 1
|
|
970
|
+
end
|
|
971
|
+
return nil if index.zero?
|
|
972
|
+
|
|
973
|
+
url = value[0...index]
|
|
974
|
+
remainder = value[index..].to_s.strip
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
return [url, nil] if remainder.empty?
|
|
978
|
+
|
|
979
|
+
title = parse_reference_title(remainder)
|
|
980
|
+
title ? [url, title] : nil
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
def parse_reference_title(value)
|
|
984
|
+
return nil unless value.length >= 2
|
|
985
|
+
|
|
986
|
+
opener = value[0]
|
|
987
|
+
closer = { '"' => '"', "'" => "'", "(" => ")" }[opener]
|
|
988
|
+
return nil unless closer && value.end_with?(closer)
|
|
989
|
+
|
|
990
|
+
value[1...-1]
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
def parse_footnote_definition(state)
|
|
994
|
+
match = state.current_line.match(FOOTNOTE_DEF_REGEXP)
|
|
995
|
+
return false unless match
|
|
996
|
+
|
|
997
|
+
start_line = state.line
|
|
998
|
+
raw_lines = [state.current_line]
|
|
999
|
+
content = match[2]
|
|
529
1000
|
state.next_line
|
|
1001
|
+
while !state.eof? && state.current_line.match?(/\A {4}/)
|
|
1002
|
+
raw_lines << state.current_line
|
|
1003
|
+
content = "#{content}\n#{state.current_line.sub(/\A {4}/, "")}"
|
|
1004
|
+
state.next_line
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
state.tokens << Token.new(
|
|
1008
|
+
type: :footnote_definition,
|
|
1009
|
+
content: raw_lines.join("\n") + "\n",
|
|
1010
|
+
attrs: { label: match[1].downcase, content: content },
|
|
1011
|
+
map: [start_line, state.line]
|
|
1012
|
+
)
|
|
530
1013
|
true
|
|
531
1014
|
end
|
|
532
1015
|
|
|
@@ -539,18 +1022,25 @@ module Mdlint
|
|
|
539
1022
|
while !state.eof? && !state.blank_line?
|
|
540
1023
|
line = state.current_line
|
|
541
1024
|
break if line.match?(ATX_HEADING_REGEXP) ||
|
|
542
|
-
line.match?(
|
|
1025
|
+
line.match?(MATH_BLOCK_REGEXP) ||
|
|
1026
|
+
fence_match(line) ||
|
|
543
1027
|
line.match?(HR_REGEXP) ||
|
|
544
1028
|
line.match?(BLOCKQUOTE_REGEXP) ||
|
|
545
1029
|
line.match?(BULLET_LIST_REGEXP) ||
|
|
546
1030
|
line.match?(ORDERED_LIST_REGEXP) ||
|
|
547
|
-
line.match?(
|
|
1031
|
+
(html_block_start?(line) && !line.match?(HTML_BLOCK_START_7)) ||
|
|
1032
|
+
(reference_definition_candidate?(state) && content_lines.empty?) ||
|
|
1033
|
+
line.match?(FOOTNOTE_DEF_REGEXP)
|
|
548
1034
|
|
|
549
1035
|
if state.peek_line&.match?(SETEXT_HEADING_REGEXP)
|
|
550
1036
|
break if content_lines.any?
|
|
551
1037
|
end
|
|
552
1038
|
|
|
553
|
-
content_lines <<
|
|
1039
|
+
content_lines << if content_lines.empty?
|
|
1040
|
+
state.raw_line.sub(/\A {0,3}/, "")
|
|
1041
|
+
else
|
|
1042
|
+
state.raw_line.sub(/\A\s+/, "")
|
|
1043
|
+
end
|
|
554
1044
|
state.next_line
|
|
555
1045
|
end
|
|
556
1046
|
|
|
@@ -566,7 +1056,7 @@ module Mdlint
|
|
|
566
1056
|
|
|
567
1057
|
state.tokens << Token.new(
|
|
568
1058
|
type: :inline,
|
|
569
|
-
content: content_lines.join("\n"),
|
|
1059
|
+
content: content_lines.join("\n").strip.gsub(/(?<! ) \n/, "\n"),
|
|
570
1060
|
level: state.level + 1,
|
|
571
1061
|
map: [start_line, state.line]
|
|
572
1062
|
)
|
|
@@ -580,6 +1070,93 @@ module Mdlint
|
|
|
580
1070
|
|
|
581
1071
|
true
|
|
582
1072
|
end
|
|
1073
|
+
|
|
1074
|
+
def alert_attributes(content_lines)
|
|
1075
|
+
match = content_lines.first.to_s.match(/\A\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/i)
|
|
1076
|
+
match ? { alert: match[1].downcase } : {}
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
def block_boundary?(line)
|
|
1080
|
+
line.match?(ATX_HEADING_REGEXP) || fence_match(line) ||
|
|
1081
|
+
line.match?(HR_REGEXP) || line.match?(BLOCKQUOTE_REGEXP) ||
|
|
1082
|
+
line.match?(BULLET_LIST_REGEXP) || line.match?(ORDERED_LIST_REGEXP) ||
|
|
1083
|
+
line.match?(REFERENCE_DEF_REGEXP) || line.match?(FOOTNOTE_DEF_REGEXP)
|
|
1084
|
+
end
|
|
1085
|
+
|
|
1086
|
+
def find_directive_closing_line(state, name)
|
|
1087
|
+
depth = 1
|
|
1088
|
+
index = state.line + 1
|
|
1089
|
+
while index < state.lines.length
|
|
1090
|
+
candidate = state.lines[index]
|
|
1091
|
+
nested = candidate.match(/\A {0,3}:::([A-Za-z][\w-]*)(?:\s+.*)?\s*\z/)
|
|
1092
|
+
if nested
|
|
1093
|
+
depth += 1
|
|
1094
|
+
elsif candidate.strip == ":::"
|
|
1095
|
+
depth -= 1
|
|
1096
|
+
return index if depth.zero?
|
|
1097
|
+
end
|
|
1098
|
+
index += 1
|
|
1099
|
+
end
|
|
1100
|
+
nil
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
def strip_code_indent(raw_line, expanded_line)
|
|
1104
|
+
return raw_line unless expanded_line.start_with?(" ")
|
|
1105
|
+
|
|
1106
|
+
consumed = 0
|
|
1107
|
+
index = 0
|
|
1108
|
+
raw_line.each_char do |character|
|
|
1109
|
+
break if consumed >= 4
|
|
1110
|
+
|
|
1111
|
+
consumed += character == "\t" ? 4 - (consumed % 4) : 1
|
|
1112
|
+
index += 1
|
|
1113
|
+
end
|
|
1114
|
+
raw_line[index..] || ""
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1117
|
+
def fence_match(line)
|
|
1118
|
+
match = line.to_s.match(FENCE_OPEN_REGEXP)
|
|
1119
|
+
return unless match
|
|
1120
|
+
return if match[1].start_with?("`") && match[2].include?("`")
|
|
1121
|
+
|
|
1122
|
+
match
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
def front_matter_payload?(format, lines)
|
|
1126
|
+
return false if lines.empty? || lines.all? { |line| line.strip.empty? }
|
|
1127
|
+
|
|
1128
|
+
case format
|
|
1129
|
+
when :yaml
|
|
1130
|
+
lines.any? { |line| line.match?(/\A\s*[^#\s][^:]*:/) }
|
|
1131
|
+
when :toml
|
|
1132
|
+
lines.any? { |line| line.match?(/\A\s*[^#\s=]+\s*=/) }
|
|
1133
|
+
when :json
|
|
1134
|
+
lines.join.match?(/[{}\[\]]|\A\s*\"[^\"]+\"\s*:/)
|
|
1135
|
+
else
|
|
1136
|
+
false
|
|
1137
|
+
end
|
|
1138
|
+
end
|
|
1139
|
+
|
|
1140
|
+
def strip_fence_indent(raw_line, indent)
|
|
1141
|
+
return raw_line if indent.zero?
|
|
1142
|
+
|
|
1143
|
+
raw_line.sub(/\A {0,#{indent}}/, "")
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
def task_attributes(content)
|
|
1147
|
+
return {} unless @dialect.feature?(:task_lists)
|
|
1148
|
+
|
|
1149
|
+
match = content.match(/\A\[([ xX])\]\s+/)
|
|
1150
|
+
return {} unless match
|
|
1151
|
+
|
|
1152
|
+
{ task: true, checked: match[1].downcase == "x" }
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
def strip_task_marker(content)
|
|
1156
|
+
return content unless @dialect.feature?(:task_lists)
|
|
1157
|
+
|
|
1158
|
+
content.sub(/\A\[[ xX]\]\s+/, "")
|
|
1159
|
+
end
|
|
583
1160
|
end
|
|
584
1161
|
end
|
|
585
1162
|
end
|