Skip to content

Commit d43141a

Browse files
committed
Lookup variables in the CSS theme
1 parent 78802a3 commit d43141a

File tree

5 files changed

+75
-13
lines changed

5 files changed

+75
-13
lines changed

packages/tailwindcss-language-server/src/util/v4/design-system.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,22 @@ export async function loadDesignSystem(
172172
? postcss.root({ nodes }).toString().trim()
173173
: nodes.toString().trim()
174174
},
175+
176+
lookupThemeValue(themeKey: string): string | null {
177+
if (!themeKey.startsWith('--')) {
178+
return null
179+
}
180+
181+
let details = design.theme.values.get(themeKey)
182+
183+
// Early versions of v4 had string values
184+
if (typeof details === 'string') {
185+
return details
186+
}
187+
188+
// Later versions have an object with more data
189+
return details.value
190+
}
175191
})
176192

177193
return design

packages/tailwindcss-language-service/src/util/color.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ const colorRegex = new RegExp(
5050
'gi',
5151
)
5252

53-
function getColorsInString(str: string): (culori.Color | KeywordColor)[] {
53+
function getColorsInString(state: State, str: string): (culori.Color | KeywordColor)[] {
5454
if (/(?:box|drop)-shadow/.test(str)) return []
5555

5656
function toColor(match: RegExpMatchArray) {
5757
let color = match[1].replace(/var\([^)]+\)/, '1')
5858
return getKeywordColor(color) ?? culori.parse(color)
5959
}
6060

61-
str = replaceCssVarsWithFallbacks(str)
61+
str = replaceCssVarsWithFallbacks(state, str)
6262
str = removeColorMixWherePossible(str)
6363

6464
let possibleColors = str.matchAll(colorRegex)
@@ -67,6 +67,7 @@ function getColorsInString(str: string): (culori.Color | KeywordColor)[] {
6767
}
6868

6969
function getColorFromDecls(
70+
state: State,
7071
decls: Record<string, string | string[]>,
7172
): culori.Color | KeywordColor | null {
7273
let props = Object.keys(decls).filter((prop) => {
@@ -93,7 +94,9 @@ function getColorFromDecls(
9394

9495
const propsToCheck = areAllCustom ? props : nonCustomProps
9596

96-
const colors = propsToCheck.flatMap((prop) => ensureArray(decls[prop]).flatMap(getColorsInString))
97+
const colors = propsToCheck.flatMap((prop) => ensureArray(decls[prop]).flatMap((str) => {
98+
return getColorsInString(state, str)
99+
}))
97100

98101
// check that all of the values are valid colors
99102
// if (colors.some((color) => color instanceof TinyColor && !color.isValid)) {
@@ -164,7 +167,7 @@ function getColorFromRoot(state: State, css: postcss.Root): culori.Color | Keywo
164167
decls[decl.prop].push(decl.value)
165168
})
166169

167-
return getColorFromDecls(decls)
170+
return getColorFromDecls(state, decls)
168171
}
169172

170173
export function getColor(state: State, className: string): culori.Color | KeywordColor | null {
@@ -180,7 +183,7 @@ export function getColor(state: State, className: string): culori.Color | Keywor
180183
if (state.classNames) {
181184
const item = dlv(state.classNames.classNames, [className, '__info'])
182185
if (item && item.__rule) {
183-
return getColorFromDecls(removeMeta(item))
186+
return getColorFromDecls(state, removeMeta(item))
184187
}
185188
}
186189

@@ -209,7 +212,7 @@ export function getColor(state: State, className: string): culori.Color | Keywor
209212
decls[decl.prop] = decl.value
210213
}
211214
})
212-
return getColorFromDecls(decls)
215+
return getColorFromDecls(state, decls)
213216
}
214217

215218
let parts = getClassNameParts(state, className)
@@ -218,7 +221,7 @@ export function getColor(state: State, className: string): culori.Color | Keywor
218221
const item = dlv(state.classNames.classNames, [...parts, '__info'])
219222
if (!item.__rule) return null
220223

221-
return getColorFromDecls(removeMeta(item))
224+
return getColorFromDecls(state, removeMeta(item))
222225
}
223226

224227
export function getColorFromValue(value: unknown): culori.Color | KeywordColor | null {
Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,53 @@
11
import { expect, test } from 'vitest'
22
import { replaceCssVarsWithFallbacks } from './css-vars'
3+
import { State } from './state'
4+
import { DesignSystem } from './v4'
35

46
test('replacing CSS variables with their fallbacks (when they have them)', () => {
5-
expect(replaceCssVarsWithFallbacks('var(--foo, red)')).toBe(' red')
6-
expect(replaceCssVarsWithFallbacks('var(--foo, )')).toBe(' ')
7+
let map = new Map<string, string>([
8+
['--foo', 'blue'],
9+
])
710

8-
expect(replaceCssVarsWithFallbacks('rgb(var(--foo, 255 0 0))')).toBe('rgb( 255 0 0)')
9-
expect(replaceCssVarsWithFallbacks('rgb(var(--foo, var(--bar)))')).toBe('rgb( var(--bar))')
11+
let state: State = {
12+
enabled: true,
13+
designSystem: {
14+
lookupThemeValue: (name) => map.get(name) ?? null,
15+
} as DesignSystem,
16+
}
17+
18+
expect(replaceCssVarsWithFallbacks(state, 'var(--foo, red)')).toBe(' red')
19+
expect(replaceCssVarsWithFallbacks(state, 'var(--foo, )')).toBe(' ')
20+
21+
expect(replaceCssVarsWithFallbacks(state, 'rgb(var(--foo, 255 0 0))')).toBe('rgb( 255 0 0)')
22+
expect(replaceCssVarsWithFallbacks(state, 'rgb(var(--foo, var(--bar)))')).toBe('rgb( var(--bar))')
1023

1124
expect(
12-
replaceCssVarsWithFallbacks('rgb(var(var(--bar, var(--baz), var(--qux), var(--thing))))'),
25+
replaceCssVarsWithFallbacks(
26+
state,
27+
'rgb(var(var(--bar, var(--baz), var(--qux), var(--thing))))',
28+
),
1329
).toBe('rgb(var(var(--bar, var(--baz), var(--qux), var(--thing))))')
1430

1531
expect(
1632
replaceCssVarsWithFallbacks(
33+
state,
1734
'rgb(var(--one, var(--bar, var(--baz), var(--qux), var(--thing))))',
1835
),
1936
).toBe('rgb( var(--baz), var(--qux), var(--thing))')
2037

2138
expect(
2239
replaceCssVarsWithFallbacks(
40+
state,
2341
'color-mix(in srgb, var(--color-primary, oklch(0 0 0 / 2.5)), var(--color-secondary, oklch(0 0 0 / 2.5)), 50%)',
2442
),
2543
).toBe('color-mix(in srgb, oklch(0 0 0 / 2.5), oklch(0 0 0 / 2.5), 50%)')
44+
45+
// Fallbacks take precedence over theme values
46+
expect(replaceCssVarsWithFallbacks(state, 'var(--foo, red)')).toBe(' red')
47+
48+
// Values from the theme are used when no fallback is provided
49+
expect(replaceCssVarsWithFallbacks(state, 'var(--foo)')).toBe('blue')
50+
51+
// Values not in the theme don't get replaced if no fallback is provided
52+
expect(replaceCssVarsWithFallbacks(state, 'var(--bar)')).toBe('var(--bar)')
2653
})

packages/tailwindcss-language-service/src/util/css-vars.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
export function replaceCssVarsWithFallbacks(str: string): string {
1+
import type { State } from "./state"
2+
3+
export function replaceCssVarsWithFallbacks(state: State, str: string): string {
24
return replaceCssVars(str, (name, fallback) => {
35
// If we have a fallback then we should use that value directly
46
if (fallback) return fallback
57

8+
// In a v4 project we should attempt to look up the value from the theme
9+
// this is because not all v4 projects will generate utilities where
10+
// variables have fallbacks
11+
if (state.designSystem) {
12+
return state.designSystem.lookupThemeValue(name)
13+
}
14+
615
// Don't replace the variable otherwise
716
return null
817
})

packages/tailwindcss-language-service/src/util/v4/design-system.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ export interface Theme {
66
// Prefix didn't exist for earlier Tailwind versions
77
prefix?: string
88
entries(): [string, any][]
9+
10+
// In early alpha versions theme contained string values
11+
// Later versions turned this into an object with more data
12+
// but we only care about the value
13+
// TODO: This is private API to Tailwind and we shouldn't be using it
14+
values: Map<string, string | { value: string }>
915
}
1016

1117
export interface ClassMetadata {
@@ -37,4 +43,5 @@ export interface DesignSystem {
3743
export interface DesignSystem {
3844
compile(classes: string[]): postcss.Root[]
3945
toCss(nodes: postcss.Root | postcss.Node[]): string
46+
lookupThemeValue(themeKey: string): string | null
4047
}

0 commit comments

Comments
 (0)