postcss-calc 11.1.2 → 11.2.1
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 +40 -18
- package/package.json +18 -14
- package/src/index.js +8 -5
- package/src/lib/analyze.js +272 -0
- package/src/lib/block-index.js +118 -0
- package/src/lib/compile.js +80 -0
- package/src/lib/functions.js +394 -0
- package/src/lib/limits.js +49 -0
- package/src/lib/node.js +63 -21
- package/src/lib/opaque.js +12 -32
- package/src/lib/parser.js +373 -328
- package/src/lib/print.js +75 -0
- package/src/lib/regex.js +4 -0
- package/src/lib/scan.js +61 -0
- package/src/lib/serialize.js +626 -223
- package/src/lib/simplify/call.js +5 -98
- package/src/lib/simplify/round.js +4 -4
- package/src/lib/simplify/sum.js +14 -5
- package/src/lib/simplify.js +20 -5
- package/src/reduce.js +46 -115
- package/types/index.d.ts +4 -0
- package/types/lib/analyze.d.ts +45 -0
- package/types/lib/block-index.d.ts +35 -0
- package/types/lib/calculation-type.d.ts +18 -0
- package/types/lib/compile.d.ts +37 -0
- package/types/lib/functions.d.ts +58 -0
- package/types/lib/limits.d.ts +8 -0
- package/types/lib/node.d.ts +19 -3
- package/types/lib/opaque.d.ts +8 -14
- package/types/lib/parser.d.ts +42 -48
- package/types/lib/print.d.ts +16 -0
- package/types/lib/regex.d.ts +2 -0
- package/types/lib/scan.d.ts +35 -0
- package/types/lib/serialize.d.ts +19 -2
- package/types/lib/simplify/call.d.ts +4 -18
- package/types/lib/simplify/round.d.ts +3 -1
- package/types/lib/simplify.d.ts +5 -2
- package/types/reduce.d.ts +47 -6
- package/src/lib/tokenizer.js +0 -10
- package/types/lib/tokenizer.d.ts +0 -3
package/src/lib/parser.js
CHANGED
|
@@ -1,227 +1,129 @@
|
|
|
1
1
|
// Pratt parser over native @csstools/css-tokenizer tokens.
|
|
2
2
|
import { TokenType as CssType } from '@csstools/css-tokenizer';
|
|
3
3
|
import { baseOf } from './convertUnits.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { call, dim, ident, mkProduct, mkSum, num, opaqueCall } from './node.js';
|
|
5
|
+
import { isCalculationFunction, isSupportedMathFunction } from './functions.js';
|
|
6
|
+
import { assertDepth } from './limits.js';
|
|
7
|
+
import { CSS_NUMBER_PREFIX } from './regex.js';
|
|
7
8
|
|
|
8
9
|
/** @typedef {import('@csstools/css-tokenizer').CSSToken} CSSToken */
|
|
9
10
|
/** @typedef {import('./node.js').Node} Node */
|
|
10
|
-
/** @typedef {
|
|
11
|
+
/** @typedef {import('./node.js').OpaqueComponent} OpaqueComponent */
|
|
12
|
+
/** @typedef {ReturnType<typeof import('./block-index.js').indexBlocks>} BlockIndex */
|
|
13
|
+
/** @typedef {{raw: string, pos: number, ws: boolean, index: number}} TokenBase */
|
|
14
|
+
/** @typedef {TokenBase & {type: 'number', value: number, signCharacter?: '+' | '-'}} NumberToken */
|
|
15
|
+
/** @typedef {TokenBase & {type: 'dimension', value: number, unit: string, rawUnit: string, signCharacter?: '+' | '-'}} DimensionToken */
|
|
16
|
+
/** @typedef {TokenBase & {type: 'ident', value: string}} IdentToken */
|
|
17
|
+
/** @typedef {TokenBase & {type: 'function', value: string}} FunctionToken */
|
|
18
|
+
/** @typedef {'(' | ')' | ',' | '+' | '-' | '*' | '/'} Punctuator */
|
|
19
|
+
/** @typedef {TokenBase & {type: 'punct', value: Punctuator}} PunctToken */
|
|
20
|
+
/** @typedef {TokenBase & {type: 'eof', value: '', raw: ''}} EofToken */
|
|
21
|
+
/** @typedef {NumberToken | DimensionToken | IdentToken | FunctionToken | PunctToken | EofToken} Token */
|
|
11
22
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @
|
|
14
|
-
* @property {string | number} value
|
|
15
|
-
* @property {string} raw
|
|
16
|
-
* @property {string} [unit]
|
|
17
|
-
* @property {string} [rawUnit]
|
|
18
|
-
* @property {'+' | '-'} [signCharacter]
|
|
19
|
-
* @property {number} pos
|
|
20
|
-
* @property {boolean} ws
|
|
23
|
+
* Immutable bounds and shared block index for one parse range.
|
|
24
|
+
* @typedef {Readonly<{tokens: CSSToken[], end: number, index: BlockIndex}>} ParseInput
|
|
21
25
|
*/
|
|
22
|
-
/** @typedef {(p: Parser, token: Token) => Node} PrefixParselet */
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
/** @param {string} value @return {value is '+' | '-' | '*' | '/'} */
|
|
28
|
+
function isOperator(value) {
|
|
29
|
+
return value === '+' || value === '-' || value === '*' || value === '/';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* CSS numeric tokens do not retain a signed-zero distinction. Keep that
|
|
34
|
+
* normalization at the source boundary so a later IEEE-754 `-0` can only
|
|
35
|
+
* have been introduced by calculation evaluation.
|
|
36
|
+
*
|
|
37
|
+
* @param {number} value
|
|
38
|
+
* @return {number}
|
|
39
|
+
*/
|
|
40
|
+
function normalizeSourceZero(value) {
|
|
41
|
+
return value === 0 ? 0 : value;
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
/** @param {string} raw @param {string} decoded */
|
|
33
45
|
function sourceSpelling(raw, decoded) {
|
|
34
46
|
return raw === decoded ? undefined : raw;
|
|
35
47
|
}
|
|
36
48
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @param {CSSToken[]} tokens
|
|
54
|
-
* @param {number} start
|
|
55
|
-
* @param {number} end
|
|
56
|
-
* @param {Map<number, number>} [ends]
|
|
57
|
-
*/
|
|
58
|
-
constructor(tokens, start, end, ends = blockEnds(tokens, start, end)) {
|
|
59
|
-
this.#tokens = tokens;
|
|
60
|
-
this.#end = end;
|
|
61
|
-
this.#ends = ends;
|
|
62
|
-
this.#i = start;
|
|
63
|
-
this.#precededByWhitespace = true;
|
|
64
|
-
this.#lookahead = null;
|
|
49
|
+
/**
|
|
50
|
+
* Mutable navigation state only. `index` is always the next native token
|
|
51
|
+
* position; trivia is intentionally left visible to `scanToken`.
|
|
52
|
+
*/
|
|
53
|
+
class Cursor {
|
|
54
|
+
/** @param {number} start */
|
|
55
|
+
constructor(start) {
|
|
56
|
+
/** @type {number} */
|
|
57
|
+
this.index = start;
|
|
58
|
+
/** @type {boolean} */
|
|
59
|
+
this.firstToken = true;
|
|
60
|
+
/** @type {Token | null} */
|
|
61
|
+
this.lookahead = null;
|
|
62
|
+
/** @type {number} */
|
|
63
|
+
this.lookaheadNextIndex = start;
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
/** @
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
/** @param {number} index @return {void} */
|
|
67
|
+
skipTo(index) {
|
|
68
|
+
this.index = index;
|
|
69
|
+
this.lookaheadNextIndex = index;
|
|
70
|
+
this.firstToken = false;
|
|
71
|
+
this.lookahead = null;
|
|
70
72
|
}
|
|
73
|
+
}
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
this.#end < this.#tokens.length &&
|
|
79
|
-
this.#tokens[this.#end][0] !== CssType.EOF
|
|
80
|
-
) {
|
|
81
|
-
return this.#tokens[this.#end][2];
|
|
82
|
-
}
|
|
83
|
-
for (let i = Math.min(this.#end, this.#tokens.length) - 1; i >= 0; i--) {
|
|
84
|
-
const token = this.#tokens[i];
|
|
85
|
-
if (token[0] !== CssType.EOF) return token[3] + 1;
|
|
86
|
-
}
|
|
87
|
-
return 0;
|
|
88
|
-
}
|
|
75
|
+
/** @param {OpaqueComponent[]} target @param {OpaqueComponent} part */
|
|
76
|
+
function pushComponent(target, part) {
|
|
77
|
+
if (typeof part === 'string' && typeof target.at(-1) === 'string')
|
|
78
|
+
target[target.length - 1] += part;
|
|
79
|
+
else target.push(part);
|
|
80
|
+
}
|
|
89
81
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Scan one token without consuming it. `firstToken` supplies the virtual
|
|
84
|
+
* leading trivia at a bounded parse boundary; all later whitespace state is
|
|
85
|
+
* derived from the native tokens encountered in this scan.
|
|
86
|
+
*
|
|
87
|
+
* @param {ParseInput} input
|
|
88
|
+
* @param {Cursor} cursor
|
|
89
|
+
* @return {Token}
|
|
90
|
+
*/
|
|
91
|
+
function scanToken(input, cursor) {
|
|
92
|
+
let i = cursor.index;
|
|
93
|
+
let ws = cursor.firstToken;
|
|
94
|
+
while (i < input.end) {
|
|
95
|
+
const native = input.tokens[i];
|
|
96
|
+
if (native[0] === CssType.Whitespace || native[0] === CssType.Comment) {
|
|
97
|
+
ws = true;
|
|
98
|
+
i++;
|
|
99
|
+
continue;
|
|
102
100
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
pos: this.eofPosition(),
|
|
108
|
-
ws: this.#precededByWhitespace,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** @return {Token} */
|
|
113
|
-
peek() {
|
|
114
|
-
if (this.#lookahead === null) this.#lookahead = this.read();
|
|
115
|
-
return this.#lookahead;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** @return {Token} */
|
|
119
|
-
next() {
|
|
120
|
-
const token = this.peek();
|
|
121
|
-
this.#lookahead = null;
|
|
101
|
+
if (native[0] === CssType.EOF) break;
|
|
102
|
+
const token = normalizeToken(native, i, ws);
|
|
103
|
+
cursor.lookahead = token;
|
|
104
|
+
cursor.lookaheadNextIndex = i + 1;
|
|
122
105
|
return token;
|
|
123
106
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
/** @param {string} value @param {string} [value2] @return {boolean} */
|
|
142
|
-
isPunct(value, value2) {
|
|
143
|
-
const t = this.peek();
|
|
144
|
-
return (
|
|
145
|
-
t.type === 'punct' &&
|
|
146
|
-
(t.value === value || (value2 !== undefined && t.value === value2))
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** @param {string} value @return {boolean} */
|
|
151
|
-
matchPunct(value) {
|
|
152
|
-
if (!this.isPunct(value)) return false;
|
|
153
|
-
this.next();
|
|
154
|
-
return true;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** @param {string} value @return {Token} */
|
|
158
|
-
expectPunct(value) {
|
|
159
|
-
const t = this.next();
|
|
160
|
-
if (t.type !== 'punct' || t.value !== value) {
|
|
161
|
-
throw new Error(
|
|
162
|
-
`Expected ${value} at position ${t.pos}, got "${t.value}"`
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
return t;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** @param {number} [minBp] @return {Node} */
|
|
169
|
-
parseExpr(minBp = 0) {
|
|
170
|
-
const t = this.next();
|
|
171
|
-
const key = t.type === 'punct' ? String(t.value) : t.type;
|
|
172
|
-
const prefix = PREFIX[key];
|
|
173
|
-
if (!prefix)
|
|
174
|
-
throw new Error(`Unexpected token "${t.raw}" at position ${t.pos}`);
|
|
175
|
-
let left = prefix(this, t);
|
|
176
|
-
|
|
177
|
-
while (true) {
|
|
178
|
-
const nxt = this.peek();
|
|
179
|
-
if (
|
|
180
|
-
(nxt.type === 'number' || nxt.type === 'dimension') &&
|
|
181
|
-
nxt.signCharacter !== undefined
|
|
182
|
-
) {
|
|
183
|
-
throw new Error(
|
|
184
|
-
`"${nxt.signCharacter}" must be surrounded by whitespace at position ${nxt.pos}`
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
const infixKey = nxt.type === 'punct' ? String(nxt.value) : nxt.type;
|
|
188
|
-
const rule = INFIX[infixKey];
|
|
189
|
-
if (!rule || rule.lbp < minBp) break;
|
|
190
|
-
if (infixKey === '+' || infixKey === '-') {
|
|
191
|
-
/** @type {import('./node.js').SumTerm[]} */
|
|
192
|
-
const terms = [{ sign: /** @type {1} */ (1), node: left }];
|
|
193
|
-
do {
|
|
194
|
-
const token = this.next();
|
|
195
|
-
requireSurroundingWs(this, token);
|
|
196
|
-
terms.push({
|
|
197
|
-
sign: /** @type {1 | -1} */ (token.value === '+' ? 1 : -1),
|
|
198
|
-
node: this.parseExpr(ADD_BP + 1),
|
|
199
|
-
});
|
|
200
|
-
} while (this.isPunct('+', '-'));
|
|
201
|
-
left = mkSum(terms);
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
if (infixKey === '*' || infixKey === '/') {
|
|
205
|
-
/** @type {import('./node.js').ProductFactor[]} */
|
|
206
|
-
const factors = [{ exponent: /** @type {1} */ (1), node: left }];
|
|
207
|
-
do {
|
|
208
|
-
const token = this.next();
|
|
209
|
-
factors.push({
|
|
210
|
-
exponent: /** @type {1 | -1} */ (token.value === '*' ? 1 : -1),
|
|
211
|
-
node: this.parseExpr(MUL_BP + 1),
|
|
212
|
-
});
|
|
213
|
-
} while (this.isPunct('*', '/'));
|
|
214
|
-
left = mkProduct(factors);
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
break;
|
|
218
|
-
}
|
|
219
|
-
return left;
|
|
220
|
-
}
|
|
107
|
+
/** @type {EofToken} */
|
|
108
|
+
const token = {
|
|
109
|
+
type: 'eof',
|
|
110
|
+
value: '',
|
|
111
|
+
raw: '',
|
|
112
|
+
pos: eofPositionAt(input, i),
|
|
113
|
+
ws,
|
|
114
|
+
index: i,
|
|
115
|
+
};
|
|
116
|
+
cursor.lookahead = token;
|
|
117
|
+
// Native EOF is a real token and is consumed past its array index. When
|
|
118
|
+
// the bounded range ends before native EOF, this is a virtual EOF and must
|
|
119
|
+
// remain at the range boundary.
|
|
120
|
+
cursor.lookaheadNextIndex =
|
|
121
|
+
i < input.end && input.tokens[i][0] === CssType.EOF ? i + 1 : input.end;
|
|
122
|
+
return token;
|
|
221
123
|
}
|
|
222
124
|
|
|
223
|
-
/** @param {CSSToken} t @param {boolean} ws @return {Token} */
|
|
224
|
-
function normalizeToken(t, ws) {
|
|
125
|
+
/** @param {CSSToken} t @param {number} index @param {boolean} ws @return {Token} */
|
|
126
|
+
function normalizeToken(t, index, ws) {
|
|
225
127
|
const [type, raw, pos, , detail] = t;
|
|
226
128
|
switch (type) {
|
|
227
129
|
case CssType.Number:
|
|
@@ -231,16 +133,18 @@ function normalizeToken(t, ws) {
|
|
|
231
133
|
raw,
|
|
232
134
|
pos,
|
|
233
135
|
ws,
|
|
136
|
+
index,
|
|
234
137
|
signCharacter: detail.signCharacter,
|
|
235
138
|
};
|
|
236
139
|
case CssType.Dimension: {
|
|
237
|
-
const match =
|
|
140
|
+
const match = CSS_NUMBER_PREFIX.exec(raw);
|
|
238
141
|
return {
|
|
239
142
|
type: 'dimension',
|
|
240
143
|
value: detail.value,
|
|
241
144
|
raw,
|
|
242
145
|
pos,
|
|
243
146
|
ws,
|
|
147
|
+
index,
|
|
244
148
|
unit: detail.unit,
|
|
245
149
|
rawUnit: match ? raw.slice(match[0].length) : detail.unit,
|
|
246
150
|
signCharacter: detail.signCharacter,
|
|
@@ -253,6 +157,7 @@ function normalizeToken(t, ws) {
|
|
|
253
157
|
raw,
|
|
254
158
|
pos,
|
|
255
159
|
ws,
|
|
160
|
+
index,
|
|
256
161
|
unit: '%',
|
|
257
162
|
rawUnit: '%',
|
|
258
163
|
signCharacter: detail.signCharacter,
|
|
@@ -265,18 +170,179 @@ function normalizeToken(t, ws) {
|
|
|
265
170
|
raw,
|
|
266
171
|
pos,
|
|
267
172
|
ws,
|
|
173
|
+
index,
|
|
268
174
|
};
|
|
269
175
|
case CssType.OpenParen:
|
|
176
|
+
return { type: 'punct', value: '(', raw, pos, ws, index };
|
|
270
177
|
case CssType.CloseParen:
|
|
178
|
+
return { type: 'punct', value: ')', raw, pos, ws, index };
|
|
271
179
|
case CssType.Comma:
|
|
272
|
-
return { type: 'punct', value:
|
|
180
|
+
return { type: 'punct', value: ',', raw, pos, ws, index };
|
|
273
181
|
case CssType.Delim:
|
|
274
|
-
if (
|
|
275
|
-
return {
|
|
182
|
+
if (isOperator(detail.value))
|
|
183
|
+
return {
|
|
184
|
+
type: 'punct',
|
|
185
|
+
value: detail.value,
|
|
186
|
+
raw,
|
|
187
|
+
pos,
|
|
188
|
+
ws,
|
|
189
|
+
index,
|
|
190
|
+
};
|
|
276
191
|
}
|
|
277
192
|
throw new Error(`Unexpected character "${raw[0] ?? ''}" at position ${pos}`);
|
|
278
193
|
}
|
|
279
194
|
|
|
195
|
+
/** @param {ParseInput} input @param {number} index @return {number} */
|
|
196
|
+
function eofPositionAt(input, index) {
|
|
197
|
+
if (index < input.end && input.tokens[index][0] !== CssType.EOF) {
|
|
198
|
+
return input.tokens[index][2];
|
|
199
|
+
}
|
|
200
|
+
if (
|
|
201
|
+
input.end < input.tokens.length &&
|
|
202
|
+
input.tokens[input.end][0] !== CssType.EOF
|
|
203
|
+
) {
|
|
204
|
+
return input.tokens[input.end][2];
|
|
205
|
+
}
|
|
206
|
+
for (let i = Math.min(input.end, input.tokens.length) - 1; i >= 0; i--) {
|
|
207
|
+
const token = input.tokens[i];
|
|
208
|
+
if (token[0] !== CssType.EOF) return token[3] + 1;
|
|
209
|
+
}
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** @param {ParseInput} input @param {Cursor} cursor @return {Token} */
|
|
214
|
+
function peekToken(input, cursor) {
|
|
215
|
+
return cursor.lookahead ?? scanToken(input, cursor);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Consume the cached token and advance to its native next index. This is one
|
|
220
|
+
* of the only two operations allowed to advance `cursor.index`.
|
|
221
|
+
* @param {ParseInput} input
|
|
222
|
+
* @param {Cursor} cursor
|
|
223
|
+
* @return {Token}
|
|
224
|
+
*/
|
|
225
|
+
function takeToken(input, cursor) {
|
|
226
|
+
const token = peekToken(input, cursor);
|
|
227
|
+
cursor.index = cursor.lookaheadNextIndex;
|
|
228
|
+
cursor.firstToken = false;
|
|
229
|
+
cursor.lookahead = null;
|
|
230
|
+
return token;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** @param {ParseInput} input @param {Cursor} cursor @param {Punctuator} value @param {Punctuator} [value2] @return {boolean} */
|
|
234
|
+
function isPunct(input, cursor, value, value2) {
|
|
235
|
+
const t = peekToken(input, cursor);
|
|
236
|
+
return (
|
|
237
|
+
t.type === 'punct' &&
|
|
238
|
+
(t.value === value || (value2 !== undefined && t.value === value2))
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** @param {ParseInput} input @param {Cursor} cursor @param {Punctuator} value @return {boolean} */
|
|
243
|
+
function matchPunct(input, cursor, value) {
|
|
244
|
+
if (!isPunct(input, cursor, value)) return false;
|
|
245
|
+
takeToken(input, cursor);
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** @param {ParseInput} input @param {Cursor} cursor @param {Punctuator} value @return {PunctToken} */
|
|
250
|
+
function expectPunct(input, cursor, value) {
|
|
251
|
+
const t = takeToken(input, cursor);
|
|
252
|
+
if (t.type !== 'punct' || t.value !== value) {
|
|
253
|
+
throw new Error(`Expected ${value} at position ${t.pos}, got "${t.value}"`);
|
|
254
|
+
}
|
|
255
|
+
return t;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** @param {ParseInput} input @param {Cursor} cursor @param {Token} token @param {number} depth @return {Node} */
|
|
259
|
+
function parsePrefix(input, cursor, token, depth) {
|
|
260
|
+
switch (token.type) {
|
|
261
|
+
case 'number':
|
|
262
|
+
return num(normalizeSourceZero(token.value));
|
|
263
|
+
case 'dimension': {
|
|
264
|
+
const unit = token.unit.toLowerCase();
|
|
265
|
+
return dim(
|
|
266
|
+
normalizeSourceZero(token.value),
|
|
267
|
+
unit,
|
|
268
|
+
baseOf(unit) || token.rawUnit === unit ? undefined : token.rawUnit
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
case 'ident':
|
|
272
|
+
return (
|
|
273
|
+
foldCalcKeyword(token.value) ??
|
|
274
|
+
ident(token.value, sourceSpelling(token.raw, token.value))
|
|
275
|
+
);
|
|
276
|
+
case 'function':
|
|
277
|
+
return parseCall(input, cursor, token, depth);
|
|
278
|
+
case 'punct':
|
|
279
|
+
switch (token.value) {
|
|
280
|
+
case '(': {
|
|
281
|
+
const expression = parseExpr(input, cursor, 0, depth + 1);
|
|
282
|
+
expectPunct(input, cursor, ')');
|
|
283
|
+
return expression.type === 'Sum'
|
|
284
|
+
? { ...expression, grouped: true }
|
|
285
|
+
: expression;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// No unary `+`/`-` production exists in the <calc-value> grammar; a
|
|
289
|
+
// sign is only valid inside a number/dimension token or as a binary
|
|
290
|
+
// operator. `-(...)` therefore fails to parse and is preserved.
|
|
291
|
+
}
|
|
292
|
+
throw new Error(`Unexpected token "${token.raw}" at position ${token.pos}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @param {ParseInput} input @param {Cursor} cursor @param {number} minBp @param {number} depth @return {Node} */
|
|
296
|
+
function parseExpr(input, cursor, minBp = 0, depth = 0) {
|
|
297
|
+
assertDepth(depth);
|
|
298
|
+
const t = takeToken(input, cursor);
|
|
299
|
+
let left = parsePrefix(input, cursor, t, depth);
|
|
300
|
+
|
|
301
|
+
while (true) {
|
|
302
|
+
const nxt = peekToken(input, cursor);
|
|
303
|
+
if (
|
|
304
|
+
(nxt.type === 'number' || nxt.type === 'dimension') &&
|
|
305
|
+
nxt.signCharacter !== undefined
|
|
306
|
+
) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`"${nxt.signCharacter}" must be surrounded by whitespace at position ${nxt.pos}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const infixKey = nxt.type === 'punct' ? String(nxt.value) : nxt.type;
|
|
312
|
+
const rule = INFIX[infixKey];
|
|
313
|
+
if (!rule || rule.lbp < minBp) break;
|
|
314
|
+
if (infixKey === '+' || infixKey === '-') {
|
|
315
|
+
/** @type {import('./node.js').SumTerm[]} */
|
|
316
|
+
const terms = [{ sign: /** @type {1} */ (1), node: left }];
|
|
317
|
+
do {
|
|
318
|
+
const token = takeToken(input, cursor);
|
|
319
|
+
requireSurroundingWs(input, cursor, token);
|
|
320
|
+
terms.push({
|
|
321
|
+
sign: /** @type {1 | -1} */ (token.value === '+' ? 1 : -1),
|
|
322
|
+
node: parseExpr(input, cursor, ADD_BP + 1, depth),
|
|
323
|
+
});
|
|
324
|
+
} while (isPunct(input, cursor, '+', '-'));
|
|
325
|
+
left = mkSum(terms);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (infixKey === '*' || infixKey === '/') {
|
|
329
|
+
/** @type {import('./node.js').ProductFactor[]} */
|
|
330
|
+
const factors = [{ exponent: /** @type {1} */ (1), node: left }];
|
|
331
|
+
do {
|
|
332
|
+
const token = takeToken(input, cursor);
|
|
333
|
+
factors.push({
|
|
334
|
+
exponent: /** @type {1 | -1} */ (token.value === '*' ? 1 : -1),
|
|
335
|
+
node: parseExpr(input, cursor, MUL_BP + 1, depth),
|
|
336
|
+
});
|
|
337
|
+
} while (isPunct(input, cursor, '*', '/'));
|
|
338
|
+
left = mkProduct(factors);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
return left;
|
|
344
|
+
}
|
|
345
|
+
|
|
280
346
|
/** §10.9 — case-insensitive except for NaN. @param {string} name @return {Node | null} */
|
|
281
347
|
function foldCalcKeyword(name) {
|
|
282
348
|
if (name === 'NaN' || name === '-NaN') return num(Number.NaN);
|
|
@@ -295,41 +361,45 @@ function foldCalcKeyword(name) {
|
|
|
295
361
|
|
|
296
362
|
const ADD_BP = 1;
|
|
297
363
|
const MUL_BP = 3;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const { start, close, tokens, ends } = p.functionRange();
|
|
364
|
+
/** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} token @param {string} name @param {string} rawName @return {Node} */
|
|
365
|
+
function parseOpaqueCall(input, cursor, token, name, rawName) {
|
|
366
|
+
const start = token.index + 1;
|
|
367
|
+
const close = input.index.closeOf(token.index, input.end);
|
|
303
368
|
if (close === -1)
|
|
304
|
-
throw new Error(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
369
|
+
throw new Error(
|
|
370
|
+
`Unclosed ${name}( at position ${eofPositionAt(input, cursor.index)}`
|
|
371
|
+
);
|
|
372
|
+
cursor.skipTo(close + 1);
|
|
373
|
+
return opaqueCall(
|
|
374
|
+
name,
|
|
375
|
+
componentTree(input, start, close),
|
|
376
|
+
sourceSpelling(rawName, name)
|
|
309
377
|
);
|
|
310
378
|
}
|
|
311
379
|
|
|
312
|
-
/** @param {
|
|
313
|
-
function requireSurroundingWs(
|
|
314
|
-
if (!token.ws || !
|
|
380
|
+
/** @param {ParseInput} input @param {Cursor} cursor @param {Token} token */
|
|
381
|
+
function requireSurroundingWs(input, cursor, token) {
|
|
382
|
+
if (!token.ws || !peekToken(input, cursor).ws)
|
|
315
383
|
throw new Error(
|
|
316
384
|
`"${token.value}" must be surrounded by whitespace at position ${token.pos}`
|
|
317
385
|
);
|
|
318
386
|
}
|
|
319
387
|
|
|
320
|
-
/** @param {
|
|
321
|
-
function parseCall(
|
|
322
|
-
const name =
|
|
388
|
+
/** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} t @param {number} depth @return {Node} */
|
|
389
|
+
function parseCall(input, cursor, t, depth) {
|
|
390
|
+
const name = t.value;
|
|
323
391
|
const rawName = t.raw.slice(0, -1);
|
|
324
|
-
if (name.toLowerCase() === 'var')
|
|
325
|
-
|
|
326
|
-
|
|
392
|
+
if (name.toLowerCase() === 'var')
|
|
393
|
+
return parseVar(input, cursor, t, name, rawName);
|
|
394
|
+
if (!isCalculationFunction(name) && !isSupportedMathFunction(name))
|
|
395
|
+
return parseOpaqueCall(input, cursor, t, name, rawName);
|
|
327
396
|
/** @type {Node[]} */ const args = [];
|
|
328
|
-
if (!
|
|
329
|
-
args.push(
|
|
330
|
-
while (
|
|
397
|
+
if (!isPunct(input, cursor, ')')) {
|
|
398
|
+
args.push(parseExpr(input, cursor, 0, depth + 1));
|
|
399
|
+
while (matchPunct(input, cursor, ','))
|
|
400
|
+
args.push(parseExpr(input, cursor, 0, depth + 1));
|
|
331
401
|
}
|
|
332
|
-
|
|
402
|
+
expectPunct(input, cursor, ')');
|
|
333
403
|
return call(name, args, sourceSpelling(rawName, name));
|
|
334
404
|
}
|
|
335
405
|
|
|
@@ -339,37 +409,6 @@ function rawTokens(tokens, start, end) {
|
|
|
339
409
|
for (let i = start; i < end; i++) raw += tokens[i][1];
|
|
340
410
|
return raw;
|
|
341
411
|
}
|
|
342
|
-
/** @param {CSSToken[]} tokens @param {number} start @param {number} end @param {Map<number, number>} ends @return {number} */
|
|
343
|
-
function firstComma(tokens, start, end, ends) {
|
|
344
|
-
for (let i = start; i < end; i++) {
|
|
345
|
-
const close = ends.get(i);
|
|
346
|
-
if (close !== undefined) {
|
|
347
|
-
i = close;
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
if (tokens[i][0] === CssType.Comma) return i;
|
|
351
|
-
}
|
|
352
|
-
return -1;
|
|
353
|
-
}
|
|
354
|
-
/** @param {CSSToken[]} tokens @param {number} start @param {number} end */
|
|
355
|
-
function blockEnds(tokens, start, end) {
|
|
356
|
-
/** @type {{index: number, close: import('@csstools/css-tokenizer').TokenType}[]} */
|
|
357
|
-
const stack = [];
|
|
358
|
-
/** @type {Map<number, number>} */ const ends = new Map();
|
|
359
|
-
for (let i = start; i < end; i++) {
|
|
360
|
-
const close = BLOCK_CLOSE.get(tokens[i][0]);
|
|
361
|
-
if (close !== undefined) {
|
|
362
|
-
stack.push({ index: i, close });
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
const open = stack.at(-1);
|
|
366
|
-
if (open !== undefined && tokens[i][0] === open.close) {
|
|
367
|
-
stack.pop();
|
|
368
|
-
ends.set(open.index, i);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return ends;
|
|
372
|
-
}
|
|
373
412
|
/** @param {CSSToken[]} tokens @param {number} start @param {number} end */
|
|
374
413
|
function customProperty(tokens, start, end) {
|
|
375
414
|
/** @type {CSSToken | null} */
|
|
@@ -388,88 +427,88 @@ function customProperty(tokens, start, end) {
|
|
|
388
427
|
? { decoded, raw: found[1], index: foundIndex }
|
|
389
428
|
: null;
|
|
390
429
|
}
|
|
391
|
-
/** @param {
|
|
392
|
-
function componentTree(
|
|
393
|
-
|
|
394
|
-
/** @
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
430
|
+
/** @param {ParseInput} input @param {number} start @param {number} end @param {number} [depth] @return {OpaqueComponent[]} */
|
|
431
|
+
function componentTree(input, start, end, depth = 0) {
|
|
432
|
+
assertDepth(depth);
|
|
433
|
+
/** @type {OpaqueComponent[]} */ const root = [];
|
|
434
|
+
/** @type {OpaqueComponent[]} */ let tree = root;
|
|
435
|
+
/** @type {{parent: OpaqueComponent[], tree: OpaqueComponent[], close: number, end: number}[]} */
|
|
436
|
+
const frames = [];
|
|
437
|
+
const { tokens } = input;
|
|
438
|
+
let i = start;
|
|
439
|
+
while (true) {
|
|
440
|
+
if (i >= end) {
|
|
441
|
+
if (frames.length === 0) break;
|
|
442
|
+
const frame =
|
|
443
|
+
/** @type {{parent: OpaqueComponent[], tree: OpaqueComponent[], close: number, end: number}} */ (
|
|
444
|
+
frames.pop()
|
|
445
|
+
);
|
|
446
|
+
tree = frame.parent;
|
|
447
|
+
pushComponent(tree, frame.tree);
|
|
448
|
+
pushComponent(tree, tokens[frame.close][1]);
|
|
449
|
+
end = frame.end;
|
|
450
|
+
i = frame.close + 1;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
401
454
|
const token = tokens[i];
|
|
402
|
-
const close =
|
|
455
|
+
const close = input.index.closeOf(i, end);
|
|
403
456
|
if (close === -1) {
|
|
404
|
-
|
|
457
|
+
pushComponent(tree, token[1]);
|
|
458
|
+
i++;
|
|
405
459
|
continue;
|
|
406
460
|
}
|
|
461
|
+
|
|
407
462
|
const isMathFunction =
|
|
408
463
|
token[0] === CssType.Function &&
|
|
409
|
-
(
|
|
464
|
+
(isCalculationFunction(token[4].value) ||
|
|
410
465
|
isSupportedMathFunction(token[4].value));
|
|
411
466
|
if (isMathFunction) {
|
|
412
467
|
try {
|
|
413
|
-
|
|
468
|
+
pushComponent(tree, parseRange(input, i, close + 1));
|
|
414
469
|
} catch {
|
|
415
|
-
|
|
470
|
+
pushComponent(tree, rawTokens(tokens, i, close + 1));
|
|
416
471
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
push(componentTree(tokens, i + 1, close, ends));
|
|
420
|
-
push(tokens[close][1]);
|
|
472
|
+
i = close + 1;
|
|
473
|
+
continue;
|
|
421
474
|
}
|
|
422
|
-
|
|
475
|
+
|
|
476
|
+
pushComponent(tree, token[1]);
|
|
477
|
+
assertDepth(depth + frames.length + 1);
|
|
478
|
+
/** @type {OpaqueComponent[]} */
|
|
479
|
+
const child = [];
|
|
480
|
+
frames.push({ parent: tree, tree: child, close, end });
|
|
481
|
+
tree = child;
|
|
482
|
+
end = close;
|
|
483
|
+
i++;
|
|
423
484
|
}
|
|
424
|
-
return
|
|
485
|
+
return root;
|
|
425
486
|
}
|
|
426
|
-
/** @param {
|
|
427
|
-
function parseVar(
|
|
428
|
-
const {
|
|
487
|
+
/** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} token @param {string} name @param {string} rawName @return {Node} */
|
|
488
|
+
function parseVar(input, cursor, token, name, rawName) {
|
|
489
|
+
const { tokens } = input;
|
|
490
|
+
const start = token.index + 1;
|
|
491
|
+
const close = input.index.closeOf(token.index, input.end);
|
|
429
492
|
if (close === -1)
|
|
430
|
-
throw new Error(
|
|
431
|
-
|
|
493
|
+
throw new Error(
|
|
494
|
+
`Unclosed ${name}( at position ${eofPositionAt(input, cursor.index)}`
|
|
495
|
+
);
|
|
496
|
+
const comma = input.index.firstTopLevelComma(start, close);
|
|
432
497
|
const property = customProperty(tokens, start, comma === -1 ? close : comma);
|
|
433
498
|
if (!property)
|
|
434
499
|
throw new Error(
|
|
435
|
-
`Invalid custom property in ${name}() at position ${tokens[start]?.[2] ??
|
|
500
|
+
`Invalid custom property in ${name}() at position ${tokens[start]?.[2] ?? eofPositionAt(input, cursor.index)}`
|
|
436
501
|
);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
);
|
|
502
|
+
cursor.skipTo(close + 1);
|
|
503
|
+
/** @type {OpaqueComponent[]} */
|
|
504
|
+
const components = [
|
|
505
|
+
ident(property.decoded, sourceSpelling(property.raw, property.decoded)),
|
|
506
|
+
];
|
|
443
507
|
if (comma !== -1)
|
|
444
|
-
|
|
445
|
-
return
|
|
508
|
+
components.push(...componentTree(input, property.index + 1, close));
|
|
509
|
+
return opaqueCall(name, components, sourceSpelling(rawName, name));
|
|
446
510
|
}
|
|
447
511
|
|
|
448
|
-
/** @type {Record<string, PrefixParselet>} */
|
|
449
|
-
const PREFIX = {
|
|
450
|
-
number: (_p, t) => num(/** @type {number} */ (t.value)),
|
|
451
|
-
dimension: (_p, t) => {
|
|
452
|
-
const unit = /** @type {string} */ (t.unit).toLowerCase();
|
|
453
|
-
return dim(
|
|
454
|
-
/** @type {number} */ (t.value),
|
|
455
|
-
unit,
|
|
456
|
-
baseOf(unit) || t.rawUnit === unit ? undefined : t.rawUnit
|
|
457
|
-
);
|
|
458
|
-
},
|
|
459
|
-
ident: (_p, t) => {
|
|
460
|
-
const name = String(t.value);
|
|
461
|
-
return foldCalcKeyword(name) ?? ident(name, sourceSpelling(t.raw, name));
|
|
462
|
-
},
|
|
463
|
-
function: parseCall,
|
|
464
|
-
'(': (p) => {
|
|
465
|
-
const e = p.parseExpr(0);
|
|
466
|
-
p.expectPunct(')');
|
|
467
|
-
return e.type === 'Sum' ? { ...e, grouped: true } : e;
|
|
468
|
-
},
|
|
469
|
-
'-': (p) => negate(p.parseExpr(7)),
|
|
470
|
-
'+': (p) => p.parseExpr(7),
|
|
471
|
-
};
|
|
472
|
-
|
|
473
512
|
/** @type {Record<string, {lbp: number}>} */
|
|
474
513
|
const INFIX = {
|
|
475
514
|
'+': { lbp: ADD_BP },
|
|
@@ -479,16 +518,23 @@ const INFIX = {
|
|
|
479
518
|
};
|
|
480
519
|
|
|
481
520
|
/**
|
|
482
|
-
* @param {
|
|
521
|
+
* @param {ParseInput} input
|
|
483
522
|
* @param {number} start
|
|
484
523
|
* @param {number} end
|
|
485
|
-
* @param {Map<number, number>} ends
|
|
486
524
|
* @return {Node}
|
|
487
525
|
*/
|
|
488
|
-
function parseRange(
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
526
|
+
function parseRange(input, start, end) {
|
|
527
|
+
const bounded =
|
|
528
|
+
input.end === end
|
|
529
|
+
? input
|
|
530
|
+
: /** @type {ParseInput} */ ({
|
|
531
|
+
tokens: input.tokens,
|
|
532
|
+
end,
|
|
533
|
+
index: input.index,
|
|
534
|
+
});
|
|
535
|
+
const cursor = new Cursor(start);
|
|
536
|
+
const ast = parseExpr(bounded, cursor, 0, 0);
|
|
537
|
+
const trailing = peekToken(bounded, cursor);
|
|
492
538
|
if (trailing.type !== 'eof')
|
|
493
539
|
throw new Error(
|
|
494
540
|
`Unexpected token "${trailing.raw}" at position ${trailing.pos}`
|
|
@@ -496,10 +542,9 @@ function parseRange(tokens, start, end, ends) {
|
|
|
496
542
|
return ast;
|
|
497
543
|
}
|
|
498
544
|
|
|
499
|
-
/** @param {CSSToken[]} tokens @param {number}
|
|
500
|
-
function parse(tokens, start
|
|
501
|
-
|
|
502
|
-
return parseRange(tokens, start, end, ends);
|
|
545
|
+
/** @param {CSSToken[]} tokens @param {number} start @param {number} end @param {BlockIndex} index @return {Node} */
|
|
546
|
+
function parse(tokens, start, end, index) {
|
|
547
|
+
return parseRange({ tokens, end, index }, start, end);
|
|
503
548
|
}
|
|
504
549
|
|
|
505
550
|
export { parse };
|