Changelog
Notable changes to mdrefine, newest first. The format follows Keep a Changelog, and version numbers follow the versioning policy. Each release's section is also its GitHub release's notes; those of 0.6.0 and of the prereleases there are longer than their entries here. From 0.6 on, the site's release notes describe each minor version with examples.
0.7.1 - 2026-11-04
0.7.1 formats tables in two additional Obsidian layouts while preserving the native rendering protections. What's new in 0.7 describes the fix. No upgrading steps are needed.
Fixed
- Format Obsidian tables directly below a callout's title or starting a nested list item below parent text. Tables continuing paragraph text or using Tab container prefixes keep their rendering protections (#759).
Maintenance
- Retry transient GitHub server errors during release CI evidence lookup (#880).
0.7.0 - 2026-11-03
What's new in 0.7 describes the configuration-conversion changes with examples. Read the upgrading steps before updating a workspace that relies on converted settings.
Changed
- Preserve supported EditorConfig intersection branches independently (#840).
- Convert legacy
.markdownlintignoreexclusions alongside standalone markdownlint settings (#803).
Fixed
- Honor the first ignore pattern after a UTF-8 byte-order mark (#877).
Maintenance
- Retain lazy-HTML scaling windows in failed assertions for Windows triage (#826).
- Balance edit-scaling workloads and extend their timing windows under contention (#795).
0.6.5 - 2026-11-03
0.6.5 speeds up deeply nested lists and corrects VitePress paragraph wrapping with syntax plugins. What's new in 0.6 describes both fixes. No upgrading steps are needed.
Fixed
- Avoid repeated blank-line scans and descendant event rebuilding in deeply nested builtin lists, preserving list positions and looseness; syntax plugins retain their original container routines (#859).
- Wrap VitePress body paragraphs beginning with
---when syntax plugins are enabled, while retaining actual plugin block boundaries (#869).
Maintenance
- Refresh the MathJax speech engine and remove its obsolete xmldom override (#481).
- Refresh eligible indirect dependencies used by syntax extensions, documentation and verification tools.
0.6.4 - 2026-11-02
0.6.4 corrects heading checks during markdownlint fallback, avoids refused formatting around VitePress and plugin syntax, and speeds up long lazy list paragraphs. What's new in 0.6 summarizes the fixes. No upgrading steps are needed.
Fixed
- Avoid repeated event searches in long lazy list paragraphs, preserving syntax plugin observations and falling back when a cached token changes (#796).
- Keep paragraphs whose wrapped lines would open a syntax plugin's block, avoiding refused edits while retaining safe wrapping nearby (#777).
- Wrap VitePress body entities without false unsafe-edit refusals, preserving heading anchors and context-sensitive entity distinctions (#863).
- Avoid refused wrapping of VitePress code spans that retain continuation indentation, while keeping ordinary multiline code joinable (#782).
- Avoid refused spacing proposals before table text grouped with a VitePress component, retaining safe table formatting nearby (#800).
- Bound nested directive work during markdownlint's initial-heading HTML-flow inspection, reporting an incomplete check without changing the document (#858).
- Report markdownlint heading-spacing requirements above attached comments and inside list items, preserving their formatting (#849, #850).
- Match markdownlint's initial-heading fallback for HTML headings and raw HTML flow, empty input and permitted preambles (#848, #853, #854, #860).
Maintenance
- Verify parser module-comment provenance in both directions (#851).
0.6.3 - 2026-11-01
0.6.3 corrects configuration fallback, ignore matching, heading checks around HTML comments and footnote lists. What's new in 0.6 summarizes the fixes. No upgrading steps are needed.
Fixed
Avoid false initial-heading and heading-spacing findings around HTML comments during markdownlint fallback (#801).
Keep lists on their footnote label's line stable with
startIndented: true(#802).Match Git's whole-component asterisk runs, including re-inclusion after a trailing run (#843).
Name the last matching Git ignore rule when explaining a skipped input (#844).
Retain Prettier settings when its bundled ignore matcher leaves root Markdown files unignored (#845).
Apply fallback ignores, file selections and overrides relative to an explicit
--rootworkspace, so excluded files stay excluded and scoped settings apply (#791).Stop discovering
.prettierrc.jsoncduring fallback and configuration conversion, matching Prettier's configuration search (#793).
Maintenance
Refresh the Obsidian verification image to the Node.js 24.22.1 security release, retaining the pinned app and installer.
Update the parser bundler and check its module-start comments against the reviewed module graph.
Update the task runner and typed lint tooling, and refresh the pnpm installation check.
0.6.2 - 2026-11-01
Security
- Update the shipped glob matcher for the minimatch pattern-amplification advisory, preserving configuration conversion with its new matcher API (#837).
Fixed
- Remove the incorrect claim that the repository slug is mdrefine's original project name (#832).
Maintenance
Refresh eight age-eligible indirect dependencies, including the Markdown tree traversal utilities.
Consolidate contributor and release procedures and remove redundant state and history from agent guidance (#836).
0.6.1 - 2026-10-30
The documentation is reorganized for people new to mdrefine: rules have pages of their own, upgrading steps and release notes have theirs, and the site has a glossary. What's new in 0.6 summarizes 0.6.1. No upgrading steps are needed.
Changed
mdrefine helpwith an unknown command ends withmdrefine: unknown command 'NAME',configwith options but no command names its commands,lintwith stdin and a path no longer mentions--write, which it does not take, and the conversion messages name markdownlint, Prettier, and#and underlined headings instead of competitors, ATX, and Setext (#830).A glossary defines the terms the pages use, and pages link it. Rule pages and the
style/headingmessage say "#heading" and "underlined heading" instead of ATX and Setext. The pages name markdownlint and Prettier instead of calling them competitors, so the fallback's sections are now "Fallback to markdownlint and Prettier" and "Fallback notices", and so does the notice about their inline comments. The README and the home page say that the package ismdrefineand the repository keeps the original name (#817).The site has release notes, starting with What's new in 0.6, in a menu named after the documented version in the top bar. This changelog's 0.6.0 entry is shorter, with internal work under "Maintenance", and the 0.6.0 and 0.5.0 entries point to their upgrading steps (#813).
The site's top bar has Guide and Reference menus and an Examples link, which the home page's second button also opens, and the sidebar's last group is named About. Issues about the documentation have their own form (#818).
The Obsidian, dialect, integration, VitePress, benchmark, and command line pages describe what users need, and the details for maintainers, such as the list of every Obsidian protection and the parser work behind the benchmark limits, move to maintainer notes in the repository. The command line page gathers its file access and write safety details at its end (#812).
Reference pages are easier to scan: the command line page explains the workspace root once and marks the options that only
formattakes, the configuration page numbers the search for a configuration file and patches one rule ofrecommendedin itsruleUpdatesexample, the integration guide lists which.prettierignoreentries leave Markdown to mdrefine, the library page has a table of the conversion exports, and the VitePress page explainssrcDirwith the usual layout (#816).Getting started covers running mdrefine without installing it, a global installation, pnpm, Yarn, and Bun, and what
npm installdoes in a folder withoutpackage.json. The Obsidian page says how to install for a vault, the integration guide has hooks for the pre-commit framework and lefthook, and the configuration page warns that outside a repository the search for a configuration reaches parent folders such as the home directory (#815).The documentation has an Upgrading page with the steps for each release that needs them: the markdownlint and Prettier fallback and the exit code of bare
formatin 0.6.0, and themdtoolsnames removed in 0.5.0. Getting started, the command line, configuration, and library pages no longer carry migration notes, and the JSON report's example shows the current version (#810).The rules reference has one page per rule, at
rules/<category>/<name>.html. Each page opens with what the rule does, its facts, an example, and its options; edge cases, dialect notes, and related rules follow under their own headings. The index lists the rules by category in plain words, and a link to a rule's section of the former single page, such asrules.html#stylewrap, continues to the rule's page (#811).The built-in rules' descriptions, which
mdrefine rulesprints, are the sentences of the rules index, in plain words.mdrefine ruleslists the rules sorted by ID, plugin rules among them, also with--json. Without a command,mdrefinefollows its help withmdrefine: no command given; try mdrefine lint, andmdrefine configalone names its commands.format --helpdescribes--writeas writing the formatting changes to the files (#819).Getting started picks presets from a table of renderers, shows what
lintprints, and compares the forms oflintandformatin a table. The configuration page lists whatrecommendeddoes, and the README and the home page show onlylintandformat --write. The examples' commands use the installednpx mdrefine, after copying the examples from the package, and the examples' README listsvitepress.jsonc(#814).
Fixed
- Piping output into a program that stops reading early, such as
mdrefine lint --json | head, no longer ends with anEPIPEstack trace and exit code1: the rest of the output is dropped, and the run keeps its own exit code (#820).
0.6.0 - 2026-10-30
What's new in 0.6 describes this release with examples.
Upgrading
Without an mdrefine configuration, a markdownlint or Prettier configuration in the workspace now replaces the recommended preset, and bare format exits 1 when a file would change. The upgrading steps say how to keep the earlier results.
Changed
- Without an mdrefine configuration,
lintandformatuse the settings converted from a markdownlint or Prettier configuration in the workspace, and say so on stderr;--no-competitor-configturns this off (#788). - Bare
formatprints the diff and exits1when a file would change;format --diffis the preview that exits0(#788). - An error about an unusable rule setting names the configuration file that holds it (#788).
lintandformatread stdin with--stdin-filepathalone; help shows examples, the workspace scope, and exit codes;checkandfixsuggest theformatcommand to use;rulestakes--root(#788).content/image-altalso reports an HTML<img>withoutalt(#694).style/tableformats tables inside lists and block quotes (#710).style/headingremoves the indentation of headings, writes one space after the opening hashes, and takesclosing: "add"(#676, #678, #680, #700).style/block-spacingalso separates fenced code blocks and tables from the blocks around them (#684, #697).- The documentation pages describe the current version without version notes, and each rule's section opens with what the rule does (#746).
- The comparison with Prettier and markdownlint covers every markdownlint rule and Prettier option (#668).
Added
config convertwrites an mdrefine configuration from markdownlint and Prettier settings, and the library exports the conversion (#669, #788).- During the fallback, markdownlint's inline configuration comments, Prettier's ignore comments, and
requirePragmaapply (#723). - In
recommended:style/quote-marker, one space after each>, andstyle/lazy-continuation, the>and indentation on lazy continuation lines (#682, #713). - Style rules in no preset:
style/line-ending(#712),style/code-block(#695),style/link-title(#716),style/definition-spacing(#717),style/no-tabs(#671),style/prose-space(#720),links/bare-url(#686), andlinks/allowed-notation(#696). - Problem rules in no preset:
content/html-element(#685),content/proper-names(#693),content/line-length(#673),content/emphasis-spacing,content/inline-code-spacing, andlinks/text-spacing(#689, #690, #691),content/shell-prompt,structure/unspaced-heading, andstructure/split-quote(#674, #675, #677, #683),structure/heading-punctuation,structure/emphasis-heading, andlinks/descriptive-text(#681, #688, #698),structure/heading-template(#692), andlinks/reversed(#672). - Code formatters can format YAML and TOML front matter (#718).
style/wraptakesmode: "never", which joins each paragraph onto one line (#711).style/block-spacingtakeslinesAbove,linesBelow, andincludeFrontMatterfor headings (#679).structure/heading-incrementtakesfrontMatterTitle(#699).links/validtakesignoredFragmentsandexactCase(#705).links/undefined-referencetakesshortcutSyntaxandignoredLabels(#706).style/trailing-spacetakeshardBreakSpaces,listItemEmptyLines, andcodeBlocks;style/thematic-breaktakes any exact text andconsistent;style/list-indenttakesstartIndentedandstartIndent(#687, #701, #702).style/inline-codewrites the fewest backticks a span needs, andstyle/code-fenceremoves blank space before the info string (#714, #715).style/tabletakespipeStyle,style: "tight",alignedDelimiter, andpadding: "alignment"(#708, #709, #719).links/unused-definitionandlinks/emptytakeignoredDefinitions, sorecommendedno longer reports comment definitions such as[//]: # (note)(#707).style/emphasisandstyle/strongtakemarker: "consistent"(#703, #704).
Fixed
style/wrapno longer oscillates withstyle/lazy-continuationon a quoted list item such as>10) text(#781).- In VitePress, rules leave as written a list item that markdown-it reads as a table header (#763).
style/headingno longer proposes an edit that would close front matter (#764).- In VitePress, a list item or paragraph that a component line ends is left as written (#769).
- On GitHub, definitions directly above a table that interrupts them are read as the visible text that GitHub shows (#732).
links/validaccepts a GitHubuser-content-fragment in any letter case (#739).- Rules that read changed text again use the configuration's syntax plugins (#752).
- Emphasis and strong rules keep pairs that cross protected Obsidian HTML or VitePress component blocks (#766, #768).
- A table whose header row is an HTML tag holding a pipe is read as a table, as the renderers read it, so formatting no longer drops its rows (#770).
- A lazy line such as
<b>below a list item's or quote's paragraph continues the paragraph, as most renderers read it; on GitHub, which ends the containers there, they stay as written (#762). - In Obsidian, rules no longer change a list item on a footnote's label line below an unclosed run of backticks (#670).
style/block-spacingno longer proposes blank lines that the engine refuses, below TOML front matter and in VitePress (#748).- In VitePress,
style/wrapreflows a paragraph whose braces are only in code spans, andreportUnreflowedreports the paragraphs it keeps (#755). - Forgejo and Gitea keep table rows read as ordered lists, tables hidden in quoted paragraphs, and backslash hard breaks next to URLs (#661, #663, #664).
Maintenance
- Tokenizer identity checks run as bounded unit cases (#654).
- Release evidence selects the latest check run (#647).
- A shipped internal declaration no longer imports an undeclared type (#655).
- Scanner alerts on fixture rewrites are told apart from sanitizers (#643).
- Workspace memory checks sample a fresh compiled process (#413).
- File discovery, lookup, and writes have separate internal modules (#620).
- The Obsidian check drops unused download dependencies (#639, #640).
- Dependency updates are grouped by maintenance unit (#636, #637), and the markdown-it 15 lines of three plugins are deferred (#608, #610, #611).
- Five parser extensions used only in the build are development dependencies (#624).
- Maintained parser algorithms are checked TypeScript with recorded provenance (#622, #623).
- Test suites are organized by responsibility (#634).
- Renderer protections and rule registration have their own internal modules (#618, #619).
- Workflow tests check YAML structure, and maintenance policy has one home (#635, #641, #642).
- Native renderer reports are validated against their plans, and the Obsidian check pins its versions (#632, #633).
- Release preparation reuses the release commit's CI results (#628, #629).
- Builds stage fresh output and are reused within one invocation (#626, #627).
- Native PR checks select the renderers a change can affect (#630, #631).
0.5.0 - 2026-10-24
Upgrading
The mdtools command, configuration file names, and comment prefixes are removed; the upgrading steps list the renames.
Changed
Dependency automation defers the incompatible reference-parser versions assessed in #591 through #594, with explicit reconsideration conditions, to prevent repeat incompatible PRs.
Remove every
mdtoolscompatibility form deprecated in 0.4.x: the command, all five configuration discovery names and all three suppression-comment prefixes. Replace them withmdrefinebefore upgrading; old comments are ordinary HTML and old config names are no longer discovered. The migration notice and legacy wrapper are removed. Formatting does not migrate user files (#601).
Fixed
Update the indirect
fast-uridependency to 3.1.9 for its URI parsing and normalization security fixes, and the Docker-only Obsidian check'sserialize-javascriptoverride to 7.1.3 for its serialization security fix. The repository lockfile does not ship: existing installations can retain an older indirect version. Runnpm update fast-uriin the consuming project, then usenpm ls fast-urito verify every resolved copy is 3.1.9 or a newer compatible patched release.Memory checks settle V8's collectible roots and compilation caches before sampling, rather than assuming a regular major collection has released all collectible memory. The strict source-retention allowance and negative controls remain in place (#413).
0.4.1 - 2026-10-24
Changed
Forgejo and Gitea gain automated before/after comparisons against pinned official disposable hosts, with realistic fixed-seed corpora and actual repository-file heading/fragment controls. PR and release gates use these checks; weekly adversarial discovery remains separate (#486).
The command, configuration filenames, suppression comments, temporary-file prefixes, examples and current documentation now use
mdrefine. The oldmdtoolscommand, config names and comments remain compatible but deprecated throughout 0.4.x and will all be removed in 0.5.0. Rename config files and replace comment prefixes manually; formatting does not migrate them. One CLI migration notice per run does not count against--max-warnings(#600, #601).Obsidian structural rules conservatively keep ambiguous block boundaries as written. Proposed style edits held by native-rendering protection now report the original rule and severity instead of disappearing silently; safe independent formatting remains available (#603).
Renderer PR/release gates compare existing fixtures and realistic documents at fixed seeds. Fresh adversarial generation becomes weekly/manual discovery with exact issue-linked known differences and stale-entry checks. Maintenance and review now use bounded scope and proportional verification (#603).
A release is published only after CI has passed on its exact commit and GitHub, Obsidian, Forgejo and Gitea rendering of fixed documents is unchanged by formatting at that commit. Pull requests that change parsing, rules, presets, or dependencies run the renderer comparisons. The documentation site records the release and commit it was built from in
version.json, and the current release's site can be corrected from asite-VERSIONbranch without a new version (#485).
Fixed
Builtin parsers avoid repeated nested-label lookups and batch eligible pairs of long emphasis runs. Obsidian also avoids rescanning the same unfinished wikilink or embed suffix (#94, #577).
Focused Obsidian captures record the observed app and renderer versions separately from the requested version, with hashes binding completed evidence to its input and executed spec (#580).
Obsidian keeps the boundary after standalone comments so that heading/list spacing cannot split a native paragraph. Following inline style edits remain available (#357).
Setext titles directly after standalone Obsidian comments keep their underlines, since conversion can turn Reading-view paragraph text into a heading. Titles ending in whitespace-separated raw
#runs also keep their Setext form, preserving Live Preview's title text (#383).Obsidian keeps list indentation and number width where a preceding nested list affects the painted bullet of a following quoted list. Same-width numbering, delimiter preferences and inline styles remain available (#596).
Quoted blocks opened after a Tab keep their literal source where Reading view reads code instead of the parsed list, heading or paragraph. Blank quote rows between consecutive such blocks stay as literal code content too. Independent blocks and ordinary space-only quote margins still format (#597).
Obsidian keeps the separator before a list directly following another list's final secondary paragraph or indented code block, preserving Live Preview's paragraph grouping while allowing safe inline and marker preferences (#598).
Obsidian preserves list markers and tightness across quote and table continuations, literal table rows after list code fences, adjacent Setext titles that Reading view still reads as paragraph text across Tab lines, and blank lines inside physical list fences hidden by the primary inline parse (#582, #583, #584).
Obsidian keeps adjacent display math inside its final list item and retains standalone one-line display widgets when wrapping (#586, #587).
Protected separator starts also reject plugin insertions and replacements that add a blank before the existing line break or remove a quote-only marker, while safe terminal inline edits remain available (#590).
Rendering comparisons account for omitted terminal line feeds in highlighted GitHub code, generated math response IDs inside raw HTML, and invisible Reading-view whitespace beside Obsidian display math, retaining exact source code, authored attributes, formula content and visible inline spacing (#515, #588, #589).
Release-evidence validation runs through linked checkout paths, and the parser builder rejects dependencies resolved outside its checkout consistently across platforms while accepting valid checkout directory names (#574, #575, #578, #579).
Parsing uses a portable internal adapter to reduce repeated copying and searching on documents with many containers, inline constructs or unfinished labels. Syntax plugins retain the original attention/strikethrough event behavior. GFM footnote list items have valid source ranges and read correctly as loose or tight (#300, #151, #240, #280, #542).
GitHub file-fragment checks include native headings and HTML anchors in dollar-math source, with complete Setext title text and correct duplicate numbering. The target reading is lazy and preserves the YAML selection and formatter's math behavior (#570).
GitHub repository-file fragment checks follow YAML preprocessing: mappings and YAML error displays render the body after front matter, while ordinary non-mappings remain Markdown. Unsettled YAML classes retain only anchors shared by both readings, with incomplete checks reported explicitly (#540).
GitHub fragment checks and the CommonMark fallback include headings and explicit IDs inside tagfiltered HTML, whose outer tags render as text. Unsupported inline attributes and raw-text tag spellings remain incomplete instead of producing guessed missing fragments (#541).
Raw HTML heading fragments retain literal character-reference text split by tags or comments, including the numbering of later duplicate headings (#552).
Keep two-space hard breaks inside locally paired multiline code spans, which GitHub can display as literal text after an unmatched longer backtick run. Both wrapping and inline-code joining retain the physical boundary (#335).
Preserve GitHub's heading and lazy paragraph boundaries beside dollar display math, including compact hyphen lines and Setext titles. Keep thematic-marker, heading, spacing and wrap changes from splitting those native boundaries (#336).
Keep Obsidian list markers consistent when HTML leaves one parsed list and protects a later list's markers, so formatting cannot split one Reading-view list by changing only its earlier bullet (#399).
Keep one-column Obsidian table source when missing header or delimiter borders would turn Live Preview text into a table widget, and carry Reading paragraph state across Tab lines when those borders do not form a real table (#568).
Keep a list paragraph adjacent to an Obsidian tilde opener whose info contains backticks, preserving its Live Preview continuation while allowing safe list formatting. When its primary closer opens Live Preview code, conservatively preserve the remaining source because later fences can pair differently (#567).
Retain Reading paragraph state across Tab lines after a one-column table displayed as text, preserving later literal rows while allowing genuine tables to format (#398).
Keep the physical multiline image description containing backticks in Obsidian so wrapping cannot turn displayed source into an embed; safe surrounding inline text still formats (#566).
Retain short multiline Obsidian code spans and their following inline source when Live Preview resets code styling at the physical newline. Earlier text and later independent paragraphs can still format (#565).
Keep the terminal separator before an Obsidian Tab-only paragraph continuation, preserving hard breaks and literal backslashes while allowing earlier wrapping and safe whitespace cleanup (#563).
Preserve lazy paragraph lines that determine the Live Preview nesting of later blocks in an enclosing list item (#376).
Retain the separator before an uncertain Obsidian Setext underline when trimming it would create or remove a Reading-view hard break (#383). Also retain heading forms around unwrapped leading math and sole image/embed titles, preserving Live Preview typography and placement. The host comparison now checks inherited math typography too (#569).
Preserve the spaces or tabs after a literal backslash when trimming them would create a hard line break, avoiding a guarded trailing-space refusal (#560).
Keep hard breaks written as a Tab followed by two spaces, preventing silent break loss or guarded cleanup refusals while preserving ordinary trailing-space cleanup (#562).
Obsidian wrapping retains possible comments that cross a blank line, avoiding an unsafe-format refusal when wrapping would put their closer alone on a line (#559).
Obsidian table formatting keeps a following paragraph from becoming another row when its leading-pipe style differs from the header, including alias and link-destination pipes. Ordinary table rows and text after a real blank still format (#558).
Obsidian wrapping keeps a leading inline comment with the next atom, avoiding an unsafe-format refusal when a long link or other atom would leave the comment alone on its first line. The rest of the paragraph still wraps (#557).
Obsidian formatting preserves literal blocks after root HTML whose nested tags remain open, including URL-shaped children. A parent's closer cannot prematurely end that protection; balanced, void, and self-closing HTML still permit later Markdown formatting. Parsed fences do not close the raw state; formatting conservatively retains source across them until the HTML closes (#556).
Obsidian formatting keeps thematic breaks below tables from turning their last physical line into a literal Live Preview heading, including header-only tables. Existing literal headings retain their row text and underline (#553).
Obsidian formatting preserves underlined headings displayed after lazy list or quote text, retaining the final heading line, its boundaries, and the lazy margin when list padding or number width shrinks. Safe inline changes above it remain available (#554).
Obsidian formatting preserves literal table rows after paragraph-consumed footnotes, including HTML, heading, and code endings, adjacent definitions, and ambiguous tab separators. Independent footnotes and empty or space-only separators still permit table formatting (#549, #551).
Obsidian comparisons ignore native table-cell editing controls when a rendered preview is available, while retaining table content, styling, code spacing, alignment, row counts, links, and nested authored HTML (#550).
Obsidian protection searches for backticks only within each text node, avoiding repeated scans of the remaining document (#547).
Obsidian HTML continuation retains empty container markers before fences and protects code opened by the later fence through the end of the note (#548).
Obsidian formatting retains both list boundaries around lazy text below a list-item heading and keeps top-level HTML source continued through literal quote markers (#513, #546).
Obsidian comparisons recognize single-character Setext underlines and omit unstyled records emptied by existing punctuation normalization, while keeping actual heading, paragraph, code, and widget boundaries distinct (#544, #545).
Obsidian list formatting retains ordered-list boundaries across a missing quote or footnote prefix, and its comparison keeps adjacent headings distinct with or without blank separators (#543, #544).
GitHub table headers and continued rows take precedence over dollar display math, restoring table-column diagnostics and rejecting edits that split those rows into math (#504, #534).
GitHub formatting preserves source whose structural heading HTML makes its anchor boundary uncertain. Fragment checks report that limit instead of guessing IDs, and the semantic guard rejects plugin edits that change ordinary heading anchors (#498, #535, #536).
GitHub fragment checks include HTML headings in Markdown table cells, including headings closed by their cell boundary, and report raw-table reordering as uncertain (#538, #539).
Obsidian formatting keeps non-one ordered continuations after unpiped table text as written, and its comparison ignores terminal whitespace on styled fence separators while retaining code and internal metadata whitespace (#532, #533).
Obsidian formatting preserves code carried into nested quoted lists, task checkbox separators, bare HTML-comment markers, fences whose openers remain in HTML, and indented code read outside a quote (#524, #525, #526, #529, #530).
Renderer comparisons retain empty checkbox widgets and visible hexadecimal text, and ignore lone list-paragraph wrappers whose spacing is unchanged in Obsidian's default theme (#522, #523, #528).
Obsidian formatting preserves adjacent ordered continuations of tab-continued text, separate table headers, lazy paragraphs below list headings, multiline comment endings, non-one lists below quotes, footnote continuation text outside the four-column margin, fences inside HTML after carried inline code, and setext text continued across tabs (#510, #511, #513, #516, #517, #518, #519, #520).
Renderer comparisons retain task-list delimiter boundaries, ignore incidental font styling on separating spaces, and verify GitHub's highlighted code against the source before normalizing wrapper and final-line variations (#512, #514, #515).
Obsidian formatting preserves the boundaries its views read differently: lists around HTML, task fences, multiline setext text, footnote continuation margins and separators, callout lists, heading-like table rows, and inline code or emphasis carried into quoted lists (#487, #488, #489, #492, #493, #494, #495, #496, #499, #505, #506, #507).
The Obsidian comparison finishes and publishes the editor's syntax tree before capturing long notes, and ignores empty table source rows that show nothing. Real text, embeds, and empty code lines still count (#497, #508).
GitHub dollar display fences no longer interrupt an existing paragraph in the parser. Formatting preserves dollar lines as paragraph continuations, including inside list items and block quotes (#491).
The README's opening description and repository About/topics include VitePress, matching the released dialect support. Maintenance instructions require public descriptions and discovery metadata to be updated with feature changes (#483).
0.4.0 - 2026-10-21
Added
- A
vitepressdialect and preset for VitePress 2, with explicit renderer options, preservation of attributes and Vue-sensitive whitespace, and static page, asset, heading, and include-aware link checks (#430). - Opt-in
codeFormattersroute named fenced-code languages to configured executables in CLIformatmodes and the asyncformatWithCodeNode API. Failures retain the affected block, report an operational error, and keep other safe edits (#449). - Existing heading checks accept title levels, preambles, configurable front-matter titles, and duplicate-heading scope; code-language checks accept allowed names and strict info strings. Defaults and problem-only behavior remain unchanged (#460).
- The main
mdrefineentry exportsunifiedDiff, the same scalable unified patch generator thatformat --diffuses, for scripts extending a formatting preview (#321).
Changed
- Rule helpers, syntax protections, source locations, and workspace anchor and target resolution have separate modules for maintenance (#229).
- Contributor checks use TypeScript types during source and test linting, and integration tests validate CLI JSON before inspecting it (#227).
- CI publishes unit coverage reports, complexity checks run separately from coverage, and CLI tests are grouped by responsibility with bounded integration concurrency (#227).
- Large-input regression checks compare warmed scaling measurements instead of requiring fixed execution times, preserving their output assertions (#227).
Fixed
- The semantic guard keeps a resolved filename and fragment separate, so
#inside either cannot make a rewrite to another file appear equivalent (#480). - Diagnostics at the same source position use the same rule ID order on every machine. This also stabilizes plugin edit precedence when the ranges and finding positions match (#229).
- The benchmark guide identifies the parser's repeated list/table transition performance limit and links its standalone reproduction (#300).
- Dependency documentation distinguishes exact direct pins from a consumer's resolved indirect dependencies, and explains consumer lockfiles (#472).
0.3.1 - 2026-10-21
Fixed
- The Prettier coexistence recipe excludes both
.mdand.markdownfiles, matching the file types that both tools process (#458). - Named suppression enables work after an all-rule disable, without enabling other rules or changing configuration (#456).
- Obsidian links match every path component without regard to case inside skipped folders, and report duplicate case variants as ambiguous (#459).
- Supplied documents and stdin paths obey inherited Git ignore rules even when their parent directories do not exist, with repository boundaries and negations preserved (#453).
- The nested-comment complexity check warms both input sizes and measures fresh batches, preserving its scaling bound while reducing timing noise (#227).
- The links memory-regression check waits for asynchronous full collection, keeping its existing retained-text allowance (#413).
- Invalid asynchronous plugin rules report an engine error without leaking a rejected return value into the host process; the CLI keeps exit code
2(#457). - Explicit HTML anchors decode character references in attribute values before matching fragments, including numeric references and escaped ampersands (#451).
- Large paragraphs, tables, code spans, plugin finding lists, configuration arrays, and Obsidian target searches no longer exceed the JavaScript engine's function-argument limit (#454).
- Discovery applies explicit exclusions to supplied editor buffers, including files that do not exist yet, while retaining their text for link checks (#452).
- Table formatting keeps colon-container boundaries as written, and
content/table-columnsstops checking rows at them, preserving site-generator containers (#444). - Wrapping preserves snippet-import paragraphs and keeps literal
[[toc]]markers off new line starts, preventing VitePress build failures and lost text (#445, #446). - The Obsidian check passes large
PROBEbatches through Docker's inherited environment, avoiding the Windows process argument limit (#442).
0.3.0 - 2026-10-20
Changed
recommendedenablesstyle/block-spacingatwarn.lintreports missing blank lines around headings and lists, andformatinserts them where safe, retaining the rule's existing semantic and renderer protections. To keep compact spacing, set"rules": { "style/block-spacing": "off" }(#427).
Fixed
- In the Obsidian dialect,
style/trailing-spacepreserves a separator after a list marker alone on its line. Before, removing the final separator made Live Preview display it as plain text instead of a list item (#441). - In the Obsidian dialect, formatting preserves raw source continued after an inline HTML opener, and
style/block-spacingkeeps paragraph boundaries inside it or an HTML region continued across a whitespace-only line (#435, #439). - In the Obsidian dialect,
style/block-spacingkeeps boundaries inside quotes that Live Preview reads as inline text, and between a list and an adjacent quote or an empty marker and its following text, which Reading view keeps inside the item (#436, #437, #438). - In the Obsidian dialect,
style/block-spacingpreserves the boundary after an underlined heading that a view reads as paragraph text. Before, a new blank line split that paragraph after a multiline level-1 or reference-like heading, or an underline with trailing space, extra indentation, or a lone-(#434).
0.2.7 - 2026-10-20
Fixed
- In the Obsidian dialect, unfinished code entered after a contained HTML block keeps its literal source and original document end. Before, the parser still read HTML where Live Preview had entered code, and formatting could change literal emphasis markers below it (#432).
- In the Obsidian dialect,
style/wrapintroduces no reference-like start after indentation or quote markers, and preserves existing starts and pending destinations. Live Preview hides the destination there, even without a definition line class. Complete labels stay on one line, and a label already split across lines stays as written, preserving its link styling (#432). - In the Obsidian dialect, HTML opened in a list item or quote protects the lazy lines outside its parsed container through both views' raw regions, including later openers after a void or closed element. Before, rules changed literal
*words*to_words_, and an unclosed tag could leave Live Preview reading raw source below a blank line (#432). - In the Obsidian dialect,
style/wrapleaves the first paragraph of a quote opened on a list item's marker line as written. Before, reflow moved words from that line's literal>list text into styled quote continuation lines (#432). style/block-spacingproposes no blank line inside a paragraph or underlined heading in the reading without TOML front matter. Before, separating1.and2) bdirectly below its closing fence would turn paragraph text into a list; the engine refused the unsafe proposal and reported an error (#431).- In the Obsidian dialect, the protection for an inline HTML comment that spans blocks also starts in headings and table cells. No rule changes the opening heading or table through the block holding the closer, or the rest of the note without one. Before, heading conversion, table alignment, and wrapping below them could change Live Preview's comment and container styling even though Reading view stayed the same (#422).
- In the Obsidian dialect, no rule changes a paragraph line that starts at the first column with
[, a label, and]:, or joins it to the line above or below it. Live Preview styles such a line as a link reference definition and the lines around it as text, where CommonMark reads more of the paragraph. Before,style/wrapjoinedTextand[ref]: https://example.combelow it into one line, which Live Preview shows as text (#402). - In the Obsidian dialect,
style/final-newlineadds no line break to a note that ends in a fenced code block without its closing fence, outside a list item or block quote. Obsidian's Reading view shows that line break as one more, empty line of the code (#412). - In the Obsidian dialect,
style/headingleaves an underlined heading whose one line of text starts at the first column with[, a label, and]:as written, as[ref]: see the pagewith===below it. Live Preview styles that line as a link reference definition and shows no heading, where Reading view shows one; written as# [ref]: see the page, both views showed a heading (#417). - In the Obsidian dialect, no rule changes the block below a line of blank space that holds a tab, below a line of paragraph text, unless the block interrupts a paragraph. Obsidian's Reading view reads the tab line as more of the paragraph, and then a paragraph, table, underlined heading, indented code block, definition, or numbered list that does not start at 1 there as more of its text, where CommonMark reads blocks of their own. Before,
style/emphasischanged the markers of such a paragraph, which Reading view paired otherwise across the join, andstyle/tablealigned such a table, which it showed as text (#417). - In the Obsidian dialect, when an HTML block directly follows a list, no rule changes the markers or indentation that decide whether the list continues below it, including lists nested in its last item, or adds a blank line above the block. Obsidian's Reading view keeps the HTML block inside the item and continues the list below it with every item whose marker matches, where CommonMark ends the list at the block. Before,
style/list-markergave both lists one marker, and Reading view showed one list in place of two (#417). - In the Obsidian dialect, no rule changes a note whose opening YAML fence has no ordinary closing fence. Live Preview shows the entire note as YAML source, including below a malformed
--- textcloser that Reading view accepts. Before, the rules formatted it as Markdown, changing that source and moving words into or out of YAML comments (#417). - In the Obsidian dialect, the protections for blocks that Obsidian reads otherwise also apply below TOML front matter, whose
+++lines it reads as paragraph text. Before,style/tablealigned a table directly below the closing fence andstyle/emphasischanged a footnote definition there, both of which Obsidian reads as more of that paragraph (#417). - In the Obsidian dialect, no rule changes a paragraph with an inline HTML comment that ends in another block or has no closer, nor the blocks through its closing block or the note's end. Live Preview carries comment styling across those blocks and restores quote and list styling only after the closer's line. Before,
style/wrapmoved words onto or off that line, changing their styling (#417). - In the Obsidian dialect,
style/wrapstarts no line at the first column with[, a label, and]:, which Live Preview styles as a link reference definition: the word that opens the label stays on the line before it. Before,Some words before the label [a]: tail words after it.at 30 columns broke the line before[a]:, and Live Preview showed the new line as a definition (#416). - In the Obsidian dialect,
style/wrapkeeps a tag such as#tagon the line before it in a block quote whose>markers are written without a space after them, where Live Preview reads a tag directly after the marker as text. Before, a reflowed line could start with>#tag, and Live Preview no longer showed the tag (#416). - In the Obsidian dialect,
style/trailing-spaceandstyle/wrapleave the blank space at the end of a footnote definition's last line as written where a paragraph ends the definition, because Reading view shows it before the footnote's backreference. Before,[^1]: Notebecame[^1]: Note, and the arrow moved against the text (#416). - In the Obsidian dialect,
style/wrapleaves a paragraph as written when a continuation line starts with tag-like text directly after a bare block quote marker. Before, joining>aand>#tag binto>a #tag bcreated a tag in Live Preview, which had shown plain text (#416).
0.2.6 - 2026-10-19
Fixed
- In the Forgejo and Gitea dialects,
links/validaccepts a link to a heading that starts a definition description, as: # Titlebelow a term does, which Forgejo and Gitea give an anchor. Before, it reported the fragment as missing, and also a link to a later heading with the same text, which the renderers number from that description's heading on (#389). - In the Obsidian dialect,
style/headingandstyle/trailing-spaceleave an underlined heading as written where one of Obsidian's views reads its underline as text: Reading view an underline with blank space after it, or before it beyond the heading's text, and Live Preview a lone-. Before,Titleand--below it became## Title, which Reading view had shown as text (#393). - In the Obsidian dialect, no rule joins a lazy line of
=or-in a block quote or list item to the text above it, or changes it: Reading view reads it as a heading's underline. Before,style/wrapjoined> Textand=below it into> Text =, and the heading was gone (#393). - In the Obsidian dialect, no rule changes a list item's paragraph with a lazy line, or the blocks after it in the item, which Live Preview reads outside the list. Before,
style/wrapjoined the lazy line, andstyle/emphasischanged a paragraph that Live Preview showed as code (#393). - In the Obsidian dialect, no rule changes a footnote definition directly below a line of paragraph text, also one that ends a list item or block quote, nor the blank space at the end of that line. Obsidian's Reading view reads the definition as more of the paragraph and its indented lines as code, where the rules formatted a footnote:
style/emphasischanged that code (#392). - In the Obsidian dialect, no rule changes a paragraph or heading that starts on the line below a link reference definition and further right than it. Reading view reads such a line as an indented code block, where
style/wrapandstyle/emphasischanged it as text (#392). - In the Obsidian dialect, no rule changes a fenced code block that a list item or block quote ends before its closing fence, nor anything below it. Obsidian reads the lines below as more of the code, and Live Preview pairs the fences after it otherwise than CommonMark;
style/wrapreflowed such a line and so added a line to the code (#392). - In the Obsidian dialect, no rule changes a table that Obsidian reads otherwise: one directly below a line of paragraph text, which Obsidian reads as more of the text, a one-column table whose header line does not end with a pipe, which it reads as text, and the rows from the first one without a pipe, where its table ends. Nor does any rule change a line with a pipe above
- | -, which Reading view reads as a table header. Before,style/tablealigned or wrote pipes into such text, and Obsidian then showed a table, or showed a row inside one (#394).
0.2.5 - 2026-10-19
Fixed
- In the Obsidian dialect, no rule changes a paragraph with a later line that Obsidian reads as starting a list item where CommonMark reads text: a number other than 1 with text after it, which Live Preview reads as a list item, and an empty item such as
*or-, which Reading view reads as one, nor the blocks indented below it. Before,style/wrapjoined such a line into the paragraph andstyle/headingwroteTextwith-below it as## Text(#384). - In the Obsidian dialect,
style/blank-linesandstyle/trailing-spaceleave a line of blank space that holds a tab, or that lies directly below an HTML block, as written. Obsidian's Reading view reads such a line as more of the paragraph or HTML block above it, so emptying or removing it split a paragraph, or made a heading below an HTML block render. No rule changes the blocks below such a line that Reading view shows as more of the HTML block, up to an empty line (#373). - In the Obsidian dialect, no rule changes a paragraph, heading, or table cell with a backtick that no run of backticks closes, also in a highlight, an inline comment, or the text of a link or image. Live Preview shows the text after it as code, at least to the end of its line, and Reading view pairs a longer run with a later, shorter one, so the rules changed text that Obsidian shows as code. After a run of three or more, the blocks that Live Preview shows as code below it stay as written too (#374).
- In the Obsidian dialect,
style/wrapkeeps an inline footnote,^[…], on one line, and keeps the lines of a paragraph with one that spans lines. Live Preview shows an inline footnote whose text spans lines as Markdown source, and the rule broke lines inside it (#371). - In the Obsidian dialect, a
%%comment at the start of a line below a paragraph, or one that text follows on the line where it closes, is part of the paragraph, as Obsidian reads it. Before, it was read as a block of its own, sostyle/trailing-spaceremoved a hard break above it,style/wrapkept it apart, and no rule saw the text after it. An empty comment,%%%%, in a paragraph is now a comment too, which Obsidian hides (#370). obsidian/callout-markerleaves a marker that Obsidian shows as text, because more than one space or a tab separates it from the quote's>, as in> [!NOTE]. Before, it lowercased the visible text (#372).- In the Obsidian dialect,
style/emphasisandstyle/strongkeep the markers of a paragraph, heading, or table cell whose text holds a*or_that is not a marker, as insnake_caseor5 * 3. Obsidian's Reading view pairs such a character with a later one: aftersnake_case and *em*becamesnake_case and _em_, Reading view showedcase and _emas emphasis (#368). links/pathwithleadingDotkeeps./before a name that starts with a dot, such as./.gitignoreor./.github/, and adds it there. Before, it took such a path for one that already started with./and removed the./(#365).
0.2.4 - 2026-10-19
Fixed
- In the Forgejo and Gitea dialects, no rule changes a definition description any more, a line starting with
:below its term, or the blocks indented below it. Forgejo and Gitea read the description as blocks, where the parser reads text:style/emphasischanged code in a fenced block there and the anchor of a heading, and the list rules moved a list out of the description (#354).
0.2.3 - 2026-10-19
Fixed
style/block-spacingadds no blank line below a list whose last item ends in a fenced code block without its closing fence. Renderers put that line into the code, while the parser does not, so the safety check did not see the change (#351).style/block-spacingadds no blank line between text that the parser reads as an underlined heading in place of a list, at the end of a list item or block quote, and text on the line below it. Renderers read that text as more of the list item's text, and the blank line split it off (#351).links/pathandlinks/notationwrite a|in a table cell as\|, because one that is not escaped ends the cell. Before,links/pathwrote every wikilink it rewrote with a plain|, so that it also reported one whose destination already had the configured form, and wrote a|from a file or folder name into a new destination;links/notationwrote a plain|into a new wikilink or Markdown label. The safety check refused those edits, and with them all of the rule's other edits in the document.links/pathnow changes only a wikilink's destination and keeps the rest as written, also outside tables (#349).
0.2.2 - 2026-10-18
Fixed
- Formatting leaves a list that the parser reads as text as written, with the blocks indented below it and the list after them. The parser reads a list that does not start at 1, or an empty item, as text at the start of a list item or block quote below a paragraph and after an indented code block, where GitHub reads a list. Before,
style/ordered-listandstyle/list-markerjoined the next list to it,style/list-indentmoved a list out of its items,style/emphasischanged what GitHub shows as code,style/headingmade a heading of it, andstyle/block-spacingmade it loose. Every rule's edits in such text are now left out, plugins' too.style/block-spacingnow separates text from a list such as- 1. Step, which the parser reads as GitHub does (#341). style/wrapleaves a paragraph as written when a code span in it continues on an indented lazy continuation line, as after>*without a space. Before, the safety check refused all of the rule's edits in such a document (#340).github/alert-markerno longer capitalizes a marker that GitHub shows as text: in a block quote inside another quote or a list, with text after the marker on its line, or with nothing below it. Before, the visible text changed (#337).style/wrapandstyle/inline-codekeep a code span written across lines as written when a line inside it ends with a backslash. Where its backticks do not pair, as on GitHub after an unpaired longer run, that backslash is a hard line break, which joining turned into a literal backslash (#337).style/thematic-breakno longer writes---directly below display math, which GitHub builds from a paragraph and read as a heading (#337).- Formatting no longer changes what GitHub, Forgejo, Gitea, and Obsidian show for the block directly below TOML front matter. They read the front matter's lines as a paragraph, which that block can continue or underline as a heading:
style/thematic-breakwrote---there, which made the front matter a heading, andstyle/headingsplit such a heading in two. The safety check now also compares the reading without TOML front matter, and the rules leave such a block as written (#338).
0.2.1 - 2026-10-18
Fixed
- The Obsidian dialect reads strikethrough with two tildes only, as Obsidian shows it;
~a~is text there. Before,style/emphasisleft emphasis that the misread strikethrough covered unchanged (#327). style/emphasisandstyle/strongno longer change markers that GitHub reads otherwise than the parser: after an earlier emphasis, in~~b *c~~ d*, where GitHub shows the asterisks as text; next to tildes, inx~~*b b*~~, where a_would be text on GitHub; and in runs of markers that the parser pairs otherwise than CommonMark, as inxab***a* a*y. Each rewrite changed what GitHub showed while the safety check, which uses the parser's reading, saw no change (#327).
0.2.0 - 2026-10-18
The first stable release. It holds everything in the earlier prereleases, and the changes since 0.2.0-rc.3 listed here. From this release on, npm's latest tag names the newest stable release, and prereleases stay under rc and beta; the versioning policy says what a version number promises.
Upgrading from a prerelease
From 0.2.0-rc.3:
- In the Obsidian dialect,
formatno longer breaks lines inside the text of a link, and joins link text that is written across lines, also where0.2.0-rc.3broke it. Notes that0.2.0-rc.3formatted change again: runformat --writeonce after upgrading. - A document from stdin whose
--stdin-filepathis ignored is no longer processed:lint -reports nothing andformat -prints it unchanged, with a warning. Inmdrefine/node,discoverWorkspaceno longer selectsdocumentswhose paths are ignored but lists them inskipped.
From an earlier prerelease, also read the upgrade notes of 0.2.0-rc.3.
Added
--no-warn-ignoredleaves out the warning for a file or directory named on the command line that the tool skips as ignored, for hooks and scripts that pass every changed file. The JSON report still lists it inskipped(#311).
Fixed
format --diff, whichformatalso does for files by default, takes about as long asformat --checkwhen formatting changes lines throughout a long document. Before, building the diff took time in proportion to the document's length times its changed lines: two minutes for a generated document of 8,000 wrapped paragraphs that took four seconds to check (#315).- A document from stdin whose
--stdin-filepaththe configuration, Git, or discovery ignores is left out as a named file is:lint -reports nothing,format -prints the document unchanged, and a warning says why. Before, it was linted and formatted, so an editor's format-on-save rewrote a document that the configuration says to leave alone. In the library,documentsthat ignore rules leave out are no longer selected (#316). - Stdin in a workspace root that Git ignores no longer warns that the root's files are not processed (#316).
- In the Obsidian dialect,
style/wrapkeeps the text of a link on one line and moves the link whole to the next line where it does not fit, and joins link text written across lines. Obsidian's Live Preview shows a link whose text spans lines as Markdown source.0.2.0-rc.3broke lines inside link text in every dialect; elsewhere it still does, as GitHub, Forgejo, and Gitea render such links (#312). - Lists and block quotes below a first line of
---or+++that no later line closes are read as lists and block quotes, as renderers show them. Before, the parser read them as paragraphs, andstyle/wrapjoined them to the line above, which GitHub then showed as text (#309). style/thematic-breakandstyle/headingwithsetextno longer write a line of---alone below such a first line of---, which made front matter of everything between the two lines. The safety check refused every edit of the rule in such a document (#309).
0.2.0-rc.3 - 2026-10-18
Upgrading from a prerelease
From 0.2.0-rc.2:
format --writerefuses a read-only file on every platform, where Linux and macOS used to replace it: the file gets anengine/unwritable-fileerror and stays unchanged. Make the file writable to format it.- One document that cannot be read or written, or that a rule was refused or failed on, no longer keeps
format --writefrom writing the others. Every document that formatted is written, that one with the edits that were kept; each problem is reported for its own file, and the exit code is still2. Before, such a document kept every file from being written, and a file that failed while the batch was written stopped the run. Pass--excludeto leave a file alone. - In the JSON report,
writtenlists the files that were written instead of beingtrueorfalse. A script that tested it as a boolean must test its length, because an empty list counts as true in JavaScript. - A document that starts with a byte order mark gets its findings and edits at the right positions; before, it could be refused or get findings in the wrong places. In the library,
parsenow returnsdocument.sourcewithout the mark, which the tree's positions already skipped. mdtools-disable-next-lineand line numbers also count a carriage return alone as a line ending; before, such a directive could apply to the wrong line.- A file locked by another program on Windows (
EBUSY) is handled like one that may not be read, instead of stopping every command. - A document that is not UTF-8 text gets an
engine/unreadable-fileerror, and stdin that is not stops the command with exit code2. Before, such text was decoded with U+FFFD in place of what was not UTF-8, andformat --writewrote those characters into the file. Save such files as UTF-8. format --writewrites each file into the existing file instead of replacing it with a new one, so the file keeps its permissions, owner and group, attributes, creation time, and hard links. Before, a replaced file lost what Node.js cannot copy: the other names of a hard link kept the old content, and on Windows the file's own permissions were dropped, which could let more users read it. A crash or power failure during the write can now leave a file incomplete; see what writing keeps.- When a rule's edits would change a document's meaning,
formatkeeps the other rules' edits, so--diffand the library's output show them, and reports that rule withengine/unsafe-formatat the line where the meaning would first differ. Before, the whole document was left unchanged. Edits of two rules that overlap are applied in turn instead of refusing the document. - A mistake on the command line, such as an unknown option or command or a missing option value, exits with code
2instead of1, the code for findings, and so doesmdtoolswithout a command, which prints the help on stderr. - The text output and the diff name files from the current directory instead of the workspace root. The JSON report keeps paths from the root and adds
root. config explainnames the configuration fileconfiginstead offile, adds the document'spath, and refuses a document outside the workspace.configno longer accepts--exclude,--json,--stdin-filepath, and--max-warnings, which did nothing there.- A rule that throws or returns invalid findings is skipped for that document with an
engine/rule-errorerror, inlintas well, and the exit code is2. Before,lintstopped at the first such rule, andformatreported it asengine/unsafe-format. A plugin rule with an invalid definition, such asphase: "Inline", stops the command with exit code2where it used to be ignored, and a rule or preset named like a built-in property of every object, such astoString, is reported as unknown. recommendedwarns about six more problems, listed under Added. A check that runslint --max-warnings 0fails on them until the documents are fixed or the rules are turned off.recommendedformats more, soformat --checkandlintreport documents that it used to accept untilformat --writeruns once. It removes trailing spaces, except hard line breaks, and repeated blank lines; writes code fences with backticks and thematic breaks as---; turns underlined headings into#headings and removes closing#sequences; and aligns tables. To keep a form, turn its rule off or set its option, such as"style/heading": ["warn", { "closing": "keep" }]..gitignorefiles decide as in Git: a deeper file can include again what a higher one excludes, and patterns match letter case exactly unless the repository'score.ignorecaseis on, as Git sets it on Windows and macOS. Files that were left out before can be linted now, and the reverse.- Git's other ignore rules apply too:
.gitignorefiles above the workspace root,.git/info/exclude, and the global excludes file, while a file that Git tracks is never ignored. With--rooton a folder of a repository, or with a global excludes file, files that were linted before can be left out now, and a tracked file that a pattern matches is linted. .markdownfiles are documents, and a file named on the command line is read as Markdown whatever its name. A hook that passes every changed file should pass only the Markdown ones.- An input that does not exist and contains
*,?,[, or{is a pattern, and is an error only when it matches no document. .npm-cacheis an ordinary directory, linted like any other unless.gitignoreorignoreleaves it out.style/wrapwraps more: across line breaks in links, emphasis, and code spans, inside the text of links and emphasis, in the bodies of GitHub, Forgejo, and Gitea alerts, and before words such as--configor**strong**. A check that runsformat --checkfails on documents formatted by an earlier version untilformat --writeruns once.- In the library,
resolveConfigtakes one options object,{ path, config, plugins, defaultDialect }, aslintandformatdo, so their options serve as they are. A call with the four separate arguments of the prereleases throws an error that says so. presets,builtInRules,dialects,dialectAliases, andconfigSchemaare frozen, and a change to them throws aTypeError; change a copy instead.- In the library's types,
WikiLink,ObsidianComment, andObsidianHighlightreplaceObsidianLiteral,Rule.schemais aJsonSchemainstead of Ajv's schema type, and the lists and records inConfigare read-only: replace a list or therulesof a configuration instead of changing it in place. formatthrows an error that the workspace throws, such as a loader'sEIO, aslintdoes, instead of reporting it asengine/unsafe-format.- A plugin named
engineis rejected. recommendedformats lists:*and+bullets become-,1)becomes1., numbers are made consistent, and each item gets one space after its marker and no indentation before it.format --checkfails on lists written otherwise until they are formatted or the list rules are turned off.style/wrapno longer joins the lines of a paragraph that has a line starting with:::,{%, or{{, but lines that an earlier version joined stay joined: split them again where a site generator reads them by line.- A document whose front matter would end inside a code block, an HTML block, or display math can get
engine/unsafe-formaterrors, and exit code2, fromformat, where the rules used to change it.
From 0.2.0-rc.1 or earlier, also:
- When nothing names a dialect, documents are read as GitHub Markdown, or as Obsidian notes inside a vault, where they used to be read as CommonMark. Name
"dialect": "commonmark"to keep the old reading. The library'slint,format,resolveConfig, andcreateWorkspaceassumegithubas well. $…$is math only under the conditions of each dialect's renderer; before, any text between two$signs was. Text that was left alone because of that is now wrapped and restyled like other prose.- A directory that may not be read is skipped with a warning, and a selected document that may not be read gets an
engine/unreadable-fileerror, where both used to stop the command with exit code2and no report. An.obsidian/app.jsonthat cannot be read or is not valid JSON gives a warning, and the vault is not wrapped. - In a vault below the workspace root, Obsidian links resolve from the vault's root and reach only its files. A workspace root inside a vault no longer counts as the whole vault, so links that the rest of the vault could affect are not rewritten.
- A plugin package is resolved as an
importfrom the configuration file, with the conditions given to Node.js, so a package with both animportand arequireentry loads throughimport. - In the library, a source loader given to
createWorkspacecan be called more than once for the same file and must return the same text each time, and one that throwsEACCESorEPERMmakes the file unreadable instead of stopping the run. - The
lintsummary counts errors and warnings, and messages about paths, configuration files, and plugins are worded differently. Exit codes and the JSON report keep their meaning.
From every prerelease:
- The command line, the configuration, the JSON report, and the library now follow the versioning policy. npm's
latesttag names the newest stable release; release candidates no longer move it.
Added
style/list-marker,style/ordered-list, andstyle/list-indent, inrecommended, write bullets as-and numbers as1.with consistent numbering and one space after each marker, and move each item's lines with its text. Their options cover markdownlint's MD004, MD007, MD029, and MD030 (#275).--quietreports errors only, andruleslists the rules of the configuration's plugins too, also as JSON with--json(#260).- The CLI finds
mdtools.config.jsandmdtools.config.cjs,extendsaccepts a single preset name, and a rule's setting can be its severity alone in a list, as in["warn"]. The package ships the configuration's JSON Schema asconfig.schema.json, which the documentation site serves as well, for editors to complete and check configuration files (#261). - The library loads with
require(), exportsmdrefine/package.json, and has a doc comment for every export; the package listskeywords(#261). - Nine problem rules:
links/undefined-reference,links/unused-definition,links/empty,structure/heading-increment,content/image-alt, andcontent/table-columnsinrecommended, and the optionalcontent/code-language,structure/single-title, andstructure/duplicate-heading(#237). style/block-spacingseparates headings and lists from the blocks around them with a blank line (#190).- Four style rules in
recommended:style/code-fence,style/thematic-break,style/blank-lines, andstyle/trailing-space, with markdownlint's choices as options.style/headingalso removes closing#sequences and can write underlined headings (optionsclosingandstyle),style/tablecan write compact tables (optionstyle), and both joinrecommended(#267). lintandformatacceptrangesto work on parts of a document, andformatreturns theirscope(#188, #195).mdrefine/nodeexportsdiscoverWorkspace(), the CLI's discovery of files, with the exampleexamples/library/lint-workspace.mjs(#189).- Glob patterns as inputs, such as
mdtools lint "docs/**/*.md", expanded by the CLI itself on every system (#239).
Changed
- A mistake on the command line exits with code
2, as in ESLint and Prettier (#260). - The text output names files from the current directory, and the JSON report gives the workspace
root(#260). format --checknames the files that would change (#260).- The help describes every command and lists only the options that each one reads, an unknown dialect gets the list of dialects,
lint --fixpoints toformat --write,config explainreportsconfigandpathand refuses documents outside the workspace, and theobsidian/strict-line-breaksmessage speaks to both command-line and library users (#260). - Configuration and rule option errors name the setting as it is written, in plain words, and list the allowed values, as in
"extends[1]" must be a preset name, not 5; a JSON syntax error gives its line and column (#261). resolveConfigtakes an options object, the exported tables are frozen and typed read-only, and the types of Obsidian nodes and rule schemas are their own (#261).format --writerefuses read-only files on every platform, and reports a file that fails withengine/unwritable-file(#177).- A file locked by another program on Windows counts as one that may not be read, in the CLI and in
createWorkspaceloaders (#177). - Documents and stdin that are not UTF-8 text are refused instead of being damaged, and
createWorkspacetreats a loader error with the codeERR_ENCODING_INVALID_ENCODED_DATAas an unreadable target (#234). format --writewrites into the existing file instead of replacing it, so that the file keeps its permissions, attributes, owner, creation time, and hard links, and writes the original content back when a write fails part way (#234, #255).format --writewrites every document that formatted, and reports a document that could not be read, formatted as configured, or written for its own file; the JSON report'swrittenlists the files written (#255).- The documentation is restructured, with new pages for the rules, dialects, integrations, the library API, versions, and this changelog, and the release notes come from this file (#238).
- When one rule's edits would change a document's meaning,
formatleaves out only that rule's edits and names the rule and the line, and it applies overlapping edits in turn (#241). - Plugin rule option schemas are compiled once instead of for every document (#241).
- Many repeated headings, and many emphases on one line, no longer take time that grows with the square of their number on Forgejo and Gitea, and
ignoreandoverridespatterns are compiled once instead of for every path (#273). .markdownfiles are documents and link targets, and a file named on the command line is read as Markdown whatever its name (#239).- CI tests Node.js 22.12.0, the lowest supported version, where it tested the newest Node.js 22, and also tests Node.js 26 (#257).
- The documentation site describes the newest stable release instead of the unreleased state of
main(#257). style/wrapreflows a paragraph with a line break inside a link's text, an image description, emphasis, strong text, strikethrough, or a code span, and can break lines inside the text of links and emphasis (#262).style/wrapwraps the body of a GitHub, Forgejo, or Gitea alert below the line with its marker, as it does for Obsidian callouts (#262).style/wrapreflows a paragraph whose code span goes on in a line that starts with spaces or tabs, and joins the span without them, asstyle/inline-codedoes (#288).style/wrapleaves a paragraph as written when one of its lines starts with:::,{%, or{{, such as a Docusaurus admonition or a Liquid tag, keeps a word that starts with them from starting a line, and keeps a template tag that one line holds whole. Site generators read these by line (#298).- The plugin example's style rule writes one space after a heading's
#marks, becausestyle/thematic-breaknow does what it did (#267).
Fixed
style/wrap, the other rules that leave the line of an alert's or callout's marker alone, and the safety check recognize a marker written with an escape or a character reference, such as\[!NOTE]or[!TIP], which GitHub renders as an alert. Before,style/wrapjoined the body onto such a marker, and GitHub showed an ordinary quote (#298).- A document whose front matter would end at a
---or+++inside a code block, an HTML block, or display math that a line of the front matter opens is read with that block, and formatting keeps its meaning both with and without the front matter. Before, rules could change the block's content, such as a___in code into---, which GitHub shows (#298). style/wrapwraps a paragraph that starts with a linked image, such as a badge, unless it opens a block quote. Before, it was left as written, as if it started with a callout's marker (#298).- A plugin rule's option schema of
falserefuses options, and an asynchronous schema ($async) is refused when the rule set is built instead of crashing Node.js after the run (#297). - A heading whose code span is written across lines gets the anchor that GitHub gives it, so a correct link to it is no longer reported missing, and the library's
textContentreads such a code span as renderers show it (#293). --dialectworks with an.mjsconfiguration that exports a frozen object (#260).--stdin-filepathaccepts a document inside a root that is reached through a link or a Windows junction (#260).- The configuration and command-line pages say that configuration patterns match letter case on every system, while
.gitignorefiles and input patterns ignore it on Windows and macOS (#261). formatstops on an error of the workspace, aslintdoes, instead of reporting it asengine/unsafe-format, and a plugin namedengine, whose findings would pass for the engine's own, is rejected (#261).parsereads a document in the dialect that an alias such ascodebergstands for, so Forgejo's rules apply to it, and the type declarations no longer need@types/mdastfrom another package, which a strict installation such as pnpm's does not provide (#261).- Documents and JSON configuration files that start with a byte order mark are read correctly (#174).
style/headingkeeps link reference definitions directly above an underlined heading, which it used to replace with the heading, so that the document was refused (#267).style/final-newlineadds no line ending, andstyle/block-spacingno blank line, where it would become part of an unclosed HTML or Obsidian comment, andstyle/final-newlinenone where it would add a line to a code block without its closing fence; such documents used to be refused (#267).style/wrapleaves paragraphs as written instead of refusing the document when their first word is a bare block marker (#196), when an Obsidian paragraph holds==,[[, or]]outside its highlights and links, or when the first line is lazy (#200).style/wraplets words that only start like a block marker, such as--config,#tag, or-5, begin a line, and keeps a table delimiter row from beginning one (#262).style/wrapkeeps a paragraph's first line from becoming a link reference definition, a thematic break, or display math instead of refusing the document (#262).style/wrapcounts a tab in a list item's prefix up to the next tab stop instead of as no width (#262).- A carriage return alone ends a line for suppression directives, diagnostic positions, callout titles, and
style/wrap(#196, #200). - A rule that throws or returns invalid findings is reported as
engine/rule-errorfor its document, and invalid plugin rules and names such astoStringare rejected with a clear message (#241). .gitignorefiles are applied as Git applies them, with negations in deeper files and letter case per platform (#239).- Git's ignore rules apply from the repository's top level, also above the workspace root, together with
.git/info/excludeand the global excludes file. Inside a nested repository only its own rules apply, and none applies to a file that Git tracks (#273). - Git's ignore rules match letter case as each repository's
core.ignorecasesays, and each repository, also a nested one, uses the global excludes file that its own settings name (#287). - A file or directory named on the command line that is ignored gets a warning that says why, instead of being skipped without a word (#239).
- A link into
dist,coverage,node_modules, or another directory that is not processed, or through a symbolic link or junction, is checked on disk instead of being reported missing; one that leads outside the workspace root cannot be checked (#259). - An Obsidian block identifier counts only at the end of a block and holds letters, digits, and hyphens, in every rule that reads one (#259).
- A vault's
.obsidian/app.jsonthat starts with a byte order mark is read (#259). links/notationdecodes percent escapes when it makes a wikilink (#259).- A link to a file that is not Markdown accepts any fragment, such as an image's
#gh-dark-mode-onlyor a PDF's#page=2(#259). - An Obsidian wikilink never resolves to a folder (#259).
- Obsidian links to a heading below another,
[[Note#Setup#Mobile]], are resolved (#259). - An Obsidian heading link matches the heading's source text, with its backticks, as well as its visible text (#259).
style/emphasisandstyle/strongleave the text of a collapsed or shortcut reference, such as[*Title*][], as written, because that text is also the label that matches the definition, and likewise text in brackets that the new markers would make match one. Before, all of the rule's edits in the document were refused (#274).style/emphasis,style/strong,style/inline-code, andstyle/block-spacingno longer propose edits that change the parse in rare places, where their edits were refused: crossed or shared markers, as in*a _b* c_or*****a***, markers next to dollar math or across the line of a callout's or alert's marker, code spans whose joined line would form an Obsidian highlight or wikilink, change that line, or pair with earlier backticks, a table below one that ends in a list item or block quote, and a list in a quote that ends in an empty footnote definition (#274).style/inline-codeleaves out the spaces and tabs that start a code span's next line when it joins the span, as CommonMark and GitHub do; before,npm runcontinued by an indentedbuildbecamenpm run build, which changed what GitHub shows. The safety check compares code spans the same way (#274).style/inline-codeand the safety check read a code span as GitHub shows it where a line break is at its edge, and on a lazy continuation line, one without its block quote's>or its list item's indentation, whose spaces and tabs GitHub keeps in the span. Before, the rule could change what GitHub shows, such asnpm run buildintonpm run build, and the safety check did not notice. Outside the GitHub dialect, a span on such an indented lazy line stays split, because commonmark.js leaves those spaces out.style/wrapnow also reflows a paragraph whose code span has a line break at its edge (#291).style/wrapkeeps a dollar from the edge of a line where it could begin or end math there: in Obsidian a word that ends in$stays with the word after it, and on GitHub and in Obsidian a word that starts with$stays with the word before it. Before, such a paragraph was refused in Obsidian (#274).style/wrapleaves a paragraph as written when its second line is a table delimiter row, in every dialect with tables. Directly below a table that ends in a list item or block quote, the parser reads another table as such a paragraph, and joining its lines removed a table that GitHub shows (#274).style/wrapkeeps a run of=or-that starts the body of an alert or callout together with the next word. Alone on a line, it would underline the marker's line as a heading, and the paragraph was refused (#274).- The type declaration of
defaultDialectdescribes vaults anywhere in a workspace (#184), and the CLI guide explains stdin in PowerShell (#182).
0.2.0-rc.2 - 2026-10-06
When nothing names a dialect, GitHub Markdown is assumed, or the Obsidian dialect inside a vault. Vaults are recognized anywhere in a workspace, and their links resolve within them. Directories and files that may not be read no longer stop a run. $ math follows each renderer. Plugin packages resolve like an import. Messages are clearer, link checks need about half the memory, and the package ships examples. Release notes.
0.2.0-rc.1 - 2026-10-03
New forgejo and gitea dialects and presets, with codeberg as another name for forgejo. Fragments are checked against the anchors that each renderer generates, Forgejo and Gitea heading attributes are preserved, and forgejo/heading-id reports ids that break rendering. Release notes.
0.1.0-rc.1 - 2026-09-21
Five built-in rules no longer propose edits that made formatting refuse ordinary documents. Obsidian links match without regard to case, new line breaks follow a document's first line ending, and applying many edits takes linear time. Release notes.
0.1.0-beta.4 - 2026-09-18
style/wrap wraps an Obsidian callout's body directly below its title, and reports a long title with reportUnbreakable. Release notes.
0.1.0-beta.3 - 2026-09-18
style/wrap's report options tell breakable prose from unbreakable atoms, and formatting is more than twice as fast. Release notes.
0.1.0-beta.2 - 2026-09-18
New style/inline-code and structure/initial-heading rules, the measure, reportUnreflowed, and reportUnbreakable options of style/wrap, the resolve settings, and --exclude. Link resolution fixes, and workspaces with source loaders. Release notes.
0.1.0-beta.1 - 2026-09-18
The first public release: lint and format for CommonMark, GitHub Markdown, and Obsidian, with presets, overrides, suppression comments, plugins, and link checks. Release notes.