Skip to content

Markdown lint#

View Markdown

oxct lint runs all 53 active rules from markdownlint 0.41.1 in Rust, using Ox Content's CommonMark/GFM AST. Rule IDs (MD001–MD060, excluding retired IDs), aliases, tags, per-rule parameters, and error/warning severities are supported. oxct lint --list-rules prints the exact supported catalog.

oxct lint . --threads 8
oxct lint --config .markdownlint.yaml --format json
oxct lint --fix
oxct lint --stdin --stdin-filepath page.md

Standalone executables for Linux x64, macOS Intel/Apple Silicon, and Windows x64 are attached to GitHub releases with SHA-256 checksums. They run without Node.js. You can also build cargo build --release -p ox_content_cli --bin oxct and use target/release/oxct. The npm vpx @ox-content/cli lint command uses the same Rust engine.

Markdownlint configuration#

The CLI discovers .oxlint.json, oxlint.json, .markdownlint.json, .markdownlint.jsonc, .markdownlint.yaml, then .markdownlint.yml in the current directory. --config overrides discovery. Markdownlint files can use relative extends paths; inheritance cycles fail before files are changed. JSON comments and trailing commas are accepted. .markdownlintignore uses gitignore patterns, including negation. Dependency and build directories are excluded from discovery.

{
  "default": true,
  "MD013": { "line_length": 100 },
  "MD024": { "siblings_only": true },
  "no-inline-html": false,
  "MD009": "warning"
}

An existing Ox Content config keeps its previous rules. Add "markdownlint": {} or pass --markdownlint to enable the new profile. --no-markdownlint selects the previous profile. Explicit paths override include. Unknown rule IDs, tags, and options are rejected. Native regular-expression parameters use Rust regex syntax; JavaScript lookarounds and backreferences are rejected. JavaScript configurations and custom markdownlint plugins are not loaded.

The standard profile leaves spelling, repeated-word checks, and textRules off. Enable them explicitly. Code, frontmatter, HTML blocks, link destinations, autolinks, and MDX expressions are excluded from prose checks.

<!-- markdownlint-disable-next-line MD013 -->
A deliberately long line.

<!-- markdownlint-disable MD033 -->
<span>Allowed here</span>
<!-- markdownlint-enable MD033 -->

Standard disable, enable, disable-file, enable-file, disable-line, disable-next-line, capture, restore, and configure-file comments support IDs, aliases, and tags. --no-inline-config ignores them. Markdownlint comments follow upstream behavior, including comments inside fenced code; Ox Content's oxlint-* prose comments continue to ignore code blocks.

Previous structure rules#

These options apply to the previous profile, used by existing Ox Content configs and content APIs. --strict enables its additional structure checks.

Option Rule ID Default
duplicateHeadings duplicate-heading on
headingIncrement heading-increment on
emptyHeadings empty-heading on
codeFenceClosed code-fence-closed on
emptyLinks empty-link on
firstHeadingH1 first-heading-h1 off; enabled by --strict
singleH1 single-h1 off; enabled by --strict
codeFenceLanguage code-fence-language off; enabled by --strict
imageAlt image-alt off; enabled by --strict
finalNewline final-newline off; enabled by --strict
maxConsecutiveBlankLines max-consecutive-blank-lines 1
trailingSpaces trailing-spaces on; preserves hard breaks
repeatedWords repeated-word on
repeatedPunctuation repeated-punctuation on
spellcheck spellcheck CLI/editor: off; content API: on

Opt-in prose rules#

Every textRules field is disabled when omitted. These native rules follow ideas from textlint's sentence-length and no-exclamation-question-mark rules. They have their own configuration and do not load textlint plugins or .textlintrc. Existing editor textlint integration remains available for plugins.

Create .oxlint.json:

{
  "include": ["content/**/*.{md,mdx}"],
  "ignore": ["content/generated/**"],
  "markdownlint": { "MD013": false },
  "textRules": {
    "sentenceLength": 100,
    "maxTen": 3,
    "noExclamationQuestionMark": true,
    "noTodo": true,
    "terminology": [
      { "term": "Javascript", "replacement": "JavaScript" },
      { "term": "表記ゆれ", "replacement": "表記揺れ" }
    ]
  },
  "severities": {
    "sentence-length": "error",
    "no-todo": "error",
    "terminology": "warning"
  }
}

sentenceLength counts Unicode code points in each sentence, including spaces and soft line breaks, while excluding syntax and code. It splits on ., 。, !, ?, !, and ? outside Japanese quotation marks and parentheses; decimal dots do not split a sentence. This is a lightweight splitter, without morphological analysis or textlint's full sentence-splitter behavior. maxTen counts 、 per sentence. noTodo checks uppercase TODO at word boundaries. Terminology matches literal, case-sensitive phrases; ASCII terms respect word boundaries. Rules also inspect headings, tables, and quotations.

Safe fixes and suppression#

--fix removes unnecessary trailing whitespace and excess blank lines, removes adjacent repeated words, inserts a configured final newline, and applies literal terminology replacements that cannot introduce Markdown syntax. The standard profile also fixes unambiguous ATX spacing and configured proper-name capitalization. It reports other structure changes for review. Two spaces that produce a hard break and existing CRLF line endings survive. Spelling suggestions and structural rewrites require review and are not applied.

Files are replaced atomically with their permissions preserved. Symlinks and files changed since reading are refused. --fix cannot be combined with stdin. The report describes the resulting document and includes fixedCount edits. Remaining errors or warnings above --max-warnings return exit code 1.

Use HTML comments to suppress all rules or selected rule IDs:

<!-- oxlint-disable-next-line sentence-length -->
This deliberately long sentence is exempt.

<!-- oxlint-disable terminology, no-todo -->

TODO: Preserve this quoted terminology.
<!-- oxlint-enable terminology, no-todo -->

Empty rule lists apply to all rules. Directives inside code are ignored. Suppressed diagnostics do not contribute fixes. severities accepts off, info, warning, or error; errors always fail the CLI, while info does not.

JavaScript and editors#

import { lintMarkdown, fixMarkdown } from "@ox-content/vite-plugin";

const options = {
  markdownlint: {},
  textRules: { terminology: [{ term: "Javascript", replacement: "JavaScript" }] },
};
const diagnostics = lintMarkdown("Javascript", options);
const fixed = fixMarkdown("Javascript", options);
console.log(fixed.output); // "JavaScript\n"

Diagnostic lines and UTF-16 columns are one-based. Fix ranges use UTF-8 byte offsets; use fixMarkdown to apply them to JavaScript strings safely. lintMarkdownAsync and file APIs retain opt-in CSpell standard dictionaries; fixMarkdown uses native rules and rejects external dictionaries.

For the LSP, add markdownLint options to .ox-content.json (without CLI include/ignore fields). Its presence enables native diagnostics and quick fixes. initializationOptions.markdownLint accepts the same native options; markdownLintEnabled can explicitly enable or disable them. Native LSP spelling is off by default. VS Code exposes oxContent.markdownLint.enabled; leave it unset to follow workspace configuration. Other LSP editors use the same options. Rust options express severities as {ruleId, severity} entries in an array; all JSON configurations also accept the rule-ID object shown above.

Performance verification#

CLI workers read and lint files in parallel using Rayon, with batches of at most 128 files. --threads sets the worker count (1–256); the default uses available CPUs. Workers share prepared rule settings, regular expressions, dictionaries, and terminology matchers. Pure Markdownlint runs do not create prose masks or word tokens. Small normalized labels use inline storage. Lists use an ancestor stack; line classification uses ordered indexes, avoiding repeated full-document scans. No worker writes stdout: the coordinator sorts diagnostics and serializes borrowed data through a 64 KiB buffer. Short word tokens use inline string storage. Line masks and token vectors reuse per-document storage, and fully visible lines borrow source text directly. UTF-16 position indexes are built only when a diagnostic needs columns. Single-file and one-thread runs skip worker-pool startup; JSON diagnostics serialize directly without an intermediate reference array.

The Native CLI workflow measures base/head medians on identical input, allocation counts and allocated bytes, plus serial/eight-thread CLI wall time including file discovery, reads, process startup, and clean/8192-diagnostic JSON output. Base/head result hashes and serial/parallel reports must match. Raw measurements are published as the markdown-lint-performance artifact. Timing and allocation instrumentation run separately; results include seven warm repetitions.

The workflow also verifies 271 rule/configuration cases against a pinned, test-only markdownlint 0.41.1 installation, covering all 53 active rules. These checks compare rule IDs and diagnostic lines; message text and available safe fixes are specific to Ox Content. LF and CRLF inputs, Unicode, code exclusion, references, tables, and inline controls are included. Native profile throughput and allocation measurements run with all 53 rules enabled, on one and eight threads. Native CLI measurements include process startup, discovery, file reads, and clean/2048-diagnostic JSON output.

Rule metadata and parameter definitions are derived from the markdownlint project under its MIT license; the implementations and runner are Rust.

Last updated: