@thejaredwilcurt/csslop 0.0.19 → 0.0.21
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/package.json +8 -10
- package/src/declarations/background.js +322 -0
- package/src/declarations/border.js +120 -0
- package/src/declarations/config.js +36 -0
- package/src/declarations/merge.js +323 -0
- package/src/declarations/order.js +77 -0
- package/src/declarations/process.js +186 -869
- package/src/declarations/shorthand-values.js +374 -0
- package/src/rules/normalize.js +16 -5
- package/src/rules/selectors.js +71 -8
- package/src/rules/stringify.js +10 -11
- package/src/value/color-mix.js +6 -0
- package/src/value/colors.js +15 -0
- package/src/value/minify.js +149 -31
- package/src/value/shared.js +31 -0
- package/src/value/syntax.js +82 -1
|
@@ -4,793 +4,78 @@
|
|
|
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';
|
|
9
11
|
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
|
-
}
|
|
12
|
+
getMergeProps,
|
|
13
|
+
tryMergeToShorthand
|
|
14
|
+
} from './merge.js';
|
|
15
|
+
import {
|
|
16
|
+
getOverriddenLonghands,
|
|
17
|
+
orderDeclarations
|
|
18
|
+
} from './order.js';
|
|
127
19
|
|
|
128
20
|
/**
|
|
129
|
-
*
|
|
21
|
+
* Shorthands that keep their non-important longhands in the output, so a mixed
|
|
22
|
+
* `!important` group still merges the important longhands into the shorthand.
|
|
130
23
|
*
|
|
131
|
-
* @
|
|
132
|
-
* @return {Array} A deduplicated array of all longhand property names that the shorthand overrides, including nested longhands.
|
|
24
|
+
* @type {Set<string>}
|
|
133
25
|
*/
|
|
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
|
-
}
|
|
26
|
+
const MIXED_IMPORTANT_SHORTHANDS = new Set(['margin', 'padding', 'inset']);
|
|
144
27
|
|
|
145
28
|
/**
|
|
146
|
-
*
|
|
29
|
+
* Functions and syntaxes that older browsers do not understand, so an earlier
|
|
30
|
+
* declaration using only classic syntax is kept as a fallback for them.
|
|
147
31
|
*
|
|
148
|
-
* @
|
|
149
|
-
* @return {boolean} True if the value contains a var() with a fallback comma.
|
|
32
|
+
* @type {Array}
|
|
150
33
|
*/
|
|
151
|
-
|
|
152
|
-
// Match var() containing a comma (indicating a fallback value)
|
|
153
|
-
return /var\([^)]*,/.test(value);
|
|
154
|
-
}
|
|
34
|
+
const MODERN_SYNTAX_MARKERS = ['calc(', 'env(', 'var(', '-webkit-'];
|
|
155
35
|
|
|
156
36
|
/**
|
|
157
|
-
* Determines whether a value
|
|
37
|
+
* Determines whether a value relies on syntax that older browsers cannot parse,
|
|
38
|
+
* which means a preceding declaration for the same property is an intentional
|
|
39
|
+
* fallback rather than a redundant duplicate.
|
|
158
40
|
*
|
|
159
|
-
* @param {string} value
|
|
160
|
-
* @
|
|
161
|
-
* @return {boolean} True if the value is safe to merge into a shorthand.
|
|
41
|
+
* @param {string} value The minified CSS value string.
|
|
42
|
+
* @return {boolean} Whether the value uses modern syntax.
|
|
162
43
|
*/
|
|
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);
|
|
44
|
+
function usesModernSyntax (value) {
|
|
45
|
+
return MODERN_SYNTAX_MARKERS.some((marker) => {
|
|
46
|
+
return value.includes(marker);
|
|
176
47
|
});
|
|
177
48
|
}
|
|
178
49
|
|
|
179
50
|
/**
|
|
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.
|
|
51
|
+
* Finds the index of the last declaration matching a predicate, which is the
|
|
52
|
+
* declaration that wins the cascade within a rule.
|
|
286
53
|
*
|
|
287
|
-
* @param {
|
|
288
|
-
* @
|
|
54
|
+
* @param {Array} declarations The declarations to search.
|
|
55
|
+
* @param {function(object): boolean} predicate Called with each declaration, returning whether it matches.
|
|
56
|
+
* @return {number} The index of the matching declaration, or -1 when absent.
|
|
289
57
|
*/
|
|
290
|
-
function
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
for (const character of value) {
|
|
296
|
-
if (character === '(') {
|
|
297
|
-
parenthesisDepth++;
|
|
298
|
-
} else if (character === ')') {
|
|
299
|
-
parenthesisDepth--;
|
|
58
|
+
function findLastIndex (declarations, predicate) {
|
|
59
|
+
for (let index = declarations.length - 1; index >= 0; index--) {
|
|
60
|
+
if (predicate(declarations[index])) {
|
|
61
|
+
return index;
|
|
300
62
|
}
|
|
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
63
|
}
|
|
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;
|
|
64
|
+
return -1;
|
|
331
65
|
}
|
|
332
66
|
|
|
333
67
|
/**
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
68
|
+
* Removes declarations that a later declaration for the same property makes
|
|
69
|
+
* redundant, including vendor-prefixed duplicates, while keeping intentional
|
|
70
|
+
* fallbacks for values that use modern syntax.
|
|
337
71
|
*
|
|
338
|
-
* @param {
|
|
339
|
-
* @return {
|
|
72
|
+
* @param {Array} declarations The declarations of a single rule, in source order.
|
|
73
|
+
* @return {Array} The surviving declarations, in source order.
|
|
340
74
|
*/
|
|
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
|
-
|
|
75
|
+
function deduplicateDeclarations (declarations) {
|
|
387
76
|
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
77
|
|
|
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) {
|
|
78
|
+
for (const declaration of declarations) {
|
|
794
79
|
if (declaration.type === 'rule' || declaration.type === 'media') {
|
|
795
80
|
result.push(declaration);
|
|
796
81
|
continue;
|
|
@@ -805,34 +90,26 @@ function processDeclarations (declarations, context) {
|
|
|
805
90
|
continue;
|
|
806
91
|
}
|
|
807
92
|
|
|
808
|
-
|
|
93
|
+
const minifiedValue = minifyValue(declaration);
|
|
809
94
|
|
|
810
|
-
let previousIndex =
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
previousIndex = i;
|
|
814
|
-
break;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
95
|
+
let previousIndex = findLastIndex(result, (candidate) => {
|
|
96
|
+
return candidate.property === propertyName;
|
|
97
|
+
});
|
|
817
98
|
|
|
818
|
-
//
|
|
99
|
+
// An unprefixed property with the same value also replaces its prefixed form
|
|
819
100
|
let prefixedIndex = -1;
|
|
820
101
|
if (!propertyName.startsWith('-')) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
102
|
+
prefixedIndex = findLastIndex(result, (candidate) => {
|
|
103
|
+
return (
|
|
104
|
+
candidate.property &&
|
|
105
|
+
candidate.property.endsWith(propertyName) &&
|
|
106
|
+
candidate.property.startsWith('-')
|
|
826
107
|
);
|
|
827
|
-
|
|
828
|
-
prefixedIndex = i;
|
|
829
|
-
break;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
108
|
+
});
|
|
832
109
|
}
|
|
833
110
|
|
|
834
111
|
if (prefixedIndex !== -1) {
|
|
835
|
-
|
|
112
|
+
const prefixedValue = minifyValue(result[prefixedIndex]);
|
|
836
113
|
if (minifiedValue === prefixedValue) {
|
|
837
114
|
result.splice(prefixedIndex, 1);
|
|
838
115
|
// Re-adjust previousIndex if we removed an item before it
|
|
@@ -850,19 +127,7 @@ function processDeclarations (declarations, context) {
|
|
|
850
127
|
}
|
|
851
128
|
|
|
852
129
|
// 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) {
|
|
130
|
+
if (usesModernSyntax(minifiedValue) && !usesModernSyntax(previousValue)) {
|
|
866
131
|
result.push(declaration);
|
|
867
132
|
continue;
|
|
868
133
|
}
|
|
@@ -874,37 +139,115 @@ function processDeclarations (declarations, context) {
|
|
|
874
139
|
result.push(declaration);
|
|
875
140
|
}
|
|
876
141
|
|
|
877
|
-
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
878
144
|
|
|
879
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Removes longhand declarations that appear before a shorthand which resets
|
|
147
|
+
* them, since the shorthand discards whatever the earlier longhand set.
|
|
148
|
+
*
|
|
149
|
+
* @param {Array} declarations The declarations of a single rule, in source order.
|
|
150
|
+
* @return {Array} The declarations without the overridden longhands.
|
|
151
|
+
*/
|
|
152
|
+
function removeLonghandsOverriddenByShorthands (declarations) {
|
|
880
153
|
const propertiesToRemove = new Set();
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
if (declaration.property
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
154
|
+
|
|
155
|
+
declarations.forEach((declaration, shorthandIndex) => {
|
|
156
|
+
if (!declaration.property || !shorthandMap[declaration.property]) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const overridden = getOverriddenLonghands(declaration.property);
|
|
160
|
+
for (const longhandProperty of overridden) {
|
|
161
|
+
const longhandIndex = declarations.findIndex((candidate, index) => {
|
|
162
|
+
return candidate.property === longhandProperty && index < shorthandIndex;
|
|
163
|
+
});
|
|
164
|
+
if (longhandIndex !== -1) {
|
|
165
|
+
propertiesToRemove.add(longhandProperty);
|
|
893
166
|
}
|
|
894
167
|
}
|
|
895
|
-
}
|
|
168
|
+
});
|
|
896
169
|
|
|
897
|
-
|
|
170
|
+
return declarations.filter((declaration) => {
|
|
898
171
|
return !propertiesToRemove.has(declaration.property);
|
|
899
172
|
});
|
|
900
|
-
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Collects the longhand properties that a newly built shorthand replaces. For
|
|
177
|
+
* shorthands that tolerate a mixed `!important` group, the important longhands
|
|
178
|
+
* stay in the output so they keep winning over the shorthand.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} shorthandName The shorthand that was built.
|
|
181
|
+
* @param {Array} mergeableProperties The longhand property names the shorthand covers.
|
|
182
|
+
* @param {Array} relevantDeclarations The declarations the shorthand was built from.
|
|
183
|
+
* @return {Array} The longhand property names to drop.
|
|
184
|
+
*/
|
|
185
|
+
function getReplacedLonghands (shorthandName, mergeableProperties, relevantDeclarations) {
|
|
186
|
+
const importantFlags = relevantDeclarations.map((declaration) => {
|
|
187
|
+
return minifyValue(declaration).includes('!important');
|
|
188
|
+
});
|
|
189
|
+
const hasMixedImportant = (
|
|
190
|
+
importantFlags.includes(true) &&
|
|
191
|
+
importantFlags.includes(false)
|
|
192
|
+
);
|
|
193
|
+
if (!hasMixedImportant || !MIXED_IMPORTANT_SHORTHANDS.has(shorthandName)) {
|
|
194
|
+
return mergeableProperties;
|
|
195
|
+
}
|
|
196
|
+
return mergeableProperties.filter((property) => {
|
|
197
|
+
const declaration = relevantDeclarations.find((candidate) => {
|
|
198
|
+
return candidate.property === property;
|
|
199
|
+
});
|
|
200
|
+
return declaration && !minifyValue(declaration).includes('!important');
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Drops shorthands whose longhands are entirely consumed by a higher-level
|
|
206
|
+
* shorthand built in the same pass. For example, `background-position` (x + y)
|
|
207
|
+
* is redundant once `background` has consumed those same longhands.
|
|
208
|
+
*
|
|
209
|
+
* @param {Array} builtDeclarations The shorthand declarations built in one pass.
|
|
210
|
+
* @return {Array} The shorthand declarations that are not subsumed.
|
|
211
|
+
*/
|
|
212
|
+
function removeSubsumedShorthands (builtDeclarations) {
|
|
213
|
+
return builtDeclarations.filter((declaration) => {
|
|
214
|
+
const longhands = shorthandMap[declaration.property];
|
|
215
|
+
if (!longhands) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
const isSubsumedByOtherShorthand = builtDeclarations.some((other) => {
|
|
219
|
+
if (other === declaration) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
const otherLonghands = shorthandMap[other.property];
|
|
223
|
+
if (!otherLonghands) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
return longhands.every((longhand) => {
|
|
227
|
+
return otherLonghands.includes(longhand);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
return !isSubsumedByOtherShorthand;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Builds every shorthand that the remaining longhands support, repeating until
|
|
236
|
+
* no further shorthand can be created, so that shorthands built from other
|
|
237
|
+
* shorthands (such as `border` from `border-width`) are also collapsed.
|
|
238
|
+
*
|
|
239
|
+
* @param {Array} declarations The declarations of a single rule.
|
|
240
|
+
* @param {object} context The minification context with registered custom property data.
|
|
241
|
+
* @return {Array} The declarations with longhands merged into shorthands.
|
|
242
|
+
*/
|
|
243
|
+
function mergeLonghandsIntoShorthands (declarations, context) {
|
|
244
|
+
let result = declarations;
|
|
245
|
+
let builtShorthand = true;
|
|
901
246
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const mergedProperties = new Set();
|
|
907
|
-
const newDeclarations = [];
|
|
247
|
+
while (builtShorthand) {
|
|
248
|
+
builtShorthand = false;
|
|
249
|
+
const replacedProperties = new Set();
|
|
250
|
+
const builtDeclarations = [];
|
|
908
251
|
|
|
909
252
|
for (const [shorthand, longhands] of Object.entries(shorthandMap)) {
|
|
910
253
|
const shorthandAlreadyExists = result.some((declaration) => {
|
|
@@ -926,69 +269,43 @@ function processDeclarations (declarations, context) {
|
|
|
926
269
|
continue;
|
|
927
270
|
}
|
|
928
271
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
shorthand === 'inset'
|
|
934
|
-
);
|
|
935
|
-
const someAreImportant = relevantDeclarations.some((declaration) => {
|
|
936
|
-
return minifyValue(declaration).includes('!important');
|
|
272
|
+
builtDeclarations.push({
|
|
273
|
+
property: shorthand,
|
|
274
|
+
value: mergedValue,
|
|
275
|
+
isAssembledShorthand: true
|
|
937
276
|
});
|
|
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
|
-
}
|
|
277
|
+
const replacedLonghands = getReplacedLonghands(shorthand, mergeableProperties, relevantDeclarations);
|
|
278
|
+
for (const property of replacedLonghands) {
|
|
279
|
+
replacedProperties.add(property);
|
|
956
280
|
}
|
|
957
281
|
}
|
|
958
282
|
|
|
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;
|
|
283
|
+
if (builtDeclarations.length) {
|
|
284
|
+
const keptDeclarations = result.filter((declaration) => {
|
|
285
|
+
return !replacedProperties.has(declaration.property);
|
|
982
286
|
});
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
return !mergedProperties.has(declaration.property);
|
|
986
|
-
});
|
|
987
|
-
result = [...result, ...filteredDeclarations];
|
|
988
|
-
changed = true;
|
|
287
|
+
result = [...keptDeclarations, ...removeSubsumedShorthands(builtDeclarations)];
|
|
288
|
+
builtShorthand = true;
|
|
989
289
|
}
|
|
990
290
|
}
|
|
991
291
|
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Deduplicates, merges, and optimizes CSS declarations within a rule block. Removes overridden longhands, collapses longhands into shorthands, and preserves intentional fallbacks.
|
|
297
|
+
*
|
|
298
|
+
* @param {Array} declarations The array of CSS declaration objects to process.
|
|
299
|
+
* @param {object} context The minification context with registered custom property data.
|
|
300
|
+
* @return {Array} A new array of optimized and reordered declaration objects.
|
|
301
|
+
*/
|
|
302
|
+
function processDeclarations (declarations, context) {
|
|
303
|
+
let result = deduplicateDeclarations(declarations);
|
|
304
|
+
result = removeLonghandsOverriddenByShorthands(result);
|
|
305
|
+
result = absorbBackgroundLonghandsIntoShorthand(result);
|
|
306
|
+
result = mergeLonghandsIntoShorthands(result, context);
|
|
307
|
+
result = collapseBorderTrioWithPerEdgeColor(result);
|
|
308
|
+
|
|
992
309
|
return orderDeclarations(result);
|
|
993
310
|
}
|
|
994
311
|
|