Skip to content

Commit c122323

Browse files
committed
Use existing CSS package for calc(…) evaluation
1 parent 9e1e5ea commit c122323

File tree

2 files changed

+21
-46
lines changed

2 files changed

+21
-46
lines changed

packages/tailwindcss-language-service/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"test": "vitest"
1414
},
1515
"dependencies": {
16+
"@csstools/css-calc": "2.1.2",
1617
"@csstools/css-parser-algorithms": "3.0.4",
1718
"@csstools/css-tokenizer": "3.0.3",
1819
"@csstools/media-query-list-parser": "2.0.4",
Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,30 @@
1-
function parseLength(length: string): [number, string] | null {
2-
let regex = /^(-?\d*\.?\d+)([a-z%]*)$/i
3-
let match = length.match(regex)
4-
5-
if (!match) return null
6-
7-
let numberPart = parseFloat(match[1])
8-
if (isNaN(numberPart)) return null
9-
10-
return [numberPart, match[2]]
11-
}
12-
13-
function round(n: number, precision: number): number {
14-
return Math.round(n * Math.pow(10, precision)) / Math.pow(10, precision)
15-
}
1+
import { stringify, tokenize } from '@csstools/css-tokenizer'
2+
import { isFunctionNode, parseComponentValue } from '@csstools/css-parser-algorithms'
3+
import { calcFromComponentValues } from '@csstools/css-calc'
164

175
export function evaluateExpression(str: string): string | null {
18-
// We're only interested simple calc expressions of the form
19-
// A + B, A - B, A * B, A / B
6+
let tokens = tokenize({ css: `calc(${str})` })
207

21-
let parts = str.split(/\s+([+*/-])\s+/)
8+
let components = parseComponentValue(tokens, {})
9+
if (!components) return null
2210

23-
if (parts.length === 1) return null
24-
if (parts.length !== 3) return null
11+
let result = calcFromComponentValues([[components]], {
12+
// Ensure evaluation of random() is deterministic
13+
randomSeed: 1,
2514

26-
let a = parseLength(parts[0])
27-
let b = parseLength(parts[2])
15+
// Limit precision to keep values environment independent
16+
precision: 4,
17+
})
2818

29-
// Not parsable
30-
if (!a || !b) {
31-
return null
32-
}
33-
34-
// Addition and subtraction require the same units
35-
if ((parts[1] === '+' || parts[1] === '-') && a[1] !== b[1]) {
36-
return null
37-
}
38-
39-
// Multiplication and division require at least one unit to be empty
40-
if ((parts[1] === '*' || parts[1] === '/') && a[1] !== '' && b[1] !== '') {
41-
return null
42-
}
19+
// The result array is the same shape as the original so we're guaranteed to
20+
// have an element here
21+
let node = result[0][0]
4322

44-
switch (parts[1]) {
45-
case '+':
46-
return round(a[0] + b[0], 4).toString() + a[1]
47-
case '*':
48-
return round(a[0] * b[0], 4).toString() + a[1]
49-
case '-':
50-
return round(a[0] - b[0], 4).toString() + a[1]
51-
case '/':
52-
return round(a[0] / b[0], 4).toString() + a[1]
23+
// If we have a top-level `calc(…)` node then the evaluation did not resolve
24+
// to a single value and we consider it to be incomplete
25+
if (isFunctionNode(node)) {
26+
if (node.name[1] === 'calc(') return null
5327
}
5428

55-
return null
29+
return stringify(...node.tokens())
5630
}

0 commit comments

Comments
 (0)