@thejaredwilcurt/csslop 0.0.20 → 0.0.22
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 +12 -5
- package/package.json +5 -7
- package/src/declarations/background.js +322 -0
- package/src/declarations/border.js +120 -0
- package/src/declarations/config.js +36 -0
- package/src/declarations/css-wide-keywords.js +285 -0
- package/src/declarations/merge.js +323 -0
- package/src/declarations/order.js +77 -0
- package/src/declarations/process.js +188 -869
- package/src/declarations/shorthand-values.js +374 -0
- package/src/index.js +5 -2
- package/src/rules/normalize.js +29 -5
- package/src/rules/optimize.js +5 -2
- package/src/rules/stringify.js +49 -18
- package/src/value/minify.js +102 -36
- package/src/value/syntax.js +82 -1
|
@@ -4,793 +4,79 @@
|
|
|
4
4
|
|
|
5
5
|
import { minifyValue } from '../value/minify.js';
|
|
6
6
|
import { hasInvalidQuotesCount } from '../value/quotes.js';
|
|
7
|
-
import { collapseShorthandParts } from '../value/shared.js';
|
|
8
7
|
|
|
8
|
+
import { absorbBackgroundLonghandsIntoShorthand } from './background.js';
|
|
9
|
+
import { collapseBorderTrioWithPerEdgeColor } from './border.js';
|
|
10
|
+
import { shorthandMap } from './config.js';
|
|
11
|
+
import { hoistCssWideKeywordsIntoShorthands } from './css-wide-keywords.js';
|
|
9
12
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from './
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* @param {Array} declarations The array of CSS declaration objects to reorder.
|
|
18
|
-
* @return {Array} A new array with declarations in the corrected order.
|
|
19
|
-
*/
|
|
20
|
-
function orderDeclarations (declarations) {
|
|
21
|
-
const ordered = [...declarations];
|
|
22
|
-
const moveBefore = (prop, beforeProp) => {
|
|
23
|
-
const fromIndex = ordered.findIndex((declaration) => {
|
|
24
|
-
return declaration?.property === prop;
|
|
25
|
-
});
|
|
26
|
-
const toIndex = ordered.findIndex((declaration) => {
|
|
27
|
-
return declaration?.property === beforeProp;
|
|
28
|
-
});
|
|
29
|
-
if (fromIndex === -1 || toIndex === -1 || fromIndex < toIndex) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const [item] = ordered.splice(fromIndex, 1);
|
|
33
|
-
ordered.splice(toIndex, 0, item);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
moveBefore('border', 'border-image');
|
|
37
|
-
moveBefore('font', 'font-feature-settings');
|
|
38
|
-
moveBefore('font', 'font-variant-ligatures');
|
|
39
|
-
moveBefore('font', 'font-kerning');
|
|
40
|
-
moveBefore('font', 'font-variation-settings');
|
|
41
|
-
moveBefore('mask', 'mask-border');
|
|
42
|
-
moveBefore('margin', 'margin-top');
|
|
43
|
-
moveBefore('margin', 'margin-right');
|
|
44
|
-
moveBefore('margin', 'margin-bottom');
|
|
45
|
-
moveBefore('margin', 'margin-left');
|
|
46
|
-
|
|
47
|
-
return ordered;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* 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.
|
|
52
|
-
*
|
|
53
|
-
* @param {string} shorthand The CSS shorthand property name.
|
|
54
|
-
* @param {Array} longhands The expected longhand property names for this shorthand.
|
|
55
|
-
* @param {Array} declarations The current array of CSS declaration objects.
|
|
56
|
-
* @return {Array|null} The list of longhand names to merge, or null if merging is not possible.
|
|
57
|
-
*/
|
|
58
|
-
function getMergeProps (shorthand, longhands, declarations) {
|
|
59
|
-
const presentLonghands = longhands.filter((longhand) => {
|
|
60
|
-
return declarations.some((declaration) => {
|
|
61
|
-
return declaration.property === longhand;
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
if (presentLonghands.length === 0) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
if (shorthand === 'font') {
|
|
68
|
-
const hasRequiredFontProps = presentLonghands.includes('font-size') && presentLonghands.includes('font-family');
|
|
69
|
-
if (hasRequiredFontProps) {
|
|
70
|
-
return presentLonghands;
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
if (shorthand === 'background-position') {
|
|
75
|
-
const hasBothAxes = presentLonghands.includes('background-position-x') && presentLonghands.includes('background-position-y');
|
|
76
|
-
if (hasBothAxes) {
|
|
77
|
-
return presentLonghands;
|
|
78
|
-
}
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
if (shorthand === 'background') {
|
|
82
|
-
const hasBackgroundProp = presentLonghands.includes('background-color') || presentLonghands.includes('background-image');
|
|
83
|
-
if (hasBackgroundProp) {
|
|
84
|
-
return presentLonghands;
|
|
85
|
-
}
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
if (shorthand === 'mask') {
|
|
89
|
-
if (presentLonghands.includes('mask-image')) {
|
|
90
|
-
return presentLonghands;
|
|
91
|
-
}
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
if (shorthand === 'border-image') {
|
|
95
|
-
if (presentLonghands.includes('border-image-source')) {
|
|
96
|
-
return presentLonghands;
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
if (shorthand === 'border') {
|
|
101
|
-
const hasAllBorderParts = (
|
|
102
|
-
presentLonghands.includes('border-width') &&
|
|
103
|
-
presentLonghands.includes('border-style') &&
|
|
104
|
-
presentLonghands.includes('border-color')
|
|
105
|
-
);
|
|
106
|
-
if (hasAllBorderParts) {
|
|
107
|
-
return ['border-width', 'border-style', 'border-color'];
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
if (shorthand === 'flex') {
|
|
112
|
-
const hasAllFlexParts = (
|
|
113
|
-
presentLonghands.includes('flex-grow') &&
|
|
114
|
-
presentLonghands.includes('flex-shrink') &&
|
|
115
|
-
presentLonghands.includes('flex-basis')
|
|
116
|
-
);
|
|
117
|
-
if (hasAllFlexParts) {
|
|
118
|
-
return ['flex-grow', 'flex-shrink', 'flex-basis'];
|
|
119
|
-
}
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
if (presentLonghands.length === longhands.length) {
|
|
123
|
-
return longhands;
|
|
124
|
-
}
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
13
|
+
getMergeProps,
|
|
14
|
+
tryMergeToShorthand
|
|
15
|
+
} from './merge.js';
|
|
16
|
+
import {
|
|
17
|
+
getOverriddenLonghands,
|
|
18
|
+
orderDeclarations
|
|
19
|
+
} from './order.js';
|
|
127
20
|
|
|
128
21
|
/**
|
|
129
|
-
*
|
|
22
|
+
* Shorthands that keep their non-important longhands in the output, so a mixed
|
|
23
|
+
* `!important` group still merges the important longhands into the shorthand.
|
|
130
24
|
*
|
|
131
|
-
* @
|
|
132
|
-
* @return {Array} A deduplicated array of all longhand property names that the shorthand overrides, including nested longhands.
|
|
25
|
+
* @type {Set<string>}
|
|
133
26
|
*/
|
|
134
|
-
|
|
135
|
-
const direct = shorthandMap[shorthandProp] || [];
|
|
136
|
-
const overrides = shorthandOverrideMap[shorthandProp] || [];
|
|
137
|
-
const all = [...direct, ...overrides];
|
|
138
|
-
for (const prop of direct) {
|
|
139
|
-
const nested = shorthandMap[prop] || [];
|
|
140
|
-
all.push(...nested);
|
|
141
|
-
}
|
|
142
|
-
return [...new Set(all)];
|
|
143
|
-
}
|
|
27
|
+
const MIXED_IMPORTANT_SHORTHANDS = new Set(['margin', 'padding', 'inset']);
|
|
144
28
|
|
|
145
29
|
/**
|
|
146
|
-
*
|
|
30
|
+
* Functions and syntaxes that older browsers do not understand, so an earlier
|
|
31
|
+
* declaration using only classic syntax is kept as a fallback for them.
|
|
147
32
|
*
|
|
148
|
-
* @
|
|
149
|
-
* @return {boolean} True if the value contains a var() with a fallback comma.
|
|
33
|
+
* @type {Array}
|
|
150
34
|
*/
|
|
151
|
-
|
|
152
|
-
// Match var() containing a comma (indicating a fallback value)
|
|
153
|
-
return /var\([^)]*,/.test(value);
|
|
154
|
-
}
|
|
35
|
+
const MODERN_SYNTAX_MARKERS = ['calc(', 'env(', 'var(', '-webkit-'];
|
|
155
36
|
|
|
156
37
|
/**
|
|
157
|
-
* Determines whether a value
|
|
38
|
+
* Determines whether a value relies on syntax that older browsers cannot parse,
|
|
39
|
+
* which means a preceding declaration for the same property is an intentional
|
|
40
|
+
* fallback rather than a redundant duplicate.
|
|
158
41
|
*
|
|
159
|
-
* @param {string} value
|
|
160
|
-
* @
|
|
161
|
-
* @return {boolean} True if the value is safe to merge into a shorthand.
|
|
42
|
+
* @param {string} value The minified CSS value string.
|
|
43
|
+
* @return {boolean} Whether the value uses modern syntax.
|
|
162
44
|
*/
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (!containsVar || hasVarFallback(value)) {
|
|
167
|
-
return !hasVarFallback(value);
|
|
168
|
-
}
|
|
169
|
-
// Extract all var() references with their custom property names
|
|
170
|
-
const matches = [...value.matchAll(/var\((--[A-Za-z0-9_-]+)\)/g)];
|
|
171
|
-
if (!matches.length) {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
return matches.every(([, propertyName]) => {
|
|
175
|
-
return context.registeredCustomProperties.has(propertyName);
|
|
45
|
+
function usesModernSyntax (value) {
|
|
46
|
+
return MODERN_SYNTAX_MARKERS.some((marker) => {
|
|
47
|
+
return value.includes(marker);
|
|
176
48
|
});
|
|
177
49
|
}
|
|
178
50
|
|
|
179
51
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* `background-position-x` and `background-position-y` into a single value.
|
|
183
|
-
*
|
|
184
|
-
* @param {Map} valueMap A map of property names to their minified values.
|
|
185
|
-
* @return {string|null} The resolved position string, or null if no position data is available.
|
|
186
|
-
*/
|
|
187
|
-
function resolveBackgroundPosition (valueMap) {
|
|
188
|
-
const position = valueMap.get('background-position');
|
|
189
|
-
if (position) {
|
|
190
|
-
return position;
|
|
191
|
-
}
|
|
192
|
-
const positionX = valueMap.get('background-position-x');
|
|
193
|
-
const positionY = valueMap.get('background-position-y');
|
|
194
|
-
if (positionX && positionY) {
|
|
195
|
-
return positionX + ' ' + positionY;
|
|
196
|
-
}
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const BACKGROUND_POSITION_KEYWORDS = new Set(['left', 'center', 'right', 'top', 'bottom']);
|
|
201
|
-
const BACKGROUND_REPEAT_KEYWORDS = new Set(['repeat', 'no-repeat', 'repeat-x', 'repeat-y', 'space', 'round']);
|
|
202
|
-
const BACKGROUND_ATTACHMENT_KEYWORDS = new Set(['scroll', 'fixed', 'local']);
|
|
203
|
-
const BACKGROUND_BOX_KEYWORDS = new Set(['border-box', 'padding-box', 'content-box']);
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Determines whether a token is a background image component such as `none`,
|
|
207
|
-
* `url(...)`, or an image-producing function like `linear-gradient(...)`.
|
|
208
|
-
*
|
|
209
|
-
* @param {string} token The token to classify.
|
|
210
|
-
* @return {boolean} Whether the token is a background image token.
|
|
211
|
-
*/
|
|
212
|
-
function isBackgroundImageToken (token) {
|
|
213
|
-
if (token === 'none' || token.startsWith('url(')) {
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
if (!token.endsWith(')')) {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
const functionNameMatch = token.match(/^([a-z-]+)\(/i);
|
|
220
|
-
if (!functionNameMatch) {
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
const functionName = functionNameMatch[1].toLowerCase();
|
|
224
|
-
return !['calc', 'min', 'max', 'clamp', 'var', 'env', 'rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'lab', 'lch', 'oklab', 'oklch', 'color'].includes(functionName);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Determines whether a token can participate in a background-position value.
|
|
229
|
-
*
|
|
230
|
-
* @param {string} token The token to classify.
|
|
231
|
-
* @return {boolean} Whether the token is a valid background-position token.
|
|
232
|
-
*/
|
|
233
|
-
function isBackgroundPositionToken (token) {
|
|
234
|
-
const lowercaseToken = token.toLowerCase();
|
|
235
|
-
if (BACKGROUND_POSITION_KEYWORDS.has(lowercaseToken)) {
|
|
236
|
-
return true;
|
|
237
|
-
}
|
|
238
|
-
if (/^[+-]?(?:\d+|\d*\.\d+)(?:%|[a-z]+)?$/i.test(token)) {
|
|
239
|
-
return true;
|
|
240
|
-
}
|
|
241
|
-
return /^(?:calc|min|max|clamp|var|env)\(/i.test(token);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Determines whether a token is a background color component after excluding
|
|
246
|
-
* known image, position, repeat, attachment, and box tokens.
|
|
247
|
-
*
|
|
248
|
-
* @param {string} token The token to classify.
|
|
249
|
-
* @return {boolean} Whether the token is a background color token.
|
|
250
|
-
*/
|
|
251
|
-
function isBackgroundColorToken (token) {
|
|
252
|
-
if (token === '/' || isBackgroundImageToken(token) || isBackgroundPositionToken(token)) {
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
const lowercaseToken = token.toLowerCase();
|
|
256
|
-
if (BACKGROUND_REPEAT_KEYWORDS.has(lowercaseToken) || BACKGROUND_ATTACHMENT_KEYWORDS.has(lowercaseToken) || BACKGROUND_BOX_KEYWORDS.has(lowercaseToken)) {
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
return /^#/i.test(token) || /^[a-z-]+$/i.test(token) || /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\(/i.test(token);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Splits a token like `linear-gradient(...)100%` into separate image and
|
|
264
|
-
* position tokens when the function output is immediately followed by a
|
|
265
|
-
* background-position token.
|
|
266
|
-
*
|
|
267
|
-
* @param {string} token The token to inspect.
|
|
268
|
-
* @return {Array} The original token, or separate image/position tokens.
|
|
269
|
-
*/
|
|
270
|
-
function splitAttachedBackgroundImageToken (token) {
|
|
271
|
-
const lastCloseParenthesis = token.lastIndexOf(')');
|
|
272
|
-
if (lastCloseParenthesis === -1 || lastCloseParenthesis === token.length - 1) {
|
|
273
|
-
return [token];
|
|
274
|
-
}
|
|
275
|
-
const imageToken = token.slice(0, lastCloseParenthesis + 1);
|
|
276
|
-
const followingToken = token.slice(lastCloseParenthesis + 1);
|
|
277
|
-
if (!isBackgroundImageToken(imageToken) || !isBackgroundPositionToken(followingToken)) {
|
|
278
|
-
return [token];
|
|
279
|
-
}
|
|
280
|
-
return [imageToken, followingToken];
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Splits a single-layer background shorthand into top-level tokens while
|
|
285
|
-
* respecting nested parentheses and preserving `/` as its own token.
|
|
52
|
+
* Finds the index of the last declaration matching a predicate, which is the
|
|
53
|
+
* declaration that wins the cascade within a rule.
|
|
286
54
|
*
|
|
287
|
-
* @param {
|
|
288
|
-
* @
|
|
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.
|
|
289
58
|
*/
|
|
290
|
-
function
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
for (const character of value) {
|
|
296
|
-
if (character === '(') {
|
|
297
|
-
parenthesisDepth++;
|
|
298
|
-
} else if (character === ')') {
|
|
299
|
-
parenthesisDepth--;
|
|
59
|
+
function findLastIndex (declarations, predicate) {
|
|
60
|
+
for (let index = declarations.length - 1; index >= 0; index--) {
|
|
61
|
+
if (predicate(declarations[index])) {
|
|
62
|
+
return index;
|
|
300
63
|
}
|
|
301
|
-
|
|
302
|
-
if (parenthesisDepth === 0 && character === '/') {
|
|
303
|
-
if (currentToken) {
|
|
304
|
-
tokens.push(currentToken);
|
|
305
|
-
currentToken = '';
|
|
306
|
-
}
|
|
307
|
-
tokens.push('/');
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (parenthesisDepth === 0 && /\s/.test(character)) {
|
|
312
|
-
if (currentToken) {
|
|
313
|
-
tokens.push(currentToken);
|
|
314
|
-
currentToken = '';
|
|
315
|
-
}
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
currentToken += character;
|
|
320
64
|
}
|
|
321
|
-
|
|
322
|
-
if (currentToken) {
|
|
323
|
-
tokens.push(currentToken);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const normalizedTokens = [];
|
|
327
|
-
for (const token of tokens) {
|
|
328
|
-
normalizedTokens.push(...splitAttachedBackgroundImageToken(token));
|
|
329
|
-
}
|
|
330
|
-
return normalizedTokens;
|
|
65
|
+
return -1;
|
|
331
66
|
}
|
|
332
67
|
|
|
333
68
|
/**
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
69
|
+
* Removes declarations that a later declaration for the same property makes
|
|
70
|
+
* redundant, including vendor-prefixed duplicates, while keeping intentional
|
|
71
|
+
* fallbacks for values that use modern syntax.
|
|
337
72
|
*
|
|
338
|
-
* @param {
|
|
339
|
-
* @return {
|
|
73
|
+
* @param {Array} declarations The declarations of a single rule, in source order.
|
|
74
|
+
* @return {Array} The surviving declarations, in source order.
|
|
340
75
|
*/
|
|
341
|
-
function
|
|
342
|
-
const tokens = splitBackgroundTokens(value);
|
|
343
|
-
const componentMap = new Map();
|
|
344
|
-
|
|
345
|
-
for (const token of tokens) {
|
|
346
|
-
if (token === '/') {
|
|
347
|
-
return null;
|
|
348
|
-
}
|
|
349
|
-
if (isBackgroundImageToken(token)) {
|
|
350
|
-
if (componentMap.has('background-image')) {
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
componentMap.set('background-image', token);
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
if (isBackgroundColorToken(token)) {
|
|
357
|
-
if (componentMap.has('background-color')) {
|
|
358
|
-
return null;
|
|
359
|
-
}
|
|
360
|
-
componentMap.set('background-color', token);
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
return null;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return componentMap;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Serializes normalized background components into a minified background
|
|
371
|
-
* shorthand value while omitting default sub-values.
|
|
372
|
-
*
|
|
373
|
-
* @param {Map} valueMap The normalized background component map.
|
|
374
|
-
* @param {string} importantSuffix A trailing `!important` suffix, if needed.
|
|
375
|
-
* @return {string|null} The minified background shorthand, or null.
|
|
376
|
-
*/
|
|
377
|
-
function buildBackgroundShorthandValue (valueMap, importantSuffix) {
|
|
378
|
-
const color = valueMap.get('background-color');
|
|
379
|
-
const image = valueMap.get('background-image');
|
|
380
|
-
const repeat = valueMap.get('background-repeat');
|
|
381
|
-
const attachment = valueMap.get('background-attachment');
|
|
382
|
-
const size = valueMap.get('background-size');
|
|
383
|
-
const origin = valueMap.get('background-origin');
|
|
384
|
-
const clip = valueMap.get('background-clip');
|
|
385
|
-
const position = resolveBackgroundPosition(valueMap);
|
|
386
|
-
|
|
76
|
+
function deduplicateDeclarations (declarations) {
|
|
387
77
|
const result = [];
|
|
388
|
-
if (color && color !== 'transparent') {
|
|
389
|
-
result.push(color);
|
|
390
|
-
}
|
|
391
|
-
if (image && image !== 'none') {
|
|
392
|
-
result.push(image);
|
|
393
|
-
}
|
|
394
|
-
if (position && position !== '0 0' && position !== '0% 0%') {
|
|
395
|
-
result.push(position);
|
|
396
|
-
}
|
|
397
|
-
if (size && size !== 'auto') {
|
|
398
|
-
if (position && position !== '0 0' && position !== '0% 0%') {
|
|
399
|
-
result.push('/' + size);
|
|
400
|
-
} else {
|
|
401
|
-
result.push('0 0/' + size);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
if (repeat && repeat !== 'repeat') {
|
|
405
|
-
result.push(repeat);
|
|
406
|
-
}
|
|
407
|
-
if (attachment && attachment !== 'scroll') {
|
|
408
|
-
result.push(attachment);
|
|
409
|
-
}
|
|
410
|
-
const hasNonDefaultOrigin = origin && origin !== 'padding-box';
|
|
411
|
-
const hasNonDefaultClip = clip && clip !== 'border-box';
|
|
412
|
-
if (hasNonDefaultOrigin && hasNonDefaultClip) {
|
|
413
|
-
result.push(origin);
|
|
414
|
-
result.push(clip);
|
|
415
|
-
} else if (hasNonDefaultOrigin || hasNonDefaultClip) {
|
|
416
|
-
if (origin) {
|
|
417
|
-
result.push(origin);
|
|
418
|
-
}
|
|
419
|
-
if (clip) {
|
|
420
|
-
result.push(clip);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
if (!result.length) {
|
|
424
|
-
return null;
|
|
425
|
-
}
|
|
426
|
-
return result.join(' ') + importantSuffix;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Merges later background longhands into an earlier simple background shorthand
|
|
431
|
-
* when their combined value can be reconstructed without changing semantics.
|
|
432
|
-
*
|
|
433
|
-
* @param {Array} declarations The declarations in source order.
|
|
434
|
-
* @return {Array} The updated declarations with absorbed longhands.
|
|
435
|
-
*/
|
|
436
|
-
function absorbBackgroundLonghandsIntoShorthand (declarations) {
|
|
437
|
-
const backgroundIndex = declarations.findIndex((declaration) => {
|
|
438
|
-
return declaration.property === 'background';
|
|
439
|
-
});
|
|
440
|
-
if (backgroundIndex === -1) {
|
|
441
|
-
return declarations;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
const backgroundDeclaration = declarations[backgroundIndex];
|
|
445
|
-
const backgroundValue = minifyValue(backgroundDeclaration);
|
|
446
|
-
const backgroundIsImportant = backgroundValue.includes('!important');
|
|
447
|
-
const simpleBase = extractSimpleBackgroundBase(backgroundValue.replace('!important', '').trim());
|
|
448
|
-
if (!simpleBase) {
|
|
449
|
-
return declarations;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const absorbableProperties = new Set(shorthandMap.background.filter((property) => {
|
|
453
|
-
return property !== 'background';
|
|
454
|
-
}));
|
|
455
|
-
const relevantDeclarations = declarations.filter((declaration, index) => {
|
|
456
|
-
return index > backgroundIndex && absorbableProperties.has(declaration.property);
|
|
457
|
-
});
|
|
458
|
-
if (!relevantDeclarations.length) {
|
|
459
|
-
return declarations;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const sharesImportance = relevantDeclarations.every((declaration) => {
|
|
463
|
-
return minifyValue(declaration).includes('!important') === backgroundIsImportant;
|
|
464
|
-
});
|
|
465
|
-
if (!sharesImportance) {
|
|
466
|
-
return declarations;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
for (const declaration of relevantDeclarations) {
|
|
470
|
-
simpleBase.set(declaration.property, minifyValue(declaration).replace('!important', '').trim());
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const mergedValue = buildBackgroundShorthandValue(simpleBase, backgroundIsImportant ? '!important' : '');
|
|
474
|
-
if (!mergedValue) {
|
|
475
|
-
return declarations;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
return declarations.flatMap((declaration, index) => {
|
|
479
|
-
if (index === backgroundIndex) {
|
|
480
|
-
return [{ ...declaration, value: mergedValue }];
|
|
481
|
-
}
|
|
482
|
-
if (index > backgroundIndex && absorbableProperties.has(declaration.property)) {
|
|
483
|
-
return [];
|
|
484
|
-
}
|
|
485
|
-
return [declaration];
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Try to merge longhand properties into a shorthand.
|
|
491
|
-
*
|
|
492
|
-
* @param {Array} properties The longhand property names to merge.
|
|
493
|
-
* @param {Array} declarations The CSS declaration objects to draw values from.
|
|
494
|
-
* @param {string} shorthandName The target shorthand property name.
|
|
495
|
-
* @param {object} context The minification context with registered custom property data.
|
|
496
|
-
* @return {string|null} The merged shorthand value string, or null if merging is not possible.
|
|
497
|
-
*/
|
|
498
|
-
function tryMergeToShorthand (properties, declarations, shorthandName = '', context) {
|
|
499
|
-
if (properties.length < 2) {
|
|
500
|
-
return null;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const values = properties.map((property) => {
|
|
504
|
-
const declaration = declarations.find((candidate) => {
|
|
505
|
-
return candidate.property === property;
|
|
506
|
-
});
|
|
507
|
-
if (declaration) {
|
|
508
|
-
return minifyValue(declaration);
|
|
509
|
-
}
|
|
510
|
-
return null;
|
|
511
|
-
});
|
|
512
78
|
|
|
513
|
-
|
|
514
|
-
const hasNullValue = values.some((value) => {
|
|
515
|
-
return value === null;
|
|
516
|
-
});
|
|
517
|
-
if (hasNullValue) {
|
|
518
|
-
return null;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Don't merge if any value has var() with fallback or unknown custom properties
|
|
522
|
-
const hasUnmergeableVar = values.some((value) => {
|
|
523
|
-
return !canMergeVarValue(value, context);
|
|
524
|
-
});
|
|
525
|
-
if (hasUnmergeableVar) {
|
|
526
|
-
return null;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Check if all values have the same !important status
|
|
530
|
-
const importantFlags = values.map((value) => {
|
|
531
|
-
return value.includes('!important');
|
|
532
|
-
});
|
|
533
|
-
const allImportant = importantFlags.every((flag) => {
|
|
534
|
-
return flag;
|
|
535
|
-
});
|
|
536
|
-
const noneImportant = importantFlags.every((flag) => {
|
|
537
|
-
return !flag;
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
// Allow mixed important flags for margin/padding/inset - merge without !important on shorthand
|
|
541
|
-
// For other properties, mixed important flags are not allowed
|
|
542
|
-
const allowsMixedImportant = (
|
|
543
|
-
shorthandName === 'margin' ||
|
|
544
|
-
shorthandName === 'padding' ||
|
|
545
|
-
shorthandName === 'inset' ||
|
|
546
|
-
shorthandName === 'position-try'
|
|
547
|
-
);
|
|
548
|
-
if (!allImportant && !noneImportant && !allowsMixedImportant) {
|
|
549
|
-
return null;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const cleanValues = values.map((value) => {
|
|
553
|
-
return value
|
|
554
|
-
.replace('!important', '')
|
|
555
|
-
.trim();
|
|
556
|
-
});
|
|
557
|
-
const valueMap = new Map(properties.map((property, index) => {
|
|
558
|
-
return [property, cleanValues[index]];
|
|
559
|
-
}));
|
|
560
|
-
|
|
561
|
-
// For margin/padding with mixed important, don't use !important on the shorthand
|
|
562
|
-
// Only use !important if ALL values have it
|
|
563
|
-
const useImportant = allImportant;
|
|
564
|
-
const importantSuffix = useImportant ? '!important' : '';
|
|
565
|
-
|
|
566
|
-
if (shorthandName === 'position-try') {
|
|
567
|
-
const order = valueMap.get('position-try-order');
|
|
568
|
-
const fallbacks = valueMap.get('position-try-fallbacks');
|
|
569
|
-
if (order === 'normal' && fallbacks) {
|
|
570
|
-
return fallbacks + importantSuffix;
|
|
571
|
-
}
|
|
572
|
-
return null;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
if (shorthandName === 'transition') {
|
|
576
|
-
const transitionProperty = valueMap.get('transition-property');
|
|
577
|
-
const duration = valueMap.get('transition-duration');
|
|
578
|
-
const timing = valueMap.get('transition-timing-function');
|
|
579
|
-
const delay = valueMap.get('transition-delay');
|
|
580
|
-
if (!transitionProperty || !duration) {
|
|
581
|
-
return null;
|
|
582
|
-
}
|
|
583
|
-
const result = [transitionProperty, duration];
|
|
584
|
-
if (timing && timing !== 'ease') {
|
|
585
|
-
result.push(timing);
|
|
586
|
-
}
|
|
587
|
-
if (delay && delay !== '0' && delay !== '0s') {
|
|
588
|
-
result.push(delay);
|
|
589
|
-
}
|
|
590
|
-
return result.join(' ') + importantSuffix;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
if (shorthandName === 'animation') {
|
|
594
|
-
const animationName = valueMap.get('animation-name');
|
|
595
|
-
const duration = valueMap.get('animation-duration');
|
|
596
|
-
if (!animationName || !duration) {
|
|
597
|
-
return null;
|
|
598
|
-
}
|
|
599
|
-
const result = [animationName, duration];
|
|
600
|
-
const timing = valueMap.get('animation-timing-function');
|
|
601
|
-
const delay = valueMap.get('animation-delay');
|
|
602
|
-
const iteration = valueMap.get('animation-iteration-count');
|
|
603
|
-
const direction = valueMap.get('animation-direction');
|
|
604
|
-
const fillMode = valueMap.get('animation-fill-mode');
|
|
605
|
-
const playState = valueMap.get('animation-play-state');
|
|
606
|
-
if (timing && timing !== 'ease') {
|
|
607
|
-
result.push(timing);
|
|
608
|
-
}
|
|
609
|
-
if (delay && delay !== '0' && delay !== '0s') {
|
|
610
|
-
result.push(delay);
|
|
611
|
-
}
|
|
612
|
-
if (iteration && iteration !== '1') {
|
|
613
|
-
result.push(iteration);
|
|
614
|
-
}
|
|
615
|
-
if (direction && direction !== 'normal') {
|
|
616
|
-
result.push(direction);
|
|
617
|
-
}
|
|
618
|
-
if (fillMode && fillMode !== 'none') {
|
|
619
|
-
result.push(fillMode);
|
|
620
|
-
}
|
|
621
|
-
if (playState && playState !== 'running') {
|
|
622
|
-
result.push(playState);
|
|
623
|
-
}
|
|
624
|
-
return result.join(' ') + importantSuffix;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
if (shorthandName === 'background-position') {
|
|
628
|
-
const positionX = valueMap.get('background-position-x');
|
|
629
|
-
const positionY = valueMap.get('background-position-y');
|
|
630
|
-
if (!positionX || !positionY) {
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
return positionX + ' ' + positionY + importantSuffix;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (shorthandName === 'background') {
|
|
637
|
-
return buildBackgroundShorthandValue(valueMap, importantSuffix);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
if (shorthandName === 'mask') {
|
|
641
|
-
const image = valueMap.get('mask-image');
|
|
642
|
-
const repeat = valueMap.get('mask-repeat');
|
|
643
|
-
const size = valueMap.get('mask-size');
|
|
644
|
-
if (!image) {
|
|
645
|
-
return null;
|
|
646
|
-
}
|
|
647
|
-
let result = image;
|
|
648
|
-
if (repeat) {
|
|
649
|
-
result += ' ' + repeat;
|
|
650
|
-
}
|
|
651
|
-
if (size) {
|
|
652
|
-
result += '/' + size;
|
|
653
|
-
}
|
|
654
|
-
return result + importantSuffix;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
if (shorthandName === 'border-image') {
|
|
658
|
-
const source = valueMap.get('border-image-source');
|
|
659
|
-
const slice = valueMap.get('border-image-slice');
|
|
660
|
-
const repeat = valueMap.get('border-image-repeat');
|
|
661
|
-
if (!source) {
|
|
662
|
-
return null;
|
|
663
|
-
}
|
|
664
|
-
const result = [source];
|
|
665
|
-
if (slice) {
|
|
666
|
-
result.push(slice);
|
|
667
|
-
}
|
|
668
|
-
if (repeat) {
|
|
669
|
-
result.push(repeat);
|
|
670
|
-
}
|
|
671
|
-
return result.join(' ') + importantSuffix;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
if (shorthandName === 'text-decoration') {
|
|
675
|
-
const line = valueMap.get('text-decoration-line');
|
|
676
|
-
const style = valueMap.get('text-decoration-style');
|
|
677
|
-
const color = valueMap.get('text-decoration-color');
|
|
678
|
-
if (!line) {
|
|
679
|
-
return null;
|
|
680
|
-
}
|
|
681
|
-
const result = [line];
|
|
682
|
-
if (style && style !== 'solid') {
|
|
683
|
-
result.push(style);
|
|
684
|
-
}
|
|
685
|
-
if (color && color !== 'currentcolor') {
|
|
686
|
-
result.push(color);
|
|
687
|
-
}
|
|
688
|
-
return result.join(' ') + importantSuffix;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
if (shorthandName === 'columns') {
|
|
692
|
-
return cleanValues.join(' ') + importantSuffix;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
if (shorthandName === 'list-style') {
|
|
696
|
-
const position = valueMap.get('list-style-position');
|
|
697
|
-
const image = valueMap.get('list-style-image');
|
|
698
|
-
const type = valueMap.get('list-style-type');
|
|
699
|
-
const result = [];
|
|
700
|
-
if (position && position !== 'outside') {
|
|
701
|
-
result.push(position);
|
|
702
|
-
}
|
|
703
|
-
if (image && image !== 'none') {
|
|
704
|
-
result.push(image);
|
|
705
|
-
}
|
|
706
|
-
if (type && type !== 'disc') {
|
|
707
|
-
result.push(type);
|
|
708
|
-
}
|
|
709
|
-
const joined = result.join(' ') || 'inside';
|
|
710
|
-
return joined + importantSuffix;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
if (shorthandName === 'font') {
|
|
714
|
-
const fontSize = valueMap.get('font-size');
|
|
715
|
-
const fontFamily = valueMap.get('font-family');
|
|
716
|
-
if (!fontSize || !fontFamily) {
|
|
717
|
-
return null;
|
|
718
|
-
}
|
|
719
|
-
const result = [];
|
|
720
|
-
const fontStyle = valueMap.get('font-style');
|
|
721
|
-
const fontWeight = valueMap.get('font-weight');
|
|
722
|
-
const lineHeight = valueMap.get('line-height');
|
|
723
|
-
if (fontStyle && fontStyle !== 'normal') {
|
|
724
|
-
result.push(fontStyle);
|
|
725
|
-
}
|
|
726
|
-
if (fontWeight && fontWeight !== '400' && fontWeight !== 'normal') {
|
|
727
|
-
result.push(fontWeight);
|
|
728
|
-
}
|
|
729
|
-
if (lineHeight) {
|
|
730
|
-
result.push(fontSize + '/' + lineHeight);
|
|
731
|
-
} else {
|
|
732
|
-
result.push(fontSize);
|
|
733
|
-
}
|
|
734
|
-
result.push(fontFamily);
|
|
735
|
-
return result.join(' ') + importantSuffix;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
if (shorthandName === 'flex') {
|
|
739
|
-
const grow = valueMap.get('flex-grow');
|
|
740
|
-
const shrink = valueMap.get('flex-shrink');
|
|
741
|
-
const basis = valueMap.get('flex-basis');
|
|
742
|
-
if (!grow || !shrink || !basis) {
|
|
743
|
-
return null;
|
|
744
|
-
}
|
|
745
|
-
return [grow, shrink, basis].join(' ') + importantSuffix;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// Build shorthand value
|
|
749
|
-
if (properties.length === 2) {
|
|
750
|
-
// For 2-value shorthands (logical properties)
|
|
751
|
-
if (cleanValues[0] === cleanValues[1]) {
|
|
752
|
-
return cleanValues[0] + importantSuffix;
|
|
753
|
-
}
|
|
754
|
-
return cleanValues.join(' ') + importantSuffix;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
if (properties.length === 4) {
|
|
758
|
-
// For 4-value shorthands (margin, padding, inset, etc.)
|
|
759
|
-
// Collapse redundant values: top right bottom left → fewer values when sides match
|
|
760
|
-
return collapseShorthandParts([...cleanValues]).join(' ') + importantSuffix;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
// For border shorthand (3 values: width, style, color)
|
|
764
|
-
const isBorderLikeShorthand = (
|
|
765
|
-
properties.length === 3 &&
|
|
766
|
-
(properties.includes('border-width') || properties.includes('outline-width')) &&
|
|
767
|
-
properties.some((property) => {
|
|
768
|
-
// Check if one longhand ends with "-style" (e.g. border-style, outline-style)
|
|
769
|
-
return /-style$/.test(property);
|
|
770
|
-
}) &&
|
|
771
|
-
properties.some((property) => {
|
|
772
|
-
// Check if one longhand ends with "-color" (e.g. border-color, outline-color)
|
|
773
|
-
return /-color$/.test(property);
|
|
774
|
-
})
|
|
775
|
-
);
|
|
776
|
-
if (isBorderLikeShorthand) {
|
|
777
|
-
return cleanValues.join(' ') + importantSuffix;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
return null;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
/**
|
|
784
|
-
* Deduplicates, merges, and optimizes CSS declarations within a rule block. Removes overridden longhands, collapses longhands into shorthands, and preserves intentional fallbacks.
|
|
785
|
-
*
|
|
786
|
-
* @param {Array} declarations The array of CSS declaration objects to process.
|
|
787
|
-
* @param {object} context The minification context with registered custom property data.
|
|
788
|
-
* @return {Array} A new array of optimized and reordered declaration objects.
|
|
789
|
-
*/
|
|
790
|
-
function processDeclarations (declarations, context) {
|
|
791
|
-
let result = [];
|
|
792
|
-
|
|
793
|
-
for (let declaration of declarations) {
|
|
79
|
+
for (const declaration of declarations) {
|
|
794
80
|
if (declaration.type === 'rule' || declaration.type === 'media') {
|
|
795
81
|
result.push(declaration);
|
|
796
82
|
continue;
|
|
@@ -805,34 +91,26 @@ function processDeclarations (declarations, context) {
|
|
|
805
91
|
continue;
|
|
806
92
|
}
|
|
807
93
|
|
|
808
|
-
|
|
94
|
+
const minifiedValue = minifyValue(declaration);
|
|
809
95
|
|
|
810
|
-
let previousIndex =
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
previousIndex = i;
|
|
814
|
-
break;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
96
|
+
let previousIndex = findLastIndex(result, (candidate) => {
|
|
97
|
+
return candidate.property === propertyName;
|
|
98
|
+
});
|
|
817
99
|
|
|
818
|
-
//
|
|
100
|
+
// An unprefixed property with the same value also replaces its prefixed form
|
|
819
101
|
let prefixedIndex = -1;
|
|
820
102
|
if (!propertyName.startsWith('-')) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
103
|
+
prefixedIndex = findLastIndex(result, (candidate) => {
|
|
104
|
+
return (
|
|
105
|
+
candidate.property &&
|
|
106
|
+
candidate.property.endsWith(propertyName) &&
|
|
107
|
+
candidate.property.startsWith('-')
|
|
826
108
|
);
|
|
827
|
-
|
|
828
|
-
prefixedIndex = i;
|
|
829
|
-
break;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
109
|
+
});
|
|
832
110
|
}
|
|
833
111
|
|
|
834
112
|
if (prefixedIndex !== -1) {
|
|
835
|
-
|
|
113
|
+
const prefixedValue = minifyValue(result[prefixedIndex]);
|
|
836
114
|
if (minifiedValue === prefixedValue) {
|
|
837
115
|
result.splice(prefixedIndex, 1);
|
|
838
116
|
// Re-adjust previousIndex if we removed an item before it
|
|
@@ -850,19 +128,7 @@ function processDeclarations (declarations, context) {
|
|
|
850
128
|
}
|
|
851
129
|
|
|
852
130
|
// Fallbacks for custom variables or older browser functions should be kept
|
|
853
|
-
|
|
854
|
-
minifiedValue.includes('calc(') ||
|
|
855
|
-
minifiedValue.includes('env(') ||
|
|
856
|
-
minifiedValue.includes('var(') ||
|
|
857
|
-
minifiedValue.includes('-webkit-')
|
|
858
|
-
);
|
|
859
|
-
const previousUsesModernSyntax = (
|
|
860
|
-
previousValue.includes('calc(') ||
|
|
861
|
-
previousValue.includes('env(') ||
|
|
862
|
-
previousValue.includes('var(') ||
|
|
863
|
-
previousValue.includes('-webkit-')
|
|
864
|
-
);
|
|
865
|
-
if (currentUsesModernSyntax && !previousUsesModernSyntax) {
|
|
131
|
+
if (usesModernSyntax(minifiedValue) && !usesModernSyntax(previousValue)) {
|
|
866
132
|
result.push(declaration);
|
|
867
133
|
continue;
|
|
868
134
|
}
|
|
@@ -874,37 +140,115 @@ function processDeclarations (declarations, context) {
|
|
|
874
140
|
result.push(declaration);
|
|
875
141
|
}
|
|
876
142
|
|
|
877
|
-
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
878
145
|
|
|
879
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Removes longhand declarations that appear before a shorthand which resets
|
|
148
|
+
* them, since the shorthand discards whatever the earlier longhand set.
|
|
149
|
+
*
|
|
150
|
+
* @param {Array} declarations The declarations of a single rule, in source order.
|
|
151
|
+
* @return {Array} The declarations without the overridden longhands.
|
|
152
|
+
*/
|
|
153
|
+
function removeLonghandsOverriddenByShorthands (declarations) {
|
|
880
154
|
const propertiesToRemove = new Set();
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
if (declaration.property
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
155
|
+
|
|
156
|
+
declarations.forEach((declaration, shorthandIndex) => {
|
|
157
|
+
if (!declaration.property || !shorthandMap[declaration.property]) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const overridden = getOverriddenLonghands(declaration.property);
|
|
161
|
+
for (const longhandProperty of overridden) {
|
|
162
|
+
const longhandIndex = declarations.findIndex((candidate, index) => {
|
|
163
|
+
return candidate.property === longhandProperty && index < shorthandIndex;
|
|
164
|
+
});
|
|
165
|
+
if (longhandIndex !== -1) {
|
|
166
|
+
propertiesToRemove.add(longhandProperty);
|
|
893
167
|
}
|
|
894
168
|
}
|
|
895
|
-
}
|
|
169
|
+
});
|
|
896
170
|
|
|
897
|
-
|
|
171
|
+
return declarations.filter((declaration) => {
|
|
898
172
|
return !propertiesToRemove.has(declaration.property);
|
|
899
173
|
});
|
|
900
|
-
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Collects the longhand properties that a newly built shorthand replaces. For
|
|
178
|
+
* shorthands that tolerate a mixed `!important` group, the important longhands
|
|
179
|
+
* stay in the output so they keep winning over the shorthand.
|
|
180
|
+
*
|
|
181
|
+
* @param {string} shorthandName The shorthand that was built.
|
|
182
|
+
* @param {Array} mergeableProperties The longhand property names the shorthand covers.
|
|
183
|
+
* @param {Array} relevantDeclarations The declarations the shorthand was built from.
|
|
184
|
+
* @return {Array} The longhand property names to drop.
|
|
185
|
+
*/
|
|
186
|
+
function getReplacedLonghands (shorthandName, mergeableProperties, relevantDeclarations) {
|
|
187
|
+
const importantFlags = relevantDeclarations.map((declaration) => {
|
|
188
|
+
return minifyValue(declaration).includes('!important');
|
|
189
|
+
});
|
|
190
|
+
const hasMixedImportant = (
|
|
191
|
+
importantFlags.includes(true) &&
|
|
192
|
+
importantFlags.includes(false)
|
|
193
|
+
);
|
|
194
|
+
if (!hasMixedImportant || !MIXED_IMPORTANT_SHORTHANDS.has(shorthandName)) {
|
|
195
|
+
return mergeableProperties;
|
|
196
|
+
}
|
|
197
|
+
return mergeableProperties.filter((property) => {
|
|
198
|
+
const declaration = relevantDeclarations.find((candidate) => {
|
|
199
|
+
return candidate.property === property;
|
|
200
|
+
});
|
|
201
|
+
return declaration && !minifyValue(declaration).includes('!important');
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Drops shorthands whose longhands are entirely consumed by a higher-level
|
|
207
|
+
* shorthand built in the same pass. For example, `background-position` (x + y)
|
|
208
|
+
* is redundant once `background` has consumed those same longhands.
|
|
209
|
+
*
|
|
210
|
+
* @param {Array} builtDeclarations The shorthand declarations built in one pass.
|
|
211
|
+
* @return {Array} The shorthand declarations that are not subsumed.
|
|
212
|
+
*/
|
|
213
|
+
function removeSubsumedShorthands (builtDeclarations) {
|
|
214
|
+
return builtDeclarations.filter((declaration) => {
|
|
215
|
+
const longhands = shorthandMap[declaration.property];
|
|
216
|
+
if (!longhands) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
const isSubsumedByOtherShorthand = builtDeclarations.some((other) => {
|
|
220
|
+
if (other === declaration) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
const otherLonghands = shorthandMap[other.property];
|
|
224
|
+
if (!otherLonghands) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
return longhands.every((longhand) => {
|
|
228
|
+
return otherLonghands.includes(longhand);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
return !isSubsumedByOtherShorthand;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Builds every shorthand that the remaining longhands support, repeating until
|
|
237
|
+
* no further shorthand can be created, so that shorthands built from other
|
|
238
|
+
* shorthands (such as `border` from `border-width`) are also collapsed.
|
|
239
|
+
*
|
|
240
|
+
* @param {Array} declarations The declarations of a single rule.
|
|
241
|
+
* @param {object} context The minification context with registered custom property data.
|
|
242
|
+
* @return {Array} The declarations with longhands merged into shorthands.
|
|
243
|
+
*/
|
|
244
|
+
function mergeLonghandsIntoShorthands (declarations, context) {
|
|
245
|
+
let result = declarations;
|
|
246
|
+
let builtShorthand = true;
|
|
901
247
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const mergedProperties = new Set();
|
|
907
|
-
const newDeclarations = [];
|
|
248
|
+
while (builtShorthand) {
|
|
249
|
+
builtShorthand = false;
|
|
250
|
+
const replacedProperties = new Set();
|
|
251
|
+
const builtDeclarations = [];
|
|
908
252
|
|
|
909
253
|
for (const [shorthand, longhands] of Object.entries(shorthandMap)) {
|
|
910
254
|
const shorthandAlreadyExists = result.some((declaration) => {
|
|
@@ -926,69 +270,44 @@ function processDeclarations (declarations, context) {
|
|
|
926
270
|
continue;
|
|
927
271
|
}
|
|
928
272
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
shorthand === 'inset'
|
|
934
|
-
);
|
|
935
|
-
const someAreImportant = relevantDeclarations.some((declaration) => {
|
|
936
|
-
return minifyValue(declaration).includes('!important');
|
|
273
|
+
builtDeclarations.push({
|
|
274
|
+
property: shorthand,
|
|
275
|
+
value: mergedValue,
|
|
276
|
+
isAssembledShorthand: true
|
|
937
277
|
});
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const hasMixedImportant = someAreImportant && !allAreImportant;
|
|
942
|
-
|
|
943
|
-
if (isMarginPaddingInset && hasMixedImportant) {
|
|
944
|
-
for (const property of mergeableProperties) {
|
|
945
|
-
const declaration = relevantDeclarations.find((candidate) => {
|
|
946
|
-
return candidate.property === property;
|
|
947
|
-
});
|
|
948
|
-
if (declaration && !minifyValue(declaration).includes('!important')) {
|
|
949
|
-
mergedProperties.add(property);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
} else {
|
|
953
|
-
for (const property of mergeableProperties) {
|
|
954
|
-
mergedProperties.add(property);
|
|
955
|
-
}
|
|
278
|
+
const replacedLonghands = getReplacedLonghands(shorthand, mergeableProperties, relevantDeclarations);
|
|
279
|
+
for (const property of replacedLonghands) {
|
|
280
|
+
replacedProperties.add(property);
|
|
956
281
|
}
|
|
957
282
|
}
|
|
958
283
|
|
|
959
|
-
if (
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
// For example, background-position (x + y) is redundant when
|
|
963
|
-
// background already consumed those same longhands.
|
|
964
|
-
const filteredDeclarations = newDeclarations.filter((declaration) => {
|
|
965
|
-
const longhands = shorthandMap[declaration.property];
|
|
966
|
-
if (!longhands) {
|
|
967
|
-
return true;
|
|
968
|
-
}
|
|
969
|
-
const isSubsumedByOtherShorthand = newDeclarations.some((other) => {
|
|
970
|
-
if (other === declaration) {
|
|
971
|
-
return false;
|
|
972
|
-
}
|
|
973
|
-
const otherLonghands = shorthandMap[other.property];
|
|
974
|
-
if (!otherLonghands) {
|
|
975
|
-
return false;
|
|
976
|
-
}
|
|
977
|
-
return longhands.every((longhand) => {
|
|
978
|
-
return otherLonghands.includes(longhand);
|
|
979
|
-
});
|
|
980
|
-
});
|
|
981
|
-
return !isSubsumedByOtherShorthand;
|
|
284
|
+
if (builtDeclarations.length) {
|
|
285
|
+
const keptDeclarations = result.filter((declaration) => {
|
|
286
|
+
return !replacedProperties.has(declaration.property);
|
|
982
287
|
});
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
return !mergedProperties.has(declaration.property);
|
|
986
|
-
});
|
|
987
|
-
result = [...result, ...filteredDeclarations];
|
|
988
|
-
changed = true;
|
|
288
|
+
result = [...keptDeclarations, ...removeSubsumedShorthands(builtDeclarations)];
|
|
289
|
+
builtShorthand = true;
|
|
989
290
|
}
|
|
990
291
|
}
|
|
991
292
|
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Deduplicates, merges, and optimizes CSS declarations within a rule block. Removes overridden longhands, collapses longhands into shorthands, and preserves intentional fallbacks.
|
|
298
|
+
*
|
|
299
|
+
* @param {Array} declarations The array of CSS declaration objects to process.
|
|
300
|
+
* @param {object} context The minification context with registered custom property data.
|
|
301
|
+
* @return {Array} A new array of optimized and reordered declaration objects.
|
|
302
|
+
*/
|
|
303
|
+
function processDeclarations (declarations, context) {
|
|
304
|
+
let result = deduplicateDeclarations(declarations);
|
|
305
|
+
result = removeLonghandsOverriddenByShorthands(result);
|
|
306
|
+
result = absorbBackgroundLonghandsIntoShorthand(result);
|
|
307
|
+
result = mergeLonghandsIntoShorthands(result, context);
|
|
308
|
+
result = hoistCssWideKeywordsIntoShorthands(result);
|
|
309
|
+
result = collapseBorderTrioWithPerEdgeColor(result);
|
|
310
|
+
|
|
992
311
|
return orderDeclarations(result);
|
|
993
312
|
}
|
|
994
313
|
|