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