@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
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Rewrites longhand declarations that share a CSS-wide keyword into a shorthand carrying that keyword, followed by the longhands that override it.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { minifyValue } from '../value/minify.js';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
CSS_WIDE_KEYWORDS,
|
|
9
|
+
shorthandMap,
|
|
10
|
+
shorthandOverrideMap
|
|
11
|
+
} from './config.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Expands a property into the set of leaf longhands it ultimately sets, so that
|
|
15
|
+
* different groupings of the same box, such as `border-width` and
|
|
16
|
+
* `border-top-width`, can be compared for equivalent coverage.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} property The property name to expand.
|
|
19
|
+
* @param {Set} leafProperties The set collecting the leaf longhand names.
|
|
20
|
+
* @return {Set} The set of leaf longhand property names.
|
|
21
|
+
*/
|
|
22
|
+
function expandToLeafProperties (property, leafProperties = new Set()) {
|
|
23
|
+
const longhands = shorthandMap[property];
|
|
24
|
+
if (!longhands) {
|
|
25
|
+
leafProperties.add(property);
|
|
26
|
+
return leafProperties;
|
|
27
|
+
}
|
|
28
|
+
for (const longhand of longhands) {
|
|
29
|
+
expandToLeafProperties(longhand, leafProperties);
|
|
30
|
+
}
|
|
31
|
+
return leafProperties;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Checks whether a group of longhands sets every leaf longhand that the
|
|
36
|
+
* shorthand sets. A CSS-wide keyword may only move into the shorthand when the
|
|
37
|
+
* group covers all of them, otherwise the keyword would also land on a longhand
|
|
38
|
+
* the author never declared.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} shorthandName The target shorthand property name.
|
|
41
|
+
* @param {Array} properties The longhand property names in the group.
|
|
42
|
+
* @return {boolean} Whether the group covers the whole shorthand.
|
|
43
|
+
*/
|
|
44
|
+
function coversEveryLonghandOfShorthand (shorthandName, properties) {
|
|
45
|
+
const coveredLeaves = new Set();
|
|
46
|
+
for (const property of properties) {
|
|
47
|
+
expandToLeafProperties(property, coveredLeaves);
|
|
48
|
+
}
|
|
49
|
+
return [...expandToLeafProperties(shorthandName)].every((leafProperty) => {
|
|
50
|
+
return coveredLeaves.has(leafProperty);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {object} LonghandEntry
|
|
56
|
+
* @property {object} declaration The original declaration object.
|
|
57
|
+
* @property {number} index The declaration's index within the rule.
|
|
58
|
+
* @property {string} property The longhand property name.
|
|
59
|
+
* @property {string} text The minified `property:value` text.
|
|
60
|
+
* @property {string} value The minified value, without any `!important`.
|
|
61
|
+
* @property {boolean} isImportant Whether the declaration carries `!important`.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Collects the declarations of a rule that set one of a shorthand's longhands,
|
|
66
|
+
* in source order.
|
|
67
|
+
*
|
|
68
|
+
* @param {Array} declarations The declarations of a single rule.
|
|
69
|
+
* @param {string} shorthandName The shorthand whose longhands to collect.
|
|
70
|
+
* @return {Array} The matching longhand entries, in source order.
|
|
71
|
+
*/
|
|
72
|
+
function collectLonghandEntries (declarations, shorthandName) {
|
|
73
|
+
const longhands = shorthandMap[shorthandName];
|
|
74
|
+
const entries = [];
|
|
75
|
+
declarations.forEach((declaration, index) => {
|
|
76
|
+
if (!declaration.property || !longhands.includes(declaration.property)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const minifiedValue = minifyValue(declaration);
|
|
80
|
+
const isImportant = minifiedValue.includes('!important');
|
|
81
|
+
entries.push({
|
|
82
|
+
declaration,
|
|
83
|
+
index,
|
|
84
|
+
property: declaration.property,
|
|
85
|
+
text: declaration.property + ':' + minifiedValue,
|
|
86
|
+
value: minifiedValue.replace('!important', '').trim(),
|
|
87
|
+
isImportant
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
return entries;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Checks whether a group declares the same property more than once, which
|
|
95
|
+
* happens when an intentional fallback was kept. Rewriting such a group would
|
|
96
|
+
* drop one of the two declarations, so it is left alone.
|
|
97
|
+
*
|
|
98
|
+
* @param {Array} entries The longhand entries of the group.
|
|
99
|
+
* @return {boolean} Whether any property appears more than once.
|
|
100
|
+
*/
|
|
101
|
+
function hasRepeatedProperty (entries) {
|
|
102
|
+
const seenProperties = new Set();
|
|
103
|
+
return entries.some((entry) => {
|
|
104
|
+
if (seenProperties.has(entry.property)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
seenProperties.add(entry.property);
|
|
108
|
+
return false;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolves the `!important` suffix the shorthand must carry. A group that mixes
|
|
114
|
+
* important and normal longhands cannot be rewritten, because the shorthand
|
|
115
|
+
* would either lose or gain priority over the longhands it replaces.
|
|
116
|
+
*
|
|
117
|
+
* @param {Array} entries The longhand entries of the group.
|
|
118
|
+
* @return {string|null} The suffix to append, or null when the group cannot be rewritten.
|
|
119
|
+
*/
|
|
120
|
+
function resolveImportantSuffix (entries) {
|
|
121
|
+
const allImportant = entries.every((entry) => {
|
|
122
|
+
return entry.isImportant;
|
|
123
|
+
});
|
|
124
|
+
if (allImportant) {
|
|
125
|
+
return '!important';
|
|
126
|
+
}
|
|
127
|
+
const noneImportant = entries.every((entry) => {
|
|
128
|
+
return !entry.isImportant;
|
|
129
|
+
});
|
|
130
|
+
if (noneImportant) {
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolves the single CSS-wide keyword shared by the longhands that use one.
|
|
138
|
+
* A group with no keyword has nothing to hoist, and a group mixing different
|
|
139
|
+
* keywords has no single value the shorthand could carry.
|
|
140
|
+
*
|
|
141
|
+
* @param {Array} entries The longhand entries of the group.
|
|
142
|
+
* @return {string|null} The shared keyword, or null when there is none.
|
|
143
|
+
*/
|
|
144
|
+
function resolveSharedKeyword (entries) {
|
|
145
|
+
const keywords = entries.filter((entry) => {
|
|
146
|
+
return CSS_WIDE_KEYWORDS.has(entry.value.toLowerCase());
|
|
147
|
+
}).map((entry) => {
|
|
148
|
+
return entry.value.toLowerCase();
|
|
149
|
+
});
|
|
150
|
+
if (!keywords.length) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const isSharedByAll = keywords.every((keyword) => {
|
|
154
|
+
return keyword === keywords[0];
|
|
155
|
+
});
|
|
156
|
+
if (!isSharedByAll) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return keywords[0];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Checks whether a property the shorthand also resets, such as `border-image`
|
|
164
|
+
* for `border`, is declared before the point where the shorthand would be
|
|
165
|
+
* inserted. Inserting the shorthand there would discard that declaration.
|
|
166
|
+
*
|
|
167
|
+
* @param {Array} declarations The declarations of a single rule.
|
|
168
|
+
* @param {string} shorthandName The target shorthand property name.
|
|
169
|
+
* @param {number} insertionIndex The index the shorthand would be inserted at.
|
|
170
|
+
* @return {boolean} Whether an earlier declaration would be discarded.
|
|
171
|
+
*/
|
|
172
|
+
function resetsEarlierDeclaration (declarations, shorthandName, insertionIndex) {
|
|
173
|
+
const resetProperties = shorthandOverrideMap[shorthandName] || [];
|
|
174
|
+
if (!resetProperties.length) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
return declarations.slice(0, insertionIndex).some((declaration) => {
|
|
178
|
+
return resetProperties.includes(declaration.property);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Rewrites one longhand group into a shorthand holding the shared CSS-wide
|
|
184
|
+
* keyword, followed by the longhands that override it, when that is shorter
|
|
185
|
+
* than the group of longhands it replaces.
|
|
186
|
+
*
|
|
187
|
+
* @param {Array} declarations The declarations of a single rule.
|
|
188
|
+
* @param {string} shorthandName The target shorthand property name.
|
|
189
|
+
* @return {Array|null} The rewritten declarations, or null when the rewrite does not apply.
|
|
190
|
+
*/
|
|
191
|
+
function rewriteGroupAsKeywordShorthand (declarations, shorthandName) {
|
|
192
|
+
const shorthandAlreadyExists = declarations.some((declaration) => {
|
|
193
|
+
return declaration.property === shorthandName;
|
|
194
|
+
});
|
|
195
|
+
if (shorthandAlreadyExists) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const entries = collectLonghandEntries(declarations, shorthandName);
|
|
200
|
+
if (entries.length < 2 || hasRepeatedProperty(entries)) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const properties = entries.map((entry) => {
|
|
204
|
+
return entry.property;
|
|
205
|
+
});
|
|
206
|
+
if (!coversEveryLonghandOfShorthand(shorthandName, properties)) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const importantSuffix = resolveImportantSuffix(entries);
|
|
211
|
+
const sharedKeyword = resolveSharedKeyword(entries);
|
|
212
|
+
if (importantSuffix === null || !sharedKeyword) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const insertionIndex = entries[0].index;
|
|
217
|
+
if (resetsEarlierDeclaration(declarations, shorthandName, insertionIndex)) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// The longhands whose value is not the shared keyword have to be restated
|
|
222
|
+
// after the shorthand, because the shorthand also set them to the keyword.
|
|
223
|
+
const overrideEntries = entries.filter((entry) => {
|
|
224
|
+
return entry.value.toLowerCase() !== sharedKeyword;
|
|
225
|
+
});
|
|
226
|
+
const shorthandDeclaration = {
|
|
227
|
+
property: shorthandName,
|
|
228
|
+
value: sharedKeyword + importantSuffix,
|
|
229
|
+
isAssembledShorthand: true
|
|
230
|
+
};
|
|
231
|
+
const rewrittenLength = [shorthandDeclaration.property + ':' + shorthandDeclaration.value, ...overrideEntries.map((entry) => {
|
|
232
|
+
return entry.text;
|
|
233
|
+
})].join(';').length;
|
|
234
|
+
const longhandLength = entries.map((entry) => {
|
|
235
|
+
return entry.text;
|
|
236
|
+
}).join(';').length;
|
|
237
|
+
if (rewrittenLength >= longhandLength) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const groupIndexes = new Set(entries.map((entry) => {
|
|
242
|
+
return entry.index;
|
|
243
|
+
}));
|
|
244
|
+
const result = [];
|
|
245
|
+
declarations.forEach((declaration, index) => {
|
|
246
|
+
if (index === insertionIndex) {
|
|
247
|
+
result.push(shorthandDeclaration);
|
|
248
|
+
for (const entry of overrideEntries) {
|
|
249
|
+
result.push(entry.declaration);
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (groupIndexes.has(index)) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
result.push(declaration);
|
|
257
|
+
});
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Hoists a CSS-wide keyword such as `inherit` out of a group of longhands into
|
|
263
|
+
* their shorthand. A CSS-wide keyword is only valid as a declaration's whole
|
|
264
|
+
* value, so `border-style:inherit;border-color:inherit;border-width:2px` cannot
|
|
265
|
+
* become `border:2px inherit inherit`. It can however become `border:inherit`
|
|
266
|
+
* followed by `border-width:2px`, which inherits every border property and then
|
|
267
|
+
* overrides the one that differs.
|
|
268
|
+
*
|
|
269
|
+
* @param {Array} declarations The declarations of a single rule.
|
|
270
|
+
* @return {Array} The declarations, with eligible groups rewritten.
|
|
271
|
+
*/
|
|
272
|
+
function hoistCssWideKeywordsIntoShorthands (declarations) {
|
|
273
|
+
let result = declarations;
|
|
274
|
+
// Shorthands are visited in declaration order, so the widest shorthand of a
|
|
275
|
+
// family is rewritten before the narrower shorthands it contains.
|
|
276
|
+
for (const shorthandName of Object.keys(shorthandMap)) {
|
|
277
|
+
const rewritten = rewriteGroupAsKeywordShorthand(result, shorthandName);
|
|
278
|
+
if (rewritten) {
|
|
279
|
+
result = rewritten;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export { hoistCssWideKeywordsIntoShorthands };
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Decides whether a group of longhand declarations may collapse into a shorthand and produces the merged value when it is safe.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { minifyValue } from '../value/minify.js';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
BORDER_EDGE_PROPERTIES,
|
|
9
|
+
CSS_WIDE_KEYWORDS,
|
|
10
|
+
EDGE_SHORTHANDS,
|
|
11
|
+
shorthandMap,
|
|
12
|
+
shorthandOverrideMap
|
|
13
|
+
} from './config.js';
|
|
14
|
+
import { buildShorthandValue } from './shorthand-values.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Shorthands that may still be built when only some of their longhands carry
|
|
18
|
+
* `!important`, because the remaining longhands stay in the output.
|
|
19
|
+
*
|
|
20
|
+
* @type {Set<string>}
|
|
21
|
+
*/
|
|
22
|
+
const MIXED_IMPORTANT_SHORTHANDS = new Set(['margin', 'padding', 'inset', 'position-try']);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 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.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} shorthand The CSS shorthand property name.
|
|
28
|
+
* @param {Array} longhands The expected longhand property names for this shorthand.
|
|
29
|
+
* @param {Array} declarations The current array of CSS declaration objects.
|
|
30
|
+
* @return {Array|null} The list of longhand names to merge, or null if merging is not possible.
|
|
31
|
+
*/
|
|
32
|
+
function getMergeProps (shorthand, longhands, declarations) {
|
|
33
|
+
const presentLonghands = longhands.filter((longhand) => {
|
|
34
|
+
return declarations.some((declaration) => {
|
|
35
|
+
return declaration.property === longhand;
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
if (presentLonghands.length === 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (shorthand === 'font') {
|
|
42
|
+
const hasRequiredFontProps = presentLonghands.includes('font-size') && presentLonghands.includes('font-family');
|
|
43
|
+
if (hasRequiredFontProps) {
|
|
44
|
+
return presentLonghands;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (shorthand === 'background-position') {
|
|
49
|
+
const hasBothAxes = presentLonghands.includes('background-position-x') && presentLonghands.includes('background-position-y');
|
|
50
|
+
if (hasBothAxes) {
|
|
51
|
+
return presentLonghands;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (shorthand === 'background') {
|
|
56
|
+
const hasBackgroundProp = presentLonghands.includes('background-color') || presentLonghands.includes('background-image');
|
|
57
|
+
if (hasBackgroundProp) {
|
|
58
|
+
return presentLonghands;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (shorthand === 'mask') {
|
|
63
|
+
if (presentLonghands.includes('mask-image')) {
|
|
64
|
+
return presentLonghands;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (shorthand === 'border-image') {
|
|
69
|
+
if (presentLonghands.includes('border-image-source')) {
|
|
70
|
+
return presentLonghands;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (shorthand === 'border') {
|
|
75
|
+
const hasAllBorderParts = (
|
|
76
|
+
presentLonghands.includes('border-width') &&
|
|
77
|
+
presentLonghands.includes('border-style') &&
|
|
78
|
+
presentLonghands.includes('border-color')
|
|
79
|
+
);
|
|
80
|
+
if (hasAllBorderParts) {
|
|
81
|
+
return ['border-width', 'border-style', 'border-color'];
|
|
82
|
+
}
|
|
83
|
+
const hasAllBorderEdges = BORDER_EDGE_PROPERTIES.every((edgeProperty) => {
|
|
84
|
+
return presentLonghands.includes(edgeProperty);
|
|
85
|
+
});
|
|
86
|
+
if (hasAllBorderEdges) {
|
|
87
|
+
return [...BORDER_EDGE_PROPERTIES];
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (shorthand === 'flex') {
|
|
92
|
+
const hasAllFlexParts = (
|
|
93
|
+
presentLonghands.includes('flex-grow') &&
|
|
94
|
+
presentLonghands.includes('flex-shrink') &&
|
|
95
|
+
presentLonghands.includes('flex-basis')
|
|
96
|
+
);
|
|
97
|
+
if (hasAllFlexParts) {
|
|
98
|
+
return ['flex-grow', 'flex-shrink', 'flex-basis'];
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (presentLonghands.length === longhands.length) {
|
|
103
|
+
return longhands;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if a value contains var() - don't merge if it does (safest approach).
|
|
110
|
+
*
|
|
111
|
+
* @param {string} value The minified CSS value string to check.
|
|
112
|
+
* @return {boolean} True if the value contains a var() with a fallback comma.
|
|
113
|
+
*/
|
|
114
|
+
function hasVarFallback (value) {
|
|
115
|
+
// Match var() containing a comma (indicating a fallback value)
|
|
116
|
+
return /var\([^)]*,/.test(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Determines whether a value containing var() references can safely be merged into a shorthand. Values with fallback commas or unregistered custom properties are not mergeable.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} value The minified CSS value string to check.
|
|
123
|
+
* @param {object} context The minification context with registered custom property data.
|
|
124
|
+
* @return {boolean} True if the value is safe to merge into a shorthand.
|
|
125
|
+
*/
|
|
126
|
+
function canMergeVarValue (value, context) {
|
|
127
|
+
// Check if the value contains any var() reference
|
|
128
|
+
const containsVar = /var\(/.test(value);
|
|
129
|
+
if (!containsVar || hasVarFallback(value)) {
|
|
130
|
+
return !hasVarFallback(value);
|
|
131
|
+
}
|
|
132
|
+
// Extract all var() references with their custom property names
|
|
133
|
+
const matches = [...value.matchAll(/var\((--[A-Za-z0-9_-]+)\)/g)];
|
|
134
|
+
if (!matches.length) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return matches.every(([, propertyName]) => {
|
|
138
|
+
return context.registeredCustomProperties.has(propertyName);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Checks whether a shorthand affects nothing beyond the longhands being merged.
|
|
144
|
+
* When a shorthand also resets unrelated properties (for example `border` resets
|
|
145
|
+
* `border-image`), replacing the longhands with a bare shorthand value would
|
|
146
|
+
* change the rendered result.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} shorthandName The target shorthand property name.
|
|
149
|
+
* @param {Array} properties The longhand property names being merged.
|
|
150
|
+
* @return {boolean} True when the shorthand only affects the merged longhands.
|
|
151
|
+
*/
|
|
152
|
+
function shorthandAffectsOnlyMergedLonghands (shorthandName, properties) {
|
|
153
|
+
const overrides = shorthandOverrideMap[shorthandName] || [];
|
|
154
|
+
if (overrides.length) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const longhands = shorthandMap[shorthandName] || [];
|
|
158
|
+
return longhands.every((longhand) => {
|
|
159
|
+
return properties.includes(longhand);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolves how a set of longhand values that contain CSS-wide keywords such as
|
|
165
|
+
* `inherit` may be merged. A CSS-wide keyword is only valid as a declaration's
|
|
166
|
+
* entire value, so it can never appear as one component of a shorthand value:
|
|
167
|
+
* `border-width:0;border-style:inherit;border-color:inherit` cannot become
|
|
168
|
+
* `border:0 inherit inherit`. Only a set where every longhand carries the same
|
|
169
|
+
* keyword can merge, and then only into a shorthand that affects nothing else.
|
|
170
|
+
*
|
|
171
|
+
* @param {Array} values The cleaned longhand values, in longhand order.
|
|
172
|
+
* @param {string} shorthandName The target shorthand property name.
|
|
173
|
+
* @param {Array} properties The longhand property names being merged.
|
|
174
|
+
* @return {string|null} The shared keyword to use as the whole shorthand value, or null when merging is unsafe.
|
|
175
|
+
*/
|
|
176
|
+
function resolveCssWideKeywordMerge (values, shorthandName, properties) {
|
|
177
|
+
const normalizedValues = values.map((value) => {
|
|
178
|
+
return value.toLowerCase();
|
|
179
|
+
});
|
|
180
|
+
const sharedKeyword = normalizedValues[0];
|
|
181
|
+
const isSharedByAll = normalizedValues.every((value) => {
|
|
182
|
+
return value === sharedKeyword;
|
|
183
|
+
});
|
|
184
|
+
if (!isSharedByAll || !shorthandAffectsOnlyMergedLonghands(shorthandName, properties)) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return sharedKeyword;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Collects the minified value of every longhand being merged, in shorthand
|
|
192
|
+
* order. Returns null when any longhand is missing a declaration.
|
|
193
|
+
*
|
|
194
|
+
* @param {Array} properties The longhand property names to merge.
|
|
195
|
+
* @param {Array} declarations The CSS declaration objects to draw values from.
|
|
196
|
+
* @return {Array|null} The minified longhand values, or null when one is missing.
|
|
197
|
+
*/
|
|
198
|
+
function collectLonghandValues (properties, declarations) {
|
|
199
|
+
const values = properties.map((property) => {
|
|
200
|
+
const declaration = declarations.find((candidate) => {
|
|
201
|
+
return candidate.property === property;
|
|
202
|
+
});
|
|
203
|
+
if (declaration) {
|
|
204
|
+
return minifyValue(declaration);
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
});
|
|
208
|
+
const hasNullValue = values.some((value) => {
|
|
209
|
+
return value === null;
|
|
210
|
+
});
|
|
211
|
+
if (hasNullValue) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return values;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Resolves the `!important` suffix for a merged shorthand. A shorthand only
|
|
219
|
+
* keeps `!important` when every longhand carried it, and a mixed set is only
|
|
220
|
+
* allowed for shorthands whose remaining important longhands stay in the output.
|
|
221
|
+
*
|
|
222
|
+
* @param {Array} values The minified longhand values.
|
|
223
|
+
* @param {string} shorthandName The target shorthand property name.
|
|
224
|
+
* @return {string|null} The suffix to append, or null when the mix forbids merging.
|
|
225
|
+
*/
|
|
226
|
+
function resolveImportantSuffix (values, shorthandName) {
|
|
227
|
+
const importantFlags = values.map((value) => {
|
|
228
|
+
return value.includes('!important');
|
|
229
|
+
});
|
|
230
|
+
const allImportant = importantFlags.every((flag) => {
|
|
231
|
+
return flag;
|
|
232
|
+
});
|
|
233
|
+
const noneImportant = importantFlags.every((flag) => {
|
|
234
|
+
return !flag;
|
|
235
|
+
});
|
|
236
|
+
if (allImportant) {
|
|
237
|
+
return '!important';
|
|
238
|
+
}
|
|
239
|
+
if (noneImportant || MIXED_IMPORTANT_SHORTHANDS.has(shorthandName)) {
|
|
240
|
+
return '';
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Try to merge longhand properties into a shorthand.
|
|
247
|
+
*
|
|
248
|
+
* @param {Array} properties The longhand property names to merge.
|
|
249
|
+
* @param {Array} declarations The CSS declaration objects to draw values from.
|
|
250
|
+
* @param {string} shorthandName The target shorthand property name.
|
|
251
|
+
* @param {object} context The minification context with registered custom property data.
|
|
252
|
+
* @return {string|null} The merged shorthand value string, or null if merging is not possible.
|
|
253
|
+
*/
|
|
254
|
+
function tryMergeToShorthand (properties, declarations, shorthandName = '', context) {
|
|
255
|
+
if (properties.length < 2) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const values = collectLonghandValues(properties, declarations);
|
|
260
|
+
if (!values) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Don't merge if any value has var() with fallback or unknown custom properties
|
|
265
|
+
const hasUnmergeableVar = values.some((value) => {
|
|
266
|
+
return !canMergeVarValue(value, context);
|
|
267
|
+
});
|
|
268
|
+
if (hasUnmergeableVar) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const importantSuffix = resolveImportantSuffix(values, shorthandName);
|
|
273
|
+
if (importantSuffix === null) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const cleanValues = values.map((value) => {
|
|
278
|
+
return value
|
|
279
|
+
.replace('!important', '')
|
|
280
|
+
.trim();
|
|
281
|
+
});
|
|
282
|
+
const valueMap = new Map(properties.map((property, index) => {
|
|
283
|
+
return [property, cleanValues[index]];
|
|
284
|
+
}));
|
|
285
|
+
|
|
286
|
+
const usesCssWideKeyword = cleanValues.some((value) => {
|
|
287
|
+
return CSS_WIDE_KEYWORDS.has(value.toLowerCase());
|
|
288
|
+
});
|
|
289
|
+
if (usesCssWideKeyword) {
|
|
290
|
+
const sharedKeyword = resolveCssWideKeywordMerge(cleanValues, shorthandName, properties);
|
|
291
|
+
if (!sharedKeyword) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return sharedKeyword + importantSuffix;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Each edge shorthand holds a complete width/style/color value, so the edges
|
|
298
|
+
// only collapse into their parent shorthand when they are all identical.
|
|
299
|
+
const mergesEdgeShorthands = properties.every((property) => {
|
|
300
|
+
return EDGE_SHORTHANDS.has(property);
|
|
301
|
+
});
|
|
302
|
+
if (mergesEdgeShorthands) {
|
|
303
|
+
const allEdgesMatch = cleanValues.every((value) => {
|
|
304
|
+
return value === cleanValues[0];
|
|
305
|
+
});
|
|
306
|
+
if (!allEdgesMatch) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return cleanValues[0] + importantSuffix;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return buildShorthandValue(shorthandName, {
|
|
313
|
+
properties,
|
|
314
|
+
valueMap,
|
|
315
|
+
cleanValues,
|
|
316
|
+
importantSuffix
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export {
|
|
321
|
+
getMergeProps,
|
|
322
|
+
tryMergeToShorthand
|
|
323
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Orders declarations within a rule so that shorthands stay ahead of the longhands they reset.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
shorthandMap,
|
|
7
|
+
shorthandOverrideMap
|
|
8
|
+
} from './config.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Pairs of properties where the first must be emitted before the second, because
|
|
12
|
+
* the first resets the second and a merged shorthand is appended after the
|
|
13
|
+
* longhands it was built from.
|
|
14
|
+
*
|
|
15
|
+
* @type {Array}
|
|
16
|
+
*/
|
|
17
|
+
const REQUIRED_PROPERTY_ORDER = [
|
|
18
|
+
['border', 'border-image'],
|
|
19
|
+
['font', 'font-feature-settings'],
|
|
20
|
+
['font', 'font-variant-ligatures'],
|
|
21
|
+
['font', 'font-kerning'],
|
|
22
|
+
['font', 'font-variation-settings'],
|
|
23
|
+
['mask', 'mask-border'],
|
|
24
|
+
['margin', 'margin-top'],
|
|
25
|
+
['margin', 'margin-right'],
|
|
26
|
+
['margin', 'margin-bottom'],
|
|
27
|
+
['margin', 'margin-left']
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Reorders declarations so that shorthands appear before any related longhands they would override, preventing cascade issues in the minified output.
|
|
32
|
+
*
|
|
33
|
+
* @param {Array} declarations The array of CSS declaration objects to reorder.
|
|
34
|
+
* @return {Array} A new array with declarations in the corrected order.
|
|
35
|
+
*/
|
|
36
|
+
function orderDeclarations (declarations) {
|
|
37
|
+
const ordered = [...declarations];
|
|
38
|
+
const findPropertyIndex = (property) => {
|
|
39
|
+
return ordered.findIndex((declaration) => {
|
|
40
|
+
return declaration?.property === property;
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
for (const [property, followingProperty] of REQUIRED_PROPERTY_ORDER) {
|
|
45
|
+
const fromIndex = findPropertyIndex(property);
|
|
46
|
+
const toIndex = findPropertyIndex(followingProperty);
|
|
47
|
+
if (fromIndex === -1 || toIndex === -1 || fromIndex < toIndex) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const [movedDeclaration] = ordered.splice(fromIndex, 1);
|
|
51
|
+
ordered.splice(toIndex, 0, movedDeclaration);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return ordered;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get all longhands that a shorthand would override.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} shorthandProperty The CSS shorthand property name.
|
|
61
|
+
* @return {Array} A deduplicated array of all longhand property names that the shorthand overrides, including nested longhands.
|
|
62
|
+
*/
|
|
63
|
+
function getOverriddenLonghands (shorthandProperty) {
|
|
64
|
+
const direct = shorthandMap[shorthandProperty] || [];
|
|
65
|
+
const overrides = shorthandOverrideMap[shorthandProperty] || [];
|
|
66
|
+
const all = [...direct, ...overrides];
|
|
67
|
+
for (const property of direct) {
|
|
68
|
+
const nested = shorthandMap[property] || [];
|
|
69
|
+
all.push(...nested);
|
|
70
|
+
}
|
|
71
|
+
return [...new Set(all)];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
getOverriddenLonghands,
|
|
76
|
+
orderDeclarations
|
|
77
|
+
};
|