@thejaredwilcurt/csslop 0.0.5 → 0.0.6
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 +3 -3
- package/src/index.js +7 -2
- package/src/rules/optimize.js +143 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
* Use the tests from the open source, 3rd-party, CSS minification auditing library [css-minify-tests](https://github.com/keithamus/css-minify-tests).
|
|
16
16
|
* Have AI completely generate the library logic to pass all tests.
|
|
17
|
-
* Have me (an experienced library author) validate the outcomes and [make upstream corrections](https://github.com/keithamus/css-minify-tests/
|
|
17
|
+
* Have me (an experienced library author) validate the outcomes and [make upstream corrections](https://github.com/keithamus/css-minify-tests/issues?q=author%3ATheJaredWilcurt) to the tests library.
|
|
18
18
|
|
|
19
19
|
**The Results:**
|
|
20
20
|
|
package/package.json
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"name": "@thejaredwilcurt/csslop",
|
|
3
3
|
"main": "index.js",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.6",
|
|
6
6
|
"description": "Experimental CSS minification",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"copy": "node ./scripts/copyTests.js",
|
|
9
|
-
"test": "node ./tests/
|
|
10
|
-
"real": "node ./tests/
|
|
9
|
+
"test": "node ./tests/index.test.js",
|
|
10
|
+
"real": "node ./tests/realworld.test.js",
|
|
11
11
|
"lint": "eslint *.js scripts src tests --fix",
|
|
12
12
|
"bump": "npx --yes -- @jsdevtools/version-bump-prompt && npm i",
|
|
13
13
|
"publish": "npm publish --access=public"
|
package/src/index.js
CHANGED
|
@@ -20,11 +20,13 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
deduplicateKeyframes,
|
|
22
22
|
expandPureNestedRules,
|
|
23
|
+
factorCommonParents,
|
|
23
24
|
mergeByDeclarations,
|
|
24
25
|
mergeLayerRules,
|
|
25
26
|
mergeMediaRules,
|
|
26
27
|
mergeSelectorRules,
|
|
27
|
-
nestFlatRules
|
|
28
|
+
nestFlatRules,
|
|
29
|
+
removeEmptyRules
|
|
28
30
|
} from './rules/optimize.js';
|
|
29
31
|
import { stringifyRule } from './rules/stringify.js';
|
|
30
32
|
import { minifyValue } from './value/minify.js';
|
|
@@ -84,7 +86,10 @@ export const minifyCSS = function (input) {
|
|
|
84
86
|
|
|
85
87
|
const mergedRules = mergeSelectorRules(ast.stylesheet.rules);
|
|
86
88
|
const declarationMergedRules = mergeByDeclarations(mergedRules);
|
|
87
|
-
const
|
|
89
|
+
const nestedRules = nestFlatRules(declarationMergedRules);
|
|
90
|
+
const nonEmptyRules = removeEmptyRules(nestedRules);
|
|
91
|
+
const factoredRules = factorCommonParents(nonEmptyRules);
|
|
92
|
+
const finalRules = nestFlatRules(factoredRules);
|
|
88
93
|
|
|
89
94
|
for (const rule of finalRules) {
|
|
90
95
|
output.push(stringifyRule(rule, context));
|
package/src/rules/optimize.js
CHANGED
|
@@ -123,6 +123,50 @@ function tryNestSelector (parentSel, childSel) {
|
|
|
123
123
|
return null;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Determines whether a rule is effectively empty, containing no meaningful
|
|
128
|
+
* CSS output after minification. A rule is effectively empty when it has
|
|
129
|
+
* no declarations, or all of its entries are whitespace, non-important
|
|
130
|
+
* comments, or recursively empty nested rules.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} rule The AST rule node to evaluate.
|
|
133
|
+
* @return {boolean} True if the rule produces no CSS output.
|
|
134
|
+
*/
|
|
135
|
+
function isRuleEffectivelyEmpty (rule) {
|
|
136
|
+
if (rule.type !== 'rule') {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const nonWhitespaceEntries = (rule.declarations || []).filter((declaration) => {
|
|
140
|
+
return declaration.type !== 'whitespace';
|
|
141
|
+
});
|
|
142
|
+
if (nonWhitespaceEntries.length === 0) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
return nonWhitespaceEntries.every((entry) => {
|
|
146
|
+
if (entry.type === 'comment') {
|
|
147
|
+
return !entry.comment?.startsWith('!');
|
|
148
|
+
}
|
|
149
|
+
if (entry.type === 'rule') {
|
|
150
|
+
return isRuleEffectivelyEmpty(entry);
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Filters out effectively empty rules from the rules array, preventing
|
|
158
|
+
* empty rules from being nested into parent rules during later
|
|
159
|
+
* optimization passes and producing incorrect non-empty output.
|
|
160
|
+
*
|
|
161
|
+
* @param {Array} rules The AST rule nodes to filter.
|
|
162
|
+
* @return {Array} A new array with effectively empty rules removed.
|
|
163
|
+
*/
|
|
164
|
+
function removeEmptyRules (rules) {
|
|
165
|
+
return rules.filter((rule) => {
|
|
166
|
+
return !isRuleEffectivelyEmpty(rule);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
126
170
|
/**
|
|
127
171
|
* Groups flat CSS rules into nested structures where a child selector can be expressed relative to a preceding parent, reducing output size through CSS nesting.
|
|
128
172
|
*
|
|
@@ -171,6 +215,102 @@ function nestFlatRules (rules) {
|
|
|
171
215
|
return result;
|
|
172
216
|
}
|
|
173
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Extracts the leading compound selector of a complex selector: everything up to
|
|
220
|
+
* the first top-level combinator (descendant whitespace, `>`, `+`, or `~`).
|
|
221
|
+
* Combinator characters nested inside `()` or `[]` (such as `:nth-child(2n+1)` or
|
|
222
|
+
* `[a~=b]`) are ignored so only structural combinators split the selector.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} selector The selector string to inspect.
|
|
225
|
+
* @return {string|null} The leading compound selector, or null when the selector has no descendant part to factor out.
|
|
226
|
+
*/
|
|
227
|
+
function extractLeadingCompound (selector) {
|
|
228
|
+
const trimmed = selector.trim();
|
|
229
|
+
let bracketDepth = 0;
|
|
230
|
+
for (let index = 0; index < trimmed.length; index++) {
|
|
231
|
+
const character = trimmed[index];
|
|
232
|
+
if (character === '(' || character === '[') {
|
|
233
|
+
bracketDepth++;
|
|
234
|
+
} else if (character === ')' || character === ']') {
|
|
235
|
+
bracketDepth--;
|
|
236
|
+
} else if (
|
|
237
|
+
bracketDepth === 0 &&
|
|
238
|
+
(character === ' ' || character === '>' || character === '+' || character === '~')
|
|
239
|
+
) {
|
|
240
|
+
const compound = trimmed.slice(0, index).trim();
|
|
241
|
+
return compound.length ? compound : null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Groups consecutive sibling rules that share a common leading compound selector
|
|
249
|
+
* (such as `.foo` in `.foo .a`, `.foo .b`) into a synthesized parent rule with the
|
|
250
|
+
* shared portion factored out, but only when nesting trims more characters from the
|
|
251
|
+
* child selectors than the wrapper itself costs.
|
|
252
|
+
*
|
|
253
|
+
* @param {Array} rules The flat AST rule nodes to factor.
|
|
254
|
+
* @return {Array} A new array of rules with shared parent selectors factored into nesting wrappers.
|
|
255
|
+
*/
|
|
256
|
+
function factorCommonParents (rules) {
|
|
257
|
+
const result = [];
|
|
258
|
+
let index = 0;
|
|
259
|
+
while (index < rules.length) {
|
|
260
|
+
const rule = rules[index];
|
|
261
|
+
// Only single-selector style rules can act as a factoring candidate.
|
|
262
|
+
if (rule.type !== 'rule' || rule.selectors?.length !== 1) {
|
|
263
|
+
result.push(rule);
|
|
264
|
+
index++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const candidateParent = extractLeadingCompound(rule.selectors[0]);
|
|
268
|
+
if (candidateParent === null) {
|
|
269
|
+
result.push(rule);
|
|
270
|
+
index++;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
// Collect the run of consecutive rules that can all nest under the candidate.
|
|
274
|
+
const run = [];
|
|
275
|
+
const nestedForms = [];
|
|
276
|
+
let lookahead = index;
|
|
277
|
+
while (lookahead < rules.length) {
|
|
278
|
+
const sibling = rules[lookahead];
|
|
279
|
+
if (sibling.type !== 'rule' || sibling.selectors?.length !== 1) {
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
const nestedSelector = tryNestSelector(candidateParent, sibling.selectors[0]);
|
|
283
|
+
if (nestedSelector === null) {
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
run.push(sibling);
|
|
287
|
+
nestedForms.push(nestedSelector);
|
|
288
|
+
lookahead++;
|
|
289
|
+
}
|
|
290
|
+
// The wrapper writes the shared selector once plus its surrounding braces.
|
|
291
|
+
const wrapperCost = candidateParent.length + 2;
|
|
292
|
+
const charactersSaved = run.reduce((total, sibling, position) => {
|
|
293
|
+
return total + sibling.selectors[0].trim().length - nestedForms[position].length;
|
|
294
|
+
}, 0);
|
|
295
|
+
if (run.length >= 2 && charactersSaved > wrapperCost) {
|
|
296
|
+
const children = run.map((sibling, position) => {
|
|
297
|
+
return { ...sibling, selectors: [nestedForms[position]] };
|
|
298
|
+
});
|
|
299
|
+
result.push({
|
|
300
|
+
type: 'rule',
|
|
301
|
+
selectors: [candidateParent],
|
|
302
|
+
// Recurse so deeper shared prefixes among the children also factor out.
|
|
303
|
+
declarations: factorCommonParents(children)
|
|
304
|
+
});
|
|
305
|
+
index = lookahead;
|
|
306
|
+
} else {
|
|
307
|
+
result.push(rule);
|
|
308
|
+
index++;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
|
|
174
314
|
/**
|
|
175
315
|
* Merges adjacent `@media` rules that share an identical normalized query string and deduplicates their child selector rules.
|
|
176
316
|
*
|
|
@@ -367,9 +507,11 @@ function mergeLayerRules (rules, mergeSelectorRules) {
|
|
|
367
507
|
export {
|
|
368
508
|
deduplicateKeyframes,
|
|
369
509
|
expandPureNestedRules,
|
|
510
|
+
factorCommonParents,
|
|
370
511
|
mergeByDeclarations,
|
|
371
512
|
mergeLayerRules,
|
|
372
513
|
mergeMediaRules,
|
|
373
514
|
mergeSelectorRules,
|
|
374
|
-
nestFlatRules
|
|
515
|
+
nestFlatRules,
|
|
516
|
+
removeEmptyRules
|
|
375
517
|
};
|