@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,264 @@
1
+ /**
2
+ * @file Indexes which properties each rule writes to and how strongly, so a merge can ask whether moving declarations past later rules would change the cascade.
3
+ */
4
+
5
+ import { isCustomProperty } from '../../utilities.js';
6
+
7
+ import { expandToOverridableProperties } from './declaration-writes.js';
8
+ import {
9
+ addSpecificity,
10
+ compareSpecificity,
11
+ maximumRuleSpecificity,
12
+ minimumRuleSpecificity
13
+ } from './specificity.js';
14
+
15
+ /**
16
+ * The property that rewrites every other standard property, and so conflicts
17
+ * with any declaration it is ordered against.
18
+ *
19
+ * @type {string}
20
+ */
21
+ const RESET_ALL_PROPERTY = 'all';
22
+
23
+ /**
24
+ * Index key standing for "declares anything at all", used to order a rule that
25
+ * sets `all` against every other declaration.
26
+ *
27
+ * @type {symbol}
28
+ */
29
+ const ANY_PROPERTY = Symbol('any property');
30
+
31
+ /**
32
+ * @typedef {object} RuleWriteProfile
33
+ * @property {Map} specificityByProperty The strongest specificity each leaf property is written with.
34
+ * @property {Array} weakestSpecificity The weakest specificity the rule's declarations are applied with.
35
+ * @property {Array} strongestSpecificity The strongest specificity anything in the rule is written with.
36
+ * @property {boolean} writesStandardProperty Whether the rule writes any property other than a custom property.
37
+ */
38
+
39
+ /**
40
+ * Notes in a profile that a leaf property is written with a specificity,
41
+ * keeping only the strongest specificity seen for it.
42
+ *
43
+ * @param {RuleWriteProfile} profile The profile to extend.
44
+ * @param {string} property The leaf property name.
45
+ * @param {Array} specificity The specificity the property is written with.
46
+ */
47
+ function addWrittenProperty (profile, property, specificity) {
48
+ const strongestSoFar = profile.specificityByProperty.get(property);
49
+ if (!strongestSoFar || compareSpecificity(specificity, strongestSoFar) > 0) {
50
+ profile.specificityByProperty.set(property, specificity);
51
+ }
52
+ if (compareSpecificity(specificity, profile.strongestSpecificity) > 0) {
53
+ profile.strongestSpecificity = specificity;
54
+ }
55
+ profile.writesStandardProperty = profile.writesStandardProperty || !isCustomProperty(property);
56
+ }
57
+
58
+ /**
59
+ * Adds every leaf property a rule's declarations write to into a profile,
60
+ * descending into nested rules, whose declarations only ever match through the
61
+ * parent and so carry the specificity of both selectors.
62
+ *
63
+ * @param {RuleWriteProfile} profile The profile to extend.
64
+ * @param {object} rule The AST rule node to read declarations from.
65
+ * @param {Array} specificity The specificity the rule's own declarations apply with.
66
+ */
67
+ function addWrittenProperties (profile, rule, specificity) {
68
+ for (const declaration of rule.declarations || []) {
69
+ if (declaration.type === 'rule') {
70
+ const nestedSpecificity = addSpecificity(specificity, maximumRuleSpecificity(declaration.selectors));
71
+ addWrittenProperties(profile, declaration, nestedSpecificity);
72
+ } else if (declaration.type === 'declaration' && declaration.property) {
73
+ for (const leafProperty of expandToOverridableProperties(declaration.property)) {
74
+ addWrittenProperty(profile, leafProperty, specificity);
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Summarizes which properties a rule writes to and how strongly, which is
82
+ * everything the cascade needs to know about a rule to decide whether it may be
83
+ * reordered against another one.
84
+ *
85
+ * @param {object} rule The AST rule node.
86
+ * @return {RuleWriteProfile} The rule's write profile.
87
+ */
88
+ function buildRuleWriteProfile (rule) {
89
+ const profile = {
90
+ specificityByProperty: new Map(),
91
+ weakestSpecificity: minimumRuleSpecificity(rule.selectors),
92
+ strongestSpecificity: [0, 0, 0],
93
+ writesStandardProperty: false
94
+ };
95
+ addWrittenProperties(profile, rule, maximumRuleSpecificity(rule.selectors));
96
+ return profile;
97
+ }
98
+
99
+ /**
100
+ * Folds one rule's writes into another's profile, which is how a profile stays
101
+ * current after a merge appends declarations to a rule. Both rules share the
102
+ * same selectors, so the absorbed writes keep their recorded specificity.
103
+ *
104
+ * @param {RuleWriteProfile} profile The profile of the rule that grew.
105
+ * @param {RuleWriteProfile} absorbedWrites The profile of the rule whose declarations were appended.
106
+ */
107
+ function absorbWriteProfile (profile, absorbedWrites) {
108
+ for (const [property, specificity] of absorbedWrites.specificityByProperty) {
109
+ addWrittenProperty(profile, property, specificity);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Tracks which properties each rule writes to and where it sits, so that a
115
+ * merge can ask whether anything after a position could override the
116
+ * declarations it wants to move, instead of rescanning the stylesheet once per
117
+ * merge.
118
+ *
119
+ * Entries for a property are appended in increasing position order, and each
120
+ * new entry discards the earlier ones it dominates, since a declaration that is
121
+ * both later and at least as specific blocks everything a weaker, earlier one
122
+ * would. What remains is a list whose specificity strictly decreases as
123
+ * position grows, so the first entry past a queried position is also the
124
+ * strongest one past it.
125
+ *
126
+ * @param {Map} profileByRule The write profiles already built for rules, shared by every pass of one merge.
127
+ * @return {object} An index exposing `recordRule` and `blocksRelocation`.
128
+ */
129
+ function createOverrideIndex (profileByRule) {
130
+ const entriesByProperty = new Map();
131
+
132
+ /**
133
+ * Records that a property is written at a position with a given specificity.
134
+ *
135
+ * @param {string|symbol} property The leaf property name, or an index key.
136
+ * @param {number} position The slot the writing rule occupies.
137
+ * @param {Array} specificity The specificity the declaration applies with.
138
+ */
139
+ function addEntry (property, position, specificity) {
140
+ let entries = entriesByProperty.get(property);
141
+ if (!entries) {
142
+ entries = [];
143
+ entriesByProperty.set(property, entries);
144
+ }
145
+ while (entries.length && compareSpecificity(entries[entries.length - 1].specificity, specificity) <= 0) {
146
+ entries.pop();
147
+ }
148
+ entries.push({ position, specificity });
149
+ }
150
+
151
+ /**
152
+ * Whether some rule after a position writes to a property with at least a
153
+ * given specificity, and could therefore win the cascade against it.
154
+ *
155
+ * @param {string|symbol} property The leaf property name, or an index key.
156
+ * @param {number} position The position to search after.
157
+ * @param {Array} specificity The specificity to compare against.
158
+ * @return {boolean} True when a later rule could override the property.
159
+ */
160
+ function hasStrongerEntryAfter (property, position, specificity) {
161
+ const entries = entriesByProperty.get(property);
162
+ if (!entries) {
163
+ return false;
164
+ }
165
+ let low = 0;
166
+ let high = entries.length;
167
+ while (low < high) {
168
+ const middle = Math.floor((low + high) / 2);
169
+ if (entries[middle].position > position) {
170
+ high = middle;
171
+ } else {
172
+ low = middle + 1;
173
+ }
174
+ }
175
+ if (low === entries.length) {
176
+ return false;
177
+ }
178
+ return compareSpecificity(entries[low].specificity, specificity) >= 0;
179
+ }
180
+
181
+ /**
182
+ * Returns a rule's write profile, building it the first time it is needed.
183
+ * Rules only ever grow here, by absorbing another rule's declarations, and
184
+ * `absorbRule` folds those into the profile, so a profile stays valid for
185
+ * every pass of the merge.
186
+ *
187
+ * @param {object} rule The AST rule node.
188
+ * @return {RuleWriteProfile} The rule's write profile.
189
+ */
190
+ function getWriteProfile (rule) {
191
+ let profile = profileByRule.get(rule);
192
+ if (!profile) {
193
+ profile = buildRuleWriteProfile(rule);
194
+ profileByRule.set(rule, profile);
195
+ }
196
+ return profile;
197
+ }
198
+
199
+ /**
200
+ * Records every property a rule writes to at the position it occupies.
201
+ *
202
+ * @param {object} rule The AST rule node.
203
+ * @param {number} position The slot the rule occupies.
204
+ */
205
+ function recordRule (rule, position) {
206
+ const profile = getWriteProfile(rule);
207
+ if (!profile.specificityByProperty.size) {
208
+ return;
209
+ }
210
+ for (const [property, specificity] of profile.specificityByProperty) {
211
+ addEntry(property, position, specificity);
212
+ }
213
+ addEntry(ANY_PROPERTY, position, profile.strongestSpecificity);
214
+ }
215
+
216
+ /**
217
+ * Folds a rule's writes into the rule that just absorbed its declarations, so
218
+ * the grown rule is treated as writing to both sets of properties.
219
+ *
220
+ * @param {object} rule The rule that grew.
221
+ * @param {object} absorbedRule The rule whose declarations were appended to it.
222
+ */
223
+ function absorbRule (rule, absorbedRule) {
224
+ absorbWriteProfile(getWriteProfile(rule), getWriteProfile(absorbedRule));
225
+ }
226
+
227
+ /**
228
+ * Whether moving a rule's declarations past everything recorded after a
229
+ * position would change which declaration wins the cascade. Only a rule that
230
+ * is strictly less specific is guaranteed to lose either way, so anything of
231
+ * equal or greater specificity that writes to the same properties blocks the
232
+ * move.
233
+ *
234
+ * @param {object} movingRule The rule whose declarations would relocate.
235
+ * @param {number} afterPosition The position the declarations would move across.
236
+ * @return {boolean} True when the relocation is unsafe.
237
+ */
238
+ function blocksRelocation (movingRule, afterPosition) {
239
+ const profile = getWriteProfile(movingRule);
240
+ const specificity = profile.weakestSpecificity;
241
+ for (const property of profile.specificityByProperty.keys()) {
242
+ if (hasStrongerEntryAfter(property, afterPosition, specificity)) {
243
+ return true;
244
+ }
245
+ }
246
+ if (profile.writesStandardProperty && hasStrongerEntryAfter(RESET_ALL_PROPERTY, afterPosition, specificity)) {
247
+ return true;
248
+ }
249
+ return (
250
+ profile.specificityByProperty.has(RESET_ALL_PROPERTY) &&
251
+ hasStrongerEntryAfter(ANY_PROPERTY, afterPosition, specificity)
252
+ );
253
+ }
254
+
255
+ return {
256
+ absorbRule,
257
+ blocksRelocation,
258
+ recordRule
259
+ };
260
+ }
261
+
262
+ export {
263
+ createOverrideIndex
264
+ };
@@ -0,0 +1,267 @@
1
+ /**
2
+ * @file Removes the selectors and declarations of multi-selector rules that later rules override in full, since they no longer decide anything.
3
+ */
4
+
5
+ import {
6
+ expandToOverridableProperties,
7
+ isImportantDeclaration
8
+ } from './declaration-writes.js';
9
+ import { normalizeSelector } from './selector-normalization.js';
10
+
11
+ /**
12
+ * Indexes, for every normalized selector in the stylesheet, the last rule that
13
+ * declares each property under it. Comparing that index against a rule's own
14
+ * position answers "is this selector's property overridden later" in constant
15
+ * time, instead of rescanning and renormalizing every following rule.
16
+ *
17
+ * @param {Array} rules The flat list of AST rule nodes.
18
+ * @return {Map} Map of normalized selector to a map of property name to its last declaring rule index.
19
+ */
20
+ function indexLastDeclaringRuleBySelector (rules) {
21
+ const lastRuleIndexBySelector = new Map();
22
+ rules.forEach((rule, ruleIndex) => {
23
+ if (rule.type !== 'rule' || !rule.selectors) {
24
+ return;
25
+ }
26
+ const declaredProperties = (rule.declarations || []).filter((declaration) => {
27
+ return declaration.type === 'declaration';
28
+ }).map((declaration) => {
29
+ return declaration.property;
30
+ });
31
+ if (declaredProperties.length === 0) {
32
+ return;
33
+ }
34
+ for (const selector of rule.selectors) {
35
+ const normalizedSelector = normalizeSelector(selector);
36
+ let lastRuleIndexByProperty = lastRuleIndexBySelector.get(normalizedSelector);
37
+ if (!lastRuleIndexByProperty) {
38
+ lastRuleIndexByProperty = new Map();
39
+ lastRuleIndexBySelector.set(normalizedSelector, lastRuleIndexByProperty);
40
+ }
41
+ for (const property of declaredProperties) {
42
+ lastRuleIndexByProperty.set(property, ruleIndex);
43
+ }
44
+ }
45
+ });
46
+ return lastRuleIndexBySelector;
47
+ }
48
+
49
+ /**
50
+ * Checks whether a given selector has a specific property overridden
51
+ * by any later rule in the stylesheet. A property is considered
52
+ * overridden if a subsequent rule contains that selector (as its only
53
+ * selector or among its selectors) and declares the same property.
54
+ *
55
+ * @param {Map} lastRuleIndexBySelector The index built by `indexLastDeclaringRuleBySelector`.
56
+ * @param {number} startIndex The index of the current rule (search starts after this).
57
+ * @param {string} selector The normalized selector to check.
58
+ * @param {string} property The CSS property name to check.
59
+ * @return {boolean} True if a later rule overrides this selector+property.
60
+ */
61
+ function isSelectorPropertyOverriddenLater (lastRuleIndexBySelector, startIndex, selector, property) {
62
+ const lastRuleIndex = lastRuleIndexBySelector.get(selector)?.get(property);
63
+ return lastRuleIndex !== undefined && lastRuleIndex > startIndex;
64
+ }
65
+
66
+ /**
67
+ * Indexes, for every normalized selector in the stylesheet, the last rule that
68
+ * covers each leaf property under it, where coverage follows the same "writes
69
+ * to" model as `expandToOverridableProperties` (a later `border` declaration
70
+ * on the same selector also overrides an earlier `border-color` on it), along
71
+ * with whether that covering declaration is important. A last-covering entry
72
+ * is consulted to answer "is every declaration this selector still gets from
73
+ * the rule at position X re-declared later".
74
+ *
75
+ * @param {Array} rules The flat list of AST rule nodes.
76
+ * @return {Map} Map of normalized selector to a map of covered leaf property name to `{ ruleIndex, important }`.
77
+ */
78
+ function indexFinalCoveringRuleBySelector (rules) {
79
+ const coverageBySelector = new Map();
80
+ rules.forEach((rule, ruleIndex) => {
81
+ if (rule.type !== 'rule' || !rule.selectors?.length) {
82
+ return;
83
+ }
84
+ const declarations = (rule.declarations || []).filter((declaration) => {
85
+ return declaration.type === 'declaration' && declaration.property;
86
+ });
87
+ if (!declarations.length) {
88
+ return;
89
+ }
90
+ for (const selector of rule.selectors) {
91
+ const normalizedSelector = normalizeSelector(selector);
92
+ let coverageByProperty = coverageBySelector.get(normalizedSelector);
93
+ if (!coverageByProperty) {
94
+ coverageByProperty = new Map();
95
+ coverageBySelector.set(normalizedSelector, coverageByProperty);
96
+ }
97
+ for (const declaration of declarations) {
98
+ const important = isImportantDeclaration(declaration);
99
+ for (const leafProperty of expandToOverridableProperties(declaration.property)) {
100
+ coverageByProperty.set(leafProperty, { important, ruleIndex });
101
+ }
102
+ }
103
+ }
104
+ });
105
+ return coverageBySelector;
106
+ }
107
+
108
+ /**
109
+ * Determines whether a later rule overrides every leaf property an earlier
110
+ * declaration writes to for the same selector, including via shorthand
111
+ * coverage, with an important earlier write requiring an important later
112
+ * write to beat it.
113
+ *
114
+ * @param {Map} coverageByProperty The leaf property coverage index for the selector.
115
+ * @param {object} declaration The earlier declaration to test.
116
+ * @param {number} ruleIndex The position of the rule holding the declaration.
117
+ * @param {boolean} declarationIsImportant Whether the earlier write of this property is important.
118
+ * @return {boolean} True when the declaration is conclusively overridden later.
119
+ */
120
+ function isDeclarationOverriddenLater (coverageByProperty, declaration, ruleIndex, declarationIsImportant) {
121
+ for (const leafProperty of expandToOverridableProperties(declaration.property)) {
122
+ const coverage = coverageByProperty.get(leafProperty);
123
+ if (!coverage || coverage.ruleIndex <= ruleIndex) {
124
+ return false;
125
+ }
126
+ // An important declaration outlives a later non-important one
127
+ if (declarationIsImportant && !coverage.important) {
128
+ return false;
129
+ }
130
+ }
131
+ return true;
132
+ }
133
+
134
+ /**
135
+ * Removes a selector from a multi-selector rule's list when every declaration
136
+ * the rule would still give that selector is overridden by a later rule that
137
+ * contains the same selector. For example, if `h1,h2{color:red}` is followed
138
+ * by `h2{color:tan}`, the `h2` entry contributes nothing and can be dropped,
139
+ * leaving `h1{color:red}`. If the list empties out entirely, the rule is
140
+ * stripped of declarations so `removeEmptyRules` discards it.
141
+ *
142
+ * @param {Array} rules The flat list of AST rule nodes.
143
+ * @return {Array} The rules with fully overridden selectors removed from their lists.
144
+ */
145
+ function removeFullyOverriddenSelectors (rules) {
146
+ // Pruning a selector only ever shrinks lists: a covering entry that wins
147
+ // today is supplied by a later rule, and pruning earlier rules never moves
148
+ // coverage forward, so a single index built up front stays accurate.
149
+ const coverageBySelector = indexFinalCoveringRuleBySelector(rules);
150
+
151
+ for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex++) {
152
+ const rule = rules[ruleIndex];
153
+ if (rule.type !== 'rule' || !rule.selectors || rule.selectors.length < 2) {
154
+ continue;
155
+ }
156
+ const entries = rule.declarations || [];
157
+ // Removing a selector would also un-scope nested rules, which may depend
158
+ // on it, so only rules made purely of declarations qualify
159
+ const hasNestedContent = entries.some((entry) => {
160
+ return entry.type !== 'declaration' && entry.type !== 'whitespace' && entry.type !== 'comment';
161
+ });
162
+ if (hasNestedContent) {
163
+ continue;
164
+ }
165
+ const declarations = entries.filter((entry) => {
166
+ return entry.type === 'declaration' && entry.property;
167
+ });
168
+ if (!declarations.length) {
169
+ continue;
170
+ }
171
+ // Within a rule, any important declaration of a property makes the rule's
172
+ // write of that property important (a conservative stand-in for tracking
173
+ // which of several same-property declarations survives inside the rule)
174
+ const importantByProperty = new Map();
175
+ for (const declaration of declarations) {
176
+ if (isImportantDeclaration(declaration)) {
177
+ importantByProperty.set(declaration.property, true);
178
+ } else if (!importantByProperty.has(declaration.property)) {
179
+ importantByProperty.set(declaration.property, false);
180
+ }
181
+ }
182
+ const keptSelectors = rule.selectors.filter((selector) => {
183
+ const coverageByProperty = coverageBySelector.get(normalizeSelector(selector));
184
+ if (!coverageByProperty) {
185
+ return true;
186
+ }
187
+ // Keep the selector while at least one declaration is not conclusively
188
+ // overridden for it by a later rule
189
+ return declarations.some((declaration) => {
190
+ return !isDeclarationOverriddenLater(
191
+ coverageByProperty,
192
+ declaration,
193
+ ruleIndex,
194
+ importantByProperty.get(declaration.property)
195
+ );
196
+ });
197
+ });
198
+ if (keptSelectors.length !== rule.selectors.length) {
199
+ if (!keptSelectors.length) {
200
+ rule.declarations = entries.filter((entry) => {
201
+ return entry.type !== 'declaration';
202
+ });
203
+ }
204
+ rule.selectors = keptSelectors;
205
+ }
206
+ }
207
+ return rules;
208
+ }
209
+
210
+ /**
211
+ * Removes properties from multi-selector rules when every selector in
212
+ * the rule has that property overridden by a later rule. For example,
213
+ * if `h1,h2{color:red}` is followed by `h1{color:blue}` and
214
+ * `h2{color:green}`, the `color` in the first rule is redundant and
215
+ * can be removed. If all properties are removed, the empty rule will
216
+ * be cleaned up by `removeEmptyRules`.
217
+ *
218
+ * @param {Array} rules The flat list of AST rule nodes.
219
+ * @return {Array} The rules with overridden multi-selector properties removed.
220
+ */
221
+ function removeOverriddenMultiSelectorProperties (rules) {
222
+ // Only rules that follow the one being pruned are ever consulted, and pruning
223
+ // never adds a declaration, so a single index built up front stays accurate.
224
+ const lastRuleIndexBySelector = indexLastDeclaringRuleBySelector(rules);
225
+
226
+ for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex++) {
227
+ const rule = rules[ruleIndex];
228
+ if (rule.type !== 'rule' || !rule.selectors || rule.selectors.length < 2) {
229
+ continue;
230
+ }
231
+ const normalizedSelectors = rule.selectors.map(normalizeSelector);
232
+ const declarations = (rule.declarations || []).filter((declaration) => {
233
+ return declaration.type === 'declaration';
234
+ });
235
+ if (declarations.length === 0) {
236
+ continue;
237
+ }
238
+
239
+ // For each property in this multi-selector rule, check if ALL selectors
240
+ // get that property overridden in later rules
241
+ const propertiesToRemove = new Set();
242
+ for (const declaration of declarations) {
243
+ const property = declaration.property;
244
+ const allSelectorsOverridden = normalizedSelectors.every((selector) => {
245
+ return isSelectorPropertyOverriddenLater(lastRuleIndexBySelector, ruleIndex, selector, property);
246
+ });
247
+ if (allSelectorsOverridden) {
248
+ propertiesToRemove.add(property);
249
+ }
250
+ }
251
+
252
+ if (propertiesToRemove.size > 0) {
253
+ rule.declarations = (rule.declarations || []).filter((declaration) => {
254
+ if (declaration.type !== 'declaration') {
255
+ return true;
256
+ }
257
+ return !propertiesToRemove.has(declaration.property);
258
+ });
259
+ }
260
+ }
261
+ return rules;
262
+ }
263
+
264
+ export {
265
+ removeFullyOverriddenSelectors,
266
+ removeOverriddenMultiSelectorProperties
267
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @file Normalizes selectors into one comparable spelling, memoized per stylesheet, so rules that select the same elements are recognized as such.
3
+ */
4
+
5
+ /**
6
+ * Memoizes the normalized form of each selector seen during the current
7
+ * stylesheet.
8
+ *
9
+ * @type {Map<string, string>}
10
+ */
11
+ const normalizedSelectorCache = new Map();
12
+
13
+ /**
14
+ * Discards every memoized normalized selector. The minifier calls this around
15
+ * each stylesheet, so the cache does not outlive the pass that filled it.
16
+ */
17
+ function clearNormalizedSelectorCache () {
18
+ normalizedSelectorCache.clear();
19
+ }
20
+
21
+ /**
22
+ * Normalizes a selector string for consistent comparison by trimming
23
+ * and collapsing internal whitespace.
24
+ *
25
+ * @param {string} selector The raw selector string.
26
+ * @return {string} The normalized selector.
27
+ */
28
+ function computeNormalizedSelector (selector) {
29
+ // Collapse each whitespace run into a single space
30
+ const normalized = selector.trim().replace(/\s+/g, ' ');
31
+ if (!normalized.includes('::')) {
32
+ return normalized;
33
+ }
34
+ return normalized
35
+ // Convert double-colon ::before/::after to single-colon legacy form
36
+ .replace(/::before\b/g, ':before')
37
+ .replace(/::after\b/g, ':after');
38
+ }
39
+
40
+ /**
41
+ * Normalizes a selector string for consistent comparison, reusing the result
42
+ * when the same selector was already normalized during this stylesheet. Every
43
+ * merge pass compares the same selectors again, so each is only rewritten once.
44
+ *
45
+ * @param {string} selector The raw selector string.
46
+ * @return {string} The normalized selector.
47
+ */
48
+ function normalizeSelector (selector) {
49
+ let normalized = normalizedSelectorCache.get(selector);
50
+ if (normalized === undefined) {
51
+ normalized = computeNormalizedSelector(selector);
52
+ normalizedSelectorCache.set(selector, normalized);
53
+ }
54
+ return normalized;
55
+ }
56
+
57
+ export {
58
+ clearNormalizedSelectorCache,
59
+ normalizeSelector
60
+ };