mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
fix(studio): font faces a host saves keep their own weight and style (#4753)
* fix(studio): a family's saved font faces keep their own weight and style * fix(studio): keep the font face change within the comment ratchet * fix(studio): a saved font face cannot break the page, and only weights a browser keeps are written
This commit is contained in:
@@ -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> = {}): 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 = "<html><head></head><body></body></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: "</style><b>x" })).not.toContain(
|
||||
"</style>",
|
||||
);
|
||||
});
|
||||
|
||||
it("a file name with a replacement pattern is saved as written, not expanded into the page", () => {
|
||||
const html = "<html><head></head><body>rest</body></html>";
|
||||
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(/<body>/g)).toHaveLength(1);
|
||||
const again = ensureImportedFontFace(saved, { ...odd, path: "assets/Cash$&.ttf" }, "index.html");
|
||||
expect(again).toContain("Cash$&.ttf");
|
||||
expect(again.match(/<body>/g)).toHaveLength(1);
|
||||
});
|
||||
@@ -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(/</g, "\\3c ");
|
||||
}
|
||||
|
||||
export function fontFamilyFromAssetPath(path: string): string {
|
||||
@@ -28,5 +30,12 @@ export function fontFamilyFromAssetPath(path: string): string {
|
||||
}
|
||||
|
||||
export function importedFontFaceCss(asset: ImportedFontAsset, url: string = asset.url): string {
|
||||
return `@font-face { font-family: ${cssString(asset.family)}; src: url(${cssString(url)}); font-display: swap; }`;
|
||||
const weight =
|
||||
asset.weight && /^(?:[1-9]\d{0,2}|1000)(?: (?:[1-9]\d{0,2}|1000))?$/.test(asset.weight)
|
||||
? ` font-weight: ${asset.weight};`
|
||||
: "";
|
||||
const style = asset.style
|
||||
? ` font-style: ${asset.style === "italic" ? "italic" : "normal"};`
|
||||
: "";
|
||||
return `@font-face { font-family: ${cssString(asset.family)}; src: url(${cssString(url)});${weight}${style} font-display: swap; }`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// @vitest-environment jsdom
|
||||
import { expect, it } from "vitest";
|
||||
import type { DomEditSelection } from "../components/editor/domEditing";
|
||||
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
||||
import { commitDomStyles, type DomStyleCommitContext } from "./domStyleCommit";
|
||||
|
||||
const selection = (id: string): DomEditSelection =>
|
||||
({
|
||||
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 = '<html><head></head><body><h1 id="title"></h1><p id="subtitle"></p></body></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;');
|
||||
});
|
||||
@@ -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], `<style data-hf-studio-fonts="true">\n${nextCss}\n</style>`);
|
||||
return html.replace(
|
||||
styleMatch[0],
|
||||
() => `<style data-hf-studio-fonts="true">\n${nextCss}\n</style>`,
|
||||
);
|
||||
}
|
||||
|
||||
const styleTag = `<style data-hf-studio-fonts="true">\n${css}\n</style>`;
|
||||
if (/<\/head>/i.test(html)) {
|
||||
return html.replace(/<\/head>/i, ` ${styleTag}\n </head>`);
|
||||
return html.replace(/<\/head>/i, () => ` ${styleTag}\n </head>`);
|
||||
}
|
||||
return `${styleTag}\n${html}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user