@thejaredwilcurt/csslop 0.0.50 → 0.0.52

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.50",
5
+ "version": "0.0.52",
6
6
  "description": "Experimental CSS minification",
7
7
  "scripts": {
8
8
  "prestart": "node ./scripts/pre/start.js",
@@ -37,7 +37,7 @@
37
37
  "eslint-plugin-import-x": "^4.17.0",
38
38
  "eslint-plugin-jsdoc": "^65.4.2",
39
39
  "fflate": "^0.8.3",
40
- "globals": "^17.13.0",
40
+ "globals": "^17.14.0",
41
41
  "pretty-ms": "^10.0.0",
42
42
  "real-world-css-libraries": "^1.0.9",
43
43
  "vite": "^8.3.5"
@@ -106,6 +106,33 @@ function collectCandidateShorthands (declaredProperties) {
106
106
  */
107
107
  const PARSE_TIME_FALLBACK_MARKERS = ['calc(', 'env(', '-webkit-'];
108
108
 
109
+ /**
110
+ * Vendor-prefixed gradient functions that should be removed when the standard
111
+ * version is present. These are obsolete as all modern browsers support the
112
+ * unprefixed versions.
113
+ *
114
+ * @type {Array}
115
+ */
116
+ const VENDOR_PREFIXED_GRADIENTS = [
117
+ '-moz-linear-gradient',
118
+ '-webkit-linear-gradient',
119
+ '-o-linear-gradient',
120
+ '-ms-linear-gradient'
121
+ ];
122
+
123
+ /**
124
+ * Vendor-prefixed display values that should be removed when the standard
125
+ * version is present. These are obsolete as all modern browsers support the
126
+ * unprefixed versions.
127
+ *
128
+ * @type {Array}
129
+ */
130
+ const VENDOR_PREFIXED_DISPLAY_VALUES = [
131
+ '-webkit-box',
132
+ '-ms-flexbox',
133
+ '-webkit-flex'
134
+ ];
135
+
109
136
  /**
110
137
  * Determines whether a value relies on syntax that older browsers cannot parse,
111
138
  * which means a preceding declaration for the same property is an intentional
@@ -120,6 +147,61 @@ function needsParseTimeFallback (value) {
120
147
  });
121
148
  }
122
149
 
150
+ /**
151
+ * Determines whether a value uses a vendor-prefixed gradient function.
152
+ *
153
+ * @param {string} value The minified CSS value string.
154
+ * @return {boolean} Whether the value contains a vendor-prefixed gradient.
155
+ */
156
+ function usesVendorPrefixedGradient (value) {
157
+ return VENDOR_PREFIXED_GRADIENTS.some((gradient) => {
158
+ return value.includes(gradient);
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Extracts the standard gradient from a vendor-prefixed gradient value by
164
+ * replacing the vendor prefix with the standard name.
165
+ *
166
+ * @param {string} value The minified CSS value string.
167
+ * @return {string} The value with vendor prefixes replaced.
168
+ */
169
+ function normalizeVendorPrefixedGradient (value) {
170
+ let normalized = value;
171
+ for (const prefixedGradient of VENDOR_PREFIXED_GRADIENTS) {
172
+ normalized = normalized.replace(prefixedGradient, 'linear-gradient');
173
+ }
174
+ return normalized;
175
+ }
176
+
177
+ /**
178
+ * Determines whether a value uses a vendor-prefixed display value.
179
+ *
180
+ * @param {string} value The minified CSS value string.
181
+ * @return {boolean} Whether the value contains a vendor-prefixed display value.
182
+ */
183
+ function usesVendorPrefixedDisplayValue (value) {
184
+ return VENDOR_PREFIXED_DISPLAY_VALUES.some((displayValue) => {
185
+ return value === displayValue;
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Normalizes a vendor-prefixed display value to its standard equivalent.
191
+ *
192
+ * @param {string} value The minified CSS value string.
193
+ * @return {string} The normalized display value.
194
+ */
195
+ function normalizeVendorPrefixedDisplayValue (value) {
196
+ // Map vendor-prefixed display values to their standard equivalents
197
+ const displayValueMap = {
198
+ '-webkit-box': 'flex',
199
+ '-ms-flexbox': 'flex',
200
+ '-webkit-flex': 'flex'
201
+ };
202
+ return displayValueMap[value] || value;
203
+ }
204
+
123
205
  /**
124
206
  * Creates the bookkeeping for the declarations that survive deduplication.
125
207
  *
@@ -197,6 +279,21 @@ function findLastPositionOfProperty (survivors, property) {
197
279
  return positions[positions.length - 1];
198
280
  }
199
281
 
282
+ /**
283
+ * Finds all surviving declarations that set a given property, in source order.
284
+ *
285
+ * @param {object} survivors The surviving-declaration bookkeeping.
286
+ * @param {string} property The property name to look for.
287
+ * @return {Array} The positions of all matching declarations, in source order.
288
+ */
289
+ function findAllPositionsOfProperty (survivors, property) {
290
+ const positions = survivors.positionsByProperty.get(property);
291
+ if (!positions?.length) {
292
+ return [];
293
+ }
294
+ return [...positions];
295
+ }
296
+
200
297
  /**
201
298
  * Finds the surviving vendor-prefixed declarations that set a prefixed form of
202
299
  * a property, such as `-webkit-transform` for `transform`, including legacy
@@ -264,6 +361,11 @@ function deduplicateDeclarations (declarations) {
264
361
 
265
362
  const minifiedValue = minifyValue(declaration);
266
363
 
364
+ // IE6 hack properties starting with * are obsolete and should be removed
365
+ if (propertyName.startsWith('*')) {
366
+ continue;
367
+ }
368
+
267
369
  // A vendor-prefixed property that repeats the value of its unprefixed
268
370
  // form is dead code for browsers that read the unprefixed one
269
371
  if (propertyName.startsWith('-') && !propertyName.startsWith('--')) {
@@ -273,7 +375,7 @@ function deduplicateDeclarations (declarations) {
273
375
  }
274
376
  }
275
377
 
276
- const previousPosition = findLastPositionOfProperty(survivors, propertyName);
378
+ let previousPosition = findLastPositionOfProperty(survivors, propertyName);
277
379
 
278
380
  // An unprefixed property with the same value also replaces every prefixed form of it
279
381
  let prefixedPositions = [];
@@ -288,6 +390,22 @@ function deduplicateDeclarations (declarations) {
288
390
  }
289
391
  }
290
392
 
393
+ // For display property, remove vendor-prefixed values when standard version is present
394
+ if (propertyName === 'display' && !usesVendorPrefixedDisplayValue(minifiedValue)) {
395
+ const allPositions = findAllPositionsOfProperty(survivors, propertyName);
396
+ for (const position of allPositions) {
397
+ const existingValue = minifyValue(survivors.slots[position]);
398
+ if (
399
+ usesVendorPrefixedDisplayValue(existingValue) &&
400
+ normalizeVendorPrefixedDisplayValue(existingValue) === minifiedValue
401
+ ) {
402
+ dropDeclaration(survivors, position);
403
+ }
404
+ }
405
+ // Recalculate previous position after dropping declarations
406
+ previousPosition = findLastPositionOfProperty(survivors, propertyName);
407
+ }
408
+
291
409
  if (previousPosition !== -1) {
292
410
  const previousValue = minifyValue(survivors.slots[previousPosition]);
293
411
 
@@ -295,6 +413,42 @@ function deduplicateDeclarations (declarations) {
295
413
  continue;
296
414
  }
297
415
 
416
+ // When the current value is a simple value that all browsers understand,
417
+ // remove ALL previous declarations of the same property since the current
418
+ // one works everywhere and makes all earlier ones redundant
419
+ if (!needsParseTimeFallback(minifiedValue)) {
420
+ const allPositions = findAllPositionsOfProperty(survivors, propertyName);
421
+ for (const position of allPositions) {
422
+ dropDeclaration(survivors, position);
423
+ }
424
+ keepDeclaration(survivors, declaration);
425
+ continue;
426
+ }
427
+
428
+ // When the current value is a standard gradient and the previous value
429
+ // is a vendor-prefixed version of the same gradient, remove the prefixed one
430
+ if (
431
+ !usesVendorPrefixedGradient(minifiedValue) &&
432
+ usesVendorPrefixedGradient(previousValue) &&
433
+ normalizeVendorPrefixedGradient(previousValue) === minifiedValue
434
+ ) {
435
+ dropDeclaration(survivors, previousPosition);
436
+ keepDeclaration(survivors, declaration);
437
+ continue;
438
+ }
439
+
440
+ // When the current value is a standard display value and the previous value
441
+ // is a vendor-prefixed version that maps to the same standard value, remove the prefixed one
442
+ if (
443
+ !usesVendorPrefixedDisplayValue(minifiedValue) &&
444
+ usesVendorPrefixedDisplayValue(previousValue) &&
445
+ normalizeVendorPrefixedDisplayValue(previousValue) === minifiedValue
446
+ ) {
447
+ dropDeclaration(survivors, previousPosition);
448
+ keepDeclaration(survivors, declaration);
449
+ continue;
450
+ }
451
+
298
452
  // Fallbacks for functions older browsers cannot parse should be kept
299
453
  if (needsParseTimeFallback(minifiedValue) && !needsParseTimeFallback(previousValue)) {
300
454
  keepDeclaration(survivors, declaration);
@@ -163,19 +163,56 @@ function nestFlatRules (rules) {
163
163
  const result = [];
164
164
  const indexBySelector = new Map();
165
165
  for (const rule of rules) {
166
- if (rule.type !== 'rule' || rule.selectors?.length !== 1) {
166
+ if (rule.type !== 'rule') {
167
167
  result.push(rule);
168
168
  continue;
169
169
  }
170
- const childSelector = rule.selectors[0].trim();
171
- const nesting = findNestingParent(result, indexBySelector, childSelector);
172
- if (nesting) {
173
- const parentRule = result[nesting.parentIndex];
174
- parentRule.declarations = parentRule.declarations || [];
175
- parentRule.declarations.push({ ...rule, selectors: [nesting.nestedSelector] });
170
+ if (rule.selectors?.length === 1) {
171
+ const childSelector = rule.selectors[0].trim();
172
+ const nesting = findNestingParent(result, indexBySelector, childSelector);
173
+ if (nesting) {
174
+ const parentRule = result[nesting.parentIndex];
175
+ parentRule.declarations = parentRule.declarations || [];
176
+ parentRule.declarations.push({ ...rule, selectors: [nesting.nestedSelector] });
177
+ continue;
178
+ }
179
+ indexBySelector.set(childSelector, result.length);
180
+ result.push(rule);
181
+ continue;
182
+ }
183
+ if (rule.selectors?.length > 1) {
184
+ const nestedSelectors = [];
185
+ let commonParentIndex = null;
186
+ let canNestAll = true;
187
+
188
+ for (const childSelector of rule.selectors) {
189
+ const nesting = findNestingParent(result, indexBySelector, childSelector.trim());
190
+ if (!nesting) {
191
+ canNestAll = false;
192
+ break;
193
+ }
194
+ if (commonParentIndex === null) {
195
+ commonParentIndex = nesting.parentIndex;
196
+ } else if (nesting.parentIndex !== commonParentIndex) {
197
+ canNestAll = false;
198
+ break;
199
+ }
200
+ nestedSelectors.push(nesting.nestedSelector);
201
+ }
202
+
203
+ if (canNestAll && commonParentIndex !== null) {
204
+ const parentRule = result[commonParentIndex];
205
+ parentRule.declarations = parentRule.declarations || [];
206
+ parentRule.declarations.push({ ...rule, selectors: nestedSelectors });
207
+ continue;
208
+ }
209
+
210
+ for (const selector of rule.selectors) {
211
+ indexBySelector.set(selector.trim(), result.length);
212
+ }
213
+ result.push(rule);
176
214
  continue;
177
215
  }
178
- indexBySelector.set(childSelector, result.length);
179
216
  result.push(rule);
180
217
  }
181
218
  for (const rule of result) {
@@ -465,6 +465,19 @@ function stringifyRule (rule, context) {
465
465
 
466
466
  declarations = processDeclarations(declarations, context);
467
467
 
468
+ // Check if the rule is now effectively empty after processing declarations
469
+ const isEffectivelyEmptyAfterProcessing = (
470
+ declarations.length === 0 ||
471
+ declarations.every((declaration) => {
472
+ return (
473
+ declaration.type === 'comment' && !declaration.comment?.startsWith('!')
474
+ );
475
+ })
476
+ );
477
+ if (isEffectivelyEmptyAfterProcessing) {
478
+ return '';
479
+ }
480
+
468
481
  // We need to properly output nested rules vs normal declarations.
469
482
  // Wait, the processDeclarations will now correctly keep 'rule' types because we push them in processDeclarations.
470
483
  // If we have `.foo { .bar { color: red; } }`, the inner rule is a declaration of type 'rule' with selectors: ['.bar'].