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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.pre-commit-hooks.yaml +6 -0
  3. data/CHANGELOG.md +33 -0
  4. data/README.md +136 -4
  5. data/Rakefile +14 -0
  6. data/Steepfile +17 -0
  7. data/action.yml +54 -0
  8. data/benchmark/compare.rb +49 -0
  9. data/benchmark/format.rb +29 -0
  10. data/lib/mdlint/cache_store.rb +85 -0
  11. data/lib/mdlint/cli/output_formatter.rb +150 -0
  12. data/lib/mdlint/cli.rb +268 -106
  13. data/lib/mdlint/config.rb +87 -1
  14. data/lib/mdlint/dialect.rb +53 -0
  15. data/lib/mdlint/linter/directive_filter.rb +91 -0
  16. data/lib/mdlint/linter/rule.rb +25 -5
  17. data/lib/mdlint/linter/rule_engine.rb +44 -7
  18. data/lib/mdlint/linter/rules/code_block_syntax.rb +128 -0
  19. data/lib/mdlint/linter/rules/first_line_heading.rb +10 -3
  20. data/lib/mdlint/linter/rules/heading_increment.rb +4 -3
  21. data/lib/mdlint/linter/rules/heading_style.rb +24 -2
  22. data/lib/mdlint/linter/rules/japanese.rb +201 -0
  23. data/lib/mdlint/linter/rules/line_length.rb +37 -0
  24. data/lib/mdlint/linter/rules/link_check.rb +151 -0
  25. data/lib/mdlint/linter/rules/no_multiple_blanks.rb +1 -0
  26. data/lib/mdlint/linter/rules/no_trailing_spaces.rb +1 -0
  27. data/lib/mdlint/linter/rules/source_style.rb +317 -0
  28. data/lib/mdlint/linter/violation.rb +16 -1
  29. data/lib/mdlint/linter.rb +9 -3
  30. data/lib/mdlint/lsp.rb +176 -0
  31. data/lib/mdlint/parallel_runner.rb +42 -0
  32. data/lib/mdlint/parser/block_parser.rb +627 -50
  33. data/lib/mdlint/parser/inline_parser.rb +259 -27
  34. data/lib/mdlint/parser/state.rb +21 -2
  35. data/lib/mdlint/parser.rb +5 -5
  36. data/lib/mdlint/plugin.rb +31 -0
  37. data/lib/mdlint/renderer/html_renderer.rb +346 -0
  38. data/lib/mdlint/renderer/md_renderer.rb +147 -11
  39. data/lib/mdlint/renderer.rb +5 -0
  40. data/lib/mdlint/text_width.rb +39 -0
  41. data/lib/mdlint/toc.rb +80 -0
  42. data/lib/mdlint/token.rb +3 -1
  43. data/lib/mdlint/version.rb +1 -1
  44. data/lib/mdlint.rb +25 -5
  45. data/script/commonmark_compatibility.rb +24 -0
  46. data/script/fetch_commonmark_spec.rb +14 -0
  47. data/sig/internal.rbs +405 -0
  48. data/sig/mdlint.rbs +107 -0
  49. 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