Skip to content

Commit 1a9f535

Browse files
committed
Feat: handling @import and optimice cache, closes #61
1 parent 19e71b7 commit 1a9f535

3 files changed

Lines changed: 106 additions & 59 deletions

File tree

src/api/preCache.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export async function preCache(root = document, options = {}) {
6868
}
6969

7070
if (embedFonts) {
71-
await embedCustomFonts({ ignoreIconFonts: !embedFonts, preCached: true });
71+
await embedCustomFonts({ ignoreIconFonts: !embedFonts, preCached: true });
7272
}
7373

7474
await Promise.all(promises);

src/core/cache.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ export const resourceCache = new Map();
99
export const defaultStylesCache = new Map();
1010
export const baseCSSCache = new Map();
1111
export const computedStyleCache = new WeakMap();
12+
export const processedFontURLs = new Set();
13+

src/modules/fonts.js

Lines changed: 103 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { isIconFont, extractURL} from "../utils/helpers"
7-
import { resourceCache } from "../core/cache"
7+
import { resourceCache, processedFontURLs } from "../core/cache"
88

99
/**
1010
* Converts a unicode character from an icon font into a data URL image.
@@ -52,6 +52,24 @@ export async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize
5252
return canvas.toDataURL();
5353
}
5454

55+
56+
function isStylesheetLoaded(href) {
57+
return Array.from(document.styleSheets).some(sheet => sheet.href === href);
58+
}
59+
60+
function injectLinkIfMissing(href) {
61+
return new Promise((resolve) => {
62+
if (isStylesheetLoaded(href)) return resolve(null);
63+
const link = document.createElement("link");
64+
link.rel = "stylesheet";
65+
link.href = href;
66+
link.setAttribute("data-snapdom", "injected-import");
67+
link.onload = () => resolve(link);
68+
link.onerror = () => resolve(null);
69+
document.head.appendChild(link);
70+
});
71+
}
72+
5573
/**
5674
* Embeds custom fonts found in the document as data URLs in CSS.
5775
*
@@ -61,66 +79,88 @@ export async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize
6179
* @param {boolean} [options.preCached=false] - Whether to use pre-cached resources
6280
* @returns {Promise<string>} The inlined CSS for custom fonts
6381
*/
82+
83+
84+
6485
export async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } = {}) {
65-
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter((link) => link.href);
86+
if (resourceCache.has('fonts-embed-css')) {
87+
if (preCached) {
88+
// Ya tenemos el CSS embebido, lo reaplicamos por si falta
89+
const style = document.createElement("style");
90+
style.setAttribute("data-snapdom", "embedFonts");
91+
style.textContent = resourceCache.get('fonts-embed-css');
92+
document.head.appendChild(style);
93+
}
94+
return resourceCache.get('fonts-embed-css');
95+
}
96+
97+
const importRegex = /@import\s+url\(["']?([^"')]+)["']?\)/g;
98+
const styleImports = [];
99+
100+
for (const styleTag of document.querySelectorAll("style")) {
101+
const cssText = styleTag.textContent || "";
102+
const matches = Array.from(cssText.matchAll(importRegex));
103+
for (const match of matches) {
104+
const importUrl = match[1];
105+
if (!isStylesheetLoaded(importUrl)) {
106+
styleImports.push(importUrl);
107+
}
108+
}
109+
}
110+
111+
await Promise.all(styleImports.map(injectLinkIfMissing));
112+
113+
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter(link => link.href);
66114
let finalCSS = "";
67115

68-
// Procesar <link rel="stylesheet">
69116
for (const link of links) {
70117
try {
71118
const res = await fetch(link.href);
72119
const cssText = await res.text();
73-
if (ignoreIconFonts && (isIconFont(link.href) || isIconFont(cssText))) {
74-
continue;
75-
}
120+
if (ignoreIconFonts && (isIconFont(link.href) || isIconFont(cssText))) continue;
76121

77-
const urlRegex = /url\(([^)]+)\)/g;
122+
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
78123
const inlinedCSS = await Promise.all(
79-
Array.from(cssText.matchAll(urlRegex)).map(async (match) => {
80-
let rawUrl = extractURL(match[0]);
81-
if (!rawUrl) return null;
124+
Array.from(cssText.matchAll(urlRegex)).map(async (match) => {
125+
let rawUrl = extractURL(match[0]);
126+
if (!rawUrl) return null;
82127

83-
let url = rawUrl;
84-
if (!url.startsWith("http")) {
85-
url = new URL(url, link.href).href;
86-
}
128+
let url = rawUrl;
129+
if (!url.startsWith("http") && !url.startsWith("data:")) {
130+
url = new URL(url, link.href).href;
131+
}
87132

88-
try {
89-
url = encodeURI(url);
90-
} catch (e) {
91-
console.warn("[snapdom] Failed to encode font URL:", url);
92-
}
133+
if (ignoreIconFonts && isIconFont(url)) return null;
93134

94-
if (ignoreIconFonts && isIconFont(url)) {
95-
return null;
96-
}
135+
if (resourceCache.has(url)) {
136+
processedFontURLs.add(url);
137+
return { original: match[0], inlined: `url(${resourceCache.get(url)})` };
138+
}
97139

98-
if (resourceCache.has(url)) {
99-
return { original: match[0], inlined: `url(${resourceCache.get(url)})` };
100-
}
140+
if (processedFontURLs.has(url)) return null;
141+
142+
try {
143+
const fontRes = await fetch(url);
144+
const blob = await fontRes.blob();
145+
const b64 = await new Promise(resolve => {
146+
const reader = new FileReader();
147+
reader.onload = () => resolve(reader.result);
148+
reader.readAsDataURL(blob);
149+
});
150+
resourceCache.set(url, b64);
151+
processedFontURLs.add(url);
152+
return { original: match[0], inlined: `url(${b64})` };
153+
} catch (e) {
154+
console.warn("[snapdom] Failed to fetch font resource:", url);
155+
return null;
156+
}
157+
})
158+
);
101159

102-
try {
103-
const fontRes = await fetch(url);
104-
const blob = await fontRes.blob();
105-
const b64 = await new Promise((resolve) => {
106-
const reader = new FileReader();
107-
reader.onload = () => resolve(reader.result);
108-
reader.readAsDataURL(blob);
109-
});
110-
resourceCache.set(url, b64);
111-
return { original: match[0], inlined: `url(${b64})` };
112-
} catch (err) {
113-
console.warn("[snapdom] Failed to fetch font:", url);
114-
return null;
115-
}
116-
})
117-
);
118160

119161
let cssFinal = cssText;
120162
for (const r of inlinedCSS) {
121-
if (r) {
122-
cssFinal = cssFinal.replace(r.original, r.inlined);
123-
}
163+
if (r) cssFinal = cssFinal.replace(r.original, r.inlined);
124164
}
125165

126166
finalCSS += cssFinal + "\n";
@@ -129,7 +169,6 @@ export async function embedCustomFonts({ ignoreIconFonts = true, preCached = fal
129169
}
130170
}
131171

132-
// Procesar @font-face en styleSheets accesibles
133172
for (const sheet of document.styleSheets) {
134173
try {
135174
if (!sheet.href || links.every(link => link.href !== sheet.href)) {
@@ -138,28 +177,29 @@ export async function embedCustomFonts({ ignoreIconFonts = true, preCached = fal
138177
const src = rule.style.getPropertyValue("src");
139178
if (!src) continue;
140179

141-
const urlRegex = /url\(([^)]+)\)/g;
180+
const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
142181
let inlinedSrc = src;
143182

144183
const matches = Array.from(src.matchAll(urlRegex));
145184
for (const match of matches) {
146-
let rawUrl = match[1].trim().replace(/^["']|["']$/g, "");
185+
let rawUrl = match[2].trim();
147186
if (!rawUrl) continue;
148187

149188
let url = rawUrl;
150189
if (!url.startsWith("http") && !url.startsWith("data:")) {
151190
url = new URL(url, sheet.href || location.href).href;
152191
}
153192

154-
if (ignoreIconFonts && isIconFont(url)) {
155-
continue;
156-
}
193+
if (ignoreIconFonts && isIconFont(url)) continue;
157194

158195
if (resourceCache.has(url)) {
196+
processedFontURLs.add(url);
159197
inlinedSrc = inlinedSrc.replace(match[0], `url(${resourceCache.get(url)})`);
160198
continue;
161199
}
162200

201+
if (processedFontURLs.has(url)) continue;
202+
163203
try {
164204
const res = await fetch(url);
165205
const blob = await res.blob();
@@ -169,6 +209,7 @@ export async function embedCustomFonts({ ignoreIconFonts = true, preCached = fal
169209
reader.readAsDataURL(blob);
170210
});
171211
resourceCache.set(url, b64);
212+
processedFontURLs.add(url);
172213
inlinedSrc = inlinedSrc.replace(match[0], `url(${b64})`);
173214
} catch (e) {
174215
console.warn("[snapdom] Failed to fetch font URL:", url);
@@ -189,14 +230,14 @@ export async function embedCustomFonts({ ignoreIconFonts = true, preCached = fal
189230
}
190231
}
191232

192-
// Procesar FontFace dinámicos con _snapdomSrc
193233
for (const font of document.fonts) {
194234
if (font.family && font.status === "loaded" && font._snapdomSrc) {
195235
let b64 = font._snapdomSrc;
196236
if (!b64.startsWith("data:")) {
197237
if (resourceCache.has(font._snapdomSrc)) {
198238
b64 = resourceCache.get(font._snapdomSrc);
199-
} else {
239+
processedFontURLs.add(font._snapdomSrc);
240+
} else if (!processedFontURLs.has(font._snapdomSrc)) {
200241
try {
201242
const res = await fetch(font._snapdomSrc);
202243
const blob = await res.blob();
@@ -206,6 +247,7 @@ export async function embedCustomFonts({ ignoreIconFonts = true, preCached = fal
206247
reader.readAsDataURL(blob);
207248
});
208249
resourceCache.set(font._snapdomSrc, b64);
250+
processedFontURLs.add(font._snapdomSrc);
209251
} catch (e) {
210252
console.warn("[snapdom] Failed to fetch dynamic font src:", font._snapdomSrc);
211253
continue;
@@ -222,12 +264,15 @@ export async function embedCustomFonts({ ignoreIconFonts = true, preCached = fal
222264
}
223265
}
224266

225-
if (finalCSS && preCached) {
226-
const style = document.createElement("style");
227-
style.setAttribute("data-snapdom", "embedFonts");
228-
style.textContent = finalCSS;
229-
document.head.appendChild(style);
267+
if (finalCSS) {
268+
resourceCache.set('fonts-embed-css', finalCSS);
269+
if (preCached) {
270+
const style = document.createElement("style");
271+
style.setAttribute("data-snapdom", "embedFonts");
272+
style.textContent = finalCSS;
273+
document.head.appendChild(style);
274+
}
230275
}
231276

232277
return finalCSS;
233-
}
278+
}

0 commit comments

Comments
 (0)