资讯动态

Prettier 对 Markdown 列表内嵌套 HTML 的格式化行为:从“Format on Save 缩进漂移“到 proseWrap 源码级解析

发布时间:2026/9/19 16:09:19 来源:尧图企业网站定制
Prettier 对 Markdown 列表内嵌套 HTML 的格式化行为从Format on Save 缩进漂移到 proseWrap 源码级解析【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettierMarkdown 中经常需要在有序列表项里嵌入一段多行 HTML如table而保存时自动格式化Format on Save一旦对这类内容反复重新缩进就会让表格被一层层推远、彻底脱离列表层级。本篇以 Prettier 仓库中的真实测试用例 multiline.md 为骨架结合其格式化快照与 markdown 打印器源码完整解析 Prettier 处理列表内多行 HTML 块的规则并逐一对比proseWrap的三种模式always/never/preserve对输出结果的影响帮助读者理解底层原理并正确配置自己的 Markdown 工作流。一、测试用例背景一个真实的编辑器痛点这个测试用例来自 Prettier 的 Markdown 格式化回归测试集位于 tests/format/markdown/html/multiline.md。它只有 22 行却精准复现了一个开发者经常遇到的场景在一个有序列表项中嵌套一个多行 HTML 表格当编辑器开启 Format on Save 时如果格式化器每次都把 HTML 块的缩进加深一层表格就会进一步、再进一步地偏离列表项最终完全错位。用例中的文字描述本身就点明了问题When formating on save Prettier will continue to add an indent each time pushing the table further and further out of sync.保存格式化时Prettier 会不断加一层缩进把表格越推越远、越来越不同步。这正是 Prettier 通过HTML 块原样保留策略要解决的回归问题Markdown 里的 HTML 块不需要也不应该被重新排版缩进否则每次保存都会产生新的 diff缩进漂移永无止境。二、测试输入完整还原列表项中的多行表格原测试输入如下完整还原逐字保留1. Some test text, the goal is to have the html table below nested within this number. When formating on save Prettier will continue to add an indent each time pushing the table further and further out of sync. table classtable table-striped tr thTest/th thTable/th /tr tbody tr tdwill/td tdbe/td /tr tr tdpushed/td tdWhen/td /tr tr tdFormat on/td tdSave/td /tr /tbody /table观察这个输入需要注意三个结构特征列表项有序列表项1.后面跟一段很长的说明文本长度超过默认printWidth: 80是测试proseWrap换行行为的关键嵌套 HTML 块列表项内容部分缩进 4 个空格起包含一个多行table其内部缩进本身就不规范——tr与th顶格于 4 空格缩进而tbody内的tr却多缩进了 4 个空格混合排版Markdown 文本与 HTML 标签共存于同一个列表项容器中。测试在 format.test.js 中以三种proseWrap取值分别运行runFormatTest(import.meta, [markdown], { proseWrap: always }); runFormatTest(import.meta, [markdown], { proseWrap: never }); runFormatTest(import.meta, [markdown], { proseWrap: preserve });也就是说同一个输入会生成三份快照分别对应snapshots/format.test.js.snap 中的三个multiline.md条目。三、三种 proseWrap 模式的输出对比模式一proseWrap: always—— 按 printWidth 重排文本HTML 原样保留always模式把超长的列表项文本按默认printWidth: 80重新换行同时完全不触碰 HTML 表格1. Some test text, the goal is to have the html table below nested within this number. When formating on save Prettier will continue to add an indent each time pushing the table further and further out of sync. table classtable table-striped tr thTest/th thTable/th /tr tbody tr tdwill/td tdbe/td /tr tr tdpushed/td tdWhen/td /tr tr tdFormat on/td tdSave/td /tr /tbody /table可以看到两个关键事实说明文本被折成三行且续行以 4 空格对齐列表项内容整个table从标签、属性到内部每一行逐字节保持不变——包括tbody内tr那个不规范的 8 空格缩进Prettier 也没有修正它。模式二proseWrap: never—— 文本强制单行HTML 仍原样保留never模式把整段 prose 压成一行依赖编辑器软换行HTML 表格依旧原样输出1. Some test text, the goal is to have the html table below nested within this number. When formating on save Prettier will continue to add an indent each time pushing the table further and further out of sync. table classtable table-striped tr thTest/th thTable/th /tr tbody tr tdwill/td tdbe/td /tr tr tdpushed/td tdWhen/td /tr tr tdFormat on/td tdSave/td /tr /tbody /table输入与输出除 HTML 之外完全一致——注意这里连文本的原始单行都未改变因为never的本意就是不做重排、交给查看器软换行。模式三proseWrap: preserve—— 保留既有换行HTML 依旧原样preservePrettier 的默认值既不重排也不合并 prose原样保留作者已有的换行HTML 表格同样一字不改输出与never在此用例中完全一致。三种模式对比小结模式长文本处理HTML 表格适用场景always按printWidth重排为多行原样保留需要统一换行风格的仓库、文档站never强制单行原样保留依赖编辑器软换行、追求最小 diffpreserve默认保持作者既有换行原样保留GitHub 评论、Bitbucket 等对换行敏感的平台三份快照共同印证无论proseWrap如何取值HTML 块在输出中都逐字不变——这正是保存格式化不会导致表格缩进漂移的保证来源。这也是官方 Prose Wrap 选项文档 所描述的行为在真实用例上的落地验证。四、源码级原理HTML 块为什么原样保留要理解上面的输出需要深入 Prettier 的 Markdown 打印器。核心逻辑位于 src/language-markdown/print/mdast.js 的case html分支第 217–227 行case html: { const { parent, isLast } path; const value parent.type root isLast ? node.value.trimEnd() : node.value; const isHtmlComment /^!--.*--$/s.test(value); return replaceEndOfLine( value, isHtmlComment ? hardline : markAsRoot(literalline), ); }要点解读原样输出节点内容HTML 节点的value就是源码中的原始文本打印器直接返回它不做任何缩进、折行或空白规整literalline保持换行原貌对非注释 HTML 块换行符被替换为literalline真实换行且不受缩进影响从而保证表格内部每个换行、每个空格都保持作者原样HTML 注释使用hardline纯!-- ... --注释块使用普通硬换行以便与周围 Markdown 结构如列表、段落协调空行仅对 root 末位的 HTML 做trimEnd只有位于文档根节点末尾的 HTML 才去掉尾部空白其余位置一律不动。与此同时children.js 中定义了 HTML 块在块级上下文中的排版边界isBlockHtmlWithoutBlankLineBetweenPrevHtml第 112–115 行前后两个 HTML 块之间没有空行时不强行插入空行保持紧凑isBlockHtmlWithoutBlankLineBetweenPrevParagraph第 116–120 行MDX 除外HTML 块紧跟段落且无空行时不插空行保证 HTML 能作为段落内嵌块紧贴正文isHtmlDirectAfterListItem第 121–125 行HTML 紧跟列表项内的段落时也不插入多余空行——这正是本用例中表格紧贴列表项文本、且保持 4 空格缩进不被打乱的依据。判断HTML 是否属于行内元素的依据在 src/language-markdown/utilities.js 的INLINE_NODE_WRAPPER_TYPES只有当 HTML 节点位于paragraph、heading、tableCell等行内容器内时才会被当作行内 HTML 参与文本排版而在root或listItem这种块级位置它就是独立的块级 HTML走原样输出路径。此外MDX 场景下还有一个转换插件 src/language-markdown/parse/unified-plugins/html-to-jsx.js除 HTML 注释与行内节点外块级 HTML 会被转换成jsx节点再打印——这也解释了为什么children.js中的空行逻辑对options.parser ! mdx有额外判断。五、对实际工作流的启示与配置建议结合本用例的三份快照与上述源码可以得出几条可直接落地的结论不要担心保存格式化毁掉列表里的 HTML 表格Prettier 对块级 HTML 采用原样保留策略反复保存不会让表格越陷越深。这个测试用例正是为了守住这条回归防线而存在——如果未来某个版本破坏了它快照测试会立即失败。想让文档文本也按统一宽度换行使用proseWrap: alwaysCLI 参数--prose-wrap always它只重排 Markdown 文本对 HTML 块依然秋毫无犯。在换行敏感平台GitHub 评论/Issue、Bitbucket发布内容保持默认的preserve即可若希望 diff 最小化可考虑never。完整选项语义可查阅 docs/options.md 的 Prose Wrap 一节。若自己动手验证在仓库根目录执行yarn jest tests/format/markdown/html即可运行本目录的全部 MarkdownHTML 用例或直接使用 Prettier CLI 对任意包含列表内多行 HTML的 Markdown 文件执行prettier --prose-wrap always --write example.md观察表格部分是否原样保留。六、延伸阅读同目录下的相关回归用例multiline.md并非孤立用例同一测试目录下还有一组针对Markdown 与 HTML 混合排版的互补测试值得对照阅读inline-html.md行内 HTMLem与块级 HTMLdiv的区分行为inline-vs-block.md列表项中行内标签与块级标签的空白折叠差异beginning-tag-after-a-list-item.md列表项后紧跟details、blockquote时的空行规则multiline-attribute.mdHTML 属性值跨行时的保真行为blank-line-between-htmls.md两个 HTML 块之间空行的处理。这些用例连同 format.test.js 与快照文件 format.test.js.snap共同构成了 Prettier 对Markdown 中 HTML 内容格式化契约的完整规范也是排查 Markdown 排版异常时最直接的源码级参考资料。【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价