ARTICLE DETAIL

资讯详情

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

Biome Markdown 格式化器深度解析:有序列表标记的 9 位数字上限与重编号行为(issue-17778)

Biome Markdown 格式化器深度解析:有序列表标记的 9 位数字上限与重编号行为(issue-17778) Biome Markdown 格式化器深度解析有序列表标记的 9 位数字上限与重编号行为issue-17778【免费下载链接】biomeA toolchain for web projects, aimed to provide functionalities to maintain them. Biome offers formatter and linter, usable via CLI and LSP.项目地址: https://gitcode.com/gh_mirrors/bi/biome有序列表是 Markdown 中最常用的块级语法之一但其标记规则远比表面看起来复杂。Biome 在crates/biome_markdown_formatter/tests/specs/prettier/markdown/list/parser-regression/issue-17778.md中用一个精巧的回归测试用例专门验证了有序列表标记超过 9 位数字时的解析与格式化行为。本文以该测试规格为骨架结合 Biome 的 Lexer、Parser 与 Formatter 源码逐层剖析 CommonMark §5.2 的 9 位数字上限是如何在 Biome 中落地实现的以及 Biome 与 Prettier 在这一边界场景下的行为差异。读完本文你将理解 Biome 处理超长有序列表标记的完整调用链并掌握如何复现与验证这一行为。测试规格全景issue-17778 到底在测什么关联文档 issue-17778.md 是 Biome Markdown 格式化器的回归测试输入文件全文仅 4 行内容1. list item 999999999. list item 1. list item 1000000000. ordered list marker cant have more than 9 digits这份输入被刻意设计成一组对比实验行内容数字位数是否为合法有序列表标记1. list item1 位是999999999. list item9 位是恰好是上限值1. list item1 位是1000000000. ordered list marker cant have more than 9 digits10 位否超出上限第二行的注释文字甚至直接把结论写进了测试里ordered list marker cant have more than 9 digits。这正是对 CommonMark 规范的直接引用——有序列表标记最多只能有 9 位数字。与之配套的快照文件 issue-17778.md.snap 完整记录了输入、Prettier 差异和 Biome 的格式化输出1. list item 2. list item 3. list item 1000000000. ordered list marker cant have more than 9 digits也就是说Biome 将前三个合法标记1.、999999999.、1.识别为同一个有序列表并重新编号为1.、2.、3.而 10 位数字的1000000000.行被当作普通文本原样保留。这个测试文件虽然只有 4 行却同时覆盖了解析器的标记识别边界和格式化器的列表重编号两大核心逻辑。CommonMark §5.2 的 9 位上限从规范到常量有序列表标记的位数限制并非 Biome 的独创而是 CommonMark 规范明确定义的。Biome 的 Markdown 解析器用常量把这个规范固化到了源码中。在 crates/biome_markdown_parser/src/syntax/mod.rs 中可以找到这条约束的直接实现// CommonMark §5.2 caps ordered list markers at 9 digits. pub(crate) const MAX_ORDERED_LIST_MARKER_DIGITS: usize 9;同样的约束在 crates/biome_markdown_parser/src/syntax/list.rs 的模块文档注释中也有完整表述//! ## Ordered List Markers (§5.2) //! - 1. through 999999999. (1-9 digits followed by .) //! - 1) through 999999999) (1-9 digits followed by ))这里有两个值得注意的细节标记结构有序列表标记由19 位数字 分隔符组成分隔符既可以是.点号也可以是)右括号即1.、1)、10.、999999999)都是合法标记位数上下限至少 1 位数字至多 9 位数字。999999999.恰好是 9 位因此是合法的——这正是测试用例第二行选择它的原因而1000000000.有 10 位数字超出了MAX_ORDERED_LIST_MARKER_DIGITS的上限不再是合法标记。值得一提的还有文件注释中列出的解析器限制为避免病态输入深度嵌套列表导致栈溢出嵌套深度由MarkdownParserOptions::max_nesting_depth限制默认 100更深的嵌套会发出诊断并降级处理。这说明 Biome 的 Markdown 解析器在规范兼容之外还做了健壮性防护。Lexer 层10 位数字如何被降级为文本9 位上限在词法分析Lexing阶段就已经生效。当 Lexer 遇到一行以数字开头的文本时会尝试将其识别为有序列表标记识别逻辑位于 crates/biome_markdown_parser/src/lexer/mod.rs 的consume_ordered_list_marker_or_textual函数/// Try to consume an ordered list marker (e.g., 1., 2), 10.). /// Returns MD_ORDERED_LIST_MARKER if valid, otherwise falls back to textual. /// Per CommonMark: 1-9 digits followed by . or ) followed by whitespace. fn consume_ordered_list_marker_or_textual(mut self) - MarkdownSyntaxKind { self.assert_at_char_boundary(); let start_position self.position; let mut digit_count 0; // Consume 1-9 digits while let Some(byte) self.current_byte() { if byte.is_ascii_digit() { self.advance(1); digit_count 1; // CommonMark limits to 9 digits max if digit_count MAX_ORDERED_LIST_MARKER_DIGITS { // Too many digits, not a valid marker self.position start_position; return self.consume_textual_impl::false(MarkdownLexContext::Regular); } } else { break; } } // Must have at least one digit if digit_count 0 { self.position start_position; return self.consume_textual_impl::false(MarkdownLexContext::Regular); } // Must be followed by . or ) let delimiter self.current_byte(); if !matches!(delimiter, Some(b. | b))) { self.position start_position; return self.consume_textual_impl::false(MarkdownLexContext::Regular); } self.advance(1); // Must be followed by at least one space (or end of line for edge cases) // ... }这段代码清晰地呈现了合法有序列表标记的三个必要条件19 位 ASCII 数字一旦digit_count超过MAX_ORDERED_LIST_MARKER_DIGITS9立即回退self.position start_position并调用consume_textual_impl把整段内容当作普通文本处理数字后紧跟.或)否则同样降级为文本分隔符后必须跟随空白或行尾这是 CommonMark 中标记后面要有空格要求的体现。这正好解释了测试用例的第四行1000000000.在第 10 位数字处触发位数检查Lexer 判定它不是合法标记将整行作为文本 token 输出因此它永远不会成为列表项。此外Lexer 还在同一文件 crates/biome_markdown_parser/src/lexer/mod.rs 的另一个分支中再次使用digit_count MAX_ORDERED_LIST_MARKER_DIGITS做防御性检查确保所有数字开头的路径都遵守同一上限。Parser 层9 位标记如何进入有序列表 AST词法层面产出MD_ORDERED_LIST_MARKERtoken 后语法分析器需要判定行首出现有序列表标记才算一个列表项。这一判定逻辑位于 crates/biome_markdown_parser/src/syntax/list.rs/// Check if were at the start of an ordered list item (e.g., 1., 2)). /// /// An ordered list marker is a sequence of 1-9 digits followed by . or ), /// at the start of a line. pub(crate) fn at_order_list_item(p: mut MarkdownParser) - bool { at_order_list_item_with_base_indent(p, list_marker_base_indent(p)) } fn at_order_list_item_with_base_indent(p: mut MarkdownParser, base_indent: usize) - bool { p.lookahead(|p| { if !list_item_within_indent(p, base_indent) { return false; } skip_leading_whitespace_tokens(p); // Check for ordered list marker token at line start if !p.at(MD_ORDERED_LIST_MARKER) { return false; } p.bump(MD_ORDERED_LIST_MARKER); marker_followed_by_whitespace_or_eol(p) }) }由于 Lexer 已经完成了 9 位数字的过滤Parser 只需确认当前 token 是MD_ORDERED_LIST_MARKER且后随空白或行尾即可安全地把该行作为有序列表项MdOrderedListItem进入 AST。有趣的是解析器对纯文本形式的有序标记即未通过 Lexer 识别、以普通文本 token 呈现的数字开头行也有兜底逻辑。crates/biome_markdown_parser/src/syntax/list.rs 中的textual_starts_with_ordered_marker函数同样检查digit_count MAX_ORDERED_LIST_MARKER_DIGITS并返回false——即便在文本回退路径上10 位数字也永远不可能被当作有序列表项。两道防线共同保证了规范一致性无论走哪条路径1000000000.都不会成为列表项。对于测试输入而言结果是确定的1. list item、999999999. list item、1. list item空行分隔解析为同一个有序列表的三个项起始号为 11000000000. ordered list marker cant have more than 9 digits解析为普通文本内容排在列表之后。Formatter 层列表重编号与标记规范化解析完成后轮到格式化器决定输出。Biome 的 Markdown 格式化器会对有序列表做统一重编号核心实现位于 crates/biome_markdown_formatter/src/bullet_list.rs/// The marker choice for a single parsed ordered list node. struct OrderedMarkerPlan { start: usize, delimiter: OrderedListDelimiter, use_git_diff_friendly_numbering: bool, } impl OrderedMarkerPlan { fn from_list(node: MdBulletList, list_sibling_index: usize) - OptionSelf { let numbers node .iter() .filter_map(|bullet| bullet.ordered_marker_number()) .take(3) .collect::Vec_(); let start numbers.first().copied()?; let use_git_diff_friendly_numbering has_git_diff_friendly_ordered_list(numbers); Some(Self { start, delimiter: ordered_delimiter_for_list(list_sibling_index), use_git_diff_friendly_numbering, }) } /// Returns the ordered marker for a bullet, including its delimiter. fn marker_for_index(self, index: usize) - OrderedMarker { let number if index 0 { self.start } else if self.use_git_diff_friendly_numbering { 1 } else { self.start.saturating_add(index) }; OrderedMarker::new(number, self.delimiter) } }这段代码揭示了重编号的三条规则首项保留起始号index 0时输出self.start。CommonMark 以列表首项数字作为起始值因此格式化后首项仍保持原始起始数字顺序列表递增编号默认情况下后续项按start index递增。这就是测试输入中999999999.和第二个1.分别被重写为2.、3.的原因——它们属于同一列表编号被归一化为从 1 开始连续递增Git diff 友好编号当源码采用1, 1, 1这类每项都从 1 开始的写法时格式化器会保留这种风格。判定函数has_git_diff_friendly_ordered_list的注释crates/biome_markdown_formatter/src/bullet_list.rs给出了完整判定矩阵1, 2, 3是顺序列表、1, 1, 1是 Git diff 友好列表、10, 1, 2按10, 1, 1输出、0, 1是顺序列表、0, 1, 1则判定为 Git diff 友好。其价值在于中间插入新项时只有一行发生变化Git 差异更小。分隔符的选择同样有讲究ordered_delimiter_for_listcrates/biome_markdown_formatter/src/bullet_list.rs会依据列表在相邻兄弟列表中的序号在.与)之间交替从而避免格式化后相邻的两个列表被重新解析合并成一个。这与无序列表标记-、*、的交替策略同文件 L440-L448思路一致。数字最终如何写入输出由 crates/biome_markdown_formatter/src/markdown/auxiliary/list_marker_prefix.rs 中的OrderedMarker类型负责——它按位分解数字并用 token 逐位输出同时附上.或)分隔符其width()方法则计算数字位数 分隔符宽度用于对齐多行列表的缩进min_post_marker_len保证标记后至少有指定数量的空格。此外该文件中还处理了有序标记分隔符的规范化当源码使用1)形式时格式化器会将其转换为1.见FormatMdListMarkerPrefix中is_ordered_with_paren()分支。Prettier 差异一处值得注意的行为分歧快照文件中的 Prettier differences 部分记录了 Biome 与 Prettier 在 10 位数字标记上的分歧--- Prettier Biome -2,4 2,4 2. list item 3. list item -4. ordered list marker cant have more than 9 digits 1000000000. ordered list marker cant have more than 9 digitsPrettier把 10 位数字的行也视为列表项并重编号为4.即最终输出为1. 2. 3. 4.四行完整列表Biome严格遵守 CommonMark §5.2将1000000000.保留为原始文本只对三个合法标记重编号。从规范角度讲Biome 的行为更贴近 CommonMark 原文An ordered list marker is a sequence of 1–9 arabic digits。Prettier 的实现则相对宽松在位数超限时仍按列表项处理。这类差异正是parser-regression目录存在的意义——把上游Prettier 测试集暴露过的边界场景固化为回归用例防止行为在后续重构中漂移。测试如何驱动从测试规格到快照该测试规格通过宏自动接入测试框架。crates/biome_markdown_formatter/tests/prettier_tests.rs 中有一行tests_macros::gen_tests! {tests/specs/prettier/markdown/**/*.{md}, crate::test_snapshot, }gen_tests!宏会遍历tests/specs/prettier/markdown/下的所有.md文件为每个文件生成一个测试函数test_snapshot函数同文件 L13-L29用PrettierSnapshot比较 Biome 输出与 Prettier 参考输出同时生成/校验.snap快照。因此issue-17778.md天然成为一个自动化的回归测试任何对解析器或格式化器的改动如果改变了 9 位数字标记的行为快照测试都会立即失败告警。在本地复现该行为仅查看与运行不修改仓库的方式# 运行 Biome Markdown 格式化器的全部 prettier 规格测试含 issue-17778 cargo test -p biome_markdown_formatter # 若安装了 biome CLI也可直接格式化测试输入验证 biome format crates/biome_markdown_formatter/tests/specs/prettier/markdown/list/parser-regression/issue-17778.md同目录下的其他回归用例如 issue-19152.md测试制表符与空格混合的列表缩进共同构成了有序列表边界场景的覆盖矩阵。小结一条规范如何贯穿 Lexer → Parser → Formatter回顾整个调用链CommonMark §5.2 的有序列表标记最多 9 位数字在 Biome 中被完整地实现为一道贯穿三层的约束Lexercrates/biome_markdown_parser/src/lexer/mod.rs数字位数超过 9 即放弃标记识别降级为文本 tokenParsercrates/biome_markdown_parser/src/syntax/list.rs仅接受MD_ORDERED_LIST_MARKERtoken 作为列表项起点并对文本形式的数字行二次校验位数Formattercrates/biome_markdown_formatter/src/bullet_list.rs对合法列表项统一重编号保留起始号、连续递增或 Git diff 友好编号非法标记行保持原文。issue-17778 这个只有 4 行的测试文件恰好踩中了这条链路中最微妙的一个边界999999999.9 位合法上限与1000000000.10 位非法仅一位之差却走向完全不同的格式化结果。理解这个用例也就理解了 Biome 在规范合规与格式化友好之间取舍的典型范式——当两者冲突时规范优先。【免费下载链接】biomeA toolchain for web projects, aimed to provide functionalities to maintain them. Biome offers formatter and linter, usable via CLI and LSP.项目地址: https://gitcode.com/gh_mirrors/bi/biome创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表