一个 Markdown 转换器可以相信什么
发布于 2026-11-03 · 最近复核 2026-11-06
Markdown 看起来像纯文本,所以大家都以为转换器是个无害的小东西。它不是:它是「别人写的字」变成「浏览器会照做的标记」的那一步。这一步安不安全,由两个决定说了算。第一个是读的是哪套方言——同一串字符在 GFM 与 CommonMark 里意思不同。第二个是输出里允许出现什么,而那是策略问题,不是解析问题。
这一篇把两件事都讲清楚,也讲那个让转换器「可以被检查」而不只是「被相信」的办法:它把文档写回去、再读一遍。本站的每一次转换都会跑这个检查,而一旦文字真的变了,页面会指出变在哪里。
一个模型、两个解析器、两个写出器
转换器中间只有一份文档模型:Markdown 解析进它,HTML 也解析进它,然后两个写出器把它变回其中一种语言。这就是同一个页面能同时给出两个方向、却不至于变成两台不相干工具的原因;也是审计表无论从哪个方向读进来、描述的都是同一份文档的原因——上面那份样本是 1 个标题、1 个链接、1 张表,在模型里数一次,而不是在两套实现里各数一次。
模型也是「诚实」住的地方。Markdown 表达不了的元素,在读 HTML 的时候就被记成损失,而不是悄悄拆掉;引擎不实现的语法——脚注、定义列表、数学、提醒式引用——被当成它将来会变成的文字报出来,于是它永远不会悄悄变成别的意思。
GFM 与 CommonMark 在哪里不一致
CommonMark 固定的是 Markdown 一直就有的那套语法;GFM 在它上面加了四样东西,而一个不知道这四样被打开了的读取方,会把它们当标点印出来。还是上面那份样本:GFM 给出 1 张真正的表,CommonMark 给出 0 张——竖线表退化成一段文字,竖线又变回内容。这个差别只需要表单上的一个下拉,因为它是读取方的属性,不是文档的属性。
| 构造 | CommonMark | GFM |
|---|---|---|
| 竖线表 | 带竖线的文字 | 一张能对齐的表 |
| 任务列表 | 以方括号开头的列表项 | 一个禁用的复选框 |
| 删除线 | 字面的波浪号 | 一个 del 元素 |
| 裸链接 | 碰巧长得像网址的文字 | 一个链接 |
| 缩进代码 | 四个空格开始一个代码块 | 本引擎公开的偏离:不这样 |
往返,是转换器检查自己的办法
每一次转换都会把文档重新写成 Markdown,再用同一个解析器读一遍,然后比较文字——空白合并之后再比,不比结构。这个选择就是重点:结构可以换写法,因为 _强调_ 可能被写成 *强调*,下划线式标题可能回来时变成井号式;但词一个都不许变。上面那份样本读回来是一致的,而如果不一致,页面会印出两处第一次分岔的位置。
这个检查说的是转换器,不是访客。结果是假,就意味着本仓库某处的转义出了问题,页面上的那句话会这么说。另一个方向也一样:HTML 写成 Markdown、再解析回来、再比较,所以一张被重新排过的表行或列表项,会以「变了几行」的形式出现,而不是悄悄改掉。
信任边界:转义、协议与图片
别人写的 Markdown 是不可信输入,而转换器正是「不可信输入变成标记」的那一步,所以默认值一律取保守的那一侧。原始 HTML 默认被转义,除非访客明确要求放行:<span class="badge"> 送出来的是标记,读者能看见,浏览器不能执行。能执行代码的链接被拒绝,行号印出来、文字留下来——javascript:alert(1) 只留下 1 条问题与一句没有链接的话;而在同一个页面上把开关关掉,目的地会按原样保留(kept)。
判断是在解码、去空白之后的地址上做的,因为浏览器就是这么读的:JaVaScRiPt:、javascript:、以及协议中间夹一个制表符,都会走到同一个处理器。图片与链接不是同一个问题,所以 data:image/... 可以带着像素进来,而本该是图片的地方出现一个 data: 文档时会被拒绝。页面自己也从不把结果显示出来:预览意味着把别人的标记放进这个页面,而那正是上面那些转义要避免的事。
这些数字从哪里来
- 上面每一个计数都是算出来的,不是抄下来的读数。读入样本、把两个方向各写回去、判断链接用的是哪种协议、再比较往返结果,都遵循同一套转换步骤,这一篇里的数字就是这些步骤对那段样本的返回值。Markdown 转 HTML 对访客输入的文字用的是同一套步骤。
- 每次发布都会逐份文档自动核对:往返、每一种能执行代码的协议都被拒绝、原始 HTML 被转义,三个下载也逐字节核对;另有专门的检查把读入与写出钉在它们自己的规矩上。页面上的审计表、报告与 CSV 都由同一批结果生成,三者不可能互相矛盾。
- 两份规格是被点名的,不是改写成更好听的话。两者重叠的地方,跟着 CommonMark;表里那四个构造来自 GFM。而本站公开的另两处偏离——缩进代码与列表续行——印在工具页每一个答案旁边,而不是藏进更新日志里。
这一篇没有说什么
它不声称自己是 CommonMark 实现:引擎实现的是一套被点名的子集,页面把这套子集印出来,而不是暗示其余的部分也有。它也不讲事后清洗 HTML——这里的转义发生在读文档的时候,和一个跑在成品页面上的清洗器是两层东西。它不能告诉你某个具体站点会渲染成什么样:一个宿主可能剥掉表格、改写链接或者拒绝内联图片,而这些从这个页面上一个都看不见。至于引擎不实现的语法——脚注、定义列表、数学——它只说它们将来会变成什么文字,除此之外什么也不承诺。
到 Markdown 转 HTML里转一份;想整理输出,用HTML 格式化与压缩器; 想知道文档到底说了多少字,用字数与字符统计。
以上只是解释——不构成对任何具体站点的安全评审。 阅读完整免责声明。