Prettier 格式化 Markdown 多行 HTML 属性:原样保留策略与解析、打印实现解析
发布时间:2026/9/21 20:10:15 锦皓数字建站

Prettier 格式化 Markdown 多行 HTML 属性原样保留策略与解析、打印实现解析【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier本文以 Prettier 仓库中的 Markdown 格式化测试夹具 multiline-attribute.md 为核心剖析 Prettier 对 Markdown 中跨行 HTML 标签属性multiline HTML attribute的处理策略。读者读完将理解为什么 Prettier 对这类内容「不格式化」而是逐字保留、块级与行内上下文分别遵循怎样的判定规则以及该行为在解析层micromark 扩展与打印层literalline各自的实现依据。一、测试夹具multiline-attribute.md 在测什么在 Prettier 仓库中tests/format/markdown/html/目录集中存放 Markdown 内嵌 HTML 的格式化回归测试。其中 multiline-attribute.md 专门构造了「属性值跨行」的 HTML 标签覆盖四种典型上下文unknown titleline 1 line 2 /unknown div titleline 1 line 2 /div - In list - In list unknown titleline 1 line 2 /unknown div titleline 1 line 2 /div in blockquote unknown titleline 1 line 2 /unknown div titleline 1 line 2 /div text unknown titleline 1 line 2 text /unknown text该夹具刻意混用了两种标签unknownPrettier 未知的标签名与divHTML 已知块级标签名并让它们出现在四个位置文档根级独占段落紧贴行首书写嵌套列表- In list下两层缩进6 个空格的列表项内部块引用blockquote每行以前缀标记且属性内第二行带 2 个空格缩进行内上下文夹在普通段落文本text ... text之间。这种设计意在验证无论 HTML 处于何种 Markdown 容器中跨行属性内容都能被完整解析并且最终输出与输入完全一致。二、三种 proseWrap 下的快照输入恒等于输出与仓库其他格式测试一样该夹具由 format.test.js 驱动runFormatTest(import.meta, [markdown], { proseWrap: always }); runFormatTest(import.meta, [markdown], { proseWrap: never }); runFormatTest(import.meta, [markdown], { proseWrap: preserve });即同一份输入会分别在proseWrap: always / never / preserve三种取值下运行。查看快照文件 format.test.js.snap 中对应multiline-attribute.md的三段记录可以发现一个关键事实三种模式下输出都与输入逐字符一致。例如proseWrap: preserve的输出unknown titleline 1 line 2 /unknown div titleline 1 line 2 /div - In list - In list unknown titleline 1 line 2 /unknown div titleline 1 line 2 /div in blockquote unknown titleline 1 line 2 /unknown div titleline 1 line 2 /div text unknown titleline 1 line 2 text /unknown textproseWrap本用于控制 Markdown 段落的折行行为always强制折行、never不折行、preserve保留原文换行但在这里它对多行 HTML 属性毫无影响——原因正是下文将展开的HTML 节点在 Prettier 的 Markdown 打印器中属于「literal字面量」内容其内部的换行与缩进被整体保留不参与任何折行计算。细看输出还能发现几个被刻意保留的「不整齐」细节块引用场景中unknown的结束标签前是 /unknown即前缀后紧跟空格再加引号与div的写法一致但保留原样嵌套列表中外层列表项缩进为 4 空格、内层为 6 空格属性第二行缩进 6 空格、结束标签行缩进 4 空格——这些参差缩进全部原样输出行内场景中text与text /unknown text的空格也保持不变。这正是该测试要锁定的回归行为不要试图「修正」用户在多行 HTML 属性里书写的空白与换行。三、逐场景行为拆解3.1 根级块级 HTML作为独立段落处理unknown .../unknown与div .../div独占一行、两侧有空行会被 micromark 解析为块级html节点。Prettier 输出时在节点前后维持空行分隔见快照输出第一段节点本身的内容原样保留。3.2 嵌套列表缩进原样不参与列表对齐列表内的 HTML 标签保持其相对列表项的缩进层级4 空格列表项缩进 2 空格标签缩进标签内部第二行缩进 6 空格、结束标签行缩进 4 空格全部不做归一化。对比同一目录下的 multiline.md有序列表内嵌table与 multiline-with-trailing-space.md末尾带尾随空格的多行表格可见这是一致的策略多行 HTML 的缩进层级由用户书写决定Prettier 不参与排版。3.3 块引用前缀语义完整保留块引用中每行物理行以开头而属性值内的第二行line 2前有两个空格最终输出为 line 2。Prettier 需要保证两点属性内部的换行不被当作 Markdown 段落的软换行去折叠同时前缀与内部缩进逐字保留。3.4 行内上下文内部换行不被折叠text unknown titleline 1\n line 2\ntext /unknown text中HTML 处于段落内部但标签属性里的两行换行依然完整输出没有被折成一行。这说明「行内 HTML 节点」与「段落文本折行」是两条互不干扰的打印路径。四、原理一解析层如何保留属性内空白Markdown 的 HTML 解析通常把标签视为整块文本。若解析器在遇到属性值内的换行时直接截断或规整空白后续打印层就无从保留。Prettier 的解决方案在 micromark-extension-html-text.js 中// copied from https://github.com/micromark/micromark/blob/... // and modified to preserve whitespace inside quoted attribute values该文件头部注释明确指出该扩展复制自 micromark 官方的html-text实现并做了修改以保留引号包裹的属性值内部的空白。文件内部的状态机完整实现了标签解析的各个阶段——tagOpenAttributeName属性名、tagOpenAttributeValueBefore属性值前的空白、tagOpenAttributeValueUnquoted无引号属性值、以及双引号/单引号属性值状态——正是这些状态机分支负责在引号内持续消费包括换行在内的字符从而让unknown titleline 1\n line 2\n这样的内容被完整解析进 AST属性值内的换行与缩进不会丢失。从源码结构看这一步是整个「多行属性原样保留」能力的前置条件解析器先保证不丢字符打印器才能保证不改字符。五、原理二打印层用 literalline 锁死换行解析完成后HTML 节点进入打印阶段。在 Markdown 打印器的节点分发处 mdast.jscase 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), ); }这段代码揭示了保留策略的机制literalline字面换行对非注释 HTML节点内每个换行都替换为literalline文档节点。literalline与hardline一样强制换行但区别在于换行后不做缩进规范化——这正是输出中嵌套列表那组参差缩进4/6/4 空格能原样保住的原因。同时markAsRoot(...)确保该字面量行不会被外层上下文如列表、块引用的align注入额外缩进。注释特例HTML 注释!-- ... --使用hardline因为注释参与正常的缩进对齐语义。根级尾随修剪仅当节点是文档根root的最后一个子节点时才会trimEnd()去掉结尾空白避免文件末尾残留多余换行其余位置一律不动。空白处理还调用了replaceEndOfLine将 CRLF 等行尾统一为当前换行符配置但换行位置本身保持原样。这条路径在 replaceEndOfLine 所在模块与文档构建器的配合下最终形成「逐字节级」的保真输出。六、原理三行内 HTML 的判定规则为什么第 3.4 节的行内场景不会被proseWrap折叠关键在于行内 HTML 节点的判定。在 children.jsfunction shouldPrePrintHardline({ node, parent }) { const isInlineNode INLINE_NODE_TYPES.has(node.type) !(node.type liquidNode !INLINE_NODE_WRAPPER_TYPES.has(parent.type)); const isInlineHTML node.type html INLINE_NODE_WRAPPER_TYPES.has(parent.type); return !isInlineNode !isInlineHTML; }当html节点的父节点属于 INLINE_NODE_WRAPPER_TYPES包含paragraph、heading、tableCell等行内容器时它被认定为行内 HTML前后不插入硬换行与其他行内节点文本、强调、行内代码等一样参与段落文档构建但节点内部的多行属性内容依旧通过第五节的literalline路径保留换行。于是出现了看似矛盾、实则自洽的结果HTML 节点与周围文本的关系按行内处理而 HTML 自身的内部结构按字面量处理。块引用与列表场景中 HTML 的父节点不是行内容器因此shouldPrePrintHardline返回 true节点前后会插入硬换行/空行保证块级语义正确。七、运行验证与同目录配套用例该行为可随时在仓库内复现验证该测试由 Jest 执行入口为 format.test.js其中runFormatTest是 tests/config/format-test 提供的统一测试框架自动读取同目录.md夹具、分别断言输入与快照输出更新断言则快照写入 format.test.js.snap约 1063 行涵盖该目录全部 10 个夹具在三种proseWrap下的结果若需调整实现改动点锁定在 mdast.js 的case html分支与 micromark-extension-html-text.js 的属性值状态机。同目录的配套夹具从不同侧面夯实同类行为值得对照阅读夹具验证点multiline.md有序列表内嵌多行table表格整体原样multiline-with-trailing-space.md多行表格带尾随空格时的保留inline-html.md行内 HTML 与文本的间距、div块级断行inline-vs-block.mddiv块与span行内与后续文本的间距差异beginning-tag-after-a-list-item.md列表项之后紧跟 HTML 标签的空行插入八、对使用者的实用结论多行 HTML 属性不会被「修复」如果你手写的div titleline 1\n line 2\n缩进不齐Prettier 会原样保留这既是优点对含换行的属性值如aria-label、SVG 描述、模板字符串安全也是约束想让它整齐需自己排版。proseWrap对 HTML 节点无效无论配置always、never还是preserveHTML 块内部的换行都不参与折行算法不受该选项影响。换行符仍会统一节点内的换行虽然位置不变但会经replaceEndOfLine统一为配置的行尾风格因此跨平台提交时行尾依然稳定。调试建议当发现 Markdown 中 HTML 被意外改动时优先检查标签是否被识别为行内 HTML父容器类型以及是否命中根级末尾节点的trimEnd()特殊分支——这两处是仅有的例外路径。总而言之Prettier 对 Markdown 多行 HTML 属性的处理可以概括为一句原则解析阶段不丢字符保留引号属性内的空白打印阶段用literalline不改换行从而对多行 HTML 实现输入即输出的字面量保真。该原则由 multiline-attribute.md 这一测试夹具在根级、列表、块引用、行内四种上下文中锁定是理解 Prettier Markdown 内嵌 HTML 行为的最佳入口。【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。