From 2b6c3e1c003fc19c57094bbc915db5b571cf8853 Mon Sep 17 00:00:00 2001 From: David Babinec Date: Thu, 27 Aug 2026 17:00:43 +0200 Subject: [PATCH] fix(wp): match local font file names to the sanitized upload name --- CHANGELOG.md | 1 + .../__tests__/sanitizeFontFileName.test.ts | Bin 0 -> 2271 bytes .../components/modules/fonts/utils/utils.ts | 40 ++++++++++++++++++ .../src/components/modules/fonts/FontsTab.tsx | 4 +- .../modules/fonts/components/FontsList.tsx | 4 +- .../components/modules/fonts/utils/utils.ts | 5 ++- 6 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 packages/core/src/components/modules/fonts/utils/__tests__/sanitizeFontFileName.test.ts 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 0000000000000000000000000000000000000000..b033f60ea10bf6e110c49d9bef64f6d00fc35f6b GIT binary patch literal 2271 zcma)7QE%He5Z4pKx(l+i8V8dDjDTY1RUcyL}&6XxLl8Tc#9{M*1 zY{1_46ZY1h(qFPWQgYnb>Gt4H7J2vSc=z2=OiOJX%pvDCGvbnWfnS$Ab6pS)7El;H zh3MI9Ze4FI^(B)wI&6!^OX`ft6Rq5dko+Z^qPt~EIPoiAU0o8TX|Hm^bF_Os_(_}m z)NpH;Ugrf8ohxLDJHX0P3Z8@0;3gbxrU=dmHTLfrZYl?stK4>p&IUV&!2+|E8)sJy zi4}weCJkr#4a_FYfn?|nT0t)Co3ve2Dsuu~XXUHrAobB=@;|5p=rn&|-fm@D;TJ8Y zO)v>ZFle`0(bK34(STq?&?6WQ36BWAA$US?KyXOVCD=jOiFWYcXHl1kwrKSc!RG`q zL5JX$;5UK`f=>zN1Pg*~g55yz{_XGd_U>W$F~zl$b?HV}s+XP}Gb+@0W;TCACivrhV23l&G+#WUW~ZSU{h4tG6e zvT1)XG($CXkc>WJ^z$GcCg-Ew+xofX`8=KBt}+VLVSCYTH^Ppijmqr7nuakUD88d7 zR9ZYrlruZwDA5vgVL>QhP_U_xH$F>^>V7=?0jA7lWa`!Gv_b8Xo>vK5sM9k(Cef43 zD4ArvpYEk!z-2W)2+k&hH@tM=C46``K`OsMoG4io2a=I)VaD!SD90WQjqt3%&P)mn zI~bswau{}QbXoE|MS{@m@mdbz=#{PvI${@CcuX4ka&Ir{bW*4LyxC1?H+h`kRyxx~ zvG3FW5z_DaFW^-2Yfnq0U$2tlZbG|(7PtN%>8WHl1rr9o6(~M-QwJ*?lOkJ&jE8MS zjcHPwQslv|{uFf_iF|<&sxLXa!jdwuICmsZW;nB&`d7;73M-y}P~nGjdZsV6(`(5L z%lEQ)7eQ%sUXk=z5S*jAWh-+fD6s#Sp*YVbOpWh_xYEOepye)HF(a6ABkJh^Woxim z10%{2Zob5$1bKXaN=Nz%OWEu#>-<71e_kwRU601(!(8~)5l(IJlchtw*Qy7rpW8?4 zeH!13oa(kzG&yxrr5y_PIko|#VH5_KZ<`Zwjon$|jB<6$adX8D7@pK~g030714H^O zAk_qysA(F3KX6AhP2xlFF=3k?tDM;QFBM*);p*{IB7AkGE1AOtl?HXSQDb!Vl8s@r zFppP09ggRFaYGarTl0{cMY1)o>$(UEzv%chEmkF-X(?H0QA{}TIdhm895#53(%=ES zApNCRVr?eOu*`9@Dm%lc8ily21vX9GI64OCH=W(gljCH$L>a4F=mGyn{q5pVa_ J5>WHd?_V|Gya50J literal 0 HcmV?d00001 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 ? `