Skip to content

Commit

Permalink
Lookup variables in the CSS theme (#1082)
Browse files Browse the repository at this point in the history
  • Loading branch information
thecrypticace authored Nov 8, 2024
1 parent 712b5ff commit 3014df5
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 16 deletions.
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

0 comments on commit 3014df5

Please sign in to comment.