學(xué)公式(`$...$` 與 `$$...$$`)的格式化:解析管線與保真策略詳解)
Prettier 對 Markdown 數(shù)學(xué)公式$...$與$$...$$的格式化解析管線與保真策略詳解【免費下載鏈接】prettierPrettier is an opinionated code formatter.項目地址: https://gitcode.com/gh_mirrors/pr/prettierPrettier 作為 opinionated 的代碼格式化器在格式化 Markdown 時會遇到大量無法“重排”的特殊內(nèi)容其中以 LaTeX 數(shù)學(xué)公式最為典型。本文以倉庫中的格式化測試用例 tests/format/markdown/math/issue-12793.md 為切入點結(jié)合同目錄下的完整數(shù)學(xué)測試套件與 Markdown 語言實現(xiàn)的源碼系統(tǒng)講解 Prettier 如何識別$...$行內(nèi)數(shù)學(xué)與$$...$$塊級數(shù)學(xué)、為何對行內(nèi)公式內(nèi)容采取“原樣保留”而非重新排版以及它在轉(zhuǎn)義、誤判、空白處理等邊界場景下的保真策略。讀完本文你將理解 Prettier 對數(shù)學(xué)公式的完整處理鏈路并掌握如何用現(xiàn)有測試用例驗證這些行為。測試用例 issue-12793.md 驗證了什么測試輸入文件 issue-12793.md 全文只有一行是典型的高等數(shù)學(xué)/概率論公式$ P\Big(\mathop{\cup}\limits^n_{i1}A_i\Big) \sum\limits^n_{i1}P(A_i) $這是一個使用$...$包裹的行內(nèi)數(shù)學(xué)inline math表達式內(nèi)容包含\Big、\mathop、\limits、\cup、\sum等大量 LaTeX 控制序列。該用例最初來自 Prettier 倉庫的 issue #12793用于回歸驗證 Prettier 對復(fù)雜 LaTeX 公式的格式化行為。對應(yīng)的快照 tests/format/markdown/math/snapshots/format.test.js.snap 記錄了期望結(jié)果此處省略了options等測試腳手架分隔線僅展示核心內(nèi)容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) $關(guān)鍵結(jié)論有二輸入與輸出逐字節(jié)一致。Prettier 對行內(nèi)數(shù)學(xué)公式的內(nèi)容不做任何格式化——不插入空格、不修改\limits與\Big的書寫形式也不嘗試把公式重排到printWidth: 80的限制內(nèi)。公式前的空格被保留。$與P\Big(...之間的空格在輸出中依然存在快照中可見$ P\Big這與“remark-math 會裁剪公式首尾空白”的行為形成對比詳見下文源碼分析。數(shù)學(xué)公式的解析micromark 擴展與 mdast 節(jié)點Prettier 的 Markdown 解析入口在 src/language-markdown/parse/parse-markdown.js它基于mdast-util-from-markdown構(gòu)建 AST并通過 micromark 擴展體系注冊數(shù)學(xué)語法import { mathFromMarkdown } from mdast-util-math; import { math as mathSyntax } from micromark-extension-math; ... extensions: [ gfmSyntax({ singleTilde: false }), mathSyntax(), ... ], mdastExtensions: [ gfmFromMarkdown(), mathFromMarkdown(), ... ],也就是說數(shù)學(xué)公式支持由兩個外部庫共同提供micromark-extension-mathmathSyntax()負責(zé)在詞法層面識別$...$與$$...$$的起止邊界mdast-util-mathmathFromMarkdown()負責(zé)把識別的區(qū)間轉(zhuǎn)換成 mdast AST 節(jié)點。轉(zhuǎn)換后數(shù)學(xué)公式對應(yīng)兩種節(jié)點類型math塊級由$$...$$包裹的獨立公式塊inlineMath行內(nèi)由$...$包裹、嵌在段落文字中的公式例如 issue-12793.md 中的這一行。在 src/language-markdown/traverse/visitor-keys.evaluate.js 中math: []與inlineMath: []表明二者都是葉子節(jié)點不再擁有子節(jié)點其內(nèi)容作為整體處理。同時 src/language-markdown/utilities.js 中的INLINE_NODE_TYPES集合把inlineMath與其他行內(nèi)節(jié)點inlineCode、emphasis、strong、link等并列說明它參與行內(nèi)斷行、空白合并等行內(nèi)排版邏輯。inlineMath 的打印切片原文而非重建內(nèi)容Prettier 打印 Markdown 的核心實現(xiàn)在 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不做任何格式化而是直接截取原始文本中的對應(yīng)片段原樣輸出locStart/locEnd取自 src/language-markdown/loc.js即node.position.start.offset與node.position.end.offset——由解析器提供的原始文本偏移量options.originalText.slice(...)用這兩個偏移量把原始輸入中該節(jié)點的完整文本切片出來包括首尾空格與內(nèi)部所有空白代碼注釋解釋了這樣做的理由remark-math 本身會裁剪trim公式內(nèi)容但 Prettier 不希望移除空白因為很可能某個片段只是碰巧被識別成了數(shù)學(xué)公式例如$10 - $20移除空白反而會改變原文語義。這正是 issue-12793.md 中$ P\Big(...前面空格得以保留的底層原因即使printWidth很小、即使公式中有大量連續(xù)空格行內(nèi)公式一律以原始切片形式返回絕不重排。math塊級公式的打印結(jié)構(gòu)標準化 內(nèi)容保真與行內(nèi)公式不同塊級math節(jié)點的打印邏輯會做適度的結(jié)構(gòu)標準化mdast.js#L342-L349case math: return [ $$, node.meta ? node.meta : , hardline, node.value ? [replaceEndOfLine(node.value, hardline), hardline] : , $$, ];這段代碼把任何塊級數(shù)學(xué)統(tǒng)一輸出為如下結(jié)構(gòu)起始標記$$若存在元數(shù)據(jù)node.meta即$$后緊跟的屬性文本則以一個空格分隔追加一個強制換行hardline公式正文node.value其中通過replaceEndOfLine把內(nèi)部換行統(tǒng)一為hardline末尾換行后閉合的$$。注意標準化僅限于外殼結(jié)構(gòu)換行、元數(shù)據(jù)、收尾公式的正文內(nèi)容本身原樣保留。測試套件中 issue-16664.md 驗證了單行書寫塊級公式的保持$$ \textrm{p-value} 1 - \sum_{x \leq a} H(x | N, r_1, m_1) $$快照顯示該行輸入輸出一致\textrm、\sum、下標x \leq a等寫法均不被改寫。邊界與防誤判$的歧義處理數(shù)學(xué)公式識別最大的挑戰(zhàn)是$在普通文本中的歧義。測試目錄 tests/format/markdown/math 下的其他用例專門覆蓋了這些邊界1. 金額與貨幣寫法不應(yīng)被當(dāng)作公式—— math-like.md$10 - $20 Paragraph with $14 million. But if more $dollars on the same line...快照中這兩行輸出與輸入完全一致。$10、$20、$14 million這類“形似公式”的文本不會被識別為inlineMath自然也不會觸發(fā)任何格式化或空白保留邏輯這正是 mdast.js 注釋中所說“很可能被誤識別為數(shù)學(xué)”的典型場景。2. 反斜杠轉(zhuǎn)義體系—— dollar-sign.md 依次測試了$、\$、\\$、\\\$四級轉(zhuǎn)義深度快照確認每一級在輸出中都保持不變說明 Prettier 不會去“糾正”作者對$的轉(zhuǎn)義選擇。3. 空塊級公式—— empty-block.md 的$$\n$$被原樣保留不強制插入空行或刪除。4. 極端組合回歸—— remark-math.md 移植自 remark-math 的官方 spec文件頭部的 HTML 注釋標注了來源覆蓋了$出現(xiàn)在代碼 span 內(nèi)\$\alpha$、$$前有獨立段落文本tango后自動補一個空行、引用塊內(nèi)公式 $$、帶縮進的公式塊$$$外殼縮進被歸一化但公式內(nèi)部縮進保留、$$ must 這類疑似元數(shù)據(jù)寫法等十幾組場景。這些用例共同保證外殼可以被規(guī)范化但公式內(nèi)部一個字符都不動。如何運行與驗證該測試套件的入口是 tests/format/markdown/math/format.test.js僅一行runFormatTest(import.meta, [markdown]);runFormatTest是 Prettier 自帶的格式化測試腳手架定義于 tests/config/format-test-setup.js 及相關(guān) utilities它會讀取同目錄下每個.md文件作為輸入用markdown解析器在默認配置printWidth: 80見快照頭部標注下格式化并與snapshots/format.test.js.snap 中記錄的期望輸出比對。倉庫根目錄存在 jest.config.js可通過yarn jest運行 Jest 測試新增一個數(shù)學(xué)相關(guān)用例只需在 tests/format/markdown/math 下添加.md輸入并生成對應(yīng)快照即可。小結(jié)通過 issue-12793.md 這個用例可以看到 Prettier 處理數(shù)學(xué)公式的核心哲學(xué)識別公式邊界但不干預(yù)公式內(nèi)容。行內(nèi)公式通過originalText.slice(locStart, locEnd)原樣透傳以保證語義無損塊級公式僅對$$外殼與換行做標準化。這條策略在 parse-markdown.js解析、mdast.js打印、utilities.js行內(nèi)節(jié)點歸類三處源碼中都有明確實現(xiàn)支撐并由 tests/format/markdown/math 下覆蓋轉(zhuǎn)義、誤判、空塊、極端組合的整套用例持續(xù)守護?!久赓M下載鏈接】prettierPrettier is an opinionated code formatter.項目地址: https://gitcode.com/gh_mirrors/pr/prettier創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考