@thejaredwilcurt/csslop 0.0.24 → 0.0.26

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.
@@ -7,8 +7,16 @@ import { hasInvalidQuotesCount } from '../value/quotes.js';
7
7
 
8
8
  import { absorbBackgroundLonghandsIntoShorthand } from './background.js';
9
9
  import { collapseBorderTrioWithPerEdgeColor } from './border.js';
10
- import { shorthandMap } from './config.js';
10
+ import {
11
+ getLonghandsOf,
12
+ shorthandMap
13
+ } from './config.js';
11
14
  import { hoistCssWideKeywordsIntoShorthands } from './css-wide-keywords.js';
15
+ import { foldLonghandOverridesIntoShorthands } from './fold.js';
16
+ import {
17
+ collectDeclaredProperties,
18
+ indexFirstDeclarationByProperty
19
+ } from './lookup.js';
12
20
  import {
13
21
  getMergeProps,
14
22
  tryMergeToShorthand
@@ -49,17 +57,95 @@ function usesModernSyntax (value) {
49
57
  }
50
58
 
51
59
  /**
52
- * Finds the index of the last declaration matching a predicate, which is the
53
- * declaration that wins the cascade within a rule.
60
+ * Creates the bookkeeping for the declarations that survive deduplication.
61
+ *
62
+ * A dropped declaration leaves an empty slot behind instead of being spliced
63
+ * out, so that every position already recorded stays valid. Two indexes then
64
+ * answer the questions the deduplication loop asks about the survivors: which
65
+ * declaration last set a given property, and which vendor-prefixed
66
+ * declarations are still standing.
67
+ *
68
+ * @return {object} The surviving-declaration bookkeeping.
69
+ */
70
+ function createSurvivingDeclarations () {
71
+ return {
72
+ slots: [],
73
+ positionsByProperty: new Map(),
74
+ vendorPrefixedPositions: []
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Records a declaration as surviving, at the end of the output so far.
54
80
  *
55
- * @param {Array} declarations The declarations to search.
56
- * @param {function(object): boolean} predicate Called with each declaration, returning whether it matches.
57
- * @return {number} The index of the matching declaration, or -1 when absent.
81
+ * @param {object} survivors The surviving-declaration bookkeeping.
82
+ * @param {object} declaration The declaration to keep.
58
83
  */
59
- function findLastIndex (declarations, predicate) {
60
- for (let index = declarations.length - 1; index >= 0; index--) {
61
- if (predicate(declarations[index])) {
62
- return index;
84
+ function keepDeclaration (survivors, declaration) {
85
+ const position = survivors.slots.length;
86
+ survivors.slots.push(declaration);
87
+ if (!declaration.property) {
88
+ return;
89
+ }
90
+ const positions = survivors.positionsByProperty.get(declaration.property);
91
+ if (positions) {
92
+ positions.push(position);
93
+ } else {
94
+ survivors.positionsByProperty.set(declaration.property, [position]);
95
+ }
96
+ if (declaration.property.startsWith('-')) {
97
+ survivors.vendorPrefixedPositions.push(position);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Drops a declaration that a later one made redundant. Only the last surviving
103
+ * declaration of a property is ever dropped, which is the one at the end of
104
+ * that property's position list.
105
+ *
106
+ * @param {object} survivors The surviving-declaration bookkeeping.
107
+ * @param {number} position The position of the declaration to drop.
108
+ */
109
+ function dropDeclaration (survivors, position) {
110
+ const { property } = survivors.slots[position];
111
+ survivors.slots[position] = null;
112
+ survivors.positionsByProperty.get(property).pop();
113
+ const vendorPrefixedEntry = survivors.vendorPrefixedPositions.lastIndexOf(position);
114
+ if (vendorPrefixedEntry !== -1) {
115
+ survivors.vendorPrefixedPositions.splice(vendorPrefixedEntry, 1);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Finds the surviving declaration that last set a property, which is the one
121
+ * that currently wins the cascade within the rule.
122
+ *
123
+ * @param {object} survivors The surviving-declaration bookkeeping.
124
+ * @param {string} property The property name to look for.
125
+ * @return {number} The position of the matching declaration, or -1 when absent.
126
+ */
127
+ function findLastPositionOfProperty (survivors, property) {
128
+ const positions = survivors.positionsByProperty.get(property);
129
+ if (!positions?.length) {
130
+ return -1;
131
+ }
132
+ return positions[positions.length - 1];
133
+ }
134
+
135
+ /**
136
+ * Finds the surviving vendor-prefixed declaration that last set the prefixed
137
+ * form of a property, such as `-webkit-transform` for `transform`. Only the
138
+ * prefixed declarations are visited, rather than every survivor.
139
+ *
140
+ * @param {object} survivors The surviving-declaration bookkeeping.
141
+ * @param {string} property The unprefixed property name.
142
+ * @return {number} The position of the matching declaration, or -1 when absent.
143
+ */
144
+ function findLastVendorPrefixedPosition (survivors, property) {
145
+ for (let entry = survivors.vendorPrefixedPositions.length - 1; entry >= 0; entry--) {
146
+ const position = survivors.vendorPrefixedPositions[entry];
147
+ if (survivors.slots[position].property.endsWith(property)) {
148
+ return position;
63
149
  }
64
150
  }
65
151
  return -1;
@@ -74,11 +160,11 @@ function findLastIndex (declarations, predicate) {
74
160
  * @return {Array} The surviving declarations, in source order.
75
161
  */
76
162
  function deduplicateDeclarations (declarations) {
77
- const result = [];
163
+ const survivors = createSurvivingDeclarations();
78
164
 
79
165
  for (const declaration of declarations) {
80
166
  if (declaration.type === 'rule' || declaration.type === 'media') {
81
- result.push(declaration);
167
+ keepDeclaration(survivors, declaration);
82
168
  continue;
83
169
  }
84
170
 
@@ -93,35 +179,23 @@ function deduplicateDeclarations (declarations) {
93
179
 
94
180
  const minifiedValue = minifyValue(declaration);
95
181
 
96
- let previousIndex = findLastIndex(result, (candidate) => {
97
- return candidate.property === propertyName;
98
- });
182
+ const previousPosition = findLastPositionOfProperty(survivors, propertyName);
99
183
 
100
184
  // An unprefixed property with the same value also replaces its prefixed form
101
- let prefixedIndex = -1;
185
+ let prefixedPosition = -1;
102
186
  if (!propertyName.startsWith('-')) {
103
- prefixedIndex = findLastIndex(result, (candidate) => {
104
- return (
105
- candidate.property &&
106
- candidate.property.endsWith(propertyName) &&
107
- candidate.property.startsWith('-')
108
- );
109
- });
187
+ prefixedPosition = findLastVendorPrefixedPosition(survivors, propertyName);
110
188
  }
111
189
 
112
- if (prefixedIndex !== -1) {
113
- const prefixedValue = minifyValue(result[prefixedIndex]);
190
+ if (prefixedPosition !== -1) {
191
+ const prefixedValue = minifyValue(survivors.slots[prefixedPosition]);
114
192
  if (minifiedValue === prefixedValue) {
115
- result.splice(prefixedIndex, 1);
116
- // Re-adjust previousIndex if we removed an item before it
117
- if (previousIndex > prefixedIndex) {
118
- previousIndex--;
119
- }
193
+ dropDeclaration(survivors, prefixedPosition);
120
194
  }
121
195
  }
122
196
 
123
- if (previousIndex !== -1) {
124
- const previousValue = minifyValue(result[previousIndex]);
197
+ if (previousPosition !== -1) {
198
+ const previousValue = minifyValue(survivors.slots[previousPosition]);
125
199
 
126
200
  if (previousValue.includes('!important') && !minifiedValue.includes('!important')) {
127
201
  continue;
@@ -129,18 +203,37 @@ function deduplicateDeclarations (declarations) {
129
203
 
130
204
  // Fallbacks for custom variables or older browser functions should be kept
131
205
  if (usesModernSyntax(minifiedValue) && !usesModernSyntax(previousValue)) {
132
- result.push(declaration);
206
+ keepDeclaration(survivors, declaration);
133
207
  continue;
134
208
  }
135
209
 
136
210
  // Otherwise override previous identical property
137
- result.splice(previousIndex, 1);
211
+ dropDeclaration(survivors, previousPosition);
138
212
  }
139
213
 
140
- result.push(declaration);
214
+ keepDeclaration(survivors, declaration);
141
215
  }
142
216
 
143
- return result;
217
+ return survivors.slots.filter((slot) => {
218
+ return slot !== null;
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Indexes the position of the first declaration of each property, which is the
224
+ * earliest position a property can occupy within the rule.
225
+ *
226
+ * @param {Array} declarations The declarations of a single rule, in source order.
227
+ * @return {Map} Map of property name to its first index.
228
+ */
229
+ function indexFirstDeclarationOfEachProperty (declarations) {
230
+ const firstIndexByProperty = new Map();
231
+ declarations.forEach((declaration, index) => {
232
+ if (declaration.property && !firstIndexByProperty.has(declaration.property)) {
233
+ firstIndexByProperty.set(declaration.property, index);
234
+ }
235
+ });
236
+ return firstIndexByProperty;
144
237
  }
145
238
 
146
239
  /**
@@ -152,6 +245,9 @@ function deduplicateDeclarations (declarations) {
152
245
  */
153
246
  function removeLonghandsOverriddenByShorthands (declarations) {
154
247
  const propertiesToRemove = new Set();
248
+ // A longhand precedes the shorthand exactly when its first occurrence does,
249
+ // so one index of first positions answers every shorthand's question.
250
+ const firstIndexByProperty = indexFirstDeclarationOfEachProperty(declarations);
155
251
 
156
252
  declarations.forEach((declaration, shorthandIndex) => {
157
253
  if (!declaration.property || !shorthandMap[declaration.property]) {
@@ -159,10 +255,8 @@ function removeLonghandsOverriddenByShorthands (declarations) {
159
255
  }
160
256
  const overridden = getOverriddenLonghands(declaration.property);
161
257
  for (const longhandProperty of overridden) {
162
- const longhandIndex = declarations.findIndex((candidate, index) => {
163
- return candidate.property === longhandProperty && index < shorthandIndex;
164
- });
165
- if (longhandIndex !== -1) {
258
+ const longhandIndex = firstIndexByProperty.get(longhandProperty);
259
+ if (longhandIndex !== undefined && longhandIndex < shorthandIndex) {
166
260
  propertiesToRemove.add(longhandProperty);
167
261
  }
168
262
  }
@@ -194,10 +288,9 @@ function getReplacedLonghands (shorthandName, mergeableProperties, relevantDecla
194
288
  if (!hasMixedImportant || !MIXED_IMPORTANT_SHORTHANDS.has(shorthandName)) {
195
289
  return mergeableProperties;
196
290
  }
291
+ const declarationByProperty = indexFirstDeclarationByProperty(relevantDeclarations);
197
292
  return mergeableProperties.filter((property) => {
198
- const declaration = relevantDeclarations.find((candidate) => {
199
- return candidate.property === property;
200
- });
293
+ const declaration = declarationByProperty.get(property);
201
294
  return declaration && !minifyValue(declaration).includes('!important');
202
295
  });
203
296
  }
@@ -217,15 +310,12 @@ function removeSubsumedShorthands (builtDeclarations) {
217
310
  return true;
218
311
  }
219
312
  const isSubsumedByOtherShorthand = builtDeclarations.some((other) => {
220
- if (other === declaration) {
221
- return false;
222
- }
223
- const otherLonghands = shorthandMap[other.property];
224
- if (!otherLonghands) {
313
+ if (other === declaration || !shorthandMap[other.property]) {
225
314
  return false;
226
315
  }
316
+ const otherLonghands = getLonghandsOf(other.property);
227
317
  return longhands.every((longhand) => {
228
- return otherLonghands.includes(longhand);
318
+ return otherLonghands.has(longhand);
229
319
  });
230
320
  });
231
321
  return !isSubsumedByOtherShorthand;
@@ -249,21 +339,23 @@ function mergeLonghandsIntoShorthands (declarations, context) {
249
339
  builtShorthand = false;
250
340
  const replacedProperties = new Set();
251
341
  const builtDeclarations = [];
342
+ // Every shorthand asks the same questions of the same declarations, so the
343
+ // set of declared properties is gathered once per pass rather than rescanned
344
+ // for each of the dozens of shorthand families.
345
+ const declaredProperties = collectDeclaredProperties(result);
252
346
 
253
347
  for (const [shorthand, longhands] of Object.entries(shorthandMap)) {
254
- const shorthandAlreadyExists = result.some((declaration) => {
255
- return declaration.property === shorthand;
256
- });
257
- if (shorthandAlreadyExists) {
348
+ if (declaredProperties.has(shorthand)) {
258
349
  continue;
259
350
  }
260
351
 
261
- const mergeableProperties = getMergeProps(shorthand, longhands, result);
352
+ const mergeableProperties = getMergeProps(shorthand, longhands, declaredProperties);
262
353
  if (!mergeableProperties) {
263
354
  continue;
264
355
  }
356
+ const mergeablePropertySet = new Set(mergeableProperties);
265
357
  const relevantDeclarations = result.filter((declaration) => {
266
- return mergeableProperties.includes(declaration.property);
358
+ return mergeablePropertySet.has(declaration.property);
267
359
  });
268
360
  const mergedValue = tryMergeToShorthand(mergeableProperties, relevantDeclarations, shorthand, context);
269
361
  if (!mergedValue) {
@@ -305,6 +397,7 @@ function processDeclarations (declarations, context) {
305
397
  result = removeLonghandsOverriddenByShorthands(result);
306
398
  result = absorbBackgroundLonghandsIntoShorthand(result);
307
399
  result = mergeLonghandsIntoShorthands(result, context);
400
+ result = foldLonghandOverridesIntoShorthands(result, context);
308
401
  result = hoistCssWideKeywordsIntoShorthands(result);
309
402
  result = collapseBorderTrioWithPerEdgeColor(result);
310
403
 
@@ -6,6 +6,7 @@ import { collapseShorthandParts } from '../value/shared.js';
6
6
  import { splitTopLevelComponents } from '../value/syntax.js';
7
7
 
8
8
  import { buildBackgroundShorthandValue } from './background.js';
9
+ import { UNIFORM_VALUE_SHORTHANDS } from './config.js';
9
10
 
10
11
  /**
11
12
  * @typedef {object} ShorthandComponents
@@ -275,6 +276,25 @@ function buildFlexValue ({ valueMap, importantSuffix }) {
275
276
  return [grow, shrink, basis].join(' ') + importantSuffix;
276
277
  }
277
278
 
279
+ /**
280
+ * Builds the value of a shorthand that applies one value to every longhand it
281
+ * sets, such as `marker` or the bidirectional gap decoration rules. Such a
282
+ * shorthand cannot express longhands that differ, so the group only collapses
283
+ * when every longhand already holds the same value.
284
+ *
285
+ * @param {ShorthandComponents} components The collected longhand values.
286
+ * @return {string|null} The shorthand value, or null when it cannot be built.
287
+ */
288
+ function buildUniformValue ({ cleanValues, importantSuffix }) {
289
+ const isSharedByAll = cleanValues.every((value) => {
290
+ return value === cleanValues[0];
291
+ });
292
+ if (!isSharedByAll) {
293
+ return null;
294
+ }
295
+ return cleanValues[0] + importantSuffix;
296
+ }
297
+
278
298
  /**
279
299
  * Determines whether a shorthand takes a single width, style, and color, as
280
300
  * `border` and `outline` do.
@@ -364,6 +384,9 @@ const NAMED_SHORTHAND_BUILDERS = {
364
384
  * @return {string|null} The shorthand value, or null when it cannot be built.
365
385
  */
366
386
  function buildShorthandValue (shorthandName, components) {
387
+ if (UNIFORM_VALUE_SHORTHANDS.has(shorthandName)) {
388
+ return buildUniformValue(components);
389
+ }
367
390
  const namedBuilder = NAMED_SHORTHAND_BUILDERS[shorthandName];
368
391
  if (namedBuilder) {
369
392
  return namedBuilder(components);
package/src/index.js CHANGED
@@ -41,7 +41,10 @@ import {
41
41
  removeRedundantLayerStatementSemicolon,
42
42
  stringifyRule
43
43
  } from './rules/stringify.js';
44
- import { minifyValue } from './value/minify.js';
44
+ import {
45
+ clearMinifiedValueCache,
46
+ minifyValue
47
+ } from './value/minify.js';
45
48
 
46
49
  /**
47
50
  * Splits a minified CSS selector list at top-level commas, respecting
@@ -175,6 +178,27 @@ function mergeAdjacentRulesWithIdenticalBodies (ruleStrings) {
175
178
  return result;
176
179
  }
177
180
 
181
+ /**
182
+ * Prepares the module-level state a single minification pass relies on: the
183
+ * active charset, and an empty value cache, since a value memoized under a
184
+ * different charset may no longer minify the same way.
185
+ *
186
+ * @param {string} charset The `@charset` value detected in the source.
187
+ */
188
+ function beginMinificationPass (charset) {
189
+ setActiveCharset(charset);
190
+ clearMinifiedValueCache();
191
+ }
192
+
193
+ /**
194
+ * Releases the module-level state a minification pass built up, so a cache
195
+ * filled by a large stylesheet is not retained until the next pass runs.
196
+ */
197
+ function endMinificationPass () {
198
+ clearActiveCharset();
199
+ clearMinifiedValueCache();
200
+ }
201
+
178
202
  /**
179
203
  * Parses, optimizes, and minifies a CSS string by applying rule merging, declaration deduplication, value compression, and dead-code elimination.
180
204
  *
@@ -192,7 +216,7 @@ export const minifyCSS = function (input) {
192
216
  const output = [];
193
217
 
194
218
  const detectedCharset = detectCharset(source);
195
- setActiveCharset(detectedCharset);
219
+ beginMinificationPass(detectedCharset);
196
220
 
197
221
  try {
198
222
  ast = parse(
@@ -200,7 +224,7 @@ export const minifyCSS = function (input) {
200
224
  { preserveFormatting: true, silent: true }
201
225
  );
202
226
  } catch {
203
- clearActiveCharset();
227
+ endMinificationPass();
204
228
  return source;
205
229
  }
206
230
 
@@ -248,10 +272,10 @@ export const minifyCSS = function (input) {
248
272
 
249
273
  const mergedOutput = removeRedundantLayerStatementSemicolon(mergeAdjacentRulesWithIdenticalBodies(output));
250
274
 
251
- clearActiveCharset();
275
+ endMinificationPass();
252
276
  return restoreEscapeSequences(mergedOutput.join(''));
253
277
  }
254
278
 
255
- clearActiveCharset();
279
+ endMinificationPass();
256
280
  return source;
257
281
  };
@@ -2,6 +2,8 @@
2
2
  * @file Handles `@position-try` rule analysis, usage tracking, and dead-rule elimination during CSS minification.
3
3
  */
4
4
 
5
+ import { registersCustomProperty } from './rules/property.js';
6
+
5
7
  /**
6
8
  * Scans top-level rules to register `@property` custom properties in the context and collect `@position-try` rule declarations and initial usage counts.
7
9
  *
@@ -14,7 +16,7 @@ function collectRuleMetadata (rules, context) {
14
16
  const positionTryUsage = new Map();
15
17
 
16
18
  for (const rule of rules) {
17
- if (rule.type === 'property' && rule.name) {
19
+ if (rule.type === 'property' && rule.name && registersCustomProperty(rule)) {
18
20
  context.registeredCustomProperties.add(rule.name);
19
21
  const syntaxDeclaration = (rule.declarations || []).find((declaration) => {
20
22
  return declaration.type !== 'whitespace' && declaration.property === 'syntax';