postcss-calc 10.1.0 → 11.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -1
- package/package.json +19 -13
- package/src/index.js +122 -27
- package/src/lib/convertUnits.js +105 -0
- package/src/lib/node.js +189 -0
- package/src/lib/parser.js +314 -0
- package/src/lib/serialize.js +226 -0
- package/src/lib/simplify/abs.js +21 -0
- package/src/lib/simplify/atan2.js +25 -0
- package/src/lib/simplify/bucket.js +48 -0
- package/src/lib/simplify/call.js +59 -0
- package/src/lib/simplify/cancel.js +39 -0
- package/src/lib/simplify/clamp.js +26 -0
- package/src/lib/simplify/exp.js +18 -0
- package/src/lib/simplify/fold.js +63 -0
- package/src/lib/simplify/hypot.js +22 -0
- package/src/lib/simplify/inverse-trig.js +29 -0
- package/src/lib/simplify/log.js +25 -0
- package/src/lib/simplify/min-max.js +23 -0
- package/src/lib/simplify/mod-rem.js +47 -0
- package/src/lib/simplify/pow.js +20 -0
- package/src/lib/simplify/product.js +133 -0
- package/src/lib/simplify/round.js +113 -0
- package/src/lib/simplify/sign.js +22 -0
- package/src/lib/simplify/sqrt.js +18 -0
- package/src/lib/simplify/sum.js +83 -0
- package/src/lib/simplify/trig.js +40 -0
- package/src/lib/simplify.js +40 -0
- package/src/lib/tokenizer.js +115 -0
- package/types/index.d.ts +15 -17
- package/types/lib/convertUnits.d.ts +16 -0
- package/types/lib/node.d.ts +90 -0
- package/types/lib/parser.d.ts +39 -0
- package/types/lib/serialize.d.ts +20 -0
- package/types/lib/simplify/abs.d.ts +7 -0
- package/types/lib/simplify/atan2.d.ts +7 -0
- package/types/lib/simplify/bucket.d.ts +19 -0
- package/types/lib/simplify/call.d.ts +12 -0
- package/types/lib/simplify/cancel.d.ts +19 -0
- package/types/lib/simplify/clamp.d.ts +7 -0
- package/types/lib/simplify/exp.d.ts +7 -0
- package/types/lib/simplify/fold.d.ts +12 -0
- package/types/lib/simplify/hypot.d.ts +7 -0
- package/types/lib/simplify/inverse-trig.d.ts +7 -0
- package/types/lib/simplify/log.d.ts +7 -0
- package/types/lib/simplify/min-max.d.ts +8 -0
- package/types/lib/simplify/mod-rem.d.ts +8 -0
- package/types/lib/simplify/pow.d.ts +7 -0
- package/types/lib/simplify/product.d.ts +16 -0
- package/types/lib/simplify/round.d.ts +8 -0
- package/types/lib/simplify/sign.d.ts +7 -0
- package/types/lib/simplify/sqrt.d.ts +7 -0
- package/types/lib/simplify/sum.d.ts +18 -0
- package/types/lib/simplify/trig.d.ts +7 -0
- package/types/lib/simplify.d.ts +18 -0
- package/types/lib/tokenizer.d.ts +19 -0
- package/types/lib/type.d.ts +16 -0
- package/src/lib/convertUnit.js +0 -160
- package/src/lib/reducer.js +0 -390
- package/src/lib/stringifier.js +0 -98
- package/src/lib/transform.js +0 -109
- package/src/parser.d.ts +0 -79
- package/src/parser.js +0 -4200
- package/types/lib/convertUnit.d.ts +0 -8
- package/types/lib/reducer.d.ts +0 -14
- package/types/lib/stringifier.d.ts +0 -5
- package/types/lib/transform.d.ts +0 -6
package/README.md
CHANGED
|
@@ -120,6 +120,61 @@ div[data-size="calc(3*3)"] {
|
|
|
120
120
|
}
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
#### `onParseError`
|
|
124
|
+
|
|
125
|
+
Callback invoked when a `calc()` body fails to parse or simplify. Matches
|
|
126
|
+
[`@csstools/css-calc`][csstools-css-calc]'s shape:
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
calc({
|
|
130
|
+
onParseError: (err, input) => {
|
|
131
|
+
throw err; // or log, route to a different channel, etc.
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
When omitted, errors are reported via PostCSS `result.warn()` so the
|
|
137
|
+
plugin never throws at the postcss level.
|
|
138
|
+
|
|
139
|
+
### Behavior differences from the legacy parser
|
|
140
|
+
|
|
141
|
+
The legacy [jison][jison]-generated parser was replaced by a hand-written
|
|
142
|
+
Pratt parser whose simplifier follows [CSS Values 4][css-values-4]. Most
|
|
143
|
+
inputs reduce to identical output; the differences are spec-aligned or
|
|
144
|
+
canonical-form decisions:
|
|
145
|
+
|
|
146
|
+
- **Strict whitespace (§10.1).** `calc(2px+3px)` is invalid CSS (binary
|
|
147
|
+
`+` / `-` require surrounding whitespace) and is preserved with a
|
|
148
|
+
warning instead of reduced.
|
|
149
|
+
- **Canonical operand order.** Commutative operands serialize
|
|
150
|
+
numeric-first, matching [`@csstools/css-calc`][csstools-css-calc]:
|
|
151
|
+
`calc(var(--foo) + 10px)` → `calc(10px + var(--foo))`.
|
|
152
|
+
- **Zero buckets are kept.** `calc(100px - (100px - 100%))` →
|
|
153
|
+
`calc(0px + 100%)`, not `100%` — [WPT calc-serialization-002][wpt-calc-serialization]
|
|
154
|
+
requires the zero term because it carries the length-percentage type.
|
|
155
|
+
- **Constant folding.** `calc(43 + pi)` now folds to `46.14159` (§10.7.1).
|
|
156
|
+
Previously `pi` / `e` stayed symbolic.
|
|
157
|
+
- **Reciprocal conversion.** `calc(var(--x) / 2)` becomes
|
|
158
|
+
`calc(var(--x) * 0.5)`. The two are mathematically equivalent;
|
|
159
|
+
previously the division shape was kept.
|
|
160
|
+
- **Distributive multiplication.** `calc(0.5 * (100vw - 10px))` becomes
|
|
161
|
+
`calc(50vw - 5px)`.
|
|
162
|
+
- **Unit case normalization.** `2PX` becomes `2px` (CSS units are case-
|
|
163
|
+
insensitive; lowercase is conventional).
|
|
164
|
+
- **Calc unwrap (§10.6).** `calc(var(--foo))` becomes `var(--foo)` — a
|
|
165
|
+
`calc()` containing a single value is replaced by that value.
|
|
166
|
+
- **Spec-style spaced operators.** `2px*var(--x)` is serialized as
|
|
167
|
+
`2px * var(--x)`. The tokenizer is unaffected; only output spacing
|
|
168
|
+
differs.
|
|
169
|
+
- **Division by zero / by a unit.** `calc(500px/0)` reduces to
|
|
170
|
+
`calc(infinity * 1px)` (§10.13) instead of throwing. Use `onParseError`
|
|
171
|
+
if you want validation behavior.
|
|
172
|
+
|
|
173
|
+
[css-values-4]: https://www.w3.org/TR/css-values-4/
|
|
174
|
+
[csstools-css-calc]: https://www.npmjs.com/package/@csstools/css-calc
|
|
175
|
+
[wpt-calc-serialization]: https://github.com/web-platform-tests/wpt/blob/master/css/css-values/calc-serialization-002.html
|
|
176
|
+
[jison]: https://github.com/zaach/jison
|
|
177
|
+
|
|
123
178
|
---
|
|
124
179
|
|
|
125
180
|
## Related PostCSS plugins
|
|
@@ -149,5 +204,5 @@ npm test
|
|
|
149
204
|
[PostCSS]: https://github.com/postcss
|
|
150
205
|
[PostCSS Calc]: https://github.com/postcss/postcss-calc
|
|
151
206
|
[PostCSS Custom Properties]: https://github.com/postcss/postcss-custom-properties
|
|
152
|
-
[tests]:
|
|
207
|
+
[tests]: test/index.js
|
|
153
208
|
[W3C calc() implementation]: https://www.w3.org/TR/css3-values/#calc-notation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postcss-calc",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0-rc.0",
|
|
4
4
|
"description": "PostCSS plugin to reduce calc()",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"css",
|
|
@@ -24,27 +24,33 @@
|
|
|
24
24
|
"author": "Andy Jansson",
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"engines": {
|
|
27
|
-
"node": "^
|
|
27
|
+
"node": "^22.12 || ^24.11 || >=26.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@
|
|
31
|
-
"eslint": "^
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
30
|
+
"@csstools/css-calc": "^3.2.1",
|
|
31
|
+
"@eslint/js": "^10.0.1",
|
|
32
|
+
"@rmenke/css-tokenizer-tests": "^1.2.0",
|
|
33
|
+
"@stryker-mutator/core": "^9.6.1",
|
|
34
|
+
"@stryker-mutator/typescript-checker": "^9.6.1",
|
|
35
|
+
"@types/node": "^26.1.1",
|
|
36
|
+
"eslint": "^10.7.0",
|
|
37
|
+
"eslint-config-prettier": "^10.1.8",
|
|
38
|
+
"eslint-plugin-sonarjs": "^4.2.0",
|
|
39
|
+
"fast-check": "^4.9.0",
|
|
40
|
+
"postcss": "^8.5.19",
|
|
41
|
+
"prettier": "^3.9.5",
|
|
42
|
+
"typescript": "~6.0.3"
|
|
37
43
|
},
|
|
38
44
|
"dependencies": {
|
|
39
|
-
"
|
|
45
|
+
"@csstools/css-tokenizer": "^4.0.0",
|
|
40
46
|
"postcss-value-parser": "^4.2.0"
|
|
41
47
|
},
|
|
42
48
|
"peerDependencies": {
|
|
43
|
-
"postcss": "^8.
|
|
49
|
+
"postcss": "^8.5.19"
|
|
44
50
|
},
|
|
45
51
|
"scripts": {
|
|
46
|
-
"build": "jison ./parser.jison -o src/parser.js",
|
|
47
52
|
"lint": "eslint . && tsc",
|
|
48
|
-
"test": "node --test"
|
|
53
|
+
"test": "node --test",
|
|
54
|
+
"test:mutation": "stryker run"
|
|
49
55
|
}
|
|
50
56
|
}
|
package/src/index.js
CHANGED
|
@@ -1,45 +1,140 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
// PostCSS adapter. Walks declaration values (and optionally @rule params
|
|
4
|
+
// and selectors), feeds calc() bodies through tokenize → parse → simplify
|
|
5
|
+
// → serialize, and writes the result back.
|
|
6
|
+
|
|
7
|
+
const valueParser = require('postcss-value-parser');
|
|
8
|
+
|
|
9
|
+
const { tokenize } = require('./lib/tokenizer.js');
|
|
10
|
+
const { parse } = require('./lib/parser.js');
|
|
11
|
+
const { simplify } = require('./lib/simplify.js');
|
|
12
|
+
const { serialize } = require('./lib/serialize.js');
|
|
13
|
+
|
|
14
|
+
const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
|
|
15
|
+
|
|
16
|
+
// Bare math-function calls (no calc() wrapper) — fed to the same pipeline.
|
|
17
|
+
// Mirrors the dispatch in lib/simplify/call.js.
|
|
18
|
+
const MATH_FUNCTIONS = new Set([
|
|
19
|
+
'min', 'max', 'clamp',
|
|
20
|
+
'abs', 'sign',
|
|
21
|
+
'mod', 'rem', 'round',
|
|
22
|
+
'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2',
|
|
23
|
+
'pow', 'sqrt', 'hypot', 'log', 'exp',
|
|
24
|
+
]);
|
|
3
25
|
|
|
4
26
|
/**
|
|
5
|
-
* @typedef {
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
27
|
+
* @typedef {object} PluginOptions
|
|
28
|
+
* @property {number | false} [precision]
|
|
29
|
+
* @property {boolean} [preserve]
|
|
30
|
+
* @property {boolean} [warnWhenCannotResolve]
|
|
31
|
+
* @property {boolean} [mediaQueries]
|
|
32
|
+
* @property {boolean} [selectors]
|
|
33
|
+
* @property {(error: Error, input: string) => void} [onParseError] Invoked when parse/simplify throws. Replaces the default `result.warn`.
|
|
10
34
|
*/
|
|
35
|
+
|
|
36
|
+
/** @typedef {Required<Omit<PluginOptions, 'onParseError'>> & Pick<PluginOptions, 'onParseError'>} ResolvedOptions */
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} value
|
|
40
|
+
* @param {ResolvedOptions} options
|
|
41
|
+
* @param {import('postcss').Result} result
|
|
42
|
+
* @param {import('postcss').ChildNode} item
|
|
43
|
+
* @return {string}
|
|
44
|
+
*/
|
|
45
|
+
function transformValue(value, options, result, item) {
|
|
46
|
+
return valueParser(value)
|
|
47
|
+
.walk((node) => {
|
|
48
|
+
if (node.type !== 'function') {return;}
|
|
49
|
+
const isCalc = MATCH_CALC.test(node.value);
|
|
50
|
+
const isMath = !isCalc && MATH_FUNCTIONS.has(node.value.toLowerCase());
|
|
51
|
+
if (!isCalc && !isMath) {return;}
|
|
52
|
+
|
|
53
|
+
// calc(): feed the body. Bare math: feed the whole call.
|
|
54
|
+
const inner = valueParser.stringify(node.nodes);
|
|
55
|
+
const contents = isCalc ? inner : `${node.value}(${inner})`;
|
|
56
|
+
try {
|
|
57
|
+
const simplified = simplify(parse(tokenize(contents)));
|
|
58
|
+
const str = serialize(simplified, {
|
|
59
|
+
precision: options.precision,
|
|
60
|
+
calcName: isCalc ? node.value : 'calc', // preserve vendor prefix on calc()
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (options.warnWhenCannotResolve && str.startsWith(`${node.value}(`)) {
|
|
64
|
+
result.warn('Could not reduce expression: ' + value, {
|
|
65
|
+
plugin: 'postcss-calc',
|
|
66
|
+
node: item,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Re-tag as `word` so value-parser emits `str` verbatim instead of
|
|
71
|
+
// re-wrapping it as `name(...)`. Cast widens the `'function'` literal.
|
|
72
|
+
/** @type {{type: string}} */ (node).type = 'word';
|
|
73
|
+
node.value = str;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const err = error instanceof Error ? error : new Error('Error');
|
|
76
|
+
if (options.onParseError) {
|
|
77
|
+
options.onParseError(err, contents);
|
|
78
|
+
} else {
|
|
79
|
+
result.warn(err.message, { node: item });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
})
|
|
84
|
+
.toString();
|
|
85
|
+
}
|
|
86
|
+
|
|
11
87
|
/**
|
|
12
|
-
* @type {import('postcss').PluginCreator<
|
|
13
|
-
* @param {
|
|
88
|
+
* @type {import('postcss').PluginCreator<PluginOptions>}
|
|
89
|
+
* @param {PluginOptions} [opts]
|
|
14
90
|
* @return {import('postcss').Plugin}
|
|
15
91
|
*/
|
|
16
92
|
function pluginCreator(opts) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
);
|
|
93
|
+
/** @type {ResolvedOptions} */
|
|
94
|
+
const options = {
|
|
95
|
+
precision: 5,
|
|
96
|
+
preserve: false,
|
|
97
|
+
warnWhenCannotResolve: false,
|
|
98
|
+
mediaQueries: false,
|
|
99
|
+
selectors: false,
|
|
100
|
+
...opts,
|
|
101
|
+
};
|
|
27
102
|
|
|
28
103
|
return {
|
|
29
104
|
postcssPlugin: 'postcss-calc',
|
|
30
105
|
OnceExit(css, { result }) {
|
|
31
106
|
css.walk((node) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
107
|
+
if (node.type === 'decl') {
|
|
108
|
+
const next = transformValue(node.value, options, result, node);
|
|
109
|
+
if (options.preserve && node.value !== next && node.parent) {
|
|
110
|
+
const clone = node.clone();
|
|
111
|
+
clone.value = next;
|
|
112
|
+
node.parent.insertBefore(node, clone);
|
|
113
|
+
} else {
|
|
114
|
+
node.value = next;
|
|
115
|
+
}
|
|
35
116
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
117
|
+
if (node.type === 'atrule' && options.mediaQueries) {
|
|
118
|
+
const next = transformValue(node.params, options, result, node);
|
|
119
|
+
if (options.preserve && node.params !== next && node.parent) {
|
|
120
|
+
const clone = node.clone();
|
|
121
|
+
clone.params = next;
|
|
122
|
+
node.parent.insertBefore(node, clone);
|
|
123
|
+
} else {
|
|
124
|
+
node.params = next;
|
|
125
|
+
}
|
|
39
126
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
127
|
+
if (node.type === 'rule' && options.selectors) {
|
|
128
|
+
// Reduces `:nth-child(calc(...))` via the function walk. calc() in a
|
|
129
|
+
// quoted attribute value is a literal match, so it's left untouched.
|
|
130
|
+
const next = transformValue(node.selector, options, result, node);
|
|
131
|
+
if (options.preserve && node.selector !== next && node.parent) {
|
|
132
|
+
const clone = node.clone();
|
|
133
|
+
clone.selector = next;
|
|
134
|
+
node.parent.insertBefore(node, clone);
|
|
135
|
+
} else {
|
|
136
|
+
node.selector = next;
|
|
137
|
+
}
|
|
43
138
|
}
|
|
44
139
|
});
|
|
45
140
|
},
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Spec: https://www.w3.org/TR/css-values-4/#calc-type-checking
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {'length' | 'angle' | 'time' | 'frequency' | 'resolution' | 'flex' | 'percentage'} BaseType
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** @type {Record<string, BaseType>} */
|
|
10
|
+
const UNIT_TO_BASE = {
|
|
11
|
+
px: 'length', cm: 'length', mm: 'length', q: 'length',
|
|
12
|
+
in: 'length', pt: 'length', pc: 'length',
|
|
13
|
+
em: 'length', ex: 'length', ch: 'length', rem: 'length',
|
|
14
|
+
lh: 'length', rlh: 'length', ic: 'length', cap: 'length',
|
|
15
|
+
vw: 'length', vh: 'length', vmin: 'length', vmax: 'length',
|
|
16
|
+
vb: 'length', vi: 'length',
|
|
17
|
+
svw: 'length', svh: 'length', svmin: 'length', svmax: 'length',
|
|
18
|
+
svb: 'length', svi: 'length',
|
|
19
|
+
lvw: 'length', lvh: 'length', lvmin: 'length', lvmax: 'length',
|
|
20
|
+
lvb: 'length', lvi: 'length',
|
|
21
|
+
dvw: 'length', dvh: 'length', dvmin: 'length', dvmax: 'length',
|
|
22
|
+
dvb: 'length', dvi: 'length',
|
|
23
|
+
cqw: 'length', cqh: 'length', cqi: 'length', cqb: 'length',
|
|
24
|
+
cqmin: 'length', cqmax: 'length',
|
|
25
|
+
|
|
26
|
+
deg: 'angle', grad: 'angle', rad: 'angle', turn: 'angle',
|
|
27
|
+
|
|
28
|
+
s: 'time', ms: 'time',
|
|
29
|
+
|
|
30
|
+
hz: 'frequency', khz: 'frequency',
|
|
31
|
+
|
|
32
|
+
dpi: 'resolution', dpcm: 'resolution', dppx: 'resolution', x: 'resolution',
|
|
33
|
+
|
|
34
|
+
fr: 'flex',
|
|
35
|
+
|
|
36
|
+
'%': 'percentage',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} unit
|
|
41
|
+
* @return {BaseType | null}
|
|
42
|
+
*/
|
|
43
|
+
function baseOf(unit) {
|
|
44
|
+
return UNIT_TO_BASE[unit.toLowerCase()] ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Conversion factors to each family's canonical unit. Units NOT listed
|
|
48
|
+
// (em, rem, vw, cqw, fr, % …) share a base type with something convertible
|
|
49
|
+
// but can't resolve statically — the simplifier preserves them as separate
|
|
50
|
+
// summands rather than merging.
|
|
51
|
+
/** @type {Record<string, number>} */
|
|
52
|
+
const TO_CANONICAL = {
|
|
53
|
+
px: 1,
|
|
54
|
+
cm: 96 / 2.54,
|
|
55
|
+
mm: 96 / 25.4,
|
|
56
|
+
q: 96 / 101.6,
|
|
57
|
+
in: 96,
|
|
58
|
+
pt: 96 / 72,
|
|
59
|
+
pc: 16,
|
|
60
|
+
deg: 1,
|
|
61
|
+
grad: 0.9,
|
|
62
|
+
rad: 180 / Math.PI,
|
|
63
|
+
turn: 360,
|
|
64
|
+
s: 1,
|
|
65
|
+
ms: 0.001,
|
|
66
|
+
hz: 1,
|
|
67
|
+
khz: 1000,
|
|
68
|
+
dppx: 1,
|
|
69
|
+
dpi: 1 / 96,
|
|
70
|
+
dpcm: 2.54 / 96,
|
|
71
|
+
x: 1,
|
|
72
|
+
// flex / percentage: identity — only combinable with the same unit.
|
|
73
|
+
fr: 1,
|
|
74
|
+
'%': 1,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Convert a value within a single conversion family. Returns null when
|
|
79
|
+
* either unit is missing from the table (em/rem/vw need runtime context)
|
|
80
|
+
* or when the units belong to different base types.
|
|
81
|
+
* @param {number} value
|
|
82
|
+
* @param {string} from
|
|
83
|
+
* @param {string} to
|
|
84
|
+
* @return {number | null}
|
|
85
|
+
*/
|
|
86
|
+
function convert(value, from, to) {
|
|
87
|
+
const fromKey = from.toLowerCase();
|
|
88
|
+
const toKey = to.toLowerCase();
|
|
89
|
+
if (fromKey === toKey) {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
const f = TO_CANONICAL[fromKey];
|
|
93
|
+
const t = TO_CANONICAL[toKey];
|
|
94
|
+
if (f === undefined || t === undefined) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
// Cross-family guard: `px` and `s` both have entry 1, so without this
|
|
98
|
+
// `convert(1, 'px', 's')` would silently return 1.
|
|
99
|
+
if (UNIT_TO_BASE[fromKey] !== UNIT_TO_BASE[toKey]) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return (value * f) / t;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { baseOf, convert };
|
package/src/lib/node.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Canonical AST. N-ary Sum and Product with signed numeric leaves.
|
|
4
|
+
// Invariants enforced by the constructors below:
|
|
5
|
+
//
|
|
6
|
+
// - Num/Dim values may be any finite number (negatives allowed); a `-5`
|
|
7
|
+
// is `Num(-5)`, never `Sum([{sign:-1, Num(5)}])`. One form per value.
|
|
8
|
+
// - In a SumTerm with Num/Dim node, sign is always +1; the sign slot is
|
|
9
|
+
// reserved for opaque nodes (Ident, Call, Product, multi-term Sum).
|
|
10
|
+
// - No Sum directly contains another Sum (flattened on construction).
|
|
11
|
+
// - No Product directly contains another Product (flattened).
|
|
12
|
+
// - A Sum/Product with one positive element collapses to that element.
|
|
13
|
+
// - A Sum/Product with no elements collapses to Num(0) / Num(1).
|
|
14
|
+
// - Zero-valued Nums are dropped from sums (they contribute nothing).
|
|
15
|
+
// Zero-valued Dims are kept — the unit carries type info.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {{type: 'Num', value: number}} Num
|
|
19
|
+
* @typedef {{type: 'Dim', value: number, unit: string}} Dim
|
|
20
|
+
* @typedef {{type: 'Ident', name: string}} Ident
|
|
21
|
+
* @typedef {{type: 'Call', name: string, args: Node[]}} Call
|
|
22
|
+
* @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
|
|
23
|
+
* @typedef {{type: 'Sum', terms: SumTerm[]}} Sum
|
|
24
|
+
* @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
|
|
25
|
+
* @typedef {{type: 'Product', factors: ProductFactor[]}} Product
|
|
26
|
+
* @typedef {Num | Dim | Ident | Call | Sum | Product} Node
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {number} value
|
|
31
|
+
* @return {Num}
|
|
32
|
+
*/
|
|
33
|
+
function num(value) {
|
|
34
|
+
return { type: 'Num', value };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {number} value
|
|
39
|
+
* @param {string} unit
|
|
40
|
+
* @return {Dim}
|
|
41
|
+
*/
|
|
42
|
+
function dim(value, unit) {
|
|
43
|
+
return { type: 'Dim', value, unit };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} name
|
|
48
|
+
* @return {Ident}
|
|
49
|
+
*/
|
|
50
|
+
function ident(name) {
|
|
51
|
+
return { type: 'Ident', name };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} name
|
|
56
|
+
* @param {Node[]} args
|
|
57
|
+
* @return {Call}
|
|
58
|
+
*/
|
|
59
|
+
function call(name, args) {
|
|
60
|
+
return { type: 'Call', name, args };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {SumTerm[]} rawTerms
|
|
65
|
+
* @return {Node}
|
|
66
|
+
*/
|
|
67
|
+
function mkSum(rawTerms) {
|
|
68
|
+
/** @type {SumTerm[]} */
|
|
69
|
+
const flat = [];
|
|
70
|
+
for (const t of rawTerms) {
|
|
71
|
+
pushSumTerm(flat, t);
|
|
72
|
+
}
|
|
73
|
+
if (flat.length === 0) {
|
|
74
|
+
return { type: 'Num', value: 0 };
|
|
75
|
+
}
|
|
76
|
+
if (flat.length === 1 && flat[0].sign === 1) {
|
|
77
|
+
return flat[0].node;
|
|
78
|
+
}
|
|
79
|
+
return { type: 'Sum', terms: flat };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {SumTerm[]} out
|
|
84
|
+
* @param {SumTerm} term
|
|
85
|
+
* @return {void}
|
|
86
|
+
*/
|
|
87
|
+
function pushSumTerm(out, term) {
|
|
88
|
+
let { sign, node } = term;
|
|
89
|
+
|
|
90
|
+
if (node.type === 'Sum') {
|
|
91
|
+
for (const inner of node.terms) {
|
|
92
|
+
pushSumTerm(out, {
|
|
93
|
+
sign: /** @type {1 | -1} */ (sign * inner.sign),
|
|
94
|
+
node: inner.node,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// sign=-1 around a Num/Dim leaf collapses into the value's sign — the
|
|
101
|
+
// canonical-form rule downstream code relies on.
|
|
102
|
+
if (sign === -1) {
|
|
103
|
+
if (node.type === 'Num') {
|
|
104
|
+
node = { type: 'Num', value: -node.value };
|
|
105
|
+
sign = 1;
|
|
106
|
+
} else if (node.type === 'Dim') {
|
|
107
|
+
node = { type: 'Dim', value: -node.value, unit: node.unit };
|
|
108
|
+
sign = 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Drop zero-valued Nums. Dims with value 0 stay — the unit carries type.
|
|
113
|
+
if (node.type === 'Num' && node.value === 0) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
out.push({ sign, node });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {ProductFactor[]} rawFactors
|
|
122
|
+
* @return {Node}
|
|
123
|
+
*/
|
|
124
|
+
function mkProduct(rawFactors) {
|
|
125
|
+
/** @type {ProductFactor[]} */
|
|
126
|
+
const flat = [];
|
|
127
|
+
for (const f of rawFactors) {
|
|
128
|
+
pushProductFactor(flat, f);
|
|
129
|
+
}
|
|
130
|
+
if (flat.length === 0) {
|
|
131
|
+
return { type: 'Num', value: 1 };
|
|
132
|
+
}
|
|
133
|
+
if (flat.length === 1 && flat[0].exponent === 1) {
|
|
134
|
+
return flat[0].node;
|
|
135
|
+
}
|
|
136
|
+
return { type: 'Product', factors: flat };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {ProductFactor[]} out
|
|
141
|
+
* @param {ProductFactor} f
|
|
142
|
+
* @return {void}
|
|
143
|
+
*/
|
|
144
|
+
function pushProductFactor(out, f) {
|
|
145
|
+
const n = f.node;
|
|
146
|
+
if (n.type === 'Product') {
|
|
147
|
+
for (const inner of n.factors) {
|
|
148
|
+
out.push({
|
|
149
|
+
exponent: /** @type {1 | -1} */ (f.exponent * inner.exponent),
|
|
150
|
+
node: inner.node,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Factor of 1 contributes nothing regardless of exponent (1/1 = 1).
|
|
156
|
+
if (n.type === 'Num' && n.value === 1) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
out.push(f);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Negate any node, preserving canonical form.
|
|
164
|
+
* @param {Node} node
|
|
165
|
+
* @return {Node}
|
|
166
|
+
*/
|
|
167
|
+
function negate(node) {
|
|
168
|
+
if (node.type === 'Num') {
|
|
169
|
+
return num(-node.value);
|
|
170
|
+
}
|
|
171
|
+
if (node.type === 'Dim') {
|
|
172
|
+
return dim(-node.value, node.unit);
|
|
173
|
+
}
|
|
174
|
+
if (node.type === 'Sum') {
|
|
175
|
+
return mkSum(
|
|
176
|
+
node.terms.map((t) => ({
|
|
177
|
+
sign: /** @type {1 | -1} */ (-t.sign),
|
|
178
|
+
node: t.node,
|
|
179
|
+
}))
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
// Opaque (Ident, Call, Product): wrap as a single negative-sign term —
|
|
183
|
+
// the only case where sign=-1 remains on a SumTerm.
|
|
184
|
+
return { type: 'Sum', terms: [{ sign: -1, node }] };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Stryker disable next-line all: instrumenting this line breaks Node's
|
|
188
|
+
// cjs-module-lexer named-export detection for .mjs `import { x } from` consumers.
|
|
189
|
+
module.exports = { num, dim, ident, call, mkSum, mkProduct, negate };
|