Prettier Markdown 格式化:修复列表项与嵌套子列表之间空行被删除的问题(issue 17746)

发布时间:2026/9/20 23:07:09
Prettier Markdown 格式化:修复列表项与嵌套子列表之间空行被删除的问题(issue 17746)
Prettier Markdown 格式化修复列表项与嵌套子列表之间空行被删除的问题issue #17746【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier导读本文围绕 Prettier 仓库中针对 GitHub issue #17746 的回归测试用例 issue-17746-fenced-code-then-nested-list.md 展开讲解 Prettier 在 Markdown/MDX 格式化过程中如何保留列表项内容如围栏代码块、缩进代码块、段落与嵌套子列表之间的空行。读完本文你将理解 Prettier 的 Markdown 测试驱动开发方式、快照测试的输入/输出约定、问题复现用例的写法以及底层打印器 children.js 中空行保留逻辑的实现原理并掌握在本地运行该用例验证行为的方法。一、issue #17746 是什么问题Prettier 是有主见opinionated的代码格式化器它对 Markdown 的处理同样遵循严格的规则。但在某些边界场景下规则会与用户的书写意图冲突。issue #17746 反映的正是这样一类问题当列表项list item内部包含代码块围栏代码块或缩进代码块或段落且其后紧跟一个嵌套子列表时Prettier 会删掉两者之间的空行导致原本清晰的层次结构被压缩成紧凑列表。在 CHANGELOG.md 中该修复被记录为Markdown: Fix blank lines between list items and nested sub-lists being removed in Markdown/MDX由 byplayer 在 PR #17746 中提交这条修复同时作用于 Markdown 与 MDX 两种解析器对应的实现与测试都落在src/language-markdown/目录下。为什么空行很重要Markdown 的列表是否松散loose由空行决定列表项之间若存在空行该列表会被解析为松散列表。对于包含嵌套结构的文档空行是视觉上区分外层列表项内容与内层子列表的关键分隔符。删除空行后围栏代码块会直接贴住子列表不仅在视觉上产生歧义还可能影响部分渲染器对列表紧/松状态的判断。二、关联文档一个复现问题的最小用例关联文档 issue-17746-fenced-code-then-nested-list.md 是本次修复的回归测试输入文件内容为- abc逐行解读这个用例 | 行 | 内容 | 结构含义 | |----|------|----------| | 1 | - \\\ | 外层无序列表项-其内容是围栏代码块的起始标记 | | 2 | a | 围栏代码块内部代码缩进 2 格 | | 3 | \\\ | 围栏代码块结束标记 | | 4 | 空行 | **问题关键**代码块与嵌套子列表之间的分隔空行 | | 5 | - b | 嵌套在外层列表项内部的子列表缩进 2 格 | | 6 | - c | 外层列表的第二个列表项 | 文件的核心是第 4 行的空行它位于围栏代码块之后、嵌套子列表之前。在 issue #17746 修复之前Prettier 会删除该空行把第 5 行 - b 直接顶到代码块下方。 ### 同一问题的四个变体用例 该测试目录 tests/format/markdown/list/blank-lines/ 下共有 5 个输入文件构成对 #17746 的完整覆盖矩阵 | 输入文件 | 场景描述 | |----------|----------| | [issue-17746.md](https://link.gitcode.com/i/9af0da0a37d31634a395a6c7d6afa65d) | 基础场景外层列表项内有段落与嵌套列表含连续空行 | | [issue-17746-fenced-code-then-nested-list.md](https://link.gitcode.com/i/e52c6f53d3701b51412c88fd40990b60) | 围栏代码块 嵌套子列表本文关联文档 | | [issue-17746-indented-code-then-nested-list.md](https://link.gitcode.com/i/76935dfcde477f32804f001773864dda) | 缩进代码块 嵌套子列表 | | [issue-17746-code-before-list.md](https://link.gitcode.com/i/425ae863a8c6e231aba8d693e8ea4805) | 列表项内嵌代码块后紧跟平级列表项 | | [issue-17746-code-sibling-nested-list.md](https://link.gitcode.com/i/6c4ebcc05f4a33cefb929d590a5d7381) | 代码块与嵌套子列表并存的复合场景 | 这组用例说明修复不仅针对围栏代码块也覆盖缩进代码块indented code block以及段落paragraph等多种前驱节点类型。 ## 三、快照测试如何驱动这个用例 ### 测试入口 同目录下的 [format.test.js](https://link.gitcode.com/i/6b9c444f410c41e9b0409d807fb1ebb1) 是整个测试组的入口仅一行代码 js runFormatTest(import.meta, [markdown, mdx]);runFormatTest是 Prettier 测试套件位于 tests/config/format-test/提供的约定式测试运行器它会自动收集当前目录下所有*.md输入文件分别用markdown与mdx两种解析器进行格式化并将结果与快照文件比对。快照的输入输出约定预期输出记录在同目录的snapshots/format.test.js.snap 中。该文件以 Jest Snapshot 格式展示每个用例的输入 → 输出转换其中关联文档issue-17746-fenced-code-then-nested-list.md的期望输出为- abc对比输入可以归纳出 Prettier 对空行的规范化规则 1. **保留代码块与嵌套子列表之间的空行**——这是 #17746 修复的核心行为 2. **在嵌套子列表之后补充一个空行**再输出下一个外层列表项 - c——这是 markdown 解析器对松散列表的既有约定外层列表因内部存在空行而变为松散列表列表项之间需以空行分隔。 与之呼应的是 issue-17746.md 的快照见同一快照文件第 3–27 行输入中连续的两个空行被规范化为一个空行同时嵌套列表前后各保留一个空行abcd这说明 Prettier 的规则是保留空行但不放大空行空行数量统一收敛到恰好一行。 ### 如何本地运行该用例 在仓库根目录使用 Jest 定向运行该测试组 bash yarn jest tests/format/markdown/list/blank-lines或按快照文件名过滤yarn jest tests/format/markdown/list/blank-lines --testNamePattern17746yarn test在 package.json 中被定义为jest。若想临时查看当前格式化输出与快照的差异可加-u更新快照仅用于本地调试提交前应确认差异符合预期。四、底层实现children.js 中的空行保留逻辑关键函数shouldPrePrintDoubleHardline格式化时是否在节点前插入空行对应 doc 中的hardline由 src/language-markdown/print/children.js 中的shouldPrePrintDoubleHardline(path, options)决定。该函数按解析器分两条分支处理但核心逻辑一致node.type list parent.type listItem (previous.type code || // Preserve blank line before nested list within listItem (issue #17746) previous.type paragraph) previous.position.end.line 1 node.position.start.line条件逐一拆解条件含义node.type list当前节点是列表即嵌套子列表parent.type listItem其父节点是列表项listItemprevious.type code \|\| previous.type paragraph前驱节点是代码块或段落previous.position.end.line 1 node.position.start.line前驱节点结束行 1 小于嵌套列表起始行即两者之间确实存在空行当四个条件同时成立时函数返回true打印器会在嵌套列表前插入空行从而保留源代码中的空行分隔。注意previous.position来自 Markdown 解析器remark 系产生的源位置信息position判断依据是真实行号而非缩进内容——这正是代码块之后是否真有空行的可靠判据。该分支同时存在于options.parser mdx分支第 66–77 行与常规分支第 78–97 行说明 Markdown 与 MDX 共用同一修复逻辑。此外MDX 分支还额外叠加了isLooseListItemLegacy的兼容判断用于处理 MDX 特有的松散列表兼容行为。修复的语义定位从源码结构看本次修复是在紧凑列表tight list的默认行为之上为代码块/段落 嵌套列表这一特定组合开了一个例外默认情况下 Prettier 会删除列表内多余空行以保持紧凑但当空行承担分隔块级内容与嵌套子列表的结构职责时删除会破坏可读性因此被显式保留。这种默认压缩、结构关键处保留的策略是 Prettier 在可读性与规范性之间取得平衡的典型实现。五、从用例到通用规则实际写作中的启发基于本仓库的测试与实现可以提炼出几条可复用的 Markdown 写作/配置经验在代码块与嵌套子列表之间保留空行。即使某些渲染器可以容忍紧凑写法Prettier 现在也会尊重你的空行因此不必为了迎合格式化而刻意删除分隔空行空行不会叠加。连续多个空行会被规范化为一个若需要更大的视觉间距应依赖标题、分隔线等 Markdown 原生结构而非堆叠空行MDX 与 Markdown 行为一致。本修复同时覆盖markdown与mdx两种解析器MDX 中书写嵌套列表时可放心使用相同约定代码块风格不影响规则。围栏代码块与缩进代码块4 空格缩进均被code节点类型覆盖两种写法下的空行都会被保留。六、延伸阅读完整修复说明见 CHANGELOG.md 的 Markdown 修复条目空行保留逻辑的实现位于 src/language-markdown/print/children.js其中shouldPrePrintDoubleHardline是整个列表空行策略的核心本用例的完整输入/输出快照见snapshots/format.test.js.snap 中issue-17746-fenced-code-then-nested-list.md format 1一节同一问题的其他变体用例可对照阅读 issue-17746.md、issue-17746-indented-code-then-nested-list.md、issue-17746-code-before-list.md 与 issue-17746-code-sibling-nested-list.md若想了解 Prettier 的 Markdown 打印整体架构可继续阅读 src/language-markdown/printer-markdown.js 与 src/language-markdown/index.js。【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考