@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.
- package/README.md +4 -0
- package/package.json +6 -4
- package/src/charset.js +72 -11
- package/src/declarations/background.js +26 -1
- package/src/declarations/config.js +60 -0
- package/src/declarations/css-wide-keywords.js +47 -25
- package/src/declarations/lookup.js +44 -0
- package/src/declarations/merge.js +27 -30
- package/src/declarations/order.js +32 -6
- package/src/declarations/process.js +146 -55
- package/src/index.js +29 -5
- package/src/rules/optimize.js +198 -78
- package/src/rules/stringify.js +11 -4
- package/src/value/gradients.js +87 -46
- package/src/value/math.js +17 -2
- package/src/value/minify.js +110 -14
|
@@ -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 {
|
|
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
|
-
*
|
|
53
|
-
*
|
|
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
|
|
56
|
-
* @param
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
162
|
+
const survivors = createSurvivingDeclarations();
|
|
78
163
|
|
|
79
164
|
for (const declaration of declarations) {
|
|
80
165
|
if (declaration.type === 'rule' || declaration.type === 'media') {
|
|
81
|
-
|
|
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
|
-
|
|
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
|
|
184
|
+
let prefixedPosition = -1;
|
|
102
185
|
if (!propertyName.startsWith('-')) {
|
|
103
|
-
|
|
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 (
|
|
113
|
-
const prefixedValue = minifyValue(
|
|
189
|
+
if (prefixedPosition !== -1) {
|
|
190
|
+
const prefixedValue = minifyValue(survivors.slots[prefixedPosition]);
|
|
114
191
|
if (minifiedValue === prefixedValue) {
|
|
115
|
-
|
|
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 (
|
|
124
|
-
const previousValue = minifyValue(
|
|
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
|
-
|
|
205
|
+
keepDeclaration(survivors, declaration);
|
|
133
206
|
continue;
|
|
134
207
|
}
|
|
135
208
|
|
|
136
209
|
// Otherwise override previous identical property
|
|
137
|
-
|
|
210
|
+
dropDeclaration(survivors, previousPosition);
|
|
138
211
|
}
|
|
139
212
|
|
|
140
|
-
|
|
213
|
+
keepDeclaration(survivors, declaration);
|
|
141
214
|
}
|
|
142
215
|
|
|
143
|
-
return
|
|
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 =
|
|
163
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
+
endMinificationPass();
|
|
252
276
|
return restoreEscapeSequences(mergedOutput.join(''));
|
|
253
277
|
}
|
|
254
278
|
|
|
255
|
-
|
|
279
|
+
endMinificationPass();
|
|
256
280
|
return source;
|
|
257
281
|
};
|