@thejaredwilcurt/csslop 0.0.49 → 0.0.50
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 +0 -1
- package/package.json +1 -1
- package/src/declarations/border.js +1 -1
- package/src/index.js +7 -7
- package/src/rules/optimize/at-rules.js +113 -0
- package/src/rules/optimize/declaration-writes.js +70 -0
- package/src/rules/optimize/empty-rules.js +51 -0
- package/src/rules/optimize/expand-nesting.js +236 -0
- package/src/rules/optimize/factor-parents.js +343 -0
- package/src/rules/optimize/index.js +71 -0
- package/src/rules/optimize/merge-declarations.js +199 -0
- package/src/rules/optimize/merge-nested-rules.js +117 -0
- package/src/rules/optimize/merge-selectors.js +252 -0
- package/src/rules/optimize/nest-rules.js +202 -0
- package/src/rules/optimize/override-index.js +264 -0
- package/src/rules/optimize/prune-overridden.js +267 -0
- package/src/rules/optimize/selector-normalization.js +60 -0
- package/src/rules/optimize/specificity.js +280 -0
- package/src/{position-try.js → rules/position-try.js} +1 -1
- package/src/utilities.js +14 -0
- package/src/value/border.js +85 -0
- package/src/value/{colors.js → color/conversion.js} +3 -2
- package/src/value/color/functions.js +463 -0
- package/src/value/{light-dark.js → color/light-dark.js} +1 -1
- package/src/value/{color-mix.js → color/mix.js} +3 -3
- package/src/value/{relative-colors.js → color/relative.js} +3 -3
- package/src/value/color/shorten.js +93 -0
- package/src/value/gradients.js +1 -1
- package/src/value/hyphenate-limit-chars.js +72 -0
- package/src/value/initial-keywords.js +128 -0
- package/src/value/lengths.js +55 -0
- package/src/value/math/evaluate.js +36 -0
- package/src/value/math/fold.js +315 -0
- package/src/value/math/format.js +230 -0
- package/src/value/math/functions.js +302 -0
- package/src/value/math/index.js +219 -0
- package/src/value/math/parser.js +301 -0
- package/src/value/math/property-ranges.js +174 -0
- package/src/value/math/rewrite.js +222 -0
- package/src/value/math/tokenizer.js +160 -0
- package/src/value/math/unit-totals.js +194 -0
- package/src/value/minify.js +127 -1669
- package/src/value/numbers.js +28 -0
- package/src/value/positions.js +152 -0
- package/src/value/property-optimizations.js +485 -0
- package/src/value/segments.js +74 -0
- package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
- package/src/value/{svg-markup.js → svg/markup.js} +1 -1
- package/src/value/time.js +75 -0
- package/src/value/urls.js +242 -0
- package/src/value/whitespace.js +88 -0
- package/src/rules/optimize.js +0 -2314
- package/src/value/math-expression.js +0 -1156
- package/src/value/math.js +0 -886
- /package/src/value/{named-colors.js → color/named.js} +0 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Simplifies CSS math functions (calc, min, max) by folding constant expressions, flattening nested calcs, and converting absolute lengths to pixels.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
convertAbsoluteLengthToPx,
|
|
7
|
+
formatResolvedNumber,
|
|
8
|
+
roundCompactNumber
|
|
9
|
+
} from '../shared.js';
|
|
10
|
+
|
|
11
|
+
import { simplifyCalc } from './evaluate.js';
|
|
12
|
+
import {
|
|
13
|
+
foldStandaloneCalc,
|
|
14
|
+
resolveCalcExpression
|
|
15
|
+
} from './fold.js';
|
|
16
|
+
import { MATH_FUNCTION_NAMES } from './functions.js';
|
|
17
|
+
import { clampResolvedValueToPropertyRange } from './property-ranges.js';
|
|
18
|
+
import {
|
|
19
|
+
convertReciprocalMultiplicationToDivision,
|
|
20
|
+
rewriteMathFunctionContents,
|
|
21
|
+
shortenDecimalNumbers
|
|
22
|
+
} from './rewrite.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Characters that start a token able to sit directly against a value that
|
|
26
|
+
* precedes them, so no whitespace is needed to keep the two tokens apart:
|
|
27
|
+
* whitespace already separates, and openers like `#`, `!`, or any bracket or
|
|
28
|
+
* punctuation mark their own token's start. Mirrors the tokenizer's notion of
|
|
29
|
+
* tokens marking their own start, with `/` on top since it delimits a size
|
|
30
|
+
* from a position in layered shorthands.
|
|
31
|
+
*
|
|
32
|
+
* @type {Set<string>}
|
|
33
|
+
*/
|
|
34
|
+
const SELF_DELIMITING_FOLLOWERS = new Set([
|
|
35
|
+
' ',
|
|
36
|
+
'\t',
|
|
37
|
+
'\n',
|
|
38
|
+
'\r',
|
|
39
|
+
'\f',
|
|
40
|
+
'#',
|
|
41
|
+
'!',
|
|
42
|
+
'"',
|
|
43
|
+
'\'',
|
|
44
|
+
'(',
|
|
45
|
+
')',
|
|
46
|
+
'[',
|
|
47
|
+
']',
|
|
48
|
+
'{',
|
|
49
|
+
'}',
|
|
50
|
+
',',
|
|
51
|
+
':',
|
|
52
|
+
';',
|
|
53
|
+
'/'
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reports whether a math function's replacement glues itself to the character
|
|
58
|
+
* following the matched function, merging two values into one token. A
|
|
59
|
+
* replacement that keeps its function form ends with a parenthesis, which
|
|
60
|
+
* closes itself; only a bare resolved value needs the whitespace that the
|
|
61
|
+
* parenthesis padding removal took away.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} replacement The string the math function resolved to.
|
|
64
|
+
* @param {string} followingCharacter The character directly after the matched function.
|
|
65
|
+
* @return {boolean} Whether a separating space must be restored.
|
|
66
|
+
*/
|
|
67
|
+
function mergesWithFollowingCharacter (replacement, followingCharacter) {
|
|
68
|
+
if (replacement.endsWith(')') || !followingCharacter) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return !SELF_DELIMITING_FOLLOWERS.has(followingCharacter);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Wraps a math-function replacer so a function that dissolves into a bare
|
|
76
|
+
* value does not lose the whitespace separating it from the value behind it.
|
|
77
|
+
* The values were once told apart by where the function's parentheses sat, so
|
|
78
|
+
* when the parentheses go, the separator has to stay.
|
|
79
|
+
*
|
|
80
|
+
* @param {function(string, ...(string|number|undefined)): string} replacer The replacement callback for `String.prototype.replace`.
|
|
81
|
+
* @return {function(string, ...(string|number|undefined)): string} A replacer that keeps the trailing separator intact.
|
|
82
|
+
*/
|
|
83
|
+
function keepSeparatorAfterDissolvedFunction (replacer) {
|
|
84
|
+
return (match, ...rest) => {
|
|
85
|
+
const replacement = replacer(match, ...rest);
|
|
86
|
+
// A replace callback's trailing arguments are the match offset and the full string
|
|
87
|
+
const fullText = rest[rest.length - 1];
|
|
88
|
+
const matchOffset = rest[rest.length - 2];
|
|
89
|
+
const followingCharacter = fullText[matchOffset + match.length];
|
|
90
|
+
if (mergesWithFollowingCharacter(replacement, followingCharacter)) {
|
|
91
|
+
return replacement + ' ';
|
|
92
|
+
}
|
|
93
|
+
return replacement;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Matches a math function other than `calc()` whose arguments hold no further
|
|
99
|
+
* parentheses, so that the whole of the function is matched. A `calc()` is
|
|
100
|
+
* left to its own pass, which weighs what the calculation resolves to against
|
|
101
|
+
* the calculation itself.
|
|
102
|
+
*
|
|
103
|
+
* @type {RegExp}
|
|
104
|
+
*/
|
|
105
|
+
const RESOLVABLE_MATH_FUNCTION = new RegExp(
|
|
106
|
+
'\\b(?:' +
|
|
107
|
+
[...MATH_FUNCTION_NAMES].filter((name) => {
|
|
108
|
+
return name !== 'calc';
|
|
109
|
+
}).join('|') +
|
|
110
|
+
')\\([^()]+\\)',
|
|
111
|
+
'gi'
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Runs one pass of the math simplifications over a value: the rewrites that
|
|
116
|
+
* state a calculation more compactly, and the resolutions that replace a math
|
|
117
|
+
* function with the value it works out to.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} value The CSS value string containing math functions to simplify.
|
|
120
|
+
* @param {string} property The CSS property name, whose range every resolved result is clamped into.
|
|
121
|
+
* @return {string} The value with its math functions simplified where possible.
|
|
122
|
+
*/
|
|
123
|
+
function simplifyMathFunctionsOnce (value, property) {
|
|
124
|
+
let result = rewriteMathFunctionContents(shortenDecimalNumbers, value);
|
|
125
|
+
result = rewriteMathFunctionContents(convertReciprocalMultiplicationToDivision, result);
|
|
126
|
+
|
|
127
|
+
// Unwrap calc(1 / (1 / x)) → x (double-reciprocal identity)
|
|
128
|
+
result = result.replace(/calc\(\s*1\s*\/\s*\(\s*1\s*\/\s*([^()]+)\s*\)\s*\)/gi, keepSeparatorAfterDissolvedFunction((match, inner) => {
|
|
129
|
+
return inner.trim();
|
|
130
|
+
}));
|
|
131
|
+
// Flatten calc(calc(a) ± b) → calc(a ± b)
|
|
132
|
+
result = result.replace(/calc\(\s*calc\(([^()]+)\)\s*([+-])\s*([^()]+)\s*\)/gi, keepSeparatorAfterDissolvedFunction((match, inner, operator, tail) => {
|
|
133
|
+
return 'calc(' + inner + ' ' + operator + ' ' + tail + ')';
|
|
134
|
+
}));
|
|
135
|
+
// Unwrap calc(calc(x)) → calc(x)
|
|
136
|
+
result = result.replace(/calc\(\s*calc\(([^()]+)\)\s*\)/gi, keepSeparatorAfterDissolvedFunction((match, inner) => {
|
|
137
|
+
return 'calc(' + inner + ')';
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
// Resolve every math function that compares or rounds its arguments
|
|
141
|
+
result = result.replace(RESOLVABLE_MATH_FUNCTION, keepSeparatorAfterDissolvedFunction((match) => {
|
|
142
|
+
const simplified = simplifyCalc(match);
|
|
143
|
+
if (simplified === null) {
|
|
144
|
+
return match;
|
|
145
|
+
}
|
|
146
|
+
return clampResolvedValueToPropertyRange(simplified, property);
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
// Simplify calc() expressions using constant folding and expression resolution
|
|
150
|
+
result = result.replace(/calc\(([^()]+)\)/gi, keepSeparatorAfterDissolvedFunction((match, inner) => {
|
|
151
|
+
// Collapse whitespace inside calc expression
|
|
152
|
+
const compactInner = inner.replace(/\s+/g, ' ').trim();
|
|
153
|
+
const resolved = resolveCalcExpression(match, compactInner);
|
|
154
|
+
return clampResolvedValueToPropertyRange(resolved, property);
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Simplifies every math function within a CSS value string, resolving the ones
|
|
162
|
+
* whose operands are all known and stating the rest as compactly as they can
|
|
163
|
+
* be written.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} value The CSS value string containing math functions to simplify.
|
|
166
|
+
* @param {string} property The CSS property name, whose range every resolved result is clamped into.
|
|
167
|
+
* @param {string} originalValue The value as it was authored, which tells whether a result was ever part of a calculation.
|
|
168
|
+
* @return {string} The value with math functions simplified where possible.
|
|
169
|
+
*/
|
|
170
|
+
function normalizeMathFunctions (value, property, originalValue = '') {
|
|
171
|
+
let result = value;
|
|
172
|
+
let previous;
|
|
173
|
+
|
|
174
|
+
do {
|
|
175
|
+
previous = result;
|
|
176
|
+
// Resolving a math function leaves a plain value where it stood, which can
|
|
177
|
+
// bring the function it was nested in within reach of the next pass, so
|
|
178
|
+
// the value is simplified until it settles
|
|
179
|
+
result = simplifyMathFunctionsOnce(result, property);
|
|
180
|
+
} while (result !== previous);
|
|
181
|
+
|
|
182
|
+
// When calc() folded to an absolute-length unit (pt, pc, in, cm, mm, q), convert to pixels
|
|
183
|
+
if (originalValue.includes('calc(') && /^-?(?:\d+|\d*\.\d+)(pt|pc|in|cm|mm|q)$/i.test(result)) {
|
|
184
|
+
// Extract the absolute-length unit from the folded result
|
|
185
|
+
const [, unit] = result.match(/^-?(?:\d+|\d*\.\d+)(pt|pc|in|cm|mm|q)$/i);
|
|
186
|
+
const numeric = parseFloat(result);
|
|
187
|
+
const pxValue = convertAbsoluteLengthToPx(numeric, unit);
|
|
188
|
+
if (pxValue !== null) {
|
|
189
|
+
result = roundCompactNumber(pxValue) + 'px';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Round a result whose decimal places run past the budget a resolved number is written within
|
|
194
|
+
result = result.replace(/(-?(?:\d+|\d*\.\d+)\.\d{4,})([a-z%]+)/gi, (match, number, unit) => {
|
|
195
|
+
return formatResolvedNumber(number) + unit;
|
|
196
|
+
});
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Simplifies a value that is a single `calc()`, holding whatever it resolves
|
|
202
|
+
* to inside the range the property it is declared on accepts.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} value The CSS value string that may be a standalone calc() expression.
|
|
205
|
+
* @param {string} property The CSS property the value is declared on.
|
|
206
|
+
* @return {string} The simplified value, or the original value if simplification is not applicable.
|
|
207
|
+
*/
|
|
208
|
+
function simplifyStandaloneCalc (value, property = '') {
|
|
209
|
+
// Check if value starts with calc( and ends with )
|
|
210
|
+
if (!/^calc\(/i.test(value) || !value.endsWith(')')) {
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
return clampResolvedValueToPropertyRange(foldStandaloneCalc(value), property);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export {
|
|
217
|
+
normalizeMathFunctions,
|
|
218
|
+
simplifyStandaloneCalc
|
|
219
|
+
};
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Reads the tokens of a CSS math function as sums, products, and operands, resolving each into the unit totals it states.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { MATH_FUNCTION_EVALUATORS } from './functions.js';
|
|
6
|
+
import {
|
|
7
|
+
addUnitTotals,
|
|
8
|
+
createUnitTotals,
|
|
9
|
+
divideUnitTotals,
|
|
10
|
+
multiplyUnitTotals,
|
|
11
|
+
scaleUnitTotals,
|
|
12
|
+
UNITLESS
|
|
13
|
+
} from './unit-totals.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The numeric constants a calculation may name instead of writing out, as the
|
|
17
|
+
* keywords section of the CSS Values specification defines them:
|
|
18
|
+
* https://drafts.csswg.org/css-values/#calc-keywords.
|
|
19
|
+
*
|
|
20
|
+
* @type {Map<string, number>}
|
|
21
|
+
*/
|
|
22
|
+
const NUMERIC_CONSTANTS = new Map([
|
|
23
|
+
['e', Math.E],
|
|
24
|
+
['infinity', Infinity],
|
|
25
|
+
['nan', NaN],
|
|
26
|
+
['pi', Math.PI]
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The deepest a calculation may nest one set of parentheses inside another
|
|
31
|
+
* before the evaluator gives up on it. Every level of nesting is a level of
|
|
32
|
+
* recursion, and a stylesheet states nothing at a depth anywhere near this.
|
|
33
|
+
*
|
|
34
|
+
* @type {number}
|
|
35
|
+
*/
|
|
36
|
+
const MAXIMUM_NESTING_DEPTH = 32;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {object} ExpressionReader
|
|
40
|
+
* @property {Array} tokens The tokens of the math function, in the order they are written.
|
|
41
|
+
* @property {number} index The token the parser reads next.
|
|
42
|
+
* @property {number} depth How many sets of parentheses the parser is reading inside of.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reads the token the parser stands on, or one of the tokens after it.
|
|
47
|
+
*
|
|
48
|
+
* @param {ExpressionReader} reader The token reader.
|
|
49
|
+
* @param {number} offset How many tokens past the current one to read.
|
|
50
|
+
* @return {object|undefined} The token read, or undefined past the end of the expression.
|
|
51
|
+
*/
|
|
52
|
+
function peekToken (reader, offset = 0) {
|
|
53
|
+
return reader.tokens[reader.index + offset];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reports whether the parser stands on an operator that adds or subtracts what
|
|
58
|
+
* follows it from what precedes it. A sign written directly against its number
|
|
59
|
+
* is part of that number rather than an operator, so a value written with one
|
|
60
|
+
* sits against the value before it with no operator between them, which is not
|
|
61
|
+
* a calculation any browser reads.
|
|
62
|
+
*
|
|
63
|
+
* @param {ExpressionReader} reader The token reader.
|
|
64
|
+
* @return {boolean} Whether an addition or a subtraction follows.
|
|
65
|
+
*/
|
|
66
|
+
function readsAdditiveOperator (reader) {
|
|
67
|
+
const token = peekToken(reader);
|
|
68
|
+
const isAdditive = token?.type === 'operator' && (token.operator === '+' || token.operator === '-');
|
|
69
|
+
if (!isAdditive) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const operandToken = peekToken(reader, 1);
|
|
73
|
+
const signsItsNumber = operandToken?.type === 'number' && !operandToken.precededBySpace;
|
|
74
|
+
return !signsItsNumber;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reads a sum: the terms a calculation adds together, in the order they are
|
|
79
|
+
* written.
|
|
80
|
+
*
|
|
81
|
+
* @param {ExpressionReader} reader The token reader.
|
|
82
|
+
* @return {Map|null} The unit totals the sum states, or null when it cannot be resolved.
|
|
83
|
+
*/
|
|
84
|
+
function parseSum (reader) {
|
|
85
|
+
let value = parseProduct(reader);
|
|
86
|
+
if (!value) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
while (readsAdditiveOperator(reader)) {
|
|
90
|
+
const subtracts = peekToken(reader).operator === '-';
|
|
91
|
+
reader.index++;
|
|
92
|
+
const term = parseProduct(reader);
|
|
93
|
+
if (!term) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
value = addUnitTotals(value, term, subtracts ? -1 : 1);
|
|
97
|
+
if (!value) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reads a product: the operands a calculation multiplies and divides, which
|
|
106
|
+
* bind more tightly than the terms of a sum do.
|
|
107
|
+
*
|
|
108
|
+
* @param {ExpressionReader} reader The token reader.
|
|
109
|
+
* @return {Map|null} The unit totals the product states, or null when it cannot be resolved.
|
|
110
|
+
*/
|
|
111
|
+
function parseProduct (reader) {
|
|
112
|
+
let value = parseOperand(reader);
|
|
113
|
+
if (!value) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
let token = peekToken(reader);
|
|
117
|
+
while (token?.type === 'operator' && (token.operator === '*' || token.operator === '/')) {
|
|
118
|
+
reader.index++;
|
|
119
|
+
const operand = parseOperand(reader);
|
|
120
|
+
if (!operand) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (token.operator === '*') {
|
|
124
|
+
value = multiplyUnitTotals(value, operand);
|
|
125
|
+
} else {
|
|
126
|
+
value = divideUnitTotals(value, operand);
|
|
127
|
+
}
|
|
128
|
+
if (!value) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
token = peekToken(reader);
|
|
132
|
+
}
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Reads the number a sign is written against, which is the only place a
|
|
138
|
+
* calculation states a sign: CSS has no operator that negates what follows it,
|
|
139
|
+
* so a sign standing apart from its number, or a second sign written in front
|
|
140
|
+
* of the first, is not a calculation any browser reads.
|
|
141
|
+
*
|
|
142
|
+
* @param {ExpressionReader} reader The token reader.
|
|
143
|
+
* @return {Map|null} The unit totals the signed number states, or null when the sign signs nothing.
|
|
144
|
+
*/
|
|
145
|
+
function parseSignedNumber (reader) {
|
|
146
|
+
const signToken = peekToken(reader);
|
|
147
|
+
const signedToken = peekToken(reader, 1);
|
|
148
|
+
const signsItsValue = (
|
|
149
|
+
(signedToken?.type === 'number' || signedToken?.type === 'identifier') &&
|
|
150
|
+
!signedToken.precededBySpace
|
|
151
|
+
);
|
|
152
|
+
if (!signsItsValue) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
reader.index++;
|
|
156
|
+
const value = parseOperand(reader);
|
|
157
|
+
if (!value) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (signToken.operator === '-') {
|
|
161
|
+
return scaleUnitTotals(value, -1);
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Reads a single operand: a signed number, a number, a numeric constant, a
|
|
168
|
+
* parenthesized sum, or another math function.
|
|
169
|
+
*
|
|
170
|
+
* @param {ExpressionReader} reader The token reader.
|
|
171
|
+
* @return {Map|null} The unit totals the operand states, or null when it cannot be resolved.
|
|
172
|
+
*/
|
|
173
|
+
function parseOperand (reader) {
|
|
174
|
+
const token = peekToken(reader);
|
|
175
|
+
if (!token) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
if (token.type === 'operator' && (token.operator === '+' || token.operator === '-')) {
|
|
179
|
+
return parseSignedNumber(reader);
|
|
180
|
+
}
|
|
181
|
+
if (token.type === 'number') {
|
|
182
|
+
reader.index++;
|
|
183
|
+
return createUnitTotals(token.number, token.unit, true);
|
|
184
|
+
}
|
|
185
|
+
if (token.type === 'identifier') {
|
|
186
|
+
reader.index++;
|
|
187
|
+
const constant = NUMERIC_CONSTANTS.get(token.name);
|
|
188
|
+
if (constant === undefined) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return createUnitTotals(constant, UNITLESS);
|
|
192
|
+
}
|
|
193
|
+
if (token.type === 'function') {
|
|
194
|
+
reader.index++;
|
|
195
|
+
return parseMathFunction(reader, token.name);
|
|
196
|
+
}
|
|
197
|
+
if (token.type === 'open') {
|
|
198
|
+
reader.index++;
|
|
199
|
+
return parseGroup(reader);
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Reads a sum written inside parentheses, which is read as one operand of the
|
|
206
|
+
* calculation around it.
|
|
207
|
+
*
|
|
208
|
+
* @param {ExpressionReader} reader The token reader.
|
|
209
|
+
* @return {Map|null} The unit totals the group states, or null when it cannot be resolved.
|
|
210
|
+
*/
|
|
211
|
+
function parseGroup (reader) {
|
|
212
|
+
if (reader.depth >= MAXIMUM_NESTING_DEPTH) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
reader.depth++;
|
|
216
|
+
const value = parseSum(reader);
|
|
217
|
+
reader.depth--;
|
|
218
|
+
if (!value || peekToken(reader)?.type !== 'close') {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
reader.index++;
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Reads one argument of a math function, which is either a sum or a lone
|
|
227
|
+
* keyword such as the rounding strategy of `round()` or the open end of a
|
|
228
|
+
* `clamp()` range.
|
|
229
|
+
*
|
|
230
|
+
* @param {ExpressionReader} reader The token reader.
|
|
231
|
+
* @return {object|null} The argument read, or null when it cannot be resolved.
|
|
232
|
+
*/
|
|
233
|
+
function parseFunctionArgument (reader) {
|
|
234
|
+
const token = peekToken(reader);
|
|
235
|
+
const followingToken = peekToken(reader, 1);
|
|
236
|
+
const standsAlone = followingToken?.type === 'comma' || followingToken?.type === 'close';
|
|
237
|
+
const isKeyword = token?.type === 'identifier' && standsAlone && !NUMERIC_CONSTANTS.has(token.name);
|
|
238
|
+
if (isKeyword) {
|
|
239
|
+
reader.index++;
|
|
240
|
+
return { keyword: token.name, value: null };
|
|
241
|
+
}
|
|
242
|
+
const value = parseSum(reader);
|
|
243
|
+
if (!value) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return { keyword: null, value };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Reads the comma-separated arguments a math function is called with, up to
|
|
251
|
+
* the parenthesis that closes the call.
|
|
252
|
+
*
|
|
253
|
+
* @param {ExpressionReader} reader The token reader.
|
|
254
|
+
* @return {Array|null} The arguments read, or null when the call cannot be resolved.
|
|
255
|
+
*/
|
|
256
|
+
function parseFunctionArguments (reader) {
|
|
257
|
+
const argumentList = [];
|
|
258
|
+
while (reader.index < reader.tokens.length) {
|
|
259
|
+
const argument = parseFunctionArgument(reader);
|
|
260
|
+
if (!argument) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
argumentList.push(argument);
|
|
264
|
+
const token = peekToken(reader);
|
|
265
|
+
if (token?.type === 'close') {
|
|
266
|
+
reader.index++;
|
|
267
|
+
return argumentList;
|
|
268
|
+
}
|
|
269
|
+
if (token?.type !== 'comma') {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
reader.index++;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Reads the arguments of a math function and works out the value the function
|
|
279
|
+
* resolves to.
|
|
280
|
+
*
|
|
281
|
+
* @param {ExpressionReader} reader The token reader.
|
|
282
|
+
* @param {string} name The lowercased name of the function being called.
|
|
283
|
+
* @return {Map|null} The unit totals the function resolves to, or null when it cannot be resolved.
|
|
284
|
+
*/
|
|
285
|
+
function parseMathFunction (reader, name) {
|
|
286
|
+
const resolveFunction = MATH_FUNCTION_EVALUATORS.get(name);
|
|
287
|
+
if (!resolveFunction || reader.depth >= MAXIMUM_NESTING_DEPTH) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
reader.depth++;
|
|
291
|
+
const argumentList = parseFunctionArguments(reader);
|
|
292
|
+
reader.depth--;
|
|
293
|
+
if (!argumentList) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return resolveFunction(argumentList);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export {
|
|
300
|
+
parseSum
|
|
301
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Holds what a math function resolved to inside the range of values the property it is declared on accepts.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { formatResolvedNumber } from '../shared.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The properties whose values every CSS specification defines as
|
|
9
|
+
* non-negative, so that a math function declared on one of them has its
|
|
10
|
+
* result clamped rather than read as written.
|
|
11
|
+
*
|
|
12
|
+
* @type {Set<string>}
|
|
13
|
+
*/
|
|
14
|
+
const NON_NEGATIVE_PROPERTIES = new Set([
|
|
15
|
+
'animation-duration',
|
|
16
|
+
'animation-iteration-count',
|
|
17
|
+
'background-size',
|
|
18
|
+
'block-size',
|
|
19
|
+
'border-block-end-width',
|
|
20
|
+
'border-block-start-width',
|
|
21
|
+
'border-block-width',
|
|
22
|
+
'border-bottom-left-radius',
|
|
23
|
+
'border-bottom-right-radius',
|
|
24
|
+
'border-bottom-width',
|
|
25
|
+
'border-end-end-radius',
|
|
26
|
+
'border-end-start-radius',
|
|
27
|
+
'border-image-outset',
|
|
28
|
+
'border-image-width',
|
|
29
|
+
'border-inline-end-width',
|
|
30
|
+
'border-inline-start-width',
|
|
31
|
+
'border-inline-width',
|
|
32
|
+
'border-left-width',
|
|
33
|
+
'border-radius',
|
|
34
|
+
'border-right-width',
|
|
35
|
+
'border-start-end-radius',
|
|
36
|
+
'border-start-start-radius',
|
|
37
|
+
'border-top-left-radius',
|
|
38
|
+
'border-top-right-radius',
|
|
39
|
+
'border-top-width',
|
|
40
|
+
'border-width',
|
|
41
|
+
'column-count',
|
|
42
|
+
'column-gap',
|
|
43
|
+
'column-rule-width',
|
|
44
|
+
'column-width',
|
|
45
|
+
'flex-basis',
|
|
46
|
+
'flex-grow',
|
|
47
|
+
'flex-shrink',
|
|
48
|
+
'font-size',
|
|
49
|
+
'gap',
|
|
50
|
+
'height',
|
|
51
|
+
'inline-size',
|
|
52
|
+
'line-height',
|
|
53
|
+
'mask-size',
|
|
54
|
+
'max-block-size',
|
|
55
|
+
'max-height',
|
|
56
|
+
'max-inline-size',
|
|
57
|
+
'max-width',
|
|
58
|
+
'min-block-size',
|
|
59
|
+
'min-height',
|
|
60
|
+
'min-inline-size',
|
|
61
|
+
'min-width',
|
|
62
|
+
'outline-width',
|
|
63
|
+
'padding',
|
|
64
|
+
'padding-block',
|
|
65
|
+
'padding-block-end',
|
|
66
|
+
'padding-block-start',
|
|
67
|
+
'padding-bottom',
|
|
68
|
+
'padding-inline',
|
|
69
|
+
'padding-inline-end',
|
|
70
|
+
'padding-inline-start',
|
|
71
|
+
'padding-left',
|
|
72
|
+
'padding-right',
|
|
73
|
+
'padding-top',
|
|
74
|
+
'perspective',
|
|
75
|
+
'row-gap',
|
|
76
|
+
'shape-margin',
|
|
77
|
+
'stroke-width',
|
|
78
|
+
'tab-size',
|
|
79
|
+
'transition-duration',
|
|
80
|
+
'width'
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Properties whose computed math values must be integers.
|
|
85
|
+
*
|
|
86
|
+
* @type {Set<string>}
|
|
87
|
+
*/
|
|
88
|
+
const INTEGER_VALUE_PROPERTIES = new Set([
|
|
89
|
+
'column-count',
|
|
90
|
+
'order',
|
|
91
|
+
'orphans',
|
|
92
|
+
'widows',
|
|
93
|
+
'z-index'
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The inclusive bounds that CSS applies to numeric property values after
|
|
98
|
+
* resolving a math expression.
|
|
99
|
+
*
|
|
100
|
+
* @type {Map<string, {minimum?: number, maximum?: number}>}
|
|
101
|
+
*/
|
|
102
|
+
const NUMERIC_PROPERTY_RANGES = new Map([
|
|
103
|
+
['column-count', { minimum: 1 }],
|
|
104
|
+
['opacity', { minimum: 0, maximum: 1 }]
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Matches a value written as a single negative number, with or without the
|
|
109
|
+
* unit or percent sign that follows its digits.
|
|
110
|
+
*
|
|
111
|
+
* @type {RegExp}
|
|
112
|
+
*/
|
|
113
|
+
const NEGATIVE_NUMBER_VALUE = /^-(?:\d+|\d*\.\d+)(?:[a-z]+|%)?$/i;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Matches a resolved unitless number whose value can be rounded or clamped by
|
|
117
|
+
* a property's numeric range.
|
|
118
|
+
*
|
|
119
|
+
* @type {RegExp}
|
|
120
|
+
*/
|
|
121
|
+
const UNIT_LESS_NUMBER_VALUE = /^[+-]?(?:\d+|\d*\.\d+)$/;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Clamps what a math function resolved to into the range its property
|
|
125
|
+
* accepts. A negative length written out directly makes the declaration
|
|
126
|
+
* invalid and the CSS engine throws it away, but a math function is allowed
|
|
127
|
+
* to resolve to one: its result is clamped to the nearest value the property
|
|
128
|
+
* does take, which on a property that only accepts non-negative values is
|
|
129
|
+
* zero, as the range checking section of the CSS Values specification
|
|
130
|
+
* describes: https://drafts.csswg.org/css-values/#calc-range.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} resolvedValue The value the math function resolved to.
|
|
133
|
+
* @param {string} property The property the math function is declared on.
|
|
134
|
+
* @return {string} The resolved value, held inside the property's range.
|
|
135
|
+
*/
|
|
136
|
+
function clampResolvedValueToPropertyRange (resolvedValue, property) {
|
|
137
|
+
const normalizedProperty = String(property).toLowerCase();
|
|
138
|
+
const acceptsNegativeValues = !NON_NEGATIVE_PROPERTIES.has(normalizedProperty);
|
|
139
|
+
const hasPropertyRange = NUMERIC_PROPERTY_RANGES.has(normalizedProperty);
|
|
140
|
+
const range = NUMERIC_PROPERTY_RANGES.get(normalizedProperty) || {};
|
|
141
|
+
const isNegativeNonNegativeValue = !acceptsNegativeValues && NEGATIVE_NUMBER_VALUE.test(resolvedValue);
|
|
142
|
+
if (isNegativeNonNegativeValue) {
|
|
143
|
+
const isUnitlessNumber = UNIT_LESS_NUMBER_VALUE.test(resolvedValue);
|
|
144
|
+
if (!isUnitlessNumber) {
|
|
145
|
+
return formatResolvedNumber(range.minimum === undefined ? 0 : range.minimum);
|
|
146
|
+
}
|
|
147
|
+
if (!hasPropertyRange && !INTEGER_VALUE_PROPERTIES.has(normalizedProperty)) {
|
|
148
|
+
return '0';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!hasPropertyRange && !INTEGER_VALUE_PROPERTIES.has(normalizedProperty)) {
|
|
152
|
+
return resolvedValue;
|
|
153
|
+
}
|
|
154
|
+
if (!UNIT_LESS_NUMBER_VALUE.test(resolvedValue)) {
|
|
155
|
+
return resolvedValue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const shouldRoundToInteger = INTEGER_VALUE_PROPERTIES.has(normalizedProperty);
|
|
159
|
+
let numericValue = Number(resolvedValue);
|
|
160
|
+
if (shouldRoundToInteger) {
|
|
161
|
+
numericValue = Math.round(numericValue);
|
|
162
|
+
}
|
|
163
|
+
if (range.minimum !== undefined) {
|
|
164
|
+
numericValue = Math.max(range.minimum, numericValue);
|
|
165
|
+
}
|
|
166
|
+
if (range.maximum !== undefined) {
|
|
167
|
+
numericValue = Math.min(range.maximum, numericValue);
|
|
168
|
+
}
|
|
169
|
+
return formatResolvedNumber(numericValue);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export {
|
|
173
|
+
clampResolvedValueToPropertyRange
|
|
174
|
+
};
|