diff --git a/packages/app/src/App.test.tsx b/packages/app/src/App.test.tsx
index 7daab612..87e48ed4 100644
--- a/packages/app/src/App.test.tsx
+++ b/packages/app/src/App.test.tsx
@@ -6,6 +6,7 @@ import { defaultMarkdownShortcuts } from "@markra/editor";
import { it as registerTest } from "vitest";
import desktopPackage from "../package.json";
import { defaultAiQuickActionPrompts } from "./lib/ai-actions";
+import { defaultExportSettings } from "./lib/settings/app-settings";
import {
dispatchAiEditorPreviewAction,
installAppTestHarness,
@@ -8696,6 +8697,10 @@ describe("Markra workspace", () => {
});
it("exports the current markdown document as standalone HTML from the native menu", async () => {
+ mockedGetStoredExportSettings.mockResolvedValue({
+ ...defaultExportSettings,
+ fontFamily: "Example Serif"
+ });
mockOpenMarkdownFile({
content: "# Exportable\n\nRendered from markdown.",
name: "exportable.md",
@@ -8731,6 +8736,7 @@ describe("Markra workspace", () => {
const exportedHtml = mockedSaveNativeHtmlFile.mock.calls.at(-1)?.[0].contents ?? "";
expect(exportedHtml).toContain("
Rendered from markdown.
");
expect(exportedHtml).toContain("exportable.md");
+ expect(exportedHtml).toContain('font-family: "Example Serif", ui-serif');
});
it("exports the current markdown document as PDF from the native menu", async () => {
@@ -8740,6 +8746,7 @@ describe("Markra workspace", () => {
path: "/mock-files/printable.pdf"
});
mockedGetStoredExportSettings.mockResolvedValue({
+ fontFamily: null,
pandocArgs: "",
pandocPath: "",
pdfAuthor: "Ada & Co",
@@ -8802,6 +8809,7 @@ describe("Markra workspace", () => {
path: "/mock-files/portable.docx"
});
mockedGetStoredExportSettings.mockResolvedValue({
+ fontFamily: null,
pandocArgs: "--toc",
pandocPath: "/usr/local/bin/pandoc",
pdfAuthor: "",
@@ -8895,6 +8903,7 @@ describe("Markra workspace", () => {
it("opens settings directly to the Pandoc path target", async () => {
window.history.pushState({}, "", "/?settings=1&settingsTarget=exportPandocPath");
mockedGetStoredExportSettings.mockResolvedValue({
+ fontFamily: null,
pandocArgs: "",
pandocPath: "",
pdfAuthor: "",
@@ -8918,6 +8927,7 @@ describe("Markra workspace", () => {
window.history.pushState({}, "", "/?settings=1&settingsTarget=exportPandocPath");
mockedDetectNativePandocPath.mockResolvedValue("/opt/homebrew/bin/pandoc");
mockedGetStoredExportSettings.mockResolvedValue({
+ fontFamily: null,
pandocArgs: "",
pandocPath: "",
pdfAuthor: "",
@@ -8948,6 +8958,7 @@ describe("Markra workspace", () => {
it("saves the PDF export margin from the settings export page", async () => {
window.history.pushState({}, "", "/?settings");
mockedGetStoredExportSettings.mockResolvedValue({
+ fontFamily: null,
pandocArgs: "",
pandocPath: "",
pdfAuthor: "",
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 5f26bee3..821dcb76 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -3670,6 +3670,7 @@ function WorkspaceApp() {
const pdfSettings = exported.kind === "pdf" ? exportSettings.settings : null;
const contents = buildMarkdownHtmlDocument({
bodyHtml: exported.bodyHtml,
+ fontFamily: exportSettings.settings.fontFamily,
language: appLanguage.language,
pdfAuthor: pdfSettings?.pdfAuthor,
pdfFooter: pdfSettings?.pdfFooter,
diff --git a/packages/app/src/components/SettingsWindow.tsx b/packages/app/src/components/SettingsWindow.tsx
index 17900f20..06cd6d52 100644
--- a/packages/app/src/components/SettingsWindow.tsx
+++ b/packages/app/src/components/SettingsWindow.tsx
@@ -387,6 +387,7 @@ export function SettingsWindow() {
focusTarget={settingsFocusTarget}
pandocEnabled={appFeatures.pandoc}
settings={exportSettings}
+ systemFontFamilies={systemFontFamilies}
translate={translate}
onDetectPandocPath={handleDetectPandocPath}
onFocusTargetHandled={clearSettingsFocusTarget}
diff --git a/packages/app/src/components/settings/EditorSettings.tsx b/packages/app/src/components/settings/EditorSettings.tsx
index 50013a5b..a79236cf 100644
--- a/packages/app/src/components/settings/EditorSettings.tsx
+++ b/packages/app/src/components/settings/EditorSettings.tsx
@@ -6,7 +6,6 @@ import {
Moon,
RotateCcw,
Save,
- Search,
type LucideIcon
} from "lucide-react";
import {
@@ -14,8 +13,6 @@ import {
useMemo,
useRef,
useState,
- type CSSProperties,
- type KeyboardEvent as ReactKeyboardEvent,
type MouseEvent as ReactMouseEvent
} from "react";
import {
@@ -47,12 +44,9 @@ import {
editorCustomContentWidthMin,
normalizeEditorContentWidthPx
} from "../../lib/editor-width";
-import {
- editorFontFamilyCssValue,
- normalizeSystemFontFamilyName
-} from "../../lib/editor-font";
import type { AppSystemFontFamily } from "../../runtime";
import { SortableTitlebarAction } from "../SortableTitlebarAction";
+import { FontFamilySelect } from "./FontFamilySelect";
import {
SettingsButton,
SettingsNumberInput,
@@ -131,13 +125,6 @@ const sidebarLayoutOptions: Array<{
const titlebarActionVisibleClassName =
"aria-pressed:border-transparent aria-pressed:bg-(--bg-active) aria-pressed:text-(--text-heading) aria-pressed:opacity-100 aria-pressed:hover:bg-(--bg-active)";
const titlebarActionHiddenClassName = "text-(--text-secondary) opacity-55 hover:opacity-100";
-const editorFontFamilyListboxId = "settings-editor-font-family-options";
-
-type EditorFontFamilySelectOption = {
- fontFamily: string | null;
- label: string;
-};
-
function contentWidthRatioValue(preferences: EditorPreferences) {
const contentWidthPx = preferences.contentWidthPx ?? editorContentWidthPixels[preferences.contentWidth];
@@ -151,186 +138,6 @@ function contentWidthPxFromRatio(value: number) {
return normalizeEditorContentWidthPx(Math.round(editorCustomContentWidthMin + (contentWidthRatioSpanPx * ratio) / 100));
}
-function editorFontFamilySelectOptions(
- preferences: EditorPreferences,
- systemFontFamilies: readonly AppSystemFontFamily[],
- themeLabel: string
-): EditorFontFamilySelectOption[] {
- const fontFamilyOptions = new Map();
- for (const fontFamily of systemFontFamilies) {
- const normalizedFontFamily = normalizeSystemFontFamilyName(fontFamily.family);
- if (!normalizedFontFamily) continue;
-
- const normalizedLabel = normalizeSystemFontFamilyName(fontFamily.label) ?? normalizedFontFamily;
- if (!fontFamilyOptions.has(normalizedFontFamily)) {
- fontFamilyOptions.set(normalizedFontFamily, {
- fontFamily: normalizedFontFamily,
- label: normalizedLabel
- });
- }
- }
- if (fontFamilyOptions.size === 0 && preferences.editorFontFamily.source === "system") {
- fontFamilyOptions.set(preferences.editorFontFamily.family, {
- fontFamily: preferences.editorFontFamily.family,
- label: preferences.editorFontFamily.family
- });
- }
-
- return [
- { fontFamily: null, label: themeLabel },
- ...Array.from(fontFamilyOptions.values())
- .sort((first, second) => first.label.localeCompare(second.label) || first.fontFamily!.localeCompare(second.fontFamily!))
- ];
-}
-
-function optionMatchesQuery(option: EditorFontFamilySelectOption, query: string) {
- const normalizedQuery = query.trim().toLocaleLowerCase();
- if (!normalizedQuery) return true;
-
- return option.label.toLocaleLowerCase().includes(normalizedQuery);
-}
-
-function selectedFontFamilyOption(options: readonly EditorFontFamilySelectOption[], preferences: EditorPreferences) {
- if (preferences.editorFontFamily.source === "theme") return options[0]!;
-
- return options.find((option) => option.fontFamily === preferences.editorFontFamily.family) ?? options[0]!;
-}
-
-function editorFontFamilyOptionStyle(option: EditorFontFamilySelectOption): CSSProperties | undefined {
- if (option.fontFamily === null) return undefined;
-
- return {
- fontFamily: editorFontFamilyCssValue({
- family: option.fontFamily,
- source: "system"
- }) ?? undefined
- };
-}
-
-function SearchableEditorFontFamilySelect({
- label,
- onChange,
- options,
- value
-}: {
- label: string;
- onChange: (option: EditorFontFamilySelectOption) => unknown;
- options: EditorFontFamilySelectOption[];
- value: EditorFontFamilySelectOption;
-}) {
- const [activeIndex, setActiveIndex] = useState(0);
- const [open, setOpen] = useState(false);
- const [query, setQuery] = useState("");
- const inputValue = open ? query : value.label;
- const inputStyle = open ? undefined : editorFontFamilyOptionStyle(value);
- const filteredOptions = useMemo(
- () => options.filter((option) => optionMatchesQuery(option, query)),
- [options, query]
- );
- const visibleOptions = filteredOptions;
- const activeOption = visibleOptions[activeIndex] ?? visibleOptions[0];
-
- useEffect(() => {
- setActiveIndex(0);
- }, [open, query]);
-
- const selectOption = (option: EditorFontFamilySelectOption) => {
- setQuery("");
- setOpen(false);
- onChange(option);
- };
- const handleKeyDown = (event: ReactKeyboardEvent) => {
- if (event.key === "ArrowDown") {
- event.preventDefault();
- setOpen(true);
- if (visibleOptions.length === 0) return;
- setActiveIndex((currentIndex) => Math.min(currentIndex + 1, visibleOptions.length - 1));
- return;
- }
-
- if (event.key === "ArrowUp") {
- event.preventDefault();
- setOpen(true);
- if (visibleOptions.length === 0) return;
- setActiveIndex((currentIndex) => Math.max(currentIndex - 1, 0));
- return;
- }
-
- if (event.key === "Enter" && open && activeOption) {
- event.preventDefault();
- selectOption(activeOption);
- return;
- }
-
- if (event.key === "Escape") {
- setOpen(false);
- setQuery("");
- }
- };
-
- return (
-
-
-
{
- setOpen(false);
- setQuery("");
- }}
- onChange={(event) => {
- setQuery(event.currentTarget.value);
- setOpen(true);
- }}
- onFocus={() => {
- setQuery("");
- setOpen(true);
- }}
- onKeyDown={handleKeyDown}
- />
- {open ? (
-
- {visibleOptions.map((option, index) => (
-
- ))}
-
- ) : null}
-
- );
-}
-
function SettingsContentWidthInput({
label,
onUpdatePreferences,
@@ -655,12 +462,9 @@ export function EditorSettings({
systemFontFamilies?: readonly AppSystemFontFamily[];
translate: SettingsTranslate;
}) {
- const editorFontFamilyOptions = editorFontFamilySelectOptions(
- preferences,
- systemFontFamilies,
- translate("settings.editor.fontFamily.theme")
- );
- const editorFontFamilyValue = selectedFontFamilyOption(editorFontFamilyOptions, preferences);
+ const editorFontFamily = preferences.editorFontFamily.source === "system"
+ ? preferences.editorFontFamily.family
+ : null;
return (
<>
@@ -669,16 +473,17 @@ export function EditorSettings({
title={translate("settings.editor.fontFamily")}
description={translate("settings.editor.fontFamilyDescription")}
action={
-
+ systemFontFamilies={systemFontFamilies}
+ onChange={(family) =>
onUpdatePreferences({
...preferences,
- editorFontFamily: option.fontFamily === null
+ editorFontFamily: family === null
? { family: null, source: "theme" }
- : { family: option.fontFamily, source: "system" }
+ : { family, source: "system" }
})
}
/>
diff --git a/packages/app/src/components/settings/ExportSettings.test.tsx b/packages/app/src/components/settings/ExportSettings.test.tsx
new file mode 100644
index 00000000..c903fff4
--- /dev/null
+++ b/packages/app/src/components/settings/ExportSettings.test.tsx
@@ -0,0 +1,38 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { defaultExportSettings } from "../../lib/settings/app-settings";
+import { translate } from "../../test/settings-components";
+import { ExportSettings } from "./ExportSettings";
+
+describe("ExportSettings", () => {
+ it("selects an installed font for HTML and PDF exports", () => {
+ const onUpdateSettings = vi.fn();
+ const settings = {
+ ...defaultExportSettings,
+ fontFamily: null
+ };
+
+ render(
+
+ );
+
+ const fontFamilySelect = screen.getByRole("combobox", { name: "Export font" });
+
+ expect(fontFamilySelect).toHaveValue("Default");
+ fireEvent.focus(fontFamilySelect);
+ fireEvent.change(fontFamilySelect, { target: { value: "ser" } });
+ fireEvent.click(screen.getByRole("option", { name: "Example Serif" }));
+
+ expect(onUpdateSettings).toHaveBeenCalledWith({
+ ...settings,
+ fontFamily: "Example Serif"
+ });
+ });
+});
diff --git a/packages/app/src/components/settings/ExportSettings.tsx b/packages/app/src/components/settings/ExportSettings.tsx
index f1941ba8..7837f876 100644
--- a/packages/app/src/components/settings/ExportSettings.tsx
+++ b/packages/app/src/components/settings/ExportSettings.tsx
@@ -6,6 +6,8 @@ import {
type PdfPageSize
} from "../../lib/settings/app-settings";
import type { I18nKey } from "@markra/shared";
+import type { AppSystemFontFamily } from "../../runtime";
+import { FontFamilySelect } from "./FontFamilySelect";
import {
SettingsButton,
SettingsCheckbox,
@@ -83,6 +85,7 @@ export function ExportSettings({
onUpdateSettings,
pandocEnabled = true,
settings,
+ systemFontFamilies = [],
translate
}: {
focusTarget?: "pandocPath" | null;
@@ -91,6 +94,7 @@ export function ExportSettings({
onUpdateSettings: (settings: ExportSettingsValue) => unknown;
pandocEnabled?: boolean;
settings: ExportSettingsValue;
+ systemFontFamilies?: readonly AppSystemFontFamily[];
translate: SettingsTranslate;
}) {
const pandocPathTargetRef = useRef(null);
@@ -112,6 +116,27 @@ export function ExportSettings({
return (
<>
+
+
+ onUpdateSettings({
+ ...settings,
+ fontFamily
+ })
+ }
+ />
+ }
+ />
+
+
();
+ for (const systemFontFamily of systemFontFamilies) {
+ const normalizedFamily = normalizeSystemFontFamilyName(systemFontFamily.family);
+ if (!normalizedFamily) continue;
+
+ const normalizedLabel = normalizeSystemFontFamilyName(systemFontFamily.label) ?? normalizedFamily;
+ if (!options.has(normalizedFamily)) {
+ options.set(normalizedFamily, {
+ family: normalizedFamily,
+ label: normalizedLabel
+ });
+ }
+ }
+ if (options.size === 0 && family !== null) {
+ options.set(family, {
+ family,
+ label: family
+ });
+ }
+
+ return [
+ { family: null, label: defaultLabel },
+ ...Array.from(options.values())
+ .sort((first, second) => first.label.localeCompare(second.label) || first.family!.localeCompare(second.family!))
+ ];
+}
+
+function optionMatchesQuery(option: FontFamilyOption, query: string) {
+ const normalizedQuery = query.trim().toLocaleLowerCase();
+ if (!normalizedQuery) return true;
+
+ return option.label.toLocaleLowerCase().includes(normalizedQuery);
+}
+
+function optionStyle(option: FontFamilyOption): CSSProperties | undefined {
+ if (option.family === null) return undefined;
+
+ return {
+ fontFamily: systemFontFamilyCssValue(option.family, "var(--font-ui)")
+ };
+}
+
+export function FontFamilySelect({
+ defaultLabel,
+ family,
+ label,
+ onChange,
+ systemFontFamilies
+}: {
+ defaultLabel: string;
+ family: string | null;
+ label: string;
+ onChange: (family: string | null) => unknown;
+ systemFontFamilies: readonly AppSystemFontFamily[];
+}) {
+ const listboxId = `settings-font-family-options-${useId()}`;
+ const [activeIndex, setActiveIndex] = useState(0);
+ const [open, setOpen] = useState(false);
+ const [query, setQuery] = useState("");
+ const options = useMemo(
+ () => fontFamilyOptions(family, systemFontFamilies, defaultLabel),
+ [defaultLabel, family, systemFontFamilies]
+ );
+ const value = family === null
+ ? options[0]!
+ : options.find((option) => option.family === family) ?? options[0]!;
+ const inputValue = open ? query : value.label;
+ const inputStyle = open ? undefined : optionStyle(value);
+ const visibleOptions = useMemo(
+ () => options.filter((option) => optionMatchesQuery(option, query)),
+ [options, query]
+ );
+ const activeOption = visibleOptions[activeIndex] ?? visibleOptions[0];
+
+ useEffect(() => {
+ setActiveIndex(0);
+ }, [open, query]);
+
+ const selectOption = (option: FontFamilyOption) => {
+ setQuery("");
+ setOpen(false);
+ onChange(option.family);
+ };
+ const handleKeyDown = (event: ReactKeyboardEvent) => {
+ if (event.key === "ArrowDown") {
+ event.preventDefault();
+ setOpen(true);
+ if (visibleOptions.length === 0) return;
+ setActiveIndex((currentIndex) => Math.min(currentIndex + 1, visibleOptions.length - 1));
+ return;
+ }
+
+ if (event.key === "ArrowUp") {
+ event.preventDefault();
+ setOpen(true);
+ if (visibleOptions.length === 0) return;
+ setActiveIndex((currentIndex) => Math.max(currentIndex - 1, 0));
+ return;
+ }
+
+ if (event.key === "Enter" && open && activeOption) {
+ event.preventDefault();
+ selectOption(activeOption);
+ return;
+ }
+
+ if (event.key === "Escape") {
+ setOpen(false);
+ setQuery("");
+ }
+ };
+
+ return (
+
+
+
{
+ setOpen(false);
+ setQuery("");
+ }}
+ onChange={(event) => {
+ setQuery(event.currentTarget.value);
+ setOpen(true);
+ }}
+ onFocus={() => {
+ setQuery("");
+ setOpen(true);
+ }}
+ onKeyDown={handleKeyDown}
+ />
+ {open ? (
+
+ {visibleOptions.map((option, index) => (
+
+ ))}
+
+ ) : null}
+
+ );
+}
diff --git a/packages/app/src/lib/document-export.test.ts b/packages/app/src/lib/document-export.test.ts
index 4d3b757b..fb0c114c 100644
--- a/packages/app/src/lib/document-export.test.ts
+++ b/packages/app/src/lib/document-export.test.ts
@@ -76,6 +76,18 @@ describe("document export helpers", () => {
);
});
+ it("uses a selected system font for HTML and PDF document styles", () => {
+ const html = buildMarkdownHtmlDocument({
+ bodyHtml: "Mock export
",
+ fontFamily: "Example Serif",
+ language: "zh-CN",
+ title: "Mock font export"
+ });
+
+ expect(html).toContain('font-family: "Example Serif", ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;');
+ expect(html).not.toContain('font-family: "Noto Serif CJK SC"');
+ });
+
it("removes rendered pure LaTeX macro definition blocks from standalone documents", () => {
const html = buildMarkdownHtmlDocument({
bodyHtml: [
diff --git a/packages/app/src/lib/document-export.ts b/packages/app/src/lib/document-export.ts
index 8a05ff8f..5bd994ec 100644
--- a/packages/app/src/lib/document-export.ts
+++ b/packages/app/src/lib/document-export.ts
@@ -1,13 +1,19 @@
import katexStyles from "katex/dist/katex.css?raw";
import { isMarkraMathMacroDefinitionSource } from "@markra/editor";
+import { normalizeSystemFontFamilyName, systemFontFamilyCssValue } from "./editor-font";
export type ExportDocumentFormat = "html" | "pdf" | "docx" | "epub" | "latex";
const defaultPdfMarginMm = 18;
const defaultPdfPageHeightMm = 297;
const defaultPdfPageWidthMm = 210;
+const defaultExportFontFamily = 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif';
+const simplifiedChineseExportFontFamily =
+ '"Noto Serif CJK SC", "Noto Serif SC", "Source Han Serif SC", "Source Han Serif CN", "Songti SC", STSong, SimSun, ' +
+ defaultExportFontFamily;
export type MarkdownExportStyleOptions = {
+ fontFamily?: string | null;
pdfFooter?: string;
pdfHeader?: string;
pdfHeightMm?: number;
@@ -29,6 +35,7 @@ function normalizePdfPageDimensionMm(value: number | undefined, fallback: number
}
export function createMarkdownExportStyles({
+ fontFamily,
pdfFooter,
pdfHeader,
pdfHeightMm,
@@ -41,6 +48,21 @@ export function createMarkdownExportStyles({
const pageMarginMm = normalizePdfMarginMm(pdfMarginMm);
const pageHeightMm = normalizePdfPageDimensionMm(pdfHeightMm, defaultPdfPageHeightMm);
const pageWidthMm = normalizePdfPageDimensionMm(pdfWidthMm, defaultPdfPageWidthMm);
+ const normalizedFontFamily = normalizeSystemFontFamilyName(fontFamily);
+ const exportFontFamily = normalizedFontFamily
+ ? systemFontFamilyCssValue(normalizedFontFamily, defaultExportFontFamily)
+ : defaultExportFontFamily;
+ // The language-specific stack must not override the user's explicit font choice.
+ const simplifiedChineseFontStyles = normalizedFontFamily
+ ? ""
+ : `
+
+/* Keep Han text and Latin numerals on one CJK-capable family when possible. */
+:root:lang(zh-CN),
+:root:lang(zh-SG),
+:root:lang(zh-Hans) {
+ font-family: ${simplifiedChineseExportFontFamily};
+}`;
const pageBreakStyles = pdfPageBreakOnH1
? `
@@ -59,15 +81,8 @@ ${katexStyles}
:root {
color: #222;
background: #fff;
- font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
-}
-
-/* Keep Han text and Latin numerals on one CJK-capable family when possible. */
-:root:lang(zh-CN),
-:root:lang(zh-SG),
-:root:lang(zh-Hans) {
- font-family: "Noto Serif CJK SC", "Noto Serif SC", "Source Han Serif SC", "Source Han Serif CN", "Songti SC", STSong, SimSun, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
-}
+ font-family: ${exportFontFamily};
+}${simplifiedChineseFontStyles}
body {
margin: 0;
@@ -368,6 +383,7 @@ export const markdownExportStyles = createMarkdownExportStyles();
type BuildMarkdownHtmlDocumentInput = {
bodyHtml: string;
+ fontFamily?: string | null;
language?: string;
pdfAuthor?: string;
pdfFooter?: string;
@@ -469,6 +485,7 @@ export function localFileUrlFromPath(path: string) {
export function buildMarkdownHtmlDocument({
bodyHtml,
+ fontFamily,
language = "en",
pdfAuthor,
pdfFooter,
@@ -487,6 +504,7 @@ export function buildMarkdownHtmlDocument({
const escapedHeader = escapeHtmlText(pdfHeader?.trim() ?? "");
const exportBodyHtml = removeRenderedMathMacroDefinitions(removeRenderedMathMacroDefinitionMarkers(bodyHtml));
const documentStyles = styles ?? createMarkdownExportStyles({
+ fontFamily,
pdfFooter,
pdfHeader,
pdfHeightMm,
diff --git a/packages/app/src/lib/editor-font.ts b/packages/app/src/lib/editor-font.ts
index e21212f3..06ce06a3 100644
--- a/packages/app/src/lib/editor-font.ts
+++ b/packages/app/src/lib/editor-font.ts
@@ -16,7 +16,12 @@ export const defaultEditorFontFamily: EditorFontFamilyPreference = {
const visualEditorFontFallback = "var(--font-ui)";
function quoteCssFontFamilyName(family: string) {
- return `"${family.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
+ // Angle brackets are escaped because export CSS is serialized inside a raw