@thejaredwilcurt/csslop 0.0.25 → 0.0.27
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 +1 -1
- package/package.json +2 -2
- package/src/context.js +4 -3
- package/src/declarations/border.js +12 -3
- package/src/declarations/config.js +97 -3
- package/src/declarations/css-wide-keywords.js +17 -64
- package/src/declarations/fold.js +342 -0
- package/src/declarations/lookup.js +37 -3
- package/src/declarations/merge.js +37 -29
- package/src/declarations/order.js +34 -15
- package/src/declarations/process.js +11 -2
- package/src/declarations/reset-hazards.js +108 -0
- package/src/declarations/shorthand-values.js +23 -0
- package/src/index.js +5 -0
- package/src/position-try.js +3 -1
- package/src/rules/property.js +177 -0
- package/src/rules/stringify.js +2 -26
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* Gemini 3.1 Pro (High Thinking)
|
|
32
32
|
* GPT-5.4 High (Thinking)
|
|
33
33
|
|
|
34
|
-
These were the latest and greatest models at the time.
|
|
34
|
+
These were the latest and greatest models at the time. Since then I've mostly used the latest Claude version for maintenance.
|
|
35
35
|
|
|
36
36
|
**AI's used during maintenance:**
|
|
37
37
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@thejaredwilcurt/csslop",
|
|
3
3
|
"main": "index.js",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.27",
|
|
6
6
|
"description": "Experimental CSS minification",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"prestart": "node ./scripts/prestart.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@eslint/js": "^10.0.1",
|
|
32
32
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
33
33
|
"codemirror": "^6.0.2",
|
|
34
|
-
"eslint": "^10.9.
|
|
34
|
+
"eslint": "^10.9.1",
|
|
35
35
|
"eslint-config-tjw-base": "^5.0.0",
|
|
36
36
|
"eslint-config-tjw-import-x": "^1.0.1",
|
|
37
37
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
package/src/context.js
CHANGED
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
import { isUnicodeCompatibleCharset } from './charset.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Creates a fresh minification context used to track `@property`-registered custom properties
|
|
8
|
+
* Creates a fresh minification context used to track `@property`-registered custom properties, their declared syntax types, and the properties that a newly assembled shorthand must not silently reset, across the entire stylesheet.
|
|
9
9
|
*
|
|
10
|
-
* @return {object} A context object with a registeredCustomProperties Set and a
|
|
10
|
+
* @return {object} A context object with a registeredCustomProperties Set, a registeredCustomPropertySyntax Map, and a stylesheetResetProperties Set.
|
|
11
11
|
*/
|
|
12
12
|
function createMinifyContext () {
|
|
13
13
|
return {
|
|
14
14
|
registeredCustomProperties: new Set(),
|
|
15
|
-
registeredCustomPropertySyntax: new Map()
|
|
15
|
+
registeredCustomPropertySyntax: new Map(),
|
|
16
|
+
stylesheetResetProperties: new Set()
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -6,6 +6,8 @@ import { minifyValue } from '../value/minify.js';
|
|
|
6
6
|
import { splitTopLevelComponents } from '../value/syntax.js';
|
|
7
7
|
|
|
8
8
|
import { CSS_WIDE_KEYWORDS } from './config.js';
|
|
9
|
+
import { collectDeclaredProperties } from './lookup.js';
|
|
10
|
+
import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
|
|
9
11
|
|
|
10
12
|
const BORDER_TRIO_PROPERTIES = ['border-width', 'border-style', 'border-color'];
|
|
11
13
|
|
|
@@ -32,10 +34,17 @@ function findLastDeclarationIndex (declarations, property) {
|
|
|
32
34
|
* by a `border-color` override that restores the per-edge colors. The rewrite is
|
|
33
35
|
* only applied when the resulting pair is shorter than the three longhands.
|
|
34
36
|
*
|
|
35
|
-
* @param {Array}
|
|
36
|
-
* @
|
|
37
|
+
* @param {Array} declarations The declarations of a single rule.
|
|
38
|
+
* @param {object} context The minification context with the stylesheet's reset properties.
|
|
39
|
+
* @return {Array} The declarations, with the trio rewritten when it is shorter.
|
|
37
40
|
*/
|
|
38
|
-
function collapseBorderTrioWithPerEdgeColor (declarations) {
|
|
41
|
+
function collapseBorderTrioWithPerEdgeColor (declarations, context) {
|
|
42
|
+
// The `border` shorthand also resets `border-image`, so the trio stays as it
|
|
43
|
+
// is when another rule of the stylesheet relies on that value.
|
|
44
|
+
if (resetsPropertyDeclaredElsewhere('border', collectDeclaredProperties(declarations), context)) {
|
|
45
|
+
return declarations;
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
const trioIndexes = BORDER_TRIO_PROPERTIES.map((property) => {
|
|
40
49
|
return findLastDeclarationIndex(declarations, property);
|
|
41
50
|
});
|
|
@@ -2,6 +2,46 @@
|
|
|
2
2
|
* @file Defines lookup tables mapping CSS shorthand properties to their constituent longhand properties and override relationships.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The gap decoration rule components that CSS Gaps 1 defines once per
|
|
7
|
+
* direction, as a `column-rule-*` and a `row-rule-*` longhand.
|
|
8
|
+
*
|
|
9
|
+
* @type {Array}
|
|
10
|
+
*/
|
|
11
|
+
const GAP_DECORATION_RULE_COMPONENTS = [
|
|
12
|
+
'width',
|
|
13
|
+
'style',
|
|
14
|
+
'color',
|
|
15
|
+
'break',
|
|
16
|
+
'visibility-items',
|
|
17
|
+
'inset-cap-start',
|
|
18
|
+
'inset-cap-end',
|
|
19
|
+
'inset-junction-start',
|
|
20
|
+
'inset-junction-end'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Builds the bidirectional `rule-*` gap decoration shorthands, each of which
|
|
25
|
+
* applies a single value to both the column and the row longhand of one gap
|
|
26
|
+
* decoration component.
|
|
27
|
+
*
|
|
28
|
+
* @return {object} A lookup of shorthand name to its column and row longhands.
|
|
29
|
+
*/
|
|
30
|
+
function createBidirectionalGapRuleShorthands () {
|
|
31
|
+
const shorthands = {};
|
|
32
|
+
for (const component of GAP_DECORATION_RULE_COMPONENTS) {
|
|
33
|
+
shorthands['rule-' + component] = ['column-rule-' + component, 'row-rule-' + component];
|
|
34
|
+
}
|
|
35
|
+
return shorthands;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The bidirectional gap decoration shorthands, keyed by shorthand name.
|
|
40
|
+
*
|
|
41
|
+
* @type {object}
|
|
42
|
+
*/
|
|
43
|
+
const BIDIRECTIONAL_GAP_RULE_SHORTHANDS = createBidirectionalGapRuleShorthands();
|
|
44
|
+
|
|
5
45
|
const shorthandMap = {
|
|
6
46
|
margin: ['margin-top', 'margin-right', 'margin-bottom', 'margin-left'],
|
|
7
47
|
padding: ['padding-top', 'padding-right', 'padding-bottom', 'padding-left'],
|
|
@@ -42,12 +82,15 @@ const shorthandMap = {
|
|
|
42
82
|
animation: ['animation-name', 'animation-duration', 'animation-timing-function', 'animation-delay', 'animation-iteration-count', 'animation-direction', 'animation-fill-mode', 'animation-play-state'],
|
|
43
83
|
mask: ['mask-image', 'mask-repeat', 'mask-size'],
|
|
44
84
|
'position-try': ['position-try-order', 'position-try-fallbacks'],
|
|
45
|
-
font: ['font-style', 'font-weight', 'font-size', 'line-height', 'font-family']
|
|
85
|
+
font: ['font-style', 'font-weight', 'font-size', 'line-height', 'font-family'],
|
|
86
|
+
marker: ['marker-start', 'marker-mid', 'marker-end'],
|
|
87
|
+
...BIDIRECTIONAL_GAP_RULE_SHORTHANDS
|
|
46
88
|
};
|
|
47
89
|
|
|
48
90
|
const shorthandOverrideMap = {
|
|
91
|
+
animation: ['animation-timeline', 'animation-range', 'animation-range-start', 'animation-range-end'],
|
|
49
92
|
border: ['border-image', 'border-image-source', 'border-image-slice', 'border-image-width', 'border-image-outset', 'border-image-repeat'],
|
|
50
|
-
font: ['font-variant', 'font-variant-alternates', 'font-variant-caps', 'font-variant-east-asian', 'font-variant-ligatures', 'font-variant-numeric', 'font-variant-position'],
|
|
93
|
+
font: ['font-variant', 'font-variant-alternates', 'font-variant-caps', 'font-variant-east-asian', 'font-variant-ligatures', 'font-variant-numeric', 'font-variant-position', 'font-feature-settings', 'font-kerning', 'font-language-override', 'font-optical-sizing', 'font-size-adjust', 'font-variation-settings'],
|
|
51
94
|
mask: ['mask-border', 'mask-border-source', 'mask-border-slice', 'mask-border-width', 'mask-border-outset', 'mask-border-repeat', 'mask-border-mode']
|
|
52
95
|
};
|
|
53
96
|
|
|
@@ -77,6 +120,19 @@ const EDGE_SHORTHANDS = new Set([
|
|
|
77
120
|
'border-block-end'
|
|
78
121
|
]);
|
|
79
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Shorthands that apply one value to every longhand they set, such as the SVG
|
|
125
|
+
* `marker` shorthand and the bidirectional gap decoration rules. They have no
|
|
126
|
+
* way to express longhands that differ, so a group of longhands only collapses
|
|
127
|
+
* into them when every longhand already holds the same value.
|
|
128
|
+
*
|
|
129
|
+
* @type {Set<string>}
|
|
130
|
+
*/
|
|
131
|
+
const UNIFORM_VALUE_SHORTHANDS = new Set([
|
|
132
|
+
'marker',
|
|
133
|
+
...Object.keys(BIDIRECTIONAL_GAP_RULE_SHORTHANDS)
|
|
134
|
+
]);
|
|
135
|
+
|
|
80
136
|
/**
|
|
81
137
|
* The four physical edge shorthands that together cover the `border` shorthand.
|
|
82
138
|
*
|
|
@@ -142,12 +198,50 @@ function getOverridesOf (shorthandName) {
|
|
|
142
198
|
return OVERRIDES_BY_SHORTHAND.get(shorthandName) || NO_PROPERTIES;
|
|
143
199
|
}
|
|
144
200
|
|
|
201
|
+
/**
|
|
202
|
+
* The leaf longhands each property ultimately sets, computed on first use. The
|
|
203
|
+
* shorthand tables never change, so a property always expands the same way.
|
|
204
|
+
*
|
|
205
|
+
* @type {Map<string, Set<string>>}
|
|
206
|
+
*/
|
|
207
|
+
const leafPropertiesByProperty = new Map();
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Expands a property into the set of leaf longhands it ultimately sets, so that
|
|
211
|
+
* different groupings of the same box, such as `border-width` and
|
|
212
|
+
* `border-top-width`, can be compared for equivalent coverage.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} property The property name to expand.
|
|
215
|
+
* @return {Set} The set of leaf longhand property names.
|
|
216
|
+
*/
|
|
217
|
+
function expandToLeafProperties (property) {
|
|
218
|
+
const cachedLeaves = leafPropertiesByProperty.get(property);
|
|
219
|
+
if (cachedLeaves) {
|
|
220
|
+
return cachedLeaves;
|
|
221
|
+
}
|
|
222
|
+
const leafProperties = new Set();
|
|
223
|
+
const longhands = shorthandMap[property];
|
|
224
|
+
if (!longhands) {
|
|
225
|
+
leafProperties.add(property);
|
|
226
|
+
} else {
|
|
227
|
+
for (const longhand of longhands) {
|
|
228
|
+
for (const leafProperty of expandToLeafProperties(longhand)) {
|
|
229
|
+
leafProperties.add(leafProperty);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
leafPropertiesByProperty.set(property, leafProperties);
|
|
234
|
+
return leafProperties;
|
|
235
|
+
}
|
|
236
|
+
|
|
145
237
|
export {
|
|
146
238
|
BORDER_EDGE_PROPERTIES,
|
|
147
239
|
CSS_WIDE_KEYWORDS,
|
|
148
240
|
EDGE_SHORTHANDS,
|
|
241
|
+
expandToLeafProperties,
|
|
149
242
|
getLonghandsOf,
|
|
150
243
|
getOverridesOf,
|
|
151
244
|
shorthandMap,
|
|
152
|
-
shorthandOverrideMap
|
|
245
|
+
shorthandOverrideMap,
|
|
246
|
+
UNIFORM_VALUE_SHORTHANDS
|
|
153
247
|
};
|
|
@@ -2,51 +2,18 @@
|
|
|
2
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
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { minifyValue } from '../value/minify.js';
|
|
6
|
-
|
|
7
5
|
import {
|
|
8
6
|
CSS_WIDE_KEYWORDS,
|
|
7
|
+
expandToLeafProperties,
|
|
9
8
|
getLonghandsOf,
|
|
10
9
|
getOverridesOf,
|
|
11
10
|
shorthandMap
|
|
12
11
|
} from './config.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
* @type {Map<string, Set<string>>}
|
|
20
|
-
*/
|
|
21
|
-
const leafPropertiesByProperty = new Map();
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Expands a property into the set of leaf longhands it ultimately sets, so that
|
|
25
|
-
* different groupings of the same box, such as `border-width` and
|
|
26
|
-
* `border-top-width`, can be compared for equivalent coverage.
|
|
27
|
-
*
|
|
28
|
-
* @param {string} property The property name to expand.
|
|
29
|
-
* @return {Set} The set of leaf longhand property names.
|
|
30
|
-
*/
|
|
31
|
-
function expandToLeafProperties (property) {
|
|
32
|
-
const cachedLeaves = leafPropertiesByProperty.get(property);
|
|
33
|
-
if (cachedLeaves) {
|
|
34
|
-
return cachedLeaves;
|
|
35
|
-
}
|
|
36
|
-
const leafProperties = new Set();
|
|
37
|
-
const longhands = shorthandMap[property];
|
|
38
|
-
if (!longhands) {
|
|
39
|
-
leafProperties.add(property);
|
|
40
|
-
} else {
|
|
41
|
-
for (const longhand of longhands) {
|
|
42
|
-
for (const leafProperty of expandToLeafProperties(longhand)) {
|
|
43
|
-
leafProperties.add(leafProperty);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
leafPropertiesByProperty.set(property, leafProperties);
|
|
48
|
-
return leafProperties;
|
|
49
|
-
}
|
|
12
|
+
import {
|
|
13
|
+
collectDeclaredProperties,
|
|
14
|
+
describeDeclaration
|
|
15
|
+
} from './lookup.js';
|
|
16
|
+
import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
|
|
50
17
|
|
|
51
18
|
/**
|
|
52
19
|
* Checks whether a group of longhands sets every leaf longhand that the
|
|
@@ -70,16 +37,6 @@ function coversEveryLonghandOfShorthand (shorthandName, properties) {
|
|
|
70
37
|
});
|
|
71
38
|
}
|
|
72
39
|
|
|
73
|
-
/**
|
|
74
|
-
* @typedef {object} LonghandEntry
|
|
75
|
-
* @property {object} declaration The original declaration object.
|
|
76
|
-
* @property {number} index The declaration's index within the rule.
|
|
77
|
-
* @property {string} property The longhand property name.
|
|
78
|
-
* @property {string} text The minified `property:value` text.
|
|
79
|
-
* @property {string} value The minified value, without any `!important`.
|
|
80
|
-
* @property {boolean} isImportant Whether the declaration carries `!important`.
|
|
81
|
-
*/
|
|
82
|
-
|
|
83
40
|
/**
|
|
84
41
|
* Collects the declarations of a rule that set one of a shorthand's longhands,
|
|
85
42
|
* in source order.
|
|
@@ -95,16 +52,7 @@ function collectLonghandEntries (declarations, shorthandName) {
|
|
|
95
52
|
if (!declaration.property || !longhands.has(declaration.property)) {
|
|
96
53
|
return;
|
|
97
54
|
}
|
|
98
|
-
|
|
99
|
-
const isImportant = minifiedValue.includes('!important');
|
|
100
|
-
entries.push({
|
|
101
|
-
declaration,
|
|
102
|
-
index,
|
|
103
|
-
property: declaration.property,
|
|
104
|
-
text: declaration.property + ':' + minifiedValue,
|
|
105
|
-
value: minifiedValue.replace('!important', '').trim(),
|
|
106
|
-
isImportant
|
|
107
|
-
});
|
|
55
|
+
entries.push(describeDeclaration(declaration, index));
|
|
108
56
|
});
|
|
109
57
|
return entries;
|
|
110
58
|
}
|
|
@@ -206,12 +154,16 @@ function resetsEarlierDeclaration (declarations, shorthandName, insertionIndex)
|
|
|
206
154
|
* @param {Array} declarations The declarations of a single rule.
|
|
207
155
|
* @param {string} shorthandName The target shorthand property name.
|
|
208
156
|
* @param {Set} declaredProperties The property names the rule currently declares.
|
|
157
|
+
* @param {object} context The minification context with the stylesheet's reset properties.
|
|
209
158
|
* @return {Array|null} The rewritten declarations, or null when the rewrite does not apply.
|
|
210
159
|
*/
|
|
211
|
-
function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredProperties) {
|
|
160
|
+
function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredProperties, context) {
|
|
212
161
|
if (declaredProperties.has(shorthandName)) {
|
|
213
162
|
return null;
|
|
214
163
|
}
|
|
164
|
+
if (resetsPropertyDeclaredElsewhere(shorthandName, declaredProperties, context)) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
215
167
|
|
|
216
168
|
const entries = collectLonghandEntries(declarations, shorthandName);
|
|
217
169
|
if (entries.length < 2 || hasRepeatedProperty(entries)) {
|
|
@@ -283,10 +235,11 @@ function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredPr
|
|
|
283
235
|
* followed by `border-width:2px`, which inherits every border property and then
|
|
284
236
|
* overrides the one that differs.
|
|
285
237
|
*
|
|
286
|
-
* @param {Array}
|
|
287
|
-
* @
|
|
238
|
+
* @param {Array} declarations The declarations of a single rule.
|
|
239
|
+
* @param {object} context The minification context with the stylesheet's reset properties.
|
|
240
|
+
* @return {Array} The declarations, with eligible groups rewritten.
|
|
288
241
|
*/
|
|
289
|
-
function hoistCssWideKeywordsIntoShorthands (declarations) {
|
|
242
|
+
function hoistCssWideKeywordsIntoShorthands (declarations, context) {
|
|
290
243
|
let result = declarations;
|
|
291
244
|
// Every shorthand needs to know which properties the rule declares, so that
|
|
292
245
|
// set is kept alongside the declarations and only rebuilt after a rewrite
|
|
@@ -295,7 +248,7 @@ function hoistCssWideKeywordsIntoShorthands (declarations) {
|
|
|
295
248
|
// Shorthands are visited in declaration order, so the widest shorthand of a
|
|
296
249
|
// family is rewritten before the narrower shorthands it contains.
|
|
297
250
|
for (const shorthandName of Object.keys(shorthandMap)) {
|
|
298
|
-
const rewritten = rewriteGroupAsKeywordShorthand(result, shorthandName, declaredProperties);
|
|
251
|
+
const rewritten = rewriteGroupAsKeywordShorthand(result, shorthandName, declaredProperties, context);
|
|
299
252
|
if (rewritten) {
|
|
300
253
|
result = rewritten;
|
|
301
254
|
declaredProperties = collectDeclaredProperties(result);
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Folds longhand declarations that follow their own shorthand back into that shorthand, whenever restating the whole shorthand is shorter than keeping the shorthand and its overrides apart.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { splitTopLevelComponents } from '../value/syntax.js';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
CSS_WIDE_KEYWORDS,
|
|
9
|
+
expandToLeafProperties,
|
|
10
|
+
shorthandMap
|
|
11
|
+
} from './config.js';
|
|
12
|
+
import { describeDeclaration } from './lookup.js';
|
|
13
|
+
import { canMergeVarValue } from './merge.js';
|
|
14
|
+
import { buildShorthandValue } from './shorthand-values.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The shorthands whose value is a positional list of the values of their
|
|
18
|
+
* longhands: either a start/end pair or the four sides of a box. Only these can
|
|
19
|
+
* be expanded back into one value per longhand and rebuilt around an override.
|
|
20
|
+
*
|
|
21
|
+
* @type {Array}
|
|
22
|
+
*/
|
|
23
|
+
const POSITIONAL_SHORTHAND_NAMES = [
|
|
24
|
+
'margin',
|
|
25
|
+
'padding',
|
|
26
|
+
'inset',
|
|
27
|
+
'gap',
|
|
28
|
+
'overflow',
|
|
29
|
+
'place-items',
|
|
30
|
+
'place-content',
|
|
31
|
+
'place-self',
|
|
32
|
+
'border-width',
|
|
33
|
+
'border-style',
|
|
34
|
+
'border-color',
|
|
35
|
+
'border-radius',
|
|
36
|
+
'margin-inline',
|
|
37
|
+
'margin-block',
|
|
38
|
+
'padding-inline',
|
|
39
|
+
'padding-block',
|
|
40
|
+
'inset-inline',
|
|
41
|
+
'inset-block',
|
|
42
|
+
'border-inline-width',
|
|
43
|
+
'border-block-width'
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The words of the property names that describe the same box as a shorthand,
|
|
48
|
+
* computed on first use. The shorthand tables never change, so a shorthand
|
|
49
|
+
* always has the same family.
|
|
50
|
+
*
|
|
51
|
+
* @type {Map<string, Set<string>>}
|
|
52
|
+
*/
|
|
53
|
+
const familyWordsByShorthand = new Map();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Collects the hyphen separated words of a shorthand and of every longhand it
|
|
57
|
+
* sets. Logical properties such as `padding-inline-start` describe the same box
|
|
58
|
+
* as physical ones such as `padding-right`, but which physical side they map to
|
|
59
|
+
* depends on the writing mode, so the words of the names are what relates them.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} shorthandName The CSS shorthand property name.
|
|
62
|
+
* @return {Set} The words that mark a property as part of the same family.
|
|
63
|
+
*/
|
|
64
|
+
function collectFamilyWords (shorthandName) {
|
|
65
|
+
const cachedWords = familyWordsByShorthand.get(shorthandName);
|
|
66
|
+
if (cachedWords) {
|
|
67
|
+
return cachedWords;
|
|
68
|
+
}
|
|
69
|
+
const words = new Set(shorthandName.split('-'));
|
|
70
|
+
for (const leafProperty of expandToLeafProperties(shorthandName)) {
|
|
71
|
+
for (const word of leafProperty.split('-')) {
|
|
72
|
+
words.add(word);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
familyWordsByShorthand.set(shorthandName, words);
|
|
76
|
+
return words;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Checks whether a property might set part of the same box as a shorthand,
|
|
81
|
+
* which it does when the two names have a word in common.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} property The property name to classify.
|
|
84
|
+
* @param {Set} familyWords The words of the shorthand's family.
|
|
85
|
+
* @return {boolean} Whether the property belongs to the same family.
|
|
86
|
+
*/
|
|
87
|
+
function belongsToFamily (property, familyWords) {
|
|
88
|
+
return property.split('-').some((word) => {
|
|
89
|
+
return familyWords.has(word);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @typedef {object} FoldCandidate
|
|
95
|
+
* @property {object} shorthandEntry The description of the shorthand declaration.
|
|
96
|
+
* @property {Array} overrideEntries The descriptions of the longhands that follow it.
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Finds the shorthand of a family and the longhands declared after it, which
|
|
101
|
+
* are the declarations a fold would replace with a single shorthand. A family
|
|
102
|
+
* that states its shorthand or one of its longhands twice keeps an intentional
|
|
103
|
+
* fallback, so it is left alone.
|
|
104
|
+
*
|
|
105
|
+
* @param {Array} declarations The declarations of a single rule, in source order.
|
|
106
|
+
* @param {string} shorthandName The CSS shorthand property name.
|
|
107
|
+
* @return {FoldCandidate|null} The declarations to fold, or null when there are none.
|
|
108
|
+
*/
|
|
109
|
+
function findFoldCandidate (declarations, shorthandName) {
|
|
110
|
+
const longhandProperties = new Set(shorthandMap[shorthandName]);
|
|
111
|
+
const shorthandEntries = [];
|
|
112
|
+
const overrideEntries = [];
|
|
113
|
+
const overriddenProperties = new Set();
|
|
114
|
+
let hasRepeatedOverride = false;
|
|
115
|
+
|
|
116
|
+
declarations.forEach((declaration, index) => {
|
|
117
|
+
if (declaration.property === shorthandName) {
|
|
118
|
+
shorthandEntries.push(describeDeclaration(declaration, index));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!shorthandEntries.length || !longhandProperties.has(declaration.property)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (overriddenProperties.has(declaration.property)) {
|
|
125
|
+
hasRepeatedOverride = true;
|
|
126
|
+
}
|
|
127
|
+
overriddenProperties.add(declaration.property);
|
|
128
|
+
overrideEntries.push(describeDeclaration(declaration, index));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (shorthandEntries.length !== 1 || !overrideEntries.length || hasRepeatedOverride) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
shorthandEntry: shorthandEntries[0],
|
|
136
|
+
overrideEntries
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Checks whether a declaration lying between the shorthand and the last of its
|
|
142
|
+
* overrides would change meaning once the overrides move up to the shorthand.
|
|
143
|
+
* Only a declaration from another family is harmless, and a nested rule is
|
|
144
|
+
* never safe to step over because its own declarations may set the same box.
|
|
145
|
+
*
|
|
146
|
+
* @param {Array} declarations The declarations of a single rule, in source order.
|
|
147
|
+
* @param {string} shorthandName The CSS shorthand property name.
|
|
148
|
+
* @param {object} candidate The shorthand and the longhands that follow it.
|
|
149
|
+
* @return {boolean} Whether nothing stands in the way of the fold.
|
|
150
|
+
*/
|
|
151
|
+
function isFoldPathClear (declarations, shorthandName, candidate) {
|
|
152
|
+
const { shorthandEntry, overrideEntries } = candidate;
|
|
153
|
+
const foldedIndexes = new Set(overrideEntries.map((entry) => {
|
|
154
|
+
return entry.index;
|
|
155
|
+
}));
|
|
156
|
+
const lastOverrideIndex = overrideEntries[overrideEntries.length - 1].index;
|
|
157
|
+
const familyWords = collectFamilyWords(shorthandName);
|
|
158
|
+
|
|
159
|
+
for (let index = shorthandEntry.index + 1; index < lastOverrideIndex; index++) {
|
|
160
|
+
if (foldedIndexes.has(index)) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const { property } = declarations[index];
|
|
164
|
+
if (!property || belongsToFamily(property, familyWords)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Determines whether a value can stand as one component of a positional
|
|
173
|
+
* shorthand. A CSS-wide keyword is only valid as a declaration's entire value,
|
|
174
|
+
* a `/` separates the two radii of a corner rather than two components, and a
|
|
175
|
+
* `var()` may expand to any number of components at computed value time.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} component The value component to check.
|
|
178
|
+
* @param {object} context The minification context with registered custom property data.
|
|
179
|
+
* @return {boolean} Whether the component can be positioned in a shorthand.
|
|
180
|
+
*/
|
|
181
|
+
function isPositionalComponent (component, context) {
|
|
182
|
+
return (
|
|
183
|
+
!component.includes('/') &&
|
|
184
|
+
!CSS_WIDE_KEYWORDS.has(component.toLowerCase()) &&
|
|
185
|
+
canMergeVarValue(component, context)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Expands the components of a positional shorthand into one value per longhand,
|
|
191
|
+
* repeating the value of the opposite side for every component the author left
|
|
192
|
+
* out, as the CSS box model rules require.
|
|
193
|
+
*
|
|
194
|
+
* @param {Array} components The value components the shorthand was written with.
|
|
195
|
+
* @param {number} longhandCount The number of longhands the shorthand sets.
|
|
196
|
+
* @return {Array} One value per longhand, in longhand order.
|
|
197
|
+
*/
|
|
198
|
+
function expandPositionalComponents (components, longhandCount) {
|
|
199
|
+
if (longhandCount === 2) {
|
|
200
|
+
const [start, end = start] = components;
|
|
201
|
+
return [start, end];
|
|
202
|
+
}
|
|
203
|
+
const [top, right = top, bottom = top, left = right] = components;
|
|
204
|
+
return [top, right, bottom, left];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Rebuilds a shorthand from its own value plus the longhands declared after it.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} shorthandName The CSS shorthand property name.
|
|
211
|
+
* @param {object} candidate The shorthand and the longhands that follow it.
|
|
212
|
+
* @param {object} context The minification context with registered custom property data.
|
|
213
|
+
* @return {string|null} The rebuilt shorthand value, or null when it cannot be built.
|
|
214
|
+
*/
|
|
215
|
+
function buildFoldedValue (shorthandName, candidate, context) {
|
|
216
|
+
const { shorthandEntry, overrideEntries } = candidate;
|
|
217
|
+
const longhands = shorthandMap[shorthandName];
|
|
218
|
+
const components = splitTopLevelComponents(shorthandEntry.value);
|
|
219
|
+
if (!components.length || components.length > longhands.length) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const expandedValues = expandPositionalComponents(components, longhands.length);
|
|
224
|
+
const valueByProperty = new Map(longhands.map((property, index) => {
|
|
225
|
+
return [property, expandedValues[index]];
|
|
226
|
+
}));
|
|
227
|
+
for (const entry of overrideEntries) {
|
|
228
|
+
const overrideComponents = splitTopLevelComponents(entry.value);
|
|
229
|
+
if (overrideComponents.length !== 1) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
valueByProperty.set(entry.property, entry.value);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const cleanValues = longhands.map((property) => {
|
|
236
|
+
return valueByProperty.get(property);
|
|
237
|
+
});
|
|
238
|
+
const areAllPositional = cleanValues.every((value) => {
|
|
239
|
+
return isPositionalComponent(value, context);
|
|
240
|
+
});
|
|
241
|
+
if (!areAllPositional) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return buildShorthandValue(shorthandName, {
|
|
246
|
+
properties: longhands,
|
|
247
|
+
valueMap: valueByProperty,
|
|
248
|
+
cleanValues,
|
|
249
|
+
importantSuffix: shorthandEntry.isImportant ? '!important' : ''
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Replaces a shorthand and the longhands declared after it with the single
|
|
255
|
+
* shorthand that states the same box.
|
|
256
|
+
*
|
|
257
|
+
* @param {Array} declarations The declarations of a single rule, in source order.
|
|
258
|
+
* @param {string} shorthandName The CSS shorthand property name.
|
|
259
|
+
* @param {object} candidate The shorthand and the longhands that follow it.
|
|
260
|
+
* @param {string} foldedValue The rebuilt shorthand value.
|
|
261
|
+
* @return {Array} The declarations, with the family stated once.
|
|
262
|
+
*/
|
|
263
|
+
function applyFold (declarations, shorthandName, candidate, foldedValue) {
|
|
264
|
+
const { shorthandEntry, overrideEntries } = candidate;
|
|
265
|
+
const foldedIndexes = new Set(overrideEntries.map((entry) => {
|
|
266
|
+
return entry.index;
|
|
267
|
+
}));
|
|
268
|
+
return declarations.flatMap((declaration, index) => {
|
|
269
|
+
if (index === shorthandEntry.index) {
|
|
270
|
+
return [{
|
|
271
|
+
property: shorthandName,
|
|
272
|
+
value: foldedValue,
|
|
273
|
+
isAssembledShorthand: true
|
|
274
|
+
}];
|
|
275
|
+
}
|
|
276
|
+
if (foldedIndexes.has(index)) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
return [declaration];
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Folds the longhands that follow one shorthand back into it, when the single
|
|
285
|
+
* rebuilt shorthand is shorter than the declarations it replaces.
|
|
286
|
+
*
|
|
287
|
+
* @param {Array} declarations The declarations of a single rule, in source order.
|
|
288
|
+
* @param {string} shorthandName The CSS shorthand property name.
|
|
289
|
+
* @param {object} context The minification context with registered custom property data.
|
|
290
|
+
* @return {Array} The declarations, folded when that is shorter.
|
|
291
|
+
*/
|
|
292
|
+
function foldOverridesIntoShorthand (declarations, shorthandName, context) {
|
|
293
|
+
const candidate = findFoldCandidate(declarations, shorthandName);
|
|
294
|
+
if (!candidate) {
|
|
295
|
+
return declarations;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// A shorthand and a longhand of differing importance do not describe one
|
|
299
|
+
// cascade step, so the pair cannot be restated as a single declaration.
|
|
300
|
+
const { shorthandEntry, overrideEntries } = candidate;
|
|
301
|
+
const shareImportance = overrideEntries.every((entry) => {
|
|
302
|
+
return entry.isImportant === shorthandEntry.isImportant;
|
|
303
|
+
});
|
|
304
|
+
if (!shareImportance || !isFoldPathClear(declarations, shorthandName, candidate)) {
|
|
305
|
+
return declarations;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const foldedValue = buildFoldedValue(shorthandName, candidate, context);
|
|
309
|
+
if (!foldedValue) {
|
|
310
|
+
return declarations;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const foldedLength = (shorthandName + ':' + foldedValue).length;
|
|
314
|
+
const originalLength = [shorthandEntry.text, ...overrideEntries.map((entry) => {
|
|
315
|
+
return entry.text;
|
|
316
|
+
})].join(';').length;
|
|
317
|
+
if (foldedLength >= originalLength) {
|
|
318
|
+
return declarations;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return applyFold(declarations, shorthandName, candidate, foldedValue);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Folds every longhand that follows its own shorthand back into that shorthand.
|
|
326
|
+
* A longhand after a shorthand only overrides the one side the shorthand had
|
|
327
|
+
* already set, so `padding:10px;padding-right:20px` states the same box as
|
|
328
|
+
* `padding:10px 20px 10px 10px`, and the shorter of the two is kept.
|
|
329
|
+
*
|
|
330
|
+
* @param {Array} declarations The declarations of a single rule, in source order.
|
|
331
|
+
* @param {object} context The minification context with registered custom property data.
|
|
332
|
+
* @return {Array} The declarations, with eligible families folded.
|
|
333
|
+
*/
|
|
334
|
+
function foldLonghandOverridesIntoShorthands (declarations, context) {
|
|
335
|
+
let result = declarations;
|
|
336
|
+
for (const shorthandName of POSITIONAL_SHORTHAND_NAMES) {
|
|
337
|
+
result = foldOverridesIntoShorthand(result, shorthandName, context);
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export { foldLonghandOverridesIntoShorthands };
|