
Prettier Markdown 有序列表对齐规则解析从align.md测试用例到list.js源码实现【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettierPrettier 是一个 opinionated 的代码格式化器其 Markdown 格式化器对有序列表的“对齐alignment”有一套精细且保守的规则只在少数明确可对齐的场景下补充空格其余情况一律保留原样。本文以仓库中的格式化测试用例 align.md 为骨架结合 list.js 与 preprocess.js 的源码实现逐条讲解对齐判定、前缀生成、缩进规避等核心机制。读完后你将能准确预测 Prettier 对任意有序/无序列表的格式化结果并理解其底层 doc 构建原理。一、测试用例的构成与运行方式align.md位于tests/format/markdown/list/目录下是 Prettier 格式化测试体系中的一组“输入文件”。同目录下的 format.test.js 只有一行核心调用runFormatTest(import.meta, [markdown], { proseWrap: always });它告诉测试框架用markdown解析器并以proseWrap: always配置对同目录下的每一个.md文件运行格式化然后将输入与输出一并写入快照文件snapshots/format.test.js.snap。该快照中align.md对应的条目完整记录了输入、输出以及生效选项parsers: [markdown]、proseWrap: always、printWidth: 80默认值。align.md本身按---分隔符thematic break切分成 11 个独立场景逐一验证有序列表的数字编号宽度1/11/111/1111/11111、标记后空格数1 个或 2 个、多列表项、以及嵌套列表在不同对齐状态下的表现。这是理解 Prettier 列表对齐行为最直接、最完整的测试样本。二、核心结论速览哪些输入保持不变哪些会被改写对照快照输出align.md的 11 个场景可以归纳为三类输入形式输出形式结论1. 123、11. 123、111. 123等单列表项11111. 123除外原样输出单列表项直接保留原始前缀宽度11111. 123单个超长编号原样输出11111. 123数字再长也只影响前缀不触发对齐补空格1. 123单个列表项、编号后 2 空格1. 123已经“对齐”的多余空格被保留1. 1232. 123两个编号等宽、各 2 空格1. 1232. 123已对齐列表保持不动11. 1231. 123首项 2 位次项 1 位且 1 空格11. 1231. 123未对齐列表不强行对齐原样保留11. 1231. 123次项编号后 2 空格与首项内容列对齐11. 1231. 123未对齐起始列不同但保留原样嵌套父项1.、子项1.后 4 空格子项前缀缩减为1.嵌套子列表前缀会被裁剪对齐嵌套父项1.后 2 空格、子项1.后 4 空格子项前缀缩减为1.保持与父项内容对齐父项已对齐时子项前缀对齐到内容列无序列表下嵌套1. 123原样保留混合嵌套不改变子前缀一句话总结Prettier 对有序列表采取“要么已经对齐就保持要么明显未对齐也不强行补空格”的保守策略真正动手修改的只有嵌套子列表的前缀宽度。三、源码级解析isAligned判定算法对齐判定发生在预处理器 preprocess.js 的markAlignedList中。它对 AST 中的每个list节点计算isAligned布尔值后续打印阶段依据该标记决定是否调用alignListPrefix。3.1 三层前置约束function markAlignedList(ast, options) { return mapAst(ast, (node, index, parentStack) { if (node.type list node.children.length 0) { // 1) 父列表未对齐子列表不可能对齐 for (let i 0; i parentStack.length; i) { const parent parentStack[i]; if (parent.type list !parent.isAligned) { node.isAligned false; return node; } } // 2) 列表后紧跟缩进代码块不对齐 const next parentStack[0]?.children[index 1]; if (next?.type code next.isIndented) { node.isAligned false; return node; } // 3) 正式判定 node.isAligned isAligned(node); } return node; }); }从源码结构看isAligned的判定受三个前置条件约束父链上任一列表未对齐则子列表继承false保证嵌套层级一致性列表紧邻缩进代码块时放弃对齐因为对齐会与代码块缩进语义冲突之后才进入isAligned函数本体。3.2 有序列表的判定细则isAligned(list)依次检查无序列表直接返回true- 123、- 123这类列表天然对齐。首项标记后有多余空格leadingSpaces.length 1则判定为已对齐对应1. 123或1. 1231. 123的情形——作者“有意”书写了宽空格视为对齐意图。列表项无内容start 为 -1返回false。只有一个列表项时firstStart % options.tabWidth 0才判定对齐。这解释了align.md中11111. 123的结果tabWidth默认为 2编号 5 位11111.共 7 列firstStart 66 % 2 0因此被标记为对齐而1. 123firstStart 00 % 2 0同样对齐保持单空格原样。多个列表项时若各列表项内容起始列不同如11. 123与1. 123的firstStart分别为 2 和 0返回false若起始列相同且firstStart % tabWidth 0返回true否则检查第二项标记后的空格数是否大于 1即11. 1231. 123的情形——第二项用 2 空格把内容对齐到第一项内容列同样视为对齐。getOrderedListItemInfo位于 utilities.js负责从原始文本中解析出列表项编号和标记后的空格串其正则^\s*(?numberText\d)(\.|\))(?leadingSpaces\s*)也印证了上述判定依赖的数据来源。四、源码级解析前缀生成与alignListPrefix打印阶段在 list.js 的printList中完成。getPrefix()先生成“原始前缀”再经过两轮加工4.1 原始前缀的生成const rawPrefix node.ordered ? (path.isFirst ? node.start : isGitDiffFriendlyOrderedList ? 1 : Math.min(node.start path.index, MAXIMUM_ORDERED_LIST_MARKER)) (nthSiblingIndex % 2 0 ? . : ) ) : nthSiblingIndex % 2 0 ? - : * ;要点有序列表编号按位置递增且以999_999_999CommonMark 规范允许的最大列表项编号为上限对应源码注释引用的 CommonMark 规范 行为交替列表项使用.与)两种标记形式相邻项标记符号交替这与nthSiblingIndex的奇偶有关git-diff-friendly模式proseWrap: always下默认开启见 start.md 测试会把后续项编号重置为 1保证 diff 友好hasGitDiffFriendlyOrderedListutilities.js通过检查第二项编号是否为 1、首项是否为 0 来识别该模式。4.2 对齐前缀与缩进规避let prefix node.isAligned node.ordered ? alignListPrefix(rawPrefix, options) : rawPrefix; if (prefix.length minIndent) { return prefix; } prefix prefix.trimEnd(); const trailingSpaces Math.min(minIndent - prefix.length, 4); // 5 will cause indented code block if (trailingSpaces 0) { prefix .repeat(trailingSpaces); } const leadingSpaces Math.min(minIndent - prefix.length, 3); // 4 will cause indented code block if (leadingSpaces 0) { prefix .repeat(leadingSpaces) prefix; }alignListPrefix按tabWidth默认 2把前缀总长度补到整数倍function alignListPrefix(prefix, options) { const additionalSpaces getAdditionalSpaces(); return prefix .repeat(additionalSpaces 4 ? 0 : additionalSpaces); function getAdditionalSpaces() { const restSpaces prefix.length % options.tabWidth; return restSpaces 0 ? 0 : options.tabWidth - restSpaces; } }这解释了1. 123前缀1.长度 3补 1 空格到 4为何保持 2 空格而11. 123前缀长度 4已是 2 的整数倍不加空格。requiredIndent逻辑list.js则保证列表后跟缩进代码块时前缀至少缩进到比代码块多 1 个空格的深度否则返回 0 不干预。4.3 嵌套列表的裁剪align.md中最“激进”的改写出现在嵌套场景1. 123 2. 123 1. 123 → 1. 123 2. 123 → 2. 123子列表前缀从 4 空格裁剪为 1 空格是因为打印子列表时使用了align( .repeat(prefix.length), ...)与clamp(options.tabWidth - listPrefix.length, 0, 3)的组合list.js对齐宽度按tabWidth计算并夹取在 0~3 之间——源码注释明确写着// 4 will cause indented code block即超过 4 个空格会被 Markdown 解析为缩进代码块因此必须收紧。这正是1.前缀长 3tabWidth2下子项从 4 空格降到 1 空格的原因。而第二个嵌套场景中父项为1. 123已对齐前缀长 4子项1.前缀被补齐到 4得到1. 123——子项内容与父项内容列严格对齐保持了整体视觉上的对齐层次。五、混合嵌套与边界行为align.md最后一个场景是无序列表中嵌套有序列表- 123 - 123 1. 123 2. 123输出保持不变。这印证了源码中的对称设计无序列表isAligned恒为true但alignListPrefix仅在node.ordered时生效node.isAligned node.ordered无序列表前缀固定为-/*不做空格补齐。另外两点边界行为值得注意MDX 差异从 mdast.js 可见options.parser mdx时走printListLegacy分支其getPrefix对isAligned的判定额外叠加了hasIndentedCodeblock条件workaround 对应 remark 的历史 issue。因此 MDX 文档中的列表对齐行为可能与标准 Markdown 略有差异。单列表项超大编号11111. 123保持原样说明编号位数本身不触发“补空格到整 tab 宽”的重写——补空格只发生在tabWidth余数非零且余数小于 4 时additionalSpaces 4 ? 0 : additionalSpaces例如9. 123前缀长 3会补成9. 123。六、可验证的实验方法如果你想亲自动手验证上述结论无需修改仓库直接使用仓库自带的测试与 CLI 即可运行快照测试在仓库根目录执行yarn jest tests/format/markdown/list会运行 format.test.js 并比对 快照。单文件格式化对任意 Markdown 输入执行yarn prettier --parser markdown --prose-wrap always tests/format/markdown/list/align.md可直接观察align.md的格式化输出需先yarn install。注意proseWrap仅对普通段落生效列表前缀宽度不受其影响但测试约定中统一使用always以覆盖段落换行场景。修改输入验证边界将align.md中的11111.换成9.或9999.观察前缀补空格行为随编号位数与tabWidth的变化可进一步验证第四节中的补空格规则。七、总结通过align.md这 11 个场景与源码的对照可以提炼出 Prettier Markdown 列表对齐的完整规则链判定markAlignedListpreprocess.js依据父链状态、后继代码块、首项空格数、内容起始列与tabWidth的余数关系计算isAligned生成printListlist.js生成带编号/符号的前缀alignListPrefix按tabWidth补齐空格约束一切补空格以“不超过 3~4 个空格、避免触发缩进代码块”为硬边界必要时裁剪嵌套子列表前缀例外MDX 走printListLegacy分支行为略有差异。这套“保守对齐 严格防缩进代码块”的设计正是 Prettier 在保持 Markdown 语义安全的前提下追求视觉整洁的典型体现。理解它之后你可以准确预测任何列表的格式化结果也能在贡献测试用例时快速定位对应的源码位置。【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考