@thejaredwilcurt/csslop 0.0.49 → 0.0.51
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 +0 -1
- package/package.json +2 -2
- package/src/declarations/border.js +1 -1
- package/src/declarations/process.js +155 -1
- package/src/index.js +7 -7
- package/src/rules/optimize/at-rules.js +113 -0
- package/src/rules/optimize/declaration-writes.js +70 -0
- package/src/rules/optimize/empty-rules.js +51 -0
- package/src/rules/optimize/expand-nesting.js +236 -0
- package/src/rules/optimize/factor-parents.js +343 -0
- package/src/rules/optimize/index.js +71 -0
- package/src/rules/optimize/merge-declarations.js +199 -0
- package/src/rules/optimize/merge-nested-rules.js +117 -0
- package/src/rules/optimize/merge-selectors.js +252 -0
- package/src/rules/optimize/nest-rules.js +202 -0
- package/src/rules/optimize/override-index.js +264 -0
- package/src/rules/optimize/prune-overridden.js +267 -0
- package/src/rules/optimize/selector-normalization.js +60 -0
- package/src/rules/optimize/specificity.js +280 -0
- package/src/{position-try.js → rules/position-try.js} +1 -1
- package/src/rules/stringify.js +13 -0
- package/src/utilities.js +14 -0
- package/src/value/border.js +85 -0
- package/src/value/{colors.js → color/conversion.js} +3 -2
- package/src/value/color/functions.js +463 -0
- package/src/value/{light-dark.js → color/light-dark.js} +1 -1
- package/src/value/{color-mix.js → color/mix.js} +3 -3
- package/src/value/{relative-colors.js → color/relative.js} +3 -3
- package/src/value/color/shorten.js +93 -0
- package/src/value/gradients.js +1 -1
- package/src/value/hyphenate-limit-chars.js +72 -0
- package/src/value/initial-keywords.js +128 -0
- package/src/value/lengths.js +55 -0
- package/src/value/math/evaluate.js +36 -0
- package/src/value/math/fold.js +315 -0
- package/src/value/math/format.js +230 -0
- package/src/value/math/functions.js +302 -0
- package/src/value/math/index.js +219 -0
- package/src/value/math/parser.js +301 -0
- package/src/value/math/property-ranges.js +174 -0
- package/src/value/math/rewrite.js +222 -0
- package/src/value/math/tokenizer.js +160 -0
- package/src/value/math/unit-totals.js +194 -0
- package/src/value/minify.js +127 -1669
- package/src/value/numbers.js +28 -0
- package/src/value/positions.js +152 -0
- package/src/value/property-optimizations.js +485 -0
- package/src/value/segments.js +74 -0
- package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
- package/src/value/{svg-markup.js → svg/markup.js} +1 -1
- package/src/value/time.js +75 -0
- package/src/value/urls.js +242 -0
- package/src/value/whitespace.js +88 -0
- package/src/rules/optimize.js +0 -2314
- package/src/value/math-expression.js +0 -1156
- package/src/value/math.js +0 -886
- /package/src/value/{named-colors.js → color/named.js} +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Expands nested rules into flat rules with combined selectors whenever that lets them merge with rules that already exist, and flattens `&` rules into their parent.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { tokenizeCssValue } from '../../parser/tokenizer.js';
|
|
6
|
+
import { containsPseudoElement } from '../selectors.js';
|
|
7
|
+
|
|
8
|
+
import { selectorsShareSpecificity } from './specificity.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Reports whether an entry of a rule's body is itself a nested rule rather
|
|
12
|
+
* than a plain declaration.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} entry An entry of a rule's declaration list.
|
|
15
|
+
* @return {boolean} True when the entry is a nested rule.
|
|
16
|
+
*/
|
|
17
|
+
function isNestedRule (entry) {
|
|
18
|
+
return entry.type === 'rule';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Expands the rules nested inside each rule's body, so a chain of wrappers
|
|
23
|
+
* collapses from the inside out and the outermost wrapper is then judged
|
|
24
|
+
* against an already flattened child. A rule's body holds the same kinds of
|
|
25
|
+
* entries a stylesheet does, so the body is expanded by the same rules, and
|
|
26
|
+
* the plain declarations it also holds pass through untouched.
|
|
27
|
+
*
|
|
28
|
+
* @param {Array} rules The AST rule nodes whose bodies may hold nested rules.
|
|
29
|
+
* @return {Array} The rules, each with its body expanded.
|
|
30
|
+
*/
|
|
31
|
+
function expandNestedRuleBodies (rules) {
|
|
32
|
+
return rules.map((rule) => {
|
|
33
|
+
if (rule.type !== 'rule' || !(rule.declarations || []).some(isNestedRule)) {
|
|
34
|
+
return rule;
|
|
35
|
+
}
|
|
36
|
+
const declarations = expandPureNestedRules(rule.declarations);
|
|
37
|
+
const canFlattenSelf = selectorsShareSpecificity(rule.selectors) && !rule.selectors.some(containsPseudoElement);
|
|
38
|
+
let encounteredNestedContent = false;
|
|
39
|
+
const flattened = declarations.flatMap((entry) => {
|
|
40
|
+
const isSelfRule = entry.type === 'rule' && entry.selectors?.length === 1 && entry.selectors[0].trim() === '&';
|
|
41
|
+
if (canFlattenSelf && isSelfRule && !encounteredNestedContent) {
|
|
42
|
+
return entry.declarations || [];
|
|
43
|
+
}
|
|
44
|
+
if (entry.type !== 'declaration' && entry.type !== 'whitespace' && entry.type !== 'comment') {
|
|
45
|
+
encounteredNestedContent = true;
|
|
46
|
+
}
|
|
47
|
+
if (entry.type === 'rule' && rule.selectors.length === 1) {
|
|
48
|
+
entry.selectors = entry.selectors.map((selector) => {
|
|
49
|
+
return shortenRepeatedNestingSelector(rule.selectors[0], selector);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return [entry];
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
...rule,
|
|
56
|
+
declarations: flattened
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Replaces repetitions of a simple class or ID beside a leading nesting
|
|
63
|
+
* selector. Token comparisons keep longer names and escaped text intact.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} parentSelector The sole parent selector.
|
|
66
|
+
* @param {string} childSelector The nested selector.
|
|
67
|
+
* @return {string} The shorter equivalent selector.
|
|
68
|
+
*/
|
|
69
|
+
function shortenRepeatedNestingSelector (parentSelector, childSelector) {
|
|
70
|
+
const parentTokens = tokenizeCssValue(parentSelector);
|
|
71
|
+
const isClass = parentTokens.length === 2 && parentTokens[0].text === '.' && parentTokens[1].type === 'ident';
|
|
72
|
+
const isIdentifier = parentTokens.length === 1 && parentTokens[0].type === 'hash';
|
|
73
|
+
if (!isClass && !isIdentifier) {
|
|
74
|
+
return childSelector;
|
|
75
|
+
}
|
|
76
|
+
const childTokens = tokenizeCssValue(childSelector);
|
|
77
|
+
if (childTokens[0]?.text !== '&') {
|
|
78
|
+
return childSelector;
|
|
79
|
+
}
|
|
80
|
+
let index = 1;
|
|
81
|
+
let result = '&';
|
|
82
|
+
while (index < childTokens.length) {
|
|
83
|
+
const matchesParent = parentTokens.every((token, position) => {
|
|
84
|
+
return childTokens[index + position]?.text === token.text;
|
|
85
|
+
});
|
|
86
|
+
if (!matchesParent) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
result += '&';
|
|
90
|
+
index += parentTokens.length;
|
|
91
|
+
}
|
|
92
|
+
return result + childTokens.slice(index).map((token) => {
|
|
93
|
+
return token.text;
|
|
94
|
+
}).join('');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reports whether a nested child selector still means the same thing once it
|
|
99
|
+
* is written out in full under its parent.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} childSelector The selector of a rule nested inside another.
|
|
102
|
+
* @return {boolean} True when the child can be written as a flat selector.
|
|
103
|
+
*/
|
|
104
|
+
function canFlattenChildSelector (childSelector) {
|
|
105
|
+
const child = childSelector.trim();
|
|
106
|
+
if (!child) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
// A `&` anywhere but the very start puts the parent where flattening cannot:
|
|
110
|
+
// `.b &` means the parent follows the child rather than leading it
|
|
111
|
+
return !child.slice(1).includes('&');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Writes a nested child selector out in full, as the selector it stands for
|
|
116
|
+
* when read through its parent.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} parentSelector The selector of the rule the child is nested in.
|
|
119
|
+
* @param {string} childSelector The selector of the nested child rule.
|
|
120
|
+
* @return {string} The flat selector the nested pair stands for.
|
|
121
|
+
*/
|
|
122
|
+
function combineNestedSelector (parentSelector, childSelector) {
|
|
123
|
+
const parent = parentSelector.trim();
|
|
124
|
+
const child = childSelector.trim();
|
|
125
|
+
// A leading `&` names the parent itself, so it is what the parent replaces
|
|
126
|
+
if (child.startsWith('&')) {
|
|
127
|
+
return parent + child.slice(1);
|
|
128
|
+
}
|
|
129
|
+
return parent + ' ' + child;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Expands rules that contain only nested sub-rules into flat rules with combined selectors, enabling further merging when the combined selectors already exist elsewhere.
|
|
134
|
+
*
|
|
135
|
+
* @param {Array} inputRules The AST rule nodes to process.
|
|
136
|
+
* @return {Array} A new array of rules with pure-nested rules expanded.
|
|
137
|
+
*/
|
|
138
|
+
function expandPureNestedRules (inputRules) {
|
|
139
|
+
const rules = expandNestedRuleBodies(inputRules);
|
|
140
|
+
const flatSelectors = new Set();
|
|
141
|
+
for (const rule of rules) {
|
|
142
|
+
if (rule.type !== 'rule' || !rule.selectors?.length) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const nonWhitespace = (rule.declarations || []).filter((declaration) => {
|
|
146
|
+
return declaration.type !== 'whitespace';
|
|
147
|
+
});
|
|
148
|
+
const hasNonRuleDeclarations = nonWhitespace.some((declaration) => {
|
|
149
|
+
return declaration.type !== 'rule';
|
|
150
|
+
});
|
|
151
|
+
if (hasNonRuleDeclarations) {
|
|
152
|
+
rule.selectors.forEach((selector) => {
|
|
153
|
+
// Normalize selector whitespace to single space for deduplication
|
|
154
|
+
flatSelectors.add(selector.trim().replace(/\s+/g, ' '));
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const result = [];
|
|
160
|
+
for (const rule of rules) {
|
|
161
|
+
if (rule.type !== 'rule' || !rule.selectors?.length) {
|
|
162
|
+
result.push(rule);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const nonWhitespace = (rule.declarations || []).filter((declaration) => {
|
|
166
|
+
return declaration.type !== 'whitespace';
|
|
167
|
+
});
|
|
168
|
+
const isPureNested = nonWhitespace.length > 0 && nonWhitespace.every((declaration) => {
|
|
169
|
+
return declaration.type === 'rule';
|
|
170
|
+
});
|
|
171
|
+
// A comma-separated parent selector list behaves like :is() for specificity,
|
|
172
|
+
// so expanding it into descendant combinations (e.g. #a,.b → #a .c,.b .c)
|
|
173
|
+
// would change specificity. Only single-selector parents can expand safely.
|
|
174
|
+
if (!isPureNested || rule.selectors.length !== 1) {
|
|
175
|
+
result.push(rule);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let anyMatch = false;
|
|
180
|
+
let expandedRules = [];
|
|
181
|
+
let canExpand = true;
|
|
182
|
+
|
|
183
|
+
for (const nestedRule of nonWhitespace) {
|
|
184
|
+
if (!nestedRule.selectors?.length) {
|
|
185
|
+
canExpand = false;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
// A child written as a comma-separated selector list, such as `.b,.c`,
|
|
189
|
+
// repeats the parent in front of every item when expanded (`.a .b,.a .c`),
|
|
190
|
+
// while the nested form states the parent once. Expanding is always
|
|
191
|
+
// longer, and leaving the rule nested keeps one pass from expanding what
|
|
192
|
+
// the next pass would then keep, so the output stays idempotent.
|
|
193
|
+
if (nestedRule.selectors.length > 1) {
|
|
194
|
+
canExpand = false;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
if (!nestedRule.selectors.every(canFlattenChildSelector)) {
|
|
198
|
+
canExpand = false;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
const combinedSelectors = [];
|
|
202
|
+
for (const parentSelector of rule.selectors) {
|
|
203
|
+
for (const childSelector of nestedRule.selectors) {
|
|
204
|
+
const combinedSelector = combineNestedSelector(parentSelector, childSelector);
|
|
205
|
+
combinedSelectors.push(combinedSelector);
|
|
206
|
+
if (flatSelectors.has(combinedSelector)) {
|
|
207
|
+
anyMatch = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
expandedRules.push({ ...nestedRule, selectors: combinedSelectors });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const allChildSelectorStrings = expandedRules.map((expandedRule) => {
|
|
215
|
+
return expandedRule.selectors.join(',');
|
|
216
|
+
});
|
|
217
|
+
const allSame = (
|
|
218
|
+
allChildSelectorStrings.length > 0 &&
|
|
219
|
+
allChildSelectorStrings.every((selectorString) => {
|
|
220
|
+
return selectorString === allChildSelectorStrings[0];
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
if (canExpand && (allSame || anyMatch)) {
|
|
224
|
+
for (const expandedRule of expandedRules) {
|
|
225
|
+
result.push(expandedRule);
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
result.push(rule);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export {
|
|
235
|
+
expandPureNestedRules
|
|
236
|
+
};
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Factors the selector portions that consecutive rules share into a synthesized nesting wrapper, or into an `:is()` group when a wrapper would not pay for itself.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
containsPseudoElement,
|
|
7
|
+
isUniversallyRecognizableSelector
|
|
8
|
+
} from '../selectors.js';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
collectCandidateParentSelectors,
|
|
12
|
+
createNestedSelectorMatcher,
|
|
13
|
+
isSelectorWhitespace
|
|
14
|
+
} from './nest-rules.js';
|
|
15
|
+
import { selectorsShareSpecificity } from './specificity.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Splits a selector into the part before its final top-level combinator and
|
|
19
|
+
* the trailing compound that follows it, so selectors that only differ in
|
|
20
|
+
* that leading part can share the trailing compound through nesting.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} selector The selector string to split.
|
|
23
|
+
* @return {object|null} The `head`, `combinator` (`' '` for descendant), and `tail`, or null when the selector has no top-level combinator.
|
|
24
|
+
*/
|
|
25
|
+
function splitSelectorTail (selector) {
|
|
26
|
+
const trimmed = selector.trim();
|
|
27
|
+
let bracketDepth = 0;
|
|
28
|
+
let parenthesisDepth = 0;
|
|
29
|
+
let combinatorStart = -1;
|
|
30
|
+
let combinator = '';
|
|
31
|
+
let tailStart = -1;
|
|
32
|
+
for (let index = 0; index < trimmed.length; index++) {
|
|
33
|
+
const character = trimmed[index];
|
|
34
|
+
if (character === '(') {
|
|
35
|
+
parenthesisDepth++;
|
|
36
|
+
} else if (character === ')') {
|
|
37
|
+
parenthesisDepth--;
|
|
38
|
+
} else if (character === '[') {
|
|
39
|
+
bracketDepth++;
|
|
40
|
+
} else if (character === ']') {
|
|
41
|
+
bracketDepth--;
|
|
42
|
+
} else if (parenthesisDepth === 0 && bracketDepth === 0) {
|
|
43
|
+
if (character === '>' || character === '+' || character === '~') {
|
|
44
|
+
// Whitespace written after an explicit combinator belongs to it
|
|
45
|
+
let end = index + 1;
|
|
46
|
+
while (end < trimmed.length && isSelectorWhitespace(trimmed[end])) {
|
|
47
|
+
end++;
|
|
48
|
+
}
|
|
49
|
+
combinator = character;
|
|
50
|
+
combinatorStart = index;
|
|
51
|
+
tailStart = end;
|
|
52
|
+
index = end - 1;
|
|
53
|
+
} else if (isSelectorWhitespace(character)) {
|
|
54
|
+
// Whitespace adjacent to an explicit combinator belongs to that
|
|
55
|
+
// combinator, so only a self-standing run is a descendant combinator
|
|
56
|
+
const previous = trimmed[index - 1];
|
|
57
|
+
let end = index;
|
|
58
|
+
while (end < trimmed.length && isSelectorWhitespace(trimmed[end])) {
|
|
59
|
+
end++;
|
|
60
|
+
}
|
|
61
|
+
const next = trimmed[end];
|
|
62
|
+
if (
|
|
63
|
+
next &&
|
|
64
|
+
next !== '>' && next !== '+' && next !== '~' &&
|
|
65
|
+
previous !== '>' && previous !== '+' && previous !== '~'
|
|
66
|
+
) {
|
|
67
|
+
combinator = ' ';
|
|
68
|
+
combinatorStart = index;
|
|
69
|
+
tailStart = end;
|
|
70
|
+
}
|
|
71
|
+
index = end - 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!combinator || combinatorStart === 0) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const head = trimmed.slice(0, combinatorStart).trim();
|
|
79
|
+
const tail = trimmed.slice(tailStart).trim();
|
|
80
|
+
if (!head || !tail) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
combinator,
|
|
85
|
+
head,
|
|
86
|
+
tail
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Rewrites a multi-selector rule whose selectors only differ before their
|
|
92
|
+
* final combinator into a wrapper rule that nests the shared trailing
|
|
93
|
+
* compound once, such as `&:hover .x,&:focus .x` becoming
|
|
94
|
+
* `&:hover,&:focus{.x{...}}`. The heads join a comma list, which resolves
|
|
95
|
+
* like `:is()`, so they may only be grouped while they share one specificity.
|
|
96
|
+
*
|
|
97
|
+
* @param {object} rule The AST rule node whose selector list may be factored.
|
|
98
|
+
* @return {object} The rule, rewritten when factoring is valid and shorter.
|
|
99
|
+
*/
|
|
100
|
+
function factorCommonTrailingCompound (rule) {
|
|
101
|
+
if (rule.type !== 'rule' || !rule.selectors || rule.selectors.length < 2) {
|
|
102
|
+
return rule;
|
|
103
|
+
}
|
|
104
|
+
const splitSelectors = rule.selectors.map(splitSelectorTail);
|
|
105
|
+
if (splitSelectors.some((split) => {
|
|
106
|
+
return split === null;
|
|
107
|
+
})) {
|
|
108
|
+
return rule;
|
|
109
|
+
}
|
|
110
|
+
const first = splitSelectors[0];
|
|
111
|
+
const allShareTrailingCompound = splitSelectors.every((split) => {
|
|
112
|
+
return split.tail === first.tail && split.combinator === first.combinator;
|
|
113
|
+
});
|
|
114
|
+
if (!allShareTrailingCompound) {
|
|
115
|
+
return rule;
|
|
116
|
+
}
|
|
117
|
+
const heads = splitSelectors.map((split) => {
|
|
118
|
+
return split.head;
|
|
119
|
+
});
|
|
120
|
+
if (!selectorsShareSpecificity(heads)) {
|
|
121
|
+
return rule;
|
|
122
|
+
}
|
|
123
|
+
const nestedSelector = first.combinator === ' ' ? first.tail : first.combinator + first.tail;
|
|
124
|
+
const flatLength = rule.selectors.join(',').length;
|
|
125
|
+
const factoredLength = heads.join(',').length + nestedSelector.length + 2;
|
|
126
|
+
if (factoredLength >= flatLength) {
|
|
127
|
+
return rule;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
...rule,
|
|
131
|
+
selectors: heads,
|
|
132
|
+
declarations: [{
|
|
133
|
+
type: 'rule',
|
|
134
|
+
selectors: [nestedSelector],
|
|
135
|
+
declarations: rule.declarations
|
|
136
|
+
}]
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Collects every selector prefix of a rule's first selector that could serve
|
|
142
|
+
* as a shared nesting parent, including the full selector itself, which can
|
|
143
|
+
* host sibling pseudo-class variants such as `.a` of `.a,.a:hover`.
|
|
144
|
+
*
|
|
145
|
+
* @param {object} rule The AST rule node whose candidates are collected.
|
|
146
|
+
* @return {Set} The candidate parent selector strings.
|
|
147
|
+
*/
|
|
148
|
+
function collectSharedParentCandidates (rule) {
|
|
149
|
+
const firstSelector = rule.selectors[0].trim();
|
|
150
|
+
const candidates = collectCandidateParentSelectors(firstSelector);
|
|
151
|
+
if (firstSelector) {
|
|
152
|
+
candidates.add(firstSelector);
|
|
153
|
+
}
|
|
154
|
+
return candidates;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Counts how many characters wrapping a run behind a shared parent selector
|
|
159
|
+
* saves: the parent is written once with its braces, and every wrapped
|
|
160
|
+
* selector shrinks to its nested form.
|
|
161
|
+
*
|
|
162
|
+
* @param {Array} run The rules of the run paired with their nested selectors.
|
|
163
|
+
* @param {string} parent The shared parent selector string.
|
|
164
|
+
* @return {number} The character savings of the wrapper, or a non-positive number when it costs more than it saves.
|
|
165
|
+
*/
|
|
166
|
+
function measureWrapperSavings (run, parent) {
|
|
167
|
+
const charactersSaved = run.reduce((total, entry) => {
|
|
168
|
+
return total + entry.sibling.selectors.reduce((sum, selector, position) => {
|
|
169
|
+
return sum + selector.trim().length - entry.nestedSelectors[position].length;
|
|
170
|
+
}, 0);
|
|
171
|
+
}, 0);
|
|
172
|
+
// The wrapper writes the shared selector once plus its surrounding braces.
|
|
173
|
+
return charactersSaved - (parent.length + 2);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Restores the source order of rules that are being moved under their own
|
|
178
|
+
* selector. Their relative order can affect the cascade when nested rules
|
|
179
|
+
* overlap, so it must not depend on an earlier grouping optimization.
|
|
180
|
+
*
|
|
181
|
+
* @param {Array} rules The nested rules to order.
|
|
182
|
+
* @return {Array} A source-ordered copy of the rules.
|
|
183
|
+
*/
|
|
184
|
+
function orderRulesBySourcePosition (rules) {
|
|
185
|
+
return [...rules].sort((firstRule, secondRule) => {
|
|
186
|
+
return firstRule.position?.start?.offset - secondRule.position?.start?.offset;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Groups consecutive sibling rules that share a common leading selector
|
|
192
|
+
* portion (such as `.foo` in `.foo .a`, `.foo .b`, or `.menu .item` in
|
|
193
|
+
* `.menu .item:hover .x`) into a synthesized parent rule with the shared
|
|
194
|
+
* portion factored out. Every viable prefix of the first rule's selector is
|
|
195
|
+
* tried as the parent, and the grouping that saves the most characters wins,
|
|
196
|
+
* but only when it saves more than the wrapper itself costs. Rules with a
|
|
197
|
+
* comma-separated selector list can join a run when every one of their
|
|
198
|
+
* selectors nests under the parent.
|
|
199
|
+
*
|
|
200
|
+
* @param {Array} rules The flat AST rule nodes to factor.
|
|
201
|
+
* @param {boolean} allowTrailingCompound True when the rules are children of a factored wrapper, whose `&`-relative selectors may share a trailing compound that can be factored out in turn.
|
|
202
|
+
* @return {Array} A new array of rules with shared parent selectors factored into nesting wrappers.
|
|
203
|
+
*/
|
|
204
|
+
function factorCommonParents (rules, allowTrailingCompound = false) {
|
|
205
|
+
const result = [];
|
|
206
|
+
let index = 0;
|
|
207
|
+
while (index < rules.length) {
|
|
208
|
+
const rule = rules[index];
|
|
209
|
+
if (rule.type !== 'rule' || !rule.selectors?.length) {
|
|
210
|
+
result.push(rule);
|
|
211
|
+
index++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
let bestGrouping = null;
|
|
215
|
+
for (const candidateParent of collectSharedParentCandidates(rule)) {
|
|
216
|
+
if (!candidateParent || containsPseudoElement(candidateParent)) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const matchNestedSelector = createNestedSelectorMatcher(candidateParent);
|
|
220
|
+
// Collect the run of consecutive rules whose selectors can all nest under the candidate.
|
|
221
|
+
const run = [];
|
|
222
|
+
let lookahead = index;
|
|
223
|
+
while (lookahead < rules.length) {
|
|
224
|
+
const sibling = rules[lookahead];
|
|
225
|
+
if (sibling.type !== 'rule' || !sibling.selectors?.length) {
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
const nestedSelectors = sibling.selectors.map((selector) => {
|
|
229
|
+
return matchNestedSelector(selector.trim());
|
|
230
|
+
});
|
|
231
|
+
if (nestedSelectors.some((nestedSelector) => {
|
|
232
|
+
return nestedSelector === null;
|
|
233
|
+
})) {
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
run.push({ sibling, nestedSelectors });
|
|
237
|
+
lookahead++;
|
|
238
|
+
}
|
|
239
|
+
const selectorCount = run.reduce((total, entry) => {
|
|
240
|
+
return total + entry.nestedSelectors.length;
|
|
241
|
+
}, 0);
|
|
242
|
+
// A lone single-selector rule has nothing to share its parent with.
|
|
243
|
+
if (run.length < 2 && selectorCount < 2) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const savings = measureWrapperSavings(run, candidateParent);
|
|
247
|
+
if (savings > 0 && (!bestGrouping || savings > bestGrouping.savings)) {
|
|
248
|
+
bestGrouping = {
|
|
249
|
+
lookahead,
|
|
250
|
+
parent: candidateParent,
|
|
251
|
+
run,
|
|
252
|
+
savings
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!bestGrouping) {
|
|
257
|
+
if (allowTrailingCompound) {
|
|
258
|
+
result.push(factorCommonTrailingCompound(rule));
|
|
259
|
+
} else {
|
|
260
|
+
result.push(rule);
|
|
261
|
+
}
|
|
262
|
+
index++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const firstEntry = bestGrouping.run[0];
|
|
266
|
+
const firstSelectorIsParent = (
|
|
267
|
+
firstEntry.nestedSelectors.length === 1 &&
|
|
268
|
+
firstEntry.nestedSelectors[0] === '&'
|
|
269
|
+
);
|
|
270
|
+
const directDeclarations = firstSelectorIsParent ?
|
|
271
|
+
firstEntry.sibling.declarations :
|
|
272
|
+
[];
|
|
273
|
+
const entriesToNest = firstSelectorIsParent ?
|
|
274
|
+
bestGrouping.run.slice(1) :
|
|
275
|
+
bestGrouping.run;
|
|
276
|
+
const children = entriesToNest.map((entry) => {
|
|
277
|
+
return { ...entry.sibling, selectors: entry.nestedSelectors };
|
|
278
|
+
});
|
|
279
|
+
let declarations;
|
|
280
|
+
if (firstSelectorIsParent) {
|
|
281
|
+
const directNestedRules = directDeclarations.filter((declaration) => {
|
|
282
|
+
return declaration.type === 'rule';
|
|
283
|
+
});
|
|
284
|
+
const directEntries = directDeclarations.filter((declaration) => {
|
|
285
|
+
return declaration.type !== 'rule';
|
|
286
|
+
});
|
|
287
|
+
declarations = [
|
|
288
|
+
...directEntries,
|
|
289
|
+
...orderRulesBySourcePosition([...directNestedRules, ...children])
|
|
290
|
+
];
|
|
291
|
+
} else {
|
|
292
|
+
declarations = factorCommonParents(children, true);
|
|
293
|
+
}
|
|
294
|
+
result.push({
|
|
295
|
+
type: 'rule',
|
|
296
|
+
selectors: [bestGrouping.parent],
|
|
297
|
+
declarations
|
|
298
|
+
});
|
|
299
|
+
index = bestGrouping.lookahead;
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Shares a descendant prefix through `:is()` when a nesting wrapper did not
|
|
306
|
+
* save enough space to factor the parent out of the selector list.
|
|
307
|
+
*
|
|
308
|
+
* @param {object} rule The style rule whose selector list may be shortened.
|
|
309
|
+
*/
|
|
310
|
+
function factorSelectorBranches (rule) {
|
|
311
|
+
if (rule.type !== 'rule' || !rule.selectors?.length) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
for (const parent of [...rule.selectors]) {
|
|
315
|
+
const prefix = parent + ' ';
|
|
316
|
+
const branches = rule.selectors.filter((selector) => {
|
|
317
|
+
return selector.startsWith(prefix);
|
|
318
|
+
});
|
|
319
|
+
const children = branches.map((selector) => {
|
|
320
|
+
return selector.slice(prefix.length);
|
|
321
|
+
});
|
|
322
|
+
if (children.length < 2 || !children.every(isUniversallyRecognizableSelector) || !selectorsShareSpecificity(children)) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const grouped = prefix + ':is(' + children.sort().join(',') + ')';
|
|
326
|
+
if (grouped.length >= branches.join(',').length) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const branchSet = new Set(branches);
|
|
330
|
+
const firstIndex = rule.selectors.indexOf(branches[0]);
|
|
331
|
+
rule.selectors = rule.selectors.flatMap((selector, index) => {
|
|
332
|
+
if (index === firstIndex) {
|
|
333
|
+
return [grouped];
|
|
334
|
+
}
|
|
335
|
+
return branchSet.has(selector) ? [] : [selector];
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export {
|
|
341
|
+
factorCommonParents,
|
|
342
|
+
factorSelectorBranches
|
|
343
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Optimizes CSS rule structures by merging selectors, deduplicating keyframes, nesting flat rules, and consolidating `@media` and `@layer` blocks.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
deduplicateKeyframes,
|
|
7
|
+
mergeLayerRules,
|
|
8
|
+
mergeMediaRules
|
|
9
|
+
} from './at-rules.js';
|
|
10
|
+
import { removeEmptyRules } from './empty-rules.js';
|
|
11
|
+
import { expandPureNestedRules } from './expand-nesting.js';
|
|
12
|
+
import {
|
|
13
|
+
factorCommonParents,
|
|
14
|
+
factorSelectorBranches
|
|
15
|
+
} from './factor-parents.js';
|
|
16
|
+
import { mergeByDeclarations } from './merge-declarations.js';
|
|
17
|
+
import { mergeIdenticalNestedRules } from './merge-nested-rules.js';
|
|
18
|
+
import { mergeSelectorRules } from './merge-selectors.js';
|
|
19
|
+
import { nestFlatRules } from './nest-rules.js';
|
|
20
|
+
import {
|
|
21
|
+
removeFullyOverriddenSelectors,
|
|
22
|
+
removeOverriddenMultiSelectorProperties
|
|
23
|
+
} from './prune-overridden.js';
|
|
24
|
+
import { clearNormalizedSelectorCache } from './selector-normalization.js';
|
|
25
|
+
import { clearSpecificityCache } from './specificity.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Discards every memoized specificity and normalized selector. The minifier
|
|
29
|
+
* calls this around each stylesheet, so the caches do not outlive the pass that
|
|
30
|
+
* filled them.
|
|
31
|
+
*/
|
|
32
|
+
function clearSelectorCaches () {
|
|
33
|
+
clearSpecificityCache();
|
|
34
|
+
clearNormalizedSelectorCache();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Applies the structural optimization pipeline to a stylesheet or grouping
|
|
39
|
+
* block, so merging at-rules does not leave their children unoptimized.
|
|
40
|
+
*
|
|
41
|
+
* @param {Array} rules The sibling AST entries.
|
|
42
|
+
* @return {Array} The optimized sibling entries.
|
|
43
|
+
*/
|
|
44
|
+
function optimizeRuleList (rules) {
|
|
45
|
+
let result = expandPureNestedRules(rules);
|
|
46
|
+
result = mergeLayerRules(result, mergeSelectorRules);
|
|
47
|
+
result = mergeMediaRules(result, mergeSelectorRules);
|
|
48
|
+
result = deduplicateKeyframes(result);
|
|
49
|
+
for (const rule of result) {
|
|
50
|
+
if (rule.rules) {
|
|
51
|
+
rule.rules = optimizeRuleList(rule.rules);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
result = mergeSelectorRules(result);
|
|
55
|
+
result = removeOverriddenMultiSelectorProperties(result);
|
|
56
|
+
result = removeFullyOverriddenSelectors(result);
|
|
57
|
+
result = removeEmptyRules(result);
|
|
58
|
+
result = mergeByDeclarations(result);
|
|
59
|
+
result = nestFlatRules(result);
|
|
60
|
+
result = removeEmptyRules(result);
|
|
61
|
+
result = mergeByDeclarations(result);
|
|
62
|
+
result = factorCommonParents(result);
|
|
63
|
+
result = nestFlatRules(result);
|
|
64
|
+
result.forEach(factorSelectorBranches);
|
|
65
|
+
return mergeIdenticalNestedRules(result);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export {
|
|
69
|
+
clearSelectorCaches,
|
|
70
|
+
optimizeRuleList
|
|
71
|
+
};
|