资讯详情

marked 的 HTML 实体转义机制:以 amps_and_angles_encoding 规范测试为切入点

📅 2026/9/20 14:25:23 | 华诺云谱 👁 阅读
marked 的 HTML 实体转义机制:以 amps_and_angles_encoding 规范测试为切入点
前端【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址https://gitcode.com/gh_mirrors/ma/marked点击查看免费下载核心导读本篇文章围绕 marked 仓库中的规范测试文件test/specs/original/amps_and_angles_encoding.md及其配对期望输出amps_and_angles_encoding.html完整解析 Markdown 文本中、、等特殊字符在编译为 HTML 时的实体转义行为包括「已有实体不被二次转义」「链接 URL、链接文字与 title 中的 处理」「尖括号包裹的链接目标」等细节。读完本文你将能理解 marked 的escapeHtmlEntities双模式转义设计、pedantic 模式的作用以及如何基于源码与测试用例验证这类转义行为从而在自己的渲染管线中正确处理特殊字符并规避注入风险。一、规范测试文件是什么test/specs/original/amps_and_angles_encoding.md是 marked 规范测试spec体系中的一条用例。测试目录的层级含义如下test/specs/original/记录原始 Markdown.pl 时代宽松行为的回归测试集用于保证 marked 在开启 pedantic 模式时仍能复现早期 Markdown 实现的行为每个.md文件Markdown 输入都配有一个同名的.html文件期望输出二者成对构成一条完整断言测试运行入口是 test/run-spec-tests.js其中第 13-20 行通过getTests一次性加载 commonmark、gfm、new、original、redos 五组规范第 41-45 行对original组统一使用defaultMarkedOptions: { gfm: false, pedantic: true }运行。值得注意的细节本文件头部带有 YAML frontmatter--- pedantic: true ---这说明该用例必须在 pedantic 模式下才成立——它记录的是老式 Markdown 的宽松语法行为而不是 CommonMark 的严格行为。运行器runner为 original 组整体固定的{ gfm: false, pedantic: true }与该 frontmatter 声明相互印证。如何在本地运行验证在仓库根目录执行需要 Node.js 20见 package.jsonnpm run build npm run test:specstest:specs脚本package.json最终执行node --test --test-reporterspec test/run-spec-tests.js。也可以跳过构建、直接用已构建产物运行npm run build node --test test/run-spec-tests.js当test/specs/original组全部通过时意味着 marked 的 pedantic 输出与各.html期望文件完全一致。二、完整测试用例逐条对照amps_and_angles_encoding.md共包含 8 组 Markdown 输入聚焦“ampersand与 angle bracket、的编码”。以下逐条给出输入、期望输出与行为解释期望输出严格取自配对文件test/specs/original/amps_and_angles_encoding.html。用例 1段落文本中的裸输入ATT has an ampersand in their name.期望输出pATamp;T has an ampersand in their name./p裸不是合法实体后面不是数字;或字母;形式因此被编码为amp;保证最终 HTML 中不会出现歧义字符。用例 2已有实体不被二次转义输入ATamp;T is another way to write it.期望输出pATamp;T is another way to write it./p这是本规范最核心的语义之一用户已经写好的合法实体amp;保持原样输出不会被再转义成amp;amp;。要做到这一点转义逻辑必须能区分“裸”与“已成形的实体”。用例 3单词间的输入This that.期望输出pThis amp; that./p与用例 1 同理 that中的被编码。用例 4 与用例 5与的不对称处理输入4 5. 6 5.期望输出p4 lt; 5./p p6 5./p本规范记录的行为是被编码为lt;因为可能开启 HTML 标签必须消毒而保持字面输出。这是原始 Markdown.pl 时代的记录性行为而在通用转义正则见第三节中也位于可转义字符集合[]内。阅读此规范时应以.html文件记录的期望输出为准来理解“原始行为”的语义。用例 6引用式链接 URL 中的输入Heres a [link] [1] with an ampersand in the URL. [1]: http://example.com/?foo1bar2期望输出pHeres a a hrefhttp://example.com/?foo1amp;bar2link/a with an ampersand in the URL./p引用定义[1]: http://example.com/?foo1bar2中的查询参数分隔符被编码为amp;避免在href属性中出现裸。用例 7引用式链接文字与 title 中的输入Heres a link with an amersand in the link text: [ATT] [2]. [2]: http://att.com/ ATT注amersand拼写为原规范文件中的原文此处保留。期望输出pHeres a link with an amersand in the link text: a hrefhttp://att.com/ titleATamp;TATamp;T/a./p链接文字中的ATT与title 属性中的ATT都被编码为ATamp;TURL 本身http://att.com/不含特殊字符无需编码。用例 8内联链接 URL 中的含尖括号包裹形式输入Heres an inline link. Heres an inline link.期望输出pHeres an inline a href/script?foo1amp;bar2link/a./p pHeres an inline a href/script?foo1amp;bar2link/a./p两种写法殊途同归(/script?foo1bar2)与(/script?foo1bar2)都解析出同样的目标/script?foo1bar2尖括号只是链接目标的定界符不会被写入href且其中的均被编码为amp;。三、源码级机制escapeHtmlEntities 与两套转义正则上述行为并非硬编码在测试里而是由 marked 的转义基础设施统一保证的。核心实现位于 src/helpers.tsconst escapeReplacements: { [index: string]: string } { : amp;, : lt;, : gt;, : quot;, : #39;, }; const getEscapeReplacement (ch: string) escapeReplacements[ch]; export function escapeHtmlEntities(html: string, encode?: boolean) { if (encode) { if (other.escapeTest.test(html)) { return html.replace(other.escapeReplace, getEscapeReplacement); } } else { if (other.escapeTestNoEncode.test(html)) { return html.replace(other.escapeReplaceNoEncode, getEscapeReplacement); } } return html; }函数接受第二个参数encode据此选择两套不同的正则定义于 src/rules.ts模式encode 参数正则语义全量转义trueescapeTest/escapeReplace/[]/无差别转义全部五个字符用于代码块、行内代码、autolink 等“必须绝对安全”的场景保留实体false或省略escapeTestNoEncode/escapeReplaceNoEncode/[]|(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w);)/转义但对采用负向先行断言仅当不是合法实体#数字;、#x十六进制;或字母;时才转义正是第二套正则中的负向先行断言(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w);)实现了第二节用例 2 的“已有实体不被二次转义”ATT中的T不以;结尾 → 不满足“合法实体”形态 → 被转义为amp;ATamp;T中的amp;匹配\w;→ 被保留原样不会变成amp;amp;。各类输出的调用路径转义发生在渲染阶段src/Renderer.ts不同 token 走不同分支普通段落文本Renderer.textsrc/Renderer.ts调用escapeHtmlEntities(token.text)encode省略 → 走“保留实体”模式这决定了用例 1-5 的文本行为行内代码Renderer.codespansrc/Renderer.ts调用escapeHtmlEntities(text, true)全量转义——因此代码里的amp;会变成amp;amp;这与普通文本行为刻意不同代码内容应原样呈现源码而不是解析实体代码块Renderer.codesrc/Renderer.ts对代码内容同样使用全量转义链接文字Renderer.linksrc/Renderer.ts对非 autolink 的链接文字走this.parser.parseInline(tokens)内部同样命中Renderer.text的保留实体逻辑用例 7 的ATT→ATamp;Tautolink 文字同一函数对autolink: true的 token 使用escapeHtmlEntities(text, true)全量转义注释明确说明原因——“autolink 内的每个都是字面的不存在引用解析”。href 与 title 的处理链接属性在Renderer.link中const cleanHref cleanUrl(href); if (cleanHref null) { return parsedText as RendererOutput; } href escapeHtmlEntities(cleanHref, autolink);cleanUrlsrc/helpers.ts先对 href 执行encodeURI(href)对 URL 中不合法的字符做百分号编码再把%25还原为%随后escapeHtmlEntities(cleanHref, autolink)对属性值做转义非 autolink 时encode为undefined→ 保留实体模式但?foo1bar2中的不构成合法实体故编码为amp;用例 6、8title 属性同理调用escapeHtmlEntities(title)src/Renderer.ts将ATT编码为ATamp;T用例 7。至此第二节全部 8 个用例的输出都可以在这条“tokenizer 产出 token → renderer 按场景选择转义模式”的链路中找到对应解释。四、链接解析细节引用式链接与尖括号目标引用式链接的 pedantic 差异用例 6、7 是引用式链接reference link其定义行[1]: ...、[2]: ...由块级def规则解析。值得注意的是pedantic 模式下的行内引用链接规则与普通模式不同src/rules.tsconst inlinePedantic: RecordInlineKeys, RegExp { ...inlineNormal, emStrongLDelim: emStrongLDelimPedantic, emStrongRDelimAst: emStrongRDelimAstPedantic, emStrongRDelimUnd: emStrongRDelimUndPedantic, link: edit(/^!?\[(label)\]\((.*?)\)/) .replace(label, _inlineLabel) .getRegex(), reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) .replace(label, _inlineLabel) .getRegex(), };对比普通模式下的reflink/^!?\[(label)\]\[(ref)\]/src/rules.tspedantic 版允许[label]与[ref]之间存在空白\s*这正是用例 6 中[link] [1]这种带空格写法能被识别的直接原因。而链接目标中的在 tokenizer 输出 token 后统一交由Renderer.link的转义逻辑处理与写法[link] [1]还是[link][1]无关。尖括号包裹的链接目标用例 8 第二行link展示了用...包裹链接目标destination的写法。Tokenizer.linksrc/Tokenizer.ts对此有专门处理const trimmedUrl cap[2].trim(); if (!this.options.pedantic this.rules.other.startAngleBracket.test(trimmedUrl)) { // commonmark requires matching angle brackets if (!(this.rules.other.endAngleBracket.test(trimmedUrl))) { return; } ... }非 pedanticCommonMark 严格模式要求必须成对闭合且结尾尖括号不能被反斜杠转义代码里用rtrim统计尾部反斜杠数量做奇偶校验否则整个链接不成立pedantic 模式src/Tokenizer.ts允许只有起始尖括号而没有结束尖括号的写法href.slice(1)仅去掉开头当尖括号成对出现时无论哪种模式href都会被剥掉两端的最终只保留内部真实目标/script?foo1bar2这就是两个输出href完全一致的原因。此外pedantic 模式下的链接还有一处差异Tokenizer.link会先用pedanticHrefTitle/^([^]*[^\s])\s([])(.*)\2/src/rules.ts把宽松写法的 href 与 title 拆分开再对二者做反转义replace(this.rules.inline.anyPunctuation, $1)恢复被反斜杠转义的标点。五、pedantic 模式为什么这条规范必须挂在它下面默认值与规则切换marked 默认不开启pedanticsrc/defaults.tssrc/defaults.ts中pedantic: false同时gfm: true。开启后词法分析器_Lexer在构造时切换整套语法规则src/Lexer.tsif (this.options.pedantic) { rules.block block.pedantic; rules.inline inline.pedantic; } else if (this.options.gfm) { rules.block block.gfm; if (this.options.breaks) { rules.inline inline.breaks; } else { rules.inline inline.gfm; } }即规则优先级为pedantic gfm normal。此外pedantic 模式在块级处理时还会把制表符替换为 4 空格、剔除纯空格行src/Lexer.tsif (this.options.pedantic) { src src.replace(other.tabCharGlobal, ).replace(other.spaceLine, ); }pedantic 语法的典型宽松点从 src/rules.ts 与 src/rules.ts 的 pedantic 规则集可以看到一系列与 CommonMark 的差异围栏代码块fenced code被禁用fences: noopTest代码块只能靠缩进表达ATX 标题不要求#后必须有空格/^(#{1,6})(.*)(?:\n|$)/引用式链接允许[label] [ref]带空格内联链接目标采用宽松的(.*?)匹配链接目标允许只开不闭的尖括号见第四节强调/加粗的分隔符左右判定规则与 Unicode 标点处理更宽松emStrongLDelimPedantic等src/rules.ts。与本文主题的关联amps_and_angles_encoding这条规范虽然表面只讲“特殊字符转义”但它依赖的[link] [1]带空格引用链接、link尖括号目标等语法形态都属于 pedantic 语义。因此它被放在test/specs/original/目录下并在 frontmatter 中显式声明pedantic: true——脱离 pedantic 模式用例 6 的[link] [1]可能不会被识别为引用链接整个测试就不成立。这也解释了为什么运行器要为 original 组统一固定{ gfm: false, pedantic: true }。六、工程实践要点1. 默认模式与 pedantic 的选择默认gfm: true, pedantic: false是多数现代文档场景的正确选择pedantic: true仅当需要兼容上世纪 90 年代的 Markdown.pl 文档、或刻意追求最宽松语法解析时才应开启。开启 pedantic 意味着接受无围栏代码、宽松标题与链接语法等一系列取舍。2. 不要重复转义理解“已有合法实体被保留”的语义后应避免在 marked 输出前后再手动做一次全局→amp;替换——那会破坏用例 2 展示的行为把ATamp;T变成ATamp;amp;T。需要补充转义时应只对 marked 输出中真正属于你的动态数据部分做处理。3. 安全边界实体转义解决了“文本内容中的特殊字符进入 HTML 时的语义污染”但它不等于完整的安全消毒方案代码块/行内代码使用全量转义encodetrue内容绝对安全普通文本与属性走“保留实体”模式但依然全部转义无法开启标签然而 marked 对显式 HTML 块与行内 HTML 标签是原样透传的——Renderer.htmlsrc/Renderer.ts直接返回 token 文本这是 Markdown 的既定能力。因此凡是允许用户输入任意 HTML 的场景仍需在渲染链路外层叠加 HTML 消毒sanitize策略而不能只依赖实体转义。4. 用规范测试守护行为test/specs/original/这类 md/html 配对文件是 marked 保持行为稳定性的基石新增功能或重构转义逻辑时npm run test:specs会逐条校验这些历史行为。若你的项目 fork 了 marked 或自定义了 renderer同样建议以“输入 Markdown → 断言输出 HTML”的配对用例形式固化转义行为防止回归。七、小结test/specs/original/amps_and_angles_encoding.md用 8 组精简用例完整覆盖了 marked 在 pedantic 模式下对、、的编码语义裸转义、合法实体保留、转义而按原始行为保留字面、链接 URL/文字/title 中的统一编码、尖括号包裹目标被剥离。这些行为背后是escapeHtmlEntities的“全量转义 / 保留实体”双模式设计src/helpers.ts 与 src/rules.ts以及 renderer 按 token 类型选择模式的调用链src/Renderer.ts。理解这条链路不仅能准确预测 marked 的输出也能在自定义 renderer 或二次开发时正确复刻其转义语义。赞分享前端【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址https://gitcode.com/gh_mirrors/ma/marked点击查看免费下载相关推荐marked 反斜杠转义机制深度解析以 escape_within_emphasis 测试规范为例marked 反斜杠转义机制深度解析以 escape_within_emphasis 测试规范为例 在 Markdown 中反斜杠 \ 是最基础也最容易被误前端marked 强调Emphasis解析深度剖析以 em_2char 规范测试用例为切入点marked 强调Emphasis解析深度剖析以 em_2char 规范测试用例为切入点 导读 本文以 marked 仓库中的规范测试用例 test/sp前端marked 反 ReDoS 测试规范解析以 backticks_in_link_no_close 为例marked 反 ReDoS 测试规范解析以 backticks_in_link_no_close 为例 导读 本文以 marked 仓库中 test/spe前端上一篇开源项目推荐multiparty下一篇解决Unity资产混乱智能引用检测实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。