mdlint 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 +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +181 -0
- data/Rakefile +8 -0
- data/exe/mdlint +7 -0
- data/lib/mdlint/cli.rb +206 -0
- data/lib/mdlint/config.rb +103 -0
- data/lib/mdlint/linter/rule.rb +66 -0
- data/lib/mdlint/linter/rule_engine.rb +48 -0
- data/lib/mdlint/linter/rules/first_line_heading.rb +41 -0
- data/lib/mdlint/linter/rules/heading_increment.rb +36 -0
- data/lib/mdlint/linter/rules/heading_style.rb +31 -0
- data/lib/mdlint/linter/rules/no_multiple_blanks.rb +50 -0
- data/lib/mdlint/linter/rules/no_trailing_spaces.rb +38 -0
- data/lib/mdlint/linter/violation.rb +35 -0
- data/lib/mdlint/linter.rb +28 -0
- data/lib/mdlint/parser/block_parser.rb +585 -0
- data/lib/mdlint/parser/inline_parser.rb +258 -0
- data/lib/mdlint/parser/state.rb +62 -0
- data/lib/mdlint/parser.rb +29 -0
- data/lib/mdlint/renderer/md_renderer.rb +458 -0
- data/lib/mdlint/renderer.rb +13 -0
- data/lib/mdlint/token.rb +65 -0
- data/lib/mdlint/version.rb +5 -0
- data/lib/mdlint.rb +43 -0
- metadata +73 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Mdlint
|
|
6
|
+
module Renderer
|
|
7
|
+
class MdRenderer
|
|
8
|
+
BULLET_MARKERS = %w[- *].freeze
|
|
9
|
+
HORIZONTAL_RULE = "_" * 70
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
@options = options
|
|
13
|
+
@bullet_list_depth = 0
|
|
14
|
+
@reference_definitions = {}
|
|
15
|
+
@used_references = Set.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render(tokens)
|
|
19
|
+
@bullet_list_depth = 0
|
|
20
|
+
@reference_definitions = {}
|
|
21
|
+
@used_references = Set.new
|
|
22
|
+
@ordered_list_counter = 0
|
|
23
|
+
|
|
24
|
+
# First pass: collect all reference definitions
|
|
25
|
+
collect_reference_definitions(tokens)
|
|
26
|
+
|
|
27
|
+
output = []
|
|
28
|
+
render_tokens(tokens, output)
|
|
29
|
+
|
|
30
|
+
# Append used reference definitions at the end, sorted by label
|
|
31
|
+
append_reference_definitions(output)
|
|
32
|
+
|
|
33
|
+
result = output.join
|
|
34
|
+
result.gsub!(/\n{3,}/, "\n\n")
|
|
35
|
+
|
|
36
|
+
# Apply end of line setting
|
|
37
|
+
result = apply_end_of_line(result)
|
|
38
|
+
|
|
39
|
+
result.chomp!
|
|
40
|
+
result += "\n" unless result.empty?
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def apply_end_of_line(text)
|
|
45
|
+
eol = @options[:end_of_line] || :lf
|
|
46
|
+
case eol
|
|
47
|
+
when :lf
|
|
48
|
+
text.gsub(/\r\n/, "\n")
|
|
49
|
+
when :crlf
|
|
50
|
+
text.gsub(/\r?\n/, "\r\n")
|
|
51
|
+
when :keep
|
|
52
|
+
text
|
|
53
|
+
else
|
|
54
|
+
text.gsub(/\r\n/, "\n")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def collect_reference_definitions(tokens)
|
|
59
|
+
tokens.each do |token|
|
|
60
|
+
next unless token.type == :reference_definition
|
|
61
|
+
|
|
62
|
+
label = token.attrs[:label]
|
|
63
|
+
# Only keep the first definition for each label (remove duplicates)
|
|
64
|
+
@reference_definitions[label] ||= {
|
|
65
|
+
url: token.attrs[:url],
|
|
66
|
+
title: token.attrs[:title]
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def append_reference_definitions(output)
|
|
72
|
+
# mdformat converts reference links to inline links, so no definitions needed
|
|
73
|
+
# This method is kept for potential future use or configuration options
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def render_tokens(tokens, output)
|
|
79
|
+
i = 0
|
|
80
|
+
while i < tokens.length
|
|
81
|
+
token = tokens[i]
|
|
82
|
+
case token.type
|
|
83
|
+
when :heading_open
|
|
84
|
+
i = render_heading(tokens, i, output)
|
|
85
|
+
when :paragraph_open
|
|
86
|
+
i = render_paragraph(tokens, i, output)
|
|
87
|
+
when :bullet_list_open
|
|
88
|
+
i = render_bullet_list(tokens, i, output)
|
|
89
|
+
when :ordered_list_open
|
|
90
|
+
i = render_ordered_list(tokens, i, output)
|
|
91
|
+
when :blockquote_open
|
|
92
|
+
i = render_blockquote(tokens, i, output)
|
|
93
|
+
when :fence
|
|
94
|
+
render_fence(token, output)
|
|
95
|
+
i += 1
|
|
96
|
+
when :code_block
|
|
97
|
+
render_code_block(token, output)
|
|
98
|
+
i += 1
|
|
99
|
+
when :hr
|
|
100
|
+
output << "#{HORIZONTAL_RULE}\n\n"
|
|
101
|
+
i += 1
|
|
102
|
+
when :html_block
|
|
103
|
+
output << token.content
|
|
104
|
+
output << "\n" unless token.content.end_with?("\n")
|
|
105
|
+
i += 1
|
|
106
|
+
when :reference_definition
|
|
107
|
+
# Skip - will be output at the end
|
|
108
|
+
i += 1
|
|
109
|
+
else
|
|
110
|
+
i += 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_heading(tokens, start_index, output)
|
|
116
|
+
open_token = tokens[start_index]
|
|
117
|
+
level = open_token.tag[1].to_i
|
|
118
|
+
markup = "#" * level
|
|
119
|
+
|
|
120
|
+
inline_content = ""
|
|
121
|
+
i = start_index + 1
|
|
122
|
+
while i < tokens.length && tokens[i].type != :heading_close
|
|
123
|
+
if tokens[i].type == :inline
|
|
124
|
+
inline_content = render_inline(tokens[i])
|
|
125
|
+
end
|
|
126
|
+
i += 1
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
output << "#{markup} #{inline_content}\n\n"
|
|
130
|
+
i + 1
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def render_paragraph(tokens, start_index, output)
|
|
134
|
+
inline_content = ""
|
|
135
|
+
i = start_index + 1
|
|
136
|
+
while i < tokens.length && tokens[i].type != :paragraph_close
|
|
137
|
+
if tokens[i].type == :inline
|
|
138
|
+
inline_content = render_inline(tokens[i])
|
|
139
|
+
end
|
|
140
|
+
i += 1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
wrapped_content = wrap_text(inline_content)
|
|
144
|
+
output << "#{wrapped_content}\n\n"
|
|
145
|
+
i + 1
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def wrap_text(text)
|
|
149
|
+
wrap_mode = @options[:wrap] || :keep
|
|
150
|
+
|
|
151
|
+
case wrap_mode
|
|
152
|
+
when :keep
|
|
153
|
+
text
|
|
154
|
+
when :no
|
|
155
|
+
# Remove soft line breaks, join lines
|
|
156
|
+
text.gsub(/\n(?!\n)/, " ")
|
|
157
|
+
when Integer
|
|
158
|
+
# Wrap at specified width
|
|
159
|
+
wrap_at_width(text, wrap_mode)
|
|
160
|
+
else
|
|
161
|
+
text
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def wrap_at_width(text, width)
|
|
166
|
+
return text if width <= 0
|
|
167
|
+
|
|
168
|
+
lines = []
|
|
169
|
+
paragraphs = text.split(/\n\n+/)
|
|
170
|
+
|
|
171
|
+
paragraphs.each_with_index do |paragraph, idx|
|
|
172
|
+
# Join lines within paragraph
|
|
173
|
+
paragraph = paragraph.gsub(/\n(?!\n)/, " ")
|
|
174
|
+
words = paragraph.split(/\s+/)
|
|
175
|
+
current_line = ""
|
|
176
|
+
|
|
177
|
+
words.each do |word|
|
|
178
|
+
if current_line.empty?
|
|
179
|
+
current_line = word
|
|
180
|
+
elsif (current_line.length + 1 + word.length) <= width
|
|
181
|
+
current_line += " " + word
|
|
182
|
+
else
|
|
183
|
+
lines << current_line
|
|
184
|
+
current_line = word
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
lines << current_line unless current_line.empty?
|
|
189
|
+
lines << "" if idx < paragraphs.length - 1
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
lines.join("\n")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def render_bullet_list(tokens, start_index, output)
|
|
196
|
+
marker = BULLET_MARKERS[@bullet_list_depth % 2]
|
|
197
|
+
@bullet_list_depth += 1
|
|
198
|
+
i = start_index + 1
|
|
199
|
+
|
|
200
|
+
while i < tokens.length && tokens[i].type != :bullet_list_close
|
|
201
|
+
if tokens[i].type == :list_item_open
|
|
202
|
+
i = render_list_item(tokens, i, output, "#{marker} ")
|
|
203
|
+
else
|
|
204
|
+
i += 1
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
@bullet_list_depth -= 1
|
|
209
|
+
output << "\n" unless output.last&.end_with?("\n\n")
|
|
210
|
+
i + 1
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def render_ordered_list(tokens, start_index, output)
|
|
214
|
+
open_token = tokens[start_index]
|
|
215
|
+
start_num = open_token.attrs[:start] || 1
|
|
216
|
+
current_num = start_num
|
|
217
|
+
i = start_index + 1
|
|
218
|
+
|
|
219
|
+
while i < tokens.length && tokens[i].type != :ordered_list_close
|
|
220
|
+
if tokens[i].type == :list_item_open
|
|
221
|
+
if @options[:number]
|
|
222
|
+
# Consecutive numbering mode
|
|
223
|
+
i = render_list_item(tokens, i, output, "#{current_num}. ")
|
|
224
|
+
current_num += 1
|
|
225
|
+
else
|
|
226
|
+
# mdformat default: all items use the same number (start number)
|
|
227
|
+
i = render_list_item(tokens, i, output, "#{start_num}. ")
|
|
228
|
+
end
|
|
229
|
+
else
|
|
230
|
+
i += 1
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
output << "\n" unless output.last&.end_with?("\n\n")
|
|
235
|
+
i + 1
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def render_list_item(tokens, start_index, output, prefix)
|
|
239
|
+
i = start_index + 1
|
|
240
|
+
item_content = []
|
|
241
|
+
|
|
242
|
+
while i < tokens.length && tokens[i].type != :list_item_close
|
|
243
|
+
token = tokens[i]
|
|
244
|
+
case token.type
|
|
245
|
+
when :paragraph_open
|
|
246
|
+
i = collect_paragraph_content(tokens, i, item_content)
|
|
247
|
+
when :bullet_list_open
|
|
248
|
+
nested_output = []
|
|
249
|
+
i = render_bullet_list(tokens, i, nested_output)
|
|
250
|
+
nested = nested_output.join.split("\n").map { |l| " #{l}" }.join("\n")
|
|
251
|
+
item_content << nested
|
|
252
|
+
when :ordered_list_open
|
|
253
|
+
nested_output = []
|
|
254
|
+
i = render_ordered_list(tokens, i, nested_output)
|
|
255
|
+
nested = nested_output.join.split("\n").map { |l| " #{l}" }.join("\n")
|
|
256
|
+
item_content << nested
|
|
257
|
+
else
|
|
258
|
+
i += 1
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
content = item_content.join("\n").strip
|
|
263
|
+
output << "#{prefix}#{content}\n"
|
|
264
|
+
i + 1
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def collect_paragraph_content(tokens, start_index, content_array)
|
|
268
|
+
i = start_index + 1
|
|
269
|
+
while i < tokens.length && tokens[i].type != :paragraph_close
|
|
270
|
+
if tokens[i].type == :inline
|
|
271
|
+
content_array << render_inline(tokens[i])
|
|
272
|
+
end
|
|
273
|
+
i += 1
|
|
274
|
+
end
|
|
275
|
+
i + 1
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def render_blockquote(tokens, start_index, output)
|
|
279
|
+
i = start_index + 1
|
|
280
|
+
inner_output = []
|
|
281
|
+
|
|
282
|
+
while i < tokens.length && tokens[i].type != :blockquote_close
|
|
283
|
+
token = tokens[i]
|
|
284
|
+
case token.type
|
|
285
|
+
when :paragraph_open
|
|
286
|
+
i = render_paragraph(tokens, i, inner_output)
|
|
287
|
+
when :heading_open
|
|
288
|
+
i = render_heading(tokens, i, inner_output)
|
|
289
|
+
when :blockquote_open
|
|
290
|
+
i = render_blockquote(tokens, i, inner_output)
|
|
291
|
+
else
|
|
292
|
+
i += 1
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
quoted = inner_output.join.chomp.split("\n").map { |l| "> #{l}".rstrip }.join("\n")
|
|
297
|
+
output << "#{quoted}\n\n"
|
|
298
|
+
i + 1
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def render_fence(token, output)
|
|
302
|
+
info = token.info || ""
|
|
303
|
+
content = token.content.chomp
|
|
304
|
+
|
|
305
|
+
# Determine the minimum number of backticks needed
|
|
306
|
+
backtick_count = 3
|
|
307
|
+
content.scan(/`+/) do |match|
|
|
308
|
+
backtick_count = [backtick_count, match.length + 1].max
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
marker = "`" * backtick_count
|
|
312
|
+
|
|
313
|
+
output << "#{marker}#{info}\n"
|
|
314
|
+
output << "#{content}\n"
|
|
315
|
+
output << "#{marker}\n\n"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def render_code_block(token, output)
|
|
319
|
+
# Convert indented code blocks to fenced code blocks (mdformat style)
|
|
320
|
+
content = token.content.chomp
|
|
321
|
+
|
|
322
|
+
# Determine the minimum number of backticks needed
|
|
323
|
+
backtick_count = 3
|
|
324
|
+
content.scan(/`+/) do |match|
|
|
325
|
+
backtick_count = [backtick_count, match.length + 1].max
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
marker = "`" * backtick_count
|
|
329
|
+
|
|
330
|
+
output << "#{marker}\n"
|
|
331
|
+
output << "#{content}\n"
|
|
332
|
+
output << "#{marker}\n\n"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def render_inline(token)
|
|
336
|
+
return token.content if token.children.empty?
|
|
337
|
+
|
|
338
|
+
render_inline_tokens(token.children)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def render_inline_tokens(tokens)
|
|
342
|
+
output = []
|
|
343
|
+
i = 0
|
|
344
|
+
|
|
345
|
+
while i < tokens.length
|
|
346
|
+
token = tokens[i]
|
|
347
|
+
case token.type
|
|
348
|
+
when :text
|
|
349
|
+
output << token.content
|
|
350
|
+
when :code_inline
|
|
351
|
+
content = token.content
|
|
352
|
+
# Strip unnecessary leading/trailing spaces (unless content contains backticks)
|
|
353
|
+
unless content.include?("`")
|
|
354
|
+
content = content.strip
|
|
355
|
+
end
|
|
356
|
+
# Determine minimum backticks needed
|
|
357
|
+
backtick_count = 1
|
|
358
|
+
content.scan(/`+/) do |match|
|
|
359
|
+
backtick_count = [backtick_count, match.length + 1].max
|
|
360
|
+
end
|
|
361
|
+
backticks = "`" * backtick_count
|
|
362
|
+
# Add space padding if content starts/ends with backtick
|
|
363
|
+
if content.start_with?("`") || content.end_with?("`")
|
|
364
|
+
output << "#{backticks} #{content} #{backticks}"
|
|
365
|
+
else
|
|
366
|
+
output << "#{backticks}#{content}#{backticks}"
|
|
367
|
+
end
|
|
368
|
+
when :strong_open
|
|
369
|
+
markup = token.markup.empty? ? "**" : token.markup
|
|
370
|
+
close_index = find_close_token(tokens, i, :strong_close)
|
|
371
|
+
inner = render_inline_tokens(tokens[(i + 1)...close_index])
|
|
372
|
+
output << "#{markup}#{inner}#{markup}"
|
|
373
|
+
i = close_index
|
|
374
|
+
when :em_open
|
|
375
|
+
markup = token.markup.empty? ? "*" : token.markup
|
|
376
|
+
close_index = find_close_token(tokens, i, :em_close)
|
|
377
|
+
inner = render_inline_tokens(tokens[(i + 1)...close_index])
|
|
378
|
+
output << "#{markup}#{inner}#{markup}"
|
|
379
|
+
i = close_index
|
|
380
|
+
when :link_open
|
|
381
|
+
close_index = find_close_token(tokens, i, :link_close)
|
|
382
|
+
inner = render_inline_tokens(tokens[(i + 1)...close_index])
|
|
383
|
+
|
|
384
|
+
if token.markup == "autolink"
|
|
385
|
+
href = token.attrs[:href] || ""
|
|
386
|
+
output << "<#{href}>"
|
|
387
|
+
elsif token.markup == "reference"
|
|
388
|
+
# Reference link - resolve from definitions
|
|
389
|
+
label = token.attrs[:reference_label]
|
|
390
|
+
ref = @reference_definitions[label]
|
|
391
|
+
if ref
|
|
392
|
+
@used_references << label
|
|
393
|
+
formatted_href = format_link_url(ref[:url])
|
|
394
|
+
if ref[:title]
|
|
395
|
+
output << "[#{inner}](#{formatted_href} \"#{ref[:title]}\")"
|
|
396
|
+
else
|
|
397
|
+
output << "[#{inner}](#{formatted_href})"
|
|
398
|
+
end
|
|
399
|
+
else
|
|
400
|
+
# Reference not found, keep as-is
|
|
401
|
+
output << "[#{inner}]"
|
|
402
|
+
end
|
|
403
|
+
else
|
|
404
|
+
href = token.attrs[:href] || ""
|
|
405
|
+
title = token.attrs[:title]
|
|
406
|
+
formatted_href = format_link_url(href)
|
|
407
|
+
if title
|
|
408
|
+
output << "[#{inner}](#{formatted_href} \"#{title}\")"
|
|
409
|
+
else
|
|
410
|
+
output << "[#{inner}](#{formatted_href})"
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
i = close_index
|
|
414
|
+
when :image
|
|
415
|
+
alt = token.attrs[:alt] || token.content || ""
|
|
416
|
+
src = token.attrs[:src] || ""
|
|
417
|
+
title = token.attrs[:title]
|
|
418
|
+
if title
|
|
419
|
+
output << ""
|
|
420
|
+
else
|
|
421
|
+
output << ""
|
|
422
|
+
end
|
|
423
|
+
when :softbreak
|
|
424
|
+
output << "\n"
|
|
425
|
+
when :hardbreak
|
|
426
|
+
output << "\\\n"
|
|
427
|
+
when :html_inline
|
|
428
|
+
output << token.content
|
|
429
|
+
end
|
|
430
|
+
i += 1
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
output.join
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def find_close_token(tokens, start_index, close_type)
|
|
437
|
+
i = start_index + 1
|
|
438
|
+
while i < tokens.length
|
|
439
|
+
return i if tokens[i].type == close_type
|
|
440
|
+
|
|
441
|
+
i += 1
|
|
442
|
+
end
|
|
443
|
+
tokens.length
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def format_link_url(url)
|
|
447
|
+
# Remove angle brackets if present and not needed
|
|
448
|
+
url = url.gsub(/^<|>$/, "")
|
|
449
|
+
# Add angle brackets only if URL contains spaces or parentheses
|
|
450
|
+
if url.match?(/[\s()]/)
|
|
451
|
+
"<#{url}>"
|
|
452
|
+
else
|
|
453
|
+
url
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
data/lib/mdlint/token.rb
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mdlint
|
|
4
|
+
class Token
|
|
5
|
+
attr_accessor :type, :tag, :nesting, :level, :content, :markup, :info, :meta, :map, :children, :attrs
|
|
6
|
+
|
|
7
|
+
def initialize(type:, **kwargs)
|
|
8
|
+
@type = type
|
|
9
|
+
@tag = kwargs[:tag]
|
|
10
|
+
@nesting = kwargs[:nesting] || 0
|
|
11
|
+
@level = kwargs[:level] || 0
|
|
12
|
+
@content = kwargs[:content] || ""
|
|
13
|
+
@markup = kwargs[:markup] || ""
|
|
14
|
+
@info = kwargs[:info] || ""
|
|
15
|
+
@meta = kwargs[:meta] || {}
|
|
16
|
+
@map = kwargs[:map]
|
|
17
|
+
@children = kwargs[:children] || []
|
|
18
|
+
@attrs = kwargs[:attrs] || {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def opening?
|
|
22
|
+
@nesting == 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def closing?
|
|
26
|
+
@nesting == -1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self_closing?
|
|
30
|
+
@nesting.zero?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def block?
|
|
34
|
+
BLOCK_TYPES.include?(@type)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def inline?
|
|
38
|
+
INLINE_TYPES.include?(@type)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
BLOCK_TYPES = %i[
|
|
42
|
+
document
|
|
43
|
+
heading_open heading_close
|
|
44
|
+
paragraph_open paragraph_close
|
|
45
|
+
bullet_list_open bullet_list_close
|
|
46
|
+
ordered_list_open ordered_list_close
|
|
47
|
+
list_item_open list_item_close
|
|
48
|
+
blockquote_open blockquote_close
|
|
49
|
+
code_block fence
|
|
50
|
+
hr html_block
|
|
51
|
+
inline
|
|
52
|
+
].freeze
|
|
53
|
+
|
|
54
|
+
INLINE_TYPES = %i[
|
|
55
|
+
text
|
|
56
|
+
strong_open strong_close
|
|
57
|
+
em_open em_close
|
|
58
|
+
code_inline
|
|
59
|
+
link_open link_close
|
|
60
|
+
image
|
|
61
|
+
softbreak hardbreak
|
|
62
|
+
html_inline
|
|
63
|
+
].freeze
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/mdlint.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mdlint/version"
|
|
4
|
+
require_relative "mdlint/token"
|
|
5
|
+
require_relative "mdlint/parser"
|
|
6
|
+
require_relative "mdlint/renderer"
|
|
7
|
+
require_relative "mdlint/linter"
|
|
8
|
+
|
|
9
|
+
module Mdlint
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def format(src, options = {})
|
|
14
|
+
tokens = Parser.parse(src)
|
|
15
|
+
Renderer.render(tokens, options)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def format_file(path, options = {})
|
|
19
|
+
src = File.read(path)
|
|
20
|
+
formatted = format(src, options)
|
|
21
|
+
|
|
22
|
+
if options[:check]
|
|
23
|
+
src != formatted
|
|
24
|
+
else
|
|
25
|
+
File.write(path, formatted) unless options[:diff]
|
|
26
|
+
formatted
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse(src)
|
|
31
|
+
Parser.parse(src)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def lint(src, options = {})
|
|
35
|
+
Linter.check(src, options)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def lint_file(path, options = {})
|
|
39
|
+
src = File.read(path)
|
|
40
|
+
lint(src, options)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mdlint
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Mdlint is a Pure Ruby Markdown linter and formatter with no external
|
|
13
|
+
dependencies. It provides configurable lint rules and automatic formatting for consistent
|
|
14
|
+
Markdown files.
|
|
15
|
+
email:
|
|
16
|
+
- t.yudai92@gmail.com
|
|
17
|
+
executables:
|
|
18
|
+
- mdlint
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- CHANGELOG.md
|
|
23
|
+
- LICENSE.txt
|
|
24
|
+
- README.md
|
|
25
|
+
- Rakefile
|
|
26
|
+
- exe/mdlint
|
|
27
|
+
- lib/mdlint.rb
|
|
28
|
+
- lib/mdlint/cli.rb
|
|
29
|
+
- lib/mdlint/config.rb
|
|
30
|
+
- lib/mdlint/linter.rb
|
|
31
|
+
- lib/mdlint/linter/rule.rb
|
|
32
|
+
- lib/mdlint/linter/rule_engine.rb
|
|
33
|
+
- lib/mdlint/linter/rules/first_line_heading.rb
|
|
34
|
+
- lib/mdlint/linter/rules/heading_increment.rb
|
|
35
|
+
- lib/mdlint/linter/rules/heading_style.rb
|
|
36
|
+
- lib/mdlint/linter/rules/no_multiple_blanks.rb
|
|
37
|
+
- lib/mdlint/linter/rules/no_trailing_spaces.rb
|
|
38
|
+
- lib/mdlint/linter/violation.rb
|
|
39
|
+
- lib/mdlint/parser.rb
|
|
40
|
+
- lib/mdlint/parser/block_parser.rb
|
|
41
|
+
- lib/mdlint/parser/inline_parser.rb
|
|
42
|
+
- lib/mdlint/parser/state.rb
|
|
43
|
+
- lib/mdlint/renderer.rb
|
|
44
|
+
- lib/mdlint/renderer/md_renderer.rb
|
|
45
|
+
- lib/mdlint/token.rb
|
|
46
|
+
- lib/mdlint/version.rb
|
|
47
|
+
homepage: https://github.com/ydah/mdlint
|
|
48
|
+
licenses:
|
|
49
|
+
- MIT
|
|
50
|
+
metadata:
|
|
51
|
+
allowed_push_host: https://rubygems.org
|
|
52
|
+
homepage_uri: https://github.com/ydah/mdlint
|
|
53
|
+
source_code_uri: https://github.com/ydah/mdlint
|
|
54
|
+
changelog_uri: https://github.com/ydah/mdlint/blob/main/CHANGELOG.md
|
|
55
|
+
rubygems_mfa_required: 'true'
|
|
56
|
+
rdoc_options: []
|
|
57
|
+
require_paths:
|
|
58
|
+
- lib
|
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: 3.2.0
|
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
requirements: []
|
|
70
|
+
rubygems_version: 4.0.4
|
|
71
|
+
specification_version: 4
|
|
72
|
+
summary: A Pure Ruby Markdown linter and formatter
|
|
73
|
+
test_files: []
|