@@ -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 Sass Importer base class that provides the load logic to rebase all `url()` functions
2418 * within a stylesheet. The rebasing will ensure that the URLs in the output of the Sass compiler
@@ -45,44 +39,42 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
4539
4640 load ( canonicalUrl : URL ) : ImporterResult | null {
4741 const stylesheetPath = fileURLToPath ( canonicalUrl ) ;
42+ const stylesheetDirectory = dirname ( stylesheetPath ) ;
4843 let contents = readFileSync ( stylesheetPath , 'utf-8' ) ;
4944
5045 // Rebase any URLs that are found
51- if ( contents . includes ( 'url(' ) ) {
52- const stylesheetDirectory = dirname ( stylesheetPath ) ;
53-
54- let match ;
55- URL_REGEXP . lastIndex = 0 ;
56- let updatedContents ;
57- while ( ( match = URL_REGEXP . exec ( contents ) ) ) {
58- const originalUrl = match [ 2 ] ;
59-
60- // If root-relative, absolute or protocol relative url, leave as-is
61- if ( / ^ ( (?: \w + : ) ? \/ \/ | d a t a : | c h r o m e : | # | \/ ) / . test ( originalUrl ) ) {
62- continue ;
63- }
46+ let updatedContents ;
47+ for ( const { start, end, value } of findUrls ( contents ) ) {
48+ // Skip if value is empty or a Sass variable
49+ if ( value . length === 0 || value . startsWith ( '$' ) ) {
50+ continue ;
51+ }
6452
65- const rebasedPath = relative ( this . entryDirectory , join ( stylesheetDirectory , originalUrl ) ) ;
53+ // Skip if root-relative, absolute or protocol relative url
54+ if ( / ^ ( (?: \w + : ) ? \/ \/ | d a t a : | c h r o m e : | # | \/ ) / . test ( value ) ) {
55+ continue ;
56+ }
6657
67- // Normalize path separators and escape characters
68- // https://developer.mozilla.org/en-US/docs/Web/CSS/url#syntax
69- const rebasedUrl = './' + rebasedPath . replace ( / \\ / g, '/' ) . replace ( / [ ( ) \s ' " ] / g, '\\$&' ) ;
58+ const rebasedPath = relative ( this . entryDirectory , join ( stylesheetDirectory , value ) ) ;
7059
71- updatedContents ??= new MagicString ( contents ) ;
72- updatedContents . update ( match . index , match . index + match [ 0 ] . length , ` url( ${ rebasedUrl } )` ) ;
73- }
60+ // Normalize path separators and escape characters
61+ // https://developer.mozilla.org/en-US/docs/Web/CSS/ url#syntax
62+ const rebasedUrl = './' + rebasedPath . replace ( / \\ / g , '/' ) . replace ( / [ ( ) \s ' " ] / g , '\\$&' ) ;
7463
75- if ( updatedContents ) {
76- contents = updatedContents . toString ( ) ;
77- if ( this . rebaseSourceMaps ) {
78- // Generate an intermediate source map for the rebasing changes
79- const map = updatedContents . generateMap ( {
80- hires : true ,
81- includeContent : true ,
82- source : canonicalUrl . href ,
83- } ) ;
84- this . rebaseSourceMaps . set ( canonicalUrl . href , map as RawSourceMap ) ;
85- }
64+ updatedContents ??= new MagicString ( contents ) ;
65+ updatedContents . update ( start , end , rebasedUrl ) ;
66+ }
67+
68+ if ( updatedContents ) {
69+ contents = updatedContents . toString ( ) ;
70+ if ( this . rebaseSourceMaps ) {
71+ // Generate an intermediate source map for the rebasing changes
72+ const map = updatedContents . generateMap ( {
73+ hires : true ,
74+ includeContent : true ,
75+ source : canonicalUrl . href ,
76+ } ) ;
77+ this . rebaseSourceMaps . set ( canonicalUrl . href , map as RawSourceMap ) ;
8678 }
8779 }
8880
@@ -107,6 +99,164 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
10799 }
108100}
109101
102+ /**
103+ * Determines if a unicode code point is a CSS whitespace character.
104+ * @param code The unicode code point to test.
105+ * @returns true, if the code point is CSS whitespace; false, otherwise.
106+ */
107+ function isWhitespace ( code : number ) : boolean {
108+ // Based on https://www.w3.org/TR/css-syntax-3/#whitespace
109+ switch ( code ) {
110+ case 0x0009 : // tab
111+ case 0x0020 : // space
112+ case 0x000a : // line feed
113+ case 0x000c : // form feed
114+ case 0x000d : // carriage return
115+ return true ;
116+ default :
117+ return false ;
118+ }
119+ }
120+
121+ /**
122+ * Scans a CSS or Sass file and locates all valid url function values as defined by the CSS
123+ * syntax specification.
124+ * @param contents A string containing a CSS or Sass file to scan.
125+ * @returns An iterable that yields each CSS url function value found.
126+ */
127+ function * findUrls ( contents : string ) : Iterable < { start : number ; end : number ; value : string } > {
128+ let pos = 0 ;
129+ let width = 1 ;
130+ let current = - 1 ;
131+ const next = ( ) => {
132+ pos += width ;
133+ current = contents . codePointAt ( pos ) ?? - 1 ;
134+ width = current > 0xffff ? 2 : 1 ;
135+
136+ return current ;
137+ } ;
138+
139+ // Based on https://www.w3.org/TR/css-syntax-3/#consume-ident-like-token
140+ while ( ( pos = contents . indexOf ( 'url(' , pos ) ) !== - 1 ) {
141+ // Set to position of the (
142+ pos += 3 ;
143+ width = 1 ;
144+
145+ // Consume all leading whitespace
146+ while ( isWhitespace ( next ( ) ) ) {
147+ /* empty */
148+ }
149+
150+ // Initialize URL state
151+ const url = { start : pos , end : - 1 , value : '' } ;
152+ let complete = false ;
153+
154+ // If " or ', then consume the value as a string
155+ if ( current === 0x0022 || current === 0x0027 ) {
156+ const ending = current ;
157+ // Based on https://www.w3.org/TR/css-syntax-3/#consume-string-token
158+ while ( ! complete ) {
159+ switch ( next ( ) ) {
160+ case - 1 : // EOF
161+ return ;
162+ case 0x000a : // line feed
163+ case 0x000c : // form feed
164+ case 0x000d : // carriage return
165+ // Invalid
166+ complete = true ;
167+ break ;
168+ case 0x005c : // \ -- character escape
169+ // If not EOF or newline, add the character after the escape
170+ switch ( next ( ) ) {
171+ case - 1 :
172+ return ;
173+ case 0x000a : // line feed
174+ case 0x000c : // form feed
175+ case 0x000d : // carriage return
176+ // Skip when inside a string
177+ break ;
178+ default :
179+ // TODO: Handle hex escape codes
180+ url . value += String . fromCodePoint ( current ) ;
181+ break ;
182+ }
183+ break ;
184+ case ending :
185+ // Full string position should include the quotes for replacement
186+ url . end = pos + 1 ;
187+ complete = true ;
188+ yield url ;
189+ break ;
190+ default :
191+ url . value += String . fromCodePoint ( current ) ;
192+ break ;
193+ }
194+ }
195+
196+ next ( ) ;
197+ continue ;
198+ }
199+
200+ // Based on https://www.w3.org/TR/css-syntax-3/#consume-url-token
201+ while ( ! complete ) {
202+ switch ( current ) {
203+ case - 1 : // EOF
204+ return ;
205+ case 0x0022 : // "
206+ case 0x0027 : // '
207+ case 0x0028 : // (
208+ // Invalid
209+ complete = true ;
210+ break ;
211+ case 0x0029 : // )
212+ // URL is valid and complete
213+ url . end = pos ;
214+ complete = true ;
215+ break ;
216+ case 0x005c : // \ -- character escape
217+ // If not EOF or newline, add the character after the escape
218+ switch ( next ( ) ) {
219+ case - 1 : // EOF
220+ return ;
221+ case 0x000a : // line feed
222+ case 0x000c : // form feed
223+ case 0x000d : // carriage return
224+ // Invalid
225+ complete = true ;
226+ break ;
227+ default :
228+ // TODO: Handle hex escape codes
229+ url . value += String . fromCodePoint ( current ) ;
230+ break ;
231+ }
232+ break ;
233+ default :
234+ if ( isWhitespace ( current ) ) {
235+ while ( isWhitespace ( next ( ) ) ) {
236+ /* empty */
237+ }
238+ // Unescaped whitespace is only valid before the closing )
239+ if ( current === 0x0029 ) {
240+ // URL is valid
241+ url . end = pos ;
242+ }
243+ complete = true ;
244+ } else {
245+ // Add the character to the url value
246+ url . value += String . fromCodePoint ( current ) ;
247+ }
248+ break ;
249+ }
250+ next ( ) ;
251+ }
252+
253+ // An end position indicates a URL was found
254+ if ( url . end !== - 1 ) {
255+ yield url ;
256+ }
257+ }
258+ }
259+
110260/**
111261 * Provides the Sass importer logic to resolve relative stylesheet imports via both import and use rules
112262 * and also rebase any `url()` function usage within those stylesheets. The rebasing will ensure that
0 commit comments