@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 +1 -1
- package/package.json +2 -2
- package/src/declarations/border.js +47 -33
- package/src/declarations/process.js +2 -2
- package/src/index.js +6 -33
- package/src/rules/optimize.js +167 -2
- package/src/rules/selectors.js +41 -0
- package/src/rules/stringify.js +2 -0
- package/src/rules/validate.js +145 -0
- package/src/value/color-mix.js +157 -68
- package/src/value/colors.js +36 -1
- package/src/value/font.js +111 -0
- package/src/value/minify.js +160 -33
- package/src/value/svg-data-uri.js +280 -0
- package/src/value/svg-markup.js +557 -0
- package/src/value/tokens.js +38 -1
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.
|
|
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.
|
|
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 {
|
|
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
|
|
111
|
-
*
|
|
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
|
|
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
|
|
154
|
-
const
|
|
155
|
-
return
|
|
156
|
-
};
|
|
157
|
-
const
|
|
158
|
-
|
|
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 =
|
|
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
|
|
173
|
-
const
|
|
174
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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));
|
package/src/rules/optimize.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
package/src/rules/selectors.js
CHANGED
|
@@ -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
|
};
|
package/src/rules/stringify.js
CHANGED
|
@@ -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.
|