From ce974f7ca74427b918a7eb20af46e9016be01174 Mon Sep 17 00:00:00 2001 From: MuRong Date: Tue, 28 Jul 2026 17:24:36 +0800 Subject: [PATCH] build(fonts): remove legacy KaTeX font formats --- .../editor/src/codemirror/code-block.test.ts | 6 +- .../codemirror/frontmatter-preview.test.ts | 6 +- packages/scripts/src/vite/index.ts | 2 + packages/scripts/src/vite/katex-fonts.test.ts | 73 +++++++++++++++++++ packages/scripts/src/vite/katex-fonts.ts | 59 +++++++++++++++ 5 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 packages/scripts/src/vite/katex-fonts.test.ts create mode 100644 packages/scripts/src/vite/katex-fonts.ts diff --git a/packages/editor/src/codemirror/code-block.test.ts b/packages/editor/src/codemirror/code-block.test.ts index ad364a44..46dd1c8b 100644 --- a/packages/editor/src/codemirror/code-block.test.ts +++ b/packages/editor/src/codemirror/code-block.test.ts @@ -49,9 +49,9 @@ describe("codeBlockPreviewPlugin", () => { const header = view.dom.querySelector(".cm-markra-code-header"); expect(header?.textContent).toBe("ts"); - expect(header && getComputedStyle(header).fontFamily).toContain( - "var(--font-ui", - ); + const headerFontFamily = header && getComputedStyle(header).fontFamily; + expect(headerFontFamily).toContain("var(--font-ui"); + expect(headerFontFamily).toContain("Noto Sans SC Variable"); expect( view.dom.querySelectorAll(".cm-markra-code-content-line"), ).toHaveLength(2); diff --git a/packages/editor/src/codemirror/frontmatter-preview.test.ts b/packages/editor/src/codemirror/frontmatter-preview.test.ts index e7431009..c76f4e1d 100644 --- a/packages/editor/src/codemirror/frontmatter-preview.test.ts +++ b/packages/editor/src/codemirror/frontmatter-preview.test.ts @@ -73,9 +73,9 @@ describe("frontmatterPreviewPlugin", () => { expect(editor?.value).toBe("title: Synthetic"); expect(view.dom.querySelector(".cm-markra-frontmatter")).not.toBeNull(); - expect(label && getComputedStyle(label).fontFamily).toContain( - "var(--font-ui", - ); + const labelFontFamily = label && getComputedStyle(label).fontFamily; + expect(labelFontFamily).toContain("var(--font-ui"); + expect(labelFontFamily).toContain("Noto Sans SC Variable"); expect(editor && getComputedStyle(editor).fontFamily).toContain("ui-monospace"); expect( [...view.dom.querySelectorAll(".cm-markra-frontmatter-hidden-line")] diff --git a/packages/scripts/src/vite/index.ts b/packages/scripts/src/vite/index.ts index e040bed8..f930250f 100644 --- a/packages/scripts/src/vite/index.ts +++ b/packages/scripts/src/vite/index.ts @@ -3,6 +3,7 @@ import tailwindcss from "@tailwindcss/vite"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; +import { katexFontsPlugin } from "./katex-fonts.ts"; import { stripDebugPlugin } from "./strip-debug.ts"; import type { UserConfig } from "vite"; @@ -126,6 +127,7 @@ export function createMarkraAppViteConfig(options: MarkraAppViteConfigOptions) { plugins: [ react(), tailwindcss(), + katexFontsPlugin(), ...(options.stripDebug !== false && mode === "production" ? [stripDebugPlugin()] : []) ], build: { diff --git a/packages/scripts/src/vite/katex-fonts.test.ts b/packages/scripts/src/vite/katex-fonts.test.ts new file mode 100644 index 00000000..b74c788e --- /dev/null +++ b/packages/scripts/src/vite/katex-fonts.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; +import { + canDeleteLegacyKatexFontAsset, + isLegacyKatexFontAsset, + keepWoff2KatexFontSources +} from "./katex-fonts"; + +describe("keepWoff2KatexFontSources", () => { + it("keeps the WOFF2 source and removes legacy KaTeX font formats", () => { + const css = [ + "@font-face {", + ' font-family: "KaTeX_Main";', + " src:", + ' url("./fonts/KaTeX_Main-Regular.woff2") format("woff2"),', + ' url("./fonts/KaTeX_Main-Regular.woff") format("woff"),', + ' url("./fonts/KaTeX_Main-Regular.ttf") format("truetype");', + "}" + ].join("\n"); + + expect(keepWoff2KatexFontSources(css)).toContain( + 'src: url("./fonts/KaTeX_Main-Regular.woff2") format("woff2");' + ); + expect(keepWoff2KatexFontSources(css)).not.toContain(".woff\""); + expect(keepWoff2KatexFontSources(css)).not.toContain(".ttf"); + }); + + it("leaves non-KaTeX font sources unchanged", () => { + const css = [ + "@font-face {", + ' font-family: "Synthetic Sans";', + ' src: url("./synthetic.woff2") format("woff2"), url("./synthetic.woff") format("woff");', + "}" + ].join("\n"); + + expect(keepWoff2KatexFontSources(css)).toBe(css); + }); + + it("keeps an inlined WOFF2 source without breaking its data URL", () => { + const css = [ + "@font-face {", + ' font-family: "KaTeX_Size3";', + " src:", + ' url("data:font/woff2;base64,d09GMgABAAAA") format("woff2"),', + ' url("./fonts/KaTeX_Size3-Regular.woff") format("woff"),', + ' url("./fonts/KaTeX_Size3-Regular.ttf") format("truetype");', + "}" + ].join("\n"); + + expect(keepWoff2KatexFontSources(css)).toContain( + 'src: url("data:font/woff2;base64,d09GMgABAAAA") format("woff2");' + ); + expect(keepWoff2KatexFontSources(css)).not.toContain("KaTeX_Size3-Regular.woff"); + expect(keepWoff2KatexFontSources(css)).not.toContain("KaTeX_Size3-Regular.ttf"); + }); +}); + +describe("isLegacyKatexFontAsset", () => { + it("matches only KaTeX WOFF and TTF build assets", () => { + expect(isLegacyKatexFontAsset("assets/fonts/KaTeX_Main-Regular-example.woff")).toBe(true); + expect(isLegacyKatexFontAsset("assets/fonts/KaTeX_Main-Regular-example.ttf")).toBe(true); + expect(isLegacyKatexFontAsset("assets/fonts/KaTeX_Main-Regular-example.woff2")).toBe(false); + expect(isLegacyKatexFontAsset("assets/fonts/Synthetic-Regular-example.woff")).toBe(false); + }); +}); + +describe("canDeleteLegacyKatexFontAsset", () => { + it("keeps a legacy asset while generated CSS still references it", () => { + const fileName = "assets/fonts/KaTeX_Size3-Regular-example.woff"; + + expect(canDeleteLegacyKatexFontAsset(fileName, [`src:url(/${fileName})`])).toBe(false); + expect(canDeleteLegacyKatexFontAsset(fileName, ["src:url(data:font/woff2;base64,AAAA)"])).toBe(true); + }); +}); diff --git a/packages/scripts/src/vite/katex-fonts.ts b/packages/scripts/src/vite/katex-fonts.ts new file mode 100644 index 00000000..4ccadfd5 --- /dev/null +++ b/packages/scripts/src/vite/katex-fonts.ts @@ -0,0 +1,59 @@ +import type { Plugin } from "vite"; + +const katexFontFacePattern = /@font-face\s*\{[^}]*font-family\s*:\s*["']?KaTeX_[^}]*\}/g; +const fontSourcePattern = /src\s*:\s*([\s\S]*?);(?=\s*(?:font-|}))/; +const legacyKatexFontAssetPattern = /(?:^|\/)KaTeX_[^/]+\.(?:ttf|woff)$/i; + +export function keepWoff2KatexFontSources(css: string) { + return css.replace(katexFontFacePattern, (fontFace) => { + return fontFace.replace(fontSourcePattern, (declaration, sources: string) => { + const woff2Sources = sources + .split(/,\s*(?=url\()/) + .filter((source) => + /\.woff2(?:["')?#]|$)/i.test(source) + || /format\(\s*["']?woff2["']?\s*\)/i.test(source) + ); + + return woff2Sources.length > 0 + ? `src: ${woff2Sources.join(", ")};` + : declaration; + }); + }); +} + +export function isLegacyKatexFontAsset(fileName: string) { + return legacyKatexFontAssetPattern.test(fileName); +} + +export function canDeleteLegacyKatexFontAsset(fileName: string, cssSources: string[]) { + return isLegacyKatexFontAsset(fileName) + && cssSources.every((css) => !css.includes(fileName)); +} + +export function katexFontsPlugin(): Plugin { + return { + apply: "build", + enforce: "post", + name: "markra-katex-fonts", + generateBundle(_options, bundle) { + const cssSources: string[] = []; + + for (const output of Object.values(bundle)) { + if (output.type !== "asset" || !output.fileName.endsWith(".css")) continue; + + const css = typeof output.source === "string" + ? output.source + : new TextDecoder().decode(output.source); + const optimizedCss = keepWoff2KatexFontSources(css); + output.source = optimizedCss; + cssSources.push(optimizedCss); + } + + for (const fileName of Object.keys(bundle)) { + // Keep any fallback that a future KaTeX or Vite output still references; + // deleting is safe only after the rewritten CSS has stopped using it. + if (canDeleteLegacyKatexFontAsset(fileName, cssSources)) delete bundle[fileName]; + } + } + }; +}