marked 如何识别并拒绝无效 HTML 标签:深入解析 html_invalid_tag_name 规范测试
【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked
导读
本文以 marked 仓库中 html_invalid_tag_name.md 这份规范测试规格为切入点,完整剖析 marked(一个面向性能设计的 Markdown 解析与编译器)如何判定 HTML 标签名的合法性:哪些"貌似标签"的输入会被当作真实 HTML 原样输出,哪些会被识别为无效标签而转义为普通文本。读完本文,你将掌握 marked 块级与行内 HTML 识别的双轨正则机制、标签名合法字符集约束,以及一个 spec 测试用例从输入到期望输出的完整判定链路。
一、测试规格原文:三组"无效标签"输入
该规格位于 test/specs/new/html_invalid_tag_name.md,输入仅有三组"伪标签":
foo <bar_baz> qux foo </a:b> qux <div_>其对应的期望输出记录在同目录的 html_invalid_tag_name.html 中:
<p>foo <bar_baz> qux</p> <p>foo </a:b> qux</p> <p><div_></p>三组输入分别覆盖三种常见的"非法标签名"形态:
| 输入片段 | 非法原因 | 期望输出 |
|---|---|---|
<bar_baz> | 标签名含下划线_ | <bar_baz>(作为普通文本转义) |
</a:b> | 闭合标签名含冒号: | </a:b>(作为普通文本转义) |
<div_> | 标签名以_结尾,含非法字符 | <div_>(作为普通文本转义) |
注意三个关键行为:
- 非法标签不会被当作 HTML 原样透传,而是退化为普通文本;
- 退化后的文本在输出时,尖括号会被转义为
</>; - 每个输入行仍构成独立的
<p>段落,说明这些"伪标签"没有打断段落的正常解析流程。
二、块级与行内:marked 对 HTML 的双轨识别机制
marked 对 HTML 的识别分为块级与行内两套独立规则,均定义在 src/rules.ts 中。要理解上述规格为何成立,必须先弄清这两条规则各自的门槛。
2.1 块级 HTML:白名单标签 + 行结构约束
块级 HTML 正则见 src/rules.ts,它定义了七种类型(源码注释中编号 (1)~(7)):
<script|pre|style|textarea>起始的整块原始内容;- HTML 注释
<!-- ... -->; - 处理指令
<? ... ?>; - 声明
<!A-Z ...>(如<!DOCTYPE html>); - CDATA 段
<![CDATA[...]]>; - 由白名单标签
_tag构成的开关标签(</?(tag)(?: +|\n|/?>)),内容延伸到空行; - 整行式的普通开/闭标签,要求标签名匹配
[a-z][a-z0-9-]*且标签后必须紧跟行尾((?=[ \t]*(?:\n|$)))。
其中类型 (6) 使用的白名单_tag定义在 src/rules.ts,是一份固定的 HTML 标签列表:
address|article|aside|base|basefont|blockquote|body|caption |center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption |figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe |legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option |p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title |tr|track|ul回到本规格:<bar_baz>、</a:b>、<div_>都不在白名单中(类型 6 不匹配),且bar_baz、a:b、div_的标签名含_或:,不满足类型 (7) 的[a-z][a-z0-9-]*字符集约束,因此块级 HTML 规则整体拒绝这三段输入。
2.2 行内 HTML:标签名字符集是唯一准绳
当块级规则不命中时,行内解析器会用行内tag正则再次尝试,该正则定义在 src/rules.ts:
'^comment' + '|^</[a-zA-Z][a-zA-Z0-9-]*\\s*>' // 闭合标签 + '|^<[a-zA-Z][a-zA-Z0-9-]*(?:attribute)*?\\s*/?>' // 开标签 + '|^<\\?[\\s\\S]*?\\?>' // 处理指令 + '|^<![a-zA-Z]+\\s[\\s\\S]*?>' // 声明 + '|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>' // CDATA 段行内 HTML 标签名的判定规则是:以 ASCII 字母开头,后跟零个或多个字母、数字或连字符([a-zA-Z][a-zA-Z0-9-]*)。这与 CommonMark 0.31.2 规范对标签名的定义一致(仓库测试基准见 test/specs/commonmark/commonmark.0.31.2.json)。
逐一验证本规格的三组输入:
bar_baz:_不在[a-zA-Z0-9-]字符集内 → 行内 tag 正则不匹配;a:b::同样不在字符集内(注意:属性名允许冒号,见 src/rules.ts 中attribute的[a-zA-Z:_],但标签名不允许)→ 不匹配;div_:末尾的_导致整体不满足字符集约束 → 不匹配。
正是这套"标签名字符集白名单",让所有三个"貌似标签"的片段被行内规则拒绝。
三、无效标签的归宿:退化为文本并被转义
行内与块级规则双双拒绝之后,这些片段会落入普通文本 token 的处理路径。此时尖括号必须被转义,否则会破坏 HTML 安全性。转义由 src/helpers.ts 中的escapeHtmlEntities完成,它依据转义表将<、>、&、"、'等特殊字符替换为对应实体(源码第 10~12 行可见"→"、'→'等映射)。
因此<bar_baz>先退化为纯文本<bar_baz>,再在渲染阶段被转义为<bar_baz>,最终落入<p>段落标签之间输出。这也是 CommonMark 安全模型的关键一环:未通过标签名校验的内容绝不原样透传到输出 HTML 中,从而避免用户输入中的伪标签被当作真实标签注入。
四、从 Tokenizer 到输出的完整判定链路
整个判定过程在 src/Tokenizer.ts 中按顺序执行:
- 块级判定:
html(src)方法(src/Tokenizer.ts)用this.rules.block.html尝试匹配块级 HTML,命中则生成block: true的 html token,并依据捕获组判断pre标志(script/pre/style 原始块); - 行内判定:
tag(src)方法(src/Tokenizer.ts)用行内tag正则匹配,命中则生成block: false的 html token,同时维护inLink与inRawBlock两个词法状态(用于<a>与<script>等上下文的正确嵌套判断); - 退化为文本:两次判定均失败后,片段作为普通文本继续解析;
- 转义输出:文本 token 渲染时调用
escapeHtmlEntities完成实体转义,得到规格文件中的期望 HTML。
从源码结构看,Token 类型Tokens.HTML同时承载block布尔标志区分块级/行内两种形态,而tag方法中对inLink/inRawBlock状态的跟踪(src/Tokenizer.ts)保证了对<a>链接内标签与原始块内标签的特殊处理,这些细节共同保证了标签判定的精确性。
五、该规格如何被测试框架执行
这份规格是 marked 自建的specs/new测试套件的一部分。测试框架 test/run-spec-tests.js 启动时会同时加载五组规格:
const [commonMarkTests, gfmTests, newTests, originalTests, redosTests] = ...其中newTests对应./specs/new目录(见 test/run-spec-tests.js),即 marked 针对自身行为特性补充的新规格集,与 CommonMark 官方套件(specs/commonmark)、GFM 套件(specs/gfm,使用gfm: true)、历史行为套件(specs/original,使用pedantic: true)并列。
执行机制为:逐一读取specs/new下成对的.md输入与.html期望输出,用 marked 解析.md,再将结果与.html逐字比对。因此 html_invalid_tag_name.md 与其同名的 html_invalid_tag_name.html 构成一个最小化的、可自动回归验证的行为契约:任何未来对正则或转义逻辑的改动,只要破坏这三组输入输出的对应关系,测试就会立即失败。
六、边界场景与实战启示
基于本规格与源码,可以总结出几条对日常使用 marked 有直接指导意义的结论:
- 标签名合法字符集是
[a-zA-Z][a-zA-Z0-9-]*:<my-tag>、<h1>合法;<my_tag>、<a:b>、<div_>、<1abc>均非法,会被转义为普通文本; - 属性名与标签名的字符集不同:属性名允许冒号(
[a-zA-Z:_][\w.:-]*,见 src/rules.ts),标签名不允许,这是容易踩坑的差异点; - 块级 HTML 还要求"行结构":白名单标签(类型 6)或整行标签(类型 7)才能形成块级 HTML;位于段落中间的标签只走行内判定,例如本规格的
foo <bar_baz> qux整行只是普通段落; - 转义是不可绕过的安全底线:只要标签名校验失败,尖括号一律实体化输出,防止伪标签注入破坏页面结构。
如果你想亲自验证这套行为,可以打开 test/specs/new/html_invalid_tag_name.md 修改输入,对照 html_invalid_tag_name.html 观察期望输出,并通过 test/run-spec-tests.js 驱动回归测试——这是理解 marked 解析边界最直接、也最可靠的方式。
【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考