@thejaredwilcurt/csslop 0.0.38 → 0.0.39
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 +7 -2
- package/package.json +7 -6
- package/src/declarations/background.js +29 -37
- package/src/declarations/border.js +1 -1
- package/src/declarations/fold.js +1 -1
- package/src/declarations/process.js +17 -8
- package/src/declarations/shorthand-values.js +1 -1
- package/src/index.js +15 -44
- package/src/parser/at-rules.js +291 -0
- package/src/parser/index.js +826 -0
- package/src/parser/positions.js +71 -0
- package/src/parser/scanner.js +229 -0
- package/src/parser/source-search.js +287 -0
- package/src/parser/tokenizer.js +396 -0
- package/src/preprocess.js +20 -228
- package/src/rules/lang.js +20 -56
- package/src/rules/normalize.js +80 -0
- package/src/rules/optimize.js +183 -110
- package/src/rules/selectors.js +56 -221
- package/src/rules/stringify.js +4 -5
- package/src/value/color-mix.js +22 -47
- package/src/value/gradients.js +44 -71
- package/src/value/light-dark.js +9 -94
- package/src/value/math.js +3 -23
- package/src/value/minify.js +84 -51
- package/src/value/relative-colors.js +15 -78
- package/src/value/shared.js +1 -0
- package/src/value/tokens.js +4 -387
- package/src/value/transforms.js +20 -53
- package/src/value/syntax.js +0 -144
package/src/value/gradients.js
CHANGED
|
@@ -2,40 +2,16 @@
|
|
|
2
2
|
* @file Parses and minifies CSS gradient function calls by splitting arguments, normalizing default directions, and removing redundant stop positions.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
findMatchingParenthesis,
|
|
7
|
+
splitTopLevelCommaList
|
|
8
|
+
} from '../parser/source-search.js';
|
|
9
|
+
|
|
5
10
|
import {
|
|
6
11
|
parseHex,
|
|
7
12
|
shortestColor
|
|
8
13
|
} from './colors.js';
|
|
9
14
|
|
|
10
|
-
/**
|
|
11
|
-
* Splits a gradient function's argument string at top-level commas, correctly handling nested parentheses.
|
|
12
|
-
*
|
|
13
|
-
* @param {string} argumentString The raw gradient arguments string.
|
|
14
|
-
* @return {Array} An array of trimmed argument strings.
|
|
15
|
-
*/
|
|
16
|
-
function splitGradientArgs (argumentString) {
|
|
17
|
-
const parts = [];
|
|
18
|
-
let depth = 0;
|
|
19
|
-
let current = '';
|
|
20
|
-
for (const character of argumentString) {
|
|
21
|
-
if (character === '(') {
|
|
22
|
-
depth++;
|
|
23
|
-
} else if (character === ')') {
|
|
24
|
-
depth--;
|
|
25
|
-
}
|
|
26
|
-
if (character === ',' && depth === 0) {
|
|
27
|
-
parts.push(current.trim());
|
|
28
|
-
current = '';
|
|
29
|
-
} else {
|
|
30
|
-
current += character;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (current.trim() || parts.length) {
|
|
34
|
-
parts.push(current.trim());
|
|
35
|
-
}
|
|
36
|
-
return parts;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
15
|
/**
|
|
40
16
|
* Checks whether a string is a valid gradient stop position consisting of one
|
|
41
17
|
* or two numeric tokens with optional CSS units.
|
|
@@ -435,45 +411,45 @@ function normalizeRadialGradientShape (args) {
|
|
|
435
411
|
/**
|
|
436
412
|
* Optimizes gradient arguments by removing default direction or shape keywords, combining adjacent identical color stops, and trimming redundant 0% or 100% stop positions from the first and last stops.
|
|
437
413
|
*
|
|
438
|
-
* @param {string}
|
|
439
|
-
* @param {string}
|
|
440
|
-
* @return {string}
|
|
414
|
+
* @param {string} functionName The gradient function name (e.g. "linear-gradient").
|
|
415
|
+
* @param {string} argumentText The raw comma-separated gradient arguments string.
|
|
416
|
+
* @return {string} The optimized gradient arguments string.
|
|
441
417
|
*/
|
|
442
|
-
function
|
|
443
|
-
const
|
|
444
|
-
const
|
|
418
|
+
function processGradientArguments (functionName, argumentText) {
|
|
419
|
+
const argumentList = splitTopLevelCommaList(argumentText);
|
|
420
|
+
const lowercaseName = functionName.toLowerCase();
|
|
445
421
|
|
|
446
|
-
let
|
|
422
|
+
let directionArgumentCount = 0;
|
|
447
423
|
|
|
448
|
-
if (
|
|
449
|
-
if (
|
|
450
|
-
|
|
451
|
-
} else if (
|
|
452
|
-
|
|
424
|
+
if (argumentList.length > 1) {
|
|
425
|
+
if (lowercaseName.includes('linear')) {
|
|
426
|
+
directionArgumentCount = normalizeLinearGradientDirection(argumentList);
|
|
427
|
+
} else if (lowercaseName.includes('radial')) {
|
|
428
|
+
directionArgumentCount = normalizeRadialGradientShape(argumentList);
|
|
453
429
|
}
|
|
454
430
|
}
|
|
455
431
|
|
|
456
|
-
// Extract color stop
|
|
457
|
-
const
|
|
458
|
-
return normalizeColorStop(
|
|
432
|
+
// Extract color stop arguments (everything after the direction/shape argument)
|
|
433
|
+
const colorStopArguments = argumentList.slice(directionArgumentCount).map((argument) => {
|
|
434
|
+
return normalizeColorStop(argument);
|
|
459
435
|
});
|
|
460
|
-
if (
|
|
461
|
-
const normalizedStops =
|
|
462
|
-
combineAdjacentIdenticalStops(
|
|
463
|
-
|
|
464
|
-
|
|
436
|
+
if (colorStopArguments.length > 0) {
|
|
437
|
+
const normalizedStops = colorStopArguments.length >= 2 ?
|
|
438
|
+
combineAdjacentIdenticalStops(colorStopArguments) :
|
|
439
|
+
colorStopArguments;
|
|
440
|
+
argumentList.splice(directionArgumentCount, colorStopArguments.length, ...normalizedStops);
|
|
465
441
|
}
|
|
466
442
|
|
|
467
|
-
if (
|
|
468
|
-
const firstStopIndex =
|
|
469
|
-
const lastStopIndex =
|
|
443
|
+
if (argumentList.length > directionArgumentCount) {
|
|
444
|
+
const firstStopIndex = directionArgumentCount;
|
|
445
|
+
const lastStopIndex = argumentList.length - 1;
|
|
470
446
|
// Remove default 0% stop position from the first gradient stop
|
|
471
|
-
|
|
447
|
+
argumentList[firstStopIndex] = argumentList[firstStopIndex].replace(/^(.*\S)\s+0%$/, '$1');
|
|
472
448
|
// Remove default 100% stop position from the last gradient stop
|
|
473
|
-
|
|
449
|
+
argumentList[lastStopIndex] = argumentList[lastStopIndex].replace(/^(.*\S)\s+100%$/, '$1');
|
|
474
450
|
}
|
|
475
451
|
|
|
476
|
-
return
|
|
452
|
+
return argumentList.join(',');
|
|
477
453
|
}
|
|
478
454
|
|
|
479
455
|
/**
|
|
@@ -490,24 +466,21 @@ function minifyGradients (value) {
|
|
|
490
466
|
// Match gradient function names: linear-gradient, radial-gradient, conic-gradient, and their repeating- variants
|
|
491
467
|
const gradientMatch = rest.match(/^((?:repeating-)?(?:linear|radial|conic)-gradient)\(/i);
|
|
492
468
|
if (gradientMatch) {
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
depth--;
|
|
501
|
-
}
|
|
502
|
-
end++;
|
|
469
|
+
const functionName = gradientMatch[1];
|
|
470
|
+
const openIndex = position + functionName.length;
|
|
471
|
+
const closeIndex = findMatchingParenthesis(value, openIndex);
|
|
472
|
+
if (closeIndex === -1) {
|
|
473
|
+
// A gradient whose arguments never close is left exactly as it was written
|
|
474
|
+
result += value.slice(position);
|
|
475
|
+
break;
|
|
503
476
|
}
|
|
504
|
-
const
|
|
505
|
-
result +=
|
|
506
|
-
position =
|
|
507
|
-
|
|
508
|
-
result += value[position];
|
|
509
|
-
position++;
|
|
477
|
+
const argumentText = value.slice(openIndex + 1, closeIndex);
|
|
478
|
+
result += functionName + '(' + processGradientArguments(functionName, argumentText) + ')';
|
|
479
|
+
position = closeIndex + 1;
|
|
480
|
+
continue;
|
|
510
481
|
}
|
|
482
|
+
result += value[position];
|
|
483
|
+
position++;
|
|
511
484
|
}
|
|
512
485
|
return result;
|
|
513
486
|
}
|
package/src/value/light-dark.js
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
* @file Simplifies CSS light-dark functions whose light and dark values are equivalent.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
findMatchingParenthesis,
|
|
7
|
+
skipQuotedString,
|
|
8
|
+
splitTopLevelCommaList,
|
|
9
|
+
startsUrlToken
|
|
10
|
+
} from '../parser/source-search.js';
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* Determines whether a character can appear inside a CSS identifier.
|
|
@@ -35,75 +40,6 @@ function startsLightDarkFunction (value, index) {
|
|
|
35
40
|
return !isIdentifierCharacter(value[index - 1]);
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
/**
|
|
39
|
-
* Splits a CSS function argument list at top-level commas while respecting
|
|
40
|
-
* nested parentheses and quoted strings.
|
|
41
|
-
*
|
|
42
|
-
* @param {string} argumentString The raw content between function parentheses.
|
|
43
|
-
* @return {Array} The top-level argument strings.
|
|
44
|
-
*/
|
|
45
|
-
function splitTopLevelFunctionArguments (argumentString) {
|
|
46
|
-
const argumentsList = [];
|
|
47
|
-
let currentArgument = '';
|
|
48
|
-
let depth = 0;
|
|
49
|
-
let index = 0;
|
|
50
|
-
let activeQuote = '';
|
|
51
|
-
|
|
52
|
-
while (index < argumentString.length) {
|
|
53
|
-
const character = argumentString[index];
|
|
54
|
-
if (activeQuote) {
|
|
55
|
-
currentArgument += character;
|
|
56
|
-
if (character === '\\') {
|
|
57
|
-
if (index + 1 < argumentString.length) {
|
|
58
|
-
currentArgument += argumentString[index + 1];
|
|
59
|
-
index += 2;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
} else if (character === activeQuote) {
|
|
63
|
-
activeQuote = '';
|
|
64
|
-
}
|
|
65
|
-
index++;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (character === '"' || character === '\'') {
|
|
70
|
-
activeQuote = character;
|
|
71
|
-
currentArgument += character;
|
|
72
|
-
index++;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (character === '(') {
|
|
77
|
-
depth++;
|
|
78
|
-
currentArgument += character;
|
|
79
|
-
index++;
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (character === ')') {
|
|
84
|
-
if (depth > 0) {
|
|
85
|
-
depth--;
|
|
86
|
-
}
|
|
87
|
-
currentArgument += character;
|
|
88
|
-
index++;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (character === ',' && depth === 0) {
|
|
93
|
-
argumentsList.push(currentArgument.trim());
|
|
94
|
-
currentArgument = '';
|
|
95
|
-
index++;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
currentArgument += character;
|
|
100
|
-
index++;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
argumentsList.push(currentArgument.trim());
|
|
104
|
-
return argumentsList;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
43
|
/**
|
|
108
44
|
* Simplifies `light-dark(a,b)` to `a` when both top-level arguments are identical
|
|
109
45
|
* after prior minification has normalized them.
|
|
@@ -115,36 +51,15 @@ function simplifyEquivalentLightDarkFunctions (value) {
|
|
|
115
51
|
let result = '';
|
|
116
52
|
let index = 0;
|
|
117
53
|
|
|
118
|
-
const consumeQuoted = (start) => {
|
|
119
|
-
const quote = value[start];
|
|
120
|
-
let end = start + 1;
|
|
121
|
-
while (end < value.length) {
|
|
122
|
-
if (value[end] === '\\') {
|
|
123
|
-
end += 2;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (value[end] === quote) {
|
|
127
|
-
end++;
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
end++;
|
|
131
|
-
}
|
|
132
|
-
return end;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const startsUrl = (start) => {
|
|
136
|
-
return value.slice(start, start + 4).toLowerCase() === 'url(';
|
|
137
|
-
};
|
|
138
|
-
|
|
139
54
|
while (index < value.length) {
|
|
140
55
|
if (value[index] === '"' || value[index] === '\'') {
|
|
141
|
-
const end =
|
|
56
|
+
const end = skipQuotedString(value, index);
|
|
142
57
|
result += value.slice(index, end);
|
|
143
58
|
index = end;
|
|
144
59
|
continue;
|
|
145
60
|
}
|
|
146
61
|
|
|
147
|
-
if (
|
|
62
|
+
if (startsUrlToken(value, index)) {
|
|
148
63
|
const end = findMatchingParenthesis(value, index + 3);
|
|
149
64
|
if (end === -1) {
|
|
150
65
|
result += value.slice(index);
|
|
@@ -164,7 +79,7 @@ function simplifyEquivalentLightDarkFunctions (value) {
|
|
|
164
79
|
}
|
|
165
80
|
|
|
166
81
|
const argumentString = value.slice(openParenIndex + 1, closingParenIndex);
|
|
167
|
-
const argumentsList =
|
|
82
|
+
const argumentsList = splitTopLevelCommaList(argumentString);
|
|
168
83
|
if (argumentsList.length === 2) {
|
|
169
84
|
const firstArgument = simplifyEquivalentLightDarkFunctions(argumentsList[0]);
|
|
170
85
|
const secondArgument = simplifyEquivalentLightDarkFunctions(argumentsList[1]);
|
package/src/value/math.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { calc } from '@csstools/css-calc';
|
|
6
6
|
|
|
7
|
+
import { findMatchingParenthesis } from '../parser/source-search.js';
|
|
8
|
+
|
|
7
9
|
import {
|
|
8
10
|
convertAbsoluteLengthToPx,
|
|
9
11
|
formatDimension,
|
|
@@ -124,28 +126,6 @@ function findFunctionNameStart (value, openIndex) {
|
|
|
124
126
|
return index;
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
/**
|
|
128
|
-
* Finds the parenthesis closing the one at the given index.
|
|
129
|
-
*
|
|
130
|
-
* @param {string} value The CSS value string being scanned.
|
|
131
|
-
* @param {number} openIndex The index of the opening parenthesis.
|
|
132
|
-
* @return {number} The index of the matching closing parenthesis, or -1 when it never closes.
|
|
133
|
-
*/
|
|
134
|
-
function findClosingParenthesis (value, openIndex) {
|
|
135
|
-
let depth = 0;
|
|
136
|
-
for (let index = openIndex; index < value.length; index++) {
|
|
137
|
-
if (value[index] === '(') {
|
|
138
|
-
depth++;
|
|
139
|
-
} else if (value[index] === ')') {
|
|
140
|
-
depth--;
|
|
141
|
-
if (depth === 0) {
|
|
142
|
-
return index;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return -1;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
129
|
/**
|
|
150
130
|
* Rewrites the contents of every math function in a value, leaving the rest of
|
|
151
131
|
* the value untouched. A `*` only means multiplication inside these functions,
|
|
@@ -166,7 +146,7 @@ function rewriteMathFunctionContents (rewriteExpression, value) {
|
|
|
166
146
|
break;
|
|
167
147
|
}
|
|
168
148
|
const functionName = value.slice(findFunctionNameStart(value, openIndex), openIndex).toLowerCase();
|
|
169
|
-
const closeIndex =
|
|
149
|
+
const closeIndex = findMatchingParenthesis(value, openIndex);
|
|
170
150
|
if (!MATH_FUNCTION_NAMES.has(functionName) || closeIndex === -1) {
|
|
171
151
|
result += value.slice(index, openIndex + 1);
|
|
172
152
|
index = openIndex + 1;
|
package/src/value/minify.js
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { isUnicodeCharset } from '../context.js';
|
|
6
|
+
import {
|
|
7
|
+
findMatchingParenthesis,
|
|
8
|
+
skipParenthesizedGroup,
|
|
9
|
+
skipQuotedString,
|
|
10
|
+
startsUrlToken
|
|
11
|
+
} from '../parser/source-search.js';
|
|
6
12
|
import { resolveUnicodeEscape } from '../utilities.js';
|
|
7
13
|
|
|
8
14
|
import { evaluateColorMix } from './color-mix.js';
|
|
@@ -31,12 +37,12 @@ import {
|
|
|
31
37
|
import {
|
|
32
38
|
collapseShorthandParts,
|
|
33
39
|
convertAbsoluteLengthToPx,
|
|
40
|
+
hasSubstitutedParts,
|
|
34
41
|
normalizeScaleComponent,
|
|
35
42
|
parseAlphaString,
|
|
36
43
|
parseAngleToDegrees,
|
|
37
44
|
roundCompactNumber
|
|
38
45
|
} from './shared.js';
|
|
39
|
-
import { findMatchingParenthesis } from './syntax.js';
|
|
40
46
|
import { elideRedundantSeparators } from './tokens.js';
|
|
41
47
|
import { minifyTransformValue } from './transforms.js';
|
|
42
48
|
import { optimizeUnicodeRange } from './unicode-range.js';
|
|
@@ -136,68 +142,24 @@ function splitValueSegments (value) {
|
|
|
136
142
|
const segments = [];
|
|
137
143
|
let index = 0;
|
|
138
144
|
|
|
139
|
-
const consumeQuoted = (start) => {
|
|
140
|
-
const quote = value[start];
|
|
141
|
-
let end = start + 1;
|
|
142
|
-
while (end < value.length) {
|
|
143
|
-
if (value[end] === '\\') {
|
|
144
|
-
end += 2;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (value[end] === quote) {
|
|
148
|
-
end++;
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
end++;
|
|
152
|
-
}
|
|
153
|
-
return end;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
const startsUrl = (start) => {
|
|
157
|
-
return value.slice(start, start + 4).toLowerCase() === 'url(';
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const consumeUrl = (start) => {
|
|
161
|
-
let depth = 1;
|
|
162
|
-
let end = start + 4;
|
|
163
|
-
while (end < value.length && depth > 0) {
|
|
164
|
-
if (value[end] === '"' || value[end] === '\'') {
|
|
165
|
-
end = consumeQuoted(end);
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
// An escaped character is url data, never the parenthesis that ends it
|
|
169
|
-
if (value[end] === '\\') {
|
|
170
|
-
end += 2;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (value[end] === '(') {
|
|
174
|
-
depth++;
|
|
175
|
-
}
|
|
176
|
-
if (value[end] === ')') {
|
|
177
|
-
depth--;
|
|
178
|
-
}
|
|
179
|
-
end++;
|
|
180
|
-
}
|
|
181
|
-
return end;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
145
|
while (index < value.length) {
|
|
185
146
|
if (value[index] === '"' || value[index] === '\'') {
|
|
186
|
-
const end =
|
|
147
|
+
const end = skipQuotedString(value, index);
|
|
187
148
|
segments.push({ text: value.slice(index, end), isLiteral: true });
|
|
188
149
|
index = end;
|
|
189
150
|
continue;
|
|
190
151
|
}
|
|
191
152
|
|
|
192
|
-
if (
|
|
193
|
-
|
|
153
|
+
if (startsUrlToken(value, index)) {
|
|
154
|
+
// The `url(` token opens its parenthesized group at its fourth character
|
|
155
|
+
const end = skipParenthesizedGroup(value, index + 3);
|
|
194
156
|
segments.push({ text: value.slice(index, end), isLiteral: true });
|
|
195
157
|
index = end;
|
|
196
158
|
continue;
|
|
197
159
|
}
|
|
198
160
|
|
|
199
161
|
const start = index;
|
|
200
|
-
while (index < value.length && value[index] !== '"' && value[index] !== '\'' && !
|
|
162
|
+
while (index < value.length && value[index] !== '"' && value[index] !== '\'' && !startsUrlToken(value, index)) {
|
|
201
163
|
index++;
|
|
202
164
|
}
|
|
203
165
|
segments.push({ text: value.slice(start, index), isLiteral: false });
|
|
@@ -836,7 +798,11 @@ const ZERO_INITIAL_PROPERTIES = new Set(['margin', 'padding']);
|
|
|
836
798
|
*
|
|
837
799
|
* @type {Set<string>}
|
|
838
800
|
*/
|
|
839
|
-
const AUTO_INITIAL_PROPERTIES = new Set([
|
|
801
|
+
const AUTO_INITIAL_PROPERTIES = new Set([
|
|
802
|
+
'min-width',
|
|
803
|
+
'min-height',
|
|
804
|
+
'hyphenate-limit-chars'
|
|
805
|
+
]);
|
|
840
806
|
|
|
841
807
|
/**
|
|
842
808
|
* Properties whose initial value is a single keyword longer than the global
|
|
@@ -1008,6 +974,69 @@ function reorderBorderWidthBeforeStyle (value) {
|
|
|
1008
974
|
return value.replace(BORDER_STYLE_BEFORE_WIDTH_PATTERN, '$2 $1');
|
|
1009
975
|
}
|
|
1010
976
|
|
|
977
|
+
/**
|
|
978
|
+
* Matches a trailing `!important` flag, so the components in front of it can be
|
|
979
|
+
* rewritten without the flag being mistaken for part of the last one.
|
|
980
|
+
*
|
|
981
|
+
* @type {RegExp}
|
|
982
|
+
*/
|
|
983
|
+
const IMPORTANT_SUFFIX_PATTERN = /!important$/i;
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Separates a value from the `!important` flag it may end with.
|
|
987
|
+
*
|
|
988
|
+
* @param {string} value The declaration value, possibly flagged important.
|
|
989
|
+
* @return {object} The components in front of the flag, and the flag itself or an empty string.
|
|
990
|
+
*/
|
|
991
|
+
function splitImportantSuffix (value) {
|
|
992
|
+
const match = value.match(IMPORTANT_SUFFIX_PATTERN);
|
|
993
|
+
if (!match) {
|
|
994
|
+
return { components: value, importantSuffix: '' };
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
components: value.slice(0, match.index).trim(),
|
|
998
|
+
importantSuffix: match[0]
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Collapses a `hyphenate-limit-chars` value to its shortest equivalent form.
|
|
1004
|
+
* The property states up to three components: the shortest word that may be
|
|
1005
|
+
* hyphenated, then the fewest characters allowed before a hyphen, then the
|
|
1006
|
+
* fewest allowed after it. An unstated "before" component is read as `auto`,
|
|
1007
|
+
* and an unstated "after" component repeats whatever "before" holds, so a
|
|
1008
|
+
* trailing component that only restates the default it stands in for is
|
|
1009
|
+
* redundant.
|
|
1010
|
+
*
|
|
1011
|
+
* @param {string} value The `hyphenate-limit-chars` value.
|
|
1012
|
+
* @return {string} The value with its redundant trailing components dropped.
|
|
1013
|
+
*/
|
|
1014
|
+
function collapseHyphenateLimitChars (value) {
|
|
1015
|
+
const {
|
|
1016
|
+
components,
|
|
1017
|
+
importantSuffix
|
|
1018
|
+
} = splitImportantSuffix(value);
|
|
1019
|
+
const parts = components.split(' ').filter(Boolean);
|
|
1020
|
+
if (hasSubstitutedParts(parts)) {
|
|
1021
|
+
return value;
|
|
1022
|
+
}
|
|
1023
|
+
const isAfterMatchingBefore = (
|
|
1024
|
+
parts.length === 3 &&
|
|
1025
|
+
parts[2].toLowerCase() === parts[1].toLowerCase()
|
|
1026
|
+
);
|
|
1027
|
+
if (isAfterMatchingBefore) {
|
|
1028
|
+
parts.pop();
|
|
1029
|
+
}
|
|
1030
|
+
const isBeforeLeftAtDefault = (
|
|
1031
|
+
parts.length === 2 &&
|
|
1032
|
+
parts[1].toLowerCase() === 'auto'
|
|
1033
|
+
);
|
|
1034
|
+
if (isBeforeLeftAtDefault) {
|
|
1035
|
+
parts.pop();
|
|
1036
|
+
}
|
|
1037
|
+
return parts.join(' ') + importantSuffix;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1011
1040
|
/**
|
|
1012
1041
|
* Minifies a declaration's value for output, rewriting its initial keyword to
|
|
1013
1042
|
* the shorter global `initial` keyword after shorthand assembly. Both are
|
|
@@ -1301,6 +1330,10 @@ function applyPropertyOptimizations (val, property) {
|
|
|
1301
1330
|
val = collapseShorthandParts(val.split(' ')).join(' ');
|
|
1302
1331
|
}
|
|
1303
1332
|
|
|
1333
|
+
if (property === 'hyphenate-limit-chars') {
|
|
1334
|
+
val = collapseHyphenateLimitChars(val);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1304
1337
|
if (property === 'border-radius') {
|
|
1305
1338
|
const segments = val.split('/').map((segment) => {
|
|
1306
1339
|
return segment.trim();
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* @file Evaluates and minifies CSS relative color syntax.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
findMatchingParenthesis,
|
|
7
|
+
findTopLevelDelimiter,
|
|
8
|
+
splitTopLevelComponents
|
|
9
|
+
} from '../parser/source-search.js';
|
|
10
|
+
|
|
5
11
|
import {
|
|
6
12
|
parseColor,
|
|
7
13
|
rgbaToHex,
|
|
@@ -48,69 +54,6 @@ const RELATIVE_COLOR_CHANNELS = {
|
|
|
48
54
|
oklch: ['l', 'c', 'h']
|
|
49
55
|
};
|
|
50
56
|
|
|
51
|
-
/**
|
|
52
|
-
* Finds the closing parenthesis matching the opening one at the given index,
|
|
53
|
-
* accounting for nested parentheses.
|
|
54
|
-
*
|
|
55
|
-
* @param {string} value The string being scanned.
|
|
56
|
-
* @param {number} openIndex Index of the opening parenthesis.
|
|
57
|
-
* @return {number} Index of the matching close parenthesis, or -1.
|
|
58
|
-
*/
|
|
59
|
-
function findClosingParenthesis (value, openIndex) {
|
|
60
|
-
let depth = 1;
|
|
61
|
-
let index = openIndex + 1;
|
|
62
|
-
while (index < value.length) {
|
|
63
|
-
const character = value[index];
|
|
64
|
-
if (character === '(') {
|
|
65
|
-
depth++;
|
|
66
|
-
} else if (character === ')') {
|
|
67
|
-
depth--;
|
|
68
|
-
if (depth === 0) {
|
|
69
|
-
return index;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
index++;
|
|
73
|
-
}
|
|
74
|
-
return -1;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Splits a string on top-level whitespace while treating each parenthesized
|
|
79
|
-
* group as part of a single token, keeping nested function arguments intact.
|
|
80
|
-
*
|
|
81
|
-
* @param {string} text The text to split.
|
|
82
|
-
* @return {Array} The list of top-level tokens.
|
|
83
|
-
*/
|
|
84
|
-
function splitTopLevelWhitespace (text) {
|
|
85
|
-
const tokens = [];
|
|
86
|
-
let current = '';
|
|
87
|
-
let depth = 0;
|
|
88
|
-
for (let index = 0; index < text.length; index++) {
|
|
89
|
-
const character = text[index];
|
|
90
|
-
if (character === '(') {
|
|
91
|
-
depth++;
|
|
92
|
-
current += character;
|
|
93
|
-
} else if (character === ')') {
|
|
94
|
-
if (depth > 0) {
|
|
95
|
-
depth--;
|
|
96
|
-
}
|
|
97
|
-
current += character;
|
|
98
|
-
// Match any single whitespace character at the top level
|
|
99
|
-
} else if (depth === 0 && /\s/.test(character)) {
|
|
100
|
-
if (current) {
|
|
101
|
-
tokens.push(current);
|
|
102
|
-
current = '';
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
current += character;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (current) {
|
|
109
|
-
tokens.push(current);
|
|
110
|
-
}
|
|
111
|
-
return tokens;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
57
|
/**
|
|
115
58
|
* Splits a relative-color body into its channel portion and its optional alpha
|
|
116
59
|
* portion at the top-level `/` separator.
|
|
@@ -119,20 +62,14 @@ function splitTopLevelWhitespace (text) {
|
|
|
119
62
|
* @return {Array} A two-element array of [channelsPart, alphaPart|null].
|
|
120
63
|
*/
|
|
121
64
|
function splitRelativeAlpha (body) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (character === '(') {
|
|
126
|
-
depth++;
|
|
127
|
-
} else if (character === ')') {
|
|
128
|
-
if (depth > 0) {
|
|
129
|
-
depth--;
|
|
130
|
-
}
|
|
131
|
-
} else if (character === '/' && depth === 0) {
|
|
132
|
-
return [body.slice(0, index).trim(), body.slice(index + 1).trim()];
|
|
133
|
-
}
|
|
65
|
+
const separatorIndex = findTopLevelDelimiter(body, '/', 0);
|
|
66
|
+
if (separatorIndex === -1) {
|
|
67
|
+
return [body.trim(), null];
|
|
134
68
|
}
|
|
135
|
-
return [
|
|
69
|
+
return [
|
|
70
|
+
body.slice(0, separatorIndex).trim(),
|
|
71
|
+
body.slice(separatorIndex + 1).trim()
|
|
72
|
+
];
|
|
136
73
|
}
|
|
137
74
|
|
|
138
75
|
/**
|
|
@@ -276,7 +213,7 @@ function rewriteRelativeColor (functionName, inner) {
|
|
|
276
213
|
// Strip the leading `from` keyword, allowing it to abut the base color
|
|
277
214
|
const body = inner.replace(/^\s*from\b\s*/i, '').trim();
|
|
278
215
|
const [channelsPart, alphaRaw] = splitRelativeAlpha(body);
|
|
279
|
-
const tokens =
|
|
216
|
+
const tokens = splitTopLevelComponents(channelsPart);
|
|
280
217
|
if (tokens.length < 2) {
|
|
281
218
|
return functionName + '(' + inner + ')';
|
|
282
219
|
}
|
|
@@ -316,7 +253,7 @@ function minifyRelativeColorSyntax (value) {
|
|
|
316
253
|
if (match && !precededByIdentifier) {
|
|
317
254
|
const functionName = match[1].toLowerCase();
|
|
318
255
|
const openParenIndex = index + match[1].length;
|
|
319
|
-
const closingParenIndex =
|
|
256
|
+
const closingParenIndex = findMatchingParenthesis(value, openParenIndex);
|
|
320
257
|
if (closingParenIndex !== -1) {
|
|
321
258
|
const inner = value.slice(openParenIndex + 1, closingParenIndex);
|
|
322
259
|
result += rewriteRelativeColor(functionName, inner);
|