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,17 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../dialect"
|
|
4
|
+
|
|
3
5
|
module Mdlint
|
|
4
6
|
module Parser
|
|
5
7
|
class InlineParser
|
|
6
8
|
ESCAPE_CHARS = '!"#$%&\'()*+,\\-./:;<=>?@[\\\\\\]^_`{|}~'
|
|
7
9
|
ESCAPE_REGEXP = /\\([#{Regexp.escape(ESCAPE_CHARS)}])/
|
|
8
10
|
BACKTICK_REGEXP = /(`+)(.+?)\1(?!`)/
|
|
9
|
-
AUTOLINK_REGEXP = %r{<((
|
|
11
|
+
AUTOLINK_REGEXP = %r{<(([A-Za-z][A-Za-z0-9+.-]{1,31}):[^\s<>]+)>}
|
|
10
12
|
EMAIL_AUTOLINK_REGEXP = /<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/
|
|
11
|
-
HTML_INLINE_REGEXP = %r{</?[a-zA-Z][a-zA-Z0-9]*(?:\s+[^>]*)
|
|
13
|
+
HTML_INLINE_REGEXP = %r{</?[a-zA-Z][a-zA-Z0-9:-]*(?:\s+[^>]*)?/?>}
|
|
12
14
|
|
|
13
15
|
def initialize(options = {})
|
|
14
16
|
@options = options
|
|
17
|
+
@dialect = Dialect.resolve(options[:dialect])
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
def parse(content)
|
|
@@ -27,24 +30,62 @@ module Mdlint
|
|
|
27
30
|
while pos < content.length
|
|
28
31
|
remaining = content[pos..]
|
|
29
32
|
|
|
30
|
-
if (match = remaining.match(/\A
|
|
33
|
+
if (match = remaining.match(/\A\[\^([^\]]+)\]/))
|
|
34
|
+
flush_text(text_buffer, tokens)
|
|
35
|
+
text_buffer = ""
|
|
36
|
+
tokens << Token.new(type: :footnote_ref, attrs: { label: match[1].downcase })
|
|
37
|
+
pos += match[0].length
|
|
38
|
+
elsif (match = remaining.match(/\A\$([^$\n]+)\$/))
|
|
39
|
+
flush_text(text_buffer, tokens)
|
|
40
|
+
text_buffer = ""
|
|
41
|
+
tokens << Token.new(type: :math_inline, content: match[1])
|
|
42
|
+
pos += match[0].length
|
|
43
|
+
elsif remaining.start_with?("\\\n")
|
|
44
|
+
flush_text(text_buffer, tokens)
|
|
45
|
+
text_buffer = ""
|
|
46
|
+
tokens << Token.new(type: :hardbreak, tag: "br")
|
|
47
|
+
pos += 2
|
|
48
|
+
elsif (match = remaining.match(/\A#{ESCAPE_REGEXP}/))
|
|
31
49
|
flush_text(text_buffer, tokens)
|
|
32
50
|
text_buffer = ""
|
|
33
51
|
tokens << Token.new(type: :text, content: match[1])
|
|
34
52
|
pos += match[0].length
|
|
35
53
|
elsif remaining.start_with?("![")
|
|
36
|
-
if (match = remaining
|
|
54
|
+
if (match = inline_link_match(remaining, image: true))
|
|
37
55
|
flush_text(text_buffer, tokens)
|
|
38
56
|
text_buffer = ""
|
|
39
57
|
tokens << Token.new(
|
|
40
58
|
type: :image,
|
|
41
59
|
tag: "img",
|
|
42
60
|
attrs: {
|
|
43
|
-
src: match[
|
|
44
|
-
alt: match[
|
|
45
|
-
title: match[
|
|
61
|
+
src: match[:url],
|
|
62
|
+
alt: match[:text],
|
|
63
|
+
title: match[:title]
|
|
46
64
|
}.compact,
|
|
47
|
-
content: match[
|
|
65
|
+
content: match[:text]
|
|
66
|
+
)
|
|
67
|
+
pos += match[:length]
|
|
68
|
+
elsif (match = remaining.match(/\A!\[([^\]]*)\]\[([^\]]*)\]/))
|
|
69
|
+
flush_text(text_buffer, tokens)
|
|
70
|
+
text_buffer = ""
|
|
71
|
+
label = match[2].empty? ? match[1] : match[2]
|
|
72
|
+
tokens << Token.new(
|
|
73
|
+
type: :image,
|
|
74
|
+
tag: "img",
|
|
75
|
+
attrs: { reference_label: label.downcase, reference_kind: :full, alt: match[1] },
|
|
76
|
+
content: match[1],
|
|
77
|
+
markup: "reference"
|
|
78
|
+
)
|
|
79
|
+
pos += match[0].length
|
|
80
|
+
elsif (match = remaining.match(/\A!\[([^\]]+)\](?!\()/))
|
|
81
|
+
flush_text(text_buffer, tokens)
|
|
82
|
+
text_buffer = ""
|
|
83
|
+
tokens << Token.new(
|
|
84
|
+
type: :image,
|
|
85
|
+
tag: "img",
|
|
86
|
+
attrs: { reference_label: match[1].downcase, reference_kind: :shortcut, alt: match[1] },
|
|
87
|
+
content: match[1],
|
|
88
|
+
markup: "reference"
|
|
48
89
|
)
|
|
49
90
|
pos += match[0].length
|
|
50
91
|
else
|
|
@@ -53,7 +94,7 @@ module Mdlint
|
|
|
53
94
|
end
|
|
54
95
|
elsif remaining.start_with?("[")
|
|
55
96
|
# Inline link: [text](url "title")
|
|
56
|
-
if (match = remaining
|
|
97
|
+
if (match = inline_link_match(remaining))
|
|
57
98
|
flush_text(text_buffer, tokens)
|
|
58
99
|
text_buffer = ""
|
|
59
100
|
tokens << Token.new(
|
|
@@ -61,17 +102,17 @@ module Mdlint
|
|
|
61
102
|
tag: "a",
|
|
62
103
|
nesting: 1,
|
|
63
104
|
attrs: {
|
|
64
|
-
href: match[
|
|
65
|
-
title: match[
|
|
105
|
+
href: match[:url],
|
|
106
|
+
title: match[:title]
|
|
66
107
|
}.compact
|
|
67
108
|
)
|
|
68
|
-
parse_inline(match[
|
|
109
|
+
parse_inline(match[:text], tokens)
|
|
69
110
|
tokens << Token.new(
|
|
70
111
|
type: :link_close,
|
|
71
112
|
tag: "a",
|
|
72
113
|
nesting: -1
|
|
73
114
|
)
|
|
74
|
-
pos += match[
|
|
115
|
+
pos += match[:length]
|
|
75
116
|
# Full reference link: [text][label]
|
|
76
117
|
elsif (match = remaining.match(/\A\[([^\]]*)\]\[([^\]]*)\]/))
|
|
77
118
|
flush_text(text_buffer, tokens)
|
|
@@ -81,7 +122,7 @@ module Mdlint
|
|
|
81
122
|
type: :link_open,
|
|
82
123
|
tag: "a",
|
|
83
124
|
nesting: 1,
|
|
84
|
-
attrs: { reference_label: label.downcase },
|
|
125
|
+
attrs: { reference_label: label.downcase, reference_kind: :full },
|
|
85
126
|
markup: "reference"
|
|
86
127
|
)
|
|
87
128
|
parse_inline(match[1], tokens)
|
|
@@ -93,7 +134,7 @@ module Mdlint
|
|
|
93
134
|
)
|
|
94
135
|
pos += match[0].length
|
|
95
136
|
# Shortcut reference link: [label]
|
|
96
|
-
elsif (match = remaining.match(/\A\[([^\]]+)\](?!\(|\[)/))
|
|
137
|
+
elsif (match = remaining.match(/\A\[([^\[\]]+)\](?!\(|\[)/))
|
|
97
138
|
flush_text(text_buffer, tokens)
|
|
98
139
|
text_buffer = ""
|
|
99
140
|
label = match[1]
|
|
@@ -101,7 +142,7 @@ module Mdlint
|
|
|
101
142
|
type: :link_open,
|
|
102
143
|
tag: "a",
|
|
103
144
|
nesting: 1,
|
|
104
|
-
attrs: { reference_label: label.downcase },
|
|
145
|
+
attrs: { reference_label: label.downcase, reference_kind: :shortcut },
|
|
105
146
|
markup: "reference"
|
|
106
147
|
)
|
|
107
148
|
tokens << Token.new(type: :text, content: label)
|
|
@@ -117,24 +158,27 @@ module Mdlint
|
|
|
117
158
|
pos += 1
|
|
118
159
|
end
|
|
119
160
|
elsif remaining.start_with?("`")
|
|
120
|
-
if (match = remaining
|
|
161
|
+
if (match = code_span_match(remaining))
|
|
121
162
|
flush_text(text_buffer, tokens)
|
|
122
163
|
text_buffer = ""
|
|
123
|
-
code_content = match[
|
|
124
|
-
code_content = code_content.
|
|
164
|
+
code_content = match[:content]
|
|
165
|
+
code_content = code_content.gsub(/\r?\n/, " ")
|
|
166
|
+
if code_content.start_with?(" ") && code_content.end_with?(" ") && !code_content.match?(/\A +\z/)
|
|
167
|
+
code_content = code_content[1...-1]
|
|
168
|
+
end
|
|
125
169
|
tokens << Token.new(
|
|
126
170
|
type: :code_inline,
|
|
127
171
|
tag: "code",
|
|
128
172
|
content: code_content,
|
|
129
|
-
markup: match[
|
|
173
|
+
markup: match[:delimiter]
|
|
130
174
|
)
|
|
131
|
-
pos += match[
|
|
175
|
+
pos += match[:length]
|
|
132
176
|
else
|
|
133
177
|
text_buffer += remaining[0]
|
|
134
178
|
pos += 1
|
|
135
179
|
end
|
|
136
180
|
elsif remaining.start_with?("**")
|
|
137
|
-
if (match = remaining.
|
|
181
|
+
if (match = emphasis_match(remaining, "**", previous: pos.zero? ? nil : content[pos - 1], strong: true))
|
|
138
182
|
flush_text(text_buffer, tokens)
|
|
139
183
|
text_buffer = ""
|
|
140
184
|
tokens << Token.new(type: :strong_open, tag: "strong", nesting: 1, markup: "**")
|
|
@@ -146,7 +190,7 @@ module Mdlint
|
|
|
146
190
|
pos += 1
|
|
147
191
|
end
|
|
148
192
|
elsif remaining.start_with?("__")
|
|
149
|
-
if (match = remaining.
|
|
193
|
+
if (match = emphasis_match(remaining, "__", previous: pos.zero? ? nil : content[pos - 1], strong: true))
|
|
150
194
|
flush_text(text_buffer, tokens)
|
|
151
195
|
text_buffer = ""
|
|
152
196
|
tokens << Token.new(type: :strong_open, tag: "strong", nesting: 1, markup: "__")
|
|
@@ -157,8 +201,20 @@ module Mdlint
|
|
|
157
201
|
text_buffer += remaining[0]
|
|
158
202
|
pos += 1
|
|
159
203
|
end
|
|
204
|
+
elsif @dialect.feature?(:strikethrough) && remaining.start_with?("~~")
|
|
205
|
+
if (match = remaining.match(/\A~~(?!\s)(.+?)(?<!\s)~~/m))
|
|
206
|
+
flush_text(text_buffer, tokens)
|
|
207
|
+
text_buffer = ""
|
|
208
|
+
tokens << Token.new(type: :s_open, tag: "del", nesting: 1, markup: "~~")
|
|
209
|
+
parse_inline(match[1], tokens)
|
|
210
|
+
tokens << Token.new(type: :s_close, tag: "del", nesting: -1, markup: "~~")
|
|
211
|
+
pos += match[0].length
|
|
212
|
+
else
|
|
213
|
+
text_buffer += remaining[0]
|
|
214
|
+
pos += 1
|
|
215
|
+
end
|
|
160
216
|
elsif remaining.start_with?("*")
|
|
161
|
-
if (match = remaining.
|
|
217
|
+
if (match = emphasis_match(remaining, "*", previous: pos.zero? ? nil : content[pos - 1]))
|
|
162
218
|
flush_text(text_buffer, tokens)
|
|
163
219
|
text_buffer = ""
|
|
164
220
|
tokens << Token.new(type: :em_open, tag: "em", nesting: 1, markup: "*")
|
|
@@ -170,7 +226,7 @@ module Mdlint
|
|
|
170
226
|
pos += 1
|
|
171
227
|
end
|
|
172
228
|
elsif remaining.start_with?("_")
|
|
173
|
-
if (match = remaining.
|
|
229
|
+
if (match = emphasis_match(remaining, "_", previous: pos.zero? ? nil : content[pos - 1]))
|
|
174
230
|
flush_text(text_buffer, tokens)
|
|
175
231
|
text_buffer = ""
|
|
176
232
|
tokens << Token.new(type: :em_open, tag: "em", nesting: 1, markup: "_")
|
|
@@ -227,11 +283,26 @@ module Mdlint
|
|
|
227
283
|
text_buffer += remaining[0]
|
|
228
284
|
pos += 1
|
|
229
285
|
end
|
|
230
|
-
elsif
|
|
286
|
+
elsif @dialect.feature?(:bare_autolinks) && (match = remaining.match(/\A((?:https?|ftp):\/\/[^\s<]+)/))
|
|
287
|
+
flush_text(text_buffer, tokens)
|
|
288
|
+
text_buffer = ""
|
|
289
|
+
href = match[1].sub(/[.,!?;:]\z/, "")
|
|
290
|
+
consumed = href.length
|
|
291
|
+
tokens << Token.new(
|
|
292
|
+
type: :link_open,
|
|
293
|
+
tag: "a",
|
|
294
|
+
nesting: 1,
|
|
295
|
+
attrs: { href: href },
|
|
296
|
+
markup: "autolink"
|
|
297
|
+
)
|
|
298
|
+
tokens << Token.new(type: :text, content: href)
|
|
299
|
+
tokens << Token.new(type: :link_close, tag: "a", nesting: -1, markup: "autolink")
|
|
300
|
+
pos += consumed
|
|
301
|
+
elsif (match = remaining.match(/\A {2,}\n/))
|
|
231
302
|
flush_text(text_buffer, tokens)
|
|
232
303
|
text_buffer = ""
|
|
233
304
|
tokens << Token.new(type: :hardbreak, tag: "br")
|
|
234
|
-
pos +=
|
|
305
|
+
pos += match[0].length
|
|
235
306
|
elsif remaining[0] == "\n"
|
|
236
307
|
flush_text(text_buffer, tokens)
|
|
237
308
|
text_buffer = ""
|
|
@@ -248,6 +319,167 @@ module Mdlint
|
|
|
248
319
|
|
|
249
320
|
private
|
|
250
321
|
|
|
322
|
+
def inline_link_match(value, image: false)
|
|
323
|
+
prefix = image ? "![" : "["
|
|
324
|
+
return unless value.start_with?(prefix)
|
|
325
|
+
|
|
326
|
+
depth = 1
|
|
327
|
+
index = prefix.length
|
|
328
|
+
while index < value.length
|
|
329
|
+
if value[index] == "\\"
|
|
330
|
+
index += 2
|
|
331
|
+
next
|
|
332
|
+
end
|
|
333
|
+
if value[index] == "`" && (code = code_span_match(value[index..]))
|
|
334
|
+
index += code[:length]
|
|
335
|
+
next
|
|
336
|
+
end
|
|
337
|
+
if value[index] == "["
|
|
338
|
+
depth += 1
|
|
339
|
+
elsif value[index] == "]"
|
|
340
|
+
depth -= 1
|
|
341
|
+
break if depth.zero?
|
|
342
|
+
end
|
|
343
|
+
index += 1
|
|
344
|
+
end
|
|
345
|
+
return unless depth.zero? && value[index + 1] == "("
|
|
346
|
+
|
|
347
|
+
destination = parse_link_destination(value[(index + 2)..])
|
|
348
|
+
return unless destination
|
|
349
|
+
|
|
350
|
+
{
|
|
351
|
+
text: value[prefix.length...index],
|
|
352
|
+
url: destination[:url],
|
|
353
|
+
title: destination[:title],
|
|
354
|
+
length: index + 2 + destination[:length]
|
|
355
|
+
}
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def parse_link_destination(value)
|
|
359
|
+
index = 0
|
|
360
|
+
index += 1 while index < value.length && value[index].match?(/\s/)
|
|
361
|
+
return { url: "", title: nil, length: index + 1 } if value[index] == ")"
|
|
362
|
+
|
|
363
|
+
if value[index] == "<"
|
|
364
|
+
closing = find_unescaped(value, ">", index + 1)
|
|
365
|
+
return unless closing
|
|
366
|
+
|
|
367
|
+
url = value[(index + 1)...closing]
|
|
368
|
+
return if url.match?(/[\r\n]/)
|
|
369
|
+
index = closing + 1
|
|
370
|
+
else
|
|
371
|
+
start = index
|
|
372
|
+
depth = 0
|
|
373
|
+
while index < value.length
|
|
374
|
+
character = value[index]
|
|
375
|
+
if character == "\\"
|
|
376
|
+
index += 2
|
|
377
|
+
next
|
|
378
|
+
end
|
|
379
|
+
if character == "("
|
|
380
|
+
depth += 1
|
|
381
|
+
elsif character == ")"
|
|
382
|
+
break if depth.zero?
|
|
383
|
+
|
|
384
|
+
depth -= 1
|
|
385
|
+
elsif character.match?(/\s/) && depth.zero?
|
|
386
|
+
break
|
|
387
|
+
end
|
|
388
|
+
index += 1
|
|
389
|
+
end
|
|
390
|
+
return if index == start
|
|
391
|
+
|
|
392
|
+
url = value[start...index]
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
whitespace = index
|
|
396
|
+
index += 1 while index < value.length && value[index].match?(/\s/)
|
|
397
|
+
return { url: url, title: nil, length: index + 1 } if value[index] == ")"
|
|
398
|
+
return unless index > whitespace
|
|
399
|
+
|
|
400
|
+
title_start = value[index]
|
|
401
|
+
title_end = { '"' => '"', "'" => "'", "(" => ")" }[title_start]
|
|
402
|
+
return unless title_end
|
|
403
|
+
|
|
404
|
+
closing = find_unescaped(value, title_end, index + 1)
|
|
405
|
+
return unless closing
|
|
406
|
+
remainder = closing + 1
|
|
407
|
+
remainder += 1 while remainder < value.length && value[remainder].match?(/\s/)
|
|
408
|
+
return unless value[remainder] == ")"
|
|
409
|
+
|
|
410
|
+
{ url: url, title: value[(index + 1)...closing], length: remainder + 1 }
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def find_unescaped(value, character, start)
|
|
414
|
+
index = start
|
|
415
|
+
while index < value.length
|
|
416
|
+
return index if value[index] == character && (index.zero? || value[index - 1] != "\\")
|
|
417
|
+
|
|
418
|
+
index += 1
|
|
419
|
+
end
|
|
420
|
+
nil
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def code_span_match(value)
|
|
424
|
+
opening = value[/\A`+/]
|
|
425
|
+
return unless opening
|
|
426
|
+
|
|
427
|
+
delimiter = opening
|
|
428
|
+
search = delimiter.length
|
|
429
|
+
while (closing = value.index(delimiter, search))
|
|
430
|
+
run = value[closing..].match(/\A`+/)[0].length
|
|
431
|
+
if run == delimiter.length
|
|
432
|
+
return { delimiter: delimiter, content: value[delimiter.length...closing], length: closing + delimiter.length }
|
|
433
|
+
end
|
|
434
|
+
search = closing + run
|
|
435
|
+
end
|
|
436
|
+
nil
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def emphasis_match(value, delimiter, previous:, strong: false)
|
|
440
|
+
escaped = Regexp.escape(delimiter)
|
|
441
|
+
match = value.match(/\A#{escaped}(?!\s)(.+?)(?<!\s)#{escaped}/m)
|
|
442
|
+
return unless match
|
|
443
|
+
|
|
444
|
+
opening_next = match[1][0]
|
|
445
|
+
closing_previous = match[1][-1]
|
|
446
|
+
after = value[match[0].length]
|
|
447
|
+
return unless delimiter_open?(delimiter, previous, opening_next)
|
|
448
|
+
return unless delimiter_close?(delimiter, closing_previous, after)
|
|
449
|
+
|
|
450
|
+
match
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def delimiter_open?(delimiter, before, after)
|
|
454
|
+
before_space = whitespace_character?(before)
|
|
455
|
+
after_space = whitespace_character?(after)
|
|
456
|
+
before_punctuation = punctuation_character?(before)
|
|
457
|
+
after_punctuation = punctuation_character?(after)
|
|
458
|
+
left_flanking = !after_space && (!after_punctuation || before_space || before_punctuation)
|
|
459
|
+
right_flanking = !before_space && (!before_punctuation || after_space || after_punctuation)
|
|
460
|
+
|
|
461
|
+
delimiter.start_with?("_") ? left_flanking && (!right_flanking || before_punctuation || before_space) : left_flanking
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def delimiter_close?(delimiter, before, after)
|
|
465
|
+
before_space = whitespace_character?(before)
|
|
466
|
+
after_space = whitespace_character?(after)
|
|
467
|
+
before_punctuation = punctuation_character?(before)
|
|
468
|
+
after_punctuation = punctuation_character?(after)
|
|
469
|
+
left_flanking = !after_space && (!after_punctuation || before_space || before_punctuation)
|
|
470
|
+
right_flanking = !before_space && (!before_punctuation || after_space || after_punctuation)
|
|
471
|
+
|
|
472
|
+
delimiter.start_with?("_") ? right_flanking && (!left_flanking || after_punctuation || after_space) : right_flanking
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def whitespace_character?(character)
|
|
476
|
+
character.nil? || character.match?(/\s|\p{Space}/u)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def punctuation_character?(character)
|
|
480
|
+
!character.nil? && character.match?(/[[:punct:]]/u)
|
|
481
|
+
end
|
|
482
|
+
|
|
251
483
|
def flush_text(buffer, tokens)
|
|
252
484
|
return if buffer.empty?
|
|
253
485
|
|
data/lib/mdlint/parser/state.rb
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
module Mdlint
|
|
4
4
|
module Parser
|
|
5
5
|
class State
|
|
6
|
-
attr_reader :src, :lines, :line_offsets
|
|
6
|
+
attr_reader :src, :lines, :raw_lines, :line_offsets
|
|
7
7
|
attr_accessor :line, :pos, :tokens, :level
|
|
8
8
|
|
|
9
9
|
def initialize(src)
|
|
10
10
|
@src = src
|
|
11
|
-
@
|
|
11
|
+
@raw_lines = src.split("\n", -1)
|
|
12
|
+
@lines = @raw_lines.map { |line| expand_tabs(line) }
|
|
12
13
|
@line_offsets = build_line_offsets
|
|
13
14
|
@line = 0
|
|
14
15
|
@pos = 0
|
|
@@ -24,6 +25,10 @@ module Mdlint
|
|
|
24
25
|
@lines[@line]
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
def raw_line
|
|
29
|
+
@raw_lines[@line]
|
|
30
|
+
end
|
|
31
|
+
|
|
27
32
|
def next_line
|
|
28
33
|
@line += 1
|
|
29
34
|
end
|
|
@@ -50,6 +55,20 @@ module Mdlint
|
|
|
50
55
|
|
|
51
56
|
private
|
|
52
57
|
|
|
58
|
+
def expand_tabs(line)
|
|
59
|
+
column = 0
|
|
60
|
+
line.each_char.with_object(+'') do |character, expanded|
|
|
61
|
+
if character == "\t"
|
|
62
|
+
spaces = 4 - (column % 4)
|
|
63
|
+
expanded << (" " * spaces)
|
|
64
|
+
column += spaces
|
|
65
|
+
else
|
|
66
|
+
expanded << character
|
|
67
|
+
column += 1
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
53
72
|
def build_line_offsets
|
|
54
73
|
offsets = [0]
|
|
55
74
|
@lines.each do |line|
|
data/lib/mdlint/parser.rb
CHANGED
|
@@ -7,17 +7,17 @@ require_relative "parser/inline_parser"
|
|
|
7
7
|
module Mdlint
|
|
8
8
|
module Parser
|
|
9
9
|
class << self
|
|
10
|
-
def parse(src)
|
|
11
|
-
block_parser = BlockParser.new
|
|
10
|
+
def parse(src, options = {})
|
|
11
|
+
block_parser = BlockParser.new(options)
|
|
12
12
|
tokens = block_parser.parse(src)
|
|
13
|
-
parse_inline_tokens(tokens)
|
|
13
|
+
parse_inline_tokens(tokens, options)
|
|
14
14
|
tokens
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
|
-
def parse_inline_tokens(tokens)
|
|
20
|
-
inline_parser = InlineParser.new
|
|
19
|
+
def parse_inline_tokens(tokens, options)
|
|
20
|
+
inline_parser = InlineParser.new(options)
|
|
21
21
|
tokens.each do |token|
|
|
22
22
|
next unless token.type == :inline
|
|
23
23
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mdlint
|
|
4
|
+
module Plugin
|
|
5
|
+
class Error < Mdlint::Error
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.register_rule(rule_class)
|
|
9
|
+
unless defined?(Mdlint::Linter::Rule) && rule_class.is_a?(Class) && rule_class < Mdlint::Linter::Rule
|
|
10
|
+
raise Error, "plugin rules must inherit from Mdlint::Linter::Rule"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
Mdlint::Linter::RuleRegistry.register(rule_class)
|
|
14
|
+
rule_class
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.register_dialect(name, features: [])
|
|
18
|
+
Mdlint::Dialect.register(name, features: features)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.unregister_rule(rule_class)
|
|
22
|
+
Mdlint::Linter::RuleRegistry.unregister(rule_class)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.load(path)
|
|
26
|
+
require File.expand_path(path, Dir.pwd)
|
|
27
|
+
rescue LoadError, StandardError => error
|
|
28
|
+
raise Error, "could not load plugin #{path}: #{error.message}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|