markdown-parity-check
一个命令行工具和TypeScript库,用于比较页面的HTML和Markdown版本的主要内容。该报告列出了丢失、添加或更改的块,以及不同的编号和链接,每个块都有其源位置。它可以请求一个URL的两个版本,或者读取两个本地文件。
安装
包裹需要 Node.js 22或更新版本. 它是MIT授权的。GitHub Actions在Windows和Ubuntu上测试Node.js 22和24。
运行它而不安装任何东西:
npx --yes markdown-parity-check --url https://example.com/page
全局安装CLI,或者将其作为库添加到项目中:
npm install -g markdown-parity-check
npm install markdown-parity-check
在浏览器中试试
这个 托管Markdown奇偶校验 在turva.dev上运行这个比较。它目前只检查已发布的turva.dev页面,除了两个工具页面,并且该页面在其前两段和地址字段中声明了限制。任何其他站点上的地址都会被拒绝,并显示一条指向命令行工具的消息。
Fill in一个示例按钮将turva.dev自己的工具页面放在地址字段中,Check按钮将比较作为单独的步骤运行。
检查你自己的网站
将地址替换为您的页面。工具请求它两次,与 Accept: text/html 和 Accept: text/markdown. 如果Markdown有自己的地址,请添加 --markdown-url:
npx --yes markdown-parity-check --url https://example.com/page --markdown-url https://example.com/page.md
在CI中,将JSON报告写入文件,并让警告失败。
npx --yes markdown-parity-check --url https://example.com/page --format json --output report.json --strict
要比较两个本地文件,传递两个文件和它们的相对链接的基本URL:
npx --yes markdown-parity-check --html-file page.html --markdown-file page.md --base-url https://example.com/page
退出代码
| 代码 | 意义 |
|---|---|
0 | 比较结束了,没有否定的发现。 |
1 | 比较完成后,发现了拒绝内容或传递方式的差异。 |
2 | 输入、获取、解析或报告错误阻止了可靠的比较。 |
退出码0表示执行的检查没有发现要拒绝的内容。这并不能证明这两个版本的意思是一样的。
选项
| 选项 | 目的 |
|---|---|
--url URL | 从一个URL请求两个版本。 |
--markdown-url URL | 从另一个地址请求降价。需要 --url。 |
--html-file PATH --markdown-file PATH | 而是比较两个本地文件。 |
--base-url URL | 解析本地文件中的相对链接。仅支持本地文件模式。 |
--selector CSS | 选择HTML内容容器。 |
--html-profile generic 或 --html-profile starlight | 使用通用的HTML规则,这是默认的,或者Starlight规则,它们也会比较非活动选项卡面板,并保留Expressive Code换行符。 |
--front-matter keep 或 --front-matter strip | 保留Markdown前事项,这是默认的,或者在比较之前删除它。 |
--format text 或 --format json | 打印可读的报告(默认值)或结构化报告。 |
--output PATH | 将报告写入文件。输入文件永远不会被覆盖。 |
--strict | 将警告视为拒绝调查结果。 |
--timeout-ms MS | 每个请求以及解析和比较步骤的截止日期。默认值是15 000毫秒,通过它的运行将以退出代码2停止。十进制正整数,取值范围不大于2147483647。 |
--max-bytes N | 每个响应的解码字节限制。缺省值是5mib。取值必须为不大于9007199254740991的十进制正整数。 |
--help, -h | 打印帮助。 |
--version, -V | 打印版本。 |
超出这些范围的值,或者不是普通的数字运行,将停止运行并显示退出代码2和错误消息。
示例输出
这是此存储库中匹配fixture对的文本报告,在2026-11-04上以版本0.2.31运行。确切的命令在下面的开发部分中。文本报告最后列出了一些限制,在此省略:
markdown-parity-check 0.2.31 (offline mode)
Result: PASS (0 error(s), 0 warning(s), 1 info)
HTML: test/fixtures/same/page.html (file, 1,017 bytes, base https://example.com/page)
Markdown: test/fixtures/same/page.md (file, 514 bytes, base https://example.com/page)
Extraction: HTML via main (high confidence), 9 block(s); Markdown 9 block(s).
note: Front matter (3 lines) was stripped as requested (--front-matter strip).
Coverage: 9/9 HTML block(s) aligned (100 %), 9/9 Markdown block(s) aligned (100 %).
Findings (1):
1. [info] MARKDOWN_FRONT_MATTER_STRIPPED: Lines 1-3 were stripped as front matter (--front-matter strip) and not compared.
Markdown line 1 "title: Agent readiness audit"
比较什么
HTML容器
没有 --selector,该工具查找一个可见的 main,然后是可见的 article,然后是可见的 [role=main],并使用具有1的第一种匹配中的第一个匹配。
- 当候选对象和父对象都不隐藏时,在模板中或在下面列出的遗漏子树中,候选对象是可见的
article在一个nav没有使用。 - 一个
form一个例外吗main在一个form这个没有遗漏的角色仍然被使用,无论表单位于多高的位置。 - 如果它们都不存在,则使用页面主体,并且报告警告页面chrome可能会泄漏。
- 相同类型的两个不包含彼此的可见元素也会发出警告,因为只比较第一个元素。
默认情况下,前端内容保留,并作为内容进行比较。如果HTML不包含相同的文本,则仅凭这一点就无法进行比较。当Markdown开始使用看起来像YAML前端内容的块时,该工具还会发出警告 --front-matter strip 删除它。
块和严重性
标题、段落、列表项、表格、代码块和链接都是逐块比较的。将包含块元素的链接(例如带有标题和段落的链接卡片)与这些块进行比较:每个块都保持自己的类型和标题级别并携带链接,Markdown侧的读取方式相同,因此 ## [Title](url) 是带有链接的标题。在表格单元格中,链接是作为单元格文本的一部分读取的。内容标记 hidden 或 aria-hidden="true" ,当原始HTML包装器(如 <div hidden> 在一个Markdown区打开,在另一个Markdown区关闭。表标题算作表前的一个段落。
- 错误:丢失和添加的块,文本或数字的更改,更改的表或链接,以及显示另一个数字或不同的已选中或未选中状态的列表项。
- 警告:一个只出现在一侧的复选框,一个顺序上的差异,对于只在大小写、标点符号或排版字符上移动和改变的块,以及标题级别、大小写、标点符号、列表类型或嵌套级别的差异,也会报告。Markdown中的每个脚注都是一个警告,因为脚注不会进行比较。这些是主要的案例
code每个发现都有一个确切的规则。重复块的丢失或额外副本被报告为丢失或添加的块,这是一个错误。 - 信息:每个跳过的比较,其中没有失败的检查,即使没有
--strict。
当其中一个单元格的rowspan或colspan大于1、rowspan为0或值为空或不以数字开头时,将跳过表的逐单元格比较。这个数字是从值的前导数字中读取的,所以 2abc 算2。colspan为0,rowspan或colspan为1不会触发它。这种表格的链接和标题仍然是比较的。反向列表的编号也不进行比较。
遗漏的内容
有些内容即使在主内容中也会被遗漏,在代码块中也是如此。这些是的子树:
form,button,input,select,textarea,dialog,iframe,svg,canvas,object,embed和map。nav,script,style,noscript和template。- 任何具有角色的元素
navigation,banner,contentinfo,complementary,search,menu,menubar或dialog。 - 一个
header或footer它就在正下方body或html,除非是内容容器本身。
这个 starlight 概要文件保留一个具有源和标题的iframe作为链接。同样的列表适用于Markdown中的内联原始HTML和任务列表项的复选框,该复选框必须属于项目本身。
在降价段落中被省略掉的标记,如 <button> 或者一个 <script>在同一段中跳过与其匹配的结束标记。没有结束标记的标记将作为警告报告,其文本将作为可见比较。void标签 input 和 embed 不需要结束标记,可以自己跳过。一个自闭 <svg/> 与HTML中一样,被视为封闭的,并且 <button/> 没有。
一个 <br> 在代码块中有一个换行符。只有空格不同的代码块是一个警告,其中空格是指ASCII空白字符和Unicode空格分隔符,如不间断空格。代码中的任何其他差异,包括软连字符等不可见字符,都是错误的。图像仅通过其alt文本进行比较,而不是通过其地址进行比较,因此具有相同alt文本的两个不同图像比较相等。
URL方式交付
在URL模式下,首先检查Markdown响应。HTTP错误或代替Markdown的HTML页面不通过检查,不会比较任何内容。Markdown响应标记为 text/plain 是警告,还有其他内容类型吗 text/markdown 或 text/x-markdown 是一个错误。如果HTML请求本身返回HTTP错误,则运行以退出代码2结束。
方法的局限性
JavaScript不会被执行。页面在浏览器中构建的内容在服务器发送时进行比较。块匹配是启发式的,因此一些布局需要显式选择器。报表屏蔽URL查询值和片段,并从URL中删除任何用户名或密码。摘要可能仍然包含私有页面内容。在共享报告之前,请先查看报告。
命令行和托管页面
两者都使用这个包的比较核心。它们的不同之处在于它们到达的页数和它们的极限:
| 主题 | 命令行工具 | 主持页面 |
|---|---|---|
| 页面 | 任何公共HTTP或HTTPS地址,或两个本地文件。 | 只发布了turva.dev页面,除了两个工具页面,这个页面和llms.txt验证器。 |
| 抓取 | 来自您机器的网络请求。非公共地址也被拒绝,仅次于DNS和重定向。该工具直接连接到已验证的地址,并且不使用 HTTP_PROXY 或 HTTPS_PROXY因此,在强制代理后面使用文件模式。 | dev Worker自己呈现两个版本,因此检查不通过网络发送请求。 |
| 响应大小 | 缺省情况下,每个响应5mib,可通过 --max-bytes。 | HTML高达512 KiB。降价至128千元。 |
| 比较大小 | 最多4 000 000个块对。 | 多达250,000个块对。 |
| 率 | 不受工具限制。 | 每分钟从每个Cloudflare位置的一个IP地址进行大约10次检查。 |
托管页面还会响应页面上描述的JSON POST。它运行这个固定的包的释放 turva-worker,它可能比最新的NPM版本更老。该报告的 toolVersion Field显示了哪个版本产生了它。
有些限制不在表中:
- 包含用户名或密码的URL即使在其主机是公共的情况下也会被拒绝,对于重定向目标也是如此。
- 工具发送
Accept-Encoding: gzip, deflate, br并对没有内容编码的响应进行解码,identity,gzip,x-gzip,deflate或br. 任何其他Content-Encoding的响应,zstd例如,被拒绝,并且运行以退出码2结束。 - 命令行工具最多遵循5个重定向。
- 当元素嵌套深度超过1024级时,在HTML页面中,在Markdown内部编写的原始HTML块中,或者在Markdown自己的结构中,提取将以退出代码2停止。
- 当必须考虑超过1,000,000个相似的块对时,匹配以同样的方式停止。图书馆
limits默认为相同的两个值。 - 托管页面使用较低的嵌套和匹配限制:512个级别和50,000个类似块对。
托管检查也可能在turva.dev自己的页面上失败。在2026-09-11检查 https://turva.dev/tools 返回 fail 有五个错误。Markdown带有一个Related标题和四个HTML页面遗漏的链接,报告列出了它们中的每一个。相同的四个目标是该页的工具卡中的链接,因此报告发现了一个缺失的结构,而不是缺失的内容。2026-10-02页面再次通过。 HTML和Markdown可能不同意 读取结果为full。
使用from Node和TypeScript
该包是一个用TypeScript编写的ES模块,并附带了它的类型声明。库项导出比较结果,而不需要命令行界面、文件阅读器或网络代码。它不导入Node.js内置模块,这就是托管页面将其捆绑到Cloudflare Worker中的方式。
import { run, renderJson } from 'markdown-parity-check';
import type { Report, SourceInput } from 'markdown-parity-check';
// htmlText and markdownText are the two documents you already fetched or read.
declare const htmlText: string;
declare const markdownText: string;
const base = 'https://example.com/page';
const source = (file: string, body: string): SourceInput => ({
meta: { kind: 'file', file, bytes: new TextEncoder().encode(body).length, baseUrl: base },
body,
base,
});
const report: 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) 返回 Report 对象。 renderJson(report) 打印出来 --format json 可以,但URL查询值、片段和凭据被屏蔽。的对象 run return不被屏蔽。一次完整的运行就得到了结果 pass 退出码为0或 fail 出口代码是1。当比较无法完成时,例如由于内容为空或超出了限制, run 抛出一个 RunError。 errorReport 生成在这种情况下命令打印的错误报告。
选项类型为 RunOptions. 它需要 selector, frontMatter, strict, mode 和 htmlProfile,也就是 generic 或 starlight. 星光规则在 文档/ starlight.md. 报告记录了 mode 作为 url 或 offline,它不会打开或关闭任何检查开关。
当Markdown源有 kind: 'url' 在其 meta时,交付检查首先运行并读取HTTP status 和 contentType 从那 meta。
CPU或内存预算较小的主机可以通过较低的分数 limits,输入如下 RunLimits: maxAlignmentPairs, maxSimilarityCandidates, maxSimilarityWork 和 maxNestingDepth. 嵌套限制也适用于Markdown中的原始HTML块。
调用者获取页面并决定允许哪些地址。该命令应用的地址策略导出为 assertPublicHost 和 isPublicAddress。
报告格式
JSON报告是 Report 类型。它的主要领域是:
| 场 | 内容 |
|---|---|
schemaVersion | 数量 1。 |
toolVersion | 发布报告的新闻稿。 |
mode | url 或 offline。 |
sources | HTML和Markdown输入的元数据。 |
extraction | 双方是如何被提取出来的。它是 null 在错误报告和交付检查失败的结果中,因为没有报告提取结果。 |
summary | result,也就是 pass, fail 或 error,然后 strict,计数 errors, warnings 和 infos, coverage,也就是 null 当没有比较时, exitCode ,当 result 是 error, error 有时 errorValue。 |
findings | 以下是 Finding 对象,每个都有 code, severity, direction 和 message,以及他们在哪里应用 html 和 markdown 地点和 before 和 after 文本。 |
limitations | 固定的清单,上面列有支票不包括的项目。 |
发展
源代码在TypeScript下面 src/,由 tsc 来 dist/. 克隆存储库,构建它并运行捆绑的示例:
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
该示例比较匹配的内容和代码0的退出。它显式地删除Markdown文件的前端内容。没有 --front-matter strip 同一对失败了,因为前面的内容被作为内容进行比较。
npm run typecheck 运行 tsc --noEmit。 npm test 先构建,然后运行测试 dist/。
发布过程
在CI通过主分支之后,发布工作流 .github/workflows/release.yml 打包一次tarball,并在版本尚未发布时通过OIDC可信发布将其发布到npm。然后,它下载注册表副本,并根据打包的文件检查其完整性哈希值及其内容。不存储npm令牌。
只有在检查之后,它才会创建带有已验证tarball附件的GitHub版本。发行说明是CHANGELOG。版本的Md部分,所以没有一个的版本会停止发布。存在于npm上但内容不同的版本也会停止发布。npm也打包了这个README,所以对它的修改只会在一个新版本中到达npm页面。
验证发布
GitHub Actions以npm可信发布的方式发布从0.1.2开始的每个版本,每个版本都带有一个来源证明。要检查,请将其安装到一个空目录中,并要求npm验证签名。将版本替换为需要检查的版本。
mkdir verify-mpc && cd verify-mpc && npm init -y && npm install [email protected] --ignore-scripts && npm audit signatures
npm audit signatures 检查具有注册表签名的每个已安装包的注册表签名和来源证明。这个包的证明列出了三个东西,以便与您期望的东西进行比较:存储库 github.com/erekola/markdown-parity-check,工作流文件 .github/workflows/release.yml 以及生成tarball的提交。npm版本页面将它们显示在出处下面。标签 v0.2.31 必须指向同一个提交,并且 gh api repos/erekola/markdown-parity-check/commits/v0.2.31 --jq .sha 打印它。不匹配是不使用该版本的原因。出处证明哪个工作流构建了一个发布,从哪个提交。它不能证明代码是安全的,因此阅读源代码和依赖项仍然是您的工作。
安全
私下向 [email protected]. 看到 SECURITY.md 报告说明和操作限制。
许可证
麻省理工学院。看到 许可证。