marked 的 Markdown 解析边界:从 docs/broken.md 看引擎差异与列表/引用块实现原理
2026/9/19 17:48:29 网站建设 项目流程

marked 的 Markdown 解析边界:从 docs/broken.md 看引擎差异与列表/引用块实现原理

【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked

本文以 docs/broken.md 为核心,系统梳理各 Markdown 引擎(markdown.pl、markdown.js、sundown/upskirt、discount 等)在列表、代码块、引用块、HTML 块等解析上的行为差异,并对照 marked 的实际输出与 src/Tokenizer.ts、src/rules.ts 的源码实现,解释 marked 为何在这些边界场景中给出更合理的结果。读完本文,你将理解 Markdown 解析中缩进、嵌套与惰性续行的底层规则,掌握用 marked CLI 复现这些差异的验证方法,并了解仓库测试是如何固化这些行为的。

一、为什么会有 broken.md 这份文档

docs/broken.md是 marked 作者多年来收集的 "markdown 引擎怪癖" 笔记。它的价值不在于规范 Markdown 语法,而在于展示一个事实:在没有统一规范(CommonMark 规范直到 2014 年才发布首个版本)的年代,不同引擎对同一段输入的解析结果可以天差地别,甚至产生非法的 HTML。

文档明确指出,许多例子只拿某个引擎与 marked 对比,但 markdown.pl 的例子几乎可以原样套用到 discount、upskirt 或 markdown.js 上,而且会暴露出更多不一致。作者的写作背景是"对引擎间不一致感到非常不满",因此文中语气带有情绪化表达,但这并不影响其技术价值——它是一份难得的引擎行为差异对照表

从 package.json 可知,marked 是 "A markdown parser and compiler. Built for speed.",其命令行入口是bin/marked.js(由 bin/main.js 实现)。本文所有 marked 输出均可通过npx marked(从 stdin 读入、Ctrl-D 结束)复现。

二、列表解析的"愚蠢"示例:缩进感知是分水岭

2.1 例一:列表项间的文本归属

文档第一个例子,输入为:

* item1 * item2 text
  • markdown.pl 输出<li><p>item1</p> ... <p><p>text</p></li>,产生了<p><p></ul></p>这类错位的嵌套标签,HTML 结构非法;
  • marked 输出<li><p>item1</p><ul><li>item2</li></ul><p>text</p></li>,结构完整闭合。

差异根源在于缩进感知(indentation-aware)解析。在 src/Tokenizer.ts 的list()方法中,marked 通过line.search(nonSpaceChar)找到首行第一个非空白字符,以此计算indent,再决定后续行归属哪个列表项;而 markdown.pl 基于正则逐行扫描,遇到缩进就"断片",最终把text错误地并入了前一个<li>

2.2 例二:列表项内嵌引用块

输入:

* hello > world
  • markdown.pl<p><ul><li>hello</p><blockquote><p>world</li></ul></p></blockquote>——<ul>出现在<p>内、</blockquote>出现在</ul>外,完全错位;
  • sundown(upskirt)<li>hello\n&gt; world</li>——把> world当成普通文本并转义,根本不识别引用块
  • marked<ul><li>hello<blockquote><p>world</p></blockquote></li></ul>,引用块正确嵌套在列表项内。

marked 的实现依据在 src/Tokenizer.ts:列表项解析循环中专门检查blockquoteBeginRegex(定义于 src/rules.ts),一旦发现^ {0,indent}>开头的行就结束当前列表项、将引用块交给 blockquote 处理。仓库中的测试用例 test/specs/new/blockquote_list_item.md 第一行就写着 "This fails in markdown.pl and upskirt",其后输入正是* hello+> world,说明该项目把这类边界场景固化为回归测试。

2.3 例三:代码块缩进的两难

输入(缩进 6 空格):

* hello * world * hi code
  • markdown.plcode没有变成代码块,而是被吞进<li>hi\n code</li>
  • 再增加两个空格(8 空格,超过常见的 4 空格缩进规则)后,markdown.pl 依然不识别代码块,且第三个列表项hi甚至没有被解析为独立列表项——这正是文档所说的"indentation unaware parsing"
  • marked<pre><code>var a = 1;</code></pre>正确生成代码块。

关键在 src/Tokenizer.ts 的这行注释与逻辑:

indent = line.search(this.rules.other.nonSpaceChar); // Find first non-space char indent = indent > 4 ? 1 : indent; // Treat indented code blocks (> 4 spaces) as having only 1 indent

marked 把超过 4 空格的首行缩进按"代码块"处理(缩进计为 1),从而允许代码块在列表项中以合理的方式出现。文档在此处的反问:"Why shouldn't code blocks be able to appear in list items in a sane way?" 正是 marked 的设计取向。而 src/Tokenizer.ts 中>= 4的 indented code block 分支,则为列表项内嵌代码块提供了第二个层次的判断。

2.4 例四:复杂嵌套列表

输入:

* hello * world how are you * today * hi
  • markdown.plhow被吞入world项、are you被当作列表外层段落、todayhello同级错乱;
  • markedworld/howare/you各自成段、today正确成为hello项的二级列表兄弟项、hi成为一级列表兄弟项,结构完全符合直觉。

这与 src/Tokenizer.ts 的列表项收集循环有关:marked 使用nextBulletRegex(indent)hrRegex(indent)fencesBeginRegex(indent)等一组按当前缩进动态生成的正则(见 src/rules.ts 附近的cachedIndentRegex工具),逐行判断后续行应归属、跳出还是开启新块,从而保持嵌套结构的正确闭合。

三、引用块的歧义:markdown.js 的三个翻车现场

3.1 连续引用块被吞并

输入:

> a > b > c
  • markdown.js<blockquote><p>a</p><p>bundefined&gt; c</p></blockquote>——第二个引用块的开头>被吞成文本,还莫名输出undefined
  • marked:输出三个相互独立<blockquote>,每个含一段<p>

marked 的引用块实现在 src/Tokenizer.ts 的blockquote():先按blockquoteStart(src/rules.ts,^ {0,3}>)切分连续引用行,若遇到空行间断则停止收集、返回当前块,从而保证相邻引用块互不干扰。若引用块后面紧跟列表,还会在 src/Tokenizer.ts 走 "include continuation in nested list" 分支做合并处理。

3.2 图片嵌套链接解析

输入:

an image
  • markdown.js<a href="/image)](/link">an image</a>——把)和 `` 会按imagelink的优先级在括号匹配完整的前提下逐层解析),](结构不会被错误消耗。文档末尾附有对应 issue(markdown-js#24/#27 等)的链接,属于历史佐证。

3.3 行内 HTML 块的直通

输入:

<div>hello</div> <span>hello</span>
  • markdown.js:把<div><span>都转义成&lt;div&gt;文本;
  • marked<div>hello</div>原样输出(作为 block-level HTML 块直通),<span>hello</span>则包进<p>

这源于 src/Tokenizer.ts 的html()方法:marked 使用 src/rules.ts 中_tag定义的 block-level 标签清单(address|article|aside|base|basefont|blockquote|body|caption|...|div|...),命中则产生type: 'html'block: true的 token 原样透传;而<span>不在块级清单内,退回普通行内 HTML 处理并包裹<p>

四、深入源码:这些行为是"设计"而非"巧合"

将上文现象对照源码,可以总结出 marked 在列表与引用块上的三条核心设计:

  1. 缩进即结构list()indent的计算(src/Tokenizer.ts)贯穿整个列表项收集循环,缩进决定行归属、决定是否开启代码块/引用块/新列表项;
  2. 块级中断(interrupt)规则:列表项循环中按顺序检查 fences、heading、html、blockquote、新 bullet、hr 的起始正则(src/Tokenizer.ts),任何一种命中都会结束当前列表项,交由对应 tokenizer 处理——这与 src/rules.ts 中lheading_paragraph等规则里blockquote/list/html可中断段落的设定一脉相承;
  3. 引用块内部按顶层重解析blockquote()剥离>前缀后,调用this.lexer.blockTokens(currentText, tokens, true)且临时置state.top = true(src/Tokenizer.ts),将引用内容当作顶层 token 流重新解析,因此引用块内的列表、嵌套引用、代码块都能获得与正文一致的解析结果。

仓库测试目录 test/specs/new/ 中,除了上文提到的 blockquote_list_item.md,还有 nested_blockquote_in_list.md(覆盖引用块作为列表项子级/兄弟级/父级三种嵌套位置)、adjacent_lists.md、tricky_list.md 等,共同构成对列表/引用块边界行为的回归保障。这些.md文件与同名.html文件一一对应(如 blockquote_list_item.html),由 test/run-spec-tests.js 驱动比对,任何解析回归都会在 CI 中暴露。

五、动手复现:用 marked CLI 验证引擎差异

文档中的对照均在 shell 中完成,你可以用相同的流程亲手验证:

# 以第一个列表示例为例,从 stdin 读入,Ctrl-D 结束 npx marked * item1 * item2 text ^D # 输出应为: # <ul> # <li><p>item1</p> # <ul> # <li>item2</li> # </ul> # <p>text</p> # </li> # </ul>

若本地已安装 marked(bin字段指向bin/marked.js),也可直接调用:

printf '* hello\n > world\n' | ./bin/marked.js # <ul><li>hello <blockquote><p>world</p></blockquote></li></ul>

想要观察 token 流而非 HTML,可使用 bin/main.js 提供的--tokens能力(输出JSON.stringify(marked.lexer(data, options), null, 2)),它会把listblockquotecode等 token 及looseorderedstart等元信息打印出来,便于理解 marked 是如何对上述输入分层的。

六、小结:从 broken.md 到健壮解析

docs/broken.md收集的"怪癖"在今天看来,多数已被 CommonMark 规范收敛,但它的方法论依然有效:用边界输入去戳穿引擎的实现假设。对照 marked 的 src/Tokenizer.ts 与 src/rules.ts 可以看到,marked 对列表缩进、块级中断、引用块重解析的处理是显式设计的,并且通过 test/specs/new/ 下成对的.md/.html用例固化为可回归的契约。

如果你的业务场景需要把用户输入的 Markdown 渲染成可信的 HTML(尤其是列表、引用、代码块混排的富文本),理解这些边界行为能帮你预判渲染结果、规避 XSS 或结构错乱风险,并在必要时通过 docs/USING_ADVANCED.md 所述的扩展机制定制解析行为。

【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询