@@ -26,36 +26,44 @@ import { isSyntaxMatchError, isSyntaxReferenceError } from "../util.js";
2626//-----------------------------------------------------------------------------
2727
2828/**
29- * Replaces all instances of a regex pattern with a replacement and tracks the offsets
30- * @param {string } text The text to perform replacements on
31- * @param {string } varName The regex pattern string to search for
32- * @param {string } replaceValue The string to replace with
33- * @returns {{text: string, offsets: Array<number>} } The updated text and array of offsets
34- * where replacements occurred
29+ * Extracts the list of fallback value or variable name used in a `var()` that is used as fallback function.
30+ * For example, for `var(--my-color, var(--fallback-color, red));` it will return `["--fallback-color", "red"]`.
31+ * @param {string } value The fallback value that is used in `var()`.
32+ * @return {Array<string> } The list of variable names of fallback value.
3533 */
36- function replaceWithOffsets ( text , varName , replaceValue ) {
37- const offsets = [ ] ;
38- let result = "" ;
39- let lastIndex = 0 ;
34+ function getVarFallbackList ( value ) {
35+ const list = [ ] ;
36+ let currentValue = value ;
4037
41- const regex = new RegExp ( `var\\(\\s*${ varName } \\s*\\)` , "gu" ) ;
42- let match ;
38+ while ( true ) {
39+ const match = currentValue . match (
40+ / v a r \( \s * ( - - [ ^ , \s ) ] + ) \s * (?: , \s * ( .+ ) ) ? \) / u,
41+ ) ;
4342
44- while ( ( match = regex . exec ( text ) ) !== null ) {
45- result += text . slice ( lastIndex , match . index ) ;
43+ if ( ! match ) {
44+ break ;
45+ }
4646
47- /*
48- * We need the offset of the replacement after other replacements have
49- * been made, so we push the current length of the result before appending
50- * the replacement value.
51- */
52- offsets . push ( result . length ) ;
53- result += replaceValue ;
54- lastIndex = match . index + match [ 0 ] . length ;
47+ const prop = match [ 1 ] . trim ( ) ;
48+ const fallback = match [ 2 ] ?. trim ( ) ;
49+
50+ list . push ( prop ) ;
51+
52+ if ( ! fallback ) {
53+ break ;
54+ }
55+
56+ // If fallback is not another var(), we're done
57+ if ( ! fallback . includes ( "var(" ) ) {
58+ list . push ( fallback ) ;
59+ break ;
60+ }
61+
62+ // Continue parsing from fallback
63+ currentValue = fallback ;
5564 }
5665
57- result += text . slice ( lastIndex ) ;
58- return { text : result , offsets } ;
66+ return list ;
5967}
6068
6169//-----------------------------------------------------------------------------
@@ -146,53 +154,151 @@ export default {
146154
147155 const varsFound = replacements . pop ( ) ;
148156
149- /** @type {Map<number ,CssLocationRange> } */
150- const varsFoundLocs = new Map ( ) ;
157+ /** @type {Map<string ,CssLocationRange> } */
158+ const valuesWithVarLocs = new Map ( ) ;
151159 const usingVars = varsFound ?. size > 0 ;
152160 let value = node . value ;
153161
154162 if ( usingVars ) {
155- // need to use a text version of the value here
156- value = sourceCode . getText ( node . value ) ;
157- let offsets ;
158-
159- // replace any custom properties with their values
160- for ( const [ name , func ] of varsFound ) {
161- const varValue = vars . get ( name ) ;
162-
163- if ( varValue ) {
164- ( { text : value , offsets } = replaceWithOffsets (
165- value ,
166- name ,
167- sourceCode . getText ( varValue ) . trim ( ) ,
168- ) ) ;
169-
170- /*
171- * Store the offsets of the replacements so we can
172- * report the correct location of any validation error.
173- */
174- offsets . forEach ( offset => {
175- varsFoundLocs . set ( offset , func . loc ) ;
176- } ) ;
177- } else if ( ! allowUnknownVariables ) {
178- context . report ( {
179- loc : func . children [ 0 ] . loc ,
180- messageId : "unknownVar" ,
181- data : {
182- var : name ,
183- } ,
184- } ) ;
185-
186- return ;
163+ const valueList = [ ] ;
164+ const valueNodes = node . value . children ;
165+
166+ // When `var()` is used, we store all the values to `valueList` with the replacement of `var()` with there values or fallback values
167+ for ( const child of valueNodes ) {
168+ // If value is a function starts with `var()`
169+ if ( child . type === "Function" && child . name === "var" ) {
170+ const varValue = vars . get ( child . children [ 0 ] . name ) ;
171+
172+ // If the variable is found, use its value, otherwise check for fallback values
173+ if ( varValue ) {
174+ const varValueText = sourceCode
175+ . getText ( varValue )
176+ . trim ( ) ;
177+
178+ valueList . push ( varValueText ) ;
179+ valuesWithVarLocs . set ( varValueText , child . loc ) ;
180+ } else {
181+ // If the variable is not found and doesn't have a fallback value, report it
182+ if ( child . children . length === 1 ) {
183+ if ( ! allowUnknownVariables ) {
184+ context . report ( {
185+ loc : child . children [ 0 ] . loc ,
186+ messageId : "unknownVar" ,
187+ data : {
188+ var : child . children [ 0 ] . name ,
189+ } ,
190+ } ) ;
191+
192+ return ;
193+ }
194+ } else {
195+ // If it has a fallback value, use that
196+ if ( child . children [ 2 ] . type === "Raw" ) {
197+ const fallbackVarList =
198+ getVarFallbackList (
199+ child . children [ 2 ] . value . trim ( ) ,
200+ ) ;
201+ if ( fallbackVarList . length > 0 ) {
202+ let gotFallbackVarValue = false ;
203+
204+ for ( const fallbackVar of fallbackVarList ) {
205+ if (
206+ fallbackVar . startsWith ( "--" )
207+ ) {
208+ const fallbackVarValue =
209+ vars . get ( fallbackVar ) ;
210+
211+ if ( ! fallbackVarValue ) {
212+ continue ; // Try the next fallback
213+ }
214+
215+ valueList . push (
216+ sourceCode
217+ . getText (
218+ fallbackVarValue ,
219+ )
220+ . trim ( ) ,
221+ ) ;
222+ valuesWithVarLocs . set (
223+ sourceCode
224+ . getText (
225+ fallbackVarValue ,
226+ )
227+ . trim ( ) ,
228+ child . loc ,
229+ ) ;
230+ gotFallbackVarValue = true ;
231+ break ; // Stop after finding the first valid variable
232+ } else {
233+ const fallbackValue =
234+ fallbackVar . trim ( ) ;
235+ valueList . push (
236+ fallbackValue ,
237+ ) ;
238+ valuesWithVarLocs . set (
239+ fallbackValue ,
240+ child . loc ,
241+ ) ;
242+ gotFallbackVarValue = true ;
243+ break ; // Stop after finding the first non-variable fallback
244+ }
245+ }
246+
247+ // If none of the fallback value is defined then report an error
248+ if (
249+ ! allowUnknownVariables &&
250+ ! gotFallbackVarValue
251+ ) {
252+ context . report ( {
253+ loc : child . children [ 0 ] . loc ,
254+ messageId : "unknownVar" ,
255+ data : {
256+ var : child . children [ 0 ]
257+ . name ,
258+ } ,
259+ } ) ;
260+
261+ return ;
262+ }
263+ } else {
264+ // if it has a fallback value, use that
265+ const fallbackValue =
266+ child . children [ 2 ] . value . trim ( ) ;
267+ valueList . push ( fallbackValue ) ;
268+ valuesWithVarLocs . set (
269+ fallbackValue ,
270+ child . loc ,
271+ ) ;
272+ }
273+ }
274+ }
275+ }
276+ } else {
277+ // If the child is not a `var()` function, just add its text to the `valueList`
278+ const valueText = sourceCode . getText ( child ) . trim ( ) ;
279+ valueList . push ( valueText ) ;
280+ valuesWithVarLocs . set ( valueText , child . loc ) ;
187281 }
188282 }
283+
284+ value =
285+ valueList . length > 0
286+ ? valueList . join ( " " )
287+ : sourceCode . getText ( node . value ) ;
189288 }
190289
191290 const { error } = lexer . matchProperty ( node . property , value ) ;
192291
193292 if ( error ) {
194293 // validation failure
195294 if ( isSyntaxMatchError ( error ) ) {
295+ const errorValue =
296+ usingVars &&
297+ value . slice (
298+ error . mismatchOffset ,
299+ error . mismatchOffset + error . mismatchLength ,
300+ ) ;
301+
196302 context . report ( {
197303 /*
198304 * When using variables, check to see if the error
@@ -201,7 +307,7 @@ export default {
201307 * reported location.
202308 */
203309 loc : usingVars
204- ? ( varsFoundLocs . get ( error . mismatchOffset ) ??
310+ ? ( valuesWithVarLocs . get ( errorValue ) ??
205311 node . value . loc )
206312 : error . loc ,
207313 messageId : "invalidPropertyValue" ,
0 commit comments