Skip to content

Commit ccb7bdf

Browse files
committed
Perform CSS variable replacements recursively
1 parent ddd48b7 commit ccb7bdf

File tree

3 files changed

+45
-4
lines changed

3 files changed

+45
-4
lines changed

packages/tailwindcss-language-service/src/util/rewriting/add-theme-values.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export function addThemeValues(css: string, state: State, settings: TailwindCssS
6565
})
6666

6767
css = replaceCssVars(css, {
68+
recursive: false,
6869
replace({ name, range }) {
6970
if (!name.startsWith('--')) return null
7071

packages/tailwindcss-language-service/src/util/rewriting/index.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,16 @@ import { State, TailwindCssSettings } from '../state'
99
import { DesignSystem } from '../v4'
1010

1111
test('replacing CSS variables with their fallbacks (when they have them)', () => {
12-
let map = new Map<string, string>([['--known', 'blue']])
12+
let map = new Map<string, string>([
13+
['--known', 'blue'],
14+
['--level-1', 'var(--known)'],
15+
['--level-2', 'var(--level-1)'],
16+
['--level-3', 'var(--level-2)'],
17+
18+
['--circular-1', 'var(--circular-3)'],
19+
['--circular-2', 'var(--circular-1)'],
20+
['--circular-3', 'var(--circular-2)'],
21+
])
1322

1423
let state: State = {
1524
enabled: true,
@@ -57,6 +66,17 @@ test('replacing CSS variables with their fallbacks (when they have them)', () =>
5766

5867
// Unknown theme keys without fallbacks are not replaced
5968
expect(replaceCssVarsWithFallbacks(state, 'var(--unknown)')).toBe('var(--unknown)')
69+
70+
// Fallbacks are replaced recursively
71+
expect(replaceCssVarsWithFallbacks(state, 'var(--unknown,var(--unknown-2,red))')).toBe('red')
72+
expect(replaceCssVarsWithFallbacks(state, 'var(--level-1)')).toBe('blue')
73+
expect(replaceCssVarsWithFallbacks(state, 'var(--level-2)')).toBe('blue')
74+
expect(replaceCssVarsWithFallbacks(state, 'var(--level-3)')).toBe('blue')
75+
76+
// Circular replacements don't cause infinite loops
77+
expect(replaceCssVarsWithFallbacks(state, 'var(--circular-1)')).toBe('var(--circular-3)')
78+
expect(replaceCssVarsWithFallbacks(state, 'var(--circular-2)')).toBe('var(--circular-1)')
79+
expect(replaceCssVarsWithFallbacks(state, 'var(--circular-3)')).toBe('var(--circular-2)')
6080
})
6181

6282
test('Evaluating CSS calc expressions', () => {

packages/tailwindcss-language-service/src/util/rewriting/replacements.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ export interface Range {
1717
}
1818

1919
export interface ReplacerOptions {
20+
/**
21+
* Whether or not the replacement should be performed recursively
22+
*
23+
* default: true
24+
*/
25+
recursive?: boolean
26+
2027
/**
2128
* How to replace the CSS variable
2229
*/
@@ -32,6 +39,8 @@ export function replaceCssVars(
3239
str: string,
3340
{ replace, recursive = true }: ReplacerOptions,
3441
): string {
42+
let seen = new Set<string>()
43+
3544
for (let i = 0; i < str.length; ++i) {
3645
if (!str.startsWith('var(', i)) continue
3746

@@ -68,9 +77,20 @@ export function replaceCssVars(
6877
str = str.slice(0, i) + replacement + str.slice(j + 1)
6978
}
7079

71-
// We don't want to skip past anything here because `replacement`
72-
// might contain more var(…) calls in which case `i` will already
73-
// be pointing at the right spot to start looking for them
80+
// Move the index back one so it can look at the spot again since it'll
81+
// be incremented by the outer loop. However, since we're replacing
82+
// variables recursively we might end up in a loop so we need to keep
83+
// track of which variables we've already seen and where they were
84+
// replaced to avoid infinite loops.
85+
if (recursive) {
86+
let key = `${i}:${replacement}`
87+
88+
if (!seen.has(key)) {
89+
seen.add(key)
90+
i -= 1
91+
}
92+
}
93+
7494
break
7595
}
7696
}

0 commit comments

Comments
 (0)