Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/editor/src/codemirror/code-block.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ describe("codeBlockPreviewPlugin", () => {
const header = view.dom.querySelector<HTMLElement>(".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);
Expand Down
6 changes: 3 additions & 3 deletions packages/editor/src/codemirror/frontmatter-preview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>(".cm-markra-frontmatter-hidden-line")]
Expand Down
2 changes: 2 additions & 0 deletions packages/scripts/src/vite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -126,6 +127,7 @@ export function createMarkraAppViteConfig(options: MarkraAppViteConfigOptions) {
plugins: [
react(),
tailwindcss(),
katexFontsPlugin(),
...(options.stripDebug !== false && mode === "production" ? [stripDebugPlugin()] : [])
],
build: {
Expand Down
73 changes: 73 additions & 0 deletions packages/scripts/src/vite/katex-fonts.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
59 changes: 59 additions & 0 deletions packages/scripts/src/vite/katex-fonts.ts
Original file line number Diff line number Diff line change
@@ -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];
}
}
};
}
Loading