@tangkikodo/mdiff
v0.1.0
Published
Framework-agnostic structural diff for rendered Markdown
Maintainers
Readme
mdiff
Framework-agnostic structural diff for two Markdown strings. The library compares parsed GFM nodes rather than rendered HTML, so changes inside tables, lists, links, and other nested structures remain valid and renderable.
Install
npm install @tangkikodo/mdiffThe package is ESM-only and supports Node.js 20 or newer and modern browsers.
Usage
import { diffMarkdown } from "@tangkikodo/mdiff";
const diff = diffMarkdown(
`
| Service | Status |
| --- | --- |
| API | healthy |
`,
`
| Service | Status |
| --- | --- |
| API | degraded |
| Web | healthy |
`
);
console.log(diff.children[0]?.table?.rows);diffMarkdown() returns data only. It does not emit HTML or depend on a UI
framework.
Model
Every DiffNode has a stable ID for that invocation, its Markdown node type,
its change state, and optional before and after snapshots:
type ChangeType = "unchanged" | "added" | "removed" | "modified";Changed leaf nodes expose text segments. Renderers can map removed
segments to <del> and added segments to <ins>.
Tables expose additional structure:
columnsmaps old and new indexes, including inserted, removed, or moved columns.rowsmaps old and new zero-based row indexes. The header is row0, so body rows start at1.- Added or removed rows assign the same state to every cell in that row.
- Modified cells contain their own recursive Markdown and text diff.
For an inline renderer, iterate the aligned rows and render removed rows before
added rows, using the row or cell state for red/green styling. For a split
renderer, order each side by beforeIndex or afterIndex and use the shared
row and column IDs to align both sides.
Options
diffMarkdown(before, after, {
blockMatchThreshold: 0.35,
tableRowMatchThreshold: 0.3,
tableColumnMatching: "header-similarity",
characterRefinement: true
});- Text is compared by words, with automatic character-level refinement for similar replacements and CJK text.
- Table columns use header similarity by default. Blank headers fall back to positional matching.
- Ordinary block moves are represented as removal plus addition.
- Raw HTML is converted to plain text nodes with
escapedHtml: trueand emits a warning. Consumers must still escape all text when rendering HTML.
Development
npm run check # typecheck + test + build
npm run bench # run benchmarks
npm run demo # build + serve the local visual demo at http://localhost:3000License
MIT © allmonday
