@thejaredwilcurt/csslop 0.0.43 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -296,7 +296,7 @@ Two different cases:
296
296
  1. `git add -A && git commit -m "Updated tests"`
297
297
  1. Then run `npm t` to see if any tests fail
298
298
  1. If they fail, give an AI this prompt:
299
- * **PROMPT:** Run `npm t` and fix all failing tests by modifying files in `src`. Do not use naive solutions, hacks, or hard coded values. Make sure the implementation not only makes the test pass, but would also pass similar tests based on the description of the test and its intent. Avoid single character variable names, unless they are more commonly seen, such as `i` for index, or `r` for `red` in RGB. Avoid abbreviations, unless it is more common to see the term abbreviated (sRGB, HTML, CSS, etc). Group related logic into well named functions. Ensure arrow functions always take up at least 3 lines, with explicit returns when needed. Always comment regex if used. Any test fails related to idempotency should be handled last. Run `npm run lint` and correct any linter warnings/errors that occur in the `/src` folder. When all done, run the `beep` command to alert me you finished.
299
+ * **PROMPT:** Run `npm t` and fix all failing tests by modifying files in `src`. Do not use naive solutions, hacks, or hard coded values. Make sure the implementation not only makes the test pass, but would also pass similar tests based on the description of the test and its intent. Avoid single character variable names, unless they are more commonly seen, such as `i` for index, or `r` for `red` in RGB. Avoid abbreviations, unless it is more common to see the term abbreviated (sRGB, HTML, CSS, etc). Group related logic into well named functions. Ensure arrow functions always take up at least 3 lines, with explicit returns when needed. Always comment regex if used. Any test fails related to idempotency should be handled last. DO NOT touch anything related to real-world tests (including `npm run idem` and `npm run real`). Run `npm run lint` and correct any linter warnings/errors that occur in the `/src` folder. When all done, run the `beep` command to alert me you finished.
300
300
  1. Verify only code in the `src` folder was modified
301
301
  1. Verify `npm t` passes with a 100% score
302
302
  1. Run `npm run lint`, if anything fails, have the AI fix it.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@thejaredwilcurt/csslop",
3
3
  "main": "index.js",
4
4
  "type": "module",
5
- "version": "0.0.43",
5
+ "version": "0.0.45",
6
6
  "description": "Experimental CSS minification",
7
7
  "scripts": {
8
8
  "prestart": "node ./scripts/pre/start.js",
@@ -35,7 +35,7 @@
35
35
  "eslint-config-tjw-import-x": "^1.0.1",
36
36
  "eslint-config-tjw-jsdoc": "^2.0.1",
37
37
  "eslint-plugin-import-x": "^4.17.0",
38
- "eslint-plugin-jsdoc": "^65.3.8",
38
+ "eslint-plugin-jsdoc": "^65.3.11",
39
39
  "fflate": "^0.8.3",
40
40
  "globals": "^17.13.0",
41
41
  "pretty-ms": "^10.0.0",
@@ -5,7 +5,10 @@
5
5
  import { splitTopLevelComponents } from '../parser/source-search.js';
6
6
  import { minifyValue } from '../value/minify.js';
7
7
  import { namedColors } from '../value/named-colors.js';
8
- import { collapseShorthandParts } from '../value/shared.js';
8
+ import {
9
+ collapseShorthandParts,
10
+ hasSubstitutedParts
11
+ } from '../value/shared.js';
9
12
 
10
13
  import {
11
14
  BORDER_EDGE_PROPERTIES,
@@ -107,16 +110,14 @@ function findLastDeclarationIndex (declarations, property) {
107
110
  }
108
111
 
109
112
  /**
110
- * Rewrites a `border-width` + `border-style` + `border-color` trio whose color
111
- * differs per edge into a `border` shorthand built from the first color, followed
112
- * by a `border-color` override that restores the per-edge colors. The rewrite is
113
- * only applied when the resulting pair is shorter than the three longhands.
113
+ * Rewrites a border trio into a shorthand and overrides for components that
114
+ * differ by edge. Only shorter rewrites with uniform importance are used.
114
115
  *
115
116
  * @param {Array} declarations The declarations of a single rule.
116
117
  * @param {object} context The minification context with the stylesheet's reset properties.
117
118
  * @return {Array} The declarations, with the trio rewritten when it is shorter.
118
119
  */
119
- function collapseBorderTrioWithPerEdgeColor (declarations, context) {
120
+ function collapseBorderTrioWithPerEdgeOverrides (declarations, context) {
120
121
  // The `border` shorthand also resets `border-image`, so the trio stays as it
121
122
  // is when another rule of the stylesheet relies on that value.
122
123
  if (resetsPropertyDeclaredElsewhere('border', collectDeclaredProperties(declarations), context)) {
@@ -132,6 +133,18 @@ function collapseBorderTrioWithPerEdgeColor (declarations, context) {
132
133
  if (!hasWholeTrio) {
133
134
  return declarations;
134
135
  }
136
+ const hasCompetingBorderWrites = declarations.some((declaration) => {
137
+ const property = declaration.property || '';
138
+ if (BORDER_TRIO_PROPERTIES.includes(property)) {
139
+ return declarations.filter((entry) => {
140
+ return entry.property === property;
141
+ }).length !== 1;
142
+ }
143
+ return property === 'border' || (property.startsWith('border-') && !property.startsWith('border-image'));
144
+ });
145
+ if (hasCompetingBorderWrites) {
146
+ return declarations;
147
+ }
135
148
 
136
149
  const trioValues = trioIndexes.map((index) => {
137
150
  return minifyValue(declarations[index]);
@@ -150,28 +163,38 @@ function collapseBorderTrioWithPerEdgeColor (declarations, context) {
150
163
  const [width, style, color] = trioValues.map((value) => {
151
164
  return value.replace('!important', '').trim();
152
165
  });
153
- const colorComponents = splitTopLevelComponents(color);
154
- const isSingleComponent = (value) => {
155
- return splitTopLevelComponents(value).length === 1;
156
- };
157
- const canBuildShorthand = (
158
- isSingleComponent(width) &&
159
- isSingleComponent(style) &&
160
- colorComponents.length > 1
161
- );
162
- if (!canBuildShorthand) {
166
+ const componentsByProperty = [width, style, color].map(splitTopLevelComponents);
167
+ const hasPerEdgeComponents = componentsByProperty.some((components) => {
168
+ return components.length > 1;
169
+ });
170
+ const allComponents = componentsByProperty.flat();
171
+ if (!hasPerEdgeComponents || hasSubstitutedParts(allComponents)) {
163
172
  return declarations;
164
173
  }
165
- const usesCssWideKeyword = [width, style, ...colorComponents].some((value) => {
174
+ const usesCssWideKeyword = allComponents.some((value) => {
166
175
  return CSS_WIDE_KEYWORDS.has(value.toLowerCase());
167
176
  });
168
177
  if (usesCssWideKeyword) {
169
178
  return declarations;
170
179
  }
171
180
 
172
- const borderValue = buildBorderShorthandValue(width, style, colorComponents[0]) + importantSuffix;
173
- const colorValue = color + importantSuffix;
174
- const rewrittenLength = ('border:' + borderValue + ';border-color:' + colorValue).length;
181
+ const [widthComponents, styleComponents, colorComponents] = componentsByProperty;
182
+ const borderValue = buildBorderShorthandValue(
183
+ widthComponents.length === 1 ? width : '',
184
+ styleComponents.length === 1 ? style : '',
185
+ colorComponents[0]
186
+ ) + importantSuffix;
187
+ const overrides = BORDER_TRIO_PROPERTIES.flatMap((property, index) => {
188
+ if (componentsByProperty[index].length === 1) {
189
+ return [];
190
+ }
191
+ return [{
192
+ property,
193
+ value: trioValues[index]
194
+ }];
195
+ });
196
+ const replacement = [{ property: 'border', value: borderValue }, ...overrides];
197
+ const rewrittenLength = measureDeclarations(replacement);
175
198
  const longhandLength = (
176
199
  'border-width:' + trioValues[0] +
177
200
  ';border-style:' + trioValues[1] +
@@ -185,16 +208,7 @@ function collapseBorderTrioWithPerEdgeColor (declarations, context) {
185
208
  const insertionIndex = Math.min(...trioIndexes);
186
209
  return declarations.flatMap((declaration, index) => {
187
210
  if (index === insertionIndex) {
188
- return [
189
- {
190
- property: 'border',
191
- value: borderValue
192
- },
193
- {
194
- property: 'border-color',
195
- value: colorValue
196
- }
197
- ];
211
+ return replacement;
198
212
  }
199
213
  if (replacedIndexes.has(index)) {
200
214
  return [];
@@ -216,8 +230,8 @@ function classifyBorderEdgeComponent (component) {
216
230
  if (BORDER_STYLE_KEYWORDS.has(keyword)) {
217
231
  return 'border-style';
218
232
  }
219
- // Match a number with an optional unit, which is how a length is written
220
- const isLength = /^[+-]?(?:\d+|\d*\.\d+)[a-z]*$/i.test(component);
233
+ // Match a number, optional exponent, and optional length unit.
234
+ const isLength = /^[+-]?(?:\d+|\d*\.\d+)(?:e[+-]?\d+)?[a-z]*$/i.test(component);
221
235
  if (BORDER_WIDTH_KEYWORDS.has(keyword) || isLength || LENGTH_FUNCTION_PATTERN.test(component)) {
222
236
  return 'border-width';
223
237
  }
@@ -411,6 +425,6 @@ function splitBorderEdgesIntoTrio (declarations) {
411
425
  }
412
426
 
413
427
  export {
414
- collapseBorderTrioWithPerEdgeColor,
428
+ collapseBorderTrioWithPerEdgeOverrides,
415
429
  splitBorderEdgesIntoTrio
416
430
  };
@@ -7,7 +7,7 @@ import { hasInvalidQuotesCount } from '../value/quotes.js';
7
7
 
8
8
  import { absorbBackgroundLonghandsIntoShorthand } from './background.js';
9
9
  import {
10
- collapseBorderTrioWithPerEdgeColor,
10
+ collapseBorderTrioWithPerEdgeOverrides,
11
11
  splitBorderEdgesIntoTrio
12
12
  } from './border.js';
13
13
  import {
@@ -449,7 +449,7 @@ function processDeclarations (declarations, context) {
449
449
  result = foldLonghandOverridesIntoShorthands(result, context);
450
450
  result = hoistCssWideKeywordsIntoShorthands(result, context);
451
451
  result = splitBorderEdgesIntoTrio(result);
452
- result = collapseBorderTrioWithPerEdgeColor(result, context);
452
+ result = collapseBorderTrioWithPerEdgeOverrides(result, context);
453
453
 
454
454
  return orderDeclarations(result);
455
455
  }
package/src/index.js CHANGED
@@ -23,24 +23,13 @@ import {
23
23
  import { preprocessDeclarationBlocks } from './preprocess.js';
24
24
  import { combineLanguageSelectors } from './rules/lang.js';
25
25
  import { normalizeRuleSelectors } from './rules/normalize.js';
26
- import {
27
- deduplicateKeyframes,
28
- expandPureNestedRules,
29
- factorCommonParents,
30
- mergeByDeclarations,
31
- mergeIdenticalNestedRules,
32
- mergeLayerRules,
33
- mergeMediaRules,
34
- mergeSelectorRules,
35
- nestFlatRules,
36
- removeEmptyRules,
37
- removeFullyOverriddenSelectors,
38
- removeOverriddenMultiSelectorProperties
39
- } from './rules/optimize.js';
26
+ import { optimizeRuleList } from './rules/optimize.js';
27
+ import { combineLegacyAnySelectors } from './rules/selectors.js';
40
28
  import {
41
29
  removeRedundantLayerStatementSemicolon,
42
30
  stringifyRule
43
31
  } from './rules/stringify.js';
32
+ import { removeInvalidSelectorRules } from './rules/validate.js';
44
33
  import {
45
34
  clearMinifiedValueCache,
46
35
  minifyValue
@@ -64,7 +53,7 @@ function deduplicateSelectorList (selectorList) {
64
53
  seen.add(selector);
65
54
  return true;
66
55
  });
67
- return combineLanguageSelectors(unique).join(',');
56
+ return combineLanguageSelectors(combineLegacyAnySelectors(unique)).join(',');
68
57
  }
69
58
 
70
59
  /**
@@ -199,6 +188,7 @@ export const minifyCSS = function (input) {
199
188
  const context = createMinifyContext();
200
189
 
201
190
  if (ast?.stylesheet?.rules) {
191
+ ast.stylesheet.rules = removeInvalidSelectorRules(ast.stylesheet.rules);
202
192
  // Every later pass reads selectors, so they are written the one canonical
203
193
  // way before any of them runs.
204
194
  normalizeRuleSelectors(ast.stylesheet.rules);
@@ -227,24 +217,7 @@ export const minifyCSS = function (input) {
227
217
  );
228
218
  ast.stylesheet.rules = filterRedundantCharsets(ast.stylesheet.rules);
229
219
 
230
- ast.stylesheet.rules = expandPureNestedRules(ast.stylesheet.rules);
231
- ast.stylesheet.rules = mergeLayerRules(ast.stylesheet.rules, mergeSelectorRules);
232
- ast.stylesheet.rules = mergeMediaRules(ast.stylesheet.rules, mergeSelectorRules);
233
- ast.stylesheet.rules = deduplicateKeyframes(ast.stylesheet.rules);
234
-
235
- const mergedRules = mergeSelectorRules(ast.stylesheet.rules);
236
- const overrideCleanedRules = removeOverriddenMultiSelectorProperties(mergedRules);
237
- const selectorPrunedRules = removeFullyOverriddenSelectors(overrideCleanedRules);
238
- const preCleanedRules = removeEmptyRules(selectorPrunedRules);
239
- const declarationMergedRules = mergeByDeclarations(preCleanedRules);
240
- const nestedRules = nestFlatRules(declarationMergedRules);
241
- const nonEmptyRules = removeEmptyRules(nestedRules);
242
- // Nesting is what reveals that two rules style the same thing: until their
243
- // descendants have moved inside them, the rules only share a declaration.
244
- const nestingMergedRules = mergeByDeclarations(nonEmptyRules);
245
- const factoredRules = factorCommonParents(nestingMergedRules);
246
- const nestedFinalRules = nestFlatRules(factoredRules);
247
- const finalRules = mergeIdenticalNestedRules(nestedFinalRules);
220
+ const finalRules = optimizeRuleList(ast.stylesheet.rules);
248
221
 
249
222
  for (const rule of finalRules) {
250
223
  output.push(stringifyRule(rule, context));
@@ -11,12 +11,17 @@ import {
11
11
  findMatchingParenthesis,
12
12
  splitTopLevelCommaList
13
13
  } from '../parser/source-search.js';
14
+ import { tokenizeCssValue } from '../parser/tokenizer.js';
14
15
  import { escapeRegexString } from '../utilities.js';
15
16
 
16
17
  import {
17
18
  normalizeLayerNames,
18
19
  normalizeMedia
19
20
  } from './normalize.js';
21
+ import {
22
+ containsPseudoElement,
23
+ isUniversallyRecognizableSelector
24
+ } from './selectors.js';
20
25
 
21
26
  /**
22
27
  * Reports whether an entry of a rule's body is itself a nested rule rather
@@ -44,13 +49,67 @@ function expandNestedRuleBodies (rules) {
44
49
  if (rule.type !== 'rule' || !(rule.declarations || []).some(isNestedRule)) {
45
50
  return rule;
46
51
  }
52
+ const declarations = expandPureNestedRules(rule.declarations);
53
+ const canFlattenSelf = selectorsShareSpecificity(rule.selectors) && !rule.selectors.some(containsPseudoElement);
54
+ let encounteredNestedContent = false;
55
+ const flattened = declarations.flatMap((entry) => {
56
+ const isSelfRule = entry.type === 'rule' && entry.selectors?.length === 1 && entry.selectors[0].trim() === '&';
57
+ if (canFlattenSelf && isSelfRule && !encounteredNestedContent) {
58
+ return entry.declarations || [];
59
+ }
60
+ if (entry.type !== 'declaration' && entry.type !== 'whitespace' && entry.type !== 'comment') {
61
+ encounteredNestedContent = true;
62
+ }
63
+ if (entry.type === 'rule' && rule.selectors.length === 1) {
64
+ entry.selectors = entry.selectors.map((selector) => {
65
+ return shortenRepeatedNestingSelector(rule.selectors[0], selector);
66
+ });
67
+ }
68
+ return [entry];
69
+ });
47
70
  return {
48
71
  ...rule,
49
- declarations: expandPureNestedRules(rule.declarations)
72
+ declarations: flattened
50
73
  };
51
74
  });
52
75
  }
53
76
 
77
+ /**
78
+ * Replaces repetitions of a simple class or ID beside a leading nesting
79
+ * selector. Token comparisons keep longer names and escaped text intact.
80
+ *
81
+ * @param {string} parentSelector The sole parent selector.
82
+ * @param {string} childSelector The nested selector.
83
+ * @return {string} The shorter equivalent selector.
84
+ */
85
+ function shortenRepeatedNestingSelector (parentSelector, childSelector) {
86
+ const parentTokens = tokenizeCssValue(parentSelector);
87
+ const isClass = parentTokens.length === 2 && parentTokens[0].text === '.' && parentTokens[1].type === 'ident';
88
+ const isIdentifier = parentTokens.length === 1 && parentTokens[0].type === 'hash';
89
+ if (!isClass && !isIdentifier) {
90
+ return childSelector;
91
+ }
92
+ const childTokens = tokenizeCssValue(childSelector);
93
+ if (childTokens[0]?.text !== '&') {
94
+ return childSelector;
95
+ }
96
+ let index = 1;
97
+ let result = '&';
98
+ while (index < childTokens.length) {
99
+ const matchesParent = parentTokens.every((token, position) => {
100
+ return childTokens[index + position]?.text === token.text;
101
+ });
102
+ if (!matchesParent) {
103
+ break;
104
+ }
105
+ result += '&';
106
+ index += parentTokens.length;
107
+ }
108
+ return result + childTokens.slice(index).map((token) => {
109
+ return token.text;
110
+ }).join('');
111
+ }
112
+
54
113
  /**
55
114
  * Reports whether a nested child selector still means the same thing once it
56
115
  * is written out in full under its parent.
@@ -437,6 +496,26 @@ function factorCommonParents (rules) {
437
496
  let index = 0;
438
497
  while (index < rules.length) {
439
498
  const rule = rules[index];
499
+ if (rule.type === 'rule' && rule.selectors?.length > 1) {
500
+ const candidateParent = extractLeadingCompound(rule.selectors[0]);
501
+ if (candidateParent) {
502
+ const matchNestedSelector = createNestedSelectorMatcher(candidateParent);
503
+ const childSelectors = rule.selectors.map(matchNestedSelector);
504
+ const allMatch = childSelectors.every((selector) => {
505
+ return selector !== null;
506
+ });
507
+ const wrapperLength = candidateParent.length + 2 + childSelectors.join(',').length;
508
+ if (allMatch && !containsPseudoElement(candidateParent) && wrapperLength < rule.selectors.join(',').length) {
509
+ result.push({
510
+ type: 'rule',
511
+ selectors: [candidateParent],
512
+ declarations: [{ ...rule, selectors: childSelectors }]
513
+ });
514
+ index++;
515
+ continue;
516
+ }
517
+ }
518
+ }
440
519
  // Only single-selector style rules can act as a factoring candidate.
441
520
  if (rule.type !== 'rule' || rule.selectors?.length !== 1) {
442
521
  result.push(rule);
@@ -491,6 +570,42 @@ function factorCommonParents (rules) {
491
570
  return result;
492
571
  }
493
572
 
573
+ /**
574
+ * Shares a descendant prefix through `:is()` when the parent itself is also
575
+ * in the selector list, preventing a nesting wrapper from saving space.
576
+ *
577
+ * @param {object} rule The style rule whose selector list may be shortened.
578
+ */
579
+ function factorSelectorBranches (rule) {
580
+ if (rule.type !== 'rule' || !rule.selectors?.length) {
581
+ return;
582
+ }
583
+ for (const parent of [...rule.selectors]) {
584
+ const prefix = parent + ' ';
585
+ const branches = rule.selectors.filter((selector) => {
586
+ return selector.startsWith(prefix);
587
+ });
588
+ const children = branches.map((selector) => {
589
+ return selector.slice(prefix.length);
590
+ });
591
+ if (children.length < 2 || !children.every(isUniversallyRecognizableSelector) || !selectorsShareSpecificity(children)) {
592
+ continue;
593
+ }
594
+ const grouped = prefix + ':is(' + children.sort().join(',') + ')';
595
+ if (grouped.length >= branches.join(',').length) {
596
+ continue;
597
+ }
598
+ const branchSet = new Set(branches);
599
+ const firstIndex = rule.selectors.indexOf(branches[0]);
600
+ rule.selectors = rule.selectors.flatMap((selector, index) => {
601
+ if (index === firstIndex) {
602
+ return [grouped];
603
+ }
604
+ return branchSet.has(selector) ? [] : [selector];
605
+ });
606
+ }
607
+ }
608
+
494
609
  /**
495
610
  * Merges adjacent `@media` rules that share an identical normalized query string and deduplicates their child selector rules.
496
611
  *
@@ -1469,7 +1584,7 @@ function chooseMergePlacement (overrideIndex, earlierRule, laterRule, earlierPos
1469
1584
  * @param {Array} rules The AST rule nodes to merge.
1470
1585
  * @return {Array} A new array of rules with same-selector rules combined.
1471
1586
  */
1472
- function mergeSelectorRules (rules) {
1587
+ function mergeSelectorRulePass (rules) {
1473
1588
  // A rule merged at the later of the two positions moves to the end of the
1474
1589
  // output. Its old slot is emptied instead of spliced out so that every
1475
1590
  // recorded position stays valid, and the position map locates that slot
@@ -1524,6 +1639,24 @@ function mergeSelectorRules (rules) {
1524
1639
  });
1525
1640
  }
1526
1641
 
1642
+ /**
1643
+ * Reconsiders blocked pairs after merges relocate the rules that blocked them.
1644
+ * Each productive pass removes a rule, so this reaches a fixed point without
1645
+ * rerunning parsing or unrelated optimizations.
1646
+ *
1647
+ * @param {Array} rules The sibling rules to merge.
1648
+ * @return {Array} Rules with all currently safe same-selector merges.
1649
+ */
1650
+ function mergeSelectorRules (rules) {
1651
+ let previousRules = rules;
1652
+ let mergedRules = mergeSelectorRulePass(previousRules);
1653
+ while (mergedRules.length < previousRules.length) {
1654
+ previousRules = mergedRules;
1655
+ mergedRules = mergeSelectorRulePass(previousRules);
1656
+ }
1657
+ return mergedRules;
1658
+ }
1659
+
1527
1660
  /**
1528
1661
  * Merges `@layer` blocks with the same name by combining their child rules, deduplicates layer statements, and merges selector rules within each block.
1529
1662
  *
@@ -1847,6 +1980,37 @@ function removeOverriddenMultiSelectorProperties (rules) {
1847
1980
  return rules;
1848
1981
  }
1849
1982
 
1983
+ /**
1984
+ * Applies the structural optimization pipeline to a stylesheet or grouping
1985
+ * block, so merging at-rules does not leave their children unoptimized.
1986
+ *
1987
+ * @param {Array} rules The sibling AST entries.
1988
+ * @return {Array} The optimized sibling entries.
1989
+ */
1990
+ function optimizeRuleList (rules) {
1991
+ let result = expandPureNestedRules(rules);
1992
+ result = mergeLayerRules(result, mergeSelectorRules);
1993
+ result = mergeMediaRules(result, mergeSelectorRules);
1994
+ result = deduplicateKeyframes(result);
1995
+ for (const rule of result) {
1996
+ if (rule.rules) {
1997
+ rule.rules = optimizeRuleList(rule.rules);
1998
+ }
1999
+ }
2000
+ result = mergeSelectorRules(result);
2001
+ result = removeOverriddenMultiSelectorProperties(result);
2002
+ result = removeFullyOverriddenSelectors(result);
2003
+ result = removeEmptyRules(result);
2004
+ result = mergeByDeclarations(result);
2005
+ result = nestFlatRules(result);
2006
+ result = removeEmptyRules(result);
2007
+ result = mergeByDeclarations(result);
2008
+ result = factorCommonParents(result);
2009
+ result = nestFlatRules(result);
2010
+ result.forEach(factorSelectorBranches);
2011
+ return mergeIdenticalNestedRules(result);
2012
+ }
2013
+
1850
2014
  export {
1851
2015
  deduplicateKeyframes,
1852
2016
  expandPureNestedRules,
@@ -1857,6 +2021,7 @@ export {
1857
2021
  mergeMediaRules,
1858
2022
  mergeSelectorRules,
1859
2023
  nestFlatRules,
2024
+ optimizeRuleList,
1860
2025
  removeEmptyRules,
1861
2026
  removeFullyOverriddenSelectors,
1862
2027
  removeOverriddenMultiSelectorProperties
@@ -719,9 +719,50 @@ function flattenNestingParentIsSelector (selector) {
719
719
  return parts;
720
720
  }
721
721
 
722
+ /**
723
+ * Replaces matching legacy any-functions only when multiple vendor variants
724
+ * supply the same selector. A lone vendor-specific selector stays scoped to
725
+ * the browsers that understand it.
726
+ *
727
+ * @param {Array} selectors Selectors with identical declarations.
728
+ * @return {Array} The selectors with redundant legacy forms combined.
729
+ */
730
+ function combineLegacyAnySelectors (selectors) {
731
+ const legacyFunctions = [':-moz-any(', ':-webkit-any(', ':matches('];
732
+ const groups = new Map();
733
+ const entries = selectors.map((selector) => {
734
+ let canonical = selector;
735
+ const variants = new Set();
736
+ for (const functionCall of legacyFunctions) {
737
+ let position = findNextFunctionCallOutsideStrings(canonical, functionCall, 0);
738
+ while (position !== -1) {
739
+ variants.add(functionCall);
740
+ canonical = canonical.slice(0, position) + ':is(' + canonical.slice(position + functionCall.length);
741
+ position = findNextFunctionCallOutsideStrings(canonical, functionCall, position + 4);
742
+ }
743
+ }
744
+ let group = groups.get(canonical);
745
+ if (!group) {
746
+ group = new Set();
747
+ groups.set(canonical, group);
748
+ }
749
+ const variant = variants.size === 1 ? [...variants][0] : null;
750
+ if (variant) {
751
+ group.add(variant);
752
+ }
753
+ return { selector, canonical, variant };
754
+ });
755
+ return [...new Set(entries.map((entry) => {
756
+ return entry.variant && groups.get(entry.canonical).size > 1 ? entry.canonical : entry.selector;
757
+ }))];
758
+ }
759
+
722
760
  export {
761
+ combineLegacyAnySelectors,
762
+ containsPseudoElement,
723
763
  findNextFunctionCallOutsideStrings,
724
764
  flattenNestingParentIsSelector,
765
+ isUniversallyRecognizableSelector,
725
766
  mergeAdjacentWherePseudoClasses,
726
767
  processIsSelector
727
768
  };
@@ -24,6 +24,7 @@ import {
24
24
  } from './normalize.js';
25
25
  import { resolvePropertyDescriptors } from './property.js';
26
26
  import {
27
+ combineLegacyAnySelectors,
27
28
  flattenNestingParentIsSelector,
28
29
  mergeAdjacentWherePseudoClasses,
29
30
  processIsSelector
@@ -257,6 +258,7 @@ function stringifyRule (rule, context) {
257
258
  minified = minifyLanguageSelector(minified);
258
259
  return minified;
259
260
  });
261
+ uniqueSelectors = combineLegacyAnySelectors(uniqueSelectors);
260
262
  // When this rule is a nesting parent, its whole selector list is treated
261
263
  // as :is() for the children's specificity, so a top-level :is() can be
262
264
  // safely lifted into the list without altering specificity.