@thejaredwilcurt/csslop 0.0.52 → 0.0.53

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/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.52",
5
+ "version": "0.0.53",
6
6
  "description": "Experimental CSS minification",
7
7
  "scripts": {
8
8
  "prestart": "node ./scripts/pre/start.js",
@@ -35,12 +35,12 @@
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.4.2",
38
+ "eslint-plugin-jsdoc": "^65.4.4",
39
39
  "fflate": "^0.8.3",
40
40
  "globals": "^17.14.0",
41
41
  "pretty-ms": "^10.0.0",
42
42
  "real-world-css-libraries": "^1.0.9",
43
- "vite": "^8.3.5"
43
+ "vite": "^8.3.6"
44
44
  },
45
45
  "author": "The Jared Wilcurt",
46
46
  "homepage": "https://github.com/TheJaredWilcurt/csslop#readme",
@@ -61,7 +61,7 @@
61
61
  },
62
62
  "packageManager": {
63
63
  "name": "npm",
64
- "version": "12.2.1"
64
+ "version": "12.2.2"
65
65
  }
66
66
  }
67
67
  }
@@ -186,6 +186,17 @@ function usesVendorPrefixedDisplayValue (value) {
186
186
  });
187
187
  }
188
188
 
189
+ /**
190
+ * Checks if a declaration value contains the IE6-IE9 double !important hack.
191
+ *
192
+ * @param {string} value The minified CSS value string.
193
+ * @return {boolean} Whether the value contains double !important.
194
+ */
195
+ function hasDoubleImportant (value) {
196
+ // Match !important appearing twice in the value
197
+ return /!important.*!important/.test(value);
198
+ }
199
+
189
200
  /**
190
201
  * Normalizes a vendor-prefixed display value to its standard equivalent.
191
202
  *
@@ -366,6 +377,11 @@ function deduplicateDeclarations (declarations) {
366
377
  continue;
367
378
  }
368
379
 
380
+ // IE6-IE9 double !important properties are invalid and should be removed
381
+ if (hasDoubleImportant(minifiedValue)) {
382
+ continue;
383
+ }
384
+
369
385
  // A vendor-prefixed property that repeats the value of its unprefixed
370
386
  // form is dead code for browsers that read the unprefixed one
371
387
  if (propertyName.startsWith('-') && !propertyName.startsWith('--')) {
@@ -141,6 +141,17 @@ function normalizeLayerNames (layerNames) {
141
141
  return String(layerNames ?? '').trim().replace(/\s*,\s*/g, ',');
142
142
  }
143
143
 
144
+ /**
145
+ * Checks if a media query contains the IE10/IE11 exclusive `\0` hack.
146
+ *
147
+ * @param {string} media The media query string to check.
148
+ * @return {boolean} True if the media query contains the IE hack.
149
+ */
150
+ function containsIEHack (media) {
151
+ // Match the \0 character which is used as an IE10/IE11 exclusive hack
152
+ return /\\0/.test(media);
153
+ }
154
+
144
155
  /**
145
156
  * Normalizes a `@media` query string by collapsing whitespace, stripping the default "all and" prefix, and converting min/max-width to range syntax.
146
157
  *
@@ -236,6 +247,7 @@ function canUnwrapSupports (supports) {
236
247
 
237
248
  export {
238
249
  canUnwrapSupports,
250
+ containsIEHack,
239
251
  normalizeLayerNames,
240
252
  normalizeMedia,
241
253
  normalizeRuleSelectors,
@@ -3,10 +3,26 @@
3
3
  */
4
4
 
5
5
  import {
6
+ containsIEHack,
6
7
  normalizeLayerNames,
7
8
  normalizeMedia
8
9
  } from '../normalize.js';
9
10
 
11
+ /**
12
+ * Removes `@media` rules that contain IE10/IE11 exclusive hacks (the `\0` character).
13
+ *
14
+ * @param {Array} rules The AST rule nodes to process.
15
+ * @return {Array} A new array of rules with IE-exclusive media queries removed.
16
+ */
17
+ function removeIEMediaRules (rules) {
18
+ return rules.filter((rule) => {
19
+ if (rule.type === 'media' && containsIEHack(rule.media)) {
20
+ return false;
21
+ }
22
+ return true;
23
+ });
24
+ }
25
+
10
26
  /**
11
27
  * Merges adjacent `@media` rules that share an identical normalized query string and deduplicates their child selector rules.
12
28
  *
@@ -109,5 +125,6 @@ function mergeLayerRules (rules, mergeSelectorRules) {
109
125
  export {
110
126
  deduplicateKeyframes,
111
127
  mergeLayerRules,
112
- mergeMediaRules
128
+ mergeMediaRules,
129
+ removeIEMediaRules
113
130
  };
@@ -5,7 +5,8 @@
5
5
  import {
6
6
  deduplicateKeyframes,
7
7
  mergeLayerRules,
8
- mergeMediaRules
8
+ mergeMediaRules,
9
+ removeIEMediaRules
9
10
  } from './at-rules.js';
10
11
  import { removeEmptyRules } from './empty-rules.js';
11
12
  import { expandPureNestedRules } from './expand-nesting.js';
@@ -43,6 +44,7 @@ function clearSelectorCaches () {
43
44
  */
44
45
  function optimizeRuleList (rules) {
45
46
  let result = expandPureNestedRules(rules);
47
+ result = removeIEMediaRules(result);
46
48
  result = mergeLayerRules(result, mergeSelectorRules);
47
49
  result = mergeMediaRules(result, mergeSelectorRules);
48
50
  result = deduplicateKeyframes(result);
@@ -175,7 +175,193 @@ function mergeRuleIntoPredecessor (previousRule, rule) {
175
175
  }
176
176
 
177
177
  /**
178
- * Merges consecutive rules whose declarations are a subset of the following rule, combining their selectors and splitting out any extra declarations.
178
+ * Finds properties that are present in all adjacent rules with matching values.
179
+ *
180
+ * @param {Array} adjacentRules The consecutive rule nodes to compare.
181
+ * @return {Map} Map of property name to value for properties common to all rules.
182
+ */
183
+ function findCommonProperties (adjacentRules) {
184
+ if (adjacentRules.length < 2) {
185
+ return new Map();
186
+ }
187
+
188
+ const firstRuleBody = partitionRuleBody(adjacentRules[0].declarations);
189
+ const commonProperties = new Map(
190
+ firstRuleBody.plainDeclarations.map((declaration) => {
191
+ return [declaration.property, (declaration.value || '').trim()];
192
+ })
193
+ );
194
+
195
+ for (let ruleIndex = 1; ruleIndex < adjacentRules.length; ruleIndex++) {
196
+ const currentRuleBody = partitionRuleBody(adjacentRules[ruleIndex].declarations);
197
+ const currentValueByProperty = new Map(
198
+ currentRuleBody.plainDeclarations.map((declaration) => {
199
+ return [declaration.property, (declaration.value || '').trim()];
200
+ })
201
+ );
202
+
203
+ for (const [property, value] of commonProperties) {
204
+ if (currentValueByProperty.get(property) !== value) {
205
+ commonProperties.delete(property);
206
+ }
207
+ }
208
+ }
209
+
210
+ return commonProperties;
211
+ }
212
+
213
+ /**
214
+ * Checks if a rule contains the `all` property, which resets all other properties.
215
+ *
216
+ * @param {object} rule The rule node to check.
217
+ * @return {boolean} True if the rule contains the `all` property.
218
+ */
219
+ function hasAllProperty (rule) {
220
+ const ruleBody = partitionRuleBody(rule.declarations);
221
+ return ruleBody.plainDeclarations.some((declaration) => {
222
+ return declaration.property === 'all';
223
+ });
224
+ }
225
+
226
+ /**
227
+ * Checks if any rule in a list contains the `all` property.
228
+ *
229
+ * @param {Array} rules The rule nodes to check.
230
+ * @return {boolean} True if any rule contains the `all` property.
231
+ */
232
+ function anyRuleHasAllProperty (rules) {
233
+ return rules.some(hasAllProperty);
234
+ }
235
+
236
+ /**
237
+ * Checks if any rule has nested blocks.
238
+ *
239
+ * @param {Array} rules The rule nodes to check.
240
+ * @return {boolean} True if any rule has nested blocks.
241
+ */
242
+ function anyRuleHasNestedBlocks (rules) {
243
+ return rules.some((rule) => {
244
+ const ruleBody = partitionRuleBody(rule.declarations);
245
+ return ruleBody.nestedBlocks.length > 0;
246
+ });
247
+ }
248
+
249
+ /**
250
+ * Combines adjacent rules that share common properties by creating a merged rule
251
+ * with all properties from the first rule, and keeping only the differing properties
252
+ * in each subsequent rule.
253
+ *
254
+ * @param {Array} adjacentRules The consecutive rule nodes to merge.
255
+ * @param {Map} commonProps The properties common to all rules.
256
+ * @return {Array} The rules after merging common properties.
257
+ */
258
+ function mergeCommonProperties (adjacentRules, commonProps) {
259
+ if (commonProps.size === 0) {
260
+ return adjacentRules;
261
+ }
262
+
263
+ // Skip merging if any rule has the `all` property
264
+ if (anyRuleHasAllProperty(adjacentRules)) {
265
+ return adjacentRules;
266
+ }
267
+
268
+ // Skip merging if any rule has nested blocks
269
+ if (anyRuleHasNestedBlocks(adjacentRules)) {
270
+ return adjacentRules;
271
+ }
272
+
273
+ const firstRuleBody = partitionRuleBody(adjacentRules[0].declarations);
274
+
275
+ const combinedSelectors = [];
276
+ for (const rule of adjacentRules) {
277
+ combinedSelectors.push(...rule.selectors);
278
+ }
279
+
280
+ const mergedRule = {
281
+ ...adjacentRules[0],
282
+ selectors: deduplicateSelectors(combinedSelectors),
283
+ declarations: firstRuleBody.plainDeclarations
284
+ };
285
+
286
+ const result = [mergedRule];
287
+
288
+ for (let ruleIndex = 1; ruleIndex < adjacentRules.length; ruleIndex++) {
289
+ const rule = adjacentRules[ruleIndex];
290
+ const ruleBody = partitionRuleBody(rule.declarations);
291
+ const differingDeclarations = ruleBody.plainDeclarations.filter((declaration) => {
292
+ const firstRuleValue = firstRuleBody.plainDeclarations.find((decl) => {
293
+ return decl.property === declaration.property;
294
+ });
295
+ if (!firstRuleValue) {
296
+ return true;
297
+ }
298
+ return (declaration.value || '').trim() !== (firstRuleValue.value || '').trim();
299
+ });
300
+
301
+ if (differingDeclarations.length > 0) {
302
+ result.push({
303
+ ...rule,
304
+ declarations: differingDeclarations
305
+ });
306
+ }
307
+ }
308
+
309
+ // Check if the merge actually reduces size
310
+ const originalSize = adjacentRules.reduce((sum, rule) => {
311
+ return sum + calculateRuleSize(rule);
312
+ }, 0);
313
+
314
+ const mergedSize = result.reduce((sum, rule) => {
315
+ return sum + calculateRuleSize(rule);
316
+ }, 0);
317
+
318
+ if (mergedSize >= originalSize) {
319
+ return adjacentRules;
320
+ }
321
+
322
+ return result;
323
+ }
324
+
325
+ /**
326
+ * Calculates the approximate character size of a rule's selectors.
327
+ *
328
+ * @param {object} rule The rule node.
329
+ * @return {number} The total character count of all selectors.
330
+ */
331
+ function calculateSelectorSize (rule) {
332
+ if (!rule.selectors) {
333
+ return 0;
334
+ }
335
+ return rule.selectors.join(',').length;
336
+ }
337
+
338
+ /**
339
+ * Calculates the approximate character size of a rule's declarations.
340
+ *
341
+ * @param {object} rule The rule node.
342
+ * @return {number} The total character count of all declarations.
343
+ */
344
+ function calculateDeclarationSize (rule) {
345
+ const ruleBody = partitionRuleBody(rule.declarations);
346
+ let size = 0;
347
+ for (const declaration of ruleBody.plainDeclarations) {
348
+ size += declaration.property.length + 1 + (declaration.value || '').length;
349
+ }
350
+ return size;
351
+ }
352
+
353
+ /**
354
+ * Calculates the approximate character size of a rule.
355
+ *
356
+ * @param {object} rule The rule node.
357
+ * @return {number} The total character count of the rule.
358
+ */
359
+ function calculateRuleSize (rule) {
360
+ return calculateSelectorSize(rule) + calculateDeclarationSize(rule);
361
+ }
362
+
363
+ /**
364
+ * Merges consecutive rules whose declarations are a subset of the following rule, combining their selectors and splitting out any extra declarations. Also combines common properties across adjacent rules.
179
365
  *
180
366
  * @param {Array} rules The AST rule nodes to merge.
181
367
  * @return {Array} A new array of rules with declaration-compatible consecutive rules combined.
@@ -191,7 +377,36 @@ function mergeByDeclarations (rules) {
191
377
  }
192
378
  result.push(rule);
193
379
  }
194
- return result;
380
+
381
+ // Pass to combine common properties across adjacent rules
382
+ const finalResult = [];
383
+ let adjacentRules = [];
384
+
385
+ for (const rule of result) {
386
+ if (rule.type === 'rule' && rule.selectors?.length) {
387
+ adjacentRules.push(rule);
388
+ } else {
389
+ if (adjacentRules.length >= 2) {
390
+ const commonProps = findCommonProperties(adjacentRules);
391
+ const merged = mergeCommonProperties(adjacentRules, commonProps);
392
+ finalResult.push(...merged);
393
+ } else if (adjacentRules.length === 1) {
394
+ finalResult.push(adjacentRules[0]);
395
+ }
396
+ adjacentRules = [];
397
+ finalResult.push(rule);
398
+ }
399
+ }
400
+
401
+ if (adjacentRules.length >= 2) {
402
+ const commonProps = findCommonProperties(adjacentRules);
403
+ const merged = mergeCommonProperties(adjacentRules, commonProps);
404
+ finalResult.push(...merged);
405
+ } else if (adjacentRules.length === 1) {
406
+ finalResult.push(adjacentRules[0]);
407
+ }
408
+
409
+ return finalResult;
195
410
  }
196
411
 
197
412
  export {