postcss-calc 11.1.0 → 11.1.2
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 +38 -8
- package/package.json +6 -5
- package/src/index.js +16 -4
- package/src/lib/convertUnits.js +15 -1
- package/src/lib/node.js +22 -18
- package/src/lib/opaque.js +40 -0
- package/src/lib/parser.js +381 -200
- package/src/lib/serialize.js +49 -16
- package/src/lib/simplify/abs.js +1 -1
- package/src/lib/simplify/bucket.js +18 -23
- package/src/lib/simplify/call.js +8 -1
- package/src/lib/simplify/product.js +7 -4
- package/src/lib/simplify/sum.js +8 -5
- package/src/lib/tokenizer.js +6 -135
- package/src/reduce.js +22 -14
- package/types/lib/convertUnits.d.ts +8 -1
- package/types/lib/node.d.ts +12 -6
- package/types/lib/opaque.d.ts +15 -0
- package/types/lib/parser.d.ts +40 -36
- package/types/lib/serialize.d.ts +4 -0
- package/types/lib/simplify/bucket.d.ts +2 -2
- package/types/lib/tokenizer.d.ts +3 -32
- package/types/reduce.d.ts +4 -1
package/src/lib/parser.js
CHANGED
|
@@ -1,82 +1,144 @@
|
|
|
1
|
-
// Pratt parser
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// Pratt parser over native @csstools/css-tokenizer tokens.
|
|
2
|
+
import { TokenType as CssType } from '@csstools/css-tokenizer';
|
|
3
|
+
import { baseOf } from './convertUnits.js';
|
|
4
4
|
import { mkSum, mkProduct, negate, num, dim, ident, call } from './node.js';
|
|
5
|
+
import { setComponents } from './opaque.js';
|
|
6
|
+
import { isSupportedMathFunction } from './simplify/call.js';
|
|
5
7
|
|
|
8
|
+
/** @typedef {import('@csstools/css-tokenizer').CSSToken} CSSToken */
|
|
9
|
+
/** @typedef {import('./node.js').Node} Node */
|
|
10
|
+
/** @typedef {string | Node | Component[]} Component */
|
|
6
11
|
/**
|
|
7
|
-
* @typedef {
|
|
8
|
-
* @
|
|
9
|
-
* @
|
|
10
|
-
* @
|
|
12
|
+
* @typedef {object} Token
|
|
13
|
+
* @property {'number' | 'dimension' | 'ident' | 'function' | 'punct' | 'eof'} type
|
|
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
|
|
11
21
|
*/
|
|
22
|
+
/** @typedef {(p: Parser, token: Token) => Node} PrefixParselet */
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
switch (name.toLowerCase()) {
|
|
26
|
-
case 'pi':
|
|
27
|
-
return num(Math.PI);
|
|
28
|
-
case 'e':
|
|
29
|
-
return num(Math.E);
|
|
30
|
-
case 'infinity':
|
|
31
|
-
return num(Infinity);
|
|
32
|
-
case '-infinity':
|
|
33
|
-
return num(-Infinity);
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
24
|
+
const NUMERIC_RAW = /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?/;
|
|
25
|
+
const PUNCT_DELIMS = new Set(['+', '-', '*', '/']);
|
|
26
|
+
const BLOCK_CLOSE = new Map([
|
|
27
|
+
[CssType.Function, CssType.CloseParen],
|
|
28
|
+
[CssType.OpenParen, CssType.CloseParen],
|
|
29
|
+
[CssType.OpenSquare, CssType.CloseSquare],
|
|
30
|
+
[CssType.OpenCurly, CssType.CloseCurly],
|
|
31
|
+
]);
|
|
32
|
+
/** @param {string} raw @param {string} decoded */
|
|
33
|
+
function sourceSpelling(raw, decoded) {
|
|
34
|
+
return raw === decoded ? undefined : raw;
|
|
36
35
|
}
|
|
37
36
|
|
|
37
|
+
/** Bounded cursor that skips trivia but records whether it preceded a token. */
|
|
38
38
|
class Parser {
|
|
39
|
+
/** @type {CSSToken[]} */
|
|
40
|
+
#tokens;
|
|
41
|
+
/** @type {number} */
|
|
42
|
+
#end;
|
|
43
|
+
/** @type {Map<number, number>} */
|
|
44
|
+
#ends;
|
|
45
|
+
/** @type {number} */
|
|
46
|
+
#i;
|
|
47
|
+
/** @type {boolean} */
|
|
48
|
+
#precededByWhitespace = true;
|
|
49
|
+
/** @type {Token | null} */
|
|
50
|
+
#lookahead = null;
|
|
51
|
+
|
|
39
52
|
/**
|
|
40
|
-
* @param {
|
|
53
|
+
* @param {CSSToken[]} tokens
|
|
54
|
+
* @param {number} start
|
|
55
|
+
* @param {number} end
|
|
56
|
+
* @param {Map<number, number>} [ends]
|
|
41
57
|
*/
|
|
42
|
-
constructor(tokens) {
|
|
43
|
-
|
|
44
|
-
this
|
|
45
|
-
|
|
46
|
-
this
|
|
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;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @return {Map<number, number>} */
|
|
68
|
+
get ends() {
|
|
69
|
+
return this.#ends;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @return {number} */
|
|
73
|
+
eofPosition() {
|
|
74
|
+
if (this.#i < this.#end && this.#tokens[this.#i][0] !== CssType.EOF) {
|
|
75
|
+
return this.#tokens[this.#i][2];
|
|
76
|
+
}
|
|
77
|
+
if (
|
|
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
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @return {Token} */
|
|
91
|
+
read() {
|
|
92
|
+
while (this.#i < this.#end) {
|
|
93
|
+
const native = this.#tokens[this.#i++];
|
|
94
|
+
if (native[0] === CssType.Whitespace || native[0] === CssType.Comment) {
|
|
95
|
+
this.#precededByWhitespace = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (native[0] === CssType.EOF) break;
|
|
99
|
+
const ws = this.#precededByWhitespace;
|
|
100
|
+
this.#precededByWhitespace = false;
|
|
101
|
+
return normalizeToken(native, ws);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
type: 'eof',
|
|
105
|
+
value: '',
|
|
106
|
+
raw: '',
|
|
107
|
+
pos: this.eofPosition(),
|
|
108
|
+
ws: this.#precededByWhitespace,
|
|
109
|
+
};
|
|
47
110
|
}
|
|
48
111
|
|
|
49
112
|
/** @return {Token} */
|
|
50
113
|
peek() {
|
|
51
|
-
|
|
114
|
+
if (this.#lookahead === null) this.#lookahead = this.read();
|
|
115
|
+
return this.#lookahead;
|
|
52
116
|
}
|
|
53
117
|
|
|
54
118
|
/** @return {Token} */
|
|
55
119
|
next() {
|
|
56
|
-
|
|
120
|
+
const token = this.peek();
|
|
121
|
+
this.#lookahead = null;
|
|
122
|
+
return token;
|
|
57
123
|
}
|
|
58
124
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const want = value ?? type;
|
|
68
|
-
throw new Error(
|
|
69
|
-
`Expected ${want} at position ${t.pos}, got "${t.value}"`
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
return t;
|
|
125
|
+
/** @return {{start: number, close: number, tokens: CSSToken[], ends: Map<number, number>}} */
|
|
126
|
+
functionRange() {
|
|
127
|
+
return {
|
|
128
|
+
start: this.#i,
|
|
129
|
+
close: this.#ends.get(this.#i - 1) ?? -1,
|
|
130
|
+
tokens: this.#tokens,
|
|
131
|
+
ends: this.#ends,
|
|
132
|
+
};
|
|
73
133
|
}
|
|
74
134
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
135
|
+
/** @param {number} index */
|
|
136
|
+
consumeThrough(index) {
|
|
137
|
+
this.#i = index;
|
|
138
|
+
this.#lookahead = null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** @param {string} value @param {string} [value2] @return {boolean} */
|
|
80
142
|
isPunct(value, value2) {
|
|
81
143
|
const t = this.peek();
|
|
82
144
|
return (
|
|
@@ -85,46 +147,46 @@ class Parser {
|
|
|
85
147
|
);
|
|
86
148
|
}
|
|
87
149
|
|
|
88
|
-
/**
|
|
89
|
-
* @param {string} value
|
|
90
|
-
* @return {boolean}
|
|
91
|
-
*/
|
|
150
|
+
/** @param {string} value @return {boolean} */
|
|
92
151
|
matchPunct(value) {
|
|
93
|
-
if (this.isPunct(value))
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
return false;
|
|
152
|
+
if (!this.isPunct(value)) return false;
|
|
153
|
+
this.next();
|
|
154
|
+
return true;
|
|
98
155
|
}
|
|
99
156
|
|
|
100
|
-
/**
|
|
101
|
-
* @param {string} value
|
|
102
|
-
* @return {Token}
|
|
103
|
-
*/
|
|
157
|
+
/** @param {string} value @return {Token} */
|
|
104
158
|
expectPunct(value) {
|
|
105
|
-
|
|
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;
|
|
106
166
|
}
|
|
107
167
|
|
|
108
|
-
/**
|
|
109
|
-
* @param {number} [minBp]
|
|
110
|
-
* @return {Node}
|
|
111
|
-
*/
|
|
168
|
+
/** @param {number} [minBp] @return {Node} */
|
|
112
169
|
parseExpr(minBp = 0) {
|
|
113
170
|
const t = this.next();
|
|
114
|
-
const key = t.type === 'punct' ? t.value : t.type;
|
|
171
|
+
const key = t.type === 'punct' ? String(t.value) : t.type;
|
|
115
172
|
const prefix = PREFIX[key];
|
|
116
|
-
if (!prefix)
|
|
117
|
-
throw new Error(`Unexpected token "${t.
|
|
118
|
-
}
|
|
173
|
+
if (!prefix)
|
|
174
|
+
throw new Error(`Unexpected token "${t.raw}" at position ${t.pos}`);
|
|
119
175
|
let left = prefix(this, t);
|
|
120
176
|
|
|
121
177
|
while (true) {
|
|
122
178
|
const nxt = this.peek();
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
);
|
|
127
186
|
}
|
|
187
|
+
const infixKey = nxt.type === 'punct' ? String(nxt.value) : nxt.type;
|
|
188
|
+
const rule = INFIX[infixKey];
|
|
189
|
+
if (!rule || rule.lbp < minBp) break;
|
|
128
190
|
if (infixKey === '+' || infixKey === '-') {
|
|
129
191
|
/** @type {import('./node.js').SumTerm[]} */
|
|
130
192
|
const terms = [{ sign: /** @type {1} */ (1), node: left }];
|
|
@@ -139,7 +201,6 @@ class Parser {
|
|
|
139
201
|
left = mkSum(terms);
|
|
140
202
|
continue;
|
|
141
203
|
}
|
|
142
|
-
|
|
143
204
|
if (infixKey === '*' || infixKey === '/') {
|
|
144
205
|
/** @type {import('./node.js').ProductFactor[]} */
|
|
145
206
|
const factors = [{ exponent: /** @type {1} */ (1), node: left }];
|
|
@@ -153,148 +214,260 @@ class Parser {
|
|
|
153
214
|
left = mkProduct(factors);
|
|
154
215
|
continue;
|
|
155
216
|
}
|
|
156
|
-
|
|
157
|
-
// Every infix operator is handled above. Keep this defensive exit in
|
|
158
|
-
// case a future parselet is added without a chain implementation.
|
|
159
217
|
break;
|
|
160
218
|
}
|
|
161
|
-
|
|
162
219
|
return left;
|
|
163
220
|
}
|
|
164
221
|
}
|
|
165
222
|
|
|
223
|
+
/** @param {CSSToken} t @param {boolean} ws @return {Token} */
|
|
224
|
+
function normalizeToken(t, ws) {
|
|
225
|
+
const [type, raw, pos, , detail] = t;
|
|
226
|
+
switch (type) {
|
|
227
|
+
case CssType.Number:
|
|
228
|
+
return {
|
|
229
|
+
type: 'number',
|
|
230
|
+
value: detail.value,
|
|
231
|
+
raw,
|
|
232
|
+
pos,
|
|
233
|
+
ws,
|
|
234
|
+
signCharacter: detail.signCharacter,
|
|
235
|
+
};
|
|
236
|
+
case CssType.Dimension: {
|
|
237
|
+
const match = NUMERIC_RAW.exec(raw);
|
|
238
|
+
return {
|
|
239
|
+
type: 'dimension',
|
|
240
|
+
value: detail.value,
|
|
241
|
+
raw,
|
|
242
|
+
pos,
|
|
243
|
+
ws,
|
|
244
|
+
unit: detail.unit,
|
|
245
|
+
rawUnit: match ? raw.slice(match[0].length) : detail.unit,
|
|
246
|
+
signCharacter: detail.signCharacter,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
case CssType.Percentage:
|
|
250
|
+
return {
|
|
251
|
+
type: 'dimension',
|
|
252
|
+
value: detail.value,
|
|
253
|
+
raw,
|
|
254
|
+
pos,
|
|
255
|
+
ws,
|
|
256
|
+
unit: '%',
|
|
257
|
+
rawUnit: '%',
|
|
258
|
+
signCharacter: detail.signCharacter,
|
|
259
|
+
};
|
|
260
|
+
case CssType.Ident:
|
|
261
|
+
case CssType.Function:
|
|
262
|
+
return {
|
|
263
|
+
type: type === CssType.Ident ? 'ident' : 'function',
|
|
264
|
+
value: detail.value,
|
|
265
|
+
raw,
|
|
266
|
+
pos,
|
|
267
|
+
ws,
|
|
268
|
+
};
|
|
269
|
+
case CssType.OpenParen:
|
|
270
|
+
case CssType.CloseParen:
|
|
271
|
+
case CssType.Comma:
|
|
272
|
+
return { type: 'punct', value: raw, raw, pos, ws };
|
|
273
|
+
case CssType.Delim:
|
|
274
|
+
if (PUNCT_DELIMS.has(detail.value))
|
|
275
|
+
return { type: 'punct', value: detail.value, raw, pos, ws };
|
|
276
|
+
}
|
|
277
|
+
throw new Error(`Unexpected character "${raw[0] ?? ''}" at position ${pos}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** §10.9 — case-insensitive except for NaN. @param {string} name @return {Node | null} */
|
|
281
|
+
function foldCalcKeyword(name) {
|
|
282
|
+
if (name === 'NaN' || name === '-NaN') return num(Number.NaN);
|
|
283
|
+
switch (name.toLowerCase()) {
|
|
284
|
+
case 'pi':
|
|
285
|
+
return num(Math.PI);
|
|
286
|
+
case 'e':
|
|
287
|
+
return num(Math.E);
|
|
288
|
+
case 'infinity':
|
|
289
|
+
return num(Infinity);
|
|
290
|
+
case '-infinity':
|
|
291
|
+
return num(-Infinity);
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
166
296
|
const ADD_BP = 1;
|
|
167
297
|
const MUL_BP = 3;
|
|
168
|
-
const
|
|
298
|
+
const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
|
|
169
299
|
|
|
170
|
-
/**
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
300
|
+
/** @param {Parser} p @param {string} name @param {string} rawName @return {Node} */
|
|
301
|
+
function parseOpaqueCall(p, name, rawName) {
|
|
302
|
+
const { start, close, tokens, ends } = p.functionRange();
|
|
303
|
+
if (close === -1)
|
|
304
|
+
throw new Error(`Unclosed ${name}( at position ${p.eofPosition()}`);
|
|
305
|
+
p.consumeThrough(close + 1);
|
|
306
|
+
return setComponents(
|
|
307
|
+
call(name, [], sourceSpelling(rawName, name)),
|
|
308
|
+
componentTree(tokens, start, close, ends)
|
|
309
|
+
);
|
|
310
|
+
}
|
|
177
311
|
|
|
178
|
-
/**
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
312
|
+
/** @param {Parser} p @param {Token} token */
|
|
313
|
+
function requireSurroundingWs(p, token) {
|
|
314
|
+
if (!token.ws || !p.peek().ws)
|
|
315
|
+
throw new Error(
|
|
316
|
+
`"${token.value}" must be surrounded by whitespace at position ${token.pos}`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** @param {Parser} p @param {Token} t @return {Node} */
|
|
321
|
+
function parseCall(p, t) {
|
|
322
|
+
const name = String(t.value);
|
|
323
|
+
const rawName = t.raw.slice(0, -1);
|
|
324
|
+
if (name.toLowerCase() === 'var') return parseVar(p, name, rawName);
|
|
325
|
+
if (!MATCH_CALC.test(name) && !isSupportedMathFunction(name))
|
|
326
|
+
return parseOpaqueCall(p, name, rawName);
|
|
327
|
+
/** @type {Node[]} */ const args = [];
|
|
328
|
+
if (!p.isPunct(')')) {
|
|
329
|
+
args.push(p.parseExpr(0));
|
|
330
|
+
while (p.matchPunct(',')) args.push(p.parseExpr(0));
|
|
186
331
|
}
|
|
187
|
-
|
|
332
|
+
p.expectPunct(')');
|
|
333
|
+
return call(name, args, sourceSpelling(rawName, name));
|
|
188
334
|
}
|
|
189
335
|
|
|
190
|
-
/**
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
336
|
+
/** @param {CSSToken[]} tokens @param {number} start @param {number} end */
|
|
337
|
+
function rawTokens(tokens, start, end) {
|
|
338
|
+
let raw = '';
|
|
339
|
+
for (let i = start; i < end; i++) raw += tokens[i][1];
|
|
340
|
+
return raw;
|
|
341
|
+
}
|
|
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;
|
|
205
364
|
}
|
|
206
|
-
|
|
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
|
+
/** @param {CSSToken[]} tokens @param {number} start @param {number} end */
|
|
374
|
+
function customProperty(tokens, start, end) {
|
|
375
|
+
/** @type {CSSToken | null} */
|
|
376
|
+
let found = null;
|
|
377
|
+
let foundIndex = -1;
|
|
378
|
+
for (let i = start; i < end; i++) {
|
|
379
|
+
const type = tokens[i][0];
|
|
380
|
+
if (type === CssType.Whitespace || type === CssType.Comment) continue;
|
|
381
|
+
if (found !== null) return null;
|
|
382
|
+
found = tokens[i];
|
|
383
|
+
foundIndex = i;
|
|
384
|
+
}
|
|
385
|
+
if (!found || found[0] !== CssType.Ident) return null;
|
|
386
|
+
const decoded = found[4].value;
|
|
387
|
+
return decoded.startsWith('--') && decoded !== '--'
|
|
388
|
+
? { decoded, raw: found[1], index: foundIndex }
|
|
389
|
+
: null;
|
|
390
|
+
}
|
|
391
|
+
/** @param {CSSToken[]} tokens @param {number} start @param {number} end @param {Map<number, number>} ends @return {Component[]} */
|
|
392
|
+
function componentTree(tokens, start, end, ends) {
|
|
393
|
+
/** @type {Component[]} */ const tree = [];
|
|
394
|
+
/** @param {Component} part */
|
|
395
|
+
const push = (part) => {
|
|
396
|
+
if (typeof part === 'string' && typeof tree.at(-1) === 'string')
|
|
397
|
+
tree[tree.length - 1] += part;
|
|
398
|
+
else tree.push(part);
|
|
207
399
|
};
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
400
|
+
for (let i = start; i < end; i++) {
|
|
401
|
+
const token = tokens[i];
|
|
402
|
+
const close = ends.get(i) ?? -1;
|
|
403
|
+
if (close === -1) {
|
|
404
|
+
push(token[1]);
|
|
405
|
+
continue;
|
|
212
406
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
} else if (tk.value === ',' && depth === 1) {
|
|
224
|
-
p.next();
|
|
225
|
-
flush();
|
|
226
|
-
continue;
|
|
407
|
+
const isMathFunction =
|
|
408
|
+
token[0] === CssType.Function &&
|
|
409
|
+
(MATCH_CALC.test(token[4].value) ||
|
|
410
|
+
isSupportedMathFunction(token[4].value));
|
|
411
|
+
if (isMathFunction) {
|
|
412
|
+
try {
|
|
413
|
+
push(parseRange(tokens, i, close + 1, ends));
|
|
414
|
+
} catch {
|
|
415
|
+
push(rawTokens(tokens, i, close + 1));
|
|
227
416
|
}
|
|
417
|
+
} else {
|
|
418
|
+
push(token[1]);
|
|
419
|
+
push(componentTree(tokens, i + 1, close, ends));
|
|
420
|
+
push(tokens[close][1]);
|
|
228
421
|
}
|
|
229
|
-
|
|
230
|
-
buf += ' ';
|
|
231
|
-
}
|
|
232
|
-
buf += tokenText(tk);
|
|
233
|
-
p.next();
|
|
422
|
+
i = close;
|
|
234
423
|
}
|
|
424
|
+
return tree;
|
|
235
425
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
* @param {Token} token
|
|
245
|
-
* @return {void}
|
|
246
|
-
*/
|
|
247
|
-
function requireSurroundingWs(p, token) {
|
|
248
|
-
const next = p.peek();
|
|
249
|
-
if (!token.ws || !next.ws) {
|
|
426
|
+
/** @param {Parser} p @param {string} name @param {string} rawName @return {Node} */
|
|
427
|
+
function parseVar(p, name, rawName) {
|
|
428
|
+
const { start, close, tokens, ends } = p.functionRange();
|
|
429
|
+
if (close === -1)
|
|
430
|
+
throw new Error(`Unclosed ${name}( at position ${p.eofPosition()}`);
|
|
431
|
+
const comma = firstComma(tokens, start, close, ends);
|
|
432
|
+
const property = customProperty(tokens, start, comma === -1 ? close : comma);
|
|
433
|
+
if (!property)
|
|
250
434
|
throw new Error(
|
|
251
|
-
`
|
|
435
|
+
`Invalid custom property in ${name}() at position ${tokens[start]?.[2] ?? p.eofPosition()}`
|
|
252
436
|
);
|
|
253
|
-
|
|
437
|
+
p.consumeThrough(close + 1);
|
|
438
|
+
const node = call(
|
|
439
|
+
name,
|
|
440
|
+
[ident(property.decoded, sourceSpelling(property.raw, property.decoded))],
|
|
441
|
+
sourceSpelling(rawName, name)
|
|
442
|
+
);
|
|
443
|
+
if (comma !== -1)
|
|
444
|
+
setComponents(node, componentTree(tokens, property.index + 1, close, ends));
|
|
445
|
+
return node;
|
|
254
446
|
}
|
|
255
447
|
|
|
256
448
|
/** @type {Record<string, PrefixParselet>} */
|
|
257
449
|
const PREFIX = {
|
|
258
|
-
number: (_p, t) => num(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
t.
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
ident: (p, t) => {
|
|
268
|
-
if (p.matchPunct('(')) {
|
|
269
|
-
if (OPAQUE_ARG_FUNCTIONS.has(t.value.toLowerCase())) {
|
|
270
|
-
return parseOpaqueCall(p, t.value);
|
|
271
|
-
}
|
|
272
|
-
/** @type {Node[]} */
|
|
273
|
-
const args = [];
|
|
274
|
-
if (!p.isPunct(')')) {
|
|
275
|
-
args.push(p.parseExpr(0));
|
|
276
|
-
while (p.matchPunct(',')) {
|
|
277
|
-
args.push(p.parseExpr(0));
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
p.expectPunct(')');
|
|
281
|
-
return call(t.value, args);
|
|
282
|
-
}
|
|
283
|
-
const kw = foldCalcKeyword(t.value);
|
|
284
|
-
if (kw) {
|
|
285
|
-
return kw;
|
|
286
|
-
}
|
|
287
|
-
return ident(t.value);
|
|
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
|
+
);
|
|
288
458
|
},
|
|
289
|
-
|
|
459
|
+
ident: (_p, t) => {
|
|
460
|
+
const name = String(t.value);
|
|
461
|
+
return foldCalcKeyword(name) ?? ident(name, sourceSpelling(t.raw, name));
|
|
462
|
+
},
|
|
463
|
+
function: parseCall,
|
|
290
464
|
'(': (p) => {
|
|
291
465
|
const e = p.parseExpr(0);
|
|
292
466
|
p.expectPunct(')');
|
|
293
467
|
return e.type === 'Sum' ? { ...e, grouped: true } : e;
|
|
294
468
|
},
|
|
295
|
-
|
|
296
|
-
'
|
|
297
|
-
'+': (p) => p.parseExpr(UNARY_BP),
|
|
469
|
+
'-': (p) => negate(p.parseExpr(7)),
|
|
470
|
+
'+': (p) => p.parseExpr(7),
|
|
298
471
|
};
|
|
299
472
|
|
|
300
473
|
/** @type {Record<string, {lbp: number}>} */
|
|
@@ -306,19 +479,27 @@ const INFIX = {
|
|
|
306
479
|
};
|
|
307
480
|
|
|
308
481
|
/**
|
|
309
|
-
* @param {
|
|
482
|
+
* @param {CSSToken[]} tokens
|
|
483
|
+
* @param {number} start
|
|
484
|
+
* @param {number} end
|
|
485
|
+
* @param {Map<number, number>} ends
|
|
310
486
|
* @return {Node}
|
|
311
487
|
*/
|
|
312
|
-
function
|
|
313
|
-
const p = new Parser(tokens);
|
|
488
|
+
function parseRange(tokens, start, end, ends) {
|
|
489
|
+
const p = new Parser(tokens, start, end, ends);
|
|
314
490
|
const ast = p.parseExpr(0);
|
|
315
491
|
const trailing = p.peek();
|
|
316
|
-
if (trailing.type !== 'eof')
|
|
492
|
+
if (trailing.type !== 'eof')
|
|
317
493
|
throw new Error(
|
|
318
|
-
`Unexpected token "${trailing.
|
|
494
|
+
`Unexpected token "${trailing.raw}" at position ${trailing.pos}`
|
|
319
495
|
);
|
|
320
|
-
}
|
|
321
496
|
return ast;
|
|
322
497
|
}
|
|
323
498
|
|
|
499
|
+
/** @param {CSSToken[]} tokens @param {number} [start] @param {number} [end] @return {Node} */
|
|
500
|
+
function parse(tokens, start = 0, end = tokens.length) {
|
|
501
|
+
const ends = blockEnds(tokens, start, end);
|
|
502
|
+
return parseRange(tokens, start, end, ends);
|
|
503
|
+
}
|
|
504
|
+
|
|
324
505
|
export { parse };
|