@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.
Files changed (55) hide show
  1. package/README.md +0 -1
  2. package/package.json +1 -1
  3. package/src/declarations/border.js +1 -1
  4. package/src/index.js +7 -7
  5. package/src/rules/optimize/at-rules.js +113 -0
  6. package/src/rules/optimize/declaration-writes.js +70 -0
  7. package/src/rules/optimize/empty-rules.js +51 -0
  8. package/src/rules/optimize/expand-nesting.js +236 -0
  9. package/src/rules/optimize/factor-parents.js +343 -0
  10. package/src/rules/optimize/index.js +71 -0
  11. package/src/rules/optimize/merge-declarations.js +199 -0
  12. package/src/rules/optimize/merge-nested-rules.js +117 -0
  13. package/src/rules/optimize/merge-selectors.js +252 -0
  14. package/src/rules/optimize/nest-rules.js +202 -0
  15. package/src/rules/optimize/override-index.js +264 -0
  16. package/src/rules/optimize/prune-overridden.js +267 -0
  17. package/src/rules/optimize/selector-normalization.js +60 -0
  18. package/src/rules/optimize/specificity.js +280 -0
  19. package/src/{position-try.js → rules/position-try.js} +1 -1
  20. package/src/utilities.js +14 -0
  21. package/src/value/border.js +85 -0
  22. package/src/value/{colors.js → color/conversion.js} +3 -2
  23. package/src/value/color/functions.js +463 -0
  24. package/src/value/{light-dark.js → color/light-dark.js} +1 -1
  25. package/src/value/{color-mix.js → color/mix.js} +3 -3
  26. package/src/value/{relative-colors.js → color/relative.js} +3 -3
  27. package/src/value/color/shorten.js +93 -0
  28. package/src/value/gradients.js +1 -1
  29. package/src/value/hyphenate-limit-chars.js +72 -0
  30. package/src/value/initial-keywords.js +128 -0
  31. package/src/value/lengths.js +55 -0
  32. package/src/value/math/evaluate.js +36 -0
  33. package/src/value/math/fold.js +315 -0
  34. package/src/value/math/format.js +230 -0
  35. package/src/value/math/functions.js +302 -0
  36. package/src/value/math/index.js +219 -0
  37. package/src/value/math/parser.js +301 -0
  38. package/src/value/math/property-ranges.js +174 -0
  39. package/src/value/math/rewrite.js +222 -0
  40. package/src/value/math/tokenizer.js +160 -0
  41. package/src/value/math/unit-totals.js +194 -0
  42. package/src/value/minify.js +127 -1669
  43. package/src/value/numbers.js +28 -0
  44. package/src/value/positions.js +152 -0
  45. package/src/value/property-optimizations.js +485 -0
  46. package/src/value/segments.js +74 -0
  47. package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
  48. package/src/value/{svg-markup.js → svg/markup.js} +1 -1
  49. package/src/value/time.js +75 -0
  50. package/src/value/urls.js +242 -0
  51. package/src/value/whitespace.js +88 -0
  52. package/src/rules/optimize.js +0 -2314
  53. package/src/value/math-expression.js +0 -1156
  54. package/src/value/math.js +0 -886
  55. /package/src/value/{named-colors.js → color/named.js} +0 -0
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @file Combines consecutive nested rules that share a declaration body and a specificity into one rule with a comma-separated selector list.
3
+ */
4
+
5
+ import {
6
+ compareSpecificity,
7
+ maxSelectorSpecificity
8
+ } from './specificity.js';
9
+
10
+ /**
11
+ * Builds a normalized signature of a rule's declaration body, or null when the
12
+ * rule contains anything other than plain declarations (e.g. nested rules).
13
+ *
14
+ * @param {object} rule The AST rule node.
15
+ * @return {string|null} The sorted "property:value" signature, or null.
16
+ */
17
+ function nestedRuleBodySignature (rule) {
18
+ const declarations = (rule.declarations || []).filter((declaration) => {
19
+ return declaration.type !== 'whitespace' && declaration.type !== 'comment';
20
+ });
21
+ if (declarations.length === 0) {
22
+ return null;
23
+ }
24
+ const isAllPlainDeclarations = declarations.every((declaration) => {
25
+ return declaration.type === 'declaration' && declaration.property;
26
+ });
27
+ if (!isAllPlainDeclarations) {
28
+ return null;
29
+ }
30
+ return declarations
31
+ .map((declaration) => {
32
+ return declaration.property + ':' + (declaration.value || '').trim();
33
+ })
34
+ .sort()
35
+ .join(';');
36
+ }
37
+
38
+ /**
39
+ * Determines whether two nested rules can be merged into a shared selector list.
40
+ * They must have identical declaration bodies and the same specificity level,
41
+ * since comma-separated nested selectors share the highest specificity of the
42
+ * group (like `:is()`).
43
+ *
44
+ * @param {object} first The first nested rule.
45
+ * @param {object} second The second nested rule.
46
+ * @return {boolean} Whether the two nested rules may be merged.
47
+ */
48
+ function nestedRulesMergeable (first, second) {
49
+ if (!first.selectors?.length || !second.selectors?.length) {
50
+ return false;
51
+ }
52
+ const firstSignature = nestedRuleBodySignature(first);
53
+ if (firstSignature === null || firstSignature !== nestedRuleBodySignature(second)) {
54
+ return false;
55
+ }
56
+ const firstSpecificity = maxSelectorSpecificity(first.selectors.join(','));
57
+ const secondSpecificity = maxSelectorSpecificity(second.selectors.join(','));
58
+ return compareSpecificity(firstSpecificity, secondSpecificity) === 0;
59
+ }
60
+
61
+ /**
62
+ * Recursively merges consecutive nested rules within a declaration list when
63
+ * they share identical bodies and specificity, combining their selector lists.
64
+ *
65
+ * @param {Array} declarations The declaration/nested-rule entries of a parent rule.
66
+ * @return {Array} The declaration list with mergeable nested rules combined.
67
+ */
68
+ function mergeConsecutiveNestedRules (declarations) {
69
+ const recursed = declarations.map((declaration) => {
70
+ if (declaration.type === 'rule' && declaration.declarations) {
71
+ return { ...declaration, declarations: mergeConsecutiveNestedRules(declaration.declarations) };
72
+ }
73
+ return declaration;
74
+ });
75
+
76
+ const result = [];
77
+ for (const declaration of recursed) {
78
+ const previous = result[result.length - 1];
79
+ if (
80
+ declaration.type === 'rule' &&
81
+ previous &&
82
+ previous.type === 'rule' &&
83
+ nestedRulesMergeable(previous, declaration)
84
+ ) {
85
+ result[result.length - 1] = {
86
+ ...previous,
87
+ selectors: [...previous.selectors, ...declaration.selectors]
88
+ };
89
+ continue;
90
+ }
91
+ result.push(declaration);
92
+ }
93
+ return result;
94
+ }
95
+
96
+ /**
97
+ * Merges mergeable nested rules within every rule's body across the stylesheet,
98
+ * recursing into `@media` and `@layer` blocks. Top-level rules are not merged
99
+ * here, since only nested (comma-grouped) selectors share specificity.
100
+ *
101
+ * @param {Array} rules The AST rule nodes to process.
102
+ * @return {Array} The rules with mergeable nested rules combined.
103
+ */
104
+ function mergeIdenticalNestedRules (rules) {
105
+ for (const rule of rules) {
106
+ if (rule.type === 'rule' && rule.declarations) {
107
+ rule.declarations = mergeConsecutiveNestedRules(rule.declarations);
108
+ } else if ((rule.type === 'media' || rule.type === 'layer') && rule.rules) {
109
+ rule.rules = mergeIdenticalNestedRules(rule.rules);
110
+ }
111
+ }
112
+ return rules;
113
+ }
114
+
115
+ export {
116
+ mergeIdenticalNestedRules
117
+ };
@@ -0,0 +1,252 @@
1
+ /**
2
+ * @file Merges rules that share a selector, placing the merged rule wherever the cascade reads it the same way it read the two rules apart.
3
+ */
4
+
5
+ import {
6
+ expandToOverridableProperties,
7
+ isImportantDeclaration,
8
+ isPropertyDeclaration
9
+ } from './declaration-writes.js';
10
+ import { createOverrideIndex } from './override-index.js';
11
+ import { normalizeSelector } from './selector-normalization.js';
12
+
13
+ /**
14
+ * Builds the key two rules must share to be considered the same selector.
15
+ *
16
+ * @param {object} rule The AST rule node.
17
+ * @return {string} The normalized, order independent selector key.
18
+ */
19
+ function buildSelectorKey (rule) {
20
+ if (!rule.selectors) {
21
+ return '';
22
+ }
23
+ return rule.selectors
24
+ .map((selector) => {
25
+ return normalizeSelector(selector);
26
+ })
27
+ .sort()
28
+ .join(',');
29
+ }
30
+
31
+ /**
32
+ * Returns a rule's selector key, building it the first time it is needed. A
33
+ * merge only ever grows a rule's declarations, never its selectors, so a key
34
+ * stays valid for every pass of the merge.
35
+ *
36
+ * @param {object} rule The AST rule node.
37
+ * @param {Map} selectorKeyByRule The selector keys already built, shared by every pass of one merge.
38
+ * @return {string} The normalized, order independent selector key.
39
+ */
40
+ function getSelectorKey (rule, selectorKeyByRule) {
41
+ let selectorKey = selectorKeyByRule.get(rule);
42
+ if (selectorKey === undefined) {
43
+ selectorKey = buildSelectorKey(rule);
44
+ selectorKeyByRule.set(rule, selectorKey);
45
+ }
46
+ return selectorKey;
47
+ }
48
+
49
+ /**
50
+ * Placement for a merged rule whose combined declarations belong where the
51
+ * later of the two rules was, because the earlier declarations moved down.
52
+ *
53
+ * @type {string}
54
+ */
55
+ const MERGE_AT_LATER_RULE = 'later';
56
+
57
+ /**
58
+ * Placement for a merged rule whose combined declarations belong where the
59
+ * earlier of the two rules was, because the later declarations moved up.
60
+ *
61
+ * @type {string}
62
+ */
63
+ const MERGE_AT_EARLIER_RULE = 'earlier';
64
+
65
+ /**
66
+ * Finds which declaration decides each leaf property once a list of
67
+ * declarations is read as one rule's body. Within a rule, an important
68
+ * declaration beats a non-important one, and between two of equal weight the
69
+ * last one written wins.
70
+ *
71
+ * @param {Array} declarations The declarations of the combined rule, in the order they would be written.
72
+ * @return {Map} Map of leaf property name to the declaration that decides it.
73
+ */
74
+ function findDecidingDeclarations (declarations) {
75
+ const decidingDeclarations = new Map();
76
+ const importantDeclarations = new Set();
77
+ for (const declaration of declarations) {
78
+ if (!isPropertyDeclaration(declaration)) {
79
+ continue;
80
+ }
81
+ const isImportant = isImportantDeclaration(declaration);
82
+ if (isImportant) {
83
+ importantDeclarations.add(declaration);
84
+ }
85
+ for (const leafProperty of expandToOverridableProperties(declaration.property)) {
86
+ const decidingDeclaration = decidingDeclarations.get(leafProperty);
87
+ const outranksIt = (
88
+ !decidingDeclaration ||
89
+ isImportant ||
90
+ !importantDeclarations.has(decidingDeclaration)
91
+ );
92
+ if (outranksIt) {
93
+ decidingDeclarations.set(leafProperty, declaration);
94
+ }
95
+ }
96
+ }
97
+ return decidingDeclarations;
98
+ }
99
+
100
+ /**
101
+ * Reports whether anything a rule brings to a merge still decides a property
102
+ * of the combined rule. A declaration that loses every property it writes to
103
+ * another declaration of the same rule is dropped from the merged rule, so
104
+ * where the merged rule sits cannot change how it reads.
105
+ *
106
+ * @param {object} rule The rule contributing the entries.
107
+ * @param {Map} decidingDeclarations The deciding declaration of each leaf property of the merged rule.
108
+ * @return {boolean} True when the rule still decides something after the merge.
109
+ */
110
+ function decidesAnythingAfterMerge (rule, decidingDeclarations) {
111
+ return (rule.declarations || []).some((entry) => {
112
+ if (!isPropertyDeclaration(entry)) {
113
+ return entry.type !== 'whitespace' && entry.type !== 'comment';
114
+ }
115
+ return [...expandToOverridableProperties(entry.property)].some((leafProperty) => {
116
+ return decidingDeclarations.get(leafProperty) === entry;
117
+ });
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Decides where two rules with the same selector can be combined. Merging them
123
+ * always makes one set of declarations cross whatever separates the two rules,
124
+ * which can flip a conflict the crossing declarations used to win or lose, so
125
+ * the merged rule goes wherever the set that moved keeps its old standing.
126
+ * Nothing separates adjacent rules, so those always merge.
127
+ *
128
+ * The declarations one rule loses to the other are the exception: they are
129
+ * dropped from the merged rule, so the merged rule belongs wherever the
130
+ * declarations that outlive the merge already were, and nothing has to cross
131
+ * anything at all. That is what lets `.a{color:red!important}` swallow a later
132
+ * `.a{color:tan}` across a rule that also writes `color`.
133
+ *
134
+ * @param {object} overrideIndex The index of what each position writes to.
135
+ * @param {object} earlierRule The first of the two rules with this selector.
136
+ * @param {object} laterRule The second of the two rules with this selector.
137
+ * @param {number} earlierPosition The slot the earlier rule occupies.
138
+ * @param {number} laterPosition The slot the later rule would occupy.
139
+ * @return {string|null} Where to place the merged rule, or null when merging is unsafe.
140
+ */
141
+ function chooseMergePlacement (overrideIndex, earlierRule, laterRule, earlierPosition, laterPosition) {
142
+ const rulesAreAdjacent = earlierPosition === laterPosition - 1;
143
+ if (rulesAreAdjacent) {
144
+ return MERGE_AT_LATER_RULE;
145
+ }
146
+ const decidingDeclarations = findDecidingDeclarations([
147
+ ...(earlierRule.declarations || []),
148
+ ...(laterRule.declarations || [])
149
+ ]);
150
+ if (!decidesAnythingAfterMerge(laterRule, decidingDeclarations)) {
151
+ return MERGE_AT_EARLIER_RULE;
152
+ }
153
+ if (!decidesAnythingAfterMerge(earlierRule, decidingDeclarations)) {
154
+ return MERGE_AT_LATER_RULE;
155
+ }
156
+ if (!overrideIndex.blocksRelocation(earlierRule, earlierPosition)) {
157
+ return MERGE_AT_LATER_RULE;
158
+ }
159
+ if (!overrideIndex.blocksRelocation(laterRule, earlierPosition)) {
160
+ return MERGE_AT_EARLIER_RULE;
161
+ }
162
+ return null;
163
+ }
164
+
165
+ /**
166
+ * Merges rules with identical normalized selectors by combining their declarations, as long as `chooseMergePlacement` finds a spot for the combined rule that the cascade reads the same way. Non-rule entries that affect the cascade (like `@media`) break the merge window, while comments do not, since comments never influence how declarations apply.
167
+ *
168
+ * @param {Array} rules The AST rule nodes to merge.
169
+ * @param {object} mergeCaches The write profiles and selector keys shared by every pass of one merge.
170
+ * @return {Array} A new array of rules with same-selector rules combined.
171
+ */
172
+ function mergeSelectorRulePass (rules, mergeCaches) {
173
+ // A rule merged at the later of the two positions moves to the end of the
174
+ // output. Its old slot is emptied instead of spliced out so that every
175
+ // recorded position stays valid, and the position map locates that slot
176
+ // without searching the output.
177
+ const slots = [];
178
+ const positionByRule = new Map();
179
+ const selectorMap = new Map();
180
+ const overrideIndex = createOverrideIndex(mergeCaches.profileByRule);
181
+ for (const rule of rules) {
182
+ if (rule.type === 'whitespace') {
183
+ continue;
184
+ }
185
+ if (rule.type !== 'rule') {
186
+ slots.push(rule);
187
+ // Clearing a map replaces its storage, so already empty maps are left alone
188
+ if (rule.type !== 'comment' && selectorMap.size) {
189
+ selectorMap.clear();
190
+ positionByRule.clear();
191
+ }
192
+ continue;
193
+ }
194
+ const selectorKey = getSelectorKey(rule, mergeCaches.selectorKeyByRule);
195
+ const existingRule = selectorKey && selectorMap.get(selectorKey);
196
+ if (existingRule) {
197
+ const existingPosition = positionByRule.get(existingRule);
198
+ const mergedPosition = slots.length;
199
+ const placement = chooseMergePlacement(overrideIndex, existingRule, rule, existingPosition, mergedPosition);
200
+ if (placement) {
201
+ existingRule.declarations.push(...(rule.declarations || []));
202
+ overrideIndex.absorbRule(existingRule, rule);
203
+ if (placement === MERGE_AT_LATER_RULE) {
204
+ slots[existingPosition] = null;
205
+ positionByRule.set(existingRule, mergedPosition);
206
+ slots.push(existingRule);
207
+ overrideIndex.recordRule(existingRule, mergedPosition);
208
+ } else {
209
+ // The merged declarations now live at the earlier slot, but they are
210
+ // recorded at the later one, since the index is only ever appended
211
+ // to. Reading them as later than they are can only hold back a
212
+ // further merge, never allow an unsafe one.
213
+ overrideIndex.recordRule(rule, mergedPosition);
214
+ }
215
+ continue;
216
+ }
217
+ }
218
+ selectorMap.set(selectorKey, rule);
219
+ positionByRule.set(rule, slots.length);
220
+ overrideIndex.recordRule(rule, slots.length);
221
+ slots.push(rule);
222
+ }
223
+ return slots.filter((slot) => {
224
+ return slot !== null;
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Reconsiders blocked pairs after merges relocate the rules that blocked them.
230
+ * Each productive pass removes a rule, so this reaches a fixed point without
231
+ * rerunning parsing or unrelated optimizations.
232
+ *
233
+ * @param {Array} rules The sibling rules to merge.
234
+ * @return {Array} Rules with all currently safe same-selector merges.
235
+ */
236
+ function mergeSelectorRules (rules) {
237
+ const mergeCaches = {
238
+ profileByRule: new Map(),
239
+ selectorKeyByRule: new Map()
240
+ };
241
+ let previousRules = rules;
242
+ let mergedRules = mergeSelectorRulePass(previousRules, mergeCaches);
243
+ while (mergedRules.length < previousRules.length) {
244
+ previousRules = mergedRules;
245
+ mergedRules = mergeSelectorRulePass(previousRules, mergeCaches);
246
+ }
247
+ return mergedRules;
248
+ }
249
+
250
+ export {
251
+ mergeSelectorRules
252
+ };
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @file Nests flat rules under a preceding rule whose selector their own selector starts with, so CSS nesting writes the shared part once.
3
+ */
4
+
5
+ /**
6
+ * Matches the part of a child selector that follows its parent when that part
7
+ * starts with an explicit combinator. Optional whitespace, then the combinator
8
+ * (`>`, `+`, or `~`) is captured, then optional whitespace, then the rest of
9
+ * the selector is captured.
10
+ *
11
+ * @type {RegExp}
12
+ */
13
+ const LEADING_COMBINATOR_PATTERN = /^\s*([>+~])\s*(.+)$/;
14
+
15
+ /**
16
+ * Expresses the part of a child selector that follows its parent as the
17
+ * selector it would be once nested inside that parent.
18
+ *
19
+ * @param {string} remainder The child selector with the parent prefix removed.
20
+ * @return {string|null} The nested selector, or null when the remainder cannot be nested.
21
+ */
22
+ function nestSelectorRemainder (remainder) {
23
+ if (!remainder) {
24
+ return '&';
25
+ }
26
+ if (remainder.startsWith(':')) {
27
+ return '&' + remainder;
28
+ }
29
+ if (remainder.startsWith(' ')) {
30
+ return remainder.slice(1);
31
+ }
32
+ const combinatorMatch = remainder.match(LEADING_COMBINATOR_PATTERN);
33
+ if (combinatorMatch) {
34
+ return combinatorMatch[1] + combinatorMatch[2];
35
+ }
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * Builds a reusable matcher that expresses child selectors as nested selectors
41
+ * relative to one parent. Most children tested against a parent do not start
42
+ * with it at all, so that cheap prefix check runs before anything else.
43
+ *
44
+ * @param {string} parentSelector The parent selector string.
45
+ * @return {function(string): (string|null)} A matcher returning the nested selector, or null when the child cannot be nested.
46
+ */
47
+ function createNestedSelectorMatcher (parentSelector) {
48
+ const parent = parentSelector.trim();
49
+
50
+ return (childSelector) => {
51
+ const child = childSelector.trim();
52
+ if (!child.startsWith(parent)) {
53
+ return null;
54
+ }
55
+ return nestSelectorRemainder(child.slice(parent.length));
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Attempts to express a child selector as a nested selector relative to a parent, returning the nested form or null if nesting is not possible.
61
+ *
62
+ * @param {string} parentSelector The parent selector string.
63
+ * @param {string} childSelector The child selector string to try nesting.
64
+ * @return {string|null} The nested selector using & syntax, or null if the child cannot be nested under the parent.
65
+ */
66
+ function tryNestSelector (parentSelector, childSelector) {
67
+ return createNestedSelectorMatcher(parentSelector)(childSelector);
68
+ }
69
+
70
+ /**
71
+ * Determines whether a character is CSS whitespace.
72
+ *
73
+ * @param {string} character A single character from a selector.
74
+ * @return {boolean} Whether the character is whitespace.
75
+ */
76
+ function isSelectorWhitespace (character) {
77
+ // Match a single whitespace character
78
+ return (/\s/).test(character);
79
+ }
80
+
81
+ /**
82
+ * Collects every prefix of a child selector that could act as its nesting
83
+ * parent. A parent always ends immediately before a pseudo-class colon, a
84
+ * descendant whitespace run, or the whitespace run leading into a combinator,
85
+ * so only those few positions can start a nestable remainder. Enumerating them
86
+ * turns "which earlier rule can host this one" into a set of key lookups
87
+ * instead of a comparison against every preceding rule.
88
+ *
89
+ * @param {string} childSelector The trimmed child selector.
90
+ * @return {Set} The candidate parent selector strings.
91
+ */
92
+ function collectCandidateParentSelectors (childSelector) {
93
+ const candidates = new Set();
94
+ for (let index = 1; index < childSelector.length; index++) {
95
+ const character = childSelector[index];
96
+ if (character === ':' || isSelectorWhitespace(character)) {
97
+ // The second colon of a pseudo-element is part of its `::` introducer,
98
+ // not a boundary between a potential parent and nested child.
99
+ if (character === ':' && childSelector[index - 1] === ':') {
100
+ continue;
101
+ }
102
+ candidates.add(childSelector.slice(0, index));
103
+ continue;
104
+ }
105
+ if (character !== '>' && character !== '+' && character !== '~') {
106
+ continue;
107
+ }
108
+ // A combinator may be separated from its parent by whitespace, which
109
+ // belongs to neither side, so the parent ends where that run begins.
110
+ let boundary = index;
111
+ while (boundary > 0 && isSelectorWhitespace(childSelector[boundary - 1])) {
112
+ boundary--;
113
+ }
114
+ if (boundary > 0) {
115
+ candidates.add(childSelector.slice(0, boundary));
116
+ }
117
+ }
118
+ return candidates;
119
+ }
120
+
121
+ /**
122
+ * Finds the most recently emitted rule that can host a child selector as a
123
+ * nested rule, matching the behaviour of scanning backwards through the emitted
124
+ * rules but only visiting the handful of rules whose selector is a viable
125
+ * parent prefix.
126
+ *
127
+ * @param {Array} emittedRules The rules emitted so far.
128
+ * @param {Map} indexBySelector Map of each emitted single-selector rule's selector to its index.
129
+ * @param {string} childSelector The trimmed child selector to nest.
130
+ * @return {object|null} The `parentIndex` and `nestedSelector`, or null when nothing can host the child.
131
+ */
132
+ function findNestingParent (emittedRules, indexBySelector, childSelector) {
133
+ const parentIndexes = [];
134
+ for (const candidateParent of collectCandidateParentSelectors(childSelector)) {
135
+ const parentIndex = indexBySelector.get(candidateParent);
136
+ if (parentIndex !== undefined) {
137
+ parentIndexes.push(parentIndex);
138
+ }
139
+ }
140
+ // The nearest preceding parent wins, exactly as a backwards scan would.
141
+ parentIndexes.sort((first, second) => {
142
+ return second - first;
143
+ });
144
+ for (const parentIndex of parentIndexes) {
145
+ const nestedSelector = tryNestSelector(emittedRules[parentIndex].selectors[0], childSelector);
146
+ if (nestedSelector !== null) {
147
+ return {
148
+ nestedSelector,
149
+ parentIndex
150
+ };
151
+ }
152
+ }
153
+ return null;
154
+ }
155
+
156
+ /**
157
+ * Groups flat CSS rules into nested structures where a child selector can be expressed relative to a preceding parent, reducing output size through CSS nesting.
158
+ *
159
+ * @param {Array} rules The flat AST rule nodes to nest.
160
+ * @return {Array} A new array of rules with applicable children nested under their parents.
161
+ */
162
+ function nestFlatRules (rules) {
163
+ const result = [];
164
+ const indexBySelector = new Map();
165
+ for (const rule of rules) {
166
+ if (rule.type !== 'rule' || rule.selectors?.length !== 1) {
167
+ result.push(rule);
168
+ continue;
169
+ }
170
+ const childSelector = rule.selectors[0].trim();
171
+ const nesting = findNestingParent(result, indexBySelector, childSelector);
172
+ if (nesting) {
173
+ const parentRule = result[nesting.parentIndex];
174
+ parentRule.declarations = parentRule.declarations || [];
175
+ parentRule.declarations.push({ ...rule, selectors: [nesting.nestedSelector] });
176
+ continue;
177
+ }
178
+ indexBySelector.set(childSelector, result.length);
179
+ result.push(rule);
180
+ }
181
+ for (const rule of result) {
182
+ if (rule.type === 'rule' && rule.declarations) {
183
+ const innerRules = rule.declarations.filter((declaration) => {
184
+ return declaration.type === 'rule';
185
+ });
186
+ if (innerRules.length > 0) {
187
+ const nonRuleDeclarations = rule.declarations.filter((declaration) => {
188
+ return declaration.type !== 'rule';
189
+ });
190
+ rule.declarations = [...nonRuleDeclarations, ...nestFlatRules(innerRules)];
191
+ }
192
+ }
193
+ }
194
+ return result;
195
+ }
196
+
197
+ export {
198
+ collectCandidateParentSelectors,
199
+ createNestedSelectorMatcher,
200
+ isSelectorWhitespace,
201
+ nestFlatRules
202
+ };