|
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' |
16 | 4 |
|
17 | 5 | 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})` }) |
20 | 7 |
|
21 |
| - let parts = str.split(/\s+([+*/-])\s+/) |
| 8 | + let components = parseComponentValue(tokens, {}) |
| 9 | + if (!components) return null |
22 | 10 |
|
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, |
25 | 14 |
|
26 |
| - let a = parseLength(parts[0]) |
27 |
| - let b = parseLength(parts[2]) |
| 15 | + // Limit precision to keep values environment independent |
| 16 | + precision: 4, |
| 17 | + }) |
28 | 18 |
|
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] |
43 | 22 |
|
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 |
53 | 27 | }
|
54 | 28 |
|
55 |
| - return null |
| 29 | + return stringify(...node.tokens()) |
56 | 30 | }
|
0 commit comments