ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

marked 有序列表数字对齐解析规则全解析:从 list_align_number 规范到源码实现

marked 有序列表数字对齐解析规则全解析:从 list_align_number 规范到源码实现 marked 有序列表数字对齐解析规则全解析从 list_align_number 规范到源码实现【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked导读本文以 marked 仓库中的 test/specs/new/list_align_number.md 规范测试为核心深入剖析 marked 如何解析数字编号逐级对齐的有序列表为什么 1、10、100、1000、10000 这类编号会以不同缩进呈现为什么编号位数不足 4 位的项会被判为缩进代码块以及start属性如何驱动ol start10的渲染。读者将掌握 CommonMark 缩进语义在列表解析中的实际体现并能基于 src/Tokenizer.ts、src/rules.ts 与 src/Renderer.ts 的源码证据理解 marked 的列表判定全链路。一、规范用例list_align_number 的输入与期望输出该规范文件位于 test/specs/new/list_align_number.md全文仅 5 行是 marked 针对有序列表编号右对齐场景设计的典型输入1. Item 1 10. Item 10 100. Item 100 1000. Item 1000 10000. Item 10000对应的期望输出在 test/specs/new/list_align_number.html 中定义precode1. Item 1 /code/pre ol start10 liItem 10/li liItem 100/li liItem 1000/li liItem 10000/li /ol这是一个相当反直觉的结果它揭示了 marked以及 CommonMark 规范对列表标记缩进的一条关键规则有序列表标记最多允许 3 个前置空格。超过 3 个空格的行不再被当作列表项而是退化为缩进代码块。逐行分析如下输入行前置空格数判定结果原因1. Item 10缩进代码块无编号不构成列表项且后续行整体进入代码块上下文10. Item 101列表项编号 10前置空格 1 个符合{0,3}规则100. Item 1002列表项编号 100前置空格 2 个1000. Item 10003列表项编号 1000前置空格 3 个10000. Item 100004列表项编号 10000前置空格 4 个由start决定从 10 开始连续编号注意第一行1. Item 1本身没有前置空格但它出现在整个多行块的首位且不能与后续行构成列表因为后续行缩进不同、且首行编号 1 与后续编号不在同一缩进层级marked 最终将首行判定为缩进代码块。这正是该规范用例存在的意义它锁定了编号对齐 缩进阈值组合下的确定性行为防止解析器在不同版本间产生漂移。二、列表解析的入口Lexer 与 Tokenizer 的分工在 marked 中块级语法由 src/Lexer.ts 驱动。在 src/Lexer.ts 中blockTokens会对每个块级 token 依次尝试匹配其中列表匹配位于// list if (token this.tokenizer.list(src)) { tokens.push(token); src src.substring(token.raw.length); continue; }也就是说当 src/Tokenizer.ts 的list()方法返回一个有效的Tokens.List对象时Lexer 会消费其raw原文并继续处理剩余输入若返回undefined判定失败则尝试其他块级规则。这一调用关系说明列表是否成立完全由 Tokenizer 的list()方法内的正则与状态机决定而 Lexer 只负责按顺序尝试各规则并推进源码游标。三、源码级剖析list() 的判定逻辑src/Tokenizer.ts 是理解本规范用例的核心。关键步骤如下3.1 首个标记的捕获与有序性识别list(src: string): Tokens.List | undefined { let cap this.rules.block.list.exec(src); if (cap) { let bull cap[1].trim(); const isordered bull.length 1;bull是首个列表标记如10.isordered bull.length 1意味着只要标记长度大于 1 就被视为有序列表。由于有序标记形如数字 点号其长度必然大于 1因此这一判断在语义上等价于是否为有序列表。3.2 有序列表 start 值的提取const list: Tokens.List { type: list, raw: , ordered: isordered, start: isordered ? bull.slice(0, -1) : , ... };start取值为bull去掉最后一个字符点号后转为数字。对本用例而言首个匹配到的标记是10.因为1. Item 1不在匹配范围内见下节因此start 10 10。3.3 后续列表项的正则约束bull isordered ? \\d{1,9}\\${bull.slice(-1)} : \\${bull}; // Get next list item const itemRegex this.rules.other.listItemRegex(bull);对于有序列表后续列表项的标记被约束为\d{1,9}加原来的分隔符.即编号允许 1~9 位数字。listItemRegex定义在 src/rules.tslistItemRegex: (bull: string) new RegExp(^( {0,3}${bull})((?:[\t ][^\n]*)?(?:\\n|$))),这就是缩进阈值的关键所在^ {0,3}明确限定列表标记前最多只能有 3 个空格。这正是 CommonMark 对列表标记缩进的语义任何以 4 个或更多空格开头的行都属于缩进代码块而不是列表项。3.4 为什么1. Item 1没有成为列表项本用例的输入中首行1. Item 1前置 0 个空格单独看完全符合^ {0,3}约束。但它不是首个被匹配的标记——Lexer 逐块消费源码时1. Item 1与后续四行组成了一个更大的块而该块的整体结构被判定为缩进代码块见后文整体块级判定因此list()从未以1.作为首个标记执行。这也解释了期望输出中第一行被渲染为precode的原因它被归入缩进代码块而代码块内容不会进入列表解析。四、渲染阶段start 属性如何生成ol start10列表解析完成后token 进入 src/Renderer.ts 的渲染阶段。核心实现在 src/Renderer.tslist(token: Tokens.List): RendererOutput { const ordered token.ordered; const start token.start; ... const type ordered ? ol : ul; const startAttr (ordered start ! 1) ? ( start start ) : ; return type startAttr \n body / type \n as RendererOutput; }这里体现了 marked 的一个渲染细节当且仅当start ! 1时才会输出start属性本用例start 10因此输出ol start10若列表从 1 开始如普通1. a\n2. b则不会输出冗余的start1输出干净的ol。列表项本身的渲染则通过 src/Renderer.ts 的listitem()完成每个li内部的内容交由parser.parse(item.tokens)递归渲染。注意期望输出中前两个li之间没有额外的空行或包裹标签Item 10、Item 100等作为纯文本段落输出与源码保持一致。五、整体块级判定为何首行落入代码块要理解1. Item 1的归属需要结合 src/rules.ts 中的块级规则顺序。在 marked 中缩进代码块的规则优先于段落与部分列表场景。相关规则定义于 src/rules.ts 附近其中indented代码块规则匹配 4 个空格或一个 tab 的缩进。对本用例而言整体输入可拆分为两层第 1 行1. Item 1以 0 空格开头但第 3 行起100. ...有 2 空格、第 4 行1000. ...有 3 空格、第 5 行10000. ...有 4 空格的缩进递增使整块无法在list()的首标记匹配中被整体捕获由于列表规则要求整个列表的所有项共享同一缩进层级1.与后续项的缩进不一致marked 将该块降级为其他块类型最终首行被渲染为缩进代码块。从源码结构看这一行为正是listItemRegex中{0,3}缩进上限与Lexer块级规则优先级共同作用的结果。该规范用例的价值在于它把编号长度增长导致缩进递增这一边界情形固化为回归测试确保任何对列表正则的改动都不会破坏这一行为。六、规范测试体系new 目录如何参与验证list_align_number属于 marked 的new规格目录即 marked 自定义的扩展/边界规范与 test/specs/commonmarkCommonMark 官方用例、test/specs/gfm、test/specs/originalMarkdown.pl 原始用例、test/specs/redos性能安全用例并列。运行规范测试的命令定义在 package.jsonnpm run test:specs # 完整运行全部规范用例含 new 目录 npm run test:specs:only # 仅运行标记为 .only 的规范用例测试入口为 test/run-spec-tests.js其中明确将new目录纳入测试范围test/run-spec-tests.jsconst [commonMarkTests, gfmTests, newTests, originalTests, redosTests] [ resolve(__dirname, ./specs/commonmark), resolve(__dirname, ./specs/gfm), resolve(__dirname, ./specs/new), resolve(__dirname, ./specs/original), resolve(__dirname, ./specs/redos), ... ];测试框架会将list_align_number.md的输入交给 marked 编译并逐字节比对输出与list_align_number.html。任何解析或渲染行为的变化都会导致该用例失败从而保护本规范所锁定的行为不被回归。七、给使用者的实践要点结合本规范用例与源码使用 marked 时应注意以下几点列表标记最多允许 3 个前置空格。若你的 Markdown 中列表项意外显示为代码块先检查是否出现了 4 个空格以上的缩进。这是 CommonMark 与 marked 共同的硬性约束。有序列表的起始编号由首个列表项的编号决定。marked 会通过start属性保留这一语义src/Renderer.ts因此10.开头的列表渲染为ol start10浏览器会据此正确显示从 10 开始的编号无需手动改写 HTML。编号位数不齐右对齐排版的列表可能产生非预期结果。当编号从 1 位增长到 5 位时前置空格随之递增超过 3 空格阈值的项将脱离列表语义。若需要长编号列表建议使用等宽对齐的 4 空格方案并接受代码块结果或在业务层预先规范化编号。编号最大支持 9 位\d{1,9}见 src/Tokenizer.ts超过 9 位的编号不会被识别为列表标记。八、总结list_align_number虽然只有 5 行输入却是 marked 列表解析边界行为的一个缩影它同时验证了列表标记 0~3 空格缩进阈值、有序列表 start 值提取与渲染以及缩进代码块与列表的优先级判定三条关键规则。通过对 src/Tokenizer.ts、src/rules.ts 与 src/Renderer.ts 的源码解读读者可以清晰地看到 marked 如何将 CommonMark 的缩进语义映射为可回归验证的确定性输出——这正是该规范文件在仓库测试体系中的核心价值。【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址: https://gitcode.com/gh_mirrors/ma/marked创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表