@thejaredwilcurt/csslop 0.0.1
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 +213 -0
- package/index.js +5 -0
- package/package.json +48 -0
- package/src/context.js +17 -0
- package/src/declarations/config.js +56 -0
- package/src/declarations/process.js +662 -0
- package/src/index.js +90 -0
- package/src/position-try.js +199 -0
- package/src/preprocess.js +27 -0
- package/src/rules/normalize.js +108 -0
- package/src/rules/optimize.js +375 -0
- package/src/rules/stringify.js +556 -0
- package/src/utilities.js +37 -0
- package/src/value/colors.js +780 -0
- package/src/value/gradients.js +121 -0
- package/src/value/math.js +281 -0
- package/src/value/minify.js +716 -0
- package/src/value/named-colors.js +159 -0
- package/src/value/shared.js +146 -0
- package/src/value/transforms.js +222 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Parses and minifies CSS gradient function calls by splitting arguments, normalizing default directions, and removing redundant stop positions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Splits a gradient function's argument string at top-level commas, correctly handling nested parentheses.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} argumentString The raw gradient arguments string.
|
|
9
|
+
* @return {Array} An array of trimmed argument strings.
|
|
10
|
+
*/
|
|
11
|
+
function splitGradientArgs (argumentString) {
|
|
12
|
+
const parts = [];
|
|
13
|
+
let depth = 0;
|
|
14
|
+
let current = '';
|
|
15
|
+
for (const character of argumentString) {
|
|
16
|
+
if (character === '(') {
|
|
17
|
+
depth++;
|
|
18
|
+
} else if (character === ')') {
|
|
19
|
+
depth--;
|
|
20
|
+
}
|
|
21
|
+
if (character === ',' && depth === 0) {
|
|
22
|
+
parts.push(current.trim());
|
|
23
|
+
current = '';
|
|
24
|
+
} else {
|
|
25
|
+
current += character;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (current.trim() || parts.length) {
|
|
29
|
+
parts.push(current.trim());
|
|
30
|
+
}
|
|
31
|
+
return parts;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Optimizes gradient arguments by removing default direction or shape keywords and trimming redundant 0% or 100% stop positions from the first and last stops.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} func The gradient function name (e.g. "linear-gradient").
|
|
38
|
+
* @param {string} argsStr The raw comma-separated gradient arguments string.
|
|
39
|
+
* @return {string} The optimized gradient arguments string.
|
|
40
|
+
*/
|
|
41
|
+
function processGradientArgs (func, argsStr) {
|
|
42
|
+
const args = splitGradientArgs(argsStr);
|
|
43
|
+
const functionLower = func.toLowerCase();
|
|
44
|
+
|
|
45
|
+
if (functionLower.includes('linear')) {
|
|
46
|
+
if (args.length > 1) {
|
|
47
|
+
const firstDirection = args[0].toLowerCase().replace(/\s+/g, ' ').trim();
|
|
48
|
+
if (firstDirection === 'to bottom' || firstDirection === '180deg') {
|
|
49
|
+
args.shift();
|
|
50
|
+
} else if (firstDirection === 'to top') {
|
|
51
|
+
args[0] = '0deg';
|
|
52
|
+
} else if (firstDirection === 'to right') {
|
|
53
|
+
args[0] = '90deg';
|
|
54
|
+
} else if (firstDirection === 'to left') {
|
|
55
|
+
args[0] = '270deg';
|
|
56
|
+
} else if (firstDirection === 'to top right' || firstDirection === 'to right top') {
|
|
57
|
+
args[0] = '45deg';
|
|
58
|
+
} else if (firstDirection === 'to bottom right' || firstDirection === 'to right bottom') {
|
|
59
|
+
args[0] = '135deg';
|
|
60
|
+
} else if (firstDirection === 'to bottom left' || firstDirection === 'to left bottom') {
|
|
61
|
+
args[0] = '225deg';
|
|
62
|
+
} else if (firstDirection === 'to top left' || firstDirection === 'to left top') {
|
|
63
|
+
args[0] = '315deg';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} else if (functionLower.includes('radial')) {
|
|
67
|
+
if (args.length > 1) {
|
|
68
|
+
const firstShape = args[0].toLowerCase().replace(/\s+/g, ' ').trim();
|
|
69
|
+
if (firstShape === 'ellipse at center' || firstShape === 'circle at center') {
|
|
70
|
+
args.shift();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (args.length > 0) {
|
|
76
|
+
// Remove default 0% stop position from the first gradient stop
|
|
77
|
+
args[0] = args[0].replace(/^(.*\S)\s+0%$/, '$1');
|
|
78
|
+
// Remove default 100% stop position from the last gradient stop
|
|
79
|
+
args[args.length - 1] = args[args.length - 1].replace(/^(.*\S)\s+100%$/, '$1');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return args.join(',');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Finds and minifies all gradient function calls within a CSS value string, applying argument optimization to each one.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} value The CSS value string potentially containing gradient functions.
|
|
89
|
+
* @return {string} The value string with all gradient calls minified.
|
|
90
|
+
*/
|
|
91
|
+
function minifyGradients (value) {
|
|
92
|
+
let result = '';
|
|
93
|
+
let position = 0;
|
|
94
|
+
while (position < value.length) {
|
|
95
|
+
const rest = value.slice(position);
|
|
96
|
+
// Match gradient function names: linear-gradient, radial-gradient, conic-gradient, and their repeating- variants
|
|
97
|
+
const gradientMatch = rest.match(/^((?:repeating-)?(?:linear|radial|conic)-gradient)\(/i);
|
|
98
|
+
if (gradientMatch) {
|
|
99
|
+
const func = gradientMatch[1];
|
|
100
|
+
let depth = 1;
|
|
101
|
+
let end = position + func.length + 1;
|
|
102
|
+
while (end < value.length && depth > 0) {
|
|
103
|
+
if (value[end] === '(') {
|
|
104
|
+
depth++;
|
|
105
|
+
} else if (value[end] === ')') {
|
|
106
|
+
depth--;
|
|
107
|
+
}
|
|
108
|
+
end++;
|
|
109
|
+
}
|
|
110
|
+
const argsStr = value.slice(position + func.length + 1, end - 1);
|
|
111
|
+
result += func + '(' + processGradientArgs(func, argsStr) + ')';
|
|
112
|
+
position = end;
|
|
113
|
+
} else {
|
|
114
|
+
result += value[position];
|
|
115
|
+
position++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export { minifyGradients };
|
|
@@ -0,0 +1,281 @@
|
|
|
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 { calc } from '@csstools/css-calc';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
convertAbsoluteLengthToPx,
|
|
9
|
+
formatDimension,
|
|
10
|
+
roundCompactNumber
|
|
11
|
+
} from './shared.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 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.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} expression The expression inside calc() to attempt folding.
|
|
17
|
+
* @return {string|null} The simplified expression, or null if it cannot be folded.
|
|
18
|
+
*/
|
|
19
|
+
function tryFoldCalcExpression (expression) {
|
|
20
|
+
let expr = expression.trim();
|
|
21
|
+
let previous;
|
|
22
|
+
|
|
23
|
+
do {
|
|
24
|
+
previous = expr;
|
|
25
|
+
// Remove innermost non-nested parentheses (flatten simple grouping)
|
|
26
|
+
expr = expr.replace(/\(([^()]+)\)/g, '$1');
|
|
27
|
+
// Fold: <number> * <number><unit> → computed result in same unit
|
|
28
|
+
expr = expr.replace(/(-?(?:\d*\.\d+|\d+))\s*\*\s*(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q|%)/gi, (match, a, b, unit) => {
|
|
29
|
+
return formatDimension(parseFloat(a) * parseFloat(b), unit);
|
|
30
|
+
});
|
|
31
|
+
// Fold: <number><unit> * <number> → computed result in same unit
|
|
32
|
+
expr = expr.replace(/(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q|%)\s*\*\s*(-?(?:\d*\.\d+|\d+))/gi, (match, a, unit, b) => {
|
|
33
|
+
return formatDimension(parseFloat(a) * parseFloat(b), unit);
|
|
34
|
+
});
|
|
35
|
+
// Fold: <number><unit> / <number> → computed result in same unit
|
|
36
|
+
expr = expr.replace(/(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q)\s*\/\s*(-?(?:\d*\.\d+|\d+))/gi, (match, value, unit, divisor) => {
|
|
37
|
+
return formatDimension(parseFloat(value) / parseFloat(divisor), unit);
|
|
38
|
+
});
|
|
39
|
+
// Collapse zero-with-unit terms (e.g. 0px, 0%) to plain 0 or remove them
|
|
40
|
+
expr = expr.replace(/(^|[+-])\s*0(?:px|pt|pc|in|cm|mm|q|%)\b/g, (match, sign) => {
|
|
41
|
+
if (sign && sign !== '+') {
|
|
42
|
+
return sign + ' 0';
|
|
43
|
+
}
|
|
44
|
+
return '';
|
|
45
|
+
});
|
|
46
|
+
// Remove additive zero terms
|
|
47
|
+
expr = expr.replace(/\+\s*0\b/g, '');
|
|
48
|
+
// Remove subtractive zero terms
|
|
49
|
+
expr = expr.replace(/-\s*0\b/g, '');
|
|
50
|
+
// Collapse whitespace
|
|
51
|
+
expr = expr.replace(/\s+/g, ' ').trim();
|
|
52
|
+
} while (expr !== previous);
|
|
53
|
+
|
|
54
|
+
// Simplify trivial identity division: 1 / 1 / <dimension> → <dimension>
|
|
55
|
+
if (/^1\s*\/\s*1\s*\/\s*(-?(?:\d*\.\d+|\d+)(?:px|pt|pc|in|cm|mm|q))$/i.test(expr)) {
|
|
56
|
+
return expr.replace(/^1\s*\/\s*1\s*\/\s*/i, '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Remove all whitespace for validation
|
|
60
|
+
const normalized = expr.replace(/\s+/g, '');
|
|
61
|
+
// Validate that the expression is a simple sequence of signed terms with optional units
|
|
62
|
+
if (!/^[+-]?(?:\d*\.\d+|\d+)(?:[a-z%]+)?(?:[+-](?:\d*\.\d+|\d+)(?:[a-z%]+)?)*$/i.test(normalized)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Extract each signed term with its optional unit
|
|
67
|
+
const terms = normalized.match(/[+-]?(?:\d*\.\d+|\d+)(?:[a-z%]+)?/gi) || [];
|
|
68
|
+
const totals = new Map();
|
|
69
|
+
|
|
70
|
+
for (const term of terms) {
|
|
71
|
+
// Parse each term into sign, number, and unit parts
|
|
72
|
+
const match = term.match(/^([+-]?)(\d*\.\d+|\d+)([a-z%]+)?$/i);
|
|
73
|
+
if (!match) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const [, sign, rawNumber, rawUnit = ''] = match;
|
|
77
|
+
let number = parseFloat(rawNumber) * (sign === '-' ? -1 : 1);
|
|
78
|
+
let unit = rawUnit.toLowerCase();
|
|
79
|
+
|
|
80
|
+
if (unit && unit !== '%' && unit !== 'px') {
|
|
81
|
+
const pxValue = convertAbsoluteLengthToPx(number, unit);
|
|
82
|
+
if (pxValue === null) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
number = pxValue;
|
|
86
|
+
unit = 'px';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
totals.set(unit, (totals.get(unit) || 0) + number);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const orderedUnits = ['%', '', 'px', ...[...totals.keys()].filter((unit) => {
|
|
93
|
+
return !['%', '', 'px'].includes(unit);
|
|
94
|
+
}).sort()];
|
|
95
|
+
const outputTerms = [];
|
|
96
|
+
|
|
97
|
+
for (const unit of orderedUnits) {
|
|
98
|
+
if (!totals.has(unit)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const value = totals.get(unit);
|
|
102
|
+
if (Math.abs(value) < 1e-12) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
outputTerms.push({ unit, value });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!outputTerms.length) {
|
|
109
|
+
return '0';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (outputTerms.length === 1) {
|
|
113
|
+
const { unit, value } = outputTerms[0];
|
|
114
|
+
if (unit) {
|
|
115
|
+
return roundCompactNumber(value) + unit;
|
|
116
|
+
}
|
|
117
|
+
return roundCompactNumber(value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const [first, ...rest] = outputTerms;
|
|
121
|
+
let result = roundCompactNumber(first.value) + first.unit;
|
|
122
|
+
for (const term of rest) {
|
|
123
|
+
const sign = term.value < 0 ? '-' : '+';
|
|
124
|
+
result += ' ' + sign + ' ' + roundCompactNumber(Math.abs(term.value)) + term.unit;
|
|
125
|
+
}
|
|
126
|
+
return 'calc(' + result + ')';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Simplifies calc(), min(), and max() expressions within a CSS value string using the `@csstools`/css-calc library, falling back to the original value on failure.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} value The CSS value string containing math functions to simplify.
|
|
133
|
+
* @param {string} property The CSS property name, used for context-aware simplification.
|
|
134
|
+
* @param {string} originalValue The original unmodified value to fall back to if simplification produces an invalid result.
|
|
135
|
+
* @return {string} The value with math functions simplified where possible.
|
|
136
|
+
*/
|
|
137
|
+
function normalizeMathFunctions (value, property, originalValue = '') {
|
|
138
|
+
let result = value;
|
|
139
|
+
|
|
140
|
+
// Unwrap calc(1 / (1 / x)) → x (double-reciprocal identity)
|
|
141
|
+
result = result.replace(/calc\(\s*1\s*\/\s*\(\s*1\s*\/\s*([^()]+)\s*\)\s*\)/gi, (match, inner) => {
|
|
142
|
+
return inner.trim();
|
|
143
|
+
});
|
|
144
|
+
// Flatten calc(calc(a) ± b) → calc(a ± b)
|
|
145
|
+
result = result.replace(/calc\(\s*calc\(([^()]+)\)\s*([+-])\s*([^()]+)\s*\)/gi, (match, inner, operator, tail) => {
|
|
146
|
+
return 'calc(' + inner + ' ' + operator + ' ' + tail + ')';
|
|
147
|
+
});
|
|
148
|
+
// Unwrap calc(calc(x)) → calc(x)
|
|
149
|
+
result = result.replace(/calc\(\s*calc\(([^()]+)\)\s*\)/gi, (match, inner) => {
|
|
150
|
+
return 'calc(' + inner + ')';
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Simplify min()/max() expressions using @csstools/css-calc
|
|
154
|
+
result = result.replace(/\b(min|max)\(([^()]+)\)/gi, (match) => {
|
|
155
|
+
try {
|
|
156
|
+
const simplified = calc(match);
|
|
157
|
+
return typeof simplified === 'string' ? simplified : match;
|
|
158
|
+
} catch {
|
|
159
|
+
return match;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Simplify calc() expressions using constant folding and @csstools/css-calc
|
|
164
|
+
result = result.replace(/calc\(([^()]+)\)/gi, (match, inner) => {
|
|
165
|
+
// Collapse whitespace inside calc expression
|
|
166
|
+
const compactInner = inner.replace(/\s+/g, ' ').trim();
|
|
167
|
+
// Preserve percent-times-number expressions (e.g. 50%*2 or 2*50%) — just strip inner spaces
|
|
168
|
+
if (/^(?:-?(?:\d*\.\d+|\d+)%\s*\*\s*-?(?:\d*\.\d+|\d+)|-?(?:\d*\.\d+|\d+)\s*\*\s*-?(?:\d*\.\d+|\d+)%)$/i.test(compactInner)) {
|
|
169
|
+
// Remove whitespace around multiplication/division operators
|
|
170
|
+
return 'calc(' + compactInner.replace(/\s*([*/])\s*/g, '$1') + ')';
|
|
171
|
+
}
|
|
172
|
+
// Preserve percent/number division expressions (e.g. 100%/3) — just strip inner spaces
|
|
173
|
+
if (/^\d+(?:\.\d+)?%\s*\/\s*\d+(?:\.\d+)?$/i.test(compactInner)) {
|
|
174
|
+
// Remove whitespace around division operator
|
|
175
|
+
return 'calc(' + compactInner.replace(/\s*\/\s*/g, '/') + ')';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const folded = tryFoldCalcExpression(compactInner);
|
|
179
|
+
if (folded) {
|
|
180
|
+
return folded;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const simplified = calc(match);
|
|
185
|
+
if (typeof simplified !== 'string') {
|
|
186
|
+
return match;
|
|
187
|
+
}
|
|
188
|
+
// If simplified to a bare percentage but original had division, preserve the calc form
|
|
189
|
+
if (/^-?(?:\d+|\d*\.\d+)%$/.test(simplified) && /%\s*\//.test(match)) {
|
|
190
|
+
return 'calc(' + compactInner.replace(/\s*\/\s*/g, '/') + ')';
|
|
191
|
+
}
|
|
192
|
+
return simplified;
|
|
193
|
+
} catch {
|
|
194
|
+
return match;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// When calc() folded to an absolute-length unit (pt, pc, in, cm, mm, q), convert to pixels
|
|
199
|
+
if (originalValue.includes('calc(') && /^-?(?:\d+|\d*\.\d+)(pt|pc|in|cm|mm|q)$/i.test(result)) {
|
|
200
|
+
// Extract the absolute-length unit from the folded result
|
|
201
|
+
const [, unit] = result.match(/^-?(?:\d+|\d*\.\d+)(pt|pc|in|cm|mm|q)$/i);
|
|
202
|
+
const numeric = parseFloat(result);
|
|
203
|
+
const pxValue = convertAbsoluteLengthToPx(numeric, unit);
|
|
204
|
+
if (pxValue !== null) {
|
|
205
|
+
result = roundCompactNumber(pxValue) + 'px';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Round results with excessive decimal places (4+ digits after the decimal)
|
|
210
|
+
result = result.replace(/(-?(?:\d+|\d*\.\d+)\.\d{4,})([a-z%]+)/gi, (match, number, unit) => {
|
|
211
|
+
return roundCompactNumber(number) + unit;
|
|
212
|
+
});
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Simplifies a standalone calc() value by flattening nested calc expressions, converting absolute length units to pixels, and folding constant terms.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} value The CSS value string that may be a standalone calc() expression.
|
|
220
|
+
* @return {string} The simplified value, or the original value if simplification is not applicable.
|
|
221
|
+
*/
|
|
222
|
+
function simplifyStandaloneCalc (value) {
|
|
223
|
+
// Check if value starts with calc( and ends with )
|
|
224
|
+
if (!/^calc\(/i.test(value) || !value.endsWith(')')) {
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
let inner = value.slice(5, -1).trim();
|
|
228
|
+
|
|
229
|
+
// Preserve percent-times-number expressions, only stripping whitespace around operators
|
|
230
|
+
if (/^(?:-?(?:\d*\.\d+|\d+)%\s*\*\s*-?(?:\d*\.\d+|\d+)|-?(?:\d*\.\d+|\d+)\s*\*\s*-?(?:\d*\.\d+|\d+)%)$/i.test(inner.replace(/\s+/g, ' ').trim())) {
|
|
231
|
+
return 'calc(' + inner.replace(/\s*([*/])\s*/g, '$1') + ')';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// If no nested function calls and no multiplication/division involving parens, try flattening
|
|
235
|
+
if (!/[A-Za-z-]+\(/.test(inner) && !/[*/]\s*\(|\)\s*[*/]/.test(inner)) {
|
|
236
|
+
// Remove all parentheses and collapse whitespace for folding
|
|
237
|
+
const flattened = inner.replace(/[()]/g, '').replace(/\s+/g, ' ').trim();
|
|
238
|
+
const foldedFlattened = tryFoldCalcExpression(flattened);
|
|
239
|
+
if (foldedFlattened) {
|
|
240
|
+
return foldedFlattened;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let previous;
|
|
245
|
+
|
|
246
|
+
do {
|
|
247
|
+
previous = inner;
|
|
248
|
+
// Fold innermost parenthesized sub-expressions that aren't function calls
|
|
249
|
+
inner = inner.replace(/(^|[^A-Za-z-])\(([^()]+)\)/g, (match, prefix, content) => {
|
|
250
|
+
const trimmed = content.trim();
|
|
251
|
+
const folded = tryFoldCalcExpression(trimmed);
|
|
252
|
+
if (folded) {
|
|
253
|
+
if (folded.startsWith('calc(') && folded.endsWith(')')) {
|
|
254
|
+
return prefix + trimmed;
|
|
255
|
+
}
|
|
256
|
+
return prefix + folded;
|
|
257
|
+
}
|
|
258
|
+
return prefix + trimmed;
|
|
259
|
+
});
|
|
260
|
+
} while (inner !== previous);
|
|
261
|
+
|
|
262
|
+
const folded = tryFoldCalcExpression(inner);
|
|
263
|
+
if (folded) {
|
|
264
|
+
return folded;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Collapse whitespace and check for percent-division expressions
|
|
268
|
+
const compactInner = inner.replace(/\s+/g, ' ').trim();
|
|
269
|
+
// Preserve percent/number division, just strip whitespace around the operator
|
|
270
|
+
if (/^\d+(?:\.\d+)?%\s*\/\s*\d+(?:\.\d+)?$/i.test(compactInner)) {
|
|
271
|
+
return 'calc(' + compactInner.replace(/\s*\/\s*/g, '/') + ')';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Default: strip whitespace around multiplication/division operators
|
|
275
|
+
return 'calc(' + compactInner.replace(/\s*([*/])\s*/g, '$1') + ')';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export {
|
|
279
|
+
normalizeMathFunctions,
|
|
280
|
+
simplifyStandaloneCalc
|
|
281
|
+
};
|