Prettier 对 Markdown 数学公式(`$...$` 与 `$$...$$`)的格式化:解析管线与保真策略详解

📅 发布时间:2026/9/20 13:59:33
Prettier 对 Markdown 数学公式(`$...$` 与 `$$...$$`)的格式化:解析管线与保真策略详解
Prettier 对 Markdown 数学公式$...$与$$...$$的格式化解析管线与保真策略详解【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettierPrettier 作为 opinionated 的代码格式化器在格式化 Markdown 时会遇到大量无法“重排”的特殊内容其中以 LaTeX 数学公式最为典型。本文以仓库中的格式化测试用例 tests/format/markdown/math/issue-12793.md 为切入点结合同目录下的完整数学测试套件与 Markdown 语言实现的源码系统讲解 Prettier 如何识别$...$行内数学与$$...$$块级数学、为何对行内公式内容采取“原样保留”而非重新排版以及它在转义、误判、空白处理等边界场景下的保真策略。读完本文你将理解 Prettier 对数学公式的完整处理链路并掌握如何用现有测试用例验证这些行为。测试用例 issue-12793.md 验证了什么测试输入文件 issue-12793.md 全文只有一行是典型的高等数学/概率论公式$ P\Big(\mathop{\cup}\limits^n_{i1}A_i\Big) \sum\limits^n_{i1}P(A_i) $这是一个使用$...$包裹的行内数学inline math表达式内容包含\Big、\mathop、\limits、\cup、\sum等大量 LaTeX 控制序列。该用例最初来自 Prettier 仓库的 issue #12793用于回归验证 Prettier 对复杂 LaTeX 公式的格式化行为。对应的快照 tests/format/markdown/math/snapshots/format.test.js.snap 记录了期望结果此处省略了options等测试脚手架分隔线仅展示核心内容parsers: [markdown] printWidth: 80 (default) input $ P\Big(\mathop{\cup}\limits^n_{i1}A_i\Big) \sum\limits^n_{i1}P(A_i) $ output $ P\Big(\mathop{\cup}\limits^n_{i1}A_i\Big) \sum\limits^n_{i1}P(A_i) $关键结论有二输入与输出逐字节一致。Prettier 对行内数学公式的内容不做任何格式化——不插入空格、不修改\limits与\Big的书写形式也不尝试把公式重排到printWidth: 80的限制内。公式前的空格被保留。$与P\Big(...之间的空格在输出中依然存在快照中可见$ P\Big这与“remark-math 会裁剪公式首尾空白”的行为形成对比详见下文源码分析。数学公式的解析micromark 扩展与 mdast 节点Prettier 的 Markdown 解析入口在 src/language-markdown/parse/parse-markdown.js它基于mdast-util-from-markdown构建 AST并通过 micromark 扩展体系注册数学语法import { mathFromMarkdown } from mdast-util-math; import { math as mathSyntax } from micromark-extension-math; ... extensions: [ gfmSyntax({ singleTilde: false }), mathSyntax(), ... ], mdastExtensions: [ gfmFromMarkdown(), mathFromMarkdown(), ... ],也就是说数学公式支持由两个外部库共同提供micromark-extension-mathmathSyntax()负责在词法层面识别$...$与$$...$$的起止边界mdast-util-mathmathFromMarkdown()负责把识别的区间转换成 mdast AST 节点。转换后数学公式对应两种节点类型math块级由$$...$$包裹的独立公式块inlineMath行内由$...$包裹、嵌在段落文字中的公式例如 issue-12793.md 中的这一行。在 src/language-markdown/traverse/visitor-keys.evaluate.js 中math: []与inlineMath: []表明二者都是叶子节点不再拥有子节点其内容作为整体处理。同时 src/language-markdown/utilities.js 中的INLINE_NODE_TYPES集合把inlineMath与其他行内节点inlineCode、emphasis、strong、link等并列说明它参与行内断行、空白合并等行内排版逻辑。inlineMath 的打印切片原文而非重建内容Prettier 打印 Markdown 的核心实现在 src/language-markdown/print/mdast.js。其中inlineMath分支的处理非常特殊mdast.js#L350-L353case inlineMath: // remark-math trims content but we dont want to remove whitespaces // since its very possible that its recognized as math accidentally return options.originalText.slice(locStart(node), locEnd(node));这里 Prettier不做任何格式化而是直接截取原始文本中的对应片段原样输出locStart/locEnd取自 src/language-markdown/loc.js即node.position.start.offset与node.position.end.offset——由解析器提供的原始文本偏移量options.originalText.slice(...)用这两个偏移量把原始输入中该节点的完整文本切片出来包括首尾空格与内部所有空白代码注释解释了这样做的理由remark-math 本身会裁剪trim公式内容但 Prettier 不希望移除空白因为很可能某个片段只是碰巧被识别成了数学公式例如$10 - $20移除空白反而会改变原文语义。这正是 issue-12793.md 中$ P\Big(...前面空格得以保留的底层原因即使printWidth很小、即使公式中有大量连续空格行内公式一律以原始切片形式返回绝不重排。math块级公式的打印结构标准化 内容保真与行内公式不同块级math节点的打印逻辑会做适度的结构标准化mdast.js#L342-L349case math: return [ $$, node.meta ? node.meta : , hardline, node.value ? [replaceEndOfLine(node.value, hardline), hardline] : , $$, ];这段代码把任何块级数学统一输出为如下结构起始标记$$若存在元数据node.meta即$$后紧跟的属性文本则以一个空格分隔追加一个强制换行hardline公式正文node.value其中通过replaceEndOfLine把内部换行统一为hardline末尾换行后闭合的$$。注意标准化仅限于外壳结构换行、元数据、收尾公式的正文内容本身原样保留。测试套件中 issue-16664.md 验证了单行书写块级公式的保持$$ \textrm{p-value} 1 - \sum_{x \leq a} H(x | N, r_1, m_1) $$快照显示该行输入输出一致\textrm、\sum、下标x \leq a等写法均不被改写。边界与防误判$的歧义处理数学公式识别最大的挑战是$在普通文本中的歧义。测试目录 tests/format/markdown/math 下的其他用例专门覆盖了这些边界1. 金额与货币写法不应被当作公式—— math-like.md$10 - $20 Paragraph with $14 million. But if more $dollars on the same line...快照中这两行输出与输入完全一致。$10、$20、$14 million这类“形似公式”的文本不会被识别为inlineMath自然也不会触发任何格式化或空白保留逻辑这正是 mdast.js 注释中所说“很可能被误识别为数学”的典型场景。2. 反斜杠转义体系—— dollar-sign.md 依次测试了$、\$、\\$、\\\$四级转义深度快照确认每一级在输出中都保持不变说明 Prettier 不会去“纠正”作者对$的转义选择。3. 空块级公式—— empty-block.md 的$$\n$$被原样保留不强制插入空行或删除。4. 极端组合回归—— remark-math.md 移植自 remark-math 的官方 spec文件头部的 HTML 注释标注了来源覆盖了$出现在代码 span 内\$\alpha$、$$前有独立段落文本tango后自动补一个空行、引用块内公式 $$、带缩进的公式块$$$外壳缩进被归一化但公式内部缩进保留、$$ must 这类疑似元数据写法等十几组场景。这些用例共同保证外壳可以被规范化但公式内部一个字符都不动。如何运行与验证该测试套件的入口是 tests/format/markdown/math/format.test.js仅一行runFormatTest(import.meta, [markdown]);runFormatTest是 Prettier 自带的格式化测试脚手架定义于 tests/config/format-test-setup.js 及相关 utilities它会读取同目录下每个.md文件作为输入用markdown解析器在默认配置printWidth: 80见快照头部标注下格式化并与snapshots/format.test.js.snap 中记录的期望输出比对。仓库根目录存在 jest.config.js可通过yarn jest运行 Jest 测试新增一个数学相关用例只需在 tests/format/markdown/math 下添加.md输入并生成对应快照即可。小结通过 issue-12793.md 这个用例可以看到 Prettier 处理数学公式的核心哲学识别公式边界但不干预公式内容。行内公式通过originalText.slice(locStart, locEnd)原样透传以保证语义无损块级公式仅对$$外壳与换行做标准化。这条策略在 parse-markdown.js解析、mdast.js打印、utilities.js行内节点归类三处源码中都有明确实现支撑并由 tests/format/markdown/math 下覆盖转义、误判、空块、极端组合的整套用例持续守护。【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考