Integrations
Continuous integration
Two commands check a workspace without writing anything. Each fails with exit code 1 when it finds something, and with 2 when it cannot do its job:
npx mdrefine lint --max-warnings 0
npx mdrefine format --checkThe examples include a complete GitHub Actions workflow that runs both on every pull request; copy it to .github/workflows/markdown.yml. Forgejo Actions, on Codeberg and other Forgejo instances, runs the same workflow from .forgejo/workflows/: set runs-on to a label that the instance's runners offer, and check that the actions it uses are available there.
Git hooks
The CLI reads files from the working tree, not the staged content of the index. Pass it the files that a commit changes, and never run it with an empty list, which would check the whole workspace.
With lint-staged, which passes the staged files that match a pattern, keeps unstaged changes out of the way while it runs, and adds what the commands changed to the commit, configure it in package.json:
{
"lint-staged": {
"*.{md,markdown}": [
"mdrefine format --write --no-warn-ignored",
"mdrefine lint --max-warnings 0 --no-warn-ignored"
]
}
}and run npx lint-staged from a pre-commit hook, for example through husky. Without it, a .git/hooks/pre-commit script can lint the Markdown files that the commit adds or changes:
#!/bin/sh
git diff --cached --name-only -z --diff-filter=ACMR -- '*.md' '*.markdown' |
xargs -0 -r npx mdrefine lint --max-warnings 0 --no-warn-ignored --In both, --no-warn-ignored leaves out the warning for a changed file that the tool skips, such as one that the configuration's ignore patterns match. --diff-filter=ACMR leaves out deleted files, which would be an error as paths. xargs -r skips the command when no file changed. -- keeps a file name that starts with - from being read as an option. This hook checks each file as it is on disk, including changes that are not staged.
With the pre-commit framework, a hook in the repository's .pre-commit-config.yaml installs mdrefine in an environment of its own, with the computer's Node.js, and passes it the staged Markdown files; it needs no package.json:
repos:
- repo: local
hooks:
- id: mdrefine
name: mdrefine
language: node
additional_dependencies: ["mdrefine@0.7.1"]
entry: mdrefine lint --max-warnings 0 --no-warn-ignored
types: [markdown]With lefthook, installed in the project next to mdrefine, add to lefthook.yml:
pre-commit:
commands:
mdrefine:
glob: "*.{md,markdown}"
run: npx --no-install mdrefine lint --max-warnings 0 --no-warn-ignored -- {staged_files}Both skip the command when the commit changes no Markdown file, and lefthook's glob also matches files in subdirectories.
Editors
There is no editor extension or language server. An editor that can format a buffer through an external command can run npx mdrefine format - --stdin-filepath path/of/the/file.md, which reads the buffer from stdin and prints the formatted text; the path gives the buffer its file's configuration, dialect, and link targets. Exit code 1 means that findings remain, and the output is still the formatted text; with 2, keep the buffer as it was. npx mdrefine lint - --stdin-filepath path/of/the/file.md --json returns the findings with their positions. An editor that formats a selection can use the library's ranges. An editor that reads JSON Schemas completes and checks the configuration file with the package's schema.
Next to Prettier or markdownlint
Prettier formats Markdown too, and two formatters undo each other's changes. Leave Markdown to mdrefine in this order:
Run
npx mdrefine config convert --writeto keep the style that Prettier gave your documents; conversion and fallback describes what it carries over. Skip this step to use mdrefine'srecommendedpreset instead.Add both file extensions to
.prettierignore:text**/*.md **/*.markdown
The order matters. Without an mdrefine configuration, mdrefine falls back to the Prettier configuration, but not once .prettierignore excludes all Markdown: Prettier then formats no Markdown in the workspace, so its options do not describe your documents, and config convert and the fallback leave the Prettier configuration out and say so. A .prettierignore counts as excluding all Markdown only when that can be established, reading each entry as Prettier does, as a gitignore pattern without regard to letter case:
- It counts when one entry by itself excludes every
.mdfile, whatever its name and directory, such as*.md,**/*.md,*.MD, or*. An entry for.markdownis not needed: an entry for.mdfiles alone is the usual way to leave Markdown to another tool. - It does not count with only narrower entries, such as
docs/*.md, or/*.md, which leaves the Markdown in subdirectories to Prettier. - It never counts with a negated (
!) entry or a character class such as*.[mM][dD], because then it cannot be proved to leave Prettier no Markdown:**/*.mdwith!README.mddoes not count, nor even**/*.mdwith!docs/, which re-includes no document.
markdownlint and mdrefine can run side by side, but rules that overlap can disagree, such as markdownlint's emphasis style (MD049) and style/emphasis. Turn off in one tool what the other covers; the tables below list the overlaps.
Coming from Prettier or markdownlint
mdrefine is meant to replace these tools for Markdown. The tables below list every rule of markdownlint 0.42 and every Markdown option and formatting choice of Prettier 3.9, with the mdrefine rule that covers each and where it behaves differently. Each row follows the check's documented purpose, not the other tool's known defects.
Configuration conversion and fallback
Run mdrefine config convert to print an mdrefine.config.jsonc, or add --write to create it where lint looks for the workspace's configuration; an existing mdrefine configuration there is an error. The report on stderr lists sources, files not used, omissions with reasons and applicable tracking issues, resolved conflicts, and suggested extra checks. Review the result before replacing your existing tools. Convert a configuration describes its options.
Without an mdrefine configuration, the same conversion runs in memory. Use --no-competitor-config to disable this fallback. config explain <file> shows its sources and converted settings. The search stops at the same repository or vault boundary as the ordinary configuration search. When none of the files found can be used, the recommended preset applies, as without them; the notices on stderr name each such file.
The result uses no preset. markdownlint's enabled checks map to their counterparts, including its default rule set unless default: false is set. Prettier enables its fixed styles. proseWrap: "always" enables wrapping at printWidth (80 by default), "never" joins paragraph lines, and "preserve" leaves wrapping off. Only stated markdownlint option values win fixed Prettier styles; Prettier options win the settings they control. Missing behavior is reported. Enabled markdownlint checks use error, matching its failing exit status. Rules enabled only by Prettier use warn; format --check fails regardless of severity. A markdownlint disable keeps an overlapping Prettier style enabled. MD013 does not enable reflow: without Prettier wrapping, paragraph lines are reported as unchecked, with a suggestion to enable style/wrap at that width. The mapping follows intended behavior and retains mdrefine's safety guards.
Fallback loading reads JSON, JSONC, and YAML, including markdownlint extends, markdownlint-cli2 config (without one, a .markdownlint.* file or else markdownlint's defaults, as markdownlint-cli2 uses), Prettier's package.json or package.yaml key, Markdown overrides, and relevant .editorconfig values only when a Prettier configuration exists. JavaScript and TypeScript configurations are named without executing them and contribute nothing; the explicit convert command loads them through Node.js. TOML and JSON5 are not read, by either. .prettierrc.jsonc is not a Prettier configuration filename and is not searched. An extensionless .prettierrc is read as YAML, as Prettier reads it, so it can hold JSON; a JSON comment in it becomes part of the next key in Prettier too, which ignores that setting, and conversion reports it. A file that cannot be parsed is an error that names it. .prettierignore, active .markdownlintignore files, and markdownlint-cli2 ignores and globs become ignore. The legacy .markdownlintignore belongs to markdownlint-cli (CLI1). It participates with standalone markdownlint settings when no CLI2 configuration is present. CLI2 does not read that file, so it is named as unused alongside a CLI2 configuration. An ignore file alone does not activate the fallback. When CLI1 and Prettier ignore files both participate, their exclusions combine.
Negated entries in either ignore file cannot be converted: config convert reports them for manual migration and converts the other patterns, so review its ignore before using it. The fallback, which has no such review, applies none of that file's patterns when it has a negated entry: it checks the documents that the file excludes rather than apply the exclusions without their exceptions, which can leave nothing to check. Other participating ignore files still apply. The notices and conversion.unused identify unused sources and explain why.
Patterns convert within the subset of glob syntax that each source documents. A pattern outside it, such as a character class or a negated .prettierignore entry, is reported with its source for manual migration rather than converted. An initial UTF-8 byte-order mark in an ignore file is discarded before reading its first pattern. Prettier and CLI1 match ignore patterns without regard to letter case, while converted patterns match case exactly; conversion says so once per ignore file. The maintainer notes list the exact subsets.
For overlapping EditorConfig sections, conversion retains the intersections it can represent exactly, including separate brace alternatives. Branches proved disjoint need no overlap notice. Any unsupported remainder is reported for manual migration while the other exact intersections remain in use.
During the fallback, markdownlint inline comments and Prettier ignore comments and requirePragma apply. With an mdrefine configuration, including one that config convert writes, they have no effect: migrate them to mdrefine's own suppression comments.
Markdownlint compatibility also keeps MD041's reading of headings in raw HTML and its treatment of HTML comment blocks before the initial heading. See initial-heading for empty documents and permitted preambles. MD022 keeps comment-aware blank-line counts around headings, including reports for protected boundaries around list-item headings and attached comments. Those reports require manual resolution to preserve list tightness and comment attachment; see block-spacing. These exemptions apply to markdownlint findings; an independent Prettier spacing preference still applies at warning severity. Native mdrefine configuration keeps the policies described on the rules' pages.
markdownlint's disable, enable, disable-line, disable-next-line, disable-file, and enable-file select enabled markdownlint checks by ID, alias, or tag, without regard to case. File directives apply throughout the file; other transitions start on their own line. capture saves one snapshot of enabled checks, replacing the previous snapshot; restore restores it, initially the file's configuration. configure-file merges JSON settings in source order for the whole file and uses the configuration converter. Unknown names or checks without counterparts suppress nothing and are reported once per run, with a tracking issue where available.
Where these comments turn a markdownlint check off in part of a file, a Prettier style that covers the same thing stays on there, at warning severity, when a Prettier configuration is in use; a rule that carries several checks reports each under the check that found it. An edit is made only where every part of the file that it touches allows it, so a table or list can stay as written when only part of it is suppressed. The maintainer notes describe how the rules are evaluated per region.
Prettier's ignore preserves the next parsed node; ignore-start and ignore-end preserve a top-level range, with blank lines before the comments. They prevent edits in that source even from a shared rule, while markdownlint findings still report without edits. requirePragma: true limits Prettier formatting to documents beginning with <!-- @format --> or <!-- @prettier -->, also immediately after front matter; markdownlint checks still run. insertPragma is not carried over.
markdownlint rules
markdownlint enables its default rule set; template, proper-name, notation, and column-style checks require configuration. mdrefine's presets enable fewer: a rule that no preset in your configuration enables, such as structure/heading-punctuation, needs an entry under rules. Each rule's page names its presets.
| markdownlint | mdrefine |
|---|---|
MD001 heading-increment | structure/heading-increment, with block quotes checked apart, and frontMatterTitle, off by default |
MD003 heading-style | style/heading, whose default converts underlined headings to # headings; closing: "add" writes closing # sequences |
MD004 ul-style | style/list-marker, with consistent and sublist |
MD005 list-indent | style/list-indent |
MD007 ul-indent | style/list-indent's nestedIndent, startIndented, and startIndent |
MD009 no-trailing-spaces | style/trailing-space: hardBreakSpaces, listItemEmptyLines, and report-only codeBlocks; strict cleanup outside hard breaks |
MD010 no-hard-tabs | style/no-tabs; literal content is report-only |
MD011 no-reversed-links | links/reversed |
MD012 no-multiple-blanks | style/blank-lines, with maximum |
MD013 line-length | style/wrap, which wraps paragraphs instead of only reporting. content/line-length reports headings, code blocks, and tables |
MD014 commands-show-output | content/shell-prompt, for shell and unlabeled code blocks, with $ and % prompts |
MD018 no-missing-space-atx | structure/unspaced-heading, which leaves Obsidian tags and issue references alone |
MD019 no-multiple-space-atx | style/heading |
MD020 no-missing-space-closed-atx | structure/unspaced-heading, for paragraph text such as #Heading# |
MD021 no-multiple-space-closed-atx | style/heading; removes closing # by default or normalizes retained separators |
MD022 blanks-around-headings | style/block-spacing, with linesAbove, linesBelow, and includeFrontMatter; counts that style/blank-lines would remove are refused |
MD023 heading-start-left | style/heading; preserves container prefixes |
MD024 no-duplicate-heading | structure/duplicate-heading; siblingsOnly defaults to true |
MD025 single-title | structure/single-title |
MD026 no-trailing-punctuation | structure/heading-punctuation, which reads headings as renderers show them and leaves abbreviations, ellipses, and emoji shortcodes alone |
MD027 no-multiple-space-blockquote | style/quote-marker, with listItems |
MD028 no-blanks-blockquote | structure/split-quote, except before an alert or callout |
MD029 ol-prefix | style/ordered-list; same repeats the existing start; conversion reports that one and zero start-number checks are not carried over, because the rendered first number is preserved |
MD030 list-marker-space | style/list-indent, with the same four spacings |
MD031 blanks-around-fences | style/block-spacing; outside list items and footnote definitions, as with list_items: false |
MD032 blanks-around-lists | style/block-spacing |
MD033 no-inline-html | content/html-element, with allowedElements and tableAllowedElements |
MD034 no-bare-urls | links/bare-url |
MD035 hr-style | style/thematic-break, with exact break text or consistent |
MD036 no-emphasis-as-heading | structure/emphasis-heading, whatever the paragraph's line length |
MD037 no-space-in-emphasis | content/emphasis-spacing, reported only where the parser reads emphasis once the spaces are gone; not a single * or _ with spaces on both sides at both ends, such as a * b * c |
MD038 no-space-in-code | content/inline-code-spacing, reported only, next to a letter or digit; not spaces that show syntax or indentation, as in - , or that a backtick at an end of the code needs |
MD039 no-space-in-links | links/text-spacing, reported only, because moving the spaces changes the link text and where they belong depends on the text around the link |
MD040 fenced-code-language | content/code-language |
MD041 first-line-heading | structure/initial-heading |
MD042 no-empty-links | links/empty |
MD043 required-headings | structure/heading-template, checking the document's own outline only |
MD044 proper-names | content/proper-names, reported only, skipping inline code and, by default, code blocks |
MD045 no-alt-text | content/image-alt, also HTML <img> without alt; alt="" marks a decorative image, and aria-hidden="true" counts outside GitHub |
MD046 code-block-style | style/code-block, with consistent, fenced, or indented; indented blocks are fenced where that is safe, outside VitePress, and other findings are reported only |
MD047 single-trailing-newline | style/final-newline |
MD048 code-fence-style | style/code-fence |
MD049 emphasis-style | style/emphasis. Includes marker: "consistent" |
MD050 strong-style | style/strong. Includes marker: "consistent" |
MD051 link-fragments | links/valid, which also checks that local files exist. Includes ignored_pattern as ignoredFragments, and exactCase, the inverse of ignore_case: ignore_case: true is exactCase: false, the default |
MD052 reference-links-images | links/undefined-reference, which also checks footnotes. Includes shortcutSyntax, and ignoredLabels with default [] |
MD053 link-image-reference-definitions | links/unused-definition, which also checks footnotes, with ignoredDefinitions |
MD054 link-image-style | links/allowed-notation |
MD055 table-pipe-style | style/table, with pipeStyle for explicit borders, document consistency, or per-table consistency; required pipes are retained |
MD056 table-column-count | content/table-columns |
MD058 blanks-around-tables | style/block-spacing; next to blocks other than headings and lists where every renderer reads the same blocks |
MD059 descriptive-link-text | links/descriptive-text, with prohibited_texts as prohibitedTexts, also for Obsidian wikilinks |
MD060 table-column-style | style/table, aligned, compact, or tight, with alignedDelimiter. A formatter selects a target style rather than accepting any |
markdownlint's comments such as <!-- markdownlint-disable --> correspond to mdrefine's suppression comments, and its custom rules to plugins.
For the heading checks, markdownlint's level, allow_preamble, and front_matter_title choices correspond to mdrefine's level, allowPreamble, and frontMatterTitle. MD001 reads a front-matter title by default; structure/heading-increment does so with frontMatterTitle: true, off by default because the renderers that mdrefine supports do not show that title as a heading. MD022's lines_above, lines_below, and include_front_matter are linesAbove, linesBelow, and includeFrontMatter, with the same counts; mdrefine refuses counts that style/blank-lines would remove, where markdownlint leaves MD022 and MD012 to disagree. For duplicate headings, use siblingsOnly; its default is true, so set it to false for whole-outline checking. For fenced-code languages, use allowedLanguages and languageOnly, corresponding to allowed_languages and language_only. For unused definitions, use ignoredDefinitions, corresponding to ignored_definitions with the same default, ["//"], for comments written as [//]: # (comment); links/empty takes it too, so that it does not report such a comment's #. These are option mappings, not a promise of identical defaults or behavior: initial-heading's and heading-increment's metadata-title recognition is off by default, and the heading checks use Markdown headings with independent quote and footnote outlines. See each rule's reference for its exact contract.
Prettier
Prettier keeps underlined headings; conversion uses style/heading to write them as # headings. mdrefine has no mode that keeps a mix of underlined and # headings while normalizing heading spacing. Wrapping also leaves paragraphs with hard line breaks as written, where Prettier can wrap their individual lines.
Language-only options such as semi, trailingComma, quoteProps, jsxSingleQuote, bracketSpacing, bracketSameLine, arrowParens, htmlWhitespaceSensitivity, vueIndentScriptAndStyle, objectWrap, experimentalTernaries, and experimentalOperatorPosition do not select a Markdown rule. External embedded-code formatting needs explicit code tools. Parser, plugin, range, and file-context settings are reported rather than executed or used to change dialect detection.
| Prettier | mdrefine |
|---|---|
proseWrap: "always" and printWidth | style/wrap and its width |
proseWrap: "preserve", the default | Turn style/wrap off and enable style/prose-space |
proseWrap: "never" | style/wrap with mode: "never", preserving the same safety exceptions as wrapping |
tabWidth, for nested lists | style/list-indent's nestedIndent |
useTabs | Nothing to set: Prettier indents Markdown with spaces either way |
endOfLine | style/line-ending, opt-in; literal content keeps its bytes |
embeddedLanguageFormatting | Opt-in code formatters, selected by fence language |
singleQuote and other title delimiters | style/link-title: double, single, parentheses, or consistent; keeps delimiters where conversion would need escapes |
requirePragma and insertPragma | requirePragma applies during fallback; insertPragma is not carried over, and no pragma applies with an mdrefine configuration |
<!-- prettier-ignore --> and its start and end | Honored during fallback; with a written configuration use <!-- mdrefine-disable --> and its relatives |
_ for emphasis, ** for strong text | style/emphasis, style/strong |
# headings without closing # | style/heading, with one space after # and no heading-owned indentation |
- for bullets, alternating for adjacent lists | style/list-marker |
1. and numbering | style/ordered-list; auto repeats a number when the first two items share it, where Prettier counts up unless the second number is 1 |
| One space after list markers, list indentation | style/list-indent |
| Backtick fences as short as the code allows | style/code-fence, which also removes space before the info string |
--- for thematic breaks | style/thematic-break |
| Aligned tables | style/table, with padding: "alignment" and display-column widths |
| Trailing spaces removed, hard breaks kept | style/trailing-space, with hardBreakSpaces: 2 for exact two-space breaks |
| Repeated blank lines collapsed | style/blank-lines |
| Blank lines around blocks | style/block-spacing, for headings, lists, fences, and tables |
| Final newline | style/final-newline |
| Code spans with the fewest backticks | style/inline-code |
One space after > | style/quote-marker |
| Markers on lazy continuation lines | style/lazy-continuation; footnote definitions keep their first line on the label's line |
| One space in link reference definitions | style/definition-spacing; keeps multiline definitions and Obsidian spacing |
| YAML front matter | Covered by configuring an external front matter formatter |
Prettier keeps line breaks by default (proseWrap: "preserve"), while recommended wraps at 80 columns. style/wrap keeps the lines of site-generator syntax such as :::note and {% tab %}; for other syntax that depends on its line breaks, turn the rule off or use a suppression comment.
Agents and scripts
Read the JSON report and the exit code, not the text output, whose wording can change in any release. A diagnostic names its rule, its position, and, for a style rule, the edit it proposes. A safe loop is: format --diff to see the changes, format --write to apply them, then lint again for what needs a person. The examples include a script that summarizes a report. To report a defect of the tool, follow the contribution guide.