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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25d6971b46664870bcf318a6013fe69079a6e8208f061d22e39ec18cce41d15a
|
|
4
|
+
data.tar.gz: 471f942064bb9944fd5344766811f32b9ab28e15112c873399e5fb034fa52770
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 241b2426e28d513cc63daccc62dc22ae264d0269d12301ae324ba8318ebe0a08c90f6a9523e421ee3e577dcedf694ae957c68034ec8bcd266db78502ca7e281d
|
|
7
|
+
data.tar.gz: a5e7d1800c8ec2c090b5de2d3605dafc66bf0ed087565dd97db4136b680b4a0464e70c603ed23960a54e395c92e7e1a14408898cdc90446887d204a65bc64ad6
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.2.0 - 2026-08-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added `lint`, `fix`, and `lsp` CLI commands while keeping formatting as the default command.
|
|
15
|
+
- Added lint fixes with `--fix`, `--fix-only`, and `--dry-run`.
|
|
16
|
+
- Added inline directives for disabling and enabling rules, including file-level and next-line controls.
|
|
17
|
+
- Added rule discovery and guidance through `--list-rules` and `--explain`, with Markdownlint-compatible rule IDs and aliases.
|
|
18
|
+
- Added configurable severities, fail levels, and text, JSON, SARIF, GitHub, Checkstyle, JUnit, and reviewdog output formats.
|
|
19
|
+
- Added YAML, TOML, and JSON front matter preservation during parsing and formatting.
|
|
20
|
+
- Added GFM tables, task lists, strikethrough, bare URL autolinks, and selectable Markdown dialects.
|
|
21
|
+
- Added HTML rendering through `Mdlint.html`.
|
|
22
|
+
- Added `--auto-gen-config`, parallel file processing, and content/configuration-hash caching.
|
|
23
|
+
- Added GitHub Action, reviewdog integration, and a pre-commit hook.
|
|
24
|
+
- Added LSP diagnostics, formatting, and quick fixes.
|
|
25
|
+
- Added the Japanese technical-writing preset (`JA001`–`JA007`), CJK-aware wrapping, link and anchor checks, and table-of-contents updates.
|
|
26
|
+
- Added optional fenced-code validation and formatting commands with configurable timeouts.
|
|
27
|
+
- Added 29 built-in lint rules and a stable plugin API for custom rules and dialect feature sets.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- `Mdlint.lint` now honors rule selections, disabled rules, aliases, severity settings, and configuration-file options from `.mdlint.yml`.
|
|
32
|
+
- `Mdlint.fix` and rule fixes now consistently return formatted source strings.
|
|
33
|
+
- Markdown parsing and HTML rendering now handle nested lists, loose list items, reference links and images, entities, code spans, block quotes, directives, and common URI autolinks more consistently.
|
|
34
|
+
- GFM behavior is enabled explicitly with `dialect: :gfm` or `--dialect gfm`; CommonMark remains the default dialect.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- Fixed front matter being interpreted as thematic breaks, headings, or ordinary Markdown content.
|
|
39
|
+
- Fixed configured rules and disabled rules being ignored by the linter.
|
|
40
|
+
- Fixed inline links, reference links, images, escaped destinations, and nested link destinations being parsed or rendered incorrectly.
|
|
41
|
+
- Fixed formatter output changing document meaning in supported Markdown cases and added idempotence and HTML meaning-preservation checks.
|
|
42
|
+
|
|
10
43
|
## 0.1.0 - 2025-01-23
|
|
11
44
|
|
|
12
45
|
- Initial release
|
data/README.md
CHANGED
|
@@ -24,7 +24,18 @@
|
|
|
24
24
|
- Linting with configurable rules and clear violations
|
|
25
25
|
- Auto-formatting that follows mdformat conventions
|
|
26
26
|
- CLI for CI-friendly workflows and local formatting
|
|
27
|
-
-
|
|
27
|
+
- YAML/TOML/JSON front matter preservation
|
|
28
|
+
- GFM tables, task lists, strikethrough, and configurable dialects
|
|
29
|
+
- Text, JSON, SARIF, Checkstyle, JUnit, and GitHub diagnostic output
|
|
30
|
+
- Optional HTML rendering through the Ruby API
|
|
31
|
+
- GitHub Action and RBS/Steep signatures for integration and tooling
|
|
32
|
+
- reviewdog-compatible RDJSON output through the CLI and GitHub Action
|
|
33
|
+
- Language Server Protocol diagnostics, formatting, and quick fixes
|
|
34
|
+
- Parallel linting with a content/configuration-hash cache
|
|
35
|
+
- Japanese technical-writing preset, code-block syntax checks, link/anchor checks, and TOC updates
|
|
36
|
+
- Optional language commands for code-block validation and formatting
|
|
37
|
+
- 29 built-in lint rules with Markdownlint-compatible IDs and aliases
|
|
38
|
+
- Stable plugin registration for custom rules and dialect feature sets
|
|
28
39
|
|
|
29
40
|
## Quickstart
|
|
30
41
|
|
|
@@ -39,6 +50,12 @@ mdlint --check README.md
|
|
|
39
50
|
|
|
40
51
|
# show diffs
|
|
41
52
|
mdlint --diff README.md
|
|
53
|
+
|
|
54
|
+
# lint and report violations
|
|
55
|
+
mdlint lint docs/
|
|
56
|
+
|
|
57
|
+
# fix fixable lint violations
|
|
58
|
+
mdlint fix --dry-run docs/
|
|
42
59
|
```
|
|
43
60
|
|
|
44
61
|
## Installation
|
|
@@ -63,12 +80,16 @@ Command line:
|
|
|
63
80
|
mdlint README.md docs/
|
|
64
81
|
mdlint --check README.md
|
|
65
82
|
mdlint --diff README.md
|
|
83
|
+
mdlint lint README.md
|
|
84
|
+
mdlint fix README.md
|
|
66
85
|
```
|
|
67
86
|
|
|
68
87
|
Options:
|
|
69
88
|
|
|
70
89
|
```
|
|
71
|
-
Usage: mdlint [options] [paths...]
|
|
90
|
+
Usage: mdlint [command] [options] [paths...]
|
|
91
|
+
|
|
92
|
+
Commands: format (default), lint, fix, lsp
|
|
72
93
|
|
|
73
94
|
Options:
|
|
74
95
|
-c, --check Check if files are formatted, exit with error if not
|
|
@@ -77,7 +98,34 @@ Options:
|
|
|
77
98
|
-e, --exclude PATTERN Exclude files matching pattern
|
|
78
99
|
-w, --wrap MODE Paragraph wrapping: keep (default), no, or INTEGER
|
|
79
100
|
--number Use consecutive numbering for ordered lists
|
|
80
|
-
|
|
101
|
+
--end-of-line MODE End of line: lf (default), crlf, keep
|
|
102
|
+
--fix Fix fixable lint violations
|
|
103
|
+
--fix-only Apply fixes without reporting remaining violations
|
|
104
|
+
--dry-run Do not write fixes to files
|
|
105
|
+
--disable RULES Disable comma-separated rules
|
|
106
|
+
--rule RULES Run only comma-separated rules
|
|
107
|
+
--severity LEVEL Default severity: error, warning, or info
|
|
108
|
+
--fail-level LEVEL Fail at severity: error, warning, or info
|
|
109
|
+
--format FORMAT Lint output: text, json, sarif, github, checkstyle, junit, reviewdog
|
|
110
|
+
--stdin-filename NAME Filename to use for stdin diagnostics
|
|
111
|
+
--dialect DIALECT Markdown dialect: commonmark or gfm
|
|
112
|
+
--preset NAME Enable a rule preset, such as japanese
|
|
113
|
+
--check-links Check relative link, image, and anchor targets
|
|
114
|
+
--check-external-links Check external HTTP(S) links
|
|
115
|
+
--check-code-blocks Validate supported fenced code blocks
|
|
116
|
+
--code-block-command SPEC Validate a language block with LANG=COMMAND
|
|
117
|
+
--code-block-formatter SPEC Format a language block with LANG=COMMAND
|
|
118
|
+
--code-block-timeout SECONDS Timeout for external code-block commands
|
|
119
|
+
--toc Update table-of-contents markers
|
|
120
|
+
--no-table-align Do not pad GFM table columns
|
|
121
|
+
--jobs N Process files concurrently
|
|
122
|
+
--cache Cache lint diagnostics
|
|
123
|
+
--cache-path PATH Path for the lint cache
|
|
124
|
+
--lsp Run the Language Server Protocol server
|
|
125
|
+
--list-rules List available lint rules
|
|
126
|
+
--explain RULE Explain a lint rule
|
|
127
|
+
--require PATH Load a custom rule file
|
|
128
|
+
--auto-gen-config Write a config disabling current violations
|
|
81
129
|
-v, --version Show version
|
|
82
130
|
-h, --help Show help
|
|
83
131
|
```
|
|
@@ -96,6 +144,11 @@ violations = Mdlint.lint("# Heading\n\n\n\nParagraph")
|
|
|
96
144
|
violations.each { |v| puts v }
|
|
97
145
|
|
|
98
146
|
violations = Mdlint.lint_file("README.md")
|
|
147
|
+
|
|
148
|
+
fixed = Mdlint.fix("Line \n")
|
|
149
|
+
html = Mdlint.html("# Hello\n\n**world**\n")
|
|
150
|
+
gfm_tokens = Mdlint.parse("- [x] Done\n", dialect: :gfm)
|
|
151
|
+
toc_updated = Mdlint.update_toc("# Hello\n\n<!-- toc -->\nold\n<!-- toc -->\n")
|
|
99
152
|
```
|
|
100
153
|
|
|
101
154
|
## Configuration
|
|
@@ -113,6 +166,39 @@ quiet: false
|
|
|
113
166
|
exclude:
|
|
114
167
|
- "vendor/**/*.md"
|
|
115
168
|
- "node_modules/**/*.md"
|
|
169
|
+
|
|
170
|
+
# Markdown dialect
|
|
171
|
+
dialect: commonmark # or gfm
|
|
172
|
+
|
|
173
|
+
# Optional integrations
|
|
174
|
+
preset: japanese
|
|
175
|
+
toc: false
|
|
176
|
+
check_links: false
|
|
177
|
+
check_external_links: false
|
|
178
|
+
check_code_blocks: false
|
|
179
|
+
code_block_timeout: 10
|
|
180
|
+
jobs: 2
|
|
181
|
+
cache: true
|
|
182
|
+
cache_path: .mdlint_cache
|
|
183
|
+
|
|
184
|
+
# Rule configuration (markdownlint-compatible IDs and aliases are accepted)
|
|
185
|
+
rules:
|
|
186
|
+
MD013:
|
|
187
|
+
enabled: true
|
|
188
|
+
line_length: 120
|
|
189
|
+
ignore_code_blocks: true
|
|
190
|
+
MD009: false
|
|
191
|
+
|
|
192
|
+
# Severity and CI threshold
|
|
193
|
+
severity: warning
|
|
194
|
+
fail_level: error
|
|
195
|
+
|
|
196
|
+
# Custom rule files
|
|
197
|
+
plugins:
|
|
198
|
+
- "./rules/my_rule.rb"
|
|
199
|
+
|
|
200
|
+
# A plugin may register a feature subset for a project-specific dialect.
|
|
201
|
+
# Mdlint::Plugin.register_dialect(:docs, features: [:tables])
|
|
116
202
|
```
|
|
117
203
|
|
|
118
204
|
## Lint Rules
|
|
@@ -124,13 +210,44 @@ Key rules:
|
|
|
124
210
|
- No trailing spaces
|
|
125
211
|
- No multiple consecutive blank lines
|
|
126
212
|
- First line should be a top-level heading
|
|
213
|
+
- Lines should respect MD013's configured length
|
|
214
|
+
- Relative links, images, and anchors can be checked with `mdlint lint --check-links`.
|
|
215
|
+
- External HTTP(S) links are opt-in with `--check-external-links`.
|
|
216
|
+
- `--preset japanese` enables JA001–JA007 writing checks; options include `sentence_length` and `max_commas`.
|
|
217
|
+
- `--check-code-blocks` validates JSON and Ruby fenced blocks.
|
|
218
|
+
- `--code-block-command python='python -m py_compile -'` adds opt-in validation for another language; commands receive block content on stdin.
|
|
219
|
+
- `--code-block-formatter python='black -'` can rewrite an external language block; commands are bounded by `code_block_timeout` (10 seconds by default).
|
|
220
|
+
- `--toc` regenerates content between paired `<!-- toc -->` or `<!-- toc:start -->` / `<!-- toc:end -->` markers.
|
|
221
|
+
|
|
222
|
+
Plugins can use the stable API from a required Ruby file:
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
class MyRule < Mdlint::Linter::Rule
|
|
226
|
+
self.rule_id = "PL001"
|
|
227
|
+
self.aliases = ["my-rule"]
|
|
228
|
+
self.description = "Project-specific Markdown rule"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
Mdlint::Plugin.register_rule(MyRule)
|
|
232
|
+
Mdlint::Plugin.register_dialect(:docs, features: [:tables, :task_lists])
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Inline directives can suppress diagnostics for a section or one line:
|
|
236
|
+
|
|
237
|
+
```markdown
|
|
238
|
+
<!-- mdlint-disable MD013 -->
|
|
239
|
+
long content is allowed here
|
|
240
|
+
<!-- mdlint-enable MD013 -->
|
|
241
|
+
<!-- mdlint-disable-next-line MD001 -->
|
|
242
|
+
### Intentional jump
|
|
243
|
+
```
|
|
127
244
|
|
|
128
245
|
## Formatting Style
|
|
129
246
|
|
|
130
247
|
| Element | Style |
|
|
131
248
|
|---------|-------|
|
|
132
249
|
| Headings | ATX style only (`#`) |
|
|
133
|
-
| Bullet lists | Hyphen (`-`)
|
|
250
|
+
| Bullet lists | Hyphen (`-`) at every nesting level |
|
|
134
251
|
| Ordered lists | All items use `1.` (minimizes diffs) |
|
|
135
252
|
| Code blocks | Fenced style (`` ``` ``) |
|
|
136
253
|
| Horizontal rules | 70 underscores |
|
|
@@ -153,6 +270,7 @@ Block elements:
|
|
|
153
270
|
- Horizontal rules (`---`, `***`, `___`)
|
|
154
271
|
- HTML blocks
|
|
155
272
|
- Reference definitions
|
|
273
|
+
- Footnotes, fenced math blocks, GitHub alerts, and MDX/JSX component blocks
|
|
156
274
|
|
|
157
275
|
Inline elements:
|
|
158
276
|
|
|
@@ -165,11 +283,25 @@ Inline elements:
|
|
|
165
283
|
- Autolinks (`<https://example.com>`)
|
|
166
284
|
- Hard breaks (backslash + newline)
|
|
167
285
|
|
|
286
|
+
With `dialect: gfm`, tables, task lists, strikethrough, and bare URL autolinks are supported. Footnote definitions/references, fenced math blocks, GitHub alerts, and MDX/JSX blocks are preserved and included in HTML output.
|
|
287
|
+
|
|
168
288
|
## Development
|
|
169
289
|
|
|
170
290
|
```bash
|
|
171
291
|
bin/setup
|
|
172
292
|
bundle exec rspec
|
|
293
|
+
rbs validate
|
|
294
|
+
steep check --no-daemon --severity-level error
|
|
295
|
+
|
|
296
|
+
# Optional quality tools
|
|
297
|
+
ruby script/fetch_commonmark_spec.rb
|
|
298
|
+
bundle exec rspec spec/commonmark_spec.rb
|
|
299
|
+
ITERATIONS=100 ruby benchmark/format.rb README.md
|
|
300
|
+
ruby script/commonmark_compatibility.rb spec/fixtures/commonmark_smoke.json
|
|
301
|
+
ITERATIONS=100 ruby benchmark/compare.rb README.md
|
|
302
|
+
# Property tests cover formatter idempotence and HTML meaning preservation.
|
|
303
|
+
bundle exec rspec spec/mdlint/property_spec.rb
|
|
304
|
+
bundle exec rake property
|
|
173
305
|
```
|
|
174
306
|
|
|
175
307
|
## Contributing
|
data/Rakefile
CHANGED
|
@@ -4,5 +4,19 @@ require "bundler/gem_tasks"
|
|
|
4
4
|
require "rspec/core/rake_task"
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
RSpec::Core::RakeTask.new(:property) do |task|
|
|
8
|
+
task.pattern = "spec/mdlint/property_spec.rb"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
namespace :commonmark do
|
|
12
|
+
task :fetch do
|
|
13
|
+
sh "ruby script/fetch_commonmark_spec.rb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
task :compatibility do
|
|
17
|
+
sh "ruby script/fetch_commonmark_spec.rb spec/fixtures/commonmark.json"
|
|
18
|
+
sh "ruby script/commonmark_compatibility.rb spec/fixtures/commonmark.json"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
7
21
|
|
|
8
22
|
task default: :spec
|
data/Steepfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
target :lib do
|
|
2
|
+
signature "sig"
|
|
3
|
+
library "cgi"
|
|
4
|
+
library "digest"
|
|
5
|
+
library "fileutils"
|
|
6
|
+
library "json"
|
|
7
|
+
library "net-http"
|
|
8
|
+
library "open3"
|
|
9
|
+
library "optparse"
|
|
10
|
+
library "ripper"
|
|
11
|
+
library "shellwords"
|
|
12
|
+
library "stringio"
|
|
13
|
+
library "timeout"
|
|
14
|
+
library "uri"
|
|
15
|
+
library "yaml"
|
|
16
|
+
check "lib"
|
|
17
|
+
end
|
data/action.yml
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: mdlint
|
|
2
|
+
description: Lint and format Markdown files with mdlint
|
|
3
|
+
branding:
|
|
4
|
+
icon: check-circle
|
|
5
|
+
color: blue
|
|
6
|
+
inputs:
|
|
7
|
+
args:
|
|
8
|
+
description: Arguments passed to mdlint
|
|
9
|
+
required: false
|
|
10
|
+
default: lint --format github
|
|
11
|
+
ruby-version:
|
|
12
|
+
description: Ruby version used by the action
|
|
13
|
+
required: false
|
|
14
|
+
default: '3.2'
|
|
15
|
+
reviewdog:
|
|
16
|
+
description: Run diagnostics through reviewdog
|
|
17
|
+
required: false
|
|
18
|
+
default: 'false'
|
|
19
|
+
reviewdog-reporter:
|
|
20
|
+
description: reviewdog reporter name
|
|
21
|
+
required: false
|
|
22
|
+
default: github-pr-check
|
|
23
|
+
reviewdog-filter-mode:
|
|
24
|
+
description: reviewdog filter mode
|
|
25
|
+
required: false
|
|
26
|
+
default: added
|
|
27
|
+
runs:
|
|
28
|
+
using: composite
|
|
29
|
+
steps:
|
|
30
|
+
- name: Set up Ruby
|
|
31
|
+
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
|
|
32
|
+
with:
|
|
33
|
+
ruby-version: ${{ inputs.ruby-version }}
|
|
34
|
+
- name: Install mdlint
|
|
35
|
+
shell: bash
|
|
36
|
+
run: gem install mdlint --no-document # zizmor: ignore[adhoc-packages]
|
|
37
|
+
- name: Set up reviewdog
|
|
38
|
+
if: inputs.reviewdog == 'true'
|
|
39
|
+
uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1
|
|
40
|
+
with:
|
|
41
|
+
reviewdog_version: latest
|
|
42
|
+
- name: Run mdlint
|
|
43
|
+
shell: bash
|
|
44
|
+
env:
|
|
45
|
+
MDLINT_ARGS: ${{ inputs.args }}
|
|
46
|
+
REVIEWDOG_ENABLED: ${{ inputs.reviewdog }}
|
|
47
|
+
REVIEWDOG_REPORTER: ${{ inputs.reviewdog-reporter }}
|
|
48
|
+
REVIEWDOG_FILTER_MODE: ${{ inputs.reviewdog-filter-mode }}
|
|
49
|
+
run: |
|
|
50
|
+
if [ "$REVIEWDOG_ENABLED" = "true" ]; then
|
|
51
|
+
mdlint $MDLINT_ARGS --format reviewdog | reviewdog -f=rdjson "-reporter=$REVIEWDOG_REPORTER" "-filter-mode=$REVIEWDOG_FILTER_MODE"
|
|
52
|
+
else
|
|
53
|
+
mdlint $MDLINT_ARGS
|
|
54
|
+
fi
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../lib/mdlint"
|
|
4
|
+
|
|
5
|
+
path = ARGV.first
|
|
6
|
+
source = path && File.file?(path) ? File.read(path) : <<~MARKDOWN
|
|
7
|
+
# Benchmark document
|
|
8
|
+
|
|
9
|
+
A paragraph with **strong text**, `inline code`, and a [link](https://example.com).
|
|
10
|
+
|
|
11
|
+
- one
|
|
12
|
+
- two
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
puts "hello"
|
|
16
|
+
```
|
|
17
|
+
MARKDOWN
|
|
18
|
+
iterations = Integer(ENV.fetch("ITERATIONS", "100"))
|
|
19
|
+
|
|
20
|
+
adapters = { "mdlint" => -> { Mdlint.html(source) } }
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
require "kramdown"
|
|
24
|
+
adapters["kramdown"] = -> { Kramdown::Document.new(source).to_html }
|
|
25
|
+
rescue LoadError
|
|
26
|
+
warn "kramdown: unavailable (install it to include this adapter)"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
require "commonmarker"
|
|
31
|
+
adapters["commonmarker"] = lambda do
|
|
32
|
+
if Commonmarker.respond_to?(:to_html)
|
|
33
|
+
Commonmarker.to_html(source)
|
|
34
|
+
elsif Commonmarker.respond_to?(:render_doc)
|
|
35
|
+
Commonmarker.render_doc(source).to_html
|
|
36
|
+
else
|
|
37
|
+
raise "unsupported commonmarker API"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
rescue LoadError
|
|
41
|
+
warn "commonmarker: unavailable (install it to include this adapter)"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
adapters.each do |name, formatter|
|
|
45
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
46
|
+
iterations.times { formatter.call }
|
|
47
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
48
|
+
puts format("%-14s %.6fs (%.2f ops/s)", name, elapsed, iterations / elapsed)
|
|
49
|
+
end
|
data/benchmark/format.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../lib/mdlint"
|
|
4
|
+
|
|
5
|
+
path = ARGV.first
|
|
6
|
+
source = path && File.file?(path) ? File.read(path) : <<~MARKDOWN
|
|
7
|
+
# Benchmark document
|
|
8
|
+
|
|
9
|
+
This is a representative paragraph with **strong text**, `inline code`, and a [link](https://example.com).
|
|
10
|
+
|
|
11
|
+
- one
|
|
12
|
+
- two
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
puts "hello"
|
|
16
|
+
```
|
|
17
|
+
MARKDOWN
|
|
18
|
+
|
|
19
|
+
iterations = Integer(ENV.fetch("ITERATIONS", "100"))
|
|
20
|
+
measure = lambda do |label, &block|
|
|
21
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
22
|
+
iterations.times(&block)
|
|
23
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
24
|
+
puts format("%-16s %.6fs (%.2f ops/s)", label, elapsed, iterations / elapsed)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
measure.call("format x#{iterations}") { Mdlint.format(source) }
|
|
28
|
+
measure.call("html x#{iterations}") { Mdlint.html(source) }
|
|
29
|
+
measure.call("lint x#{iterations}") { Mdlint.lint(source) }
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Mdlint
|
|
8
|
+
class CacheStore
|
|
9
|
+
def initialize(path)
|
|
10
|
+
@path = path
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
@data = load_data
|
|
13
|
+
@dirty = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def key(source, options)
|
|
17
|
+
Digest::SHA256.hexdigest(JSON.generate([source, canonicalize(options)]))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fetch(key)
|
|
21
|
+
@mutex.synchronize do
|
|
22
|
+
values = @data[key]
|
|
23
|
+
values&.map do |value|
|
|
24
|
+
attributes = symbolize(value)
|
|
25
|
+
Linter::Violation.new(
|
|
26
|
+
rule_id: attributes[:rule_id].to_s,
|
|
27
|
+
message: attributes[:message].to_s,
|
|
28
|
+
line: attributes[:line].to_i,
|
|
29
|
+
column: attributes[:column],
|
|
30
|
+
severity: (attributes[:severity] || :warning).to_sym,
|
|
31
|
+
fixable: attributes[:fixable] == true
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def store(key, violations)
|
|
38
|
+
@mutex.synchronize do
|
|
39
|
+
@data[key] = violations.map(&:to_h)
|
|
40
|
+
@dirty = true
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def save
|
|
45
|
+
@mutex.synchronize do
|
|
46
|
+
return unless @dirty
|
|
47
|
+
|
|
48
|
+
parent = File.dirname(@path)
|
|
49
|
+
FileUtils.mkdir_p(parent) unless parent == "."
|
|
50
|
+
File.write(@path, JSON.pretty_generate(@data) + "\n")
|
|
51
|
+
@dirty = false
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def load_data
|
|
58
|
+
return {} unless File.file?(@path)
|
|
59
|
+
|
|
60
|
+
JSON.parse(File.read(@path))
|
|
61
|
+
rescue JSON::ParserError, SystemCallError
|
|
62
|
+
{}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def canonicalize(value)
|
|
66
|
+
case value
|
|
67
|
+
when Hash
|
|
68
|
+
value.keys.map(&:to_s).sort.to_h do |key|
|
|
69
|
+
original_key = value.keys.find { |candidate| candidate.to_s == key }
|
|
70
|
+
[key, canonicalize(value[original_key])]
|
|
71
|
+
end
|
|
72
|
+
when Array
|
|
73
|
+
value.map { |item| canonicalize(item) }
|
|
74
|
+
when Symbol
|
|
75
|
+
value.to_s
|
|
76
|
+
else
|
|
77
|
+
value
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def symbolize(value)
|
|
82
|
+
value.each_with_object({}) { |(key, item), result| result[key.to_sym] = item }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Mdlint
|
|
6
|
+
class CLI
|
|
7
|
+
class OutputFormatter
|
|
8
|
+
LEVELS = { error: "error", warning: "warning", info: "note" }.freeze
|
|
9
|
+
|
|
10
|
+
def initialize(format)
|
|
11
|
+
@format = format.to_s.downcase
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render(entries)
|
|
15
|
+
return "" if entries.empty? && @format == "text"
|
|
16
|
+
|
|
17
|
+
case @format
|
|
18
|
+
when "json"
|
|
19
|
+
json(entries)
|
|
20
|
+
when "sarif"
|
|
21
|
+
sarif(entries)
|
|
22
|
+
when "github"
|
|
23
|
+
github(entries)
|
|
24
|
+
when "checkstyle"
|
|
25
|
+
checkstyle(entries)
|
|
26
|
+
when "junit"
|
|
27
|
+
junit(entries)
|
|
28
|
+
when "reviewdog", "rdjson"
|
|
29
|
+
reviewdog(entries)
|
|
30
|
+
else
|
|
31
|
+
text(entries)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def text(entries)
|
|
38
|
+
entries.map { |entry| "#{entry[:filename]}:#{entry[:violation]}" }.join("\n") + "\n"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def json(entries)
|
|
42
|
+
JSON.generate(entries.map do |entry|
|
|
43
|
+
entry[:violation].to_h.merge(file: entry[:filename])
|
|
44
|
+
end) + "\n"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def github(entries)
|
|
48
|
+
entries.map do |entry|
|
|
49
|
+
violation = entry[:violation]
|
|
50
|
+
command = violation.error? ? "error" : violation.info? ? "notice" : "warning"
|
|
51
|
+
location = "file=#{entry[:filename]},line=#{violation.line}"
|
|
52
|
+
location += ",col=#{violation.column}" if violation.column
|
|
53
|
+
"::#{command} #{location}::[#{violation.rule_id}] #{violation.message}"
|
|
54
|
+
end.join("\n") + "\n"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def sarif(entries)
|
|
58
|
+
rules = entries.map { |entry| entry[:violation].rule_id }.uniq.filter_map do |rule_id|
|
|
59
|
+
rule = Mdlint::Linter::RuleRegistry.find(rule_id)
|
|
60
|
+
next unless rule
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
id: rule.rule_id,
|
|
64
|
+
shortDescription: { text: rule.description.to_s }
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
rule_indexes = rules.each_with_index.to_h { |rule, index| [rule[:id], index] }
|
|
68
|
+
|
|
69
|
+
document = {
|
|
70
|
+
version: "2.1.0",
|
|
71
|
+
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
|
72
|
+
runs: [{
|
|
73
|
+
tool: { driver: { name: "mdlint", rules: rules } },
|
|
74
|
+
results: entries.map do |entry|
|
|
75
|
+
violation = entry[:violation]
|
|
76
|
+
location = {
|
|
77
|
+
physicalLocation: {
|
|
78
|
+
artifactLocation: { uri: entry[:filename] },
|
|
79
|
+
region: { startLine: violation.line }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
location[:physicalLocation][:region][:startColumn] = violation.column if violation.column
|
|
83
|
+
{
|
|
84
|
+
ruleId: violation.rule_id,
|
|
85
|
+
ruleIndex: rule_indexes[violation.rule_id],
|
|
86
|
+
level: LEVELS.fetch(violation.severity, "warning"),
|
|
87
|
+
message: { text: violation.message },
|
|
88
|
+
locations: [location]
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
}]
|
|
92
|
+
}
|
|
93
|
+
JSON.generate(document) + "\n"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def checkstyle(entries)
|
|
97
|
+
files = entries.group_by { |entry| entry[:filename] }.map do |filename, file_entries|
|
|
98
|
+
errors = file_entries.map do |entry|
|
|
99
|
+
violation = entry[:violation]
|
|
100
|
+
attributes = {
|
|
101
|
+
line: violation.line,
|
|
102
|
+
column: violation.column,
|
|
103
|
+
severity: violation.severity,
|
|
104
|
+
message: "[#{violation.rule_id}] #{violation.message}",
|
|
105
|
+
source: violation.rule_id
|
|
106
|
+
}.compact.map { |key, value| "#{key}=\"#{xml_escape(value)}\"" }.join(" ")
|
|
107
|
+
" <error #{attributes}/>"
|
|
108
|
+
end
|
|
109
|
+
" <file name=\"#{xml_escape(filename)}\">\n#{errors.join("\n")}\n </file>"
|
|
110
|
+
end
|
|
111
|
+
"<checkstyle version=\"1.0\">\n#{files.join("\n")}\n</checkstyle>\n"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def junit(entries)
|
|
115
|
+
cases = entries.map do |entry|
|
|
116
|
+
violation = entry[:violation]
|
|
117
|
+
name = "#{entry[:filename]}:#{violation.line}:#{violation.rule_id}"
|
|
118
|
+
" <testcase name=\"#{xml_escape(name)}\"><failure message=\"#{xml_escape(violation.message)}\"/></testcase>"
|
|
119
|
+
end
|
|
120
|
+
"<testsuite name=\"mdlint\" tests=\"#{entries.length}\" failures=\"#{entries.length}\">\n#{cases.join("\n")}\n</testsuite>\n"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def reviewdog(entries)
|
|
124
|
+
document = {
|
|
125
|
+
source: { name: "mdlint" },
|
|
126
|
+
diagnostics: entries.map do |entry|
|
|
127
|
+
violation = entry[:violation]
|
|
128
|
+
column = violation.column || 1
|
|
129
|
+
{
|
|
130
|
+
message: "[#{violation.rule_id}] #{violation.message}",
|
|
131
|
+
location: {
|
|
132
|
+
path: entry[:filename],
|
|
133
|
+
range: {
|
|
134
|
+
start: { line: violation.line, column: column },
|
|
135
|
+
end: { line: violation.line, column: column + 1 }
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
severity: violation.severity.to_s.upcase
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
}
|
|
142
|
+
JSON.generate(document) + "\n"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def xml_escape(value)
|
|
146
|
+
value.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """).gsub("'", "'")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|