44 */
55
66import { 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+
6485export 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 = / @ i m p o r t \s + u r l \( [ " ' ] ? ( [ ^ " ' ) ] + ) [ " ' ] ? \) / 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 = / u r l \( ( [ ^ ) ] + ) \) / g;
122+ const urlRegex = / u r l \( ( [ " ' ] ? ) ( [ ^ " ' ) ] + ) \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 = / u r l \( ( [ ^ ) ] + ) \) / g;
180+ const urlRegex = / u r l \( ( [ " ' ] ? ) ( [ ^ " ' ) ] + ) \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