@@ -13,12 +13,6 @@ import { basename, dirname, extname, join, relative } from 'node:path';
1313import { fileURLToPath , pathToFileURL } from 'node:url' ;
1414import type { FileImporter , Importer , ImporterResult , Syntax } from 'sass' ;
1515
16- /**
17- * A Regular expression used to find all `url()` functions within a stylesheet.
18- * From packages/angular_devkit/build_angular/src/webpack/plugins/postcss-cli-resources.ts
19- */
20- const URL_REGEXP = / u r l (?: \( \s * ( [ ' " ] ? ) ) ( .* ?) (?: \1\s * \) ) / g;
21-
2216/**
2317 * A preprocessed cache entry for the files and directories within a previously searched
2418 * directory when performing Sass import resolution.
@@ -54,44 +48,42 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
5448
5549 load ( canonicalUrl : URL ) : ImporterResult | null {
5650 const stylesheetPath = fileURLToPath ( canonicalUrl ) ;
51+ const stylesheetDirectory = dirname ( stylesheetPath ) ;
5752 let contents = readFileSync ( stylesheetPath , 'utf-8' ) ;
5853
5954 // Rebase any URLs that are found
60- if ( contents . includes ( 'url(' ) ) {
61- const stylesheetDirectory = dirname ( stylesheetPath ) ;
62-
63- let match ;
64- URL_REGEXP . lastIndex = 0 ;
65- let updatedContents ;
66- while ( ( match = URL_REGEXP . exec ( contents ) ) ) {
67- const originalUrl = match [ 2 ] ;
55+ let updatedContents ;
56+ for ( const { start, end, value } of findUrls ( contents ) ) {
57+ // Skip if value is empty or a Sass variable
58+ if ( value . length === 0 || value . startsWith ( '$' ) ) {
59+ continue ;
60+ }
6861
69- // If root-relative, absolute or protocol relative url, leave as-is
70- if ( / ^ ( (?: \w + : ) ? \/ \/ | d a t a : | c h r o m e : | # | \/ ) / . test ( originalUrl ) ) {
71- continue ;
72- }
62+ // Skip if root-relative, absolute or protocol relative url
63+ if ( / ^ ( (?: \w + : ) ? \/ \/ | d a t a : | c h r o m e : | # | \/ ) / . test ( value ) ) {
64+ continue ;
65+ }
7366
74- const rebasedPath = relative ( this . entryDirectory , join ( stylesheetDirectory , originalUrl ) ) ;
67+ const rebasedPath = relative ( this . entryDirectory , join ( stylesheetDirectory , value ) ) ;
7568
76- // Normalize path separators and escape characters
77- // https://developer.mozilla.org/en-US/docs/Web/CSS/url#syntax
78- const rebasedUrl = './' + rebasedPath . replace ( / \\ / g, '/' ) . replace ( / [ ( ) \s ' " ] / g, '\\$&' ) ;
69+ // Normalize path separators and escape characters
70+ // https://developer.mozilla.org/en-US/docs/Web/CSS/url#syntax
71+ const rebasedUrl = './' + rebasedPath . replace ( / \\ / g, '/' ) . replace ( / [ ( ) \s ' " ] / g, '\\$&' ) ;
7972
80- updatedContents ??= new MagicString ( contents ) ;
81- updatedContents . update ( match . index , match . index + match [ 0 ] . length , `url( ${ rebasedUrl } )` ) ;
82- }
73+ updatedContents ??= new MagicString ( contents ) ;
74+ updatedContents . update ( start , end , rebasedUrl ) ;
75+ }
8376
84- if ( updatedContents ) {
85- contents = updatedContents . toString ( ) ;
86- if ( this . rebaseSourceMaps ) {
87- // Generate an intermediate source map for the rebasing changes
88- const map = updatedContents . generateMap ( {
89- hires : true ,
90- includeContent : true ,
91- source : canonicalUrl . href ,
92- } ) ;
93- this . rebaseSourceMaps . set ( canonicalUrl . href , map as RawSourceMap ) ;
94- }
77+ if ( updatedContents ) {
78+ contents = updatedContents . toString ( ) ;
79+ if ( this . rebaseSourceMaps ) {
80+ // Generate an intermediate source map for the rebasing changes
81+ const map = updatedContents . generateMap ( {
82+ hires : true ,
83+ includeContent : true ,
84+ source : canonicalUrl . href ,
85+ } ) ;
86+ this . rebaseSourceMaps . set ( canonicalUrl . href , map as RawSourceMap ) ;
9587 }
9688 }
9789
@@ -116,6 +108,164 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
116108 }
117109}
118110
111+ /**
112+ * Determines if a unicode code point is a CSS whitespace character.
113+ * @param code The unicode code point to test.
114+ * @returns true, if the code point is CSS whitespace; false, otherwise.
115+ */
116+ function isWhitespace ( code : number ) : boolean {
117+ // Based on https://www.w3.org/TR/css-syntax-3/#whitespace
118+ switch ( code ) {
119+ case 0x0009 : // tab
120+ case 0x0020 : // space
121+ case 0x000a : // line feed
122+ case 0x000c : // form feed
123+ case 0x000d : // carriage return
124+ return true ;
125+ default :
126+ return false ;
127+ }
128+ }
129+
130+ /**
131+ * Scans a CSS or Sass file and locates all valid url function values as defined by the CSS
132+ * syntax specification.
133+ * @param contents A string containing a CSS or Sass file to scan.
134+ * @returns An iterable that yields each CSS url function value found.
135+ */
136+ function * findUrls ( contents : string ) : Iterable < { start : number ; end : number ; value : string } > {
137+ let pos = 0 ;
138+ let width = 1 ;
139+ let current = - 1 ;
140+ const next = ( ) => {
141+ pos += width ;
142+ current = contents . codePointAt ( pos ) ?? - 1 ;
143+ width = current > 0xffff ? 2 : 1 ;
144+
145+ return current ;
146+ } ;
147+
148+ // Based on https://www.w3.org/TR/css-syntax-3/#consume-ident-like-token
149+ while ( ( pos = contents . indexOf ( 'url(' , pos ) ) !== - 1 ) {
150+ // Set to position of the (
151+ pos += 3 ;
152+ width = 1 ;
153+
154+ // Consume all leading whitespace
155+ while ( isWhitespace ( next ( ) ) ) {
156+ /* empty */
157+ }
158+
159+ // Initialize URL state
160+ const url = { start : pos , end : - 1 , value : '' } ;
161+ let complete = false ;
162+
163+ // If " or ', then consume the value as a string
164+ if ( current === 0x0022 || current === 0x0027 ) {
165+ const ending = current ;
166+ // Based on https://www.w3.org/TR/css-syntax-3/#consume-string-token
167+ while ( ! complete ) {
168+ switch ( next ( ) ) {
169+ case - 1 : // EOF
170+ return ;
171+ case 0x000a : // line feed
172+ case 0x000c : // form feed
173+ case 0x000d : // carriage return
174+ // Invalid
175+ complete = true ;
176+ break ;
177+ case 0x005c : // \ -- character escape
178+ // If not EOF or newline, add the character after the escape
179+ switch ( next ( ) ) {
180+ case - 1 :
181+ return ;
182+ case 0x000a : // line feed
183+ case 0x000c : // form feed
184+ case 0x000d : // carriage return
185+ // Skip when inside a string
186+ break ;
187+ default :
188+ // TODO: Handle hex escape codes
189+ url . value += String . fromCodePoint ( current ) ;
190+ break ;
191+ }
192+ break ;
193+ case ending :
194+ // Full string position should include the quotes for replacement
195+ url . end = pos + 1 ;
196+ complete = true ;
197+ yield url ;
198+ break ;
199+ default :
200+ url . value += String . fromCodePoint ( current ) ;
201+ break ;
202+ }
203+ }
204+
205+ next ( ) ;
206+ continue ;
207+ }
208+
209+ // Based on https://www.w3.org/TR/css-syntax-3/#consume-url-token
210+ while ( ! complete ) {
211+ switch ( current ) {
212+ case - 1 : // EOF
213+ return ;
214+ case 0x0022 : // "
215+ case 0x0027 : // '
216+ case 0x0028 : // (
217+ // Invalid
218+ complete = true ;
219+ break ;
220+ case 0x0029 : // )
221+ // URL is valid and complete
222+ url . end = pos ;
223+ complete = true ;
224+ break ;
225+ case 0x005c : // \ -- character escape
226+ // If not EOF or newline, add the character after the escape
227+ switch ( next ( ) ) {
228+ case - 1 : // EOF
229+ return ;
230+ case 0x000a : // line feed
231+ case 0x000c : // form feed
232+ case 0x000d : // carriage return
233+ // Invalid
234+ complete = true ;
235+ break ;
236+ default :
237+ // TODO: Handle hex escape codes
238+ url . value += String . fromCodePoint ( current ) ;
239+ break ;
240+ }
241+ break ;
242+ default :
243+ if ( isWhitespace ( current ) ) {
244+ while ( isWhitespace ( next ( ) ) ) {
245+ /* empty */
246+ }
247+ // Unescaped whitespace is only valid before the closing )
248+ if ( current === 0x0029 ) {
249+ // URL is valid
250+ url . end = pos ;
251+ }
252+ complete = true ;
253+ } else {
254+ // Add the character to the url value
255+ url . value += String . fromCodePoint ( current ) ;
256+ }
257+ break ;
258+ }
259+ next ( ) ;
260+ }
261+
262+ // An end position indicates a URL was found
263+ if ( url . end !== - 1 ) {
264+ yield url ;
265+ }
266+ }
267+ }
268+
119269/**
120270 * Provides the Sass importer logic to resolve relative stylesheet imports via both import and use rules
121271 * and also rebase any `url()` function usage within those stylesheets. The rebasing will ensure that
0 commit comments