news 2026/9/19 11:50:41

marked 中强调标记的孤儿嵌套处理:从测试规格 `em_strong_orphaned_nesting` 看 `em`/`strong` 的解析规则

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
marked 中强调标记的孤儿嵌套处理:从测试规格 `em_strong_orphaned_nesting` 看 `em`/`strong` 的解析规则

marked 中强调标记的孤儿嵌套处理:从测试规格em_strong_orphaned_nestingem/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_adjacentem_strong_complex_nestingem_strong_unclosed_orphanem_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.2emstrong:渲染层的对称封装

在渲染层,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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 11:49:34

开源数字人DUIX端侧部署实战:从架构拆解到对话定制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 11:47:37

50 行 Python AI Agent 跑 ReAct 循环,模型通道改到 TaoToken 通道行不行

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 11:45:59

桌面通讯型CRM实战:从架构选型到工单联动的完整设计指南

如果只看名字&#xff0c;你可能觉得 DeskcommCRM 又是一个普通的客户管理后台。但实际上&#xff0c;Deskcomm 这个词是 Desktop 和 Communication 的合并写法&#xff0c;翻译过来就是“桌面通讯型 CRM”。这个定位很关键&#xff1a;它把客户资料、跟进记录、通话、IM 聊天、…

作者头像 李华