|
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