diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ad564f..b38b823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fixed saving from the Figma plugin, which did nothing and logged `SecurityError: Failed to read the 'localStorage' property from 'Window'`. The editor runs inside Figma's sandboxed `about:srcdoc` frame, where reading `localStorage` is denied, and the save's rate limiter read it on every push and threw before the save could run. Storage access now falls back to an in-memory store when the browser blocks it, so saving from the Figma Desktop app works again. - Fixed the Oxygen Classic builder hanging on its loading screen when the active preset contained custom fonts. The font list was written into Oxygen's `ng-init` attribute without HTML-escaping, so the first quote closed the attribute and left AngularJS with a truncated expression that never finished loading the builder. The value is now escaped as Oxygen's own core does. - Stopped disabled fonts from reaching the Oxygen and Bricks builders. The Oxygen font dropdown and the styles injected into both builders now cover only the fonts you have enabled, matching what the editor previews. +- Fixed local fonts whose family name contains a space (for example Source Sans 3) never applying on the front end. WordPress stores the uploaded file under a sanitized name (spaces become dashes), but the generated `@font-face` `src` kept the spaces, so it pointed at a file that returns 404 and the browser dropped the font silently. The upload, the generated CSS, and deletion now all use the same sanitized file name. Re-save an affected font once after updating to regenerate its CSS. ## [2.0.1] - 2026-08-18 diff --git a/packages/core/src/components/modules/fonts/utils/__tests__/sanitizeFontFileName.test.ts b/packages/core/src/components/modules/fonts/utils/__tests__/sanitizeFontFileName.test.ts new file mode 100644 index 0000000..b033f60 Binary files /dev/null and b/packages/core/src/components/modules/fonts/utils/__tests__/sanitizeFontFileName.test.ts differ diff --git a/packages/core/src/components/modules/fonts/utils/utils.ts b/packages/core/src/components/modules/fonts/utils/utils.ts index 000ed59..00c495c 100644 --- a/packages/core/src/components/modules/fonts/utils/utils.ts +++ b/packages/core/src/components/modules/fonts/utils/utils.ts @@ -33,6 +33,46 @@ export const mergeRootSelectors = (cssString: string): string => { return mergedRoot + cleanedCss; }; +/** + * Sanitize a locally-stored font file name so it matches the file WordPress + * actually writes to disk. + * + * The WordPress upload endpoint runs the name through `sanitize_file_name()`, + * which collapses runs of whitespace and hyphens to a single `-` and strips a + * set of special characters. When a font family contains a space (e.g. + * "Source Sans 3") the raw name and the on-disk name diverge, so a + * `src: url(...)` built from the raw family points at a file that does not + * exist and the browser silently drops the `@font-face`. + * + * This mirrors the transforms `sanitize_file_name()` applies, so its output is + * a fixed point of that function: whatever the client sends is left unchanged + * by the server, and the generated CSS URL matches the stored file. + */ +export const sanitizeFontFileName = (name: string): string => { + // Special characters WordPress strips (wp-includes/formatting.php). Spaces + // are NOT in this set: they are collapsed to "-" by the step below, exactly + // as WordPress does. + const specialChars = new RegExp( + "[?\\[\\]/\\\\=<>:;,'\"&$#*()|~`!{}%+\\u2019\\u00ab\\u00bb\\u201d\\u201c\\u0000]", + "g", + ); + + return name + .replace(/ /g, " ") + .replace(specialChars, "") + .replace(/%20|\+/g, "-") + .replace(/[\r\n\t -]+/g, "-") + .replace(/^[.\-_]+|[.\-_]+$/g, ""); +}; + +/** + * Build the `.woff2` file name for a locally-stored font variant, matching the + * name WordPress writes on upload. Single source of truth for both the upload + * request and the generated `@font-face` `src`, so they can never diverge. + */ +export const localFontFileName = (family: string, variantId: string): string => + sanitizeFontFileName(`${family}-${variantId}.woff2`); + export const applyFontToStylesheet = (fontFamily: string): void => { const fontUrl = `https://fonts.googleapis.com/css2?family=${fontFamily?.replace(" ", "+")}`; const linkElement = document.createElement("link"); diff --git a/packages/wp/src/components/modules/fonts/FontsTab.tsx b/packages/wp/src/components/modules/fonts/FontsTab.tsx index f3278b8..2069896 100644 --- a/packages/wp/src/components/modules/fonts/FontsTab.tsx +++ b/packages/wp/src/components/modules/fonts/FontsTab.tsx @@ -14,7 +14,7 @@ import { import { FontNotFound } from "@core-framework/core/components/modules/fonts/components/FontNotFound"; import { FontsList } from "./components/FontsList"; import { FontData, FontVariantData } from "@core-framework/core/components/modules/fonts/types"; -import { blobToBase64, generateFontFaceCSS } from "./utils/utils"; +import { blobToBase64, generateFontFaceCSS, localFontFileName } from "./utils/utils"; enum Tabs { USER_FONTS = "user_fonts", @@ -67,7 +67,7 @@ export function FontsTab() { return { font_base64: fontBase64.split(",")[1], - filename: `${font.family}-${variant}.woff2`, + filename: localFontFileName(font.family, variant), }; }), ); diff --git a/packages/wp/src/components/modules/fonts/components/FontsList.tsx b/packages/wp/src/components/modules/fonts/components/FontsList.tsx index 4cd96ec..d1c36fc 100644 --- a/packages/wp/src/components/modules/fonts/components/FontsList.tsx +++ b/packages/wp/src/components/modules/fonts/components/FontsList.tsx @@ -4,7 +4,7 @@ import { GoogleFontLogo } from "../../../../assets/icons/GoogleFontLogo.icon"; import { Remove } from "../../../../assets/icons/Remove.icon"; import { ClassHeader } from "../../../ClassHeader"; import { FontData, FontVariantData } from "@core-framework/core/components/modules/fonts/types"; -import { generateFontFaceCSS } from "../utils/utils"; +import { generateFontFaceCSS, localFontFileName } from "../utils/utils"; import { Switch } from "@mantine/core"; import clsx from "clsx"; import { useAtom } from "jotai/index"; @@ -72,7 +72,7 @@ export function FontsList({ googleFonts }: { googleFonts: FontFace[] }) { "X-WP-Nonce": window.wpApiSettings.nonce, }, body: JSON.stringify({ - fonts: selectedVariants.map((v: string) => ({ filename: `${font.family}-${v}.woff2` })), + fonts: selectedVariants.map((v: string) => ({ filename: localFontFileName(font.family, v) })), }), }); const result = await response.json(); diff --git a/packages/wp/src/components/modules/fonts/utils/utils.ts b/packages/wp/src/components/modules/fonts/utils/utils.ts index e9fd2bc..e022924 100644 --- a/packages/wp/src/components/modules/fonts/utils/utils.ts +++ b/packages/wp/src/components/modules/fonts/utils/utils.ts @@ -1,4 +1,5 @@ -export { getFontProps, mergeRootSelectors, applyFontToStylesheet } from '@core-framework/core/components/modules/fonts/utils/utils'; +export { getFontProps, mergeRootSelectors, applyFontToStylesheet, sanitizeFontFileName, localFontFileName } from '@core-framework/core/components/modules/fonts/utils/utils'; +import { localFontFileName } from '@core-framework/core/components/modules/fonts/utils/utils'; import { FontData, FontVariantData } from '@core-framework/core/components/modules/fonts/types'; import { Font } from "fontkit"; import * as fontkit from "fontkit"; @@ -36,7 +37,7 @@ ${selectedFont.customSelectors} { const weight = variant.id.match(/\d{3}/)?.[0] || "400"; const style = variant.id.includes("italic") ? "italic" : "normal"; const subPath = getCustomSubpath(); - const fontURL = `${subPath}/wp-content/uploads/core-framework/fonts/${selectedFont.family}-${variant.id}.woff2`; + const fontURL = `${subPath}/wp-content/uploads/core-framework/fonts/${localFontFileName(selectedFont.family, variant.id)}`; const comment = variant.comment ? `\n//${variant.comment}` : ""; const cssSelector = variant.cssSelector.length ? `