@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/rules/lang.js
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
findMatchingParenthesis,
|
|
9
|
+
skipParenthesizedGroup,
|
|
10
|
+
splitTopLevel
|
|
11
|
+
} from '../parser/source-search.js';
|
|
12
|
+
|
|
13
|
+
import { findNextFunctionCallOutsideStrings } from './selectors.js';
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* The `:lang()` pseudo-class opening, as it is written in minified output.
|
|
@@ -79,49 +82,6 @@ function requiresEscape (character) {
|
|
|
79
82
|
);
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
/**
|
|
83
|
-
* Splits the argument list of a `:lang()` into its individual language-codes,
|
|
84
|
-
* treating only the commas outside of a quoted code as separators.
|
|
85
|
-
*
|
|
86
|
-
* @param {string} argumentList The text between the parentheses of a `:lang()`.
|
|
87
|
-
* @return {Array} The language-codes, still in the form they were written in.
|
|
88
|
-
*/
|
|
89
|
-
function splitLanguageCodes (argumentList) {
|
|
90
|
-
const codes = [];
|
|
91
|
-
let current = '';
|
|
92
|
-
let openQuote = '';
|
|
93
|
-
for (let index = 0; index < argumentList.length; index++) {
|
|
94
|
-
const character = argumentList[index];
|
|
95
|
-
const nextCharacter = argumentList[index + 1] ?? '';
|
|
96
|
-
if (character === '\\') {
|
|
97
|
-
// An escape carries the character behind it along, whatever it may be
|
|
98
|
-
current += character + nextCharacter;
|
|
99
|
-
index++;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (openQuote) {
|
|
103
|
-
current += character;
|
|
104
|
-
if (character === openQuote) {
|
|
105
|
-
openQuote = '';
|
|
106
|
-
}
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (character === '"' || character === '\'') {
|
|
110
|
-
openQuote = character;
|
|
111
|
-
current += character;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
if (character === ',') {
|
|
115
|
-
codes.push(current);
|
|
116
|
-
current = '';
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
current += character;
|
|
120
|
-
}
|
|
121
|
-
codes.push(current);
|
|
122
|
-
return codes;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
85
|
/**
|
|
126
86
|
* Reads the plain language-code out of one `:lang()` argument, stripping the
|
|
127
87
|
* quotes or escapes that were only there to let it be written. Arguments this
|
|
@@ -191,7 +151,7 @@ function formatLanguageCode (code) {
|
|
|
191
151
|
*/
|
|
192
152
|
function minifyLanguageArguments (argumentList) {
|
|
193
153
|
const codes = [];
|
|
194
|
-
for (const argument of
|
|
154
|
+
for (const argument of splitTopLevel(argumentList, ',')) {
|
|
195
155
|
const code = readLanguageCode(argument);
|
|
196
156
|
if (code === null) {
|
|
197
157
|
return null;
|
|
@@ -217,7 +177,7 @@ function findLanguageFunctions (selector) {
|
|
|
217
177
|
break;
|
|
218
178
|
}
|
|
219
179
|
const openIndex = startIndex + LANGUAGE_FUNCTION.length - 1;
|
|
220
|
-
const closeIndex =
|
|
180
|
+
const closeIndex = findMatchingParenthesis(selector, openIndex);
|
|
221
181
|
if (closeIndex === -1) {
|
|
222
182
|
break;
|
|
223
183
|
}
|
|
@@ -271,15 +231,19 @@ function minifyLanguageSelector (selector) {
|
|
|
271
231
|
* @return {boolean} True when an enclosing parenthesis is still open at that position.
|
|
272
232
|
*/
|
|
273
233
|
function isInsideFunction (selector, index) {
|
|
274
|
-
let
|
|
275
|
-
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
234
|
+
let position = 0;
|
|
235
|
+
while (position < index) {
|
|
236
|
+
if (selector[position] !== '(') {
|
|
237
|
+
position++;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const afterGroupIndex = skipParenthesizedGroup(selector, position);
|
|
241
|
+
if (afterGroupIndex > index) {
|
|
242
|
+
return true;
|
|
280
243
|
}
|
|
244
|
+
position = afterGroupIndex;
|
|
281
245
|
}
|
|
282
|
-
return
|
|
246
|
+
return false;
|
|
283
247
|
}
|
|
284
248
|
|
|
285
249
|
/**
|
|
@@ -305,7 +269,7 @@ function splitAroundLanguageCodes (selector) {
|
|
|
305
269
|
if (isInsideFunction(selector, startIndex)) {
|
|
306
270
|
return null;
|
|
307
271
|
}
|
|
308
|
-
const codes =
|
|
272
|
+
const codes = splitTopLevel(selector.slice(openIndex + 1, closeIndex), ',').map((code) => {
|
|
309
273
|
return code.trim();
|
|
310
274
|
});
|
|
311
275
|
if (codes.some((code) => {
|
package/src/rules/normalize.js
CHANGED
|
@@ -48,6 +48,85 @@ function unescapeSelector (selector) {
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Matches a single whitespace character.
|
|
53
|
+
*
|
|
54
|
+
* @type {RegExp}
|
|
55
|
+
*/
|
|
56
|
+
const WHITESPACE_CHARACTER = /\s/;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Collapses every run of whitespace in a selector down to the single space a
|
|
60
|
+
* descendant combinator is written with, and trims the ends. Whitespace held
|
|
61
|
+
* inside a quoted string, or written as an escape, is copied through untouched:
|
|
62
|
+
* there it is part of the value being matched rather than a combinator.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} selector The raw selector string.
|
|
65
|
+
* @return {string} The selector with its combinator whitespace normalized.
|
|
66
|
+
*/
|
|
67
|
+
function normalizeSelectorWhitespace (selector) {
|
|
68
|
+
let normalized = '';
|
|
69
|
+
let quoteDelimiter = '';
|
|
70
|
+
let index = 0;
|
|
71
|
+
while (index < selector.length) {
|
|
72
|
+
const character = selector[index];
|
|
73
|
+
if (quoteDelimiter) {
|
|
74
|
+
if (character === '\\') {
|
|
75
|
+
normalized += selector.slice(index, index + 2);
|
|
76
|
+
index += 2;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (character === quoteDelimiter) {
|
|
80
|
+
quoteDelimiter = '';
|
|
81
|
+
}
|
|
82
|
+
normalized += character;
|
|
83
|
+
index++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (character === '"' || character === '\'') {
|
|
87
|
+
quoteDelimiter = character;
|
|
88
|
+
normalized += character;
|
|
89
|
+
index++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (character === '\\') {
|
|
93
|
+
normalized += selector.slice(index, index + 2);
|
|
94
|
+
index += 2;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (WHITESPACE_CHARACTER.test(character)) {
|
|
98
|
+
normalized += ' ';
|
|
99
|
+
while (index < selector.length && WHITESPACE_CHARACTER.test(selector[index])) {
|
|
100
|
+
index++;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
normalized += character;
|
|
105
|
+
index++;
|
|
106
|
+
}
|
|
107
|
+
return normalized.trim();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Rewrites every selector in a stylesheet into its whitespace-normalized form,
|
|
112
|
+
* descending into nested rules and the bodies of at-rules. Running this before
|
|
113
|
+
* any rule is rewritten means each later pass compares, nests, and prints
|
|
114
|
+
* selectors that are already written the one canonical way, so a selector that
|
|
115
|
+
* happens to be spaced out in the source cannot leak that spacing into a
|
|
116
|
+
* selector the minifier builds from it.
|
|
117
|
+
*
|
|
118
|
+
* @param {Array} rules The AST rule nodes to normalize.
|
|
119
|
+
*/
|
|
120
|
+
function normalizeRuleSelectors (rules) {
|
|
121
|
+
for (const rule of rules || []) {
|
|
122
|
+
if (rule?.selectors?.length) {
|
|
123
|
+
rule.selectors = rule.selectors.map(normalizeSelectorWhitespace);
|
|
124
|
+
}
|
|
125
|
+
normalizeRuleSelectors(rule?.rules);
|
|
126
|
+
normalizeRuleSelectors(rule?.declarations);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
51
130
|
/**
|
|
52
131
|
* Normalizes a `@layer` cascade layer name list by trimming it and removing the
|
|
53
132
|
* optional whitespace that may surround the commas separating the layer names.
|
|
@@ -126,6 +205,7 @@ export {
|
|
|
126
205
|
canUnwrapSupports,
|
|
127
206
|
normalizeLayerNames,
|
|
128
207
|
normalizeMedia,
|
|
208
|
+
normalizeRuleSelectors,
|
|
129
209
|
normalizeSupports,
|
|
130
210
|
unescapeIdent,
|
|
131
211
|
unescapeSelector
|
package/src/rules/optimize.js
CHANGED
|
@@ -6,6 +6,11 @@ import {
|
|
|
6
6
|
expandToLeafProperties,
|
|
7
7
|
getOverridesOf
|
|
8
8
|
} from '../declarations/config.js';
|
|
9
|
+
import {
|
|
10
|
+
findMatchingBracket,
|
|
11
|
+
findMatchingParenthesis,
|
|
12
|
+
splitTopLevelCommaList
|
|
13
|
+
} from '../parser/source-search.js';
|
|
9
14
|
import { escapeRegexString } from '../utilities.js';
|
|
10
15
|
|
|
11
16
|
import {
|
|
@@ -562,88 +567,187 @@ function deduplicateSelectors (selectors) {
|
|
|
562
567
|
}
|
|
563
568
|
|
|
564
569
|
/**
|
|
565
|
-
*
|
|
570
|
+
* The rule body entries that style nothing themselves, and so have no say in
|
|
571
|
+
* whether one rule's body is carried by another's.
|
|
566
572
|
*
|
|
567
|
-
* @
|
|
568
|
-
* @return {Array} A new array of rules with declaration-compatible consecutive rules combined.
|
|
573
|
+
* @type {Set<string>}
|
|
569
574
|
*/
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
+
const UNSTYLED_BODY_ENTRY_TYPES = new Set(['whitespace', 'comment']);
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Splits a rule body into the declarations it sets directly and the blocks
|
|
579
|
+
* nested inside it, leaving out the entries that style nothing.
|
|
580
|
+
*
|
|
581
|
+
* @param {Array} declarations The rule body entries.
|
|
582
|
+
* @return {object} The body's `plainDeclarations` and `nestedBlocks`.
|
|
583
|
+
*/
|
|
584
|
+
function partitionRuleBody (declarations) {
|
|
585
|
+
const plainDeclarations = [];
|
|
586
|
+
const nestedBlocks = [];
|
|
587
|
+
for (const entry of declarations || []) {
|
|
588
|
+
if (UNSTYLED_BODY_ENTRY_TYPES.has(entry.type)) {
|
|
575
589
|
continue;
|
|
576
590
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
});
|
|
582
|
-
const currentDeclarations = (rule.declarations || []).filter((declaration) => {
|
|
583
|
-
return declaration.type !== 'whitespace' && declaration.property;
|
|
584
|
-
});
|
|
585
|
-
if (previousDeclarations.length > 0 && currentDeclarations.length > 0) {
|
|
586
|
-
const currentDeclarationMap = new Map(
|
|
587
|
-
currentDeclarations.map((declaration) => {
|
|
588
|
-
return [declaration.property, (declaration.value || '').trim()];
|
|
589
|
-
})
|
|
590
|
-
);
|
|
591
|
-
const previousIsSubset = previousDeclarations.every((declaration) => {
|
|
592
|
-
return currentDeclarationMap.get(declaration.property) === (declaration.value || '').trim();
|
|
593
|
-
});
|
|
594
|
-
const currentHasAllProperty = currentDeclarations.some((declaration) => {
|
|
595
|
-
return declaration.property === 'all';
|
|
596
|
-
});
|
|
597
|
-
if (previousIsSubset && !currentHasAllProperty) {
|
|
598
|
-
const commonProperties = new Set(
|
|
599
|
-
previousDeclarations.map((declaration) => {
|
|
600
|
-
return declaration.property;
|
|
601
|
-
})
|
|
602
|
-
);
|
|
603
|
-
const currentOnlyDeclarations = currentDeclarations.filter((declaration) => {
|
|
604
|
-
return !commonProperties.has(declaration.property);
|
|
605
|
-
});
|
|
606
|
-
result.pop();
|
|
607
|
-
const combinedSelectors = deduplicateSelectors([...previousRule.selectors, ...rule.selectors]);
|
|
608
|
-
result.push({ ...previousRule, selectors: combinedSelectors });
|
|
609
|
-
if (currentOnlyDeclarations.length > 0) {
|
|
610
|
-
result.push({ ...rule, declarations: currentOnlyDeclarations });
|
|
611
|
-
}
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
591
|
+
if (entry.property) {
|
|
592
|
+
plainDeclarations.push(entry);
|
|
593
|
+
} else {
|
|
594
|
+
nestedBlocks.push(entry);
|
|
615
595
|
}
|
|
616
|
-
result.push(rule);
|
|
617
596
|
}
|
|
618
|
-
return
|
|
597
|
+
return {
|
|
598
|
+
nestedBlocks,
|
|
599
|
+
plainDeclarations
|
|
600
|
+
};
|
|
619
601
|
}
|
|
620
602
|
|
|
621
603
|
/**
|
|
622
|
-
*
|
|
623
|
-
*
|
|
604
|
+
* Describes a block nested inside a rule precisely enough to tell whether two
|
|
605
|
+
* rules nest the very same thing. Only plain nested rules can be described: an
|
|
606
|
+
* at-rule nested in a rule carries a prelude that decides when its body applies,
|
|
607
|
+
* so it is reported as indescribable rather than guessed at.
|
|
624
608
|
*
|
|
625
|
-
* @param {
|
|
626
|
-
* @return {
|
|
609
|
+
* @param {object} block A block nested inside a rule body.
|
|
610
|
+
* @return {string|null} The signature, or null when the block cannot be described.
|
|
627
611
|
*/
|
|
628
|
-
function
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
612
|
+
function nestedBlockSignature (block) {
|
|
613
|
+
if (block.type !== 'rule' || !block.selectors?.length) {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
const {
|
|
617
|
+
nestedBlocks,
|
|
618
|
+
plainDeclarations
|
|
619
|
+
} = partitionRuleBody(block.declarations);
|
|
620
|
+
const declarationParts = plainDeclarations.map((declaration) => {
|
|
621
|
+
return declaration.property + ':' + (declaration.value || '').trim();
|
|
622
|
+
});
|
|
623
|
+
const nestedParts = nestedBlocksSignature(nestedBlocks);
|
|
624
|
+
if (nestedParts === null) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
const selectors = block.selectors.map(normalizeSelector).join(',');
|
|
628
|
+
return selectors + '{' + declarationParts.join(';') + nestedParts + '}';
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Describes every block nested in a rule body as one comparable string, in the
|
|
633
|
+
* order the blocks are written, since that order decides which of them wins a
|
|
634
|
+
* conflict.
|
|
635
|
+
*
|
|
636
|
+
* @param {Array} nestedBlocks The blocks nested in a rule body.
|
|
637
|
+
* @return {string|null} The combined signature, or null when any block cannot be described.
|
|
638
|
+
*/
|
|
639
|
+
function nestedBlocksSignature (nestedBlocks) {
|
|
640
|
+
let combined = '';
|
|
641
|
+
for (const block of nestedBlocks) {
|
|
642
|
+
const signature = nestedBlockSignature(block);
|
|
643
|
+
if (signature === null) {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
combined += signature;
|
|
647
|
+
}
|
|
648
|
+
return combined;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Reports whether a rule that nests blocks may take on further selectors. A
|
|
653
|
+
* nested selector resolves against its parent's whole selector list as `:is()`,
|
|
654
|
+
* which matches with the specificity of the strongest selector in that list, so
|
|
655
|
+
* the list may only grow while every selector in it weighs the same.
|
|
656
|
+
*
|
|
657
|
+
* @param {Array} selectors The selector list the merged rule would carry.
|
|
658
|
+
* @return {boolean} Whether the nested blocks keep their current weight.
|
|
659
|
+
*/
|
|
660
|
+
function selectorsShareSpecificity (selectors) {
|
|
661
|
+
const strongest = maximumRuleSpecificity(selectors);
|
|
662
|
+
const weakest = minimumRuleSpecificity(selectors);
|
|
663
|
+
return compareSpecificity(strongest, weakest) === 0;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Combines an earlier rule with the rule that follows it when everything the
|
|
668
|
+
* earlier one styles is restated by the later one, so the earlier selector can
|
|
669
|
+
* simply join the later rule's shared part and the remainder stays behind in a
|
|
670
|
+
* rule of its own.
|
|
671
|
+
*
|
|
672
|
+
* Both rules have to nest the same blocks. A block nested in only one of them
|
|
673
|
+
* would either be lost or start matching the other rule's selector, neither of
|
|
674
|
+
* which the two rules said.
|
|
675
|
+
*
|
|
676
|
+
* @param {object} previousRule The rule immediately in front of `rule`.
|
|
677
|
+
* @param {object} rule The rule being folded into its predecessor.
|
|
678
|
+
* @return {Array|null} The rules that replace the pair, or null when they cannot be combined.
|
|
679
|
+
*/
|
|
680
|
+
function mergeRuleIntoPredecessor (previousRule, rule) {
|
|
681
|
+
const isMergeableRulePair = (
|
|
682
|
+
previousRule?.type === 'rule' &&
|
|
683
|
+
previousRule.selectors?.length &&
|
|
684
|
+
rule.type === 'rule' &&
|
|
685
|
+
rule.selectors?.length
|
|
686
|
+
);
|
|
687
|
+
if (!isMergeableRulePair) {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
const previousBody = partitionRuleBody(previousRule.declarations);
|
|
691
|
+
const currentBody = partitionRuleBody(rule.declarations);
|
|
692
|
+
if (!previousBody.plainDeclarations.length || !currentBody.plainDeclarations.length) {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
const previousNesting = nestedBlocksSignature(previousBody.nestedBlocks);
|
|
696
|
+
const currentNesting = nestedBlocksSignature(currentBody.nestedBlocks);
|
|
697
|
+
if (previousNesting === null || previousNesting !== currentNesting) {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
const currentValueByProperty = new Map(
|
|
701
|
+
currentBody.plainDeclarations.map((declaration) => {
|
|
702
|
+
return [declaration.property, (declaration.value || '').trim()];
|
|
703
|
+
})
|
|
704
|
+
);
|
|
705
|
+
const previousIsRestated = previousBody.plainDeclarations.every((declaration) => {
|
|
706
|
+
return currentValueByProperty.get(declaration.property) === (declaration.value || '').trim();
|
|
707
|
+
});
|
|
708
|
+
// `all` rewrites every other property, so the later rule restating a value is
|
|
709
|
+
// no promise that it applies the same way once `all` has run.
|
|
710
|
+
const currentResetsEverything = currentValueByProperty.has('all');
|
|
711
|
+
if (!previousIsRestated || currentResetsEverything) {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
const combinedSelectors = deduplicateSelectors([...previousRule.selectors, ...rule.selectors]);
|
|
715
|
+
if (previousBody.nestedBlocks.length && !selectorsShareSpecificity(combinedSelectors)) {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
const restatedProperties = new Set(
|
|
719
|
+
previousBody.plainDeclarations.map((declaration) => {
|
|
720
|
+
return declaration.property;
|
|
721
|
+
})
|
|
722
|
+
);
|
|
723
|
+
const remainingDeclarations = currentBody.plainDeclarations.filter((declaration) => {
|
|
724
|
+
return !restatedProperties.has(declaration.property);
|
|
725
|
+
});
|
|
726
|
+
const merged = [{ ...previousRule, selectors: combinedSelectors }];
|
|
727
|
+
if (remainingDeclarations.length) {
|
|
728
|
+
merged.push({ ...rule, declarations: remainingDeclarations });
|
|
729
|
+
}
|
|
730
|
+
return merged;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Merges consecutive rules whose declarations are a subset of the following rule, combining their selectors and splitting out any extra declarations.
|
|
735
|
+
*
|
|
736
|
+
* @param {Array} rules The AST rule nodes to merge.
|
|
737
|
+
* @return {Array} A new array of rules with declaration-compatible consecutive rules combined.
|
|
738
|
+
*/
|
|
739
|
+
function mergeByDeclarations (rules) {
|
|
740
|
+
const result = [];
|
|
741
|
+
for (const rule of rules) {
|
|
742
|
+
const merged = mergeRuleIntoPredecessor(result[result.length - 1], rule);
|
|
743
|
+
if (merged) {
|
|
744
|
+
result.pop();
|
|
745
|
+
result.push(...merged);
|
|
746
|
+
continue;
|
|
643
747
|
}
|
|
748
|
+
result.push(rule);
|
|
644
749
|
}
|
|
645
|
-
|
|
646
|
-
return selectors;
|
|
750
|
+
return result;
|
|
647
751
|
}
|
|
648
752
|
|
|
649
753
|
/**
|
|
@@ -662,28 +766,6 @@ function skipSelectorName (text, start) {
|
|
|
662
766
|
return index;
|
|
663
767
|
}
|
|
664
768
|
|
|
665
|
-
/**
|
|
666
|
-
* Finds the index of the closing parenthesis matching the one at openIndex.
|
|
667
|
-
*
|
|
668
|
-
* @param {string} text The text to scan.
|
|
669
|
-
* @param {number} openIndex Index of the opening parenthesis.
|
|
670
|
-
* @return {number} Index of the matching close parenthesis, or text length.
|
|
671
|
-
*/
|
|
672
|
-
function findMatchingParenthesisIndex (text, openIndex) {
|
|
673
|
-
let depth = 0;
|
|
674
|
-
for (let index = openIndex; index < text.length; index++) {
|
|
675
|
-
if (text[index] === '(') {
|
|
676
|
-
depth++;
|
|
677
|
-
} else if (text[index] === ')') {
|
|
678
|
-
depth--;
|
|
679
|
-
if (depth === 0) {
|
|
680
|
-
return index;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
return text.length;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
769
|
/**
|
|
688
770
|
* Compares two specificity tuples [ids, classes, types] lexicographically.
|
|
689
771
|
*
|
|
@@ -724,20 +806,9 @@ function computeSpecificity (selector) {
|
|
|
724
806
|
index = skipSelectorName(text, index + 1);
|
|
725
807
|
} else if (character === '[') {
|
|
726
808
|
specificity[1]++;
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
if (text[index] === '[') {
|
|
731
|
-
bracketDepth++;
|
|
732
|
-
} else if (text[index] === ']') {
|
|
733
|
-
bracketDepth--;
|
|
734
|
-
if (bracketDepth === 0) {
|
|
735
|
-
index++;
|
|
736
|
-
break;
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
index++;
|
|
740
|
-
}
|
|
809
|
+
// An attribute selector that never closes runs to the end of the selector
|
|
810
|
+
const closeBracketIndex = findMatchingBracket(text, index);
|
|
811
|
+
index = closeBracketIndex === -1 ? text.length : closeBracketIndex + 1;
|
|
741
812
|
} else if (character === ':') {
|
|
742
813
|
if (text[index + 1] === ':') {
|
|
743
814
|
specificity[2]++;
|
|
@@ -747,8 +818,10 @@ function computeSpecificity (selector) {
|
|
|
747
818
|
const nameEnd = skipSelectorName(text, nameStart);
|
|
748
819
|
const name = text.slice(nameStart, nameEnd).toLowerCase();
|
|
749
820
|
if (text[nameEnd] === '(') {
|
|
750
|
-
|
|
751
|
-
const
|
|
821
|
+
// An argument list that never closes runs to the end of the selector
|
|
822
|
+
const closeIndex = findMatchingParenthesis(text, nameEnd);
|
|
823
|
+
const argumentsEnd = closeIndex === -1 ? text.length : closeIndex;
|
|
824
|
+
const inner = text.slice(nameEnd + 1, argumentsEnd);
|
|
752
825
|
if (name === 'where') {
|
|
753
826
|
// :where() contributes zero specificity
|
|
754
827
|
} else if (name === 'is' || name === 'not' || name === 'has' || name === 'matches') {
|
|
@@ -759,7 +832,7 @@ function computeSpecificity (selector) {
|
|
|
759
832
|
} else {
|
|
760
833
|
specificity[1]++;
|
|
761
834
|
}
|
|
762
|
-
index =
|
|
835
|
+
index = argumentsEnd + 1;
|
|
763
836
|
} else {
|
|
764
837
|
// Legacy single-colon pseudo-elements count as pseudo-elements
|
|
765
838
|
if (name === 'before' || name === 'after' || name === 'first-line' || name === 'first-letter') {
|
|
@@ -793,7 +866,7 @@ function computeSpecificity (selector) {
|
|
|
793
866
|
*/
|
|
794
867
|
function maxSelectorSpecificity (selectorList) {
|
|
795
868
|
let maximum = [0, 0, 0];
|
|
796
|
-
for (const selector of
|
|
869
|
+
for (const selector of splitTopLevelCommaList(selectorList)) {
|
|
797
870
|
const specificity = computeSpecificity(selector);
|
|
798
871
|
if (compareSpecificity(specificity, maximum) > 0) {
|
|
799
872
|
maximum = specificity;
|
|
@@ -824,7 +897,7 @@ function maximumRuleSpecificity (selectors) {
|
|
|
824
897
|
*/
|
|
825
898
|
function minimumRuleSpecificity (selectors) {
|
|
826
899
|
let minimum = null;
|
|
827
|
-
for (const selector of
|
|
900
|
+
for (const selector of splitTopLevelCommaList((selectors || []).join(','))) {
|
|
828
901
|
const specificity = computeSpecificity(selector);
|
|
829
902
|
if (!minimum || compareSpecificity(specificity, minimum) < 0) {
|
|
830
903
|
minimum = specificity;
|