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