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
data/lib/mdlint/lsp.rb
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Mdlint
|
|
7
|
+
module Lsp
|
|
8
|
+
class Server
|
|
9
|
+
SEVERITY = { error: 1, warning: 2, info: 3 }.freeze
|
|
10
|
+
|
|
11
|
+
def initialize(input: $stdin, output: $stdout)
|
|
12
|
+
@input = input
|
|
13
|
+
@output = output
|
|
14
|
+
@documents = {}
|
|
15
|
+
@running = true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run
|
|
19
|
+
while @running
|
|
20
|
+
message = read_message
|
|
21
|
+
break unless message
|
|
22
|
+
|
|
23
|
+
response = handle_message(message)
|
|
24
|
+
write_message(response) if response
|
|
25
|
+
end
|
|
26
|
+
0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def read_message
|
|
32
|
+
headers = {}
|
|
33
|
+
loop do
|
|
34
|
+
line = @input.gets
|
|
35
|
+
return nil unless line
|
|
36
|
+
break if line == "\r\n" || line == "\n"
|
|
37
|
+
|
|
38
|
+
key, value = line.split(":", 2)
|
|
39
|
+
headers[key.downcase] = value.to_s.strip if key && value
|
|
40
|
+
end
|
|
41
|
+
length = headers.fetch("content-length").to_i
|
|
42
|
+
payload = @input.read(length)
|
|
43
|
+
return nil unless payload && payload.bytesize == length
|
|
44
|
+
|
|
45
|
+
JSON.parse(payload)
|
|
46
|
+
rescue JSON::ParserError, KeyError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def write_message(message)
|
|
51
|
+
payload = JSON.generate(message)
|
|
52
|
+
@output.write("Content-Length: #{payload.bytesize}\r\n\r\n")
|
|
53
|
+
@output.write(payload)
|
|
54
|
+
@output.flush if @output.respond_to?(:flush)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def handle_message(message)
|
|
58
|
+
method = message["method"]
|
|
59
|
+
params = message["params"] || {}
|
|
60
|
+
id = message["id"]
|
|
61
|
+
|
|
62
|
+
case method
|
|
63
|
+
when "initialize"
|
|
64
|
+
response(id, {
|
|
65
|
+
capabilities: {
|
|
66
|
+
textDocumentSync: 1,
|
|
67
|
+
documentFormattingProvider: true,
|
|
68
|
+
codeActionProvider: true
|
|
69
|
+
},
|
|
70
|
+
serverInfo: { name: "mdlint", version: Mdlint::VERSION }
|
|
71
|
+
})
|
|
72
|
+
when "initialized"
|
|
73
|
+
nil
|
|
74
|
+
when "shutdown"
|
|
75
|
+
response(id, nil)
|
|
76
|
+
when "exit"
|
|
77
|
+
@running = false
|
|
78
|
+
nil
|
|
79
|
+
when "textDocument/didOpen"
|
|
80
|
+
update_document(params.dig("textDocument", "uri"), params.dig("textDocument", "text"))
|
|
81
|
+
publish_diagnostics(params.dig("textDocument", "uri"))
|
|
82
|
+
nil
|
|
83
|
+
when "textDocument/didChange"
|
|
84
|
+
uri = params.dig("textDocument", "uri")
|
|
85
|
+
change = Array(params["contentChanges"]).last
|
|
86
|
+
text = change.is_a?(Hash) ? change["text"] : nil
|
|
87
|
+
update_document(uri, text)
|
|
88
|
+
publish_diagnostics(uri)
|
|
89
|
+
nil
|
|
90
|
+
when "textDocument/didClose"
|
|
91
|
+
@documents.delete(params.dig("textDocument", "uri"))
|
|
92
|
+
nil
|
|
93
|
+
when "textDocument/formatting"
|
|
94
|
+
response(id, formatting_edits(params.dig("textDocument", "uri")))
|
|
95
|
+
when "textDocument/codeAction"
|
|
96
|
+
response(id, code_actions(params.dig("textDocument", "uri")))
|
|
97
|
+
else
|
|
98
|
+
response(id, nil) if id
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def response(id, result)
|
|
103
|
+
{ jsonrpc: "2.0", id: id, result: result }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def notification(method, params)
|
|
107
|
+
{ jsonrpc: "2.0", method: method, params: params }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def update_document(uri, text)
|
|
111
|
+
return unless uri && text
|
|
112
|
+
|
|
113
|
+
@documents[uri] = text
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def formatting_edits(uri)
|
|
117
|
+
text = @documents[uri]
|
|
118
|
+
return [] unless text
|
|
119
|
+
|
|
120
|
+
[{ range: full_range(text), newText: Mdlint.format(text) }]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def code_actions(uri)
|
|
124
|
+
text = @documents[uri]
|
|
125
|
+
return [] unless text
|
|
126
|
+
|
|
127
|
+
fixed = Mdlint.fix(text)
|
|
128
|
+
return [] if fixed == text
|
|
129
|
+
|
|
130
|
+
[{
|
|
131
|
+
title: "Fix Markdown",
|
|
132
|
+
kind: "quickfix",
|
|
133
|
+
isPreferred: true,
|
|
134
|
+
edit: { changes: { uri => [{ range: full_range(text), newText: fixed }] } }
|
|
135
|
+
}]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def publish_diagnostics(uri)
|
|
139
|
+
text = @documents[uri]
|
|
140
|
+
return unless text
|
|
141
|
+
|
|
142
|
+
diagnostics = Mdlint.lint(text, filename: uri_path(uri)).map do |violation|
|
|
143
|
+
start_character = [violation.column.to_i - 1, 0].max
|
|
144
|
+
{
|
|
145
|
+
range: {
|
|
146
|
+
start: { line: violation.line - 1, character: start_character },
|
|
147
|
+
end: { line: violation.line - 1, character: start_character + 1 }
|
|
148
|
+
},
|
|
149
|
+
severity: SEVERITY.fetch(violation.severity, 2),
|
|
150
|
+
code: violation.rule_id,
|
|
151
|
+
source: "mdlint",
|
|
152
|
+
message: violation.message
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
write_message(notification("textDocument/publishDiagnostics", { uri: uri, diagnostics: diagnostics }))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def full_range(text)
|
|
159
|
+
lines = text.lines
|
|
160
|
+
last_line = [lines.length - 1, 0].max
|
|
161
|
+
last_character = lines.last ? lines.last.chomp.length : 0
|
|
162
|
+
{ start: { line: 0, character: 0 }, end: { line: last_line, character: last_character } }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def uri_path(uri)
|
|
166
|
+
return nil unless uri
|
|
167
|
+
return uri unless uri.start_with?("file:")
|
|
168
|
+
|
|
169
|
+
path = URI.parse(uri).path
|
|
170
|
+
URI.decode_www_form_component(path.to_s)
|
|
171
|
+
rescue URI::InvalidURIError
|
|
172
|
+
uri
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thread"
|
|
4
|
+
|
|
5
|
+
module Mdlint
|
|
6
|
+
class ParallelRunner
|
|
7
|
+
class << self
|
|
8
|
+
def map(items, jobs: 1)
|
|
9
|
+
return items.map { |item| yield item } unless jobs.to_i > 1 && items.length > 1
|
|
10
|
+
|
|
11
|
+
queue = Queue.new
|
|
12
|
+
items.each_with_index { |item, index| queue << [index, item] }
|
|
13
|
+
results = Array.new(items.length)
|
|
14
|
+
errors = Queue.new
|
|
15
|
+
workers = Array.new([jobs.to_i, items.length].min) do
|
|
16
|
+
Thread.new do
|
|
17
|
+
Thread.current.report_on_exception = false
|
|
18
|
+
process_queue(queue, results, errors) { |item| yield item }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
workers.each(&:join)
|
|
22
|
+
raise errors.pop unless errors.empty?
|
|
23
|
+
|
|
24
|
+
results
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def process_queue(queue, results, errors)
|
|
30
|
+
loop do
|
|
31
|
+
index, item = queue.pop(true)
|
|
32
|
+
results[index] = yield item
|
|
33
|
+
rescue ThreadError
|
|
34
|
+
break
|
|
35
|
+
rescue StandardError => error
|
|
36
|
+
errors << error
|
|
37
|
+
break
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|