Skip to content

Commit 8d2f0f7

Browse files
committed
wip
1 parent 70a16f8 commit 8d2f0f7

File tree

7 files changed

+320
-15
lines changed

7 files changed

+320
-15
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { State, TailwindCssSettings } from './state'
2+
3+
import type { Plugin } from 'postcss'
4+
import { inlineCalc } from './css-calc'
5+
import { getEquivalentColor } from './colorEquivalents'
6+
import { addPixelEquivalentsToValue } from './pixelEquivalents'
7+
import { Comment } from './comments'
8+
9+
export function wip(state: State, comments: Comment[], settings: TailwindCssSettings): Plugin {
10+
return {
11+
postcssPlugin: 'plugin',
12+
Declaration(decl) {
13+
let value = inlineCalc(state, decl.value)
14+
if (value === decl.value) return
15+
16+
let comment = ''
17+
18+
let color = getEquivalentColor(value)
19+
if (color !== value) {
20+
comment = `${value} = ${color}`
21+
} else {
22+
let pixels = addPixelEquivalentsToValue(value, settings.rootFontSize, false)
23+
if (pixels !== value) {
24+
comment = `${value} = ${pixels}`
25+
}
26+
}
27+
28+
comments.push({
29+
index: decl.source.end.offset,
30+
value: result,
31+
})
32+
},
33+
}
34+
}
35+
36+
export function addThemeValues(state: State, settings: TailwindCssSettings) {
37+
// Add fallbacks to variables with their theme values
38+
// Ideally these would just be commentss like
39+
// `var(--foo) /* 3rem = 48px */` or
40+
// `calc(var(--spacing) * 5) /* 1.25rem = 20px */`
41+
css = replaceCssVars(css, (name) => {
42+
if (!name.startsWith('--')) return null
43+
44+
let value = state.designSystem.resolveThemeValue?.(name) ?? null
45+
if (value === null) return null
46+
47+
let comment = ''
48+
49+
let color = getEquivalentColor(value)
50+
if (color !== value) {
51+
comment = ` /* ${value} = ${color} */`
52+
} else {
53+
let pixels = addPixelEquivalentsToValue(value, settings.rootFontSize, false)
54+
if (pixels !== value) {
55+
comment = ` /* ${value} = ${pixels} */`
56+
}
57+
}
58+
59+
return `var(${name})${comment}`
60+
})
61+
62+
return css
63+
}

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ import type { Comment } from './comments'
66

77
let allowedFunctions = ['rgb', 'rgba', 'hsl', 'hsla', 'lch', 'lab', 'oklch', 'oklab']
88

9+
export function getEquivalentColor(value: string): string {
10+
const color = getColorFromValue(value)
11+
12+
if (!color) return value
13+
if (typeof color === 'string') return value
14+
if (!inGamut('rgb')(color)) return value
15+
16+
return formatColor(color)
17+
}
18+
919
export function equivalentColorValues({ comments }: { comments: Comment[] }): Plugin {
1020
return {
1121
postcssPlugin: 'plugin',
@@ -32,12 +42,11 @@ export function equivalentColorValues({ comments }: { comments: Comment[] }): Pl
3242
return false
3343
}
3444

35-
const color = getColorFromValue(`${node.value}(${values.join(' ')})`)
36-
if (!inGamut('rgb')(color)) {
37-
return false
38-
}
45+
let color = `${node.value}(${values.join(' ')})`
46+
47+
let equivalent = getEquivalentColor(color)
3948

40-
if (!color || typeof color === 'string') {
49+
if (equivalent === color) {
4150
return false
4251
}
4352

@@ -46,7 +55,7 @@ export function equivalentColorValues({ comments }: { comments: Comment[] }): Pl
4655
decl.source.start.offset +
4756
`${decl.prop}${decl.raws.between}`.length +
4857
node.sourceEndIndex,
49-
value: formatColor(color),
58+
value: equivalent,
5059
})
5160

5261
return false
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { expect, test } from 'vitest'
2+
import { evaluateExpression, inlineCalc, replaceCssCalc } from './css-calc'
3+
import { replaceCssVars } from './css-vars'
4+
import { State } from './state'
5+
import { DesignSystem } from './v4'
6+
7+
test('Evaluating CSS calc expressions', () => {
8+
expect(replaceCssCalc('calc(1px + 1px)', evaluateExpression)).toBe('2px')
9+
expect(replaceCssCalc('calc(1px * 4)', evaluateExpression)).toBe('4px')
10+
expect(replaceCssCalc('calc(1px / 4)', evaluateExpression)).toBe('0.25px')
11+
expect(replaceCssCalc('calc(1rem + 1px)', evaluateExpression)).toBe('calc(1rem + 1px)')
12+
})
13+
14+
test('Inlicing calc expressions using the design system', () => {
15+
let map = new Map<string, string>([['--spacing', '0.25rem']])
16+
17+
let state: State = {
18+
enabled: true,
19+
designSystem: {
20+
resolveThemeValue: (name) => map.get(name) ?? null,
21+
} as DesignSystem,
22+
}
23+
24+
expect(inlineCalc(state, 'calc(var(--spacing) * 4)')).toBe('1rem')
25+
expect(inlineCalc(state, 'calc(var(--spacing) / 4)')).toBe('0.0625rem')
26+
})
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { replaceCssVars } from './css-vars'
2+
import { State } from './state'
3+
4+
interface Location {
5+
start: number
6+
end: number
7+
}
8+
type CssCalcReplacer = (name: string, location: Location) => string | null
9+
10+
export function inlineCalc(state: State, str: string): string {
11+
if (!state.designSystem) return str
12+
13+
return replaceCssCalc(str, (expr) => {
14+
expr = replaceCssVars(expr, (name) => {
15+
if (!name.startsWith('--')) return null
16+
17+
let value = state.designSystem.resolveThemeValue?.(name) ?? null
18+
if (value !== null) return value
19+
20+
return null
21+
})
22+
23+
return evaluateExpression(expr)
24+
})
25+
}
26+
27+
export function replaceCssCalc(str: string, replace: CssCalcReplacer): string {
28+
for (let i = 0; i < str.length; ++i) {
29+
if (!str.startsWith('calc(', i)) continue
30+
31+
let depth = 0
32+
33+
for (let j = i + 5; i < str.length; ++j) {
34+
if (str[j] === '(') {
35+
depth++
36+
} else if (str[j] === ')' && depth > 0) {
37+
depth--
38+
} else if (str[j] === ')' && depth === 0) {
39+
let varName = str.slice(i + 5, j)
40+
41+
let replacement = replace(varName, {
42+
start: i,
43+
end: j,
44+
})
45+
46+
if (replacement !== null) {
47+
str = str.slice(0, i) + replacement + str.slice(j + 1)
48+
}
49+
50+
// We don't want to skip past anything here because `replacement`
51+
// might contain more var(…) calls in which case `i` will already
52+
// be pointing at the right spot to start looking for them
53+
break
54+
}
55+
}
56+
}
57+
58+
return str
59+
}
60+
61+
function parseLength(length: string): [number, string] | null {
62+
let regex = /^(-?\d*\.?\d+)([a-z%]*)$/i
63+
let match = length.match(regex)
64+
65+
if (!match) return null
66+
67+
let numberPart = parseFloat(match[1])
68+
if (isNaN(numberPart)) return null
69+
70+
return [numberPart, match[2]]
71+
}
72+
73+
export function evaluateExpression(str: string): string | null {
74+
// We're only interested simple calc expressions of the form
75+
// A + B, A - B, A * B, A / B
76+
77+
let parts = str.split(/\s*([+*/-])\s*/)
78+
if (parts.length === 1) return null
79+
if (parts.length !== 3) return null
80+
81+
let a = parseLength(parts[0])
82+
let b = parseLength(parts[2])
83+
84+
// Not parsable
85+
if (!a || !b) {
86+
return null
87+
}
88+
89+
// Addition and subtraction require the same units
90+
if ((parts[1] === '+' || parts[1] === '-') && a[1] !== b[1]) {
91+
return null
92+
}
93+
94+
// Multiplication and division require at least one unit to be empty
95+
if ((parts[1] === '*' || parts[1] === '/') && a[1] !== '' && b[1] !== '') {
96+
return null
97+
}
98+
99+
switch (parts[1]) {
100+
case '+':
101+
return (a[0] + b[0]).toString() + a[1]
102+
case '*':
103+
return (a[0] * b[0]).toString() + a[1]
104+
case '-':
105+
return (a[0] - b[0]).toString() + a[1]
106+
case '/':
107+
return (a[0] / b[0]).toString() + a[1]
108+
}
109+
110+
return null
111+
}

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

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TailwindCssSettings } from './state'
1+
import type { State, TailwindCssSettings } from './state'
22
import { equivalentPixelValues } from './pixelEquivalents'
33
import { equivalentColorValues } from './colorEquivalents'
44
import postcss, { type AcceptedPlugin } from 'postcss'
@@ -28,3 +28,60 @@ export function addEquivalents(css: string, settings: TailwindCssSettings): stri
2828

2929
return applyComments(css, comments)
3030
}
31+
32+
import type { Plugin } from 'postcss'
33+
import { inlineCalc } from './css-calc'
34+
35+
export function wip(state: State, comments: Comment[], settings: TailwindCssSettings): Plugin {
36+
return {
37+
postcssPlugin: 'plugin',
38+
Declaration(decl) {
39+
let result = inlineCalc(state, decl.value)
40+
if (result === decl.value) return
41+
42+
comments.push({
43+
index: decl.source.end.offset,
44+
value: result,
45+
})
46+
},
47+
}
48+
}
49+
50+
export function addThemeValues(state: State, settings: TailwindCssSettings) {
51+
root.walkDecls((decl) => {
52+
let result = inlineCalc(state, decl.value)
53+
if (result === decl.value) return
54+
55+
comments.push({
56+
index: decl.source.end.offset,
57+
value: result,
58+
})
59+
})
60+
61+
// Add fallbacks to variables with their theme values
62+
// Ideally these would just be commentss like
63+
// `var(--foo) /* 3rem = 48px */` or
64+
// `calc(var(--spacing) * 5) /* 1.25rem = 20px */`
65+
css = replaceCssVars(css, (name) => {
66+
if (!name.startsWith('--')) return null
67+
68+
let value = state.designSystem.resolveThemeValue?.(name) ?? null
69+
if (value === null) return null
70+
71+
let comment = ''
72+
73+
let color = getEquivalentColor(value)
74+
if (color !== value) {
75+
comment = ` /* ${value} = ${color} */`
76+
} else {
77+
let pixels = addPixelEquivalentsToValue(value, settings.rootFontSize, false)
78+
if (pixels !== value) {
79+
comment = ` /* ${value} = ${pixels} */`
80+
}
81+
}
82+
83+
return `var(${name})${comment}`
84+
})
85+
86+
return css
87+
}

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

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import type { State } from './state'
1+
import type { State, TailwindCssSettings } from './state'
22
import type { Container, Document, Root, Rule, Node, AtRule } from 'postcss'
33
import { addPixelEquivalentsToValue } from './pixelEquivalents'
44
import { addEquivalents } from './equivalents'
55
import { replaceCssVars } from './css-vars'
6+
import { addColorEquivalentToValue, getEquivalentColor } from './colorEquivalents'
7+
import { inlineCalc, replaceCssCalc } from './css-calc'
8+
import { applyComments, Comment } from './comments'
69

710
export function bigSign(bigIntValue) {
811
// @ts-ignore
@@ -35,8 +38,20 @@ export function generateRules(
3538
}
3639
}
3740

38-
export function addThemeValues(css: string, state: State) {
39-
if (!state.designSystem) return css
41+
export function addThemeValues(root: Root, state: State, settings: TailwindCssSettings) {
42+
if (!state.designSystem) return root
43+
44+
let comments: Comment[] = []
45+
46+
root.walkDecls((decl) => {
47+
let result = inlineCalc(state, decl.value)
48+
if (result === decl.value) return
49+
50+
comments.push({
51+
index: decl.source.end.offset,
52+
value: result,
53+
})
54+
})
4055

4156
// Add fallbacks to variables with their theme values
4257
// Ideally these would just be commentss like
@@ -48,7 +63,19 @@ export function addThemeValues(css: string, state: State) {
4863
let value = state.designSystem.resolveThemeValue?.(name) ?? null
4964
if (value === null) return null
5065

51-
return `var(${name}, ${value})`
66+
let comment = ''
67+
68+
let color = getEquivalentColor(value)
69+
if (color !== value) {
70+
comment = ` /* ${value} = ${color} */`
71+
} else {
72+
let pixels = addPixelEquivalentsToValue(value, settings.rootFontSize, false)
73+
if (pixels !== value) {
74+
comment = ` /* ${value} = ${pixels} */`
75+
}
76+
}
77+
78+
return `var(${name})${comment}`
5279
})
5380

5481
return css
@@ -62,9 +89,10 @@ export async function stringifyRoot(state: State, root: Root, uri?: string): Pro
6289
node.remove()
6390
})
6491

92+
addThemeValues(clone, state, settings.tailwindCSS)
93+
6594
let css = clone.toString()
6695

67-
css = addThemeValues(css, state)
6896
css = addEquivalents(css, settings.tailwindCSS)
6997

7098
let identSize = state.v4 ? 2 : 4

0 commit comments

Comments
 (0)