Skip to content

Lookup variables in the CSS theme #1082

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions packages/tailwindcss-language-service/src/util/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ const colorRegex = new RegExp(
'gi',
)

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

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

str = replaceCssVarsWithFallbacks(str)
str = replaceCssVarsWithFallbacks(state, str)
str = removeColorMixWherePossible(str)

let possibleColors = str.matchAll(colorRegex)
Expand All @@ -73,6 +73,7 @@ function getColorsInString(str: string): (culori.Color | KeywordColor)[] {
}

function getColorFromDecls(
state: State,
decls: Record<string, string | string[]>,
): culori.Color | KeywordColor | null {
let props = Object.keys(decls).filter((prop) => {
Expand All @@ -99,7 +100,9 @@ function getColorFromDecls(

const propsToCheck = areAllCustom ? props : nonCustomProps

const colors = propsToCheck.flatMap((prop) => ensureArray(decls[prop]).flatMap(getColorsInString))
const colors = propsToCheck.flatMap((prop) => ensureArray(decls[prop]).flatMap((str) => {
return getColorsInString(state, str)
}))

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

return getColorFromDecls(decls)
return getColorFromDecls(state, decls)
}

export function getColor(state: State, className: string): culori.Color | KeywordColor | null {
Expand All @@ -186,7 +189,7 @@ export function getColor(state: State, className: string): culori.Color | Keywor
if (state.classNames) {
const item = dlv(state.classNames.classNames, [className, '__info'])
if (item && item.__rule) {
return getColorFromDecls(removeMeta(item))
return getColorFromDecls(state, removeMeta(item))
}
}

Expand Down Expand Up @@ -215,7 +218,7 @@ export function getColor(state: State, className: string): culori.Color | Keywor
decls[decl.prop] = decl.value
}
})
return getColorFromDecls(decls)
return getColorFromDecls(state, decls)
}

let parts = getClassNameParts(state, className)
Expand All @@ -224,7 +227,7 @@ export function getColor(state: State, className: string): culori.Color | Keywor
const item = dlv(state.classNames.classNames, [...parts, '__info'])
if (!item.__rule) return null

return getColorFromDecls(removeMeta(item))
return getColorFromDecls(state, removeMeta(item))
}

export function getColorFromValue(value: unknown): culori.Color | KeywordColor | null {
Expand Down
40 changes: 35 additions & 5 deletions packages/tailwindcss-language-service/src/util/css-vars.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,56 @@
import { expect, test } from 'vitest'
import { replaceCssVarsWithFallbacks } from './css-vars'
import { State } from './state'
import { DesignSystem } from './v4'

test('replacing CSS variables with their fallbacks (when they have them)', () => {
expect(replaceCssVarsWithFallbacks('var(--foo, red)')).toBe(' red')
expect(replaceCssVarsWithFallbacks('var(--foo, )')).toBe(' ')
let map = new Map<string, string>([
['--known', 'blue'],
])

expect(replaceCssVarsWithFallbacks('rgb(var(--foo, 255 0 0))')).toBe('rgb( 255 0 0)')
expect(replaceCssVarsWithFallbacks('rgb(var(--foo, var(--bar)))')).toBe('rgb( var(--bar))')
let state: State = {
enabled: true,
designSystem: {
resolveThemeValue: (name) => map.get(name) ?? null,
} as DesignSystem,
}

expect(replaceCssVarsWithFallbacks(state, 'var(--foo, red)')).toBe(' red')
expect(replaceCssVarsWithFallbacks(state, 'var(--foo, )')).toBe(' ')

expect(replaceCssVarsWithFallbacks(state, 'rgb(var(--foo, 255 0 0))')).toBe('rgb( 255 0 0)')
expect(replaceCssVarsWithFallbacks(state, 'rgb(var(--foo, var(--bar)))')).toBe('rgb( var(--bar))')

expect(
replaceCssVarsWithFallbacks('rgb(var(var(--bar, var(--baz), var(--qux), var(--thing))))'),
replaceCssVarsWithFallbacks(
state,
'rgb(var(var(--bar, var(--baz), var(--qux), var(--thing))))',
),
).toBe('rgb(var(var(--bar, var(--baz), var(--qux), var(--thing))))')

expect(
replaceCssVarsWithFallbacks(
state,
'rgb(var(--one, var(--bar, var(--baz), var(--qux), var(--thing))))',
),
).toBe('rgb( var(--baz), var(--qux), var(--thing))')

expect(
replaceCssVarsWithFallbacks(
state,
'color-mix(in srgb, var(--color-primary, oklch(0 0 0 / 2.5)), var(--color-secondary, oklch(0 0 0 / 2.5)), 50%)',
),
).toBe('color-mix(in srgb, oklch(0 0 0 / 2.5), oklch(0 0 0 / 2.5), 50%)')

// Known theme keys are replaced with their values
expect(replaceCssVarsWithFallbacks(state, 'var(--known)')).toBe('blue')

// Values from the theme take precedence over fallbacks
expect(replaceCssVarsWithFallbacks(state, 'var(--known, red)')).toBe('blue')

// Unknown theme keys use a fallback if provided
expect(replaceCssVarsWithFallbacks(state, 'var(--unknown, red)')).toBe(' red')

// Unknown theme keys without fallbacks are not replaced
expect(replaceCssVarsWithFallbacks(state, 'var(--unknown)')).toBe('var(--unknown)')
})
49 changes: 45 additions & 4 deletions packages/tailwindcss-language-service/src/util/css-vars.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
export function replaceCssVarsWithFallbacks(str: string): string {
import type { State } from './state'

export function replaceCssVarsWithFallbacks(state: State, str: string): string {
return replaceCssVars(str, (name, fallback) => {
// Replace with the value from the design system first. The design system
// take precedences over other sources as that emulates the behavior of a
// browser where the fallback is only used if the variable is defined.
if (state.designSystem && name.startsWith('--')) {
let value = state.designSystem.resolveThemeValue?.(name) ?? null
if (value !== null) return value
}

if (fallback) {
return fallback
}

// Don't touch it since there's no suitable replacement
return null
})
}

type CssVarReplacer = (name: string, fallback: string | null) => string | null

function replaceCssVars(str: string, replace: CssVarReplacer): string {
for (let i = 0; i < str.length; ++i) {
if (!str.startsWith('var(', i)) continue

Expand All @@ -13,13 +36,31 @@ export function replaceCssVarsWithFallbacks(str: string): string {
} else if (str[j] === ',' && depth === 0 && fallbackStart === null) {
fallbackStart = j + 1
} else if (str[j] === ')' && depth === 0) {
let varName: string
let fallback: string | null

if (fallbackStart === null) {
i = j + 1
varName = str.slice(i + 4, j)
fallback = null
} else {
varName = str.slice(i + 4, fallbackStart - 1)
fallback = str.slice(fallbackStart, j)
}

let replacement = replace(varName, fallback)

if (replacement !== null) {
str = str.slice(0, i) + replacement + str.slice(j + 1)

// We don't want to skip past anything here because `replacement`
// might contain more var(…) calls in which case `i` will already
// be pointing at the right spot to start looking for them
break
}

let fallbackEnd = j
str = str.slice(0, i) + str.slice(fallbackStart, fallbackEnd) + str.slice(j + 1)
// It can't be replaced so we can avoid unncessary work by skipping over
// the entire var(…) call.
i = j + 1
break
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ export interface DesignSystem {
export interface DesignSystem {
compile(classes: string[]): postcss.Root[]
toCss(nodes: postcss.Root | postcss.Node[]): string

// Optional because it did not exist in earlier v4 alpha versions
resolveThemeValue?(path: string): string | undefined
}
1 change: 1 addition & 0 deletions packages/vscode-tailwindcss/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Support loading TypeScript configs and plugins in v4 projects ([#1076](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1076))
- Show colors for logical border properties ([#1075](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1075))
- Show all potential class conflicts in v4 projects ([#1077](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1077))
- Lookup variables in the CSS theme ([#1082](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1082))

## 0.12.12

Expand Down