marked 围栏代码块如何打断段落:fences_breaking_paragraphs 规格测试源码级解析

发布时间:2026/9/19 6:06:24
marked 围栏代码块如何打断段落:fences_breaking_paragraphs 规格测试源码级解析 marked 围栏代码块如何打断段落fences_breaking_paragraphs 规格测试源码级解析【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked围栏式代码块fenced code blocks是 Markdown 中最常被误用的块级结构之一很多人不清楚它能否从一段正文中间拦腰截断也不清楚与~~~在开闭标记、语言标识和文件末尾EOF处理上的细微差异。本文以 marked 仓库规格测试 fences_breaking_paragraphs.md 及其期望输出 fences_breaking_paragraphs.html 为骨架逐场景拆解围栏打断段落的全部边界行为并深入 fences 正则、Tokenizer.fences 实现与 Lexer 调度顺序读完你将掌握 marked 判定一行是否构成围栏开头的完整规则以及它在段落打断与 EOF 收尾上的精确语义。一、测试用例概览一个段落打断行为矩阵该规格测试位于test/specs/new/目录采用.md输入 .html期望输出的成对结构由 test/run-spec-tests.js 通过markedjs/testutils的getTests统一加载并以 marked 默认选项gfm: true运行比对。new规格集是 marked 维护者自有的回归测试集合专门覆盖 CommonMark / GFM 官方规格之外、或容易被实现遗漏的边界行为。测试输入共 27 行构造了 4 个围栏与段落相遇的场景外加一段悬空文本场景输入特征期望行为AA paragraph后紧跟 A 反引号围栏打断段落成为独立代码块BB paragraph后紧跟~~~B波浪线围栏打断段落CC paragraph后紧跟~~~C~波浪线围栏携带非常规语言标识含反引号DD paragraph后紧跟 ~D 无效围栏不打断段落回退为段内文本尾声随后出现 且无闭合合法围栏从 EOF 处接管剩余全文下面按场景逐一还原解析过程。二、四个场景逐一拆解场景 A反引号围栏打断段落输入片段A paragraph A Here is code in backtick fences期望输出 html pA paragraph/p precode classlanguage-AHere is code in backtick fences/code/preA 在段落第二行出现标记被识别为合法 opening fence于是段落在此终止A paragraph 独立成段其后的 3 行正文成为代码块内容并由 闭合。语言标识A被提取为classlanguage-A。场景 B波浪线围栏打断段落B paragraph ~~~B Here is code in tilde fences ~~~输出与场景 A 完全对称~~~B打断段落内容以~~~闭合语言标识为B。这说明~~~与 在打断段落这一能力上地位等同。场景 C波浪线围栏携带非常规语言标识C paragraph ~~~C~ Alternative tilde fences ~~~这是整个测试最刁钻的一例opening fence 是~~~紧接的语言标识是C~——一个同时包含反引号与波浪线的标识。期望输出为precode classlanguage-C~Alternative tilde fences/code/pre它验证了两点其一波浪线围栏对语言标识几乎没有字符限制反引号、波浪线都可以原样进入lang字段其二closing fence 的匹配规则是与 opening fence同源字符的 3 个及以上重复 可选的~与后缀即[~]*因此正文中即使出现~~~、 的混写也不会提前误闭合。场景 D无效围栏回退为段落 EOF 未闭合接管输入片段D paragraph ~D Invalid use of backtick fencesThis will be read as part of a codeblock that ends with the file期望输出 html pD paragraph ~D Invalid use of backtick fences/p precode This will be read as part of a codeblock that ends with the file/code/pre这里发生了两个重要行为**~D 不是合法围栏**。反引号分支要求3 个以上反引号之后必须是一串非反引号、非换行字符并直达换行或 EOF即 lookahead (?[^\n]*(?:\n|$))。而 ~D 中最后一个字符是反引号导致该 lookahead 失败波浪线分支又要求至少 3 个 ~这里只有 1 个。两条分支全部落空fences 正则整体不匹配这一行便留在段内成为普通段落文本 D paragraph~DInvalid use of backtick fences。下一行的 成为救场围栏。它是一个完全合法的 opening fence语言标识为空在段落结束后立即接管此后直到文件末尾的内容This will be read as / part of a codeblock / that ends with the file全部成为代码块正文因为围栏可以不闭合由 EOF 隐式收尾正则末端的|$分支。这也是测试注释part of a codeblock that ends with the file的含义所在。三、fences 正则逐段拆解一条规则解释全部行为上述所有行为的判定都集中在 src/rules.ts 的block.fences一条正则上/^ {0,3}({3,}(?[^\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~]* *(?\n|$)|$)/逐段解读^ {0,3}opening fence 前最多允许 3 个空格缩进超过 3 个空格则降级为缩进代码块规则见 blockCode 正则不再走围栏路径。{3,}(?[^\n]*(?:\n|$))反引号分支。至少 3 个反引号且其后必须跟若干非反引号非换行字符直至换行/EOF。这正是场景 D 中 ~D 被拒绝的原因——~D 之后的第 4 个反引号堵死了 lookahead。~{3,}波浪线分支至少 3 个~无 lookahead 限制因此~~~C~ 这类带反引号的标识可以成立。([^\n]*)捕获语言标识可为空EOF 场景即为空串。([\s\S]*?)(?:\n|$)非贪婪捕获正文。*{0,3}\1[~]*(?\n|$)**closing fence——最多 3 空格 与 opening 同源的反引号/波浪线串\1反向引用 任意~/ 后缀 可选空格且必须直达换行。|$若没有 closing fence则正文一直延伸到文件末尾EOF 收尾对应场景 D 的尾声。这条正则还参与了其他规则的打断判定在 setext 标题规则 中fences以{0,3}(?:{3,}|~{3,})的形式替换进去注释明确写着 fenced code blocks can interrupt在 [段落正则_paragraph](https://link.gitcode.com/i/8e708f47fcecf5d590f8926157a42e7c) 中fences 也位列段落内部不可跨越的中断条件集合。四、为什么围栏能打断段落、缩进代码不能Lexer 调度顺序围栏与缩进代码块对段落的处理截然不同根源在 Lexer.ts 的调度顺序// code缩进代码块 if (token this.tokenizer.code(src)) { ... const lastToken tokens.at(-1); // An indented code block cannot interrupt a paragraph. if (lastToken?.type paragraph || lastToken?.type text) { lastToken.raw ...; lastToken.text \n token.text; ... } else { tokens.push(token); } continue; } // fences围栏代码块 if (token this.tokenizer.fences(src)) { src src.substring(token.raw.length); tokens.push(token); continue; }两处行为差异一目了然缩进代码块若当前最后一个 token 是段落则不产生新代码块而是把内容合并回段落文本源码注释 An indented code block cannot interrupt a paragraph。也就是说段落中间出现 4 空格缩进的行只会成为段落的一部分。围栏代码块fences分支无条件 push 新 token直接截断当前段落。这正是本测试标题 fences breaking paragraphs 的语义来源——围栏是段落的中断者缩进代码不是。同时注意 Lexer.ts 中fences位于heading、hr、blockquote之前被尝试优先级极高因此只要一行命中 fences 正则就优先于段落解析生效。五、Tokenizer.fences 实现语言标识与缩进补偿Tokenizer.fences 负责把正则匹配结果加工为Tokens.Codefences(src: string): Tokens.Code | undefined { const cap this.rules.block.fences.exec(src); if (cap) { const raw cap[0]; const text indentCodeCompensation(raw, cap[3] || , this.rules); return { type: code, raw, lang: cap[2] ? cap[2].trim().replace(this.rules.inline.anyPunctuation, $1) : cap[2], text, }; } }三个关键处理lang的净化语言标识先trim()再用 inline.anyPunctuation即edit(/\\(punct)/, gu)对-、_、、等标点做转义处理后作为类名输出。场景 C 中C~之所以能原样进入classlanguage-C~正源于此。indentCodeCompensation缩进补偿Tokenizer.tsrules.other.indentCodeCompensation匹配出 opening fence 自身的缩进量然后对正文每一行去掉与围栏缩进等量的前导空格某行缩进不足时则整行去除其全部缩进。这保证了围栏位于第 2~3 个空格缩进时正文相对缩进不受污染。raw原样保留整段围栏块的原始文本原样存进raw供后续 Parser 消费与源码重建。六、相关测试与验证把行为锁进回归该测试并非孤例new规格集中存在一批围栏与段落交互的兄弟用例共同构成行为矩阵tilde_fence_eof_interrupts_paragraph.md专门验证波浪线围栏在 EOF 处打断段落的行为与本文场景 D 的EOF 接管互为镜像fences_with_blankline_following_list_0.md 与_1两个变体围栏出现在列表之后时与空行的交互fences_following_list.md、fences_following_table.md、fences_following_nptable.md围栏紧随列表/表格时的段落/块级转换redos/quadratic_tilde_paragraph_interrupt.cjs针对波浪线打断段落这一路径的复杂度ReDoS压力测试防止正则退化列表中代码围栏的识别由 fencesBeginRegex^ {0,${indent}}(?:\|~~~)支撑用于判定列表项内出现的围栏。若要本地验证本用例仓库规格测试入口为 test/run-spec-tests.js它会加载test/specs/下 commonmark、gfm、new、original、redos 五套规格其中newTests即本用例所在集合以 marked 默认选项gfm: true逐一比对.md输入与.html期望输出。任何对block.fences正则或 Lexer 调度顺序的改动都会被这套成对测试即时拦截。七、小结围栏打断段落的完整判定链综合文档、正则与源码marked 对围栏是否打断段落的判定链可总结为一行以 0~3 个空格开头否则归缩进代码块不打断段落反引号分支至少 3 个且其后直至换行处不含反引号才构成 opening fence—— ~D 因此失效波浪线分支至少 3 个~对语言标识无字符限制——~~~C~ 因此成立一旦命中即打断段落Lexer 在paragraph之前无条件尝试fences并 push 独立 token闭合可省略无 closing fence 时正文延伸到 EOF由|$分支收尾语言标识经 trim 与标点转义后进入classlanguage-…正文经缩进补偿去除与围栏等量的前导空格。这套规则既保证了围栏在正文中的硬中断语义又通过 lookahead 与反向引用把误判风险压缩到最小——正是 fences_breaking_paragraphs.md 用 27 行输入覆盖 4 类边界场景想要钉死的契约。【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考