Skip to content

Repository files navigation

markdown-parity-check

Compare the main content of a page's HTML and Markdown versions. The report lists missing, added and changed blocks, numbers and links, each with its source location. It can request both versions of one URL or read two local files.

Try it in the browser

The hosted Markdown parity check runs this comparison on turva.dev. It currently checks the published pages of turva.dev only, and the page states that limit in its opening paragraph and again at the address field. An address on any other site is refused with a message that points to the command-line tool below. Fill in an example puts turva.dev's own tools page in the address field, and Check runs the comparison as a separate step.

Check your own site

Use Node.js 22 or newer. CI tests Node.js 22 and 24 on Windows and Ubuntu. npx runs the published package without a global install:

npx --yes markdown-parity-check --url https://example.com/page

Replace the address with your page. The tool requests it twice, with Accept: text/html and with Accept: text/markdown. If the Markdown has its own address, add --markdown-url:

npx --yes markdown-parity-check --url https://example.com/page --markdown-url https://example.com/page.md

In CI, write the JSON report to a file and let warnings fail the step as well:

npx --yes markdown-parity-check --url https://example.com/page --format json --output report.json --strict

To compare two local files, pass both and a base URL for their relative links:

npx --yes markdown-parity-check --html-file page.html --markdown-file page.md --base-url https://example.com/page

Exit codes

Code Meaning
0 The comparison completed without a rejecting finding.
1 The comparison completed and found rejecting content or delivery differences.
2 An input, fetch, parse or report error prevented a reliable comparison.

Exit code 0 means the implemented checks found nothing to reject. It does not prove that the two versions mean the same thing.

Options

Option Purpose
--url URL Request both versions from one URL.
--markdown-url URL Request the Markdown from a separate address. Needs --url.
--html-file PATH --markdown-file PATH Compare two local files instead.
--base-url URL Resolve relative links in local files. Local-file mode only.
--selector CSS Choose the HTML content container.
--html-profile generic or --html-profile starlight Use the generic HTML rules, which is the default, or the Starlight rules, which also compare inactive tab panels and keep Expressive Code line breaks.
--front-matter keep or --front-matter strip Keep Markdown front matter, which is the default, or remove it before the comparison.
--format text or --format json Print a readable report, which is the default, or a structured one.
--output PATH Write the report to a file. An input file is never overwritten.
--strict Treat warnings as rejecting findings.
--timeout-ms MS Deadline per request. The default is 15 000 ms. The value must be a positive decimal integer no greater than 2147483647.
--max-bytes N Limit on decoded bytes per response. The default is 5 MiB. The value must be a positive decimal integer no greater than 9007199254740991.
--help, --version Print the help or the version.

A value that is outside these ranges, or that is not a plain run of digits, stops the run with exit code 2 and an error message.

What is compared

Without --selector, the HTML content comes from the first visible main, article or [role=main]. A candidate is visible when neither it nor an ancestor is hidden, in a template or inside one of the left-out subtrees listed below, so an article inside a nav is not used. A form is the one exception, so a main inside a form that has no left-out role is still used, however far up the form sits. If none of them exists, the page body is used and the report warns that page chrome may leak in. Two visible elements of the same kind that do not contain one another also give a warning, because only the first is compared. Front matter is kept by default and compared as content. If the HTML does not carry the same text, that alone fails the comparison. The tool also warns when the Markdown starts with a block that looks like YAML front matter, and --front-matter strip removes it.

Headings, paragraphs, list items, tables, code blocks and links are compared block by block. Content marked hidden or aria-hidden is left out on both sides, also when a raw HTML wrapper such as <div hidden> is opened in one Markdown block and closed in a later one, and a table caption counts as a paragraph before the table. Missing and added blocks are errors. A change in text or numbers is an error too, as is a changed table or link, and a list item that shows another number or a different checked or unchecked state. A checkbox that is present on one side only is a warning, and so is a difference in order, which is also reported for a block that moved and was edited slightly, and a difference in heading level, case, punctuation, list kind or nesting level. Footnotes in the Markdown are not compared. Each one is reported as a warning, and repeated blocks are reported too. The cell-by-cell comparison of a table is skipped when one of its cells has a rowspan or colspan above 1, a rowspan of 0, or a value that is empty or does not begin with a number. The number is read from the value's leading digits, so 2abc counts as 2. A colspan of 0 and a rowspan or colspan of 1 do not trigger it. The links and the caption of such a table are still compared. The numbering of a reversed list is not compared either. Each skip is reported as an information finding, which does not fail a check, not even with --strict.

Some content is left out even when it sits inside the main content, in code blocks too: the subtrees of form, button, input, select, textarea, dialog, iframe, svg, canvas, object, embed and map, of nav, script, style, noscript and template, and of any element with the role navigation, banner, contentinfo, complementary, search, menu, menubar or dialog. The starlight profile keeps an iframe that has a source and a title, as a link. The same list applies to inline raw HTML in the Markdown and to the checkbox of a task list item, which must belong to the item itself. A left-out tag in a Markdown paragraph, such as a <button> or a <script>, is skipped through its matching closing tag in the same paragraph, and one with no closing tag is reported as a warning and its text is compared as visible. Only a self-closing <svg/> counts as closed by itself, as in HTML. A <br> in a code block is a line break. A code block that differs only in whitespace is a warning, where whitespace means the ASCII whitespace characters and the Unicode space separators such as the no-break space. Any other difference in code, an invisible character such as a soft hyphen included, is an error. An image is compared by its alt text only, not by its address, so two different images with the same alt text compare equal.

In URL mode the Markdown response is checked first. An HTTP error or an HTML page in place of Markdown fails the check, and nothing is compared.

JavaScript is not executed. Content that a page builds in the browser is compared as the server sent it. Block matching is heuristic, so some layouts need an explicit selector. Reports mask URL query values and fragments and remove any user name or password from a URL. Excerpts may still contain private page content. Review a report before you share it.

Command line and hosted page

Both use the comparison core of this package. They differ in which pages they reach and in their limits:

Topic Command-line tool Hosted page
Pages Any public HTTP or HTTPS address, or two local files. Published turva.dev pages only.
Fetching Network requests from your machine. Non-public addresses are refused, also behind DNS and redirects. The turva.dev Worker renders both versions itself, so a check sends no request over the network.
Response size 5 MiB per response by default, adjustable with --max-bytes. HTML up to 512 KiB. Markdown up to 128 KiB.
Comparison size Up to 4 000 000 block pairs. Up to 250 000 block pairs.
Rate Not limited by the tool. About 10 checks per minute from one IP address at each Cloudflare location.

The hosted page also answers a JSON POST, described on the page. It runs the release of this package pinned in turva-worker, which can be older than the latest npm release. The report's toolVersion field shows which release produced it.

Some limits are not in the table. A URL that contains a user name or password is refused even when its host is public, and the same holds for a redirect target. The tool sends Accept-Encoding: gzip, deflate, br and decodes responses with no Content-Encoding, identity, gzip, x-gzip, deflate or br. A response with any other Content-Encoding, zstd for example, is rejected, and the run ends with exit code 2. The command-line tool follows at most 5 redirects. When elements are nested more than 1024 levels deep, in the HTML page or in a raw HTML block written inside the Markdown, the extraction stops with exit code 2. The matching stops the same way when more than 1 000 000 similar block pairs would have to be considered, and the library limits default to the same two values.

A hosted check can fail on turva.dev's own pages as well. On 2026-09-11 the check of https://turva.dev/tools returned fail with five errors. The Markdown carries a Related heading and four links that the HTML page does not repeat as a list, and the report listed each of them. The same four targets are links inside that page's tool cards, so the report found a missing structure and not missing content. HTML and Markdown can disagree reads that result in full.

Library use

The package also exports the comparison without the command-line interface, the file reader or the network code. The library entry imports no Node.js built-in modules, which is how the hosted page bundles it into a Cloudflare Worker.

import { run, renderJson } from 'markdown-parity-check';

// htmlText and markdownText are the two documents you already fetched or read.
const base = 'https://example.com/page';
const source = (file, body) => ({
  meta: { kind: 'file', file, bytes: new TextEncoder().encode(body).length, baseUrl: base },
  body,
  base,
});

const report = run(source('page.html', htmlText), source('page.md', markdownText), { strict: false, mode: 'offline' });
console.log(report.summary.result, report.summary.exitCode);
console.log(renderJson(report));

run(html, markdown, options) returns the report object that --format json prints. A completed run has the result pass with exit code 0 or fail with exit code 1. When the comparison cannot finish, for example because the content is empty or a limit is exceeded, run throws a RunError. errorReport builds the error report the command prints in that case.

The options are selector, frontMatter, htmlProfile (generic or starlight, see docs/starlight.md), strict and mode. The report records mode as url or offline, and it does not switch any check on or off. When the Markdown source has kind: 'url' in its meta, the delivery check runs first and reads the HTTP status and contentType from that meta. A host with a smaller CPU or memory budget can pass lower limits: maxAlignmentPairs, maxSimilarityCandidates, maxSimilarityWork and maxNestingDepth. The nesting limit also applies to raw HTML blocks inside the Markdown. The caller fetches the pages and decides which addresses are allowed. The address policy the command applies is exported as assertPublicHost and isPublicAddress.

Run from source

git clone https://github.com/erekola/markdown-parity-check.git
cd markdown-parity-check
npm ci --ignore-scripts
npm run build
node dist/src/cli.js --html-file test/fixtures/same/page.html --markdown-file test/fixtures/same/page.md --base-url https://example.com/page --front-matter strip

The bundled example compares matching content and exits with code 0. It removes the Markdown file's front matter explicitly. Without --front-matter strip the same pair fails, because the front matter is compared as content.

Development

Run npm run typecheck and npm test. GitHub Actions tests Node.js 22 and 24 on Windows and Ubuntu. After CI passes on the main branch, the release workflow packs the tarball once, publishes it to npm when the version is not there yet, downloads the registry copy and checks its integrity hash and its content against the packed file. Only then does it create the GitHub release with that verified tarball attached, and the release notes are the CHANGELOG.md section of that version, so a version without one stops the release. A version that exists on npm with different content stops the release. npm packs this README too, so a change to it reaches the npm page only with a new version.

Verify a release

GitHub Actions publishes every version from 0.1.2 on with npm trusted publishing, and each one carries a provenance attestation. To check one, install it in an empty directory and ask npm to verify the signatures. Replace the version with the one you want to check.

mkdir verify-mpc && cd verify-mpc && npm init -y && npm install markdown-parity-check@0.2.23 --ignore-scripts && npm audit signatures

npm audit signatures checks the registry signature and the provenance attestation of each installed package that has one. The attestation of this package names three things to compare with what you expect: the repository github.com/erekola/markdown-parity-check, the workflow file .github/workflows/release.yml and the commit that produced the tarball. The npm version page shows them under Provenance. Tag v0.2.23 must point at that same commit, and gh api repos/erekola/markdown-parity-check/commits/v0.2.23 --jq .sha prints it. A mismatch is a reason not to use that version. Provenance proves where a release was built and from which commit. It does not prove that the code is safe, so reading the source and the dependencies stays your job.

Security

Report vulnerabilities privately to info@turva.dev. See SECURITY.md for the reporting instructions and the operating limits.

License

MIT. See LICENSE.

About

Compare HTML and Markdown page content from the CLI. Report changed text, numbers, links and blocks with source locations and JSON output.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages