A content linter for Markdown, built on mq's mq-markdown
AST, the same document model mq's own query engine and mq-lint use. mq-content-lint checks
the content of a document: heading structure, list and table consistency, whitespace,
link/image hygiene, required front matter, with coverage roughly equivalent to
markdownlint's rule set. It's a separate tool from
mq-lint, which lints .mq query
scripts, not Markdown content.
Important
This project is under active development.
- Custom rules without writing Rust: house style rules, required disclaimers, "no TODO left in shipped docs", expressed as mq queries in config instead of forking a linter. Neither markdownlint nor rumdl offer this.
- CI quality gates:
sarif/rdjsonoutput plugs straight into GitHub code scanning or reviewdog PR annotations; a non-zero exit code on any diagnostic at or above--min-severitymakes it a drop-in CI check. - Live editor feedback: the bundled LSP server gives hover text, diagnostics, and quick fixes as you type, instead of only catching issues at commit or CI time.
- Migrating from markdownlint: rule-for-rule coverage of markdownlint's checks, plus the ability to express project-specific rules that markdownlint's plugin API can't.
- Cleaning up LLM-generated Markdown: docs and READMEs increasingly start as LLM output;
--fixnormalizes heading structure, whitespace, and link/list formatting in one pass.
- 55 built-in rules covering headings, lists, whitespace, code blocks, links/images, inline
formatting, tables, and front matter. See
--list-rulesand--explain <rule-id>. - Custom rules as mq queries: define project-specific checks in config without writing Rust, the one thing neither markdownlint nor rumdl offer.
- Autofix via
--fix, a dry-run diff via--diff, and--watchto re-lint on save. --cacheskips re-linting files whose content and config haven't changed since the last run, for faster repeat runs on large repos and in CI.- Cascading TOML config (like ESLint's),
.editorconfigintegration,.gitignore-aware directory scanning, and inline<!-- mq-content-lint-disable ... -->comments. - Machine-readable output:
json,sarif(GitHub code scanning), andrdjson(for reviewdog inline PR suggestions);markdownfor a table you can drop straight into a PR description or comment. - A composite GitHub Action, a pre-commit hook, and an LSP server
(
mq-content-lint-lsp) for live editor diagnostics and quick fixes.
cargo install mq-content-lint
# Also want the language server? See "Editors" below.
cargo install mq-content-lint --features lspBuilding from source
git clone https://github.com/harehare/mq-content-lint.git
cd mq-content-lint
cargo install --path . --features lsp| Integration | Notes |
|---|---|
| VS Code | Marketplace / Extension source |
| LSP (any editor) | Point a generic LSP client at mq-content-lint-lsp for Markdown files |
| GitHub Actions | Composite action (see below) |
| pre-commit | mq-content-lint / mq-content-lint-fix hooks (see below) |
# Write a starter mq-content-lint.toml (everything commented out, see Configuration)
mq-content-lint --init
# Lint a file, a directory (recursively), or stdin
mq-content-lint README.md
mq-content-lint docs/
cat README.md | mq-content-lint
# Rewrite files in place, or preview the changes as a diff without writing anything
mq-content-lint --fix docs/
mq-content-lint --diff docs/
# Re-lint whenever a watched file changes
mq-content-lint --watch docs/
# Skip re-linting files unchanged since the last run (default cache file:
# .mq-content-lint-cache.json)
mq-content-lint --cache docs/
# Machine-readable output
mq-content-lint --format json docs/ > report.json
mq-content-lint --format sarif docs/ > report.sarif
mq-content-lint --format markdown docs/ > report.md
# Inspect the rule set
mq-content-lint --list-rules
mq-content-lint --explain line_lengthExit code is non-zero if any diagnostic at or above --min-severity (default info) was
reported. When given more than one file, files are read, fixed, and linted in parallel across CPU
cores; output stays sorted by path, so multi-file runs are deterministic to diff or snapshot in
CI.
--fix applies every diagnostic with a machine-applicable rewrite, then re-lints and re-fixes
automatically if that exposed a new issue, repeating up to 10 times until a pass makes no further
change. Not every rule can auto-fix; see --list-rules's "Fix?" column or --explain <rule-id>'s fixable: line.
- uses: harehare/[email protected]
with:
path: docs/Diagnostics are also written to the workflow run's Job Summary as a Markdown table, regardless of
format, so results are visible without opening logs (skipped when format: markdown, to avoid
duplicating it).
Pass fix: 'true' to auto-fix instead of just reporting, or set format: sarif and wire the
sarif-file output into github/codeql-action/upload-sarif. See action.yml for the full list
of inputs/outputs.
repos:
- repo: https://github.com/harehare/mq-content-lint
rev: v0.1.3 # a tag; see this repo's releases for the latest
hooks:
- id: mq-content-lint # report only
# - id: mq-content-lint-fix # or auto-fix on commit insteadmq-content-lint-lsp (installed via cargo install mq-content-lint --features lsp) is a
Language Server Protocol server: live
diagnostics, hover text, and quick-fix code actions in any LSP-capable editor. A VS Code
extension built on it is available on the Visual Studio
Marketplace. Other
editors can point their generic LSP client at the mq-content-lint-lsp binary for Markdown files.
Drop a mq-content-lint.toml file in (or above) the directory you run mq-content-lint from.
It's discovered automatically by walking up from the current directory, the same way
.eslintrc/pyproject.toml are. Config files cascade: every mq-content-lint.toml found up
to the filesystem root is loaded and layered, closer files winning over farther ones.
[rules]
heading_hierarchy_skip = "warning"
image_missing_alt = "error"
no_inline_html = false
[rules.line_length]
severity = "warning"
limit = 100
[front_matter]
required_keys = ["title"]See mq-content-lint.toml in this repo for a fully-commented example,
and mq-content-lint --print-json-schema for editor autocomplete/validation.
node_modules/target, dotfiles/dotdirs, and anything excluded by .gitignore are always
skipped when linting a directory. A .mq-content-lintignore file (gitignore syntax) or an
ignore = [...] array in mq-content-lint.toml can exclude more. A file named directly on the
command line is always linted regardless.
<!-- mq-content-lint-disable line_length -->
This line can be as long as it wants to be now.
<!-- mq-content-lint-enable line_length -->
<!-- mq-content-lint-disable-next-line no_bare_urls -->
See https://example.com for details.disable/enable take effect from that line onward (or re-enable); disable-line/
disable-next-line apply to just one line. Omit rule ids to affect every rule.
A config file can define its own rules as mq queries: every node a query selects becomes a diagnostic at that node's position, merged into the same report as the built-ins:
[[custom_rules]]
id = "no_todo"
query = 'select(contains(to_text(), "TODO"))'
message = "found a TODO marker"
severity = "warning" # optional, defaults to "warning"
fix = 'replace("TODO", "DONE")' # optional, makes the rule autofixablequery runs once per top-level node by default. For a document-wide check (counting duplicate
headings, say), start the query with nodes to gather every top-level node into one array first:
[[custom_rules]]
id = "no_multiple_h1"
query = '''
nodes
| let h1_count = len(compact(.h1))
| if (gt(h1_count, 1)):
.h1
end
'''
message = "more than one top-level heading in this document"
severity = "error"- Natural-language spelling/style checking (a Vale-style prose linter) is out of scope.
- 🐛 Report bugs
- 💡 Request features
- ⭐ Star the project if you find it useful!
Contributions are welcome! See CONTRIBUTING.md for the difference between a built-in rule and a custom rule, and the steps for adding a new built-in rule.
This project is licensed under the MIT License. See the LICENSE file for details.