开发工具格式化CLI【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址https://gitcode.com/gh_mirrors/pr/prettier点击查看免费下载本篇技术指南以 Prettier 仓库中的格式测试用例 tests/format/markdown/list/followed-by-indented-things.md 为切入点系统讲解 Prettier 在格式化 Markdown 列表时如何处理列表项后跟随缩进内容这一极易出错的场景——包括列表项文本与缩进代码块之间的对齐、嵌套列表的层级缩进以及 Markdown 将 4 空格及以上缩进解析为代码块这一语义陷阱。读完本文你将理解 Prettier Markdown 打印器的核心缩进策略align文档节点、requiredIndent计算、prefix生成掌握列表缩进对齐的底层原理并能据此预判自己项目中 Markdown 列表的格式化结果。一、测试用例概述这个文件在验证什么followed-by-indented-things.md位于tests/format/markdown/list/目录是一个格式测试输入文件fixture。它本身不是文档而是 Prettier 测试体系中的输入样例Prettier 会使用 Markdown 解析器对它进行格式化并将结果与 tests/format/markdown/list/snapshots/format.test.js.snap 中的快照逐一比对确保格式化输出稳定、符合预期。该测试目录的驱动脚本 tests/format/markdown/list/format.test.js 只有一行runFormatTest(import.meta, [markdown], { proseWrap: always });这意味着目录内所有*.md文件都会以markdown解析器、proseWrap: always选项运行格式测试。proseWrap: always会让段落文本在超出printWidth默认 80时自动换行从而测试长文本在列表项内的折行与缩进对齐。这个文件聚焦一个非常具体的主题列表项后面跟着各种缩进的东西indented things——缩进代码块、续写段落、嵌套列表、引用块、任务列表项等。Markdown 中这些内容与列表项的缩进关系决定了渲染结果而 Prettier 必须保证格式化前后语义不变同时输出整齐的缩进。二、核心问题缩进即语义4 空格是分界线MarkdownCommonMark 规范中缩进是决定语义的关键。最典型的一条规则是连续缩进 4 个空格或一个 Tab的内容会被解析为缩进代码块indented code block。Prettier 在打印列表时必须小心翼翼地处理缩进稍有不慎就会把列表项的续写文本变成代码块或把代码块变成普通文本。在 Prettier 源码中这一语义由 src/language-markdown/print/preprocess.js 的预处理逻辑标记const isIndented /^\n?(?: {4,}|\t)/.test(/* ... */); node.isIndented isIndented;即凡是以 4 个及以上空格或 Tab 开头的内容会被标记为缩进代码块isIndented。打印阶段 src/language-markdown/print/code.js 对这类节点输出固定 4 空格的alignif (node.isIndented) { const alignment .repeat(4); return align(alignment, [alignment, replaceEndOfLine(node.value, hardline)]); }于是 Prettier 面临的难题是列表项内容必须缩进到超过代码块的缩进才不会被误判为代码块。测试用例中多处出现形如- foo后跟 4 空格缩进文本的场景正是为了验证这条分界线的处理。三、逐场景拆解输入与输出对照快照 tests/format/markdown/list/snapshots/format.test.js.snap 中followed-by-indented-things.md的输入/输出完整保留。下面按场景类别逐一拆解。3.1 列表项文本 vs 缩进代码块顶层列表第一组对比最直观输入: - foo top level indented code block 输出不变: - foo top level indented code block输入中列表标记-后跟了 4 个空格foo前的内容被当作列表项文本后面 4 空格缩进的内容是顶层缩进代码块。Prettier 选择了原样保留——因为一旦把- foo改成- foo单空格后续代码块需要重新计算缩进而顶层列表的代码块基准是 4 空格任何改动都可能改变渲染语义。再看一个被修正的例子输入: 输出: - foo - foo list item text list item text输入中list item text缩进了 5 个空格这其实是列表项的续写段落不是代码块。输出中 Prettier 把- foo规范为- foo续写段落对齐到 2 空格列表标记-的长度语义不变排版更紧凑。3.2 嵌套列表中的缩进代码块输入: 输出: - foo - foo 保留 top level ... top level ...这里输入列表本身缩进了 3 空格。由于后续跟随缩进代码块Prettier 通过requiredIndent计算必须缩进量并让列表前缀保持足够的缩进使代码块与列表之间的相对关系不被破坏。类似的规则在有序列表1. foo和任务列表- [ ] foo场景中同样生效。3.3 列表项内的缩进代码块子内容输入: 输出: - foo - foo indented indented code block in bullet code block输入中代码块缩进了 8 空格。输出中列表标记被规范为- foo代码块内容缩进保持 6 空格即-的 2 空格 代码块基准 4 空格。注意Prettier 不会把列表项内的代码块缩进到统一 4 空格而是相对列表标记位置计算保证代码块始终嵌套在列表项内、且不会被提升为顶层代码块。3.4 嵌套列表的层级缩进输入: 输出: - item 1 - item 1 - item 1-1 - item 1-1 - item 1-2 - item 1-2 naive paragraph naive paragraph输入中嵌套子列表缩进了 4 空格Prettier 将其规范为2 空格一层嵌套的标准缩进续写段落同样对齐到 2 空格。这是最常见的列表项后跟嵌套列表场景。3.5 混合列表 代码块保持语义输入: 输出: - item 1 - item 1 - item 2 - item 2 top level top level indented code block indented code block两个列表项后跟一个顶层缩进代码块。输出完全保留输入列表前缀保持-4 空格标记代码块保持 4 空格缩进。为什么不能简化成- item 2 空格因为代码块是顶层元素若列表项缩进收窄代码块与列表的从属关系会被误解。这正是requiredIndent存在的意义。3.6 列表项内引用块与文本输入: 输出: - item 1 - item 1 quote quote text text top level code top level indented code block引用块 quote是列表项内容Prettier 将缩进从 8 空格规范为 5 空格-前缀长度 1后续文本同样对齐而top level indented code block作为顶层代码块保持在 4 空格与列表项内容明显区分。3.7 保持不动的场景防回归快照最后两个用例- one - one不变 two two以及多级嵌套列表 代码块的复杂组合均被原样保留。这说明 Prettier 在可能改变语义的情况下宁可不改这是防回归测试的核心价值。四、源码级原理Prettier 如何计算列表缩进理解了输出规则后我们来看 src/language-markdown/print/list.js 中的具体实现。4.1 列表前缀prefix生成getPrefix()list.js决定每个列表项前导符号无序列表按兄弟索引交替输出-与*nthSiblingIndex % 2 0 ? - : * 有序列表首个项用node.start后续项在 git-diff-friendly 模式下统一为1否则递增上限 999,999,999对应 CommonMark 有序列表标记上限。关键在后续代码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; }这里minIndent来自requiredIndent(path)。当列表项后紧跟缩进代码块时Prettier 会加宽列表前缀使列表项内容与代码块的缩进拉开距离避免列表项文本被吞进代码块或代码块被误读为列表内容。注释5 will cause indented code block直白地说明了设计约束前缀后的空格数一旦达到 5后续内容可能被解析成代码块。4.2 requiredIndent代码块缩进的上界计算requiredIndentlist.js是本节的核心函数function requiredIndent(path) { const { node, next } path; if (node.checked null) { return 0; } if (!(next?.type code next.isIndented)) { return 0; } const leadingSpaces /^[ \t]*/.exec(next.value)?.[0] ?? ; return ( 4 // base indent of the code block [...leadingSpaces].reduce( (count, char) count (char \t ? 4 : 1), 0, ) 1 // at least one space more than the code block ); }它的逻辑列表项不是任务列表项checked null或后继节点不是缩进代码块时返回 0无需特殊缩进否则计算代码块基准缩进 4 空格 代码块首行自身的缩进Tab 按 4 计 至少 1 空格确保列表项内容始终比代码块多缩进至少 1 个空格从根源上杜绝语义混淆。4.3 列表项内容的对齐printListItemlist.js处理列表项内部的子节点任务列表项[x]/[ ]前缀由node.checked决定首个非列表、非 HTML 节点按前缀长度对齐缩进代码块直接原样输出if (node.type code node.isIndented) return print();其他内容使用clamp(options.tabWidth - listPrefix.length, 0, 3)计算对齐空格——上限 3同样是防止 4 空格触发代码块语义。clamp与tabWidth直接相关tabWidth默认值是 2见 src/main/core-options.evaluate.js 中tabWidth: { type: int, default: 2, ... }。当tabWidth增大如设为 4列表项内容的对齐缩进也会相应增大这正是为什么tab-width/目录下有专门的缩进代码块测试tests/format/markdown/list/tab-width/indented-code-block.md。4.4 列表项间空行规则src/language-markdown/print/children.js 的shouldPrePrintDoubleHardline决定列表项之间是否保留空行松散列表 loose list 语义。测试用例中多个列表项 顶层代码块的输出保留了列表项间空行与代码块间的空行正是这一逻辑的体现。五、测试如何验证快照与驱动脚本Prettier 的格式测试采用快照测试机制每个*.md输入文件经runFormatTest格式化后与同名快照对比。以本用例为例快照 tests/format/markdown/list/snapshots/format.test.js.snap 中记录了完整输入与输出头部还会标注测试选项parsers: [markdown] proseWrap: always printWidth: 80 (default)这说明该用例在默认printWidth: 80、proseWrap: always下运行。如果你想亲自复现可以在仓库根目录运行yarn test tests/format/markdown/list或针对单个文件运行Prettier 测试框架支持按路径过滤yarn jest tests/format/markdown/list/format.test.js -t followed-by-indented-things修改快照需谨慎-u标志会更新快照但本项目仓库只读请勿修改文件仅用于本地验证理解。六、同类测试一览列表缩进测试矩阵tests/format/markdown/list/目录是一组围绕列表格式化的测试矩阵理解它们有助于把握followed-by-indented-things.md在整个测试体系中的位置测试文件验证主题followed-by-indented-things.md列表后跟缩进代码块/段落/嵌套列表的缩进对齐nested.md多级嵌套列表的标准缩进nested-tab.mdTab 缩进的嵌套列表规范为空格loose.md松散列表项间空行的保留task-list/任务列表项[ ]/[x]格式tab-width/indented-code-block.md不同tabWidth下缩进代码块的处理align.md有序列表标记对齐alignListPrefixgit-diff-friendly.md有序列表后续项重置为1的 git 友好模式其中 align.md 对应的alignListPrefix函数list.js会把有序列表前缀补齐到tabWidth的整数倍如11.对齐到11.之后再对齐子内容这与本文件中的1. foo前缀加宽逻辑互为补充。七、实践启示写 Markdown 时的避坑建议基于上述原理可以提炼出几条对日常写作有直接价值的结论不要手动用 4 空格对齐列表项内容。Markdown 会把 4 空格视为代码块起始列表项续写段落应使用 2 空格一层嵌套或tabWidth的整数倍其余交给 Prettier 统一。列表项后紧跟代码块时Prettier 可能故意加宽列表前缀如- foo这不是 bug而是为了防止代码块语义被破坏。嵌套列表统一用 2 空格缩进Prettier 会把 3、4、8 空格等不规则缩进规范为 2 空格层级。顶层缩进代码块必须与列表保持 4 空格基准当它紧跟在列表项之后时Prettier 会原样保留输入以避免误判。修改tabWidth会影响列表项内容的对齐宽度且对齐空格上限为 3这是代码块语义的硬约束。八、总结followed-by-indented-things.md虽是一个仅 133 行的测试输入文件却浓缩了 Prettier Markdown 打印器中最精妙的缩进权衡在美观输出与语义保真之间Prettier 通过requiredIndent的强制缩进上界、clamp(..., 0, 3)的对齐上限、以及isIndented的代码块标记将 Markdown 的缩进歧义转化为可预测、可测试的格式化规则。理解这份测试用例就等于理解了 Prettier 处理 Markdown 列表缩进的完整决策模型——无论是排查自己项目中的格式化差异还是向他人解释某个奇怪的输出本文介绍的源码与测试路径都能提供确凿依据。赞分享开发工具格式化CLI【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址https://gitcode.com/gh_mirrors/pr/prettier点击查看免费下载相关推荐Prettier Markdown 引文块blockquote格式化的深入解析基于 indented-greater-than.md 测试用例Prettier Markdown 引文块blockquote格式化的深入解析基于 indented greater than.md 测试用例 本篇技术指开发工具格式化CLIPrettier Markdown 列表格式化实战从 issue-8004 回归测试看嵌套列表缩进与对齐算法Prettier Markdown 列表格式化实战从 issue 8004 回归测试看嵌套列表缩进与对齐算法 导读 本文以 Prettier 仓库中的回归测试开发工具格式化CLIPrettier Markdown 列表格式化指南宽松列表Loose List的缩进与空行处理Prettier Markdown 列表格式化指南宽松列表Loose List的缩进与空行处理 Prettier 是一款有主见的代码格式化器opinio开发工具格式化CLI上一篇终极邮件生成解决方案10分钟掌握mailgen专业级HTML邮件开发下一篇第三方启动器Bloxstrap如何让Roblox游戏体验提升5个档次创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考