marked 中强调标记的孤儿嵌套处理:从测试规格em_strong_orphaned_nesting看em/strong的解析规则
【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked
本指南以 marked 仓库中的 em_strong_orphaned_nesting.md 测试规格为切入点,剖析强调与加粗标记在"孤儿嵌套"场景下的解析行为,并结合 Tokenizer.ts 与 Renderer.ts 的源码实现、以及new/目录下同族测试用例(em_strong_adjacent、em_strong_complex_nesting、em_strong_unclosed_orphan、em_strong_multiline等)给出可验证的结论。读完你将掌握:这类边角输入会被解析成什么 HTML、底层 tokenizer 依据什么规则做出选择、以及如何利用new/规格文件回归验证解析行为。
一、规格文件是什么:一句输入、一行断言
test/specs/new/目录存放 marked 的非 CommonMark 官方用例,每个主题由一对同名文件组成:.md为输入 Markdown,.html为期望输出。本次关联文档的规格对只有两行:
输入(em_strong_orphaned_nesting.md):
_**foo_bar**_期望输出(em_strong_orphaned_nesting.html):
<p><em><strong>foo_bar</strong></em></p>
输入串的标记结构非常刁钻:_**foo_bar**_中,开头的_与结尾的_各只有一个,而中间是成对的**,同时bar前还有一个单独的_。按直觉可能存在多种拆分方式,而规格断言了唯一合法输出:外层<em>包裹内层<strong>,内层文本中的foo_bar保持字面量、不再触发强调。这个用例在run-spec-tests.js驱动下作为解析器的回归断言存在,任何对Tokenizer.ts的改动如果改变了这一输出,都会被测试捕获。
二、规格断言了什么:孤儿嵌套的三个事实
将断言与同目录其他用例对照,可以提炼出该规格隐含的三条规则:
1. 内层成对标记优先成组,外层单标记兜底成对输入中**foo_bar**是完整的一对,因此它必然被解析为<strong>foo_bar</strong>;剩下的首尾两个_分别作为开闭定界符,包成一层<em>。这与 em_strong_unclosed_orphan.md 中**a*b*c→<p>**a<em>b</em>c</p>的"孤儿"处理思路一致:未成对的定界符不会强行为周围文本渲染,而是把能成对的部分提取出来。
2. 嵌套时内层文本中的孤立定界符不再参与解析foo_bar中的_已经是内层<strong>的内容,不会引发第二次强调匹配——内层内容在渲染时不会再回到强调定界符匹配流程。
3. 强调与加粗可任意嵌套、顺序不限外层<em>、内层<strong>与 em_strong_multiline.md 的_italic **bold** italic_→<em>italic <strong>bold</strong> italic</em>以及 em_strong_adjacent.md 中*te*__st__→<em>te</em><strong>st</strong>的相邻场景共同说明:marked 的定界符处理不区分em/strong的先后次序,只按定界符配对关系组合嵌套或并列结构。
三、源码级验证:Tokenizer 与 Renderer 的实际路径
3.1emStrong:强调定界符的统一入口
Tokenizer.ts 中emStrong(src, maskedSrc, prevChar)是强调与加粗共用的 inline 分词入口(src/Tokenizer.ts#L762-L785):
emStrong(src: string, maskedSrc: string, prevChar = ''): Tokens.Em | Tokens.Strong | undefined { let match = this.rules.inline.emStrongLDelim.exec(src); if (!match) return; if (!match[1] && !match[2] && !match[3] && !match[4]) return; // ... 对定界符 run 长度与围堵规则的检查 // 关键逻辑:midRun = prevChar === delimChar, // 只有当相邻前一字符不是相同定界符时,才允许该定界符作为开符, // 否则它会"偷走"后续 span 的开符 // (如 `**a*b*c` 必须解析为 `**a<em>b</em>c`)它对*与_使用同一套 run 长度与 flanking 规则判断,因此本规格里**foo_bar**能稳定识别为<strong>内容,而foo_bar内部的_因已被内层定界符包围、不再具备开/关定界符资格,最终以字面文本输出。
3.2em与strong:渲染层的对称封装
在渲染层,Renderer.ts 对两种节点做了对称封装(src/Renderer.ts#L141-L147):
strong({ tokens }: Tokens.Strong): RendererOutput { return `<strong>${this.parser.parseInline(tokens)}</strong>` as RendererOutput; } em({ tokens }: Tokens.Em): RendererOutput { return `<em>${this.parser.parseInline(tokens)}</em>` as RendererOutput; }两个方法都通过parser.parseInline(tokens)递归渲染内部内联 token,这正是"内层内容中的foo_bar不再触发强调匹配"的实现保证:内层<strong>的 tokens 在emStrong分词时已定型,foo_bar作为一个 text token 进入渲染,不会二次进入定界符匹配。
3.3 同族规格的交叉印证
| 规格 | 输入 | 期望输出 | 说明 |
|---|---|---|---|
| em_strong_orphaned_nesting | _**foo_bar**_ | <em><strong>foo_bar</strong></em> | 内外嵌套、内层孤立下划线字面化 |
| em_strong_unclosed_orphan | **a*b*c | **a<em>b</em>c | 未闭合的**不渲染,b内层成对 |
| em_strong_adjacent | *te*__st__等 8 组 | 相邻的<em>/<strong>并列 | em/strong 任意相邻组合 |
| em_strong_complex_nesting | **E*mp****ha****si*s** | 深层混合嵌套 | 多层交叉嵌套的配对顺序 |
| em_strong_multiline | 跨 3 行的_italic **bold** italic_ | 单<em>跨行包裹<strong> | 强调可跨行、内部嵌套保持 |
这些用例共同构成emStrong定界符算法的行为边界,run-spec-tests.js 会批量执行new/下全部.md/.html对,任何解析器改动导致输出漂移都会被 CI 或本地测试立刻发现。
四、如何本地复现与验证
仓库以 TypeScript 源码维护解析器,测试可直接运行:
# 安装依赖(首次) npm install # 运行全部规格测试(含 new/ 目录下 em_strong_* 系列) npm test若只想针对本规格快速验证,可在 Node 环境直接调用:
const { Marked } = require('./lib/marked.cjs'); // 或 import { marked } from './lib/marked.esm.js'(视构建产物而定) console.log(marked.parse('_**foo_bar**_')); // 期望输出:<p><em><strong>foo_bar</strong></em></p>需要注意的是,规格断言基于当前仓库 Tokenizer.ts 与 rules.ts 的实现;不同版本对 flanking/孤儿定界符的处理可能不同,验证时应以本仓库npm test结果为准。
五、小结
em_strong_orphaned_nesting这一对规格文件虽只有一行输入,却精确锁定了一个解析关键行为:当内层成对强调/加粗与外层单定界符嵌套时,marked 优先成组内层标记,并将内层文本中的孤立定界符字面化。通过 Tokenizer.ts 的emStrong与 Renderer.ts 的em/strong实现,可以完整还原从分词到渲染的整条调用链。对想要深入理解或修改强调解析逻辑的开发者而言,test/specs/new/em_strong_*系列规格文件是最直接的行为契约与回归测试集。
【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考