diff --git a/packages/studio/src/components/editor/fontAssets.test.ts b/packages/studio/src/components/editor/fontAssets.test.ts new file mode 100644 index 0000000000..52e5660fad --- /dev/null +++ b/packages/studio/src/components/editor/fontAssets.test.ts @@ -0,0 +1,69 @@ +// @vitest-environment jsdom +import { expect, it } from "vitest"; +import { ensureImportedFontFace, injectPreviewImportedFont } from "../../utils/studioFontHelpers"; +import { importedFontFaceCss, type ImportedFontAsset } from "./fontAssets"; + +const face = (file: string, extra: Partial = {}): ImportedFontAsset => ({ + family: "Poppins", + path: `assets/fonts/Poppins/${file}`, + url: `/preview/assets/fonts/Poppins/${file}`, + ...extra, +}); + +it("a face with no weight or style is written as before", () => { + expect(importedFontFaceCss(face("Poppins-Bold.ttf"))).toBe( + '@font-face { font-family: "Poppins"; src: url("/preview/assets/fonts/Poppins/Poppins-Bold.ttf"); font-display: swap; }', + ); +}); + +it("a face says which weight and style it draws, a variable file its weight range", () => { + expect( + importedFontFaceCss(face("Poppins-BoldItalic.ttf", { weight: "700", style: "italic" })), + ).toBe( + '@font-face { font-family: "Poppins"; src: url("/preview/assets/fonts/Poppins/Poppins-BoldItalic.ttf"); font-weight: 700; font-style: italic; font-display: swap; }', + ); + expect( + importedFontFaceCss(face("Roboto-Variable.ttf", { weight: "100 900", style: "normal" })), + ).toContain("font-weight: 100 900; font-style: normal;"); + expect(importedFontFaceCss(face("x.ttf", { weight: "700; color: red" }))).not.toContain( + "font-weight", + ); +}); + +it("two weights of one family are both kept in the file and both drawn in the preview", () => { + const bold = face("Poppins-Bold.ttf", { weight: "700", style: "normal" }); + const regular = face("Poppins-Regular.ttf", { weight: "400", style: "normal" }); + const html = ""; + const saved = ensureImportedFontFace( + ensureImportedFontFace(html, bold, "index.html"), + regular, + "index.html", + ); + expect(saved).toContain("font-weight: 700;"); + expect(saved).toContain("font-weight: 400;"); + expect(ensureImportedFontFace(saved, bold, "index.html")).toBe(saved); + injectPreviewImportedFont(document, bold); + injectPreviewImportedFont(document, regular); + injectPreviewImportedFont(document, bold); + expect(document.head.querySelectorAll("style")).toHaveLength(2); +}); + +it("a weight the browser would drop is left out, and a family cannot close the style block", () => { + for (const weight of ["0", "5000", "bold", "700;}"]) + expect(importedFontFaceCss(face("x.ttf", { weight }))).not.toContain("font-weight"); + expect(importedFontFaceCss(face("x.ttf", { weight: "1000" }))).toContain("font-weight: 1000;"); + expect(importedFontFaceCss({ ...face("x.ttf"), family: "x" })).not.toContain( + "", + ); +}); + +it("a file name with a replacement pattern is saved as written, not expanded into the page", () => { + const html = "rest"; + const odd = { family: "Cash", path: "assets/Cash$'Font.ttf", url: "/Cash$'Font.ttf" }; + const saved = ensureImportedFontFace(html, odd, "index.html"); + expect(saved).toContain("Cash$'Font.ttf"); + expect(saved.match(//g)).toHaveLength(1); + const again = ensureImportedFontFace(saved, { ...odd, path: "assets/Cash$&.ttf" }, "index.html"); + expect(again).toContain("Cash$&.ttf"); + expect(again.match(//g)).toHaveLength(1); +}); diff --git a/packages/studio/src/components/editor/fontAssets.ts b/packages/studio/src/components/editor/fontAssets.ts index df785f6558..5e75c19839 100644 --- a/packages/studio/src/components/editor/fontAssets.ts +++ b/packages/studio/src/components/editor/fontAssets.ts @@ -2,6 +2,8 @@ export interface ImportedFontAsset { family: string; path: string; url: string; + weight?: string; + style?: "normal" | "italic"; } const FONT_EXT_RE = /\.(eot|otf|ttc|ttf|woff2?)$/i; @@ -9,7 +11,7 @@ const FONT_STYLE_SUFFIX_RE = /\s+(thin|extralight|extra light|light|regular|roman|medium|semibold|semi bold|bold|extrabold|extra bold|black|italic|oblique|variable)$/i; function cssString(value: string): string { - return JSON.stringify(value); + return JSON.stringify(value).replace(/ + ({ + id, + selector: `#${id}`, + label: id, + tagName: "p", + sourceFile: "index.html", + compositionPath: "index.html", + textFields: [], + inlineStyles: {}, + computedStyles: {}, + dataAttributes: {}, + capabilities: { canSelect: true, canEditStyles: true }, + }) as unknown as DomEditSelection; + +const poppins = (file: string, weight: string): ImportedFontAsset => ({ + family: "Poppins", + path: `assets/fonts/Poppins/${file}`, + url: `/preview/assets/fonts/Poppins/${file}`, + weight, + style: "normal", +}); + +it("each font a host hands over is saved as its own face, with the weight and style it draws", async () => { + let html = '

'; + let listed: ImportedFontAsset | null = null; + const context: DomStyleCommitContext = { + activeCompPath: "index.html", + previewIframeRef: { current: null }, + showToast: () => undefined, + versions: new Map(), + resolveImportedFontAsset: () => listed, + persistDomEditOperations: (async ( + _selection: DomEditSelection, + _operations: unknown, + options?: { prepareContent?: (html: string, sourceFile: string) => string }, + ) => { + html = options?.prepareContent?.(html, "index.html") ?? html; + }) as unknown as DomStyleCommitContext["persistDomEditOperations"], + }; + listed = poppins("Poppins-SemiBold.ttf", "600"); + await commitDomStyles(context, selection("title"), { "font-family": "Poppins" }); + listed = poppins("Poppins-Regular.ttf", "400"); + await commitDomStyles(context, selection("subtitle"), { "font-family": "Poppins" }); + const faces = html.match(/@font-face[^}]*}/g) ?? []; + expect(faces).toHaveLength(2); + expect(faces[0]).toContain('Poppins-SemiBold.ttf"); font-weight: 600; font-style: normal;'); + expect(faces[1]).toContain('Poppins-Regular.ttf"); font-weight: 400; font-style: normal;'); +}); diff --git a/packages/studio/src/utils/studioFontHelpers.ts b/packages/studio/src/utils/studioFontHelpers.ts index 0fb3701412..dac634c4f2 100644 --- a/packages/studio/src/utils/studioFontHelpers.ts +++ b/packages/studio/src/utils/studioFontHelpers.ts @@ -52,7 +52,7 @@ export function injectPreviewGoogleFont(doc: Document, fontFamilyValue: string): } export function injectPreviewImportedFont(doc: Document, asset: ImportedFontAsset): void { - const id = `studio-imported-font-${asset.family.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`; + const id = `studio-imported-font-${asset.path.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`; if (doc.getElementById(id)) return; const style = doc.createElement("style"); style.id = id; @@ -72,12 +72,15 @@ export function ensureImportedFontFace( const styleMatch = styleRe.exec(html); if (styleMatch) { const nextCss = `${styleMatch[2].trim()}\n${css}`.trim(); - return html.replace(styleMatch[0], ``); + return html.replace( + styleMatch[0], + () => ``, + ); } const styleTag = ``; if (/<\/head>/i.test(html)) { - return html.replace(/<\/head>/i, ` ${styleTag}\n `); + return html.replace(/<\/head>/i, () => ` ${styleTag}\n `); } return `${styleTag}\n${html}`; }