@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
|
@@ -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
|
+
};
|