@thejaredwilcurt/csslop 0.0.49 → 0.0.50
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 +1 -1
- package/src/declarations/border.js +1 -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/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,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
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Folds a rule into the rule that follows it when the later rule restates everything the earlier one declares, so the two can share a selector list.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { normalizeSelector } from './selector-normalization.js';
|
|
6
|
+
import { selectorsShareSpecificity } from './specificity.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Removes duplicate selectors from a combined selector list by normalizing
|
|
10
|
+
* whitespace and preserving the first occurrence of each unique selector.
|
|
11
|
+
*
|
|
12
|
+
* @param {Array} selectors The selector strings to deduplicate.
|
|
13
|
+
* @return {Array} A new array with duplicate selectors removed.
|
|
14
|
+
*/
|
|
15
|
+
function deduplicateSelectors (selectors) {
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
return selectors.filter((selector) => {
|
|
18
|
+
// Normalize whitespace to single spaces for consistent comparison
|
|
19
|
+
const normalized = selector.trim().replace(/\s+/g, ' ');
|
|
20
|
+
if (seen.has(normalized)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
seen.add(normalized);
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The rule body entries that style nothing themselves, and so have no say in
|
|
30
|
+
* whether one rule's body is carried by another's.
|
|
31
|
+
*
|
|
32
|
+
* @type {Set<string>}
|
|
33
|
+
*/
|
|
34
|
+
const UNSTYLED_BODY_ENTRY_TYPES = new Set(['whitespace', 'comment']);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Splits a rule body into the declarations it sets directly and the blocks
|
|
38
|
+
* nested inside it, leaving out the entries that style nothing.
|
|
39
|
+
*
|
|
40
|
+
* @param {Array} declarations The rule body entries.
|
|
41
|
+
* @return {object} The body's `plainDeclarations` and `nestedBlocks`.
|
|
42
|
+
*/
|
|
43
|
+
function partitionRuleBody (declarations) {
|
|
44
|
+
const plainDeclarations = [];
|
|
45
|
+
const nestedBlocks = [];
|
|
46
|
+
for (const entry of declarations || []) {
|
|
47
|
+
if (UNSTYLED_BODY_ENTRY_TYPES.has(entry.type)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (entry.property) {
|
|
51
|
+
plainDeclarations.push(entry);
|
|
52
|
+
} else {
|
|
53
|
+
nestedBlocks.push(entry);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
nestedBlocks,
|
|
58
|
+
plainDeclarations
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Describes a block nested inside a rule precisely enough to tell whether two
|
|
64
|
+
* rules nest the very same thing. Only plain nested rules can be described: an
|
|
65
|
+
* at-rule nested in a rule carries a prelude that decides when its body applies,
|
|
66
|
+
* so it is reported as indescribable rather than guessed at.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} block A block nested inside a rule body.
|
|
69
|
+
* @return {string|null} The signature, or null when the block cannot be described.
|
|
70
|
+
*/
|
|
71
|
+
function nestedBlockSignature (block) {
|
|
72
|
+
if (block.type !== 'rule' || !block.selectors?.length) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const {
|
|
76
|
+
nestedBlocks,
|
|
77
|
+
plainDeclarations
|
|
78
|
+
} = partitionRuleBody(block.declarations);
|
|
79
|
+
const declarationParts = plainDeclarations.map((declaration) => {
|
|
80
|
+
return declaration.property + ':' + (declaration.value || '').trim();
|
|
81
|
+
});
|
|
82
|
+
const nestedParts = nestedBlocksSignature(nestedBlocks);
|
|
83
|
+
if (nestedParts === null) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const selectors = block.selectors.map(normalizeSelector).join(',');
|
|
87
|
+
return selectors + '{' + declarationParts.join(';') + nestedParts + '}';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Describes every block nested in a rule body as one comparable string, in the
|
|
92
|
+
* order the blocks are written, since that order decides which of them wins a
|
|
93
|
+
* conflict.
|
|
94
|
+
*
|
|
95
|
+
* @param {Array} nestedBlocks The blocks nested in a rule body.
|
|
96
|
+
* @return {string|null} The combined signature, or null when any block cannot be described.
|
|
97
|
+
*/
|
|
98
|
+
function nestedBlocksSignature (nestedBlocks) {
|
|
99
|
+
let combined = '';
|
|
100
|
+
for (const block of nestedBlocks) {
|
|
101
|
+
const signature = nestedBlockSignature(block);
|
|
102
|
+
if (signature === null) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
combined += signature;
|
|
106
|
+
}
|
|
107
|
+
return combined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Combines an earlier rule with the rule that follows it when everything the
|
|
112
|
+
* earlier one styles is restated by the later one, so the earlier selector can
|
|
113
|
+
* simply join the later rule's shared part and the remainder stays behind in a
|
|
114
|
+
* rule of its own.
|
|
115
|
+
*
|
|
116
|
+
* Both rules have to nest the same blocks. A block nested in only one of them
|
|
117
|
+
* would either be lost or start matching the other rule's selector, neither of
|
|
118
|
+
* which the two rules said.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} previousRule The rule immediately in front of `rule`.
|
|
121
|
+
* @param {object} rule The rule being folded into its predecessor.
|
|
122
|
+
* @return {Array|null} The rules that replace the pair, or null when they cannot be combined.
|
|
123
|
+
*/
|
|
124
|
+
function mergeRuleIntoPredecessor (previousRule, rule) {
|
|
125
|
+
const isMergeableRulePair = (
|
|
126
|
+
previousRule?.type === 'rule' &&
|
|
127
|
+
previousRule.selectors?.length &&
|
|
128
|
+
rule.type === 'rule' &&
|
|
129
|
+
rule.selectors?.length
|
|
130
|
+
);
|
|
131
|
+
if (!isMergeableRulePair) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const previousBody = partitionRuleBody(previousRule.declarations);
|
|
135
|
+
const currentBody = partitionRuleBody(rule.declarations);
|
|
136
|
+
if (!previousBody.plainDeclarations.length || !currentBody.plainDeclarations.length) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const previousNesting = nestedBlocksSignature(previousBody.nestedBlocks);
|
|
140
|
+
const currentNesting = nestedBlocksSignature(currentBody.nestedBlocks);
|
|
141
|
+
if (previousNesting === null || previousNesting !== currentNesting) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const currentValueByProperty = new Map(
|
|
145
|
+
currentBody.plainDeclarations.map((declaration) => {
|
|
146
|
+
return [declaration.property, (declaration.value || '').trim()];
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
const previousIsRestated = previousBody.plainDeclarations.every((declaration) => {
|
|
150
|
+
return currentValueByProperty.get(declaration.property) === (declaration.value || '').trim();
|
|
151
|
+
});
|
|
152
|
+
// `all` rewrites every other property, so the later rule restating a value is
|
|
153
|
+
// no promise that it applies the same way once `all` has run.
|
|
154
|
+
const currentResetsEverything = currentValueByProperty.has('all');
|
|
155
|
+
if (!previousIsRestated || currentResetsEverything) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const combinedSelectors = deduplicateSelectors([...previousRule.selectors, ...rule.selectors]);
|
|
159
|
+
if (previousBody.nestedBlocks.length && !selectorsShareSpecificity(combinedSelectors)) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const restatedProperties = new Set(
|
|
163
|
+
previousBody.plainDeclarations.map((declaration) => {
|
|
164
|
+
return declaration.property;
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
const remainingDeclarations = currentBody.plainDeclarations.filter((declaration) => {
|
|
168
|
+
return !restatedProperties.has(declaration.property);
|
|
169
|
+
});
|
|
170
|
+
const merged = [{ ...previousRule, selectors: combinedSelectors }];
|
|
171
|
+
if (remainingDeclarations.length) {
|
|
172
|
+
merged.push({ ...rule, declarations: remainingDeclarations });
|
|
173
|
+
}
|
|
174
|
+
return merged;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Merges consecutive rules whose declarations are a subset of the following rule, combining their selectors and splitting out any extra declarations.
|
|
179
|
+
*
|
|
180
|
+
* @param {Array} rules The AST rule nodes to merge.
|
|
181
|
+
* @return {Array} A new array of rules with declaration-compatible consecutive rules combined.
|
|
182
|
+
*/
|
|
183
|
+
function mergeByDeclarations (rules) {
|
|
184
|
+
const result = [];
|
|
185
|
+
for (const rule of rules) {
|
|
186
|
+
const merged = mergeRuleIntoPredecessor(result[result.length - 1], rule);
|
|
187
|
+
if (merged) {
|
|
188
|
+
result.pop();
|
|
189
|
+
result.push(...merged);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
result.push(rule);
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export {
|
|
198
|
+
mergeByDeclarations
|
|
199
|
+
};
|