marked 的 pedantic 列表项文本解析:list_item_text测试用例源码级剖析
【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked
导读
本文围绕 marked 仓库中test/specs/new/list_item_text.md这份回归测试用例展开,深入讲解 marked 在pedantic: true(原始 John Gruber 宽松 Markdown 规范)模式下如何解析"外层列表项内部嵌套列表 + 后续文本段落"这类边界输入。读完本文,你将掌握:该测试的输入、期望输出与 pedantic 模式的关联;列表项文本(item.text)在 Tokenizer.ts 中的提取与缩进归一化流程;pedantic选项对块级与行内语法的整体影响(见 Lexer.ts 与 rules.ts);以及如何用测试运行器复现与验证该用例。
一、测试用例全貌:输入、前置条件与期望输出
1.1 输入与前置元数据
test/specs/new/list_item_text.md全文如下:
--- pedantic: true --- * item1 * item2 text文件由两部分组成:
- YAML front matter:声明本用例仅在
pedantic: true条件下生效。marked 的测试运行器 run-specs-tests 的解析入口 会读取该元数据,把pedantic: true合并进Marked实例的选项;除显式声明的选项外,其余选项(如gfm)保持默认值。这与original/目录下所有用例统一使用{ gfm: false, pedantic: true }(见 run-spec-tests.js)的约定一致,说明 pedantic 是"原始规范回归组"的标志性选项。 - 正文 Markdown:一段以两个空格缩进开头的内容,结构为:
* item1 ← 外层无序列表项,项目符号缩进 2 空格 ← 空行 * item2 ← 内层嵌套列表项,缩进 4 空格 ← 空行 text ← 外层列表项内、嵌套列表之后的文本行(缩进回到 2 空格)1.2 期望输出
同名期望文件test/specs/new/list_item_text.html:
<ul><li><p>item1</p> <ul><li>item2 </li></ul> <p>text</p> </li></ul>从中可读出三条关键断言:
- 整个输入被解析为单一外层
<ul>,且不含缩进代码块——尽管text与item2的源码缩进恰好满足"4 空格代码块"的视觉特征,pedantic 模式下它们仍属于列表项的延续内容; - 外层列表项是"松散列表项":
item1、text各自被<p>包裹(<p>item1</p>、<p>text</p>),因为两个空行使列表变 loose(对应源码list.loose判定逻辑,见下文第三节); - 内层列表
<li>item2 </li>被渲染在第一个<p>之后,即item2属于外层项的子块内容,而非新开一个顶层列表。
也就是说,这份用例锁定的行为是:pedantic 模式下,嵌套列表后的同缩进文本行必须继续留在外层列表项内部,作为独立的段落块输出。
二、pedantic 模式:选项定义与语法集切换
2.1 选项定义与默认值
pedantic是 MarkedOptions.ts 中声明的布尔选项,默认值为false(见 defaults.ts):
// src/MarkedOptions.ts pedantic?: boolean;启用后,marked 不再追求 CommonMark/GFM 的规范兼容,而是模拟 John Gruber 原始 Markdown 的宽松行为(rules.ts 注释 明确标注"Pedantic grammar (original John Gruber's loose markdown specification)")。
2.2 语法集的切换逻辑
pedantic对词法分析的影响集中在 Lexer.ts 构造函数:三套块级/行内语法规则按优先级切换——pedantic优先于gfm:
if (this.options.pedantic) { rules.block = block.pedantic; rules.inline = inline.pedantic; } else if (this.options.gfm) { rules.block = block.gfm; rules.inline = this.options.breaks ? inline.breaks : inline.gfm; }与列表解析直接相关的差异包括:
block.pedantic(rules.ts):基于blockNormal扩展,但fences被替换为noopTest(围栏代码块不支持)、def/heading/lheading/paragraph均使用宽松版本。其中paragraph规则把|list、|fences、|html从"可中断段落"清单中移除,意味着 pedantic 下更多行会被吞并进列表上下文;block.normal(rules.ts):与block.pedantic是同源基类,本用例未开启gfm时列表、标题等主要块规则并无差异;- 预处理差异:在 Lexer.blockTokens 中,pedantic 模式额外执行
src.replace(other.tabCharGlobal, ' ').replace(other.spaceLine, ''),即把所有制表符展开为 4 空格、并删除纯空格行,这为后续列表缩进计算提供了稳定基础。
三、源码级解析流程:list_item_text在 Tokenizer 中的行走路径
外层listtoken 的产生位于 Tokenizer.list(),我们按执行顺序还原本用例的处理。
3.1 匹配首个项目符号并计算缩进
list()首先用this.rules.block.list匹配到* item1,bull = '*'(非有序,isordered = false)。pedantic 分支(L273-L275)把后续项目符号正则放宽为[*+-](任意项目符号都算同列表),而非默认模式下仅接受相同的*。这是 pedantic 列表语义的第一个差异点。
随后进入关键的第一项缩进计算(L296-L311):
let line = expandTabs(cap[2].split('\n', 1)[0], cap[1].length); ... if (this.options.pedantic) { indent = 2; // ← 固定缩进基准为 2 itemContents = line.trimStart(); // ← 去掉前导空白取内容 }在 pedantic 分支中,外层项的缩进基准被硬编码为 2(indent = 2),item1的内容直接trimStart()得到item1。而在非 pedantic 分支,缩进由"首个非空格字符位置"动态计算(L307-L310),因此本用例只有开启pedantic: true才能得到缩进基准 2,从而让后续text(2 空格缩进)恰好等于外层项的延续缩进。
3.2 子行归属判定:text为何留在列表项内
解析完* item1后,循环检查后续行(L327-L401),逐行做六大"提前终止"测试:
if (fencesBeginRegex.test(nextLine)) break; // 围栏代码块 if (headingBeginRegex.test(nextLine)) break; // ATX 标题 if (htmlBeginRegex.test(nextLine)) break; // HTML 块 if (blockquoteBeginRegex.test(nextLine)) break; // 引用块 if (nextBulletRegex.test(nextLine)) break; // 新项目符号 if (hrRegex.test(nextLine)) break; // 水平分割线这些正则均由indent(=2)动态生成,例如nextBulletRegex(rules.ts)为^ {0,2}(?:[*+-]|\d{1,9}[.)])...,即只有缩进不超过 2 空格的项目符号才被视为"新项"。
本用例的三行后续内容逐行分析:
* item2(4 空格缩进):不匹配nextBulletRegex(缩进 4 > 2),因此它不是新列表项;但它的缩进 ≥indent(2),走"继续并入列表项"分支(L371-L372),把nextLineWithoutTabs.slice(indent)追加进itemContents。这里 pedantic 有一个独特处理:先执行listReplaceNesting替换(L334-L336),即用正则^ {1,4}(?=( {4})*[^ ])(rules.ts)把"1~4 空格起始、后面紧跟 4 空格倍数缩进"的行重新对齐为 2 空格,再按indent切片。这正是 pedantic 模式下嵌套列表得以在itemContents中形成、并被下一轮递归blockTokens识别为子<ul>的关键;- 空行:
blankLine置位,但itemContents仍追加空行,作为段落分隔与 loose 判定依据; text(2 空格缩进):indent(2)≥nextLineWithoutTabs.search(nonSpaceChar)(2),同样满足并入条件,追加为第二段文本text。
3.3 最终itemContents与递归子解析
三项内容拼接后,第一项的外层item.text(即 Tokenizer.ts L418 的itemContents)为:
item1 * item2 text随后在 L438-L448 的第一轮子 tokenize 中,this.lexer.blockTokens(item.text, [])递归解析这份内容,得到三个子块:段落item1→ 嵌套list(* item2)→ 段落text。同时,由于子 token 序列中存在space类型的空行 token,且spacers.some(t => this.rules.other.anyLine.test(t.raw))判定其含完整换行(L444-L447),list.loose被置为true,随后 L496-L505 将所有子项loose置位、并把text类型子 token 升级为paragraph——这正是期望输出中<p>item1</p>、<p>text</p>的来源。
3.4 渲染输出
Parser 对listtoken 调用 Renderer.list(),遍历token.items调用 Renderer.listitem()(<li>${this.parser.parse(item.tokens)}</li>),最终把上述三个子块逐一渲染,拼出与期望文件一致的输出:
<ul><li><p>item1</p> <ul><li>item2 </li></ul> <p>text</p> </li></ul>四、对照实验:非 pedantic 模式下的差异
为印证pedantic在该用例中的决定性作用,可把同样的输入放到默认(pedantic: false)模式下对比。依据 Tokenizer.ts L307-L310 的默认分支,缩进改为由首个非空格字符位置计算,* item1的实际缩进为 2,但indent计算变为indent = line.search(nonSpaceChar)再加项目符号长度;更关键的是 L334-L339 不再执行listReplaceNesting重对齐,* item2与text的归属判定会遵循 CommonMark 缩进语义发生变化(item2可能因 4 空格缩进被当作外层项的缩进代码块或产生不同嵌套),输出结构将与list_item_text.html不一致。这解释了为什么该用例必须在 front matter 中强制pedantic: true——它本身就是一份锁定 pedantic 列表缩进语义的回归测试。
五、测试体系中的定位与复现方法
5.1 目录定位
该用例位于test/specs/new/目录。按 run-spec-tests.js 的加载逻辑,new/目录的用例使用无额外默认选项的parse函数(仅由每个用例自身的 front matter 决定选项),与original/(固定pedantic: true)、gfm/、commonmark/组成五组规范测试。
5.2 运行命令
仓库 package.json(test 脚本)提供了完整的规范测试入口:
# 仅运行规范测试(含 new/ 目录) npm run test:specs # 或仅运行规范测试的 only 标记用例 npm run test:specs:onlytest:specs实际执行node --test --test-reporter=spec test/run-spec-tests.js。若要为 new/ 组新增或修改用例,可运行npm run test:update(node test/update-specs.js)批量更新期望 HTML。运行前需先执行npm run build生成lib/下的 ESM 产物(run-spec-tests.js从../lib/marked.esm.js导入)。
5.3 手写最小验证脚本
也可绕过测试框架,直接构造Marked实例复现:
import { Marked } from './lib/marked.esm.js'; const src = ' * item1\n\n * item2\n\n text\n'; // pedantic 模式:与 list_item_text.html 期望一致 console.log(new Marked({ pedantic: true, gfm: false }).parse(src)); // 默认模式:结构不同(用于对照) console.log(new Marked({ gfm: false }).parse(src));注意 pedantic 模式下gfm会被rules.block = block.pedantic分支覆盖(见 Lexer.ts L48-L58),因此显式设置gfm: false仅用于与original/组配置保持一致、避免歧义。
六、扩展讨论:pedantic 对行内语法的连带影响
pedantic不只作用于块级列表,还通过inline.pedantic影响行内解析(Lexer.ts L49-L50)。相关实现点包括:
- 行内链接解析:
Tokenizer中 L716-L731 使用pedanticHrefTitle(rules.ts L79)以宽松方式拆分 href 与 title,并允许"只有左尖括号、没有右尖括号"的链接形式(if (this.options.pedantic && !endAngleBracket.test(trimmedUrl))); - 强调定界符:rules.ts L297 的注释指出,pedantic 的 LDelim 在
nextChar判断中排除开引号/闭引号,使*text*更宽松地触发强调。
这说明list_item_text所验证的固定indent = 2与listReplaceNesting重对齐只是 pedantic 语义在块级列表上的一个切片;同一选项还牵动链接、强调等行内规则,使用时应整体把握(详见 MarkedOptions.ts 选项说明 与 rules.ts 的 pedantic 分组)。
七、小结
list_item_text是 marked 中一份小而关键的 pedantic 回归用例,它验证了三件事:
- 固定缩进基准:pedantic 模式下列表项缩进硬编码为 2(Tokenizer.ts L301-L303),使 2 空格缩进的
text被判定为列表项延续内容; - 嵌套重对齐:
listReplaceNesting正则把 4 空格缩进的嵌套列表重新对齐为 2 空格(Tokenizer.ts L334-L336),保证* item2被递归解析为子<ul>; - 松散判定:空行使
list.loose = true(Tokenizer.ts L442-L448),最终渲染出<p>item1</p>、<p>text</p>的段落包裹结构。
理解这份用例,既能帮助你在使用pedantic: true处理遗留 Markdown 文档时预判列表结构,也能为阅读 marked 词法分析器(Lexer.ts、Tokenizer.ts)与规范测试体系(test/run-spec-tests.js)提供一条清晰的入口路径。
【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考