Skip to content

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 .markdownlintignore exclusions 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 --root workspace, so excluded files stay excluded and scoped settings apply (#791).

  • Stop discovering .prettierrc.jsonc during 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 help with an unknown command ends with mdrefine: unknown command 'NAME', config with options but no command names its commands, lint with 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/heading message 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 is mdrefine and 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 format takes, the configuration page numbers the search for a configuration file and patches one rule of recommended in its ruleUpdates example, the integration guide lists which .prettierignore entries leave Markdown to mdrefine, the library page has a table of the conversion exports, and the VitePress page explains srcDir with the usual layout (#816).

  • Getting started covers running mdrefine without installing it, a global installation, pnpm, Yarn, and Bun, and what npm install does in a folder without package.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 format in 0.6.0, and the mdtools names 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 as rules.html#stylewrap, continues to the rule's page (#811).

  • The built-in rules' descriptions, which mdrefine rules prints, are the sentences of the rules index, in plain words. mdrefine rules lists the rules sorted by ID, plugin rules among them, also with --json. Without a command, mdrefine follows its help with mdrefine: no command given; try mdrefine lint, and mdrefine config alone names its commands. format --help describes --write as writing the formatting changes to the files (#819).

  • Getting started picks presets from a table of renderers, shows what lint prints, and compares the forms of lint and format in a table. The configuration page lists what recommended does, and the README and the home page show only lint and format --write. The examples' commands use the installed npx mdrefine, after copying the examples from the package, and the examples' README lists vitepress.jsonc (#814).

Fixed ​

  • Piping output into a program that stops reading early, such as mdrefine lint --json | head, no longer ends with an EPIPE stack trace and exit code 1: 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, lint and format use the settings converted from a markdownlint or Prettier configuration in the workspace, and say so on stderr; --no-competitor-config turns this off (#788).
  • Bare format prints the diff and exits 1 when a file would change; format --diff is the preview that exits 0 (#788).
  • An error about an unusable rule setting names the configuration file that holds it (#788).
  • lint and format read stdin with --stdin-filepath alone; help shows examples, the workspace scope, and exit codes; check and fix suggest the format command to use; rules takes --root (#788).
  • content/image-alt also reports an HTML <img> without alt (#694).
  • style/table formats tables inside lists and block quotes (#710).
  • style/heading removes the indentation of headings, writes one space after the opening hashes, and takes closing: "add" (#676, #678, #680, #700).
  • style/block-spacing also 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 convert writes 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 requirePragma apply (#723).
  • In recommended: style/quote-marker, one space after each >, and style/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), and links/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, and links/text-spacing (#689, #690, #691), content/shell-prompt, structure/unspaced-heading, and structure/split-quote (#674, #675, #677, #683), structure/heading-punctuation, structure/emphasis-heading, and links/descriptive-text (#681, #688, #698), structure/heading-template (#692), and links/reversed (#672).
  • Code formatters can format YAML and TOML front matter (#718).
  • style/wrap takes mode: "never", which joins each paragraph onto one line (#711).
  • style/block-spacing takes linesAbove, linesBelow, and includeFrontMatter for headings (#679).
  • structure/heading-increment takes frontMatterTitle (#699).
  • links/valid takes ignoredFragments and exactCase (#705).
  • links/undefined-reference takes shortcutSyntax and ignoredLabels (#706).
  • style/trailing-space takes hardBreakSpaces, listItemEmptyLines, and codeBlocks; style/thematic-break takes any exact text and consistent; style/list-indent takes startIndented and startIndent (#687, #701, #702).
  • style/inline-code writes the fewest backticks a span needs, and style/code-fence removes blank space before the info string (#714, #715).
  • style/table takes pipeStyle, style: "tight", alignedDelimiter, and padding: "alignment" (#708, #709, #719).
  • links/unused-definition and links/empty take ignoredDefinitions, so recommended no longer reports comment definitions such as [//]: # (note) (#707).
  • style/emphasis and style/strong take marker: "consistent" (#703, #704).

Fixed ​

  • style/wrap no longer oscillates with style/lazy-continuation on 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/heading no 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/valid accepts a GitHub user-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-spacing no longer proposes blank lines that the engine refuses, below TOML front matter and in VitePress (#748).
  • In VitePress, style/wrap reflows a paragraph whose braces are only in code spans, and reportUnreflowed reports 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 mdtools compatibility form deprecated in 0.4.x: the command, all five configuration discovery names and all three suppression-comment prefixes. Replace them with mdrefine before 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-uri dependency to 3.1.9 for its URI parsing and normalization security fixes, and the Docker-only Obsidian check's serialize-javascript override to 7.1.3 for its serialization security fix. The repository lockfile does not ship: existing installations can retain an older indirect version. Run npm update fast-uri in the consuming project, then use npm ls fast-uri to 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 old mdtools command, 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 a site-VERSION branch 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 vitepress dialect 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 codeFormatters route named fenced-code languages to configured executables in CLI format modes and the async formatWithCode Node 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 mdrefine entry exports unifiedDiff, the same scalable unified patch generator that format --diff uses, 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 .md and .markdown files, 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-columns stops 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 PROBE batches through Docker's inherited environment, avoiding the Windows process argument limit (#442).

0.3.0 - 2026-10-20 ​

Changed ​

  • recommended enables style/block-spacing at warn. lint reports missing blank lines around headings and lists, and format inserts 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-space preserves 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-spacing keeps paragraph boundaries inside it or an HTML region continued across a whitespace-only line (#435, #439).
  • In the Obsidian dialect, style/block-spacing keeps 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-spacing preserves 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/wrap introduces 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/wrap leaves 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-spacing proposes no blank line inside a paragraph or underlined heading in the reading without TOML front matter. Before, separating 1. and 2) b directly 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/wrap joined Text and [ref]: https://example.com below it into one line, which Live Preview shows as text (#402).
  • In the Obsidian dialect, style/final-newline adds 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/heading leaves an underlined heading whose one line of text starts at the first column with [, a label, and ]: as written, as [ref]: see the page with === 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/emphasis changed the markers of such a paragraph, which Reading view paired otherwise across the join, and style/table aligned 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-marker gave 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 --- text closer 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/table aligned a table directly below the closing fence and style/emphasis changed 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/wrap moved words onto or off that line, changing their styling (#417).
  • In the Obsidian dialect, style/wrap starts 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/wrap keeps a tag such as #tag on 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-space and style/wrap leave 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]: Note became [^1]: Note, and the arrow moved against the text (#416).
  • In the Obsidian dialect, style/wrap leaves a paragraph as written when a continuation line starts with tag-like text directly after a bare block quote marker. Before, joining >a and >#tag b into >a #tag b created 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/valid accepts a link to a heading that starts a definition description, as : # Title below 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/heading and style/trailing-space leave 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, Title and -- 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/wrap joined > Text and = 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/wrap joined the lazy line, and style/emphasis changed 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/emphasis changed 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/wrap and style/emphasis changed 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/wrap reflowed 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/table aligned 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/wrap joined such a line into the paragraph and style/heading wrote Text with - below it as ## Text (#384).
  • In the Obsidian dialect, style/blank-lines and style/trailing-space leave 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/wrap keeps 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, so style/trailing-space removed a hard break above it, style/wrap kept 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-marker leaves 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/emphasis and style/strong keep the markers of a paragraph, heading, or table cell whose text holds a * or _ that is not a marker, as in snake_case or 5 * 3. Obsidian's Reading view pairs such a character with a later one: after snake_case and *em* became snake_case and _em_, Reading view showed case and _em as emphasis (#368).
  • links/path with leadingDot keeps ./ before a name that starts with a dot, such as ./.gitignore or ./.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/emphasis changed 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-spacing adds 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-spacing adds 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/path and links/notation write a | in a table cell as \|, because one that is not escaped ends the cell. Before, links/path wrote 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/notation wrote 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/path now 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-list and style/list-marker joined the next list to it, style/list-indent moved a list out of its items, style/emphasis changed what GitHub shows as code, style/heading made a heading of it, and style/block-spacing made it loose. Every rule's edits in such text are now left out, plugins' too. style/block-spacing now separates text from a list such as - 1. Step, which the parser reads as GitHub does (#341).
  • style/wrap leaves 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-marker no 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/wrap and style/inline-code keep 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-break no 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-break wrote --- there, which made the front matter a heading, and style/heading split 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/emphasis left emphasis that the misread strikethrough covered unchanged (#327).
  • style/emphasis and style/strong no 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, in x~~*b b*~~, where a _ would be text on GitHub; and in runs of markers that the parser pairs otherwise than CommonMark, as in xab***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, format no longer breaks lines inside the text of a link, and joins link text that is written across lines, also where 0.2.0-rc.3 broke it. Notes that 0.2.0-rc.3 formatted change again: run format --write once after upgrading.
  • A document from stdin whose --stdin-filepath is ignored is no longer processed: lint - reports nothing and format - prints it unchanged, with a warning. In mdrefine/node, discoverWorkspace no longer selects documents whose paths are ignored but lists them in skipped.

From an earlier prerelease, also read the upgrade notes of 0.2.0-rc.3.

Added ​

  • --no-warn-ignored leaves 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 in skipped (#311).

Fixed ​

  • format --diff, which format also does for files by default, takes about as long as format --check when 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-filepath the 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, documents that 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/wrap keeps 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.3 broke 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, and style/wrap joined them to the line above, which GitHub then showed as text (#309).
  • style/thematic-break and style/heading with setext no 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 --write refuses a read-only file on every platform, where Linux and macOS used to replace it: the file gets an engine/unwritable-file error 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 --write from 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 still 2. Before, such a document kept every file from being written, and a file that failed while the batch was written stopped the run. Pass --exclude to leave a file alone.
  • In the JSON report, written lists the files that were written instead of being true or false. 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, parse now returns document.source without the mark, which the tree's positions already skipped.
  • mdtools-disable-next-line and 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-file error, and stdin that is not stops the command with exit code 2. Before, such text was decoded with U+FFFD in place of what was not UTF-8, and format --write wrote those characters into the file. Save such files as UTF-8.
  • format --write writes 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, format keeps the other rules' edits, so --diff and the library's output show them, and reports that rule with engine/unsafe-format at 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 2 instead of 1, the code for findings, and so does mdtools without 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 explain names the configuration file config instead of file, adds the document's path, and refuses a document outside the workspace. config no 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-error error, in lint as well, and the exit code is 2. Before, lint stopped at the first such rule, and format reported it as engine/unsafe-format. A plugin rule with an invalid definition, such as phase: "Inline", stops the command with exit code 2 where it used to be ignored, and a rule or preset named like a built-in property of every object, such as toString, is reported as unknown.
  • recommended warns about six more problems, listed under Added. A check that runs lint --max-warnings 0 fails on them until the documents are fixed or the rules are turned off.
  • recommended formats more, so format --check and lint report documents that it used to accept until format --write runs 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" }].
  • .gitignore files decide as in Git: a deeper file can include again what a higher one excludes, and patterns match letter case exactly unless the repository's core.ignorecase is 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: .gitignore files above the workspace root, .git/info/exclude, and the global excludes file, while a file that Git tracks is never ignored. With --root on 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.
  • .markdown files 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-cache is an ordinary directory, linted like any other unless .gitignore or ignore leaves it out.
  • style/wrap wraps 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 --config or **strong**. A check that runs format --check fails on documents formatted by an earlier version until format --write runs once.
  • In the library, resolveConfig takes one options object, { path, config, plugins, defaultDialect }, as lint and format do, 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, and configSchema are frozen, and a change to them throws a TypeError; change a copy instead.
  • In the library's types, WikiLink, ObsidianComment, and ObsidianHighlight replace ObsidianLiteral, Rule.schema is a JsonSchema instead of Ajv's schema type, and the lists and records in Config are read-only: replace a list or the rules of a configuration instead of changing it in place.
  • format throws an error that the workspace throws, such as a loader's EIO, as lint does, instead of reporting it as engine/unsafe-format.
  • A plugin named engine is rejected.
  • recommended formats lists: * and + bullets become -, 1) becomes 1., numbers are made consistent, and each item gets one space after its marker and no indentation before it. format --check fails on lists written otherwise until they are formatted or the list rules are turned off.
  • style/wrap no 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-format errors, and exit code 2, from format, 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's lint, format, resolveConfig, and createWorkspace assume github as 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-file error, where both used to stop the command with exit code 2 and no report. An .obsidian/app.json that 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 import from the configuration file, with the conditions given to Node.js, so a package with both an import and a require entry loads through import.
  • In the library, a source loader given to createWorkspace can be called more than once for the same file and must return the same text each time, and one that throws EACCES or EPERM makes the file unreadable instead of stopping the run.
  • The lint summary 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 latest tag names the newest stable release; release candidates no longer move it.

Added ​

  • style/list-marker, style/ordered-list, and style/list-indent, in recommended, write bullets as - and numbers as 1. 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).
  • --quiet reports errors only, and rules lists the rules of the configuration's plugins too, also as JSON with --json (#260).
  • The CLI finds mdtools.config.js and mdtools.config.cjs, extends accepts 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 as config.schema.json, which the documentation site serves as well, for editors to complete and check configuration files (#261).
  • The library loads with require(), exports mdrefine/package.json, and has a doc comment for every export; the package lists keywords (#261).
  • Nine problem rules: links/undefined-reference, links/unused-definition, links/empty, structure/heading-increment, content/image-alt, and content/table-columns in recommended, and the optional content/code-language, structure/single-title, and structure/duplicate-heading (#237).
  • style/block-spacing separates 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, and style/trailing-space, with markdownlint's choices as options. style/heading also removes closing # sequences and can write underlined headings (options closing and style), style/table can write compact tables (option style), and both join recommended (#267).
  • lint and format accept ranges to work on parts of a document, and format returns their scope (#188, #195).
  • mdrefine/node exports discoverWorkspace(), the CLI's discovery of files, with the example examples/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 --check names 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 --fix points to format --write, config explain reports config and path and refuses documents outside the workspace, and the obsidian/strict-line-breaks message 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).
  • resolveConfig takes 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 --write refuses read-only files on every platform, and reports a file that fails with engine/unwritable-file (#177).
  • A file locked by another program on Windows counts as one that may not be read, in the CLI and in createWorkspace loaders (#177).
  • Documents and stdin that are not UTF-8 text are refused instead of being damaged, and createWorkspace treats a loader error with the code ERR_ENCODING_INVALID_ENCODED_DATA as an unreadable target (#234).
  • format --write writes 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 --write writes 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's written lists 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, format leaves 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 ignore and overrides patterns are compiled once instead of for every path (#273).
  • .markdown files 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/wrap reflows 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/wrap wraps the body of a GitHub, Forgejo, or Gitea alert below the line with its marker, as it does for Obsidian callouts (#262).
  • style/wrap reflows a paragraph whose code span goes on in a line that starts with spaces or tabs, and joins the span without them, as style/inline-code does (#288).
  • style/wrap leaves 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, because style/thematic-break now 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 &#91;!TIP], which GitHub renders as an alert. Before, style/wrap joined 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/wrap wraps 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 false refuses 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 textContent reads such a code span as renderers show it (#293).
  • --dialect works with an .mjs configuration that exports a frozen object (#260).
  • --stdin-filepath accepts 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 .gitignore files and input patterns ignore it on Windows and macOS (#261).
  • format stops on an error of the workspace, as lint does, instead of reporting it as engine/unsafe-format, and a plugin named engine, whose findings would pass for the engine's own, is rejected (#261).
  • parse reads a document in the dialect that an alias such as codeberg stands for, so Forgejo's rules apply to it, and the type declarations no longer need @types/mdast from 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/heading keeps 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-newline adds no line ending, and style/block-spacing no blank line, where it would become part of an unclosed HTML or Obsidian comment, and style/final-newline none where it would add a line to a code block without its closing fence; such documents used to be refused (#267).
  • style/wrap leaves 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/wrap lets 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/wrap keeps a paragraph's first line from becoming a link reference definition, a thematic break, or display math instead of refusing the document (#262).
  • style/wrap counts 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-error for its document, and invalid plugin rules and names such as toString are rejected with a clear message (#241).
  • .gitignore files 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/exclude and 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.ignorecase says, 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.json that starts with a byte order mark is read (#259).
  • links/notation decodes 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-only or 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/emphasis and style/strong leave 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, and style/block-spacing no 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-code leaves 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 run continued by an indented build became npm run build, which changed what GitHub shows. The safety check compares code spans the same way (#274).
  • style/inline-code and 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 as npm run build into npm 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/wrap now also reflows a paragraph whose code span has a line break at its edge (#291).
  • style/wrap keeps 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/wrap leaves 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/wrap keeps 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 defaultDialect describes 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.