@thejaredwilcurt/csslop 0.0.1
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 +213 -0
- package/index.js +5 -0
- package/package.json +48 -0
- package/src/context.js +17 -0
- package/src/declarations/config.js +56 -0
- package/src/declarations/process.js +662 -0
- package/src/index.js +90 -0
- package/src/position-try.js +199 -0
- package/src/preprocess.js +27 -0
- package/src/rules/normalize.js +108 -0
- package/src/rules/optimize.js +375 -0
- package/src/rules/stringify.js +556 -0
- package/src/utilities.js +37 -0
- package/src/value/colors.js +780 -0
- package/src/value/gradients.js +121 -0
- package/src/value/math.js +281 -0
- package/src/value/minify.js +716 -0
- package/src/value/named-colors.js +159 -0
- package/src/value/shared.js +146 -0
- package/src/value/transforms.js +222 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Deduplicates, merges, and optimizes CSS declarations by collapsing longhand properties into shorthands and removing overridden values.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { minifyValue } from '../value/minify.js';
|
|
6
|
+
import { collapseShorthandParts } from '../value/shared.js';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
shorthandMap,
|
|
10
|
+
shorthandOverrideMap
|
|
11
|
+
} from './config.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reorders declarations so that shorthands appear before any related longhands they would override, preventing cascade issues in the minified output.
|
|
15
|
+
*
|
|
16
|
+
* @param {Array} declarations The array of CSS declaration objects to reorder.
|
|
17
|
+
* @return {Array} A new array with declarations in the corrected order.
|
|
18
|
+
*/
|
|
19
|
+
function orderDeclarations (declarations) {
|
|
20
|
+
const ordered = [...declarations];
|
|
21
|
+
const moveBefore = (prop, beforeProp) => {
|
|
22
|
+
const fromIndex = ordered.findIndex((declaration) => {
|
|
23
|
+
return declaration?.property === prop;
|
|
24
|
+
});
|
|
25
|
+
const toIndex = ordered.findIndex((declaration) => {
|
|
26
|
+
return declaration?.property === beforeProp;
|
|
27
|
+
});
|
|
28
|
+
if (fromIndex === -1 || toIndex === -1 || fromIndex < toIndex) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const [item] = ordered.splice(fromIndex, 1);
|
|
32
|
+
ordered.splice(toIndex, 0, item);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
moveBefore('border', 'border-image');
|
|
36
|
+
moveBefore('font', 'font-feature-settings');
|
|
37
|
+
moveBefore('font', 'font-variant-ligatures');
|
|
38
|
+
moveBefore('font', 'font-kerning');
|
|
39
|
+
moveBefore('font', 'font-variation-settings');
|
|
40
|
+
moveBefore('mask', 'mask-border');
|
|
41
|
+
moveBefore('margin', 'margin-top');
|
|
42
|
+
moveBefore('margin', 'margin-right');
|
|
43
|
+
moveBefore('margin', 'margin-bottom');
|
|
44
|
+
moveBefore('margin', 'margin-left');
|
|
45
|
+
|
|
46
|
+
return ordered;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Determines which longhand properties are present and eligible for merging into a given shorthand. Returns null when the required longhands for the shorthand are not all available.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} shorthand The CSS shorthand property name.
|
|
53
|
+
* @param {Array} longhands The expected longhand property names for this shorthand.
|
|
54
|
+
* @param {Array} declarations The current array of CSS declaration objects.
|
|
55
|
+
* @return {Array|null} The list of longhand names to merge, or null if merging is not possible.
|
|
56
|
+
*/
|
|
57
|
+
function getMergeProps (shorthand, longhands, declarations) {
|
|
58
|
+
const presentLonghands = longhands.filter((longhand) => {
|
|
59
|
+
return declarations.some((declaration) => {
|
|
60
|
+
return declaration.property === longhand;
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
if (presentLonghands.length === 0) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (shorthand === 'font') {
|
|
67
|
+
const hasRequiredFontProps = presentLonghands.includes('font-size') && presentLonghands.includes('font-family');
|
|
68
|
+
if (hasRequiredFontProps) {
|
|
69
|
+
return presentLonghands;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (shorthand === 'background') {
|
|
74
|
+
const hasBackgroundProp = presentLonghands.includes('background-color') || presentLonghands.includes('background-image');
|
|
75
|
+
if (hasBackgroundProp) {
|
|
76
|
+
return presentLonghands;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (shorthand === 'mask') {
|
|
81
|
+
if (presentLonghands.includes('mask-image')) {
|
|
82
|
+
return presentLonghands;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (shorthand === 'border-image') {
|
|
87
|
+
if (presentLonghands.includes('border-image-source')) {
|
|
88
|
+
return presentLonghands;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (shorthand === 'border') {
|
|
93
|
+
const hasAllBorderParts = (
|
|
94
|
+
presentLonghands.includes('border-width') &&
|
|
95
|
+
presentLonghands.includes('border-style') &&
|
|
96
|
+
presentLonghands.includes('border-color')
|
|
97
|
+
);
|
|
98
|
+
if (hasAllBorderParts) {
|
|
99
|
+
return ['border-width', 'border-style', 'border-color'];
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (shorthand === 'flex') {
|
|
104
|
+
const hasAllFlexParts = (
|
|
105
|
+
presentLonghands.includes('flex-grow') &&
|
|
106
|
+
presentLonghands.includes('flex-shrink') &&
|
|
107
|
+
presentLonghands.includes('flex-basis')
|
|
108
|
+
);
|
|
109
|
+
if (hasAllFlexParts) {
|
|
110
|
+
return ['flex-grow', 'flex-shrink', 'flex-basis'];
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
if (presentLonghands.length === longhands.length) {
|
|
115
|
+
return longhands;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get all longhands that a shorthand would override.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} shorthandProp The CSS shorthand property name.
|
|
124
|
+
* @return {Array} A deduplicated array of all longhand property names that the shorthand overrides, including nested longhands.
|
|
125
|
+
*/
|
|
126
|
+
function getOverriddenLonghands (shorthandProp) {
|
|
127
|
+
const direct = shorthandMap[shorthandProp] || [];
|
|
128
|
+
const overrides = shorthandOverrideMap[shorthandProp] || [];
|
|
129
|
+
const all = [...direct, ...overrides];
|
|
130
|
+
for (const prop of direct) {
|
|
131
|
+
const nested = shorthandMap[prop] || [];
|
|
132
|
+
all.push(...nested);
|
|
133
|
+
}
|
|
134
|
+
return [...new Set(all)];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if a value contains var() - don't merge if it does (safest approach).
|
|
139
|
+
*
|
|
140
|
+
* @param {string} value The minified CSS value string to check.
|
|
141
|
+
* @return {boolean} True if the value contains a var() with a fallback comma.
|
|
142
|
+
*/
|
|
143
|
+
function hasVarFallback (value) {
|
|
144
|
+
// Match var() containing a comma (indicating a fallback value)
|
|
145
|
+
return /var\([^)]*,/.test(value);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Determines whether a value containing var() references can safely be merged into a shorthand. Values with fallback commas or unregistered custom properties are not mergeable.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} value The minified CSS value string to check.
|
|
152
|
+
* @param {object} context The minification context with registered custom property data.
|
|
153
|
+
* @return {boolean} True if the value is safe to merge into a shorthand.
|
|
154
|
+
*/
|
|
155
|
+
function canMergeVarValue (value, context) {
|
|
156
|
+
// Check if the value contains any var() reference
|
|
157
|
+
const containsVar = /var\(/.test(value);
|
|
158
|
+
if (!containsVar || hasVarFallback(value)) {
|
|
159
|
+
return !hasVarFallback(value);
|
|
160
|
+
}
|
|
161
|
+
// Extract all var() references with their custom property names
|
|
162
|
+
const matches = [...value.matchAll(/var\((--[A-Za-z0-9_-]+)\)/g)];
|
|
163
|
+
if (!matches.length) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return matches.every(([, propertyName]) => {
|
|
167
|
+
return context.registeredCustomProperties.has(propertyName);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Try to merge longhand properties into a shorthand.
|
|
173
|
+
*
|
|
174
|
+
* @param {Array} properties The longhand property names to merge.
|
|
175
|
+
* @param {Array} declarations The CSS declaration objects to draw values from.
|
|
176
|
+
* @param {string} shorthandName The target shorthand property name.
|
|
177
|
+
* @param {object} context The minification context with registered custom property data.
|
|
178
|
+
* @return {string|null} The merged shorthand value string, or null if merging is not possible.
|
|
179
|
+
*/
|
|
180
|
+
function tryMergeToShorthand (properties, declarations, shorthandName = '', context) {
|
|
181
|
+
if (properties.length < 2) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const values = properties.map((property) => {
|
|
186
|
+
const declaration = declarations.find((candidate) => {
|
|
187
|
+
return candidate.property === property;
|
|
188
|
+
});
|
|
189
|
+
if (declaration) {
|
|
190
|
+
return minifyValue(declaration);
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// If any value is null, can't merge
|
|
196
|
+
const hasNullValue = values.some((value) => {
|
|
197
|
+
return value === null;
|
|
198
|
+
});
|
|
199
|
+
if (hasNullValue) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Don't merge if any value has var() with fallback or unknown custom properties
|
|
204
|
+
const hasUnmergeableVar = values.some((value) => {
|
|
205
|
+
return !canMergeVarValue(value, context);
|
|
206
|
+
});
|
|
207
|
+
if (hasUnmergeableVar) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check if all values have the same !important status
|
|
212
|
+
const importantFlags = values.map((value) => {
|
|
213
|
+
return value.includes('!important');
|
|
214
|
+
});
|
|
215
|
+
const allImportant = importantFlags.every((flag) => {
|
|
216
|
+
return flag;
|
|
217
|
+
});
|
|
218
|
+
const noneImportant = importantFlags.every((flag) => {
|
|
219
|
+
return !flag;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Allow mixed important flags for margin/padding/inset - merge without !important on shorthand
|
|
223
|
+
// For other properties, mixed important flags are not allowed
|
|
224
|
+
const allowsMixedImportant = (
|
|
225
|
+
shorthandName === 'margin' ||
|
|
226
|
+
shorthandName === 'padding' ||
|
|
227
|
+
shorthandName === 'inset' ||
|
|
228
|
+
shorthandName === 'position-try'
|
|
229
|
+
);
|
|
230
|
+
if (!allImportant && !noneImportant && !allowsMixedImportant) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const cleanValues = values.map((value) => {
|
|
235
|
+
return value
|
|
236
|
+
.replace('!important', '')
|
|
237
|
+
.trim();
|
|
238
|
+
});
|
|
239
|
+
const valueMap = new Map(properties.map((property, index) => {
|
|
240
|
+
return [property, cleanValues[index]];
|
|
241
|
+
}));
|
|
242
|
+
|
|
243
|
+
// For margin/padding with mixed important, don't use !important on the shorthand
|
|
244
|
+
// Only use !important if ALL values have it
|
|
245
|
+
const useImportant = allImportant;
|
|
246
|
+
const importantSuffix = useImportant ? '!important' : '';
|
|
247
|
+
|
|
248
|
+
if (shorthandName === 'position-try') {
|
|
249
|
+
const order = valueMap.get('position-try-order');
|
|
250
|
+
const fallbacks = valueMap.get('position-try-fallbacks');
|
|
251
|
+
if (order === 'normal' && fallbacks) {
|
|
252
|
+
return fallbacks + importantSuffix;
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (shorthandName === 'transition') {
|
|
258
|
+
const transitionProperty = valueMap.get('transition-property');
|
|
259
|
+
const duration = valueMap.get('transition-duration');
|
|
260
|
+
const timing = valueMap.get('transition-timing-function');
|
|
261
|
+
const delay = valueMap.get('transition-delay');
|
|
262
|
+
if (!transitionProperty || !duration) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const result = [transitionProperty, duration];
|
|
266
|
+
if (timing && timing !== 'ease') {
|
|
267
|
+
result.push(timing);
|
|
268
|
+
}
|
|
269
|
+
if (delay && delay !== '0' && delay !== '0s') {
|
|
270
|
+
result.push(delay);
|
|
271
|
+
}
|
|
272
|
+
return result.join(' ') + importantSuffix;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (shorthandName === 'animation') {
|
|
276
|
+
const animationName = valueMap.get('animation-name');
|
|
277
|
+
const duration = valueMap.get('animation-duration');
|
|
278
|
+
if (!animationName || !duration) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const result = [animationName, duration];
|
|
282
|
+
const timing = valueMap.get('animation-timing-function');
|
|
283
|
+
const delay = valueMap.get('animation-delay');
|
|
284
|
+
const iteration = valueMap.get('animation-iteration-count');
|
|
285
|
+
const direction = valueMap.get('animation-direction');
|
|
286
|
+
const fillMode = valueMap.get('animation-fill-mode');
|
|
287
|
+
const playState = valueMap.get('animation-play-state');
|
|
288
|
+
if (timing && timing !== 'ease') {
|
|
289
|
+
result.push(timing);
|
|
290
|
+
}
|
|
291
|
+
if (delay && delay !== '0' && delay !== '0s') {
|
|
292
|
+
result.push(delay);
|
|
293
|
+
}
|
|
294
|
+
if (iteration && iteration !== '1') {
|
|
295
|
+
result.push(iteration);
|
|
296
|
+
}
|
|
297
|
+
if (direction && direction !== 'normal') {
|
|
298
|
+
result.push(direction);
|
|
299
|
+
}
|
|
300
|
+
if (fillMode && fillMode !== 'none') {
|
|
301
|
+
result.push(fillMode);
|
|
302
|
+
}
|
|
303
|
+
if (playState && playState !== 'running') {
|
|
304
|
+
result.push(playState);
|
|
305
|
+
}
|
|
306
|
+
return result.join(' ') + importantSuffix;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (shorthandName === 'background') {
|
|
310
|
+
const color = valueMap.get('background-color');
|
|
311
|
+
const image = valueMap.get('background-image');
|
|
312
|
+
const repeat = valueMap.get('background-repeat');
|
|
313
|
+
const position = valueMap.get('background-position');
|
|
314
|
+
const attachment = valueMap.get('background-attachment');
|
|
315
|
+
const result = [];
|
|
316
|
+
if (color && color !== 'transparent') {
|
|
317
|
+
result.push(color);
|
|
318
|
+
}
|
|
319
|
+
if (image && image !== 'none') {
|
|
320
|
+
result.push(image);
|
|
321
|
+
}
|
|
322
|
+
if (position && position !== '0 0' && position !== '0% 0%') {
|
|
323
|
+
result.push(position);
|
|
324
|
+
}
|
|
325
|
+
if (repeat && repeat !== 'repeat') {
|
|
326
|
+
result.push(repeat);
|
|
327
|
+
}
|
|
328
|
+
if (attachment && attachment !== 'scroll') {
|
|
329
|
+
result.push(attachment);
|
|
330
|
+
}
|
|
331
|
+
if (!result.length) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
return result.join(' ') + importantSuffix;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (shorthandName === 'mask') {
|
|
338
|
+
const image = valueMap.get('mask-image');
|
|
339
|
+
const repeat = valueMap.get('mask-repeat');
|
|
340
|
+
const size = valueMap.get('mask-size');
|
|
341
|
+
if (!image) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
let result = image;
|
|
345
|
+
if (repeat) {
|
|
346
|
+
result += ' ' + repeat;
|
|
347
|
+
}
|
|
348
|
+
if (size) {
|
|
349
|
+
result += '/' + size;
|
|
350
|
+
}
|
|
351
|
+
return result + importantSuffix;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (shorthandName === 'border-image') {
|
|
355
|
+
const source = valueMap.get('border-image-source');
|
|
356
|
+
const slice = valueMap.get('border-image-slice');
|
|
357
|
+
const repeat = valueMap.get('border-image-repeat');
|
|
358
|
+
if (!source) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const result = [source];
|
|
362
|
+
if (slice) {
|
|
363
|
+
result.push(slice);
|
|
364
|
+
}
|
|
365
|
+
if (repeat) {
|
|
366
|
+
result.push(repeat);
|
|
367
|
+
}
|
|
368
|
+
return result.join(' ') + importantSuffix;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (shorthandName === 'text-decoration') {
|
|
372
|
+
const line = valueMap.get('text-decoration-line');
|
|
373
|
+
const style = valueMap.get('text-decoration-style');
|
|
374
|
+
const color = valueMap.get('text-decoration-color');
|
|
375
|
+
if (!line) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
const result = [line];
|
|
379
|
+
if (style && style !== 'solid') {
|
|
380
|
+
result.push(style);
|
|
381
|
+
}
|
|
382
|
+
if (color && color !== 'currentcolor') {
|
|
383
|
+
result.push(color);
|
|
384
|
+
}
|
|
385
|
+
return result.join(' ') + importantSuffix;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (shorthandName === 'columns') {
|
|
389
|
+
return cleanValues.join(' ') + importantSuffix;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (shorthandName === 'list-style') {
|
|
393
|
+
const position = valueMap.get('list-style-position');
|
|
394
|
+
const image = valueMap.get('list-style-image');
|
|
395
|
+
const type = valueMap.get('list-style-type');
|
|
396
|
+
const result = [];
|
|
397
|
+
if (position && position !== 'outside') {
|
|
398
|
+
result.push(position);
|
|
399
|
+
}
|
|
400
|
+
if (image && image !== 'none') {
|
|
401
|
+
result.push(image);
|
|
402
|
+
}
|
|
403
|
+
if (type && type !== 'disc') {
|
|
404
|
+
result.push(type);
|
|
405
|
+
}
|
|
406
|
+
const joined = result.join(' ') || 'inside';
|
|
407
|
+
return joined + importantSuffix;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (shorthandName === 'font') {
|
|
411
|
+
const fontSize = valueMap.get('font-size');
|
|
412
|
+
const fontFamily = valueMap.get('font-family');
|
|
413
|
+
if (!fontSize || !fontFamily) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
const result = [];
|
|
417
|
+
const fontStyle = valueMap.get('font-style');
|
|
418
|
+
const fontWeight = valueMap.get('font-weight');
|
|
419
|
+
const lineHeight = valueMap.get('line-height');
|
|
420
|
+
if (fontStyle && fontStyle !== 'normal') {
|
|
421
|
+
result.push(fontStyle);
|
|
422
|
+
}
|
|
423
|
+
if (fontWeight && fontWeight !== '400' && fontWeight !== 'normal') {
|
|
424
|
+
result.push(fontWeight);
|
|
425
|
+
}
|
|
426
|
+
if (lineHeight) {
|
|
427
|
+
result.push(fontSize + '/' + lineHeight);
|
|
428
|
+
} else {
|
|
429
|
+
result.push(fontSize);
|
|
430
|
+
}
|
|
431
|
+
result.push(fontFamily);
|
|
432
|
+
return result.join(' ') + importantSuffix;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (shorthandName === 'flex') {
|
|
436
|
+
const grow = valueMap.get('flex-grow');
|
|
437
|
+
const shrink = valueMap.get('flex-shrink');
|
|
438
|
+
const basis = valueMap.get('flex-basis');
|
|
439
|
+
if (!grow || !shrink || !basis) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
return [grow, shrink, basis].join(' ') + importantSuffix;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Build shorthand value
|
|
446
|
+
if (properties.length === 2) {
|
|
447
|
+
// For 2-value shorthands (logical properties)
|
|
448
|
+
if (cleanValues[0] === cleanValues[1]) {
|
|
449
|
+
return cleanValues[0] + importantSuffix;
|
|
450
|
+
}
|
|
451
|
+
return cleanValues.join(' ') + importantSuffix;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (properties.length === 4) {
|
|
455
|
+
// For 4-value shorthands (margin, padding, inset, etc.)
|
|
456
|
+
// Collapse redundant values: top right bottom left → fewer values when sides match
|
|
457
|
+
return collapseShorthandParts([...cleanValues]).join(' ') + importantSuffix;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// For border shorthand (3 values: width, style, color)
|
|
461
|
+
const isBorderLikeShorthand = (
|
|
462
|
+
properties.length === 3 &&
|
|
463
|
+
(properties.includes('border-width') || properties.includes('outline-width')) &&
|
|
464
|
+
properties.some((property) => {
|
|
465
|
+
// Check if one longhand ends with "-style" (e.g. border-style, outline-style)
|
|
466
|
+
return /-style$/.test(property);
|
|
467
|
+
}) &&
|
|
468
|
+
properties.some((property) => {
|
|
469
|
+
// Check if one longhand ends with "-color" (e.g. border-color, outline-color)
|
|
470
|
+
return /-color$/.test(property);
|
|
471
|
+
})
|
|
472
|
+
);
|
|
473
|
+
if (isBorderLikeShorthand) {
|
|
474
|
+
return cleanValues.join(' ') + importantSuffix;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Deduplicates, merges, and optimizes CSS declarations within a rule block. Removes overridden longhands, collapses longhands into shorthands, and preserves intentional fallbacks.
|
|
482
|
+
*
|
|
483
|
+
* @param {Array} declarations The array of CSS declaration objects to process.
|
|
484
|
+
* @param {object} context The minification context with registered custom property data.
|
|
485
|
+
* @return {Array} A new array of optimized and reordered declaration objects.
|
|
486
|
+
*/
|
|
487
|
+
function processDeclarations (declarations, context) {
|
|
488
|
+
let result = [];
|
|
489
|
+
|
|
490
|
+
for (let declaration of declarations) {
|
|
491
|
+
if (declaration.type === 'rule' || declaration.type === 'media') {
|
|
492
|
+
result.push(declaration);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const propertyName = declaration.property;
|
|
497
|
+
if (!propertyName) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
let minifiedValue = minifyValue(declaration);
|
|
502
|
+
|
|
503
|
+
let previousIndex = -1;
|
|
504
|
+
for (let i = result.length - 1; i >= 0; i--) {
|
|
505
|
+
if (result[i].property === propertyName) {
|
|
506
|
+
previousIndex = i;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Also check if there's a prefixed version we can replace
|
|
512
|
+
let prefixedIndex = -1;
|
|
513
|
+
if (!propertyName.startsWith('-')) {
|
|
514
|
+
for (let i = result.length - 1; i >= 0; i--) {
|
|
515
|
+
const isPrefixedMatch = (
|
|
516
|
+
result[i].property.endsWith(propertyName) &&
|
|
517
|
+
result[i].property.startsWith('-')
|
|
518
|
+
);
|
|
519
|
+
if (isPrefixedMatch) {
|
|
520
|
+
prefixedIndex = i;
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (prefixedIndex !== -1) {
|
|
527
|
+
let prefixedValue = minifyValue(result[prefixedIndex]);
|
|
528
|
+
if (minifiedValue === prefixedValue) {
|
|
529
|
+
result.splice(prefixedIndex, 1);
|
|
530
|
+
// Re-adjust previousIndex if we removed an item before it
|
|
531
|
+
if (previousIndex > prefixedIndex) {
|
|
532
|
+
previousIndex--;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (previousIndex !== -1) {
|
|
538
|
+
const previousValue = minifyValue(result[previousIndex]);
|
|
539
|
+
|
|
540
|
+
if (previousValue.includes('!important') && !minifiedValue.includes('!important')) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Fallbacks for custom variables or older browser functions should be kept
|
|
545
|
+
const currentUsesModernSyntax = (
|
|
546
|
+
minifiedValue.includes('calc(') ||
|
|
547
|
+
minifiedValue.includes('env(') ||
|
|
548
|
+
minifiedValue.includes('var(') ||
|
|
549
|
+
minifiedValue.includes('-webkit-')
|
|
550
|
+
);
|
|
551
|
+
const previousUsesModernSyntax = (
|
|
552
|
+
previousValue.includes('calc(') ||
|
|
553
|
+
previousValue.includes('env(') ||
|
|
554
|
+
previousValue.includes('var(') ||
|
|
555
|
+
previousValue.includes('-webkit-')
|
|
556
|
+
);
|
|
557
|
+
if (currentUsesModernSyntax && !previousUsesModernSyntax) {
|
|
558
|
+
result.push(declaration);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Otherwise override previous identical property
|
|
563
|
+
result.splice(previousIndex, 1);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
result.push(declaration);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Handle shorthand merging
|
|
570
|
+
|
|
571
|
+
// First, remove longhand properties that are overridden by existing shorthands
|
|
572
|
+
const propertiesToRemove = new Set();
|
|
573
|
+
for (let i = 0; i < result.length; i++) {
|
|
574
|
+
const declaration = result[i];
|
|
575
|
+
if (declaration.property && shorthandMap[declaration.property]) {
|
|
576
|
+
// This is a shorthand, check if any longhands come before it
|
|
577
|
+
const overridden = getOverriddenLonghands(declaration.property);
|
|
578
|
+
for (const longhandProperty of overridden) {
|
|
579
|
+
const longhandIndex = result.findIndex((candidate, index) => {
|
|
580
|
+
return candidate.property === longhandProperty && index < i;
|
|
581
|
+
});
|
|
582
|
+
if (longhandIndex !== -1) {
|
|
583
|
+
propertiesToRemove.add(longhandProperty);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
result = result.filter((declaration) => {
|
|
590
|
+
return !propertiesToRemove.has(declaration.property);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// Try to merge remaining longhands into shorthands
|
|
594
|
+
let changed = true;
|
|
595
|
+
while (changed) {
|
|
596
|
+
changed = false;
|
|
597
|
+
const mergedProperties = new Set();
|
|
598
|
+
const newDeclarations = [];
|
|
599
|
+
|
|
600
|
+
for (const [shorthand, longhands] of Object.entries(shorthandMap)) {
|
|
601
|
+
const shorthandAlreadyExists = result.some((declaration) => {
|
|
602
|
+
return declaration.property === shorthand;
|
|
603
|
+
});
|
|
604
|
+
if (shorthandAlreadyExists) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const mergeableProperties = getMergeProps(shorthand, longhands, result);
|
|
609
|
+
if (!mergeableProperties) {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const relevantDeclarations = result.filter((declaration) => {
|
|
613
|
+
return mergeableProperties.includes(declaration.property);
|
|
614
|
+
});
|
|
615
|
+
const mergedValue = tryMergeToShorthand(mergeableProperties, relevantDeclarations, shorthand, context);
|
|
616
|
+
if (!mergedValue) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
newDeclarations.push({ property: shorthand, value: mergedValue });
|
|
621
|
+
const isMarginPaddingInset = (
|
|
622
|
+
shorthand === 'margin' ||
|
|
623
|
+
shorthand === 'padding' ||
|
|
624
|
+
shorthand === 'inset'
|
|
625
|
+
);
|
|
626
|
+
const someAreImportant = relevantDeclarations.some((declaration) => {
|
|
627
|
+
return minifyValue(declaration).includes('!important');
|
|
628
|
+
});
|
|
629
|
+
const allAreImportant = relevantDeclarations.every((declaration) => {
|
|
630
|
+
return minifyValue(declaration).includes('!important');
|
|
631
|
+
});
|
|
632
|
+
const hasMixedImportant = someAreImportant && !allAreImportant;
|
|
633
|
+
|
|
634
|
+
if (isMarginPaddingInset && hasMixedImportant) {
|
|
635
|
+
for (const property of mergeableProperties) {
|
|
636
|
+
const declaration = relevantDeclarations.find((candidate) => {
|
|
637
|
+
return candidate.property === property;
|
|
638
|
+
});
|
|
639
|
+
if (declaration && !minifyValue(declaration).includes('!important')) {
|
|
640
|
+
mergedProperties.add(property);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
for (const property of mergeableProperties) {
|
|
645
|
+
mergedProperties.add(property);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (newDeclarations.length) {
|
|
651
|
+
result = result.filter((declaration) => {
|
|
652
|
+
return !mergedProperties.has(declaration.property);
|
|
653
|
+
});
|
|
654
|
+
result = [...result, ...newDeclarations];
|
|
655
|
+
changed = true;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return orderDeclarations(result);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export { processDeclarations };
|