拓冰建站拓冰建站
首页 / 资讯中心 / 正文

marked 松散列表(loose list)解析与渲染原理:从 `list_loose` 测试用例到 Tokenizer 源码

marked 松散列表loose list解析与渲染原理从list_loose测试用例到 Tokenizer 源码【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked导读本文以 marked 仓库中的规范测试用例 list_loose.md 为切入点系统讲解 marked 如何判定一个 Markdown 列表是「松散列表loose list」还是「紧凑列表tight list」以及该判定如何影响最终的 HTML 输出。你将掌握松散列表的触发条件、loose标志在 Token 结构中的位置、从 Tokenizer.ts 到 Renderer.ts 的完整调用链并学会用仓库自带的 spec 测试验证行为。一、测试用例什么是list_loose1.1 用例原始输入test/specs/new/list_loose.md全文仅 5 行- item 1 - item 2 still item 21.2 期望输出 HTML同目录下的 list_loose.html 给出了该用例的基准输出ul li pitem 1/p /li li pitem 2/p pstill item 2/p /li /ul1.3 从输出反推行为对比输入与输出可以读出两条关键行为空行触发松散模式- item 1与第二个列表项之间有一个空行第二个列表项内部item 2与still item 2之间又有一个空行。因为列表项之间存在/包含空行整个列表被判定为loose松散。每个列表项都被p包裹在 loose 模式下marked 会把每个列表项的内容升级为独立段落——item 1、item 2、still item 2三个文本块各自生成一个p而不是只输出纯文本。也就是说这个用例刻意构造了「列表项之间空行 列表项内部多段文本」的组合用来固定 marked 的松散列表行为防止未来重构破坏它。二、源码中的松散列表判定2.1loose标志的 Token 结构在 Tokens.ts 中List和ListItem两个 Token 都带有loose: boolean字段export interface List { type: list; raw: string; ordered: boolean; start: number | ; loose: boolean; // 整个列表是否松散 items: ListItem[]; } export interface ListItem { type: list_item; raw: string; task: boolean; checked?: boolean; loose: boolean; // 单个列表项是否松散 text: string; tokens: Token[]; }值得注意的是loose是列表级的属性。只要列表被判定为松散Tokenizer.ts 会把所有列表项的loose都置为true——松散与否是「一个列表整体」的性质不会出现列表内部分项松散、部分紧凑的情况。2.2 判定逻辑两处检测点列表的loose判定分布在 Tokenizer.ts 的list()方法中分两个阶段。阶段一扫描列表项原始文本第 404-411 行在逐个切分列表项时每处理完一个列表项只要列表还不是 loose就检查两项if (!list.loose) { // If the previous item ended with a blank line, the list is loose if (endsWithBlankLine) { list.loose true; } else if (this.rules.other.doubleBlankLine.test(raw)) { endsWithBlankLine true; } }这里用到的正则来自 rules.tsblankLine: /^[ \t]*$/, doubleBlankLine: /\n[ \t]*\n[ \t]*$/,即列表项原始文本以「换行 空白 换行」结尾doubleBlankLine时认为该项以空行结束从而标记endsWithBlankLine使整个列表进入 loose 状态。阶段二分析列表项的子 Token第 437-449 行列表项全部切分完毕后marked 会先对每个列表项做一次块级分词再基于子 Token 中的space判定// First pass: tokenize items and finalize list.loose from spacers before placing checkboxes for (const item of list.items) { this.lexer.state.top false; item.tokens this.lexer.blockTokens(item.text, []); if (!list.loose) { // Check if list should be loose const spacers item.tokens.filter(t t.type space); const hasMultipleLineBreaks spacers.length 0 spacers.some(t this.rules.other.anyLine.test(t.raw)); list.loose hasMultipleLineBreaks; } }对应规则rules.tsanyLine: /\n.*\n/,anyLine能匹配「至少跨两行」的空白即列表项内部出现了多个换行例如item 2与still item 2之间夹着一个空行说明列表项内部存在多个段落列表因此变为 loose。2.3 loose 确定后的收尾处理判定结束后第 496-506 行还有一次「状态传播」// Set all items to loose if list is loose if (list.loose) { for (const item of list.items) { item.loose true; for (const token of item.tokens) { if (token.type text) { token.type paragraph; } } } }这一步很关键loose 列表中的每个textToken 都会被原地升级为paragraphToken。这是「松散列表项渲染出p」的直接原因——输出p的是paragraph类型而不是text类型。三、解析与渲染调用链3.1 Tokenizer 产出列表 Token结合 list_loose.md 的输入Tokenizer.ts 会先生成一个初始的ListTokenconst list: Tokens.List { type: list, raw: , ordered: isordered, // 无序列表为 false start: isordered ? bull.slice(0, -1) : , loose: false, // 初始为 false后续阶段判定 items: [], };之后在 while 循环中逐个收集列表项list_item最终经过阶段一、阶段二的判定后该列表的loose变为true两个列表项的textToken 都被升级为paragraph。3.2 Parser 分发到 RendererParser.ts 的parseTokens对list类型直接调用渲染器case list: { out this.renderer.list(token); break; }3.3 Renderer 输出 HTMLRenderer.ts 中的list()与listitem()决定最终标签结构list(token: Tokens.List): RendererOutput { const ordered token.ordered; const start token.start; let body ; for (let j 0; j token.items.length; j) { const item token.items[j]; body this.listitem(item); } const type ordered ? ol : ul; const startAttr (ordered start ! 1) ? ( start start ) : ; return type startAttr \n body / type \n as RendererOutput; } listitem(item: Tokens.ListItem): RendererOutput { return li${this.parser.parse(item.tokens)}/li\n as RendererOutput; }listitem内部把item.tokens交给parser.parse递归解析。由于 loose 判定阶段已把text升级为paragraphRenderer.ts 的paragraph渲染器就会为每个段落输出p.../pparagraph({ tokens }: Tokens.Paragraph): RendererOutput { return p${this.parser.parseInline(tokens)}/p\n as RendererOutput; }完整链路可归纳为list_loose.md → Tokenizer.list() 切分列表项检测空行/多行空白置 list.loose true → 将所有 item.loose 置 truetext → paragraph → Parser.parseTokens() 分发 list case → Renderer.list() → listitem() → parser.parse(item.tokens) → Renderer.paragraph() 输出 p…/p四、紧凑列表与松散列表的对比为加深理解用同一渲染管线对比两种列表形态。紧凑列表列表项之间没有空行、项内也没有多段文本- item 1 - item 2此时list.loose保持falseitem.tokens中是text类型输出为ul liitem 1/li liitem 2/li /ul松散列表即 list_loose.md 的场景- item 1 - item 2 still item 2输出为ul li pitem 1/p /li li pitem 2/p pstill item 2/p /li /ul两者的本质区别在于紧凑列表项内容以纯文本输出松散列表项内容以段落p输出。这也是 test/unit/Lexer.test.js 中大量断言loose: false的原因——例如- item 1\n- item 2这类无空行输入其List与ListItemToken 的loose字段都必须是false单元测试用精确的 Token 快照锁定了这一行为。五、松散列表与任务列表task list的交互loose标志不仅是渲染语义还影响 GFM 任务列表复选框的摆放位置。在 Tokenizer.ts 的「第二遍处理」中任务复选框的插入方式取决于list.loose列表是 loose复选框被插入到列表项第一个paragraph/textToken 内部tokens.unshift(checkboxToken)并同步修改raw/text保证复选框与段落文本处于同一段落列表是紧凑复选框直接item.tokens.unshift(checkboxToken)作为列表项的第一个内联 Token。这解释了仓库中的另一组用例 tasklist_basic.md 与 tasklist_blocks.md当任务列表中存在空行loose时复选框需要被正确放进段落 Token否则input会被错误地渲染到p之外。loose的最终确定必须发生在「放置复选框」之前这也是源码中刻意区分 first pass 与 second pass 的原因。六、如何运行与验证该用例6.1 运行 spec 测试该用例由test/run-spec-tests.js统一加载run-spec-tests.js 中通过getTests读取./specs/new目录每个.md/.html配对即是一条基准测试。执行npm run build npm run test:specstest:specs脚本对应node --test --test-reporterspec test/run-spec-tests.js见 package.json 的 scripts 字段。若未来list_loose的解析行为发生回归该用例会立即失败并指出实际输出与期望 HTML 的差异。6.2 只跑单元测试npm run test:unit对应node --test --test-reporterspec test/unit/*.test.js其中 Lexer.test.js 和 Parser.test.js 包含大量列表 Token 快照断言含loose字段。6.3 用 CLI 快速验证也可以直接调用 marked 库验证本用例的行为仓库为 ESM 项目type: module当前版本 marked 18.0.12node -e import { marked } from ./lib/marked.esm.js; console.log(marked.parse(- item 1\n-\n item 2\n\n still item 2));输出应与 list_loose.html 完全一致即每个列表项内容都被p包裹。七、要点总结触发条件列表项之间存在空行或某个列表项内部存在跨多行的空白多段落marked 即判定整个列表为 loose判定依据是 rules.ts 中的blankLine、doubleBlankLine、anyLine三个正则。判定位置全部位于 Tokenizer.ts 的list()方法内分「切分时检测endsWithBlankLine」与「分词后检测spaceToken」两步。传播规则loose是列表级状态一旦为true所有列表项loose置true且项内textToken 升级为paragraph最终由 Renderer.ts 的paragraph渲染器输出p。验证手段list_loose.md / list_loose.html 配对构成 spec 基准测试配合npm run test:specs与单元测试中的 Token 快照可长期锁定该行为。理解松散列表机制有助于排查「为什么我的列表渲染出了多余的空行或p标签」这类常见问题也为自定义list/listitem/paragraph渲染器通过options.renderer覆盖提供了正确的干预时机想要改变列表包装方式应在 Tokenizer 阶段关注loose判定在 Renderer 阶段改写list/listitem输出。【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门