postcss-calc 11.1.2 → 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 -12
- 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/functions.js +364 -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 +383 -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 +574 -215
- 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 +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/functions.d.ts +40 -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,138 @@
|
|
|
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
|
-
|
|
6
|
-
|
|
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';
|
|
7
17
|
|
|
8
18
|
/** @typedef {import('@csstools/css-tokenizer').CSSToken} CSSToken */
|
|
9
19
|
/** @typedef {import('./node.js').Node} Node */
|
|
10
|
-
/** @typedef {
|
|
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 */
|
|
11
31
|
/**
|
|
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
|
|
32
|
+
* Immutable bounds and shared block index for one parse range.
|
|
33
|
+
* @typedef {Readonly<{tokens: CSSToken[], end: number, index: BlockIndex}>} ParseInput
|
|
21
34
|
*/
|
|
22
|
-
/** @typedef {(p: Parser, token: Token) => Node} PrefixParselet */
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
/** @param {string} value @return {value is '+' | '-' | '*' | '/'} */
|
|
37
|
+
function isOperator(value) {
|
|
38
|
+
return value === '+' || value === '-' || value === '*' || value === '/';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
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}
|
|
48
|
+
*/
|
|
49
|
+
function normalizeSourceZero(value) {
|
|
50
|
+
return value === 0 ? 0 : value;
|
|
51
|
+
}
|
|
52
|
+
|
|
32
53
|
/** @param {string} raw @param {string} decoded */
|
|
33
54
|
function sourceSpelling(raw, decoded) {
|
|
34
55
|
return raw === decoded ? undefined : raw;
|
|
35
56
|
}
|
|
36
57
|
|
|
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;
|
|
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;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
/** @
|
|
68
|
-
|
|
69
|
-
|
|
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;
|
|
70
81
|
}
|
|
82
|
+
}
|
|
71
83
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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;
|
|
102
109
|
}
|
|
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;
|
|
110
|
+
if (native[0] === CssType.EOF) break;
|
|
111
|
+
const token = normalizeToken(native, i, ws);
|
|
112
|
+
cursor.lookahead = token;
|
|
113
|
+
cursor.lookaheadNextIndex = i + 1;
|
|
122
114
|
return token;
|
|
123
115
|
}
|
|
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
|
-
}
|
|
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;
|
|
221
132
|
}
|
|
222
133
|
|
|
223
|
-
/** @param {CSSToken} t @param {boolean} ws @return {Token} */
|
|
224
|
-
function normalizeToken(t, ws) {
|
|
134
|
+
/** @param {CSSToken} t @param {number} index @param {boolean} ws @return {Token} */
|
|
135
|
+
function normalizeToken(t, index, ws) {
|
|
225
136
|
const [type, raw, pos, , detail] = t;
|
|
226
137
|
switch (type) {
|
|
227
138
|
case CssType.Number:
|
|
@@ -231,16 +142,18 @@ function normalizeToken(t, ws) {
|
|
|
231
142
|
raw,
|
|
232
143
|
pos,
|
|
233
144
|
ws,
|
|
145
|
+
index,
|
|
234
146
|
signCharacter: detail.signCharacter,
|
|
235
147
|
};
|
|
236
148
|
case CssType.Dimension: {
|
|
237
|
-
const match =
|
|
149
|
+
const match = CSS_NUMBER_PREFIX.exec(raw);
|
|
238
150
|
return {
|
|
239
151
|
type: 'dimension',
|
|
240
152
|
value: detail.value,
|
|
241
153
|
raw,
|
|
242
154
|
pos,
|
|
243
155
|
ws,
|
|
156
|
+
index,
|
|
244
157
|
unit: detail.unit,
|
|
245
158
|
rawUnit: match ? raw.slice(match[0].length) : detail.unit,
|
|
246
159
|
signCharacter: detail.signCharacter,
|
|
@@ -253,6 +166,7 @@ function normalizeToken(t, ws) {
|
|
|
253
166
|
raw,
|
|
254
167
|
pos,
|
|
255
168
|
ws,
|
|
169
|
+
index,
|
|
256
170
|
unit: '%',
|
|
257
171
|
rawUnit: '%',
|
|
258
172
|
signCharacter: detail.signCharacter,
|
|
@@ -265,18 +179,180 @@ function normalizeToken(t, ws) {
|
|
|
265
179
|
raw,
|
|
266
180
|
pos,
|
|
267
181
|
ws,
|
|
182
|
+
index,
|
|
268
183
|
};
|
|
269
184
|
case CssType.OpenParen:
|
|
185
|
+
return { type: 'punct', value: '(', raw, pos, ws, index };
|
|
270
186
|
case CssType.CloseParen:
|
|
187
|
+
return { type: 'punct', value: ')', raw, pos, ws, index };
|
|
271
188
|
case CssType.Comma:
|
|
272
|
-
return { type: 'punct', value:
|
|
189
|
+
return { type: 'punct', value: ',', raw, pos, ws, index };
|
|
273
190
|
case CssType.Delim:
|
|
274
|
-
if (
|
|
275
|
-
return {
|
|
191
|
+
if (isOperator(detail.value))
|
|
192
|
+
return {
|
|
193
|
+
type: 'punct',
|
|
194
|
+
value: detail.value,
|
|
195
|
+
raw,
|
|
196
|
+
pos,
|
|
197
|
+
ws,
|
|
198
|
+
index,
|
|
199
|
+
};
|
|
276
200
|
}
|
|
277
201
|
throw new Error(`Unexpected character "${raw[0] ?? ''}" at position ${pos}`);
|
|
278
202
|
}
|
|
279
203
|
|
|
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];
|
|
208
|
+
}
|
|
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;
|
|
218
|
+
}
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** @param {ParseInput} input @param {Cursor} cursor @return {Token} */
|
|
223
|
+
function peekToken(input, cursor) {
|
|
224
|
+
return cursor.lookahead ?? scanToken(input, cursor);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
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}
|
|
233
|
+
*/
|
|
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
|
+
}
|
|
241
|
+
|
|
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}"`);
|
|
263
|
+
}
|
|
264
|
+
return t;
|
|
265
|
+
}
|
|
266
|
+
|
|
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
|
+
);
|
|
279
|
+
}
|
|
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;
|
|
295
|
+
}
|
|
296
|
+
case '-':
|
|
297
|
+
return negate(parseExpr(input, cursor, 7, depth + 1));
|
|
298
|
+
case '+':
|
|
299
|
+
return parseExpr(input, cursor, 7, depth + 1);
|
|
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;
|
|
337
|
+
}
|
|
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;
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
return left;
|
|
354
|
+
}
|
|
355
|
+
|
|
280
356
|
/** §10.9 — case-insensitive except for NaN. @param {string} name @return {Node | null} */
|
|
281
357
|
function foldCalcKeyword(name) {
|
|
282
358
|
if (name === 'NaN' || name === '-NaN') return num(Number.NaN);
|
|
@@ -295,41 +371,45 @@ function foldCalcKeyword(name) {
|
|
|
295
371
|
|
|
296
372
|
const ADD_BP = 1;
|
|
297
373
|
const MUL_BP = 3;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const { start, close, tokens, ends } = p.functionRange();
|
|
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);
|
|
303
378
|
if (close === -1)
|
|
304
|
-
throw new Error(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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)
|
|
309
387
|
);
|
|
310
388
|
}
|
|
311
389
|
|
|
312
|
-
/** @param {
|
|
313
|
-
function requireSurroundingWs(
|
|
314
|
-
if (!token.ws || !
|
|
390
|
+
/** @param {ParseInput} input @param {Cursor} cursor @param {Token} token */
|
|
391
|
+
function requireSurroundingWs(input, cursor, token) {
|
|
392
|
+
if (!token.ws || !peekToken(input, cursor).ws)
|
|
315
393
|
throw new Error(
|
|
316
394
|
`"${token.value}" must be surrounded by whitespace at position ${token.pos}`
|
|
317
395
|
);
|
|
318
396
|
}
|
|
319
397
|
|
|
320
|
-
/** @param {
|
|
321
|
-
function parseCall(
|
|
322
|
-
const name =
|
|
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;
|
|
323
401
|
const rawName = t.raw.slice(0, -1);
|
|
324
|
-
if (name.toLowerCase() === 'var')
|
|
325
|
-
|
|
326
|
-
|
|
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);
|
|
327
406
|
/** @type {Node[]} */ const args = [];
|
|
328
|
-
if (!
|
|
329
|
-
args.push(
|
|
330
|
-
while (
|
|
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));
|
|
331
411
|
}
|
|
332
|
-
|
|
412
|
+
expectPunct(input, cursor, ')');
|
|
333
413
|
return call(name, args, sourceSpelling(rawName, name));
|
|
334
414
|
}
|
|
335
415
|
|
|
@@ -339,37 +419,6 @@ function rawTokens(tokens, start, end) {
|
|
|
339
419
|
for (let i = start; i < end; i++) raw += tokens[i][1];
|
|
340
420
|
return raw;
|
|
341
421
|
}
|
|
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
422
|
/** @param {CSSToken[]} tokens @param {number} start @param {number} end */
|
|
374
423
|
function customProperty(tokens, start, end) {
|
|
375
424
|
/** @type {CSSToken | null} */
|
|
@@ -388,88 +437,88 @@ function customProperty(tokens, start, end) {
|
|
|
388
437
|
? { decoded, raw: found[1], index: foundIndex }
|
|
389
438
|
: null;
|
|
390
439
|
}
|
|
391
|
-
/** @param {
|
|
392
|
-
function componentTree(
|
|
393
|
-
|
|
394
|
-
/** @
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
+
}
|
|
463
|
+
|
|
401
464
|
const token = tokens[i];
|
|
402
|
-
const close =
|
|
465
|
+
const close = input.index.closeOf(i, end);
|
|
403
466
|
if (close === -1) {
|
|
404
|
-
|
|
467
|
+
pushComponent(tree, token[1]);
|
|
468
|
+
i++;
|
|
405
469
|
continue;
|
|
406
470
|
}
|
|
471
|
+
|
|
407
472
|
const isMathFunction =
|
|
408
473
|
token[0] === CssType.Function &&
|
|
409
|
-
(
|
|
474
|
+
(isCalculationFunction(token[4].value) ||
|
|
410
475
|
isSupportedMathFunction(token[4].value));
|
|
411
476
|
if (isMathFunction) {
|
|
412
477
|
try {
|
|
413
|
-
|
|
478
|
+
pushComponent(tree, parseRange(input, i, close + 1));
|
|
414
479
|
} catch {
|
|
415
|
-
|
|
480
|
+
pushComponent(tree, rawTokens(tokens, i, close + 1));
|
|
416
481
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
push(componentTree(tokens, i + 1, close, ends));
|
|
420
|
-
push(tokens[close][1]);
|
|
482
|
+
i = close + 1;
|
|
483
|
+
continue;
|
|
421
484
|
}
|
|
422
|
-
|
|
485
|
+
|
|
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++;
|
|
423
494
|
}
|
|
424
|
-
return
|
|
495
|
+
return root;
|
|
425
496
|
}
|
|
426
|
-
/** @param {
|
|
427
|
-
function parseVar(
|
|
428
|
-
const {
|
|
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);
|
|
429
502
|
if (close === -1)
|
|
430
|
-
throw new Error(
|
|
431
|
-
|
|
503
|
+
throw new Error(
|
|
504
|
+
`Unclosed ${name}( at position ${eofPositionAt(input, cursor.index)}`
|
|
505
|
+
);
|
|
506
|
+
const comma = input.index.firstTopLevelComma(start, close);
|
|
432
507
|
const property = customProperty(tokens, start, comma === -1 ? close : comma);
|
|
433
508
|
if (!property)
|
|
434
509
|
throw new Error(
|
|
435
|
-
`Invalid custom property in ${name}() at position ${tokens[start]?.[2] ??
|
|
510
|
+
`Invalid custom property in ${name}() at position ${tokens[start]?.[2] ?? eofPositionAt(input, cursor.index)}`
|
|
436
511
|
);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
);
|
|
512
|
+
cursor.skipTo(close + 1);
|
|
513
|
+
/** @type {OpaqueComponent[]} */
|
|
514
|
+
const components = [
|
|
515
|
+
ident(property.decoded, sourceSpelling(property.raw, property.decoded)),
|
|
516
|
+
];
|
|
443
517
|
if (comma !== -1)
|
|
444
|
-
|
|
445
|
-
return
|
|
518
|
+
components.push(...componentTree(input, property.index + 1, close));
|
|
519
|
+
return opaqueCall(name, components, sourceSpelling(rawName, name));
|
|
446
520
|
}
|
|
447
521
|
|
|
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
522
|
/** @type {Record<string, {lbp: number}>} */
|
|
474
523
|
const INFIX = {
|
|
475
524
|
'+': { lbp: ADD_BP },
|
|
@@ -479,16 +528,23 @@ const INFIX = {
|
|
|
479
528
|
};
|
|
480
529
|
|
|
481
530
|
/**
|
|
482
|
-
* @param {
|
|
531
|
+
* @param {ParseInput} input
|
|
483
532
|
* @param {number} start
|
|
484
533
|
* @param {number} end
|
|
485
|
-
* @param {Map<number, number>} ends
|
|
486
534
|
* @return {Node}
|
|
487
535
|
*/
|
|
488
|
-
function parseRange(
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
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);
|
|
492
548
|
if (trailing.type !== 'eof')
|
|
493
549
|
throw new Error(
|
|
494
550
|
`Unexpected token "${trailing.raw}" at position ${trailing.pos}`
|
|
@@ -496,10 +552,9 @@ function parseRange(tokens, start, end, ends) {
|
|
|
496
552
|
return ast;
|
|
497
553
|
}
|
|
498
554
|
|
|
499
|
-
/** @param {CSSToken[]} tokens @param {number}
|
|
500
|
-
function parse(tokens, start
|
|
501
|
-
|
|
502
|
-
return parseRange(tokens, start, end, ends);
|
|
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);
|
|
503
558
|
}
|
|
504
559
|
|
|
505
560
|
export { parse };
|