@thejaredwilcurt/csslop 0.0.37 → 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 +8 -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 +19 -46
- 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 +330 -0
- package/src/rules/normalize.js +80 -0
- package/src/rules/optimize.js +260 -120
- package/src/rules/selectors.js +67 -223
- package/src/rules/stringify.js +18 -13
- 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 +163 -1
- package/src/value/minify.js +139 -52
- 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/rules/selectors.js
CHANGED
|
@@ -2,126 +2,33 @@
|
|
|
2
2
|
* @file Selector minification utilities for CSS rule stringification.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
let currentParameter = '';
|
|
15
|
-
let parenthesisDepth = 0;
|
|
16
|
-
for (const character of parameterString) {
|
|
17
|
-
if (character === '(') {
|
|
18
|
-
parenthesisDepth++;
|
|
19
|
-
} else if (character === ')') {
|
|
20
|
-
parenthesisDepth--;
|
|
21
|
-
}
|
|
22
|
-
if (character === ',' && parenthesisDepth === 0) {
|
|
23
|
-
parameters.push(currentParameter);
|
|
24
|
-
currentParameter = '';
|
|
25
|
-
} else {
|
|
26
|
-
currentParameter += character;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
parameters.push(currentParameter);
|
|
30
|
-
return parameters;
|
|
31
|
-
}
|
|
5
|
+
import {
|
|
6
|
+
findMatchingBracket,
|
|
7
|
+
findMatchingParenthesis,
|
|
8
|
+
findTopLevelDelimiter,
|
|
9
|
+
skipBracketedGroup,
|
|
10
|
+
skipQuotedString,
|
|
11
|
+
splitTopLevel,
|
|
12
|
+
splitTopLevelCommaList
|
|
13
|
+
} from '../parser/source-search.js';
|
|
32
14
|
|
|
33
15
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
16
|
+
* The characters that end one compound selector of a complex selector: the
|
|
17
|
+
* three combinators that join compounds, and the whitespace of the descendant
|
|
18
|
+
* combinator.
|
|
36
19
|
*
|
|
37
|
-
* @
|
|
38
|
-
* @param {number} quoteIndex The index of the opening quote character.
|
|
39
|
-
* @return {number} The index right after the closing quote, or the end of the string if the quote never closes.
|
|
20
|
+
* @type {string}
|
|
40
21
|
*/
|
|
41
|
-
|
|
42
|
-
const quote = text[quoteIndex];
|
|
43
|
-
let index = quoteIndex + 1;
|
|
44
|
-
while (index < text.length) {
|
|
45
|
-
if (text[index] === '\\') {
|
|
46
|
-
index++;
|
|
47
|
-
} else if (text[index] === quote) {
|
|
48
|
-
return index + 1;
|
|
49
|
-
}
|
|
50
|
-
index++;
|
|
51
|
-
}
|
|
52
|
-
return text.length;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Advances past an attribute selector `[...]` in a selector, skipping quoted
|
|
57
|
-
* values and escaped characters so brackets inside them are not miscounted.
|
|
58
|
-
*
|
|
59
|
-
* @param {string} text The selector string being scanned.
|
|
60
|
-
* @param {number} openIndex The index of the `[` that opens the attribute selector.
|
|
61
|
-
* @return {number} The index right after the closing `]`, or the end of the string if it never closes.
|
|
62
|
-
*/
|
|
63
|
-
function skipAttributeSelector (text, openIndex) {
|
|
64
|
-
let index = openIndex + 1;
|
|
65
|
-
while (index < text.length) {
|
|
66
|
-
const character = text[index];
|
|
67
|
-
if (character === '"' || character === '\'') {
|
|
68
|
-
index = skipQuotedText(text, index);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (character === '\\') {
|
|
72
|
-
index += 2;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
if (character === ']') {
|
|
76
|
-
return index + 1;
|
|
77
|
-
}
|
|
78
|
-
index++;
|
|
79
|
-
}
|
|
80
|
-
return text.length;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Finds the index of the closing parenthesis that matches the opening
|
|
85
|
-
* parenthesis at the given position in the string, skipping over quoted
|
|
86
|
-
* strings and attribute selectors so parentheses inside them are ignored.
|
|
87
|
-
*
|
|
88
|
-
* @param {string} text The string to search within.
|
|
89
|
-
* @param {number} openIndex The index of the opening parenthesis.
|
|
90
|
-
* @return {number} The index of the matching closing parenthesis, or -1 if not found.
|
|
91
|
-
*/
|
|
92
|
-
function findMatchingCloseParenthesis (text, openIndex) {
|
|
93
|
-
let depth = 0;
|
|
94
|
-
let index = openIndex;
|
|
95
|
-
while (index < text.length) {
|
|
96
|
-
const character = text[index];
|
|
97
|
-
if (character === '"' || character === '\'') {
|
|
98
|
-
index = skipQuotedText(text, index);
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
if (character === '[') {
|
|
102
|
-
index = skipAttributeSelector(text, index);
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (character === '(') {
|
|
106
|
-
depth++;
|
|
107
|
-
} else if (character === ')') {
|
|
108
|
-
depth--;
|
|
109
|
-
if (depth === 0) {
|
|
110
|
-
return index;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
index++;
|
|
114
|
-
}
|
|
115
|
-
return -1;
|
|
116
|
-
}
|
|
22
|
+
const COMPOUND_SELECTOR_BOUNDARIES = '>+~ \t\n\r\f';
|
|
117
23
|
|
|
118
24
|
/**
|
|
119
25
|
* Finds the next occurrence of a pseudo-class function token (e.g. `:is(`)
|
|
120
26
|
* that sits at the top level of a selector, outside any quoted string or
|
|
121
|
-
* attribute selector.
|
|
27
|
+
* attribute selector. Pseudo-class names are case-insensitive, so the token is
|
|
28
|
+
* matched without regard to case and must be given in lowercase.
|
|
122
29
|
*
|
|
123
30
|
* @param {string} text The selector string to scan.
|
|
124
|
-
* @param {string} functionCall The function token to find, including its opening parenthesis.
|
|
31
|
+
* @param {string} functionCall The lowercase function token to find, including its opening parenthesis.
|
|
125
32
|
* @param {number} start The index to start scanning from.
|
|
126
33
|
* @return {number} The index of the next top-level occurrence, or -1 if none remains.
|
|
127
34
|
*/
|
|
@@ -130,18 +37,24 @@ function findNextFunctionCallOutsideStrings (text, functionCall, start) {
|
|
|
130
37
|
while (index < text.length) {
|
|
131
38
|
const character = text[index];
|
|
132
39
|
if (character === '"' || character === '\'') {
|
|
133
|
-
index =
|
|
40
|
+
index = skipQuotedString(text, index);
|
|
134
41
|
continue;
|
|
135
42
|
}
|
|
136
43
|
if (character === '[') {
|
|
137
|
-
index =
|
|
44
|
+
index = skipBracketedGroup(text, index);
|
|
138
45
|
continue;
|
|
139
46
|
}
|
|
140
47
|
if (character === '\\') {
|
|
141
48
|
index += 2;
|
|
142
49
|
continue;
|
|
143
50
|
}
|
|
144
|
-
|
|
51
|
+
// The token's first character is compared before its whole length is read,
|
|
52
|
+
// so only the few positions that could start it are lowercased
|
|
53
|
+
const startsToken = (
|
|
54
|
+
character === functionCall[0] &&
|
|
55
|
+
text.slice(index, index + functionCall.length).toLowerCase() === functionCall
|
|
56
|
+
);
|
|
57
|
+
if (startsToken) {
|
|
145
58
|
return index;
|
|
146
59
|
}
|
|
147
60
|
index++;
|
|
@@ -201,7 +114,7 @@ function buildWhereCartesianProduct (leftParts, rightParts) {
|
|
|
201
114
|
const products = [];
|
|
202
115
|
for (const leftSelector of leftParts) {
|
|
203
116
|
for (const rightSelector of rightParts) {
|
|
204
|
-
const merged = mergeCompoundSelectors(leftSelector
|
|
117
|
+
const merged = mergeCompoundSelectors(leftSelector, rightSelector);
|
|
205
118
|
if (merged === null) {
|
|
206
119
|
return null;
|
|
207
120
|
}
|
|
@@ -232,7 +145,7 @@ function mergeAdjacentWherePseudoClasses (selector) {
|
|
|
232
145
|
}
|
|
233
146
|
// Index of the '(' in the first ':where('
|
|
234
147
|
const firstOpenParenthesis = whereIndex + 6;
|
|
235
|
-
const firstCloseParenthesis =
|
|
148
|
+
const firstCloseParenthesis = findMatchingParenthesis(result, firstOpenParenthesis);
|
|
236
149
|
if (firstCloseParenthesis === -1) {
|
|
237
150
|
break;
|
|
238
151
|
}
|
|
@@ -244,14 +157,14 @@ function mergeAdjacentWherePseudoClasses (selector) {
|
|
|
244
157
|
}
|
|
245
158
|
// Index of the '(' in the second ':where('
|
|
246
159
|
const secondOpenParenthesis = adjacentStart + 6;
|
|
247
|
-
const secondCloseParenthesis =
|
|
160
|
+
const secondCloseParenthesis = findMatchingParenthesis(result, secondOpenParenthesis);
|
|
248
161
|
if (secondCloseParenthesis === -1) {
|
|
249
162
|
break;
|
|
250
163
|
}
|
|
251
164
|
const firstInnerContent = result.slice(firstOpenParenthesis + 1, firstCloseParenthesis);
|
|
252
165
|
const secondInnerContent = result.slice(secondOpenParenthesis + 1, secondCloseParenthesis);
|
|
253
|
-
const leftParts =
|
|
254
|
-
const rightParts =
|
|
166
|
+
const leftParts = splitTopLevelCommaList(firstInnerContent);
|
|
167
|
+
const rightParts = splitTopLevelCommaList(secondInnerContent);
|
|
255
168
|
const mergedParts = buildWhereCartesianProduct(leftParts, rightParts);
|
|
256
169
|
if (mergedParts === null) {
|
|
257
170
|
position = firstCloseParenthesis + 1;
|
|
@@ -461,7 +374,7 @@ function summarizeCompoundSelector (selector) {
|
|
|
461
374
|
}
|
|
462
375
|
} else if (character === '[') {
|
|
463
376
|
// Attribute selector: well-formed input has a closing bracket
|
|
464
|
-
const closeBracketIndex = selector
|
|
377
|
+
const closeBracketIndex = findMatchingBracket(selector, index);
|
|
465
378
|
if (closeBracketIndex === -1) {
|
|
466
379
|
return null;
|
|
467
380
|
}
|
|
@@ -481,7 +394,7 @@ function summarizeCompoundSelector (selector) {
|
|
|
481
394
|
index = nameEnd;
|
|
482
395
|
if (selector[index] === '(') {
|
|
483
396
|
// Functional pseudo-class: skip past its balanced argument parentheses
|
|
484
|
-
const closeParenthesisIndex =
|
|
397
|
+
const closeParenthesisIndex = findMatchingParenthesis(selector, index);
|
|
485
398
|
if (closeParenthesisIndex === -1) {
|
|
486
399
|
return null;
|
|
487
400
|
}
|
|
@@ -523,30 +436,8 @@ function summarizeCompoundSelector (selector) {
|
|
|
523
436
|
* @return {Array} The compound selector segments, combinators excluded.
|
|
524
437
|
*/
|
|
525
438
|
function splitCombinatorSegments (selector) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
let depth = 0;
|
|
529
|
-
for (const character of selector) {
|
|
530
|
-
if (character === '(' || character === '[') {
|
|
531
|
-
depth++;
|
|
532
|
-
} else if (character === ')' || character === ']') {
|
|
533
|
-
depth--;
|
|
534
|
-
}
|
|
535
|
-
// A combinator at the top level ends the current compound segment;
|
|
536
|
-
// whitespace runs collapse into a single boundary
|
|
537
|
-
if (depth === 0 && (character === '>' || character === '+' || character === '~' || /\s/.test(character))) {
|
|
538
|
-
if (currentSegment) {
|
|
539
|
-
segments.push(currentSegment);
|
|
540
|
-
currentSegment = '';
|
|
541
|
-
}
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
544
|
-
currentSegment += character;
|
|
545
|
-
}
|
|
546
|
-
if (currentSegment) {
|
|
547
|
-
segments.push(currentSegment);
|
|
548
|
-
}
|
|
549
|
-
return segments;
|
|
439
|
+
// A run of boundaries, such as the spaces around a `>`, yields empty segments
|
|
440
|
+
return splitTopLevel(selector, COMPOUND_SELECTOR_BOUNDARIES).filter(Boolean);
|
|
550
441
|
}
|
|
551
442
|
|
|
552
443
|
/**
|
|
@@ -607,18 +498,7 @@ function containsPseudoElement (selector) {
|
|
|
607
498
|
* @return {boolean} True when a top-level combinator is present.
|
|
608
499
|
*/
|
|
609
500
|
function hasTopLevelCombinator (selector) {
|
|
610
|
-
|
|
611
|
-
for (const character of selector) {
|
|
612
|
-
if (character === '(' || character === '[') {
|
|
613
|
-
depth++;
|
|
614
|
-
} else if (character === ')' || character === ']') {
|
|
615
|
-
depth--;
|
|
616
|
-
} else if (depth === 0 && (character === '>' || character === '+' || character === '~' || /\s/.test(character))) {
|
|
617
|
-
// A combinator or whitespace boundary at the top level joins two compounds
|
|
618
|
-
return true;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
return false;
|
|
501
|
+
return findTopLevelDelimiter(selector, COMPOUND_SELECTOR_BOUNDARIES, 0) !== -1;
|
|
622
502
|
}
|
|
623
503
|
|
|
624
504
|
/**
|
|
@@ -666,13 +546,13 @@ function unwrapSingleArgumentIsFunctions (selector, hasSiblingSelectors) {
|
|
|
666
546
|
break;
|
|
667
547
|
}
|
|
668
548
|
const openParenthesisIndex = isIndex + 3;
|
|
669
|
-
const closeParenthesisIndex =
|
|
549
|
+
const closeParenthesisIndex = findMatchingParenthesis(result, openParenthesisIndex);
|
|
670
550
|
if (closeParenthesisIndex === -1) {
|
|
671
551
|
break;
|
|
672
552
|
}
|
|
673
553
|
const content = result.slice(openParenthesisIndex + 1, closeParenthesisIndex);
|
|
674
554
|
const innerSelector = content.trim();
|
|
675
|
-
const parts =
|
|
555
|
+
const parts = splitTopLevelCommaList(content);
|
|
676
556
|
if (parts.length !== 1 || !innerSelector) {
|
|
677
557
|
position = closeParenthesisIndex + 1;
|
|
678
558
|
continue;
|
|
@@ -727,11 +607,7 @@ function canDecomposeIsSelector (parts) {
|
|
|
727
607
|
}
|
|
728
608
|
const summaries = [];
|
|
729
609
|
for (const part of parts) {
|
|
730
|
-
const
|
|
731
|
-
if (!trimmedPart) {
|
|
732
|
-
return false;
|
|
733
|
-
}
|
|
734
|
-
const summary = summarizeCompoundSelector(trimmedPart);
|
|
610
|
+
const summary = summarizeCompoundSelector(part);
|
|
735
611
|
if (!summary || !summary.recognizable || summary.hasPseudoElement) {
|
|
736
612
|
return false;
|
|
737
613
|
}
|
|
@@ -747,6 +623,25 @@ function canDecomposeIsSelector (parts) {
|
|
|
747
623
|
});
|
|
748
624
|
}
|
|
749
625
|
|
|
626
|
+
/**
|
|
627
|
+
* Reads the selector list of an `:is()` that makes up the whole of a selector,
|
|
628
|
+
* which is the only shape of `:is()` whose parts can be lifted out of it.
|
|
629
|
+
*
|
|
630
|
+
* @param {string} selector A minified CSS selector string.
|
|
631
|
+
* @return {Array|null} The selectors inside the `:is()`, or null when the selector is anything more than one `:is()`.
|
|
632
|
+
*/
|
|
633
|
+
function readWholeSelectorIsParts (selector) {
|
|
634
|
+
if (!selector.startsWith(':is(')) {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
// The `:is(` token opens its argument list at its fourth character
|
|
638
|
+
const closingIndex = findMatchingParenthesis(selector, 3);
|
|
639
|
+
if (closingIndex !== selector.length - 1) {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
return splitTopLevelCommaList(selector.slice(4, -1));
|
|
643
|
+
}
|
|
644
|
+
|
|
750
645
|
/**
|
|
751
646
|
* Processes a selector by unwrapping redundant single-argument `:is()`
|
|
752
647
|
* functions within it, then — for bare `:is()` selectors (where `:is()` is the
|
|
@@ -765,43 +660,10 @@ function processIsSelector (selector, hasSiblingSelectors) {
|
|
|
765
660
|
selector = selector.replace(/:is\(:visited,:link\)/g, ':any-link');
|
|
766
661
|
selector = unwrapSingleArgumentIsFunctions(selector, hasSiblingSelectors);
|
|
767
662
|
// Only process bare :is() selectors (where :is() is the entire selector)
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
}
|
|
771
|
-
let depth = 0;
|
|
772
|
-
let closingIndex = -1;
|
|
773
|
-
for (let index = 4; index < selector.length; index++) {
|
|
774
|
-
if (selector[index] === '(') {
|
|
775
|
-
depth++;
|
|
776
|
-
} else if (selector[index] === ')') {
|
|
777
|
-
if (depth === 0) {
|
|
778
|
-
closingIndex = index;
|
|
779
|
-
break;
|
|
780
|
-
}
|
|
781
|
-
depth--;
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
if (closingIndex !== selector.length - 1) {
|
|
663
|
+
let parts = readWholeSelectorIsParts(selector);
|
|
664
|
+
if (!parts) {
|
|
785
665
|
return [selector];
|
|
786
666
|
}
|
|
787
|
-
const content = selector.slice(4, -1);
|
|
788
|
-
let parts = [];
|
|
789
|
-
let currentPart = '';
|
|
790
|
-
let parenDepth = 0;
|
|
791
|
-
for (const character of content) {
|
|
792
|
-
if (character === '(') {
|
|
793
|
-
parenDepth++;
|
|
794
|
-
} else if (character === ')') {
|
|
795
|
-
parenDepth--;
|
|
796
|
-
}
|
|
797
|
-
if (character === ',' && parenDepth === 0) {
|
|
798
|
-
parts.push(currentPart);
|
|
799
|
-
currentPart = '';
|
|
800
|
-
} else {
|
|
801
|
-
currentPart += character;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
parts.push(currentPart);
|
|
805
667
|
// Replace :link + :visited with :any-link
|
|
806
668
|
const hasLink = parts.includes(':link');
|
|
807
669
|
const hasVisited = parts.includes(':visited');
|
|
@@ -819,11 +681,11 @@ function processIsSelector (selector, hasSiblingSelectors) {
|
|
|
819
681
|
parts.sort();
|
|
820
682
|
// Unwrap :is() with a single selector
|
|
821
683
|
if (parts.length === 1) {
|
|
822
|
-
const onlyPart = parts[0]
|
|
684
|
+
const onlyPart = parts[0];
|
|
823
685
|
const canUnwrap = hasSiblingSelectors ?
|
|
824
686
|
isUniversallyRecognizableSelector(onlyPart) :
|
|
825
687
|
!containsPseudoElement(onlyPart);
|
|
826
|
-
return canUnwrap ? [onlyPart] : [':is(' +
|
|
688
|
+
return canUnwrap ? [onlyPart] : [':is(' + onlyPart + ')'];
|
|
827
689
|
}
|
|
828
690
|
// Drop the :is() wrapper when the parts are equivalent as a plain selector list
|
|
829
691
|
if (canDecomposeIsSelector(parts)) {
|
|
@@ -844,29 +706,10 @@ function processIsSelector (selector, hasSiblingSelectors) {
|
|
|
844
706
|
* @return {Array} The flattened selector parts, or the original selector.
|
|
845
707
|
*/
|
|
846
708
|
function flattenNestingParentIsSelector (selector) {
|
|
847
|
-
|
|
709
|
+
const parts = readWholeSelectorIsParts(selector);
|
|
710
|
+
if (!parts) {
|
|
848
711
|
return [selector];
|
|
849
712
|
}
|
|
850
|
-
let depth = 0;
|
|
851
|
-
let closingIndex = -1;
|
|
852
|
-
for (let index = 4; index < selector.length; index++) {
|
|
853
|
-
if (selector[index] === '(') {
|
|
854
|
-
depth++;
|
|
855
|
-
} else if (selector[index] === ')') {
|
|
856
|
-
if (depth === 0) {
|
|
857
|
-
closingIndex = index;
|
|
858
|
-
break;
|
|
859
|
-
}
|
|
860
|
-
depth--;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
// The :is() must span the entire selector to be safely liftable
|
|
864
|
-
if (closingIndex !== selector.length - 1) {
|
|
865
|
-
return [selector];
|
|
866
|
-
}
|
|
867
|
-
const parts = splitParametersByComma(selector.slice(4, -1)).map((part) => {
|
|
868
|
-
return part.trim();
|
|
869
|
-
});
|
|
870
713
|
const hasPotentiallyUnsupportedPart = parts.some((part) => {
|
|
871
714
|
return part.includes(':');
|
|
872
715
|
});
|
|
@@ -875,9 +718,10 @@ function flattenNestingParentIsSelector (selector) {
|
|
|
875
718
|
}
|
|
876
719
|
return parts;
|
|
877
720
|
}
|
|
721
|
+
|
|
878
722
|
export {
|
|
723
|
+
findNextFunctionCallOutsideStrings,
|
|
879
724
|
flattenNestingParentIsSelector,
|
|
880
725
|
mergeAdjacentWherePseudoClasses,
|
|
881
|
-
processIsSelector
|
|
882
|
-
splitParametersByComma
|
|
726
|
+
processIsSelector
|
|
883
727
|
};
|
package/src/rules/stringify.js
CHANGED
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { processDeclarations } from '../declarations/process.js';
|
|
6
|
-
import {
|
|
6
|
+
import { splitTopLevelCommaList } from '../parser/source-search.js';
|
|
7
|
+
import { minifyValueForOutput } from '../value/minify.js';
|
|
7
8
|
|
|
8
9
|
import {
|
|
9
10
|
collapseCustomPropertyWhitespace,
|
|
10
11
|
processCustomPropertyComments
|
|
11
12
|
} from './custom-properties.js';
|
|
13
|
+
import {
|
|
14
|
+
combineLanguageSelectors,
|
|
15
|
+
minifyLanguageSelector
|
|
16
|
+
} from './lang.js';
|
|
12
17
|
import {
|
|
13
18
|
canUnwrapSupports,
|
|
14
19
|
normalizeLayerNames,
|
|
@@ -21,8 +26,7 @@ import { resolvePropertyDescriptors } from './property.js';
|
|
|
21
26
|
import {
|
|
22
27
|
flattenNestingParentIsSelector,
|
|
23
28
|
mergeAdjacentWherePseudoClasses,
|
|
24
|
-
processIsSelector
|
|
25
|
-
splitParametersByComma
|
|
29
|
+
processIsSelector
|
|
26
30
|
} from './selectors.js';
|
|
27
31
|
|
|
28
32
|
/**
|
|
@@ -37,7 +41,7 @@ function stringifyDeclarations (declarations) {
|
|
|
37
41
|
return declaration.type !== 'whitespace' && declaration.type !== 'comment' && declaration.property;
|
|
38
42
|
})
|
|
39
43
|
.map((declaration) => {
|
|
40
|
-
return [declaration.property, ':',
|
|
44
|
+
return [declaration.property, ':', minifyValueForOutput(declaration)].join('');
|
|
41
45
|
})
|
|
42
46
|
.join(';');
|
|
43
47
|
}
|
|
@@ -99,10 +103,9 @@ function minifyFunctionPrelude (prelude) {
|
|
|
99
103
|
const functionName = trimmedPrelude.slice(0, openParenIndex);
|
|
100
104
|
const closeParenIndex = trimmedPrelude.lastIndexOf(')');
|
|
101
105
|
const innerContent = trimmedPrelude.slice(openParenIndex + 1, closeParenIndex);
|
|
102
|
-
const
|
|
103
|
-
const minifiedParameters = parameters.map((parameter) => {
|
|
106
|
+
const minifiedParameters = splitTopLevelCommaList(innerContent).map((parameter) => {
|
|
104
107
|
// Collapse whitespace around the colon separating parameter name from default value
|
|
105
|
-
return parameter.
|
|
108
|
+
return parameter.replace(/\s*:\s*/, ':');
|
|
106
109
|
});
|
|
107
110
|
return functionName + '(' + minifiedParameters.join(',') + ')';
|
|
108
111
|
}
|
|
@@ -251,6 +254,7 @@ function stringifyRule (rule, context) {
|
|
|
251
254
|
// Remove redundant leading "& " nesting selector
|
|
252
255
|
minified = minified.replace(/^& /, '');
|
|
253
256
|
minified = mergeAdjacentWherePseudoClasses(minified);
|
|
257
|
+
minified = minifyLanguageSelector(minified);
|
|
254
258
|
return minified;
|
|
255
259
|
});
|
|
256
260
|
// When this rule is a nesting parent, its whole selector list is treated
|
|
@@ -267,6 +271,7 @@ function stringifyRule (rule, context) {
|
|
|
267
271
|
return processIsSelector(selector, hasSiblingSelectors);
|
|
268
272
|
});
|
|
269
273
|
uniqueSelectors = [...new Set(uniqueSelectors)];
|
|
274
|
+
uniqueSelectors = combineLanguageSelectors(uniqueSelectors);
|
|
270
275
|
output.push(uniqueSelectors.join(','));
|
|
271
276
|
}
|
|
272
277
|
output.push('{');
|
|
@@ -295,7 +300,7 @@ function stringifyRule (rule, context) {
|
|
|
295
300
|
if (property.startsWith('--')) {
|
|
296
301
|
const syntax = context.registeredCustomPropertySyntax.get(property);
|
|
297
302
|
if (syntax === '"<color>"') {
|
|
298
|
-
value =
|
|
303
|
+
value = minifyValueForOutput(declaration);
|
|
299
304
|
} else {
|
|
300
305
|
const rawValue = declaration.rawValue || declaration.value || '';
|
|
301
306
|
const commentProcessedValue = processCustomPropertyComments(rawValue);
|
|
@@ -323,7 +328,7 @@ function stringifyRule (rule, context) {
|
|
|
323
328
|
}
|
|
324
329
|
}
|
|
325
330
|
} else {
|
|
326
|
-
value =
|
|
331
|
+
value = minifyValueForOutput(declaration);
|
|
327
332
|
}
|
|
328
333
|
return [property, ':', value].join('');
|
|
329
334
|
})
|
|
@@ -362,7 +367,7 @@ function stringifyRule (rule, context) {
|
|
|
362
367
|
return item.type !== 'declaration';
|
|
363
368
|
});
|
|
364
369
|
const renderedDeclarations = mediaDeclarations.map((declaration) => {
|
|
365
|
-
return [unescapeIdent(declaration.property), ':',
|
|
370
|
+
return [unescapeIdent(declaration.property), ':', minifyValueForOutput(declaration)].join('');
|
|
366
371
|
}).join(';');
|
|
367
372
|
const renderedRules = stringifyChildRules(subRules, context);
|
|
368
373
|
const children = [renderedDeclarations, renderedRules].filter(Boolean).join('');
|
|
@@ -440,7 +445,7 @@ function stringifyRule (rule, context) {
|
|
|
440
445
|
return declaration.type !== 'whitespace' && declaration.type !== 'comment';
|
|
441
446
|
})
|
|
442
447
|
?.map((declaration) => {
|
|
443
|
-
return [declaration.property, ':',
|
|
448
|
+
return [declaration.property, ':', minifyValueForOutput(declaration)].join('');
|
|
444
449
|
})
|
|
445
450
|
.join(';') || '';
|
|
446
451
|
output.push(renderedKeyframeDeclarations);
|
|
@@ -558,7 +563,7 @@ function stringifyRule (rule, context) {
|
|
|
558
563
|
return innerDeclaration.type !== 'whitespace' && innerDeclaration.property;
|
|
559
564
|
})
|
|
560
565
|
.map((innerDeclaration) => {
|
|
561
|
-
return [unescapeIdent(innerDeclaration.property), ':',
|
|
566
|
+
return [unescapeIdent(innerDeclaration.property), ':', minifyValueForOutput(innerDeclaration)].join('');
|
|
562
567
|
})
|
|
563
568
|
.join(';');
|
|
564
569
|
if (innerDeclarations) {
|
|
@@ -567,7 +572,7 @@ function stringifyRule (rule, context) {
|
|
|
567
572
|
return [];
|
|
568
573
|
}
|
|
569
574
|
if (declaration.property) {
|
|
570
|
-
return [[unescapeIdent(declaration.property), ':',
|
|
575
|
+
return [[unescapeIdent(declaration.property), ':', minifyValueForOutput(declaration)].join('')];
|
|
571
576
|
}
|
|
572
577
|
return [];
|
|
573
578
|
});
|
package/src/value/color-mix.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* @file Evaluates CSS color-mix() expressions and normalizes them into compact color values.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
findMatchingParenthesis,
|
|
7
|
+
splitTopLevelCommaList
|
|
8
|
+
} from '../parser/source-search.js';
|
|
9
|
+
|
|
5
10
|
import {
|
|
6
11
|
convertOklchToHex,
|
|
7
12
|
oklabToRgb,
|
|
@@ -87,7 +92,7 @@ function findNoneChannels (rawColorStr) {
|
|
|
87
92
|
function evaluateNColorMix (colorSpace, args) {
|
|
88
93
|
const parsedArgs = [];
|
|
89
94
|
for (const arg of args) {
|
|
90
|
-
const parsed = parseColorMixArg(arg
|
|
95
|
+
const parsed = parseColorMixArg(arg);
|
|
91
96
|
if (!parsed) {
|
|
92
97
|
return null;
|
|
93
98
|
}
|
|
@@ -252,7 +257,7 @@ function mixNColorsOklab (colors, weights, alphaMultiplier) {
|
|
|
252
257
|
function evaluateColorMix (expr) {
|
|
253
258
|
// Parse: color-mix(in <space> [<hue-method>], <color> [<p>%], <color> [<p>%])
|
|
254
259
|
// We need to handle nested parentheses for inner color functions
|
|
255
|
-
const inner =
|
|
260
|
+
const inner = extractFunctionArgumentText(expr, 'color-mix');
|
|
256
261
|
if (!inner) {
|
|
257
262
|
return null;
|
|
258
263
|
}
|
|
@@ -267,7 +272,7 @@ function evaluateColorMix (expr) {
|
|
|
267
272
|
const rest = inner.slice(inMatch[0].length);
|
|
268
273
|
|
|
269
274
|
// Split color arguments (handling nested parens)
|
|
270
|
-
const args =
|
|
275
|
+
const args = splitTopLevelCommaList(rest);
|
|
271
276
|
if (args.length < 2) {
|
|
272
277
|
return null;
|
|
273
278
|
}
|
|
@@ -278,8 +283,8 @@ function evaluateColorMix (expr) {
|
|
|
278
283
|
}
|
|
279
284
|
|
|
280
285
|
// Parse each argument: "<color> [<percentage>]"
|
|
281
|
-
const parsed1 = parseColorMixArg(args[0]
|
|
282
|
-
const parsed2 = parseColorMixArg(args[1]
|
|
286
|
+
const parsed1 = parseColorMixArg(args[0]);
|
|
287
|
+
const parsed2 = parseColorMixArg(args[1]);
|
|
283
288
|
if (!parsed1 || !parsed2) {
|
|
284
289
|
return null;
|
|
285
290
|
}
|
|
@@ -385,53 +390,23 @@ function evaluateColorMix (expr) {
|
|
|
385
390
|
}
|
|
386
391
|
|
|
387
392
|
/**
|
|
388
|
-
* Extract the
|
|
393
|
+
* Extract the arguments written inside a function call.
|
|
389
394
|
*
|
|
390
|
-
* @param {string}
|
|
391
|
-
* @param {string}
|
|
392
|
-
* @return {string|null}
|
|
395
|
+
* @param {string} expression The expression string containing the function call.
|
|
396
|
+
* @param {string} functionName The function name to locate (e.g. "color-mix").
|
|
397
|
+
* @return {string|null} The text between the function's parentheses, or null when the function is not called.
|
|
393
398
|
*/
|
|
394
|
-
function
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
if (start === -1) {
|
|
399
|
+
function extractFunctionArgumentText (expression, functionName) {
|
|
400
|
+
const callIndex = expression.indexOf(functionName + '(');
|
|
401
|
+
if (callIndex === -1) {
|
|
398
402
|
return null;
|
|
399
403
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
depth++;
|
|
405
|
-
} else if (expr[position] === ')') {
|
|
406
|
-
depth--;
|
|
407
|
-
}
|
|
408
|
-
position++;
|
|
409
|
-
}
|
|
410
|
-
return expr.slice(start + prefix.length, position - 1);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Split color-mix arguments at top-level commas (handling nested parens).
|
|
415
|
-
*
|
|
416
|
-
* @param {string} str The color arguments string, with arguments separated by commas.
|
|
417
|
-
* @return {Array} An array of argument strings split at each top-level comma.
|
|
418
|
-
*/
|
|
419
|
-
function splitColorMixArgs (str) {
|
|
420
|
-
const args = [];
|
|
421
|
-
let depth = 0;
|
|
422
|
-
let start = 0;
|
|
423
|
-
for (let position = 0; position < str.length; position++) {
|
|
424
|
-
if (str[position] === '(') {
|
|
425
|
-
depth++;
|
|
426
|
-
} else if (str[position] === ')') {
|
|
427
|
-
depth--;
|
|
428
|
-
} else if (str[position] === ',' && depth === 0) {
|
|
429
|
-
args.push(str.slice(start, position));
|
|
430
|
-
start = position + 1;
|
|
431
|
-
}
|
|
404
|
+
const openIndex = callIndex + functionName.length;
|
|
405
|
+
const closeIndex = findMatchingParenthesis(expression, openIndex);
|
|
406
|
+
if (closeIndex === -1) {
|
|
407
|
+
return expression.slice(openIndex + 1);
|
|
432
408
|
}
|
|
433
|
-
|
|
434
|
-
return args;
|
|
409
|
+
return expression.slice(openIndex + 1, closeIndex);
|
|
435
410
|
}
|
|
436
411
|
|
|
437
412
|
/**
|