@thejaredwilcurt/csslop 0.0.23 → 0.0.25

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,15 @@ 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 {
16
+ collectDeclaredProperties,
17
+ indexFirstDeclarationByProperty
18
+ } from './lookup.js';
12
19
  import {
13
20
  getMergeProps,
14
21
  tryMergeToShorthand
@@ -49,17 +56,95 @@ function usesModernSyntax (value) {
49
56
  }
50
57
 
51
58
  /**
52
- * Finds the index of the last declaration matching a predicate, which is the
53
- * declaration that wins the cascade within a rule.
59
+ * Creates the bookkeeping for the declarations that survive deduplication.
60
+ *
61
+ * A dropped declaration leaves an empty slot behind instead of being spliced
62
+ * out, so that every position already recorded stays valid. Two indexes then
63
+ * answer the questions the deduplication loop asks about the survivors: which
64
+ * declaration last set a given property, and which vendor-prefixed
65
+ * declarations are still standing.
66
+ *
67
+ * @return {object} The surviving-declaration bookkeeping.
68
+ */
69
+ function createSurvivingDeclarations () {
70
+ return {
71
+ slots: [],
72
+ positionsByProperty: new Map(),
73
+ vendorPrefixedPositions: []
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Records a declaration as surviving, at the end of the output so far.
54
79
  *
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.
80
+ * @param {object} survivors The surviving-declaration bookkeeping.
81
+ * @param {object} declaration The declaration to keep.
58
82
  */
59
- function findLastIndex (declarations, predicate) {
60
- for (let index = declarations.length - 1; index >= 0; index--) {
61
- if (predicate(declarations[index])) {
62
- return index;
83
+ function keepDeclaration (survivors, declaration) {
84
+ const position = survivors.slots.length;
85
+ survivors.slots.push(declaration);
86
+ if (!declaration.property) {
87
+ return;
88
+ }
89
+ const positions = survivors.positionsByProperty.get(declaration.property);
90
+ if (positions) {
91
+ positions.push(position);
92
+ } else {
93
+ survivors.positionsByProperty.set(declaration.property, [position]);
94
+ }
95
+ if (declaration.property.startsWith('-')) {
96
+ survivors.vendorPrefixedPositions.push(position);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Drops a declaration that a later one made redundant. Only the last surviving
102
+ * declaration of a property is ever dropped, which is the one at the end of
103
+ * that property's position list.
104
+ *
105
+ * @param {object} survivors The surviving-declaration bookkeeping.
106
+ * @param {number} position The position of the declaration to drop.
107
+ */
108
+ function dropDeclaration (survivors, position) {
109
+ const { property } = survivors.slots[position];
110
+ survivors.slots[position] = null;
111
+ survivors.positionsByProperty.get(property).pop();
112
+ const vendorPrefixedEntry = survivors.vendorPrefixedPositions.lastIndexOf(position);
113
+ if (vendorPrefixedEntry !== -1) {
114
+ survivors.vendorPrefixedPositions.splice(vendorPrefixedEntry, 1);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Finds the surviving declaration that last set a property, which is the one
120
+ * that currently wins the cascade within the rule.
121
+ *
122
+ * @param {object} survivors The surviving-declaration bookkeeping.
123
+ * @param {string} property The property name to look for.
124
+ * @return {number} The position of the matching declaration, or -1 when absent.
125
+ */
126
+ function findLastPositionOfProperty (survivors, property) {
127
+ const positions = survivors.positionsByProperty.get(property);
128
+ if (!positions?.length) {
129
+ return -1;
130
+ }
131
+ return positions[positions.length - 1];
132
+ }
133
+
134
+ /**
135
+ * Finds the surviving vendor-prefixed declaration that last set the prefixed
136
+ * form of a property, such as `-webkit-transform` for `transform`. Only the
137
+ * prefixed declarations are visited, rather than every survivor.
138
+ *
139
+ * @param {object} survivors The surviving-declaration bookkeeping.
140
+ * @param {string} property The unprefixed property name.
141
+ * @return {number} The position of the matching declaration, or -1 when absent.
142
+ */
143
+ function findLastVendorPrefixedPosition (survivors, property) {
144
+ for (let entry = survivors.vendorPrefixedPositions.length - 1; entry >= 0; entry--) {
145
+ const position = survivors.vendorPrefixedPositions[entry];
146
+ if (survivors.slots[position].property.endsWith(property)) {
147
+ return position;
63
148
  }
64
149
  }
65
150
  return -1;
@@ -74,11 +159,11 @@ function findLastIndex (declarations, predicate) {
74
159
  * @return {Array} The surviving declarations, in source order.
75
160
  */
76
161
  function deduplicateDeclarations (declarations) {
77
- const result = [];
162
+ const survivors = createSurvivingDeclarations();
78
163
 
79
164
  for (const declaration of declarations) {
80
165
  if (declaration.type === 'rule' || declaration.type === 'media') {
81
- result.push(declaration);
166
+ keepDeclaration(survivors, declaration);
82
167
  continue;
83
168
  }
84
169
 
@@ -93,35 +178,23 @@ function deduplicateDeclarations (declarations) {
93
178
 
94
179
  const minifiedValue = minifyValue(declaration);
95
180
 
96
- let previousIndex = findLastIndex(result, (candidate) => {
97
- return candidate.property === propertyName;
98
- });
181
+ const previousPosition = findLastPositionOfProperty(survivors, propertyName);
99
182
 
100
183
  // An unprefixed property with the same value also replaces its prefixed form
101
- let prefixedIndex = -1;
184
+ let prefixedPosition = -1;
102
185
  if (!propertyName.startsWith('-')) {
103
- prefixedIndex = findLastIndex(result, (candidate) => {
104
- return (
105
- candidate.property &&
106
- candidate.property.endsWith(propertyName) &&
107
- candidate.property.startsWith('-')
108
- );
109
- });
186
+ prefixedPosition = findLastVendorPrefixedPosition(survivors, propertyName);
110
187
  }
111
188
 
112
- if (prefixedIndex !== -1) {
113
- const prefixedValue = minifyValue(result[prefixedIndex]);
189
+ if (prefixedPosition !== -1) {
190
+ const prefixedValue = minifyValue(survivors.slots[prefixedPosition]);
114
191
  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
- }
192
+ dropDeclaration(survivors, prefixedPosition);
120
193
  }
121
194
  }
122
195
 
123
- if (previousIndex !== -1) {
124
- const previousValue = minifyValue(result[previousIndex]);
196
+ if (previousPosition !== -1) {
197
+ const previousValue = minifyValue(survivors.slots[previousPosition]);
125
198
 
126
199
  if (previousValue.includes('!important') && !minifiedValue.includes('!important')) {
127
200
  continue;
@@ -129,18 +202,37 @@ function deduplicateDeclarations (declarations) {
129
202
 
130
203
  // Fallbacks for custom variables or older browser functions should be kept
131
204
  if (usesModernSyntax(minifiedValue) && !usesModernSyntax(previousValue)) {
132
- result.push(declaration);
205
+ keepDeclaration(survivors, declaration);
133
206
  continue;
134
207
  }
135
208
 
136
209
  // Otherwise override previous identical property
137
- result.splice(previousIndex, 1);
210
+ dropDeclaration(survivors, previousPosition);
138
211
  }
139
212
 
140
- result.push(declaration);
213
+ keepDeclaration(survivors, declaration);
141
214
  }
142
215
 
143
- return result;
216
+ return survivors.slots.filter((slot) => {
217
+ return slot !== null;
218
+ });
219
+ }
220
+
221
+ /**
222
+ * Indexes the position of the first declaration of each property, which is the
223
+ * earliest position a property can occupy within the rule.
224
+ *
225
+ * @param {Array} declarations The declarations of a single rule, in source order.
226
+ * @return {Map} Map of property name to its first index.
227
+ */
228
+ function indexFirstDeclarationOfEachProperty (declarations) {
229
+ const firstIndexByProperty = new Map();
230
+ declarations.forEach((declaration, index) => {
231
+ if (declaration.property && !firstIndexByProperty.has(declaration.property)) {
232
+ firstIndexByProperty.set(declaration.property, index);
233
+ }
234
+ });
235
+ return firstIndexByProperty;
144
236
  }
145
237
 
146
238
  /**
@@ -152,6 +244,9 @@ function deduplicateDeclarations (declarations) {
152
244
  */
153
245
  function removeLonghandsOverriddenByShorthands (declarations) {
154
246
  const propertiesToRemove = new Set();
247
+ // A longhand precedes the shorthand exactly when its first occurrence does,
248
+ // so one index of first positions answers every shorthand's question.
249
+ const firstIndexByProperty = indexFirstDeclarationOfEachProperty(declarations);
155
250
 
156
251
  declarations.forEach((declaration, shorthandIndex) => {
157
252
  if (!declaration.property || !shorthandMap[declaration.property]) {
@@ -159,10 +254,8 @@ function removeLonghandsOverriddenByShorthands (declarations) {
159
254
  }
160
255
  const overridden = getOverriddenLonghands(declaration.property);
161
256
  for (const longhandProperty of overridden) {
162
- const longhandIndex = declarations.findIndex((candidate, index) => {
163
- return candidate.property === longhandProperty && index < shorthandIndex;
164
- });
165
- if (longhandIndex !== -1) {
257
+ const longhandIndex = firstIndexByProperty.get(longhandProperty);
258
+ if (longhandIndex !== undefined && longhandIndex < shorthandIndex) {
166
259
  propertiesToRemove.add(longhandProperty);
167
260
  }
168
261
  }
@@ -194,10 +287,9 @@ function getReplacedLonghands (shorthandName, mergeableProperties, relevantDecla
194
287
  if (!hasMixedImportant || !MIXED_IMPORTANT_SHORTHANDS.has(shorthandName)) {
195
288
  return mergeableProperties;
196
289
  }
290
+ const declarationByProperty = indexFirstDeclarationByProperty(relevantDeclarations);
197
291
  return mergeableProperties.filter((property) => {
198
- const declaration = relevantDeclarations.find((candidate) => {
199
- return candidate.property === property;
200
- });
292
+ const declaration = declarationByProperty.get(property);
201
293
  return declaration && !minifyValue(declaration).includes('!important');
202
294
  });
203
295
  }
@@ -217,15 +309,12 @@ function removeSubsumedShorthands (builtDeclarations) {
217
309
  return true;
218
310
  }
219
311
  const isSubsumedByOtherShorthand = builtDeclarations.some((other) => {
220
- if (other === declaration) {
221
- return false;
222
- }
223
- const otherLonghands = shorthandMap[other.property];
224
- if (!otherLonghands) {
312
+ if (other === declaration || !shorthandMap[other.property]) {
225
313
  return false;
226
314
  }
315
+ const otherLonghands = getLonghandsOf(other.property);
227
316
  return longhands.every((longhand) => {
228
- return otherLonghands.includes(longhand);
317
+ return otherLonghands.has(longhand);
229
318
  });
230
319
  });
231
320
  return !isSubsumedByOtherShorthand;
@@ -249,21 +338,23 @@ function mergeLonghandsIntoShorthands (declarations, context) {
249
338
  builtShorthand = false;
250
339
  const replacedProperties = new Set();
251
340
  const builtDeclarations = [];
341
+ // Every shorthand asks the same questions of the same declarations, so the
342
+ // set of declared properties is gathered once per pass rather than rescanned
343
+ // for each of the dozens of shorthand families.
344
+ const declaredProperties = collectDeclaredProperties(result);
252
345
 
253
346
  for (const [shorthand, longhands] of Object.entries(shorthandMap)) {
254
- const shorthandAlreadyExists = result.some((declaration) => {
255
- return declaration.property === shorthand;
256
- });
257
- if (shorthandAlreadyExists) {
347
+ if (declaredProperties.has(shorthand)) {
258
348
  continue;
259
349
  }
260
350
 
261
- const mergeableProperties = getMergeProps(shorthand, longhands, result);
351
+ const mergeableProperties = getMergeProps(shorthand, longhands, declaredProperties);
262
352
  if (!mergeableProperties) {
263
353
  continue;
264
354
  }
355
+ const mergeablePropertySet = new Set(mergeableProperties);
265
356
  const relevantDeclarations = result.filter((declaration) => {
266
- return mergeableProperties.includes(declaration.property);
357
+ return mergeablePropertySet.has(declaration.property);
267
358
  });
268
359
  const mergedValue = tryMergeToShorthand(mergeableProperties, relevantDeclarations, shorthand, context);
269
360
  if (!mergedValue) {
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
  };