@@ -13,12 +13,6 @@ import { basename, dirname, extname, join, relative } from 'node:path';
13
13
import { fileURLToPath , pathToFileURL } from 'node:url' ;
14
14
import type { FileImporter , Importer , ImporterResult , Syntax } from 'sass' ;
15
15
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
-
22
16
/**
23
17
* A preprocessed cache entry for the files and directories within a previously searched
24
18
* directory when performing Sass import resolution.
@@ -54,44 +48,42 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
54
48
55
49
load ( canonicalUrl : URL ) : ImporterResult | null {
56
50
const stylesheetPath = fileURLToPath ( canonicalUrl ) ;
51
+ const stylesheetDirectory = dirname ( stylesheetPath ) ;
57
52
let contents = readFileSync ( stylesheetPath , 'utf-8' ) ;
58
53
59
54
// 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
+ }
68
61
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
+ }
73
66
74
- const rebasedPath = relative ( this . entryDirectory , join ( stylesheetDirectory , originalUrl ) ) ;
67
+ const rebasedPath = relative ( this . entryDirectory , join ( stylesheetDirectory , value ) ) ;
75
68
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, '\\$&' ) ;
79
72
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
+ }
83
76
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 ) ;
95
87
}
96
88
}
97
89
@@ -116,6 +108,164 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
116
108
}
117
109
}
118
110
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
+
119
269
/**
120
270
* Provides the Sass importer logic to resolve relative stylesheet imports via both import and use rules
121
271
* and also rebase any `url()` function usage within those stylesheets. The rebasing will ensure that
0 commit comments