@thejaredwilcurt/csslop 0.0.48 → 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.
Files changed (61) hide show
  1. package/README.md +3 -4
  2. package/package.json +3 -3
  3. package/src/declarations/border.js +1 -1
  4. package/src/declarations/css-wide-keywords.js +37 -1
  5. package/src/declarations/fold.js +13 -3
  6. package/src/declarations/process.js +60 -7
  7. package/src/index.js +12 -7
  8. package/src/parser/source-search.js +6 -0
  9. package/src/rules/optimize/at-rules.js +113 -0
  10. package/src/rules/optimize/declaration-writes.js +70 -0
  11. package/src/rules/optimize/empty-rules.js +51 -0
  12. package/src/rules/optimize/expand-nesting.js +236 -0
  13. package/src/rules/optimize/factor-parents.js +343 -0
  14. package/src/rules/optimize/index.js +71 -0
  15. package/src/rules/optimize/merge-declarations.js +199 -0
  16. package/src/rules/optimize/merge-nested-rules.js +117 -0
  17. package/src/rules/optimize/merge-selectors.js +252 -0
  18. package/src/rules/optimize/nest-rules.js +202 -0
  19. package/src/rules/optimize/override-index.js +264 -0
  20. package/src/rules/optimize/prune-overridden.js +267 -0
  21. package/src/rules/optimize/selector-normalization.js +60 -0
  22. package/src/rules/optimize/specificity.js +280 -0
  23. package/src/{position-try.js → rules/position-try.js} +1 -1
  24. package/src/rules/selectors.js +9 -0
  25. package/src/rules/stringify.js +138 -76
  26. package/src/utilities.js +9 -8
  27. package/src/value/border.js +85 -0
  28. package/src/value/{colors.js → color/conversion.js} +3 -2
  29. package/src/value/color/functions.js +463 -0
  30. package/src/value/{light-dark.js → color/light-dark.js} +1 -1
  31. package/src/value/{color-mix.js → color/mix.js} +3 -3
  32. package/src/value/{relative-colors.js → color/relative.js} +3 -3
  33. package/src/value/color/shorten.js +93 -0
  34. package/src/value/gradients.js +1 -1
  35. package/src/value/hyphenate-limit-chars.js +72 -0
  36. package/src/value/initial-keywords.js +128 -0
  37. package/src/value/lengths.js +55 -0
  38. package/src/value/math/evaluate.js +36 -0
  39. package/src/value/math/fold.js +315 -0
  40. package/src/value/math/format.js +230 -0
  41. package/src/value/math/functions.js +302 -0
  42. package/src/value/math/index.js +219 -0
  43. package/src/value/math/parser.js +301 -0
  44. package/src/value/math/property-ranges.js +174 -0
  45. package/src/value/math/rewrite.js +222 -0
  46. package/src/value/math/tokenizer.js +160 -0
  47. package/src/value/math/unit-totals.js +194 -0
  48. package/src/value/minify.js +162 -1678
  49. package/src/value/numbers.js +28 -0
  50. package/src/value/positions.js +152 -0
  51. package/src/value/property-optimizations.js +485 -0
  52. package/src/value/segments.js +74 -0
  53. package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
  54. package/src/value/{svg-markup.js → svg/markup.js} +1 -1
  55. package/src/value/time.js +75 -0
  56. package/src/value/urls.js +242 -0
  57. package/src/value/whitespace.js +88 -0
  58. package/src/rules/optimize.js +0 -2031
  59. package/src/value/math-expression.js +0 -1156
  60. package/src/value/math.js +0 -886
  61. /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
+ };