资讯动态

marked 段落中断机制详解:pedantic 模式下列表样式行如何留在硬换行段落内

发布时间:2026/9/19 21:31:19 来源:尧图企业网站定制
marked 段落中断机制详解pedantic 模式下列表样式行如何留在硬换行段落内【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked本篇文章围绕 marked 仓库中一个极具代表性的回归测试用例 —— test/specs/original/hard_wrapped_paragraphs_with_list_like_lines.md —— 深入剖析 Markdown 解析中的段落中断paragraph interruption机制。你将理解为什么一行以8.或*开头的硬换行文本在 marked 的 pedanticMarkdown.pl 兼容模式下会继续留在原段落里而不是被拆成一个列表项同时掌握 pedantic、CommonMark、GFM 三种语法模式在段落中断规则上的本质差异以及如何结合 src/rules.ts 与 src/Lexer.ts 的源码验证这一行为并亲手跑通对应的规格测试。一、用例速览一份记录历史问题的回归测试该夹具的 Markdown 输入全文如下test/specs/original/hard_wrapped_paragraphs_with_list_like_lines.mdIn Markdown 1.0.0 and earlier. Version 8. This line turns into a list item. Because a hard-wrapped line in the middle of a paragraph looked like a list item. Heres one with a bullet. * criminey.对应的期望输出test/specs/original/hard_wrapped_paragraphs_with_list_like_lines.htmlpIn Markdown 1.0.0 and earlier. Version 8. This line turns into a list item. Because a hard-wrapped line in the middle of a paragraph looked like a list item./p pHeres one with a bullet. * criminey./p这段文本本身就解释了测试的来龙去脉在 Markdown 1.0.0 及更早版本中一个被硬换行hard wrap拆断的段落只要中间某一行行首恰好长得像列表项比如8.或*就会被错误地解析成一个真正的列表项。本用例要锁定的是修正后的行为这类列表样式行应当作为段落的普通文本继续保留。注意这里列表样式行list-like line指的是行首字符符合列表标记形态、但在语义上只是段落续行的一行文本。二、核心概念什么是段落中断paragraph interruption要理解这个用例必须先建立段落中断这一概念。块级解析是逐行逐块推进的当解析器正在累积一个段落时如果遇到一行满足某种块级元素特征如 ATX 标题#、引用、分割线---、围栏代码块 、列表标记*/1.等就会中断当前段落把该行交给对应的块级 tokenizer 处理而不是作为段落的续行文本。这一行为在 src/rules.ts 中由paragraph正则的负向前瞻negative lookahead精确控制只有当前行不匹配这些块级特征时才允许它作为段落的续行被吞入。换言之段落正则中允许中断的块级元素清单决定了哪些行会拆散段落。这也是整个用例的技术核心——不同语法模式pedantic / CommonMark / GFM维护着不同的中断清单。marked 的块级规则在 src/rules.ts 中被组织为blockNormal、blockGfm、blockPedantic三套并在文件末尾统一导出src/rules.ts。选择哪一套由 src/Lexer.ts 依据选项决定if (this.options.pedantic) { rules.block block.pedantic; rules.inline inline.pedantic; } else if (this.options.gfm) { rules.block block.gfm; // ... }注意这里的判断顺序pedantic优先于gfm。因此 pedantic 模式必须显式关闭 GFM 特性gfm: false才能生效否则走的是 GFM 分支。这与本用例所属测试套件的运行参数完全对应见第五节。三、源码级机制pedantic 段落正则如何放行列表样式行3.1 列表标记本身长什么样无论是哪种模式列表标记bullet的基础形态都定义在 src/rules.tsconst bullet / {0,3}(?:[*-]|\d{1,9}[.)])/;即最多 3 个前导空格后跟*、、-之一或 19 位数字后跟.或)。块级list正则在此基础上构造src/rules.tsconst list edit(/^(bull)([ \t][^\n]*?)?(?:\n|$)/) .replace(/bull/g, bullet) .getRegex();按此正则8. This line turns into a list item.与* criminey.在行首时都能匹配列表标记。也就是说能否成为列表项的关键不在列表正则本身而在于段落正则是否允许它们留在段落里。3.2 关键所在pedantic 段落正则移除了list中断普通模式的段落正则_paragraph定义在 src/rules.tsconst _paragraph /^([^\n](?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table|[ \t]\n)[^\n])*)/;其中负向前瞻里的list意味着一行如果匹配列表标记就中断当前段落。而 pedantic 版段落正则src/rules.ts通过模板替换显式移除了这一项paragraph: edit(_paragraph) .replace(hr, hr) .replace(heading, *#{1,6} *[^\n]) .replace(lheading, lheading) .replace(|table, ) .replace(blockquote, {0,3}) .replace(|fences, ) .replace(|list, ) // 关键列表不再能中断段落 .replace(|html, ) .replace(|tag, ) .getRegex(),edit辅助函数src/rules.ts负责把模板占位符替换成真实正则。|list被替换为空字符串等于从负向前瞻中删除了列表中断这一分支于是段落的续行判断不再排斥列表样式行。回到用例第一段中的8. This line turns into a list item.和第二段中的* criminey.都因此被原样吸收进段落最终输出为两个完整的p。与之呼应pedantic 模式还移除了 table、fences、html 对段落的中断同样通过|xxx替换为空实现只保留hr、heading、lheading、blockquote四种中断源。此外src/Lexer.ts 在 pedantic 下还会对源文本做预处理把制表符展开为 4 个空格并删除整行只有空格的行。3.3 用例第二段pedantic 与 CommonMark 的分水岭值得强调的是本用例的两个段落敏感度并不相同第一段中的8.以 8 开头。CommonMark 规定只有以 1 开头的有序列表才能中断段落见 src/rules.ts所以8.行在 CommonMark 下本来也不会中断段落——它属于两模式行为一致的部分第二段中的* criminey.是非空无序列表标记在 CommonMark 下可以中断段落会真的变成一个列表项只有 pedantic 模式才会把它保留在段落内src/rules.ts 与 src/rules.ts 的差异所在。因此这个用例真正锁定的行为边界是在 pedanticMarkdown.pl 兼容模式下任何列表样式行——包括 CommonMark 会中断段落的*行——都不会拆散硬换行段落。四、三种语法模式的段落中断规则对比综合 src/rules.ts普通/CommonMark、src/rules.tsGFM、src/rules.tspedantic三种模式下允许中断段落的块级元素对比如下块级元素CommonMarknormalGFMpedantic分割线---✅✅✅ATX 标题#✅✅✅Setext 标题/--❌❌✅引用块✅✅✅围栏代码块✅✅❌列表⚠️ 仅非空列表且有序列表必须以1开头src/rules.ts⚠️ 同 CommonMarksrc/rules.ts❌块级 HTML✅✅❌GFM 表格❌✅❌关于列表中断的具体正则普通模式src/rules.tsconst paragraph createParagraph(/ {0,3}(?:[*-]|1[.)])[ \t][^ \t\n]/);注意两个限定条件有序标记只能是1./1)不是任意数字且标记后必须跟空白与非空内容。注释原文也写得很明确only non-empty lists starting from 1 can interrupt paragraphs只有以 1 开头且非空的列表才能中断段落。另外有一个特殊场景值得注意src/rules.ts在引用块内部任意数字的裸列表标记如2.都会开启兄弟列表因此引用块内的段落不允许被惰性续行lazy continuation吸收——这与顶层段落的行为相反注释解释为inside a blockquote a bare list marker (any number) starts a sibling list。这说明列表能否中断段落的答案还取决于当前是否处于引用块上下文中。GFM 模式下还多出一个表格中断规则gfmTable内嵌的段落部分会检测列表标记以结束表行src/rules.ts且 GFM 段落正则允许表格中断段落src/rules.ts。五、Lexer 扫描顺序与测试执行路径5.1 块级扫描中的 tokenizer 顺序理解了正则之后再看 src/Lexer.ts 中blockTokens的扫描循环。marked 在每个扫描位置依次尝试扩展 tokenizer → 空白行space→ 缩进代码块code→ 围栏代码块fences→ 标题heading→ 分割线hr→ 引用块blockquote→列表listsrc/Lexer.ts→ HTML → 链接定义def→ 表格table→ Setext 标题lheading→段落paragraphsrc/Lexer.ts→ 文本text。列表 tokenizer 排在段落之前意味着在 CommonMark/GFM 模式下一个非空*行会先被list捕获根本轮不到段落——这正是它能把段落拆成列表的原因。而在 pedantic 模式下list正则本身依然存在但当段落正则在更早的扫描位置已经以包含续行的方式吞掉了整个文本块后扫描指针已经越过那些列表样式行list自然无从触发。最终呈现的效果就是段落整体保留列表样式行成为段落文本的一部分。5.2 original 规格套件如何运行本用例marked 的规格测试统一由 test/run-spec-tests.js 驱动它从五个目录加载测试集const [commonMarkTests, gfmTests, newTests, originalTests, redosTests] await getTests([ resolve(__dirname, ./specs/commonmark), resolve(__dirname, ./specs/gfm), resolve(__dirname, ./specs/new), resolve(__dirname, ./specs/original), resolve(__dirname, ./specs/redos), ]);其中original套件本用例所属以固定参数运行test/run-spec-tests.jsrunTests({ tests: originalTests, parse, defaultMarkedOptions: { gfm: false, pedantic: true }, });也就是说test/specs/original/下的每个夹具包括本用例都只在gfm: false, pedantic: true配置下验证这与我们上文分析的pedantic 段落正则移除 list 中断完全吻合。默认情况下gfm为true、pedantic为falsesrc/defaults.ts因此普通用户在默认配置下遇到的是 CommonMark/GFM 行为pedantic 行为需要显式开启。5.3 本地运行方式在仓库根目录依次执行Node.js 版本需满足 package.json 要求的 20npm install npm run build # 生成 lib/ 下的构建产物规格测试依赖 npm run test:specs # 运行全部规格测试等价于 node --test test/run-spec-tests.js如果只想快速验证本用例也可以直接构造等价的最小输入并用 pedantic 模式解析import { Marked } from ./lib/marked.esm.js; const marked new Marked({ gfm: false, pedantic: true }); const src [ In Markdown 1.0.0 and earlier. Version, 8. This line turns into a list item., Because a hard-wrapped line in the, middle of a paragraph looked like a, list item., , Heres one with a bullet., * criminey., ].join(\n); console.log(marked.parse(src));输出应与 test/specs/original/hard_wrapped_paragraphs_with_list_like_lines.html 一致两个p列表样式行均保留在段落内。将pedantic改为false默认 CommonMark 行为再对比即可直观看到第二段中* criminey.被拆成列表项的差异。六、对实际 Markdown 编写的启示硬换行长段落有隐患如果你习惯硬换行每行约 80 字符手动折行书写长段落当某一行行首恰好以1.、*、-、等开头时在默认的 CommonMark/GFM 模式下可能被解析成列表项破坏段落结构。本用例正是这种场景的回归保障。理解三种模式的取舍pedantic: true需同时gfm: false提供 Markdown.pl 时代的宽松解析——列表、表格、围栏代码块、块级 HTML 都不再中断段落但代价是丢失 GFM 的表格、删除线等能力。生产环境应明确文档的目标语法方言再决定选项组合。写作侧规避列表前后留空行、用四个空格缩进或反引号转义列表标记、避免在段落续行行首使用列表符号都是跨模式安全的做法。对解析器开发者的参考本用例连同 test/specs/new/list_align_pedantic.md、test/specs/new/same_bullet.md、test/specs/new/pedantic_heading_interrupts_paragraph.md 等 pedantic 相关夹具共同构成了段落中断规则的行为契约。修改 src/rules.ts 中的段落正则或 src/Lexer.ts 的扫描顺序时务必运行npm run test:specs确保这些边界行为不被破坏。七、小结hard_wrapped_paragraphs_with_list_like_lines用例以两段不足十行的文本浓缩了 Markdown 块级解析中最精巧的边界问题之一——段落中断。它的核心结论可以浓缩为一句话段落正则负向前瞻中的中断清单决定了列表样式行的命运而 pedantic 模式通过移除list中断项让硬换行段落中的8.、*等行安全地留在段落内。理解了这一点你不仅能读懂本用例的期望输出还能举一反三地推断 table、fences、html 在三种模式下的中断差异并在实际项目中正确地选择pedantic、gfm与breaks等选项的组合。【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价