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