From 0d19accde5f0b6117685b95af9df279ddec6cc59 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 26 Jan 2026 01:25:10 +0100 Subject: [PATCH 1/3] Revert "fix: screenshots not supporting css @layers (@miodec) (#7450)" This reverts commit 2635d12f896bb827e1f89cb134c8abff98ea1772. --- frontend/src/styles/index.scss | 7 +- frontend/src/styles/vendor.scss | 23 +++- frontend/src/ts/test/test-screenshot.ts | 163 +----------------------- 3 files changed, 28 insertions(+), 165 deletions(-) diff --git a/frontend/src/styles/index.scss b/frontend/src/styles/index.scss index 98f89eaf2c94..9d1b742572ce 100644 --- a/frontend/src/styles/index.scss +++ b/frontend/src/styles/index.scss @@ -15,10 +15,13 @@ } } +// the screenshotting library has some issues with css layers +@import "fonts"; + @layer custom-styles { @import "buttons", "404", "ads", "account", "animations", "caret", - "commandline", "core", "fonts", "inputs", "keymap", "login", "monkey", - "nav", "notifications", "popups", "profile", "scroll", "settings", + "commandline", "core", "inputs", "keymap", "login", "monkey", "nav", + "notifications", "popups", "profile", "scroll", "settings", "account-settings", "leaderboards", "test", "loading", "friends", "media-queries"; diff --git a/frontend/src/styles/vendor.scss b/frontend/src/styles/vendor.scss index d3845c1879d7..0b716f23a71d 100644 --- a/frontend/src/styles/vendor.scss +++ b/frontend/src/styles/vendor.scss @@ -1,6 +1,27 @@ +@import "fontawesome-5"; // the screenshotting library has some issues with css layers + +/* fontawesome icon styles do not respect the hidden class from the hidden layer. +* By having these rules outside any layer we make sure that the display none is +* correctly applied when an element possesses both a .fa* class and the hidden class */ +.fas.hidden, +.fab.hidden, +.fa.hidden, +.far.hidden { + display: none; +} + +// same for invisible +.fas.invisible, +.fab.invisible, +.fa.invisible, +.far.invisible { + opacity: 0; + pointer-events: none; + visibility: hidden; +} + @import "normalize.css" layer(normalize); @layer vendor { - @import "fontawesome-5"; @import "slim-select/styles"; @import "balloon-css/src/balloon"; } diff --git a/frontend/src/ts/test/test-screenshot.ts b/frontend/src/ts/test/test-screenshot.ts index 1939d3095b04..0349011e6cc4 100644 --- a/frontend/src/ts/test/test-screenshot.ts +++ b/frontend/src/ts/test/test-screenshot.ts @@ -110,14 +110,6 @@ async function generateCanvas(): Promise { (document.querySelector("html") as HTMLElement).style.scrollBehavior = "auto"; window.scrollTo({ top: 0, behavior: "auto" }); - // --- Build embedded font CSS --- - let embeddedFontCss = ""; - try { - embeddedFontCss = await buildEmbeddedFontCss(); - } catch (e) { - console.warn("Failed to embed fonts:", e); - } - // --- Target Element Calculation --- const src = qs("#result .wrapper"); if (src === null) { @@ -126,8 +118,7 @@ async function generateCanvas(): Promise { revert(); return null; } - // Wait a frame to ensure all UI changes are rendered - await new Promise((resolve) => requestAnimationFrame(resolve)); + await Misc.sleep(50); // Small delay for render updates const sourceX = src.screenBounds().left ?? 0; const sourceY = src.screenBounds().top ?? 0; @@ -149,10 +140,6 @@ async function generateCanvas(): Promise { backgroundColor: getTheme().bg, // Sharp output scale: window.devicePixelRatio ?? 1, - - // Pass embedded font CSS with data URLs - font: embeddedFontCss ? { cssText: embeddedFontCss } : undefined, - style: { width: `${targetWidth}px`, height: `${targetHeight}px`, @@ -393,151 +380,3 @@ document.addEventListener("keyup", (event) => { ?.removeClass(["fas", "fa-download"]) ?.addClass(["far", "fa-image"]); }); - -//below is all ai magic - -/** - * Recursively extracts all @font-face rules from stylesheets, including those inside @layer - */ -function extractAllFontFaceRules(): CSSFontFaceRule[] { - const fontRules: CSSFontFaceRule[] = []; - - function traverseRules(rules: CSSRuleList): void { - for (const rule of rules) { - if (rule instanceof CSSFontFaceRule) { - fontRules.push(rule); - } else if ( - "cssRules" in rule && - typeof rule.cssRules === "object" && - rule.cssRules !== null - ) { - traverseRules(rule.cssRules as CSSRuleList); - } - } - } - - for (const sheet of document.styleSheets) { - try { - if (sheet?.cssRules?.length && sheet.cssRules.length > 0) { - traverseRules(sheet.cssRules); - } - } catch (e) { - console.warn("Cannot access stylesheet:", e); - } - } - - return fontRules; -} - -/** - * Fetches a font file and converts it to a data URL - */ -async function fontUrlToDataUrl(url: string): Promise { - try { - const absoluteUrl = new URL(url, window.location.href).href; - const response = await fetch(absoluteUrl, { - mode: "cors", - credentials: "omit", - }); - if (!response.ok) return null; - const blob = await response.blob(); - return await new Promise((resolve) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result as string); - reader.onerror = () => resolve(null); - reader.readAsDataURL(blob); - }); - } catch { - return null; - } -} - -/** - * Converts a @font-face rule to CSS text with embedded data URLs - */ -async function fontFaceRuleToEmbeddedCss( - rule: CSSFontFaceRule, -): Promise { - let cssText = rule.cssText; - const srcProperty = rule.style.getPropertyValue("src"); - - if (!srcProperty) return null; - - // Extract all url() references - const urlRegex = /url\(['"]?([^'"]+?)['"]?\)/g; - const matches = [...srcProperty.matchAll(urlRegex)]; - - if (matches.length === 0) return cssText; - - for (const match of matches) { - const originalUrl = match[1]; - if ( - typeof originalUrl !== "string" || - originalUrl === "" || - originalUrl.startsWith("data:") - ) { - continue; - } - const dataUrl = await fontUrlToDataUrl(originalUrl); - if (typeof dataUrl === "string" && dataUrl !== "") { - const urlPattern = new RegExp( - `url\\(['"]?${originalUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"]?\\)`, - "g", - ); - cssText = cssText.replace(urlPattern, () => `url(${dataUrl})`); - } - } - - return cssText; -} - -/** - * Collects all used font families in the document - */ -function getUsedFontFamilies(): Set { - const families = new Set(); - - // Walk through all elements - const walker = document.createTreeWalker( - document.body, - NodeFilter.SHOW_ELEMENT, - null, - ); - - let node: Node | null; - while ((node = walker.nextNode())) { - if (node instanceof HTMLElement) { - const fontFamily = getComputedStyle(node).fontFamily; - if (fontFamily) { - fontFamily.split(",").forEach((family) => { - families.add(family.trim().replace(/['"]/g, "").toLowerCase()); - }); - } - } - } - - return families; -} - -/** - * Builds font CSS with data URLs embedded, including fonts from @layer - */ -async function buildEmbeddedFontCss(): Promise { - const allFontRules = extractAllFontFaceRules(); - const usedFamilies = getUsedFontFamilies(); - const embeddedRules: string[] = []; - - for (const rule of allFontRules) { - const fontFamily = rule.style.getPropertyValue("font-family"); - if (!fontFamily) continue; - const normalizedFamily = fontFamily - .trim() - .replace(/['"]/g, "") - .toLowerCase(); - if (!usedFamilies.has(normalizedFamily)) continue; - const embeddedCss = await fontFaceRuleToEmbeddedCss(rule); - if (embeddedCss !== null) embeddedRules.push(embeddedCss); - } - - return embeddedRules.join("\n"); -} From 67752347b479f298762b6ecc83cd02a42e9cd189 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 26 Jan 2026 20:27:47 +0100 Subject: [PATCH 2/3] update version, re-apply stylesheet changes from #7450 --- frontend/package.json | 2 +- frontend/src/html/pages/test-result.html | 2 + frontend/src/styles/index.scss | 7 +-- frontend/src/styles/vendor.scss | 23 +------- frontend/src/ts/test/test-screenshot.ts | 4 +- pnpm-lock.yaml | 72 +++--------------------- 6 files changed, 17 insertions(+), 93 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index bac5f695da18..6f3305be43dc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -49,7 +49,7 @@ "idb": "8.0.3", "konami": "1.7.0", "lz-ts": "1.1.2", - "modern-screenshot": "4.6.5", + "modern-screenshot": "4.6.8", "object-hash": "3.0.0", "slim-select": "2.9.2", "stemmer": "2.0.1", diff --git a/frontend/src/html/pages/test-result.html b/frontend/src/html/pages/test-result.html index f40f5a3a80f8..7ccff0856f47 100644 --- a/frontend/src/html/pages/test-result.html +++ b/frontend/src/html/pages/test-result.html @@ -6,6 +6,8 @@
wpm
+ +