@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.
Files changed (55) hide show
  1. package/README.md +0 -1
  2. package/package.json +1 -1
  3. package/src/declarations/border.js +1 -1
  4. package/src/index.js +7 -7
  5. package/src/rules/optimize/at-rules.js +113 -0
  6. package/src/rules/optimize/declaration-writes.js +70 -0
  7. package/src/rules/optimize/empty-rules.js +51 -0
  8. package/src/rules/optimize/expand-nesting.js +236 -0
  9. package/src/rules/optimize/factor-parents.js +343 -0
  10. package/src/rules/optimize/index.js +71 -0
  11. package/src/rules/optimize/merge-declarations.js +199 -0
  12. package/src/rules/optimize/merge-nested-rules.js +117 -0
  13. package/src/rules/optimize/merge-selectors.js +252 -0
  14. package/src/rules/optimize/nest-rules.js +202 -0
  15. package/src/rules/optimize/override-index.js +264 -0
  16. package/src/rules/optimize/prune-overridden.js +267 -0
  17. package/src/rules/optimize/selector-normalization.js +60 -0
  18. package/src/rules/optimize/specificity.js +280 -0
  19. package/src/{position-try.js → rules/position-try.js} +1 -1
  20. package/src/utilities.js +14 -0
  21. package/src/value/border.js +85 -0
  22. package/src/value/{colors.js → color/conversion.js} +3 -2
  23. package/src/value/color/functions.js +463 -0
  24. package/src/value/{light-dark.js → color/light-dark.js} +1 -1
  25. package/src/value/{color-mix.js → color/mix.js} +3 -3
  26. package/src/value/{relative-colors.js → color/relative.js} +3 -3
  27. package/src/value/color/shorten.js +93 -0
  28. package/src/value/gradients.js +1 -1
  29. package/src/value/hyphenate-limit-chars.js +72 -0
  30. package/src/value/initial-keywords.js +128 -0
  31. package/src/value/lengths.js +55 -0
  32. package/src/value/math/evaluate.js +36 -0
  33. package/src/value/math/fold.js +315 -0
  34. package/src/value/math/format.js +230 -0
  35. package/src/value/math/functions.js +302 -0
  36. package/src/value/math/index.js +219 -0
  37. package/src/value/math/parser.js +301 -0
  38. package/src/value/math/property-ranges.js +174 -0
  39. package/src/value/math/rewrite.js +222 -0
  40. package/src/value/math/tokenizer.js +160 -0
  41. package/src/value/math/unit-totals.js +194 -0
  42. package/src/value/minify.js +127 -1669
  43. package/src/value/numbers.js +28 -0
  44. package/src/value/positions.js +152 -0
  45. package/src/value/property-optimizations.js +485 -0
  46. package/src/value/segments.js +74 -0
  47. package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
  48. package/src/value/{svg-markup.js → svg/markup.js} +1 -1
  49. package/src/value/time.js +75 -0
  50. package/src/value/urls.js +242 -0
  51. package/src/value/whitespace.js +88 -0
  52. package/src/rules/optimize.js +0 -2314
  53. package/src/value/math-expression.js +0 -1156
  54. package/src/value/math.js +0 -886
  55. /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
+ };