@thejaredwilcurt/csslop 0.0.38 → 0.0.40
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 +12 -11
- 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/custom-properties.js +86 -9
- package/src/rules/lang.js +20 -56
- package/src/rules/normalize.js +80 -0
- package/src/rules/optimize.js +271 -111
- 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 +271 -76
- package/src/value/minify.js +89 -54
- package/src/value/relative-colors.js +15 -78
- package/src/value/shared.js +53 -10
- 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,118 +2,24 @@
|
|
|
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
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Advances past a quoted string in a selector, honoring backslash escapes so a
|
|
35
|
-
* quoted close-quote does not end the skip early.
|
|
36
|
-
*
|
|
37
|
-
* @param {string} text The selector string being scanned.
|
|
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.
|
|
40
|
-
*/
|
|
41
|
-
function skipQuotedText (text, quoteIndex) {
|
|
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
|
-
}
|
|
5
|
+
import {
|
|
6
|
+
findMatchingBracket,
|
|
7
|
+
findMatchingParenthesis,
|
|
8
|
+
findTopLevelDelimiter,
|
|
9
|
+
skipBracketedGroup,
|
|
10
|
+
skipQuotedString,
|
|
11
|
+
splitTopLevel,
|
|
12
|
+
splitTopLevelCommaList
|
|
13
|
+
} from '../parser/source-search.js';
|
|
82
14
|
|
|
83
15
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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.
|
|
87
19
|
*
|
|
88
|
-
* @
|
|
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.
|
|
20
|
+
* @type {string}
|
|
91
21
|
*/
|
|
92
|
-
|
|
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(`)
|
|
@@ -131,11 +37,11 @@ function findNextFunctionCallOutsideStrings (text, functionCall, start) {
|
|
|
131
37
|
while (index < text.length) {
|
|
132
38
|
const character = text[index];
|
|
133
39
|
if (character === '"' || character === '\'') {
|
|
134
|
-
index =
|
|
40
|
+
index = skipQuotedString(text, index);
|
|
135
41
|
continue;
|
|
136
42
|
}
|
|
137
43
|
if (character === '[') {
|
|
138
|
-
index =
|
|
44
|
+
index = skipBracketedGroup(text, index);
|
|
139
45
|
continue;
|
|
140
46
|
}
|
|
141
47
|
if (character === '\\') {
|
|
@@ -208,7 +114,7 @@ function buildWhereCartesianProduct (leftParts, rightParts) {
|
|
|
208
114
|
const products = [];
|
|
209
115
|
for (const leftSelector of leftParts) {
|
|
210
116
|
for (const rightSelector of rightParts) {
|
|
211
|
-
const merged = mergeCompoundSelectors(leftSelector
|
|
117
|
+
const merged = mergeCompoundSelectors(leftSelector, rightSelector);
|
|
212
118
|
if (merged === null) {
|
|
213
119
|
return null;
|
|
214
120
|
}
|
|
@@ -239,7 +145,7 @@ function mergeAdjacentWherePseudoClasses (selector) {
|
|
|
239
145
|
}
|
|
240
146
|
// Index of the '(' in the first ':where('
|
|
241
147
|
const firstOpenParenthesis = whereIndex + 6;
|
|
242
|
-
const firstCloseParenthesis =
|
|
148
|
+
const firstCloseParenthesis = findMatchingParenthesis(result, firstOpenParenthesis);
|
|
243
149
|
if (firstCloseParenthesis === -1) {
|
|
244
150
|
break;
|
|
245
151
|
}
|
|
@@ -251,14 +157,14 @@ function mergeAdjacentWherePseudoClasses (selector) {
|
|
|
251
157
|
}
|
|
252
158
|
// Index of the '(' in the second ':where('
|
|
253
159
|
const secondOpenParenthesis = adjacentStart + 6;
|
|
254
|
-
const secondCloseParenthesis =
|
|
160
|
+
const secondCloseParenthesis = findMatchingParenthesis(result, secondOpenParenthesis);
|
|
255
161
|
if (secondCloseParenthesis === -1) {
|
|
256
162
|
break;
|
|
257
163
|
}
|
|
258
164
|
const firstInnerContent = result.slice(firstOpenParenthesis + 1, firstCloseParenthesis);
|
|
259
165
|
const secondInnerContent = result.slice(secondOpenParenthesis + 1, secondCloseParenthesis);
|
|
260
|
-
const leftParts =
|
|
261
|
-
const rightParts =
|
|
166
|
+
const leftParts = splitTopLevelCommaList(firstInnerContent);
|
|
167
|
+
const rightParts = splitTopLevelCommaList(secondInnerContent);
|
|
262
168
|
const mergedParts = buildWhereCartesianProduct(leftParts, rightParts);
|
|
263
169
|
if (mergedParts === null) {
|
|
264
170
|
position = firstCloseParenthesis + 1;
|
|
@@ -468,7 +374,7 @@ function summarizeCompoundSelector (selector) {
|
|
|
468
374
|
}
|
|
469
375
|
} else if (character === '[') {
|
|
470
376
|
// Attribute selector: well-formed input has a closing bracket
|
|
471
|
-
const closeBracketIndex = selector
|
|
377
|
+
const closeBracketIndex = findMatchingBracket(selector, index);
|
|
472
378
|
if (closeBracketIndex === -1) {
|
|
473
379
|
return null;
|
|
474
380
|
}
|
|
@@ -488,7 +394,7 @@ function summarizeCompoundSelector (selector) {
|
|
|
488
394
|
index = nameEnd;
|
|
489
395
|
if (selector[index] === '(') {
|
|
490
396
|
// Functional pseudo-class: skip past its balanced argument parentheses
|
|
491
|
-
const closeParenthesisIndex =
|
|
397
|
+
const closeParenthesisIndex = findMatchingParenthesis(selector, index);
|
|
492
398
|
if (closeParenthesisIndex === -1) {
|
|
493
399
|
return null;
|
|
494
400
|
}
|
|
@@ -530,30 +436,8 @@ function summarizeCompoundSelector (selector) {
|
|
|
530
436
|
* @return {Array} The compound selector segments, combinators excluded.
|
|
531
437
|
*/
|
|
532
438
|
function splitCombinatorSegments (selector) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
let depth = 0;
|
|
536
|
-
for (const character of selector) {
|
|
537
|
-
if (character === '(' || character === '[') {
|
|
538
|
-
depth++;
|
|
539
|
-
} else if (character === ')' || character === ']') {
|
|
540
|
-
depth--;
|
|
541
|
-
}
|
|
542
|
-
// A combinator at the top level ends the current compound segment;
|
|
543
|
-
// whitespace runs collapse into a single boundary
|
|
544
|
-
if (depth === 0 && (character === '>' || character === '+' || character === '~' || /\s/.test(character))) {
|
|
545
|
-
if (currentSegment) {
|
|
546
|
-
segments.push(currentSegment);
|
|
547
|
-
currentSegment = '';
|
|
548
|
-
}
|
|
549
|
-
continue;
|
|
550
|
-
}
|
|
551
|
-
currentSegment += character;
|
|
552
|
-
}
|
|
553
|
-
if (currentSegment) {
|
|
554
|
-
segments.push(currentSegment);
|
|
555
|
-
}
|
|
556
|
-
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);
|
|
557
441
|
}
|
|
558
442
|
|
|
559
443
|
/**
|
|
@@ -614,18 +498,7 @@ function containsPseudoElement (selector) {
|
|
|
614
498
|
* @return {boolean} True when a top-level combinator is present.
|
|
615
499
|
*/
|
|
616
500
|
function hasTopLevelCombinator (selector) {
|
|
617
|
-
|
|
618
|
-
for (const character of selector) {
|
|
619
|
-
if (character === '(' || character === '[') {
|
|
620
|
-
depth++;
|
|
621
|
-
} else if (character === ')' || character === ']') {
|
|
622
|
-
depth--;
|
|
623
|
-
} else if (depth === 0 && (character === '>' || character === '+' || character === '~' || /\s/.test(character))) {
|
|
624
|
-
// A combinator or whitespace boundary at the top level joins two compounds
|
|
625
|
-
return true;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
return false;
|
|
501
|
+
return findTopLevelDelimiter(selector, COMPOUND_SELECTOR_BOUNDARIES, 0) !== -1;
|
|
629
502
|
}
|
|
630
503
|
|
|
631
504
|
/**
|
|
@@ -673,13 +546,13 @@ function unwrapSingleArgumentIsFunctions (selector, hasSiblingSelectors) {
|
|
|
673
546
|
break;
|
|
674
547
|
}
|
|
675
548
|
const openParenthesisIndex = isIndex + 3;
|
|
676
|
-
const closeParenthesisIndex =
|
|
549
|
+
const closeParenthesisIndex = findMatchingParenthesis(result, openParenthesisIndex);
|
|
677
550
|
if (closeParenthesisIndex === -1) {
|
|
678
551
|
break;
|
|
679
552
|
}
|
|
680
553
|
const content = result.slice(openParenthesisIndex + 1, closeParenthesisIndex);
|
|
681
554
|
const innerSelector = content.trim();
|
|
682
|
-
const parts =
|
|
555
|
+
const parts = splitTopLevelCommaList(content);
|
|
683
556
|
if (parts.length !== 1 || !innerSelector) {
|
|
684
557
|
position = closeParenthesisIndex + 1;
|
|
685
558
|
continue;
|
|
@@ -734,11 +607,7 @@ function canDecomposeIsSelector (parts) {
|
|
|
734
607
|
}
|
|
735
608
|
const summaries = [];
|
|
736
609
|
for (const part of parts) {
|
|
737
|
-
const
|
|
738
|
-
if (!trimmedPart) {
|
|
739
|
-
return false;
|
|
740
|
-
}
|
|
741
|
-
const summary = summarizeCompoundSelector(trimmedPart);
|
|
610
|
+
const summary = summarizeCompoundSelector(part);
|
|
742
611
|
if (!summary || !summary.recognizable || summary.hasPseudoElement) {
|
|
743
612
|
return false;
|
|
744
613
|
}
|
|
@@ -754,6 +623,25 @@ function canDecomposeIsSelector (parts) {
|
|
|
754
623
|
});
|
|
755
624
|
}
|
|
756
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
|
+
|
|
757
645
|
/**
|
|
758
646
|
* Processes a selector by unwrapping redundant single-argument `:is()`
|
|
759
647
|
* functions within it, then — for bare `:is()` selectors (where `:is()` is the
|
|
@@ -772,43 +660,10 @@ function processIsSelector (selector, hasSiblingSelectors) {
|
|
|
772
660
|
selector = selector.replace(/:is\(:visited,:link\)/g, ':any-link');
|
|
773
661
|
selector = unwrapSingleArgumentIsFunctions(selector, hasSiblingSelectors);
|
|
774
662
|
// Only process bare :is() selectors (where :is() is the entire selector)
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
778
|
-
let depth = 0;
|
|
779
|
-
let closingIndex = -1;
|
|
780
|
-
for (let index = 4; index < selector.length; index++) {
|
|
781
|
-
if (selector[index] === '(') {
|
|
782
|
-
depth++;
|
|
783
|
-
} else if (selector[index] === ')') {
|
|
784
|
-
if (depth === 0) {
|
|
785
|
-
closingIndex = index;
|
|
786
|
-
break;
|
|
787
|
-
}
|
|
788
|
-
depth--;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
if (closingIndex !== selector.length - 1) {
|
|
663
|
+
let parts = readWholeSelectorIsParts(selector);
|
|
664
|
+
if (!parts) {
|
|
792
665
|
return [selector];
|
|
793
666
|
}
|
|
794
|
-
const content = selector.slice(4, -1);
|
|
795
|
-
let parts = [];
|
|
796
|
-
let currentPart = '';
|
|
797
|
-
let parenDepth = 0;
|
|
798
|
-
for (const character of content) {
|
|
799
|
-
if (character === '(') {
|
|
800
|
-
parenDepth++;
|
|
801
|
-
} else if (character === ')') {
|
|
802
|
-
parenDepth--;
|
|
803
|
-
}
|
|
804
|
-
if (character === ',' && parenDepth === 0) {
|
|
805
|
-
parts.push(currentPart);
|
|
806
|
-
currentPart = '';
|
|
807
|
-
} else {
|
|
808
|
-
currentPart += character;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
parts.push(currentPart);
|
|
812
667
|
// Replace :link + :visited with :any-link
|
|
813
668
|
const hasLink = parts.includes(':link');
|
|
814
669
|
const hasVisited = parts.includes(':visited');
|
|
@@ -826,11 +681,11 @@ function processIsSelector (selector, hasSiblingSelectors) {
|
|
|
826
681
|
parts.sort();
|
|
827
682
|
// Unwrap :is() with a single selector
|
|
828
683
|
if (parts.length === 1) {
|
|
829
|
-
const onlyPart = parts[0]
|
|
684
|
+
const onlyPart = parts[0];
|
|
830
685
|
const canUnwrap = hasSiblingSelectors ?
|
|
831
686
|
isUniversallyRecognizableSelector(onlyPart) :
|
|
832
687
|
!containsPseudoElement(onlyPart);
|
|
833
|
-
return canUnwrap ? [onlyPart] : [':is(' +
|
|
688
|
+
return canUnwrap ? [onlyPart] : [':is(' + onlyPart + ')'];
|
|
834
689
|
}
|
|
835
690
|
// Drop the :is() wrapper when the parts are equivalent as a plain selector list
|
|
836
691
|
if (canDecomposeIsSelector(parts)) {
|
|
@@ -851,29 +706,10 @@ function processIsSelector (selector, hasSiblingSelectors) {
|
|
|
851
706
|
* @return {Array} The flattened selector parts, or the original selector.
|
|
852
707
|
*/
|
|
853
708
|
function flattenNestingParentIsSelector (selector) {
|
|
854
|
-
|
|
709
|
+
const parts = readWholeSelectorIsParts(selector);
|
|
710
|
+
if (!parts) {
|
|
855
711
|
return [selector];
|
|
856
712
|
}
|
|
857
|
-
let depth = 0;
|
|
858
|
-
let closingIndex = -1;
|
|
859
|
-
for (let index = 4; index < selector.length; index++) {
|
|
860
|
-
if (selector[index] === '(') {
|
|
861
|
-
depth++;
|
|
862
|
-
} else if (selector[index] === ')') {
|
|
863
|
-
if (depth === 0) {
|
|
864
|
-
closingIndex = index;
|
|
865
|
-
break;
|
|
866
|
-
}
|
|
867
|
-
depth--;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
// The :is() must span the entire selector to be safely liftable
|
|
871
|
-
if (closingIndex !== selector.length - 1) {
|
|
872
|
-
return [selector];
|
|
873
|
-
}
|
|
874
|
-
const parts = splitParametersByComma(selector.slice(4, -1)).map((part) => {
|
|
875
|
-
return part.trim();
|
|
876
|
-
});
|
|
877
713
|
const hasPotentiallyUnsupportedPart = parts.some((part) => {
|
|
878
714
|
return part.includes(':');
|
|
879
715
|
});
|
|
@@ -882,11 +718,10 @@ function flattenNestingParentIsSelector (selector) {
|
|
|
882
718
|
}
|
|
883
719
|
return parts;
|
|
884
720
|
}
|
|
721
|
+
|
|
885
722
|
export {
|
|
886
|
-
findMatchingCloseParenthesis,
|
|
887
723
|
findNextFunctionCallOutsideStrings,
|
|
888
724
|
flattenNestingParentIsSelector,
|
|
889
725
|
mergeAdjacentWherePseudoClasses,
|
|
890
|
-
processIsSelector
|
|
891
|
-
splitParametersByComma
|
|
726
|
+
processIsSelector
|
|
892
727
|
};
|
package/src/rules/stringify.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { processDeclarations } from '../declarations/process.js';
|
|
6
|
+
import { splitTopLevelCommaList } from '../parser/source-search.js';
|
|
6
7
|
import { minifyValueForOutput } from '../value/minify.js';
|
|
7
8
|
|
|
8
9
|
import {
|
|
@@ -25,8 +26,7 @@ import { resolvePropertyDescriptors } from './property.js';
|
|
|
25
26
|
import {
|
|
26
27
|
flattenNestingParentIsSelector,
|
|
27
28
|
mergeAdjacentWherePseudoClasses,
|
|
28
|
-
processIsSelector
|
|
29
|
-
splitParametersByComma
|
|
29
|
+
processIsSelector
|
|
30
30
|
} from './selectors.js';
|
|
31
31
|
|
|
32
32
|
/**
|
|
@@ -103,10 +103,9 @@ function minifyFunctionPrelude (prelude) {
|
|
|
103
103
|
const functionName = trimmedPrelude.slice(0, openParenIndex);
|
|
104
104
|
const closeParenIndex = trimmedPrelude.lastIndexOf(')');
|
|
105
105
|
const innerContent = trimmedPrelude.slice(openParenIndex + 1, closeParenIndex);
|
|
106
|
-
const
|
|
107
|
-
const minifiedParameters = parameters.map((parameter) => {
|
|
106
|
+
const minifiedParameters = splitTopLevelCommaList(innerContent).map((parameter) => {
|
|
108
107
|
// Collapse whitespace around the colon separating parameter name from default value
|
|
109
|
-
return parameter.
|
|
108
|
+
return parameter.replace(/\s*:\s*/, ':');
|
|
110
109
|
});
|
|
111
110
|
return functionName + '(' + minifiedParameters.join(',') + ')';
|
|
112
111
|
}
|
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
|
/**
|