@thejaredwilcurt/csslop 0.0.49 → 0.0.51

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.
Files changed (57) hide show
  1. package/README.md +0 -1
  2. package/package.json +2 -2
  3. package/src/declarations/border.js +1 -1
  4. package/src/declarations/process.js +155 -1
  5. package/src/index.js +7 -7
  6. package/src/rules/optimize/at-rules.js +113 -0
  7. package/src/rules/optimize/declaration-writes.js +70 -0
  8. package/src/rules/optimize/empty-rules.js +51 -0
  9. package/src/rules/optimize/expand-nesting.js +236 -0
  10. package/src/rules/optimize/factor-parents.js +343 -0
  11. package/src/rules/optimize/index.js +71 -0
  12. package/src/rules/optimize/merge-declarations.js +199 -0
  13. package/src/rules/optimize/merge-nested-rules.js +117 -0
  14. package/src/rules/optimize/merge-selectors.js +252 -0
  15. package/src/rules/optimize/nest-rules.js +202 -0
  16. package/src/rules/optimize/override-index.js +264 -0
  17. package/src/rules/optimize/prune-overridden.js +267 -0
  18. package/src/rules/optimize/selector-normalization.js +60 -0
  19. package/src/rules/optimize/specificity.js +280 -0
  20. package/src/{position-try.js → rules/position-try.js} +1 -1
  21. package/src/rules/stringify.js +13 -0
  22. package/src/utilities.js +14 -0
  23. package/src/value/border.js +85 -0
  24. package/src/value/{colors.js → color/conversion.js} +3 -2
  25. package/src/value/color/functions.js +463 -0
  26. package/src/value/{light-dark.js → color/light-dark.js} +1 -1
  27. package/src/value/{color-mix.js → color/mix.js} +3 -3
  28. package/src/value/{relative-colors.js → color/relative.js} +3 -3
  29. package/src/value/color/shorten.js +93 -0
  30. package/src/value/gradients.js +1 -1
  31. package/src/value/hyphenate-limit-chars.js +72 -0
  32. package/src/value/initial-keywords.js +128 -0
  33. package/src/value/lengths.js +55 -0
  34. package/src/value/math/evaluate.js +36 -0
  35. package/src/value/math/fold.js +315 -0
  36. package/src/value/math/format.js +230 -0
  37. package/src/value/math/functions.js +302 -0
  38. package/src/value/math/index.js +219 -0
  39. package/src/value/math/parser.js +301 -0
  40. package/src/value/math/property-ranges.js +174 -0
  41. package/src/value/math/rewrite.js +222 -0
  42. package/src/value/math/tokenizer.js +160 -0
  43. package/src/value/math/unit-totals.js +194 -0
  44. package/src/value/minify.js +127 -1669
  45. package/src/value/numbers.js +28 -0
  46. package/src/value/positions.js +152 -0
  47. package/src/value/property-optimizations.js +485 -0
  48. package/src/value/segments.js +74 -0
  49. package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
  50. package/src/value/{svg-markup.js → svg/markup.js} +1 -1
  51. package/src/value/time.js +75 -0
  52. package/src/value/urls.js +242 -0
  53. package/src/value/whitespace.js +88 -0
  54. package/src/rules/optimize.js +0 -2314
  55. package/src/value/math-expression.js +0 -1156
  56. package/src/value/math.js +0 -886
  57. /package/src/value/{named-colors.js → color/named.js} +0 -0
package/README.md CHANGED
@@ -321,6 +321,5 @@ Two different cases:
321
321
  * Ensure arrow functions always take up at least 3 lines, with explicit returns when needed.
322
322
  * Always comment regex if used.
323
323
  1. After all other code edits, run `npm run lint` and ensure the linter passes.
324
- 1. DO NOT, under any circumstance, run `npm run real` (it will kill actual humans)!
325
324
  1. When all done, run the `beep` command to alert me you finished.
326
325
  ```
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.49",
5
+ "version": "0.0.51",
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"
@@ -3,8 +3,8 @@
3
3
  */
4
4
 
5
5
  import { splitTopLevelComponents } from '../parser/source-search.js';
6
+ import { namedColors } from '../value/color/named.js';
6
7
  import { minifyValue } from '../value/minify.js';
7
- import { namedColors } from '../value/named-colors.js';
8
8
  import {
9
9
  collapseShorthandParts,
10
10
  hasSubstitutedParts
@@ -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);
package/src/index.js CHANGED
@@ -14,19 +14,19 @@ import {
14
14
  import { recordStylesheetResetProperties } from './declarations/reset-hazards.js';
15
15
  import { parse } from './parser/index.js';
16
16
  import { splitTopLevelCommaList } from './parser/source-search.js';
17
- import {
18
- analyzePositionTryRules,
19
- cleanPositionTryRules,
20
- collectRuleMetadata,
21
- filterUnusedPositionTry
22
- } from './position-try.js';
23
17
  import { preprocessDeclarationBlocks } from './preprocess.js';
24
18
  import { combineLanguageSelectors } from './rules/lang.js';
25
19
  import { normalizeRuleSelectors } from './rules/normalize.js';
26
20
  import {
27
21
  clearSelectorCaches,
28
22
  optimizeRuleList
29
- } from './rules/optimize.js';
23
+ } from './rules/optimize/index.js';
24
+ import {
25
+ analyzePositionTryRules,
26
+ cleanPositionTryRules,
27
+ collectRuleMetadata,
28
+ filterUnusedPositionTry
29
+ } from './rules/position-try.js';
30
30
  import { combineLegacyAnySelectors } from './rules/selectors.js';
31
31
  import {
32
32
  removeRedundantLayerStatementSemicolon,
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @file Consolidates grouping at-rules by merging matching `@media` and `@layer` blocks, and drops the `@keyframes` that a later definition replaces.
3
+ */
4
+
5
+ import {
6
+ normalizeLayerNames,
7
+ normalizeMedia
8
+ } from '../normalize.js';
9
+
10
+ /**
11
+ * Merges adjacent `@media` rules that share an identical normalized query string and deduplicates their child selector rules.
12
+ *
13
+ * @param {Array} rules The AST rule nodes to process.
14
+ * @param {function(Array): Array} mergeSelectorRules Callback to merge selector rules within each media block.
15
+ * @return {Array} A new array of rules with consecutive identical `@media` blocks combined.
16
+ */
17
+ function mergeMediaRules (rules, mergeSelectorRules) {
18
+ const mediaMap = new Map();
19
+ const result = [];
20
+ for (const rule of rules) {
21
+ if (rule.type === 'media') {
22
+ const normalizedQuery = normalizeMedia(rule.media);
23
+ if (mediaMap.has(normalizedQuery)) {
24
+ mediaMap.get(normalizedQuery).rules.push(...(rule.rules || []));
25
+ } else {
26
+ mediaMap.set(normalizedQuery, rule);
27
+ result.push(rule);
28
+ }
29
+ } else {
30
+ // Clearing a map replaces its storage, so an already empty one is left alone
31
+ if (rule.type !== 'whitespace' && mediaMap.size) {
32
+ mediaMap.clear();
33
+ }
34
+ result.push(rule);
35
+ }
36
+ }
37
+ for (const rule of result) {
38
+ if (rule.type === 'media' && rule.rules && rule.rules.length) {
39
+ rule.rules = mergeSelectorRules(rule.rules);
40
+ }
41
+ }
42
+ return result;
43
+ }
44
+
45
+ /**
46
+ * Removes duplicate `@keyframes` definitions, keeping only the last occurrence of each named animation.
47
+ *
48
+ * @param {Array} rules The AST rule nodes to deduplicate.
49
+ * @return {Array} A new array of rules with earlier duplicate `@keyframes` removed.
50
+ */
51
+ function deduplicateKeyframes (rules) {
52
+ const lastIndexByName = new Map();
53
+ for (let i = 0; i < rules.length; i++) {
54
+ if (rules[i].type === 'keyframes' && rules[i].name) {
55
+ lastIndexByName.set(rules[i].name, i);
56
+ }
57
+ }
58
+ return rules.filter((rule, index) => {
59
+ if (rule.type === 'keyframes' && rule.name) {
60
+ return lastIndexByName.get(rule.name) === index;
61
+ }
62
+ return true;
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Merges `@layer` blocks with the same name by combining their child rules, deduplicates layer statements, and merges selector rules within each block.
68
+ *
69
+ * @param {Array} rules The AST rule nodes to process.
70
+ * @param {function(Array): Array} mergeSelectorRules Callback to merge selector rules within each layer block.
71
+ * @return {Array} A new array of rules with same-name `@layer` blocks combined.
72
+ */
73
+ function mergeLayerRules (rules, mergeSelectorRules) {
74
+ const layerBlockMap = new Map();
75
+ const layerStatementSeen = new Set();
76
+ const result = [];
77
+ for (const rule of rules) {
78
+ if (rule.type === 'layer') {
79
+ const layerName = normalizeLayerNames(rule.layer);
80
+ if (rule.rules && rule.rules.length > 0) {
81
+ if (layerName && layerBlockMap.has(layerName)) {
82
+ layerBlockMap.get(layerName).rules.push(...rule.rules);
83
+ } else {
84
+ if (layerName) {
85
+ layerBlockMap.set(layerName, rule);
86
+ }
87
+ result.push(rule);
88
+ }
89
+ } else {
90
+ if (!layerName || !layerStatementSeen.has(layerName)) {
91
+ if (layerName) {
92
+ layerStatementSeen.add(layerName);
93
+ }
94
+ result.push(rule);
95
+ }
96
+ }
97
+ } else {
98
+ result.push(rule);
99
+ }
100
+ }
101
+ for (const rule of result) {
102
+ if (rule.type === 'layer' && rule.rules && rule.rules.length) {
103
+ rule.rules = mergeSelectorRules(rule.rules);
104
+ }
105
+ }
106
+ return result;
107
+ }
108
+
109
+ export {
110
+ deduplicateKeyframes,
111
+ mergeLayerRules,
112
+ mergeMediaRules
113
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @file Describes what a declaration writes to: whether it declares a property at all, which leaf properties it can override, and whether it is flagged important.
3
+ */
4
+
5
+ import {
6
+ expandToLeafProperties,
7
+ getOverridesOf
8
+ } from '../../declarations/config.js';
9
+
10
+ /**
11
+ * The leaf properties each declared property can write to, computed on first
12
+ * use. The shorthand tables never change, so a property always expands the same
13
+ * way.
14
+ *
15
+ * @type {Map<string, Set<string>>}
16
+ */
17
+ const overridablePropertiesByProperty = new Map();
18
+
19
+ /**
20
+ * Expands a declared property into every leaf longhand it can write to,
21
+ * including the extras a shorthand resets beyond its own longhands. Two
22
+ * declarations can only override one another when these sets intersect, which
23
+ * is what makes `margin` conflict with `margin-top` but not with `color`.
24
+ *
25
+ * @param {string} property The declared property name.
26
+ * @return {Set} The leaf property names the declaration writes to.
27
+ */
28
+ function expandToOverridableProperties (property) {
29
+ const cachedProperties = overridablePropertiesByProperty.get(property);
30
+ if (cachedProperties) {
31
+ return cachedProperties;
32
+ }
33
+ const overridableProperties = new Set(expandToLeafProperties(property));
34
+ for (const resetProperty of getOverridesOf(property)) {
35
+ for (const leafProperty of expandToLeafProperties(resetProperty)) {
36
+ overridableProperties.add(leafProperty);
37
+ }
38
+ }
39
+ overridablePropertiesByProperty.set(property, overridableProperties);
40
+ return overridableProperties;
41
+ }
42
+
43
+ /**
44
+ * Reports whether an entry of a rule's body is a declaration whose standing in
45
+ * the cascade can be read off the property it writes. Anything else a body may
46
+ * hold, such as a nested rule, is never shadowed by a declaration beside it.
47
+ *
48
+ * @param {object} entry An entry of a rule's declaration list.
49
+ * @return {boolean} True when the entry is a property declaration.
50
+ */
51
+ function isPropertyDeclaration (entry) {
52
+ return entry.type === 'declaration' && Boolean(entry.property);
53
+ }
54
+
55
+ /**
56
+ * Determines whether a declaration carries a trailing `!important` flag.
57
+ *
58
+ * @param {object} declaration The AST declaration node.
59
+ * @return {boolean} True when the declaration is important.
60
+ */
61
+ function isImportantDeclaration (declaration) {
62
+ // A trailing !important suffix on the raw declaration value
63
+ return /!\s*important\s*$/i.test(declaration.rawValue || declaration.value || '');
64
+ }
65
+
66
+ export {
67
+ expandToOverridableProperties,
68
+ isImportantDeclaration,
69
+ isPropertyDeclaration
70
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @file Detects and removes rules that produce no CSS output once minified.
3
+ */
4
+
5
+ /**
6
+ * Determines whether a rule is effectively empty, containing no meaningful
7
+ * CSS output after minification. A rule is effectively empty when it has
8
+ * no declarations, or all of its entries are whitespace, non-important
9
+ * comments, or recursively empty nested rules.
10
+ *
11
+ * @param {object} rule The AST rule node to evaluate.
12
+ * @return {boolean} True if the rule produces no CSS output.
13
+ */
14
+ function isRuleEffectivelyEmpty (rule) {
15
+ if (rule.type !== 'rule') {
16
+ return false;
17
+ }
18
+ const nonWhitespaceEntries = (rule.declarations || []).filter((declaration) => {
19
+ return declaration.type !== 'whitespace';
20
+ });
21
+ if (nonWhitespaceEntries.length === 0) {
22
+ return true;
23
+ }
24
+ return nonWhitespaceEntries.every((entry) => {
25
+ if (entry.type === 'comment') {
26
+ return !entry.comment?.startsWith('!');
27
+ }
28
+ if (entry.type === 'rule') {
29
+ return isRuleEffectivelyEmpty(entry);
30
+ }
31
+ return false;
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Filters out effectively empty rules from the rules array, preventing
37
+ * empty rules from being nested into parent rules during later
38
+ * optimization passes and producing incorrect non-empty output.
39
+ *
40
+ * @param {Array} rules The AST rule nodes to filter.
41
+ * @return {Array} A new array with effectively empty rules removed.
42
+ */
43
+ function removeEmptyRules (rules) {
44
+ return rules.filter((rule) => {
45
+ return !isRuleEffectivelyEmpty(rule);
46
+ });
47
+ }
48
+
49
+ export {
50
+ removeEmptyRules
51
+ };