@thejaredwilcurt/csslop 0.0.48 → 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 (61) hide show
  1. package/README.md +3 -4
  2. package/package.json +3 -3
  3. package/src/declarations/border.js +1 -1
  4. package/src/declarations/css-wide-keywords.js +37 -1
  5. package/src/declarations/fold.js +13 -3
  6. package/src/declarations/process.js +60 -7
  7. package/src/index.js +12 -7
  8. package/src/parser/source-search.js +6 -0
  9. package/src/rules/optimize/at-rules.js +113 -0
  10. package/src/rules/optimize/declaration-writes.js +70 -0
  11. package/src/rules/optimize/empty-rules.js +51 -0
  12. package/src/rules/optimize/expand-nesting.js +236 -0
  13. package/src/rules/optimize/factor-parents.js +343 -0
  14. package/src/rules/optimize/index.js +71 -0
  15. package/src/rules/optimize/merge-declarations.js +199 -0
  16. package/src/rules/optimize/merge-nested-rules.js +117 -0
  17. package/src/rules/optimize/merge-selectors.js +252 -0
  18. package/src/rules/optimize/nest-rules.js +202 -0
  19. package/src/rules/optimize/override-index.js +264 -0
  20. package/src/rules/optimize/prune-overridden.js +267 -0
  21. package/src/rules/optimize/selector-normalization.js +60 -0
  22. package/src/rules/optimize/specificity.js +280 -0
  23. package/src/{position-try.js → rules/position-try.js} +1 -1
  24. package/src/rules/selectors.js +9 -0
  25. package/src/rules/stringify.js +138 -76
  26. package/src/utilities.js +9 -8
  27. package/src/value/border.js +85 -0
  28. package/src/value/{colors.js → color/conversion.js} +3 -2
  29. package/src/value/color/functions.js +463 -0
  30. package/src/value/{light-dark.js → color/light-dark.js} +1 -1
  31. package/src/value/{color-mix.js → color/mix.js} +3 -3
  32. package/src/value/{relative-colors.js → color/relative.js} +3 -3
  33. package/src/value/color/shorten.js +93 -0
  34. package/src/value/gradients.js +1 -1
  35. package/src/value/hyphenate-limit-chars.js +72 -0
  36. package/src/value/initial-keywords.js +128 -0
  37. package/src/value/lengths.js +55 -0
  38. package/src/value/math/evaluate.js +36 -0
  39. package/src/value/math/fold.js +315 -0
  40. package/src/value/math/format.js +230 -0
  41. package/src/value/math/functions.js +302 -0
  42. package/src/value/math/index.js +219 -0
  43. package/src/value/math/parser.js +301 -0
  44. package/src/value/math/property-ranges.js +174 -0
  45. package/src/value/math/rewrite.js +222 -0
  46. package/src/value/math/tokenizer.js +160 -0
  47. package/src/value/math/unit-totals.js +194 -0
  48. package/src/value/minify.js +162 -1678
  49. package/src/value/numbers.js +28 -0
  50. package/src/value/positions.js +152 -0
  51. package/src/value/property-optimizations.js +485 -0
  52. package/src/value/segments.js +74 -0
  53. package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
  54. package/src/value/{svg-markup.js → svg/markup.js} +1 -1
  55. package/src/value/time.js +75 -0
  56. package/src/value/urls.js +242 -0
  57. package/src/value/whitespace.js +88 -0
  58. package/src/rules/optimize.js +0 -2031
  59. package/src/value/math-expression.js +0 -1156
  60. package/src/value/math.js +0 -886
  61. /package/src/value/{named-colors.js → color/named.js} +0 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @file Rewrites a property's `initial` keyword into the shorter value it stands for, and a lengthy initial value into the shorter `initial` keyword.
3
+ */
4
+
5
+ /**
6
+ * Properties whose `initial` value is a number the browser resolves, so the
7
+ * keyword only ever shortens for the two that have a shorter written form.
8
+ *
9
+ * @type {Set<string>}
10
+ */
11
+ const NUMERIC_INITIAL_PROPERTIES = new Set([
12
+ 'opacity',
13
+ 'z-index',
14
+ 'flex-grow',
15
+ 'flex-shrink',
16
+ 'order',
17
+ 'line-height',
18
+ 'zoom'
19
+ ]);
20
+
21
+ /**
22
+ * Properties whose `initial` value is zero.
23
+ *
24
+ * @type {Set<string>}
25
+ */
26
+ const ZERO_INITIAL_PROPERTIES = new Set(['margin', 'padding']);
27
+
28
+ /**
29
+ * Properties whose `initial` value is `auto`.
30
+ *
31
+ * @type {Set<string>}
32
+ */
33
+ const AUTO_INITIAL_PROPERTIES = new Set([
34
+ 'min-width',
35
+ 'min-height',
36
+ 'hyphenate-limit-chars'
37
+ ]);
38
+
39
+ /**
40
+ * Properties whose initial value is a single keyword longer than the global
41
+ * `initial` keyword itself. These replacements are applied when serializing
42
+ * declarations, because shorthand builders reconstruct their value from the
43
+ * actual longhand keywords, and a CSS-wide keyword such as `initial` is only
44
+ * valid as a declaration's entire value.
45
+ *
46
+ * @type {Map<string, string>}
47
+ */
48
+ const LENGTHY_INITIAL_KEYWORDS = new Map([
49
+ ['background-clip', 'border-box'],
50
+ ['background-origin', 'padding-box'],
51
+ ['border-collapse', 'separate'],
52
+ ['border-color', 'currentcolor'],
53
+ ['border-top-color', 'currentcolor'],
54
+ ['border-right-color', 'currentcolor'],
55
+ ['border-bottom-color', 'currentcolor'],
56
+ ['border-left-color', 'currentcolor'],
57
+ ['box-sizing', 'content-box'],
58
+ ['mask-clip', 'border-box'],
59
+ ['mask-origin', 'border-box'],
60
+ ['vertical-align', 'baseline'],
61
+ ['writing-mode', 'horizontal-tb']
62
+ ]);
63
+
64
+ /**
65
+ * Resolves a property's `initial` keyword into the value it stands for, for the
66
+ * properties where that value is shorter to write than the keyword.
67
+ *
68
+ * @param {string} value The CSS value string after generic minification.
69
+ * @param {string} property The CSS property name.
70
+ * @return {string} The value with its `initial` keyword resolved where that is shorter.
71
+ */
72
+ function resolveInitialKeyword (value, property) {
73
+ if (value !== 'initial') {
74
+ return value;
75
+ }
76
+ if (NUMERIC_INITIAL_PROPERTIES.has(property)) {
77
+ // Just leaving them or mapping some: opacity: initial -> opacity: 1
78
+ if (property === 'opacity') {
79
+ return '1';
80
+ }
81
+ if (property === 'z-index') {
82
+ return 'auto';
83
+ }
84
+ }
85
+ if (ZERO_INITIAL_PROPERTIES.has(property)) {
86
+ return '0';
87
+ }
88
+ if (AUTO_INITIAL_PROPERTIES.has(property)) {
89
+ return 'auto';
90
+ }
91
+ // background-color: initial should become #0000 (transparent)
92
+ if (property === 'background-color') {
93
+ return '#0000';
94
+ }
95
+ return value;
96
+ }
97
+
98
+ /**
99
+ * Rewrites a value that spells its property's initial value with a keyword
100
+ * longer than the global `initial` keyword. Both are equivalent, but `initial`
101
+ * is shorter than these particular keywords and, being repeated across
102
+ * properties, gives gzip more opportunities to deduplicate it.
103
+ *
104
+ * @param {string} value The minified declaration value, possibly flagged important.
105
+ * @param {string} property The CSS property name.
106
+ * @return {string} The value with its initial keyword shortened to `initial` when that saves bytes.
107
+ */
108
+ function shortenLengthyInitialKeyword (value, property) {
109
+ // A keyword spelling the property's own initial value is shorter as the
110
+ // global keyword (box-sizing: content-box -> box-sizing: initial)
111
+ const initialKeyword = LENGTHY_INITIAL_KEYWORDS.get(property?.toLowerCase());
112
+ if (!initialKeyword || 'initial'.length >= initialKeyword.length) {
113
+ return value;
114
+ }
115
+ // A trailing `!important` rides along inside the value, so the keyword
116
+ // comparison and the rewrite only concern what sits in front of it.
117
+ const isImportant = value.endsWith('!important');
118
+ const keyword = isImportant ? value.slice(0, value.length - '!important'.length) : value;
119
+ if (keyword.toLowerCase() !== initialKeyword) {
120
+ return value;
121
+ }
122
+ return 'initial' + (isImportant ? '!important' : '');
123
+ }
124
+
125
+ export {
126
+ resolveInitialKeyword,
127
+ shortenLengthyInitialKeyword
128
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @file Converts absolute CSS lengths to the pixels they measure whenever the conversion is exact and no longer.
3
+ */
4
+
5
+ import {
6
+ convertAbsoluteLengthToPx,
7
+ roundCompactNumber
8
+ } from './shared.js';
9
+
10
+ /**
11
+ * Matches an absolute CSS length token: an optionally signed number followed by
12
+ * an absolute length unit. The lookbehind rejects digits that belong to a larger
13
+ * identifier, such as the custom property name in `var(--size-2in)`, where the
14
+ * digits and unit do not form a value of their own.
15
+ *
16
+ * @type {RegExp}
17
+ */
18
+ const ABSOLUTE_LENGTH_PATTERN = /(?<![\w#.%-])(-?(?:\d+|\d*\.\d+))(pt|pc|in|cm|mm|q)\b/gi;
19
+
20
+ /**
21
+ * The largest difference in pixels that a rounded conversion may introduce and
22
+ * still count as exact, which allows for binary floating point error without
23
+ * allowing a visible change to the rendered length.
24
+ *
25
+ * @type {number}
26
+ */
27
+ const PIXEL_ROUNDING_TOLERANCE = 1e-6;
28
+
29
+ /**
30
+ * Converts absolute length values (pt, pc, in, cm, mm, Q) to their pixel
31
+ * equivalent when the conversion is exact and the pixel form is no longer than
32
+ * the original. Normalizing to px also improves compression by reducing the
33
+ * number of distinct unit strings in the output.
34
+ *
35
+ * @param {string} value A CSS value segment, outside strings and urls.
36
+ * @return {string} The segment with eligible absolute lengths converted to px.
37
+ */
38
+ function convertAbsoluteLengthsToPx (value) {
39
+ return value.replace(ABSOLUTE_LENGTH_PATTERN, (token, amount, unit) => {
40
+ const pixels = convertAbsoluteLengthToPx(amount, unit);
41
+ if (pixels === null) {
42
+ return token;
43
+ }
44
+ const converted = roundCompactNumber(pixels) + 'px';
45
+ const isExact = Math.abs(parseFloat(converted) - pixels) < PIXEL_ROUNDING_TOLERANCE;
46
+ if (!isExact || converted.length > token.length) {
47
+ return token;
48
+ }
49
+ return converted;
50
+ });
51
+ }
52
+
53
+ export {
54
+ convertAbsoluteLengthsToPx
55
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @file Reads the arithmetic a CSS math function states and resolves it into the value it works out to, whenever every operand of it is a number already known at minification time.
3
+ */
4
+
5
+ import { formatUnitTotals } from './format.js';
6
+ import { parseSum } from './parser.js';
7
+ import { tokenizeMathExpression } from './tokenizer.js';
8
+
9
+ /**
10
+ * Resolves a CSS math function into the value its arithmetic works out to.
11
+ * A function whose operands are not all known at minification time — one
12
+ * referencing a custom property, an environment variable, or an element the
13
+ * value is only resolved against once the page is laid out — states a value
14
+ * that cannot be worked out here, and so resolves to nothing.
15
+ *
16
+ * @param {string} mathFunction The math function as it is written, such as `calc(10 / 2)` or `min(10px, 20px)`.
17
+ * @return {string|null} The value the math function states, or null when it cannot be resolved.
18
+ */
19
+ function simplifyCalc (mathFunction) {
20
+ const tokens = tokenizeMathExpression(mathFunction);
21
+ if (!tokens) {
22
+ return null;
23
+ }
24
+ const reader = { tokens, index: 0, depth: 0 };
25
+ const totals = parseSum(reader);
26
+ // A token left unread means the expression states more than the arithmetic
27
+ // that was resolved, so what was resolved is not the whole of its value
28
+ if (!totals || reader.index !== tokens.length) {
29
+ return null;
30
+ }
31
+ return formatUnitTotals(totals);
32
+ }
33
+
34
+ export {
35
+ simplifyCalc
36
+ };
@@ -0,0 +1,315 @@
1
+ /**
2
+ * @file Folds the constant arithmetic of a `calc()` into the shortest way of writing the value it states, weighing the resolved value against the calculation itself.
3
+ */
4
+
5
+ import {
6
+ convertAbsoluteLengthToPx,
7
+ formatResolvedDimension,
8
+ formatResolvedNumber
9
+ } from '../shared.js';
10
+
11
+ import { simplifyCalc } from './evaluate.js';
12
+ import { formatUnitTotals } from './format.js';
13
+
14
+ /**
15
+ * Matches a whole expression that divides a percentage by a plain number,
16
+ * capturing the percentage and the divisor it is shared out between.
17
+ *
18
+ * @type {RegExp}
19
+ */
20
+ const PERCENTAGE_DIVISION = /^([+-]?(?:\d+|\d*\.\d+))%\s*\/\s*([+-]?(?:\d+|\d*\.\d+))$/;
21
+
22
+ /**
23
+ * Picks between what a calculation resolved to and the calculation itself,
24
+ * keeping whichever takes fewer characters to write. A tie goes to the
25
+ * resolved value, which states the same thing without the arithmetic.
26
+ *
27
+ * @param {string} resolved The value the calculation resolves to.
28
+ * @param {string} calculation The calculation, written as compactly as it can be.
29
+ * @return {string} The shorter of the two.
30
+ */
31
+ function shorterOfResolutionAndCalculation (resolved, calculation) {
32
+ if (resolved.length <= calculation.length) {
33
+ return resolved;
34
+ }
35
+ return calculation;
36
+ }
37
+
38
+ /**
39
+ * Writes a calculation as compactly as its syntax allows, squeezing out every
40
+ * space but the ones a `+` or a `-` needs to be read as an operator.
41
+ *
42
+ * @param {string} inner The contents of the `calc()`.
43
+ * @return {string} The whole `calc()`, written as compactly as it can be.
44
+ */
45
+ function compactCalculation (inner) {
46
+ // Remove whitespace around multiplication and division operators
47
+ const compactInner = inner.replace(/\s+/g, ' ').trim().replace(/\s*([*/])\s*/g, '$1');
48
+ return 'calc(' + compactInner + ')';
49
+ }
50
+
51
+ /**
52
+ * Picks between a calculation and what resolving its arithmetic came to. A
53
+ * resolved value states the same thing without any arithmetic, so it wins a
54
+ * tie, but a result that is itself a calculation only states the same terms
55
+ * another way, and writing them out again for no saving gains nothing.
56
+ *
57
+ * @param {string} resolved The value the calculation resolved to.
58
+ * @param {string} calculation The calculation, written as compactly as it can be.
59
+ * @return {string} Whichever of the two the value is written as.
60
+ */
61
+ function chooseResolvedCalculation (resolved, calculation) {
62
+ // A resolution that kept its function form left terms behind that only
63
+ // resolve once the element they are measured against is laid out
64
+ if (resolved.startsWith('calc(') && resolved.length >= calculation.length) {
65
+ return calculation;
66
+ }
67
+ return shorterOfResolutionAndCalculation(resolved, calculation);
68
+ }
69
+
70
+ /**
71
+ * Resolves a percentage divided by a number into the single percentage it
72
+ * equals, which is how `calc(100%/16)` comes to be written as `6.25%`. The
73
+ * division stays as it was written when the result is no shorter, and when
74
+ * the divisor is zero, since that resolves to nothing a stylesheet can state.
75
+ *
76
+ * @param {string} expression The contents of a `calc()`, with its whitespace collapsed.
77
+ * @return {string|null} The shorter of the resolved percentage and the division, or null when the expression is not one.
78
+ */
79
+ function resolvePercentageDivision (expression) {
80
+ const division = expression.match(PERCENTAGE_DIVISION);
81
+ if (!division) {
82
+ return null;
83
+ }
84
+ const [, percentage, divisor] = division;
85
+ const calculation = 'calc(' + percentage + '%/' + divisor + ')';
86
+ if (Number(divisor) === 0) {
87
+ return calculation;
88
+ }
89
+ const resolved = formatResolvedNumber(Number(percentage) / Number(divisor)) + '%';
90
+ return shorterOfResolutionAndCalculation(resolved, calculation);
91
+ }
92
+
93
+ /**
94
+ * Attempts to simplify a calc() expression by combining like-unit terms and evaluating pure arithmetic, returning the simplified string or null if folding is not possible.
95
+ *
96
+ * @param {string} expression The expression inside calc() to attempt folding.
97
+ * @return {string|null} The simplified expression, or null if it cannot be folded.
98
+ */
99
+ function tryFoldCalcExpression (expression) {
100
+ let folded = expression.trim();
101
+ let previous;
102
+
103
+ do {
104
+ previous = folded;
105
+ // Remove innermost non-nested parentheses (flatten simple grouping)
106
+ folded = folded.replace(/\(([^()]+)\)/g, '$1');
107
+ // Fold: <number> * <number><unit> → computed result in same unit
108
+ folded = folded.replace(/(-?(?:\d*\.\d+|\d+))\s*\*\s*(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q|%)/gi, (match, multiplier, amount, unit) => {
109
+ return formatResolvedDimension(parseFloat(multiplier) * parseFloat(amount), unit);
110
+ });
111
+ // Fold: <number><unit> * <number> → computed result in same unit
112
+ folded = folded.replace(/(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q|%)\s*\*\s*(-?(?:\d*\.\d+|\d+))/gi, (match, amount, unit, multiplier) => {
113
+ return formatResolvedDimension(parseFloat(multiplier) * parseFloat(amount), unit);
114
+ });
115
+ // Fold: <number><unit> / <number> → computed result in same unit
116
+ folded = folded.replace(/(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q)\s*\/\s*(-?(?:\d*\.\d+|\d+))/gi, (match, value, unit, divisor) => {
117
+ return formatResolvedDimension(parseFloat(value) / parseFloat(divisor), unit);
118
+ });
119
+ // Collapse zero-with-unit terms (e.g. 0px, 0%) to plain 0 or remove them
120
+ folded = folded.replace(/(^|[+-])\s*0(?:px|pt|pc|in|cm|mm|q|%)\b/g, (match, sign) => {
121
+ if (sign && sign !== '+') {
122
+ return sign + ' 0';
123
+ }
124
+ return '';
125
+ });
126
+ // Remove additive zero terms
127
+ folded = folded.replace(/\+\s*0\b/g, '');
128
+ // Remove subtractive zero terms
129
+ folded = folded.replace(/-\s*0\b/g, '');
130
+ // Collapse whitespace
131
+ folded = folded.replace(/\s+/g, ' ').trim();
132
+ } while (folded !== previous);
133
+
134
+ // Simplify trivial identity division: 1 / 1 / <dimension> → <dimension>
135
+ if (/^1\s*\/\s*1\s*\/\s*(-?(?:\d*\.\d+|\d+)(?:px|pt|pc|in|cm|mm|q))$/i.test(folded)) {
136
+ return folded.replace(/^1\s*\/\s*1\s*\/\s*/i, '');
137
+ }
138
+
139
+ // Remove all whitespace for validation
140
+ const normalized = folded.replace(/\s+/g, '');
141
+ // Validate that the expression is a simple sequence of signed terms with optional units
142
+ if (!/^[+-]?(?:\d*\.\d+|\d+)(?:[a-z%]+)?(?:[+-](?:\d*\.\d+|\d+)(?:[a-z%]+)?)*$/i.test(normalized)) {
143
+ return null;
144
+ }
145
+
146
+ // Extract each signed term with its optional unit
147
+ const terms = normalized.match(/[+-]?(?:\d*\.\d+|\d+)(?:[a-z%]+)?/gi) || [];
148
+ const totals = new Map();
149
+
150
+ for (const term of terms) {
151
+ // Parse each term into sign, number, and unit parts
152
+ const match = term.match(/^([+-]?)(\d*\.\d+|\d+)([a-z%]+)?$/i);
153
+ if (!match) {
154
+ return null;
155
+ }
156
+ const [, sign, rawNumber, rawUnit = ''] = match;
157
+ let number = parseFloat(rawNumber) * (sign === '-' ? -1 : 1);
158
+ let unit = rawUnit.toLowerCase();
159
+
160
+ if (unit && unit !== '%' && unit !== 'px') {
161
+ const pxValue = convertAbsoluteLengthToPx(number, unit);
162
+ if (pxValue === null) {
163
+ return null;
164
+ }
165
+ number = pxValue;
166
+ unit = 'px';
167
+ }
168
+
169
+ totals.set(unit, (totals.get(unit) || 0) + number);
170
+ }
171
+
172
+ return formatUnitTotals(totals);
173
+ }
174
+
175
+ /**
176
+ * Matches a whole expression that scales a percentage by a plain number,
177
+ * written either way around. Both operands are already as short as they can
178
+ * be, so the expression is only ever squeezed, never resolved.
179
+ *
180
+ * @type {RegExp}
181
+ */
182
+ const PERCENTAGE_MULTIPLICATION = /^(?:-?(?:\d*\.\d+|\d+)%\s*\*\s*-?(?:\d*\.\d+|\d+)|-?(?:\d*\.\d+|\d+)\s*\*\s*-?(?:\d*\.\d+|\d+)%)$/;
183
+
184
+ /**
185
+ * Matches a value that is a single percentage, which is all a calculation
186
+ * leaves behind once every term of it has resolved into one.
187
+ *
188
+ * @type {RegExp}
189
+ */
190
+ const RESOLVED_PERCENTAGE = /^-?(?:\d+|\d*\.\d+)%$/;
191
+
192
+ /**
193
+ * Chooses how a calculation that shares a percentage out resolves. The
194
+ * library states the result to the full precision a float carries, which is
195
+ * both longer than the calculation it replaces and finer than a stylesheet
196
+ * has any use for, so the result is rounded into the budget a resolved number
197
+ * is written within and kept only when it is the shorter of the two.
198
+ *
199
+ * @param {string} simplified The result the calculation library returned.
200
+ * @param {string} compactInner The contents of the `calc()`, with its whitespace collapsed.
201
+ * @param {string} match The whole `calc()` as it was written.
202
+ * @return {string} How the resolved calculation is written.
203
+ */
204
+ function chooseResolvedPercentage (simplified, compactInner, match) {
205
+ // A division sign written after a percentage, which is what the resolved percentage was shared out by
206
+ const dividesAPercentage = RESOLVED_PERCENTAGE.test(simplified) && /%\s*\//.test(match);
207
+ if (!dividesAPercentage) {
208
+ return simplified;
209
+ }
210
+ // Remove whitespace around division operator
211
+ const calculation = 'calc(' + compactInner.replace(/\s*\/\s*/g, '/') + ')';
212
+ const resolved = formatResolvedNumber(parseFloat(simplified)) + '%';
213
+ return shorterOfResolutionAndCalculation(resolved, calculation);
214
+ }
215
+
216
+ /**
217
+ * Resolves the contents of a `calc()` into the shortest way of writing the
218
+ * same value, which is either the value the arithmetic works out to or the
219
+ * calculation itself with the whitespace squeezed out of it.
220
+ *
221
+ * @param {string} match The whole `calc()` as it was written.
222
+ * @param {string} compactInner The contents of the `calc()`, with its whitespace collapsed.
223
+ * @return {string} The shortest way of writing what the calculation states.
224
+ */
225
+ function resolveCalcExpression (match, compactInner) {
226
+ if (PERCENTAGE_MULTIPLICATION.test(compactInner)) {
227
+ return compactCalculation(compactInner);
228
+ }
229
+
230
+ const dividedPercentage = resolvePercentageDivision(compactInner);
231
+ if (dividedPercentage) {
232
+ return dividedPercentage;
233
+ }
234
+
235
+ const folded = tryFoldCalcExpression(compactInner);
236
+ if (folded) {
237
+ return folded;
238
+ }
239
+
240
+ const simplified = simplifyCalc(match);
241
+ if (simplified === null) {
242
+ return match;
243
+ }
244
+ const resolved = chooseResolvedPercentage(simplified, compactInner, match);
245
+ return chooseResolvedCalculation(resolved, compactCalculation(compactInner));
246
+ }
247
+
248
+ /**
249
+ * Simplifies a standalone calc() value by resolving its arithmetic, flattening nested calc expressions, converting absolute length units to pixels, and folding constant terms.
250
+ *
251
+ * @param {string} value The CSS value string, already known to be a single `calc()`.
252
+ * @return {string} The simplified value, or the original value if simplification is not applicable.
253
+ */
254
+ function foldStandaloneCalc (value) {
255
+ let inner = value.slice(5, -1).trim();
256
+
257
+ // Preserve percent-times-number expressions, only stripping whitespace around operators
258
+ if (PERCENTAGE_MULTIPLICATION.test(inner.replace(/\s+/g, ' ').trim())) {
259
+ return compactCalculation(inner);
260
+ }
261
+
262
+ // Resolve the calculation as it was written, which is the only reading of it
263
+ // that keeps the grouping its parentheses state. Everything below reads a
264
+ // flattened copy of the expression, where that grouping is already gone.
265
+ const resolved = simplifyCalc(value);
266
+ if (resolved !== null) {
267
+ return chooseResolvedCalculation(resolved, compactCalculation(inner));
268
+ }
269
+
270
+ // If no nested function calls and no multiplication/division involving parens, try flattening
271
+ if (!/[A-Za-z-]+\(/.test(inner) && !/[*/]\s*\(|\)\s*[*/]/.test(inner)) {
272
+ // Remove all parentheses and collapse whitespace for folding
273
+ const flattened = inner.replace(/[()]/g, '').replace(/\s+/g, ' ').trim();
274
+ const foldedFlattened = tryFoldCalcExpression(flattened);
275
+ if (foldedFlattened) {
276
+ return foldedFlattened;
277
+ }
278
+ }
279
+
280
+ let previous;
281
+
282
+ do {
283
+ previous = inner;
284
+ // Fold innermost parenthesized sub-expressions that aren't function calls
285
+ inner = inner.replace(/(^|[^A-Za-z-])\(([^()]+)\)/g, (match, prefix, content) => {
286
+ const trimmed = content.trim();
287
+ const folded = tryFoldCalcExpression(trimmed);
288
+ if (folded) {
289
+ if (folded.startsWith('calc(') && folded.endsWith(')')) {
290
+ return prefix + trimmed;
291
+ }
292
+ return prefix + folded;
293
+ }
294
+ return prefix + trimmed;
295
+ });
296
+ } while (inner !== previous);
297
+
298
+ const folded = tryFoldCalcExpression(inner);
299
+ if (folded) {
300
+ return folded;
301
+ }
302
+
303
+ // Collapse whitespace and check for percent-division expressions
304
+ const dividedPercentage = resolvePercentageDivision(inner.replace(/\s+/g, ' ').trim());
305
+ if (dividedPercentage) {
306
+ return dividedPercentage;
307
+ }
308
+
309
+ return compactCalculation(inner);
310
+ }
311
+
312
+ export {
313
+ foldStandaloneCalc,
314
+ resolveCalcExpression
315
+ };