@thejaredwilcurt/csslop 0.0.25 → 0.0.26
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 +1 -1
- package/src/declarations/config.js +96 -2
- package/src/declarations/css-wide-keywords.js +6 -59
- 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 +13 -0
- package/src/declarations/process.js +2 -0
- package/src/declarations/shorthand-values.js +23 -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,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,10 +82,13 @@ 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
93
|
font: ['font-variant', 'font-variant-alternates', 'font-variant-caps', 'font-variant-east-asian', 'font-variant-ligatures', 'font-variant-numeric', 'font-variant-position'],
|
|
51
94
|
mask: ['mask-border', 'mask-border-source', 'mask-border-slice', 'mask-border-width', 'mask-border-outset', 'mask-border-repeat', 'mask-border-mode']
|
|
@@ -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,17 @@
|
|
|
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
|
-
* shorthand tables never change, so a property always expands the same way.
|
|
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';
|
|
50
16
|
|
|
51
17
|
/**
|
|
52
18
|
* Checks whether a group of longhands sets every leaf longhand that the
|
|
@@ -70,16 +36,6 @@ function coversEveryLonghandOfShorthand (shorthandName, properties) {
|
|
|
70
36
|
});
|
|
71
37
|
}
|
|
72
38
|
|
|
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
39
|
/**
|
|
84
40
|
* Collects the declarations of a rule that set one of a shorthand's longhands,
|
|
85
41
|
* in source order.
|
|
@@ -95,16 +51,7 @@ function collectLonghandEntries (declarations, shorthandName) {
|
|
|
95
51
|
if (!declaration.property || !longhands.has(declaration.property)) {
|
|
96
52
|
return;
|
|
97
53
|
}
|
|
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
|
-
});
|
|
54
|
+
entries.push(describeDeclaration(declaration, index));
|
|
108
55
|
});
|
|
109
56
|
return entries;
|
|
110
57
|
}
|
|
@@ -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 };
|
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file Builds keyed lookups over a rule's declarations,
|
|
3
|
-
* passes can ask which properties a rule sets
|
|
4
|
-
* declarations once per property
|
|
2
|
+
* @file Builds keyed lookups and normalized views over a rule's declarations,
|
|
3
|
+
* so the shorthand passes can ask which properties a rule sets, and what each
|
|
4
|
+
* of them minifies to, without rescanning its declarations once per property
|
|
5
|
+
* they are interested in.
|
|
5
6
|
*/
|
|
6
7
|
|
|
8
|
+
import { minifyValue } from '../value/minify.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} DeclarationDescription
|
|
12
|
+
* @property {object} declaration The original declaration object.
|
|
13
|
+
* @property {number} index The declaration's index within the rule.
|
|
14
|
+
* @property {string} property The declared property name.
|
|
15
|
+
* @property {string} text The minified `property:value` text.
|
|
16
|
+
* @property {string} value The minified value, without any `!important`.
|
|
17
|
+
* @property {boolean} isImportant Whether the declaration carries `!important`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Describes a declaration through its minified value, which is the form the
|
|
22
|
+
* shorthand passes compare, rewrite, and measure the output length of.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} declaration The CSS declaration object.
|
|
25
|
+
* @param {number} index The declaration's index within the rule.
|
|
26
|
+
* @return {DeclarationDescription} The normalized view of the declaration.
|
|
27
|
+
*/
|
|
28
|
+
function describeDeclaration (declaration, index) {
|
|
29
|
+
const minifiedValue = minifyValue(declaration);
|
|
30
|
+
return {
|
|
31
|
+
declaration,
|
|
32
|
+
index,
|
|
33
|
+
property: declaration.property,
|
|
34
|
+
text: declaration.property + ':' + minifiedValue,
|
|
35
|
+
value: minifiedValue.replace('!important', '').trim(),
|
|
36
|
+
isImportant: minifiedValue.includes('!important')
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
7
40
|
/**
|
|
8
41
|
* Indexes the first declaration of each property. The first occurrence is the
|
|
9
42
|
* one a linear search would return, so this stands in for repeated scans that
|
|
@@ -40,5 +73,6 @@ function collectDeclaredProperties (declarations) {
|
|
|
40
73
|
|
|
41
74
|
export {
|
|
42
75
|
collectDeclaredProperties,
|
|
76
|
+
describeDeclaration,
|
|
43
77
|
indexFirstDeclarationByProperty
|
|
44
78
|
};
|
|
@@ -22,6 +22,39 @@ import { buildShorthandValue } from './shorthand-values.js';
|
|
|
22
22
|
*/
|
|
23
23
|
const MIXED_IMPORTANT_SHORTHANDS = new Set(['margin', 'padding', 'inset', 'position-try']);
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* The shorthands that may be built from only some of their longhands, and the
|
|
27
|
+
* longhands each of them cannot do without. Every listed group has to be
|
|
28
|
+
* satisfied by at least one of the properties it holds, so `font` needs both a
|
|
29
|
+
* size and a family, while `background` needs a color or an image.
|
|
30
|
+
*
|
|
31
|
+
* @type {{[key: string]: Array}}
|
|
32
|
+
*/
|
|
33
|
+
const PARTIAL_MERGE_REQUIREMENTS = {
|
|
34
|
+
animation: [['animation-name'], ['animation-duration']],
|
|
35
|
+
background: [['background-color', 'background-image']],
|
|
36
|
+
'background-position': [['background-position-x'], ['background-position-y']],
|
|
37
|
+
'border-image': [['border-image-source']],
|
|
38
|
+
font: [['font-size'], ['font-family']],
|
|
39
|
+
mask: [['mask-image']]
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Checks whether a rule declares enough of a shorthand's longhands for the
|
|
44
|
+
* shorthand to be built from the subset it does declare.
|
|
45
|
+
*
|
|
46
|
+
* @param {Array} requirementGroups The groups of interchangeable longhands the shorthand requires.
|
|
47
|
+
* @param {Set} declaredProperties The property names the rule currently declares.
|
|
48
|
+
* @return {boolean} Whether every requirement group is satisfied.
|
|
49
|
+
*/
|
|
50
|
+
function meetsPartialMergeRequirements (requirementGroups, declaredProperties) {
|
|
51
|
+
return requirementGroups.every((requiredProperties) => {
|
|
52
|
+
return requiredProperties.some((property) => {
|
|
53
|
+
return declaredProperties.has(property);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
25
58
|
/**
|
|
26
59
|
* 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.
|
|
27
60
|
*
|
|
@@ -37,35 +70,9 @@ function getMergeProps (shorthand, longhands, declaredProperties) {
|
|
|
37
70
|
if (presentLonghands.length === 0) {
|
|
38
71
|
return null;
|
|
39
72
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
return presentLonghands;
|
|
44
|
-
}
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
if (shorthand === 'background-position') {
|
|
48
|
-
const hasBothAxes = declaredProperties.has('background-position-x') && declaredProperties.has('background-position-y');
|
|
49
|
-
if (hasBothAxes) {
|
|
50
|
-
return presentLonghands;
|
|
51
|
-
}
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
if (shorthand === 'background') {
|
|
55
|
-
const hasBackgroundProp = declaredProperties.has('background-color') || declaredProperties.has('background-image');
|
|
56
|
-
if (hasBackgroundProp) {
|
|
57
|
-
return presentLonghands;
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
if (shorthand === 'mask') {
|
|
62
|
-
if (declaredProperties.has('mask-image')) {
|
|
63
|
-
return presentLonghands;
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
if (shorthand === 'border-image') {
|
|
68
|
-
if (declaredProperties.has('border-image-source')) {
|
|
73
|
+
const requirementGroups = PARTIAL_MERGE_REQUIREMENTS[shorthand];
|
|
74
|
+
if (requirementGroups) {
|
|
75
|
+
if (meetsPartialMergeRequirements(requirementGroups, declaredProperties)) {
|
|
69
76
|
return presentLonghands;
|
|
70
77
|
}
|
|
71
78
|
return null;
|
|
@@ -315,6 +322,7 @@ function tryMergeToShorthand (properties, declarations, shorthandName = '', cont
|
|
|
315
322
|
}
|
|
316
323
|
|
|
317
324
|
export {
|
|
325
|
+
canMergeVarValue,
|
|
318
326
|
getMergeProps,
|
|
319
327
|
tryMergeToShorthand
|
|
320
328
|
};
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
shorthandMap,
|
|
7
7
|
shorthandOverrideMap
|
|
8
8
|
} from './config.js';
|
|
9
|
+
import { collectDeclaredProperties } from './lookup.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Pairs of properties where the first must be emitted before the second, because
|
|
@@ -15,6 +16,10 @@ import {
|
|
|
15
16
|
* @type {Array}
|
|
16
17
|
*/
|
|
17
18
|
const REQUIRED_PROPERTY_ORDER = [
|
|
19
|
+
['animation', 'animation-timeline'],
|
|
20
|
+
['animation', 'animation-range'],
|
|
21
|
+
['animation', 'animation-range-start'],
|
|
22
|
+
['animation', 'animation-range-end'],
|
|
18
23
|
['border', 'border-image'],
|
|
19
24
|
['font', 'font-feature-settings'],
|
|
20
25
|
['font', 'font-variant-ligatures'],
|
|
@@ -40,8 +45,16 @@ function orderDeclarations (declarations) {
|
|
|
40
45
|
return declaration?.property === property;
|
|
41
46
|
});
|
|
42
47
|
};
|
|
48
|
+
// Most rules declare neither half of any of these pairs, so the properties a
|
|
49
|
+
// rule does declare are gathered once rather than scanned for per pair.
|
|
50
|
+
// Reordering the declarations never changes which properties are declared,
|
|
51
|
+
// so the set stays accurate as the pairs are applied.
|
|
52
|
+
const declaredProperties = collectDeclaredProperties(ordered);
|
|
43
53
|
|
|
44
54
|
for (const [property, followingProperty] of REQUIRED_PROPERTY_ORDER) {
|
|
55
|
+
if (!declaredProperties.has(property) || !declaredProperties.has(followingProperty)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
45
58
|
const fromIndex = findPropertyIndex(property);
|
|
46
59
|
const toIndex = findPropertyIndex(followingProperty);
|
|
47
60
|
if (fromIndex === -1 || toIndex === -1 || fromIndex < toIndex) {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
shorthandMap
|
|
13
13
|
} from './config.js';
|
|
14
14
|
import { hoistCssWideKeywordsIntoShorthands } from './css-wide-keywords.js';
|
|
15
|
+
import { foldLonghandOverridesIntoShorthands } from './fold.js';
|
|
15
16
|
import {
|
|
16
17
|
collectDeclaredProperties,
|
|
17
18
|
indexFirstDeclarationByProperty
|
|
@@ -396,6 +397,7 @@ function processDeclarations (declarations, context) {
|
|
|
396
397
|
result = removeLonghandsOverriddenByShorthands(result);
|
|
397
398
|
result = absorbBackgroundLonghandsIntoShorthand(result);
|
|
398
399
|
result = mergeLonghandsIntoShorthands(result, context);
|
|
400
|
+
result = foldLonghandOverridesIntoShorthands(result, context);
|
|
399
401
|
result = hoistCssWideKeywordsIntoShorthands(result);
|
|
400
402
|
result = collapseBorderTrioWithPerEdgeColor(result);
|
|
401
403
|
|
|
@@ -6,6 +6,7 @@ import { collapseShorthandParts } from '../value/shared.js';
|
|
|
6
6
|
import { splitTopLevelComponents } from '../value/syntax.js';
|
|
7
7
|
|
|
8
8
|
import { buildBackgroundShorthandValue } from './background.js';
|
|
9
|
+
import { UNIFORM_VALUE_SHORTHANDS } from './config.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @typedef {object} ShorthandComponents
|
|
@@ -275,6 +276,25 @@ function buildFlexValue ({ valueMap, importantSuffix }) {
|
|
|
275
276
|
return [grow, shrink, basis].join(' ') + importantSuffix;
|
|
276
277
|
}
|
|
277
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Builds the value of a shorthand that applies one value to every longhand it
|
|
281
|
+
* sets, such as `marker` or the bidirectional gap decoration rules. Such a
|
|
282
|
+
* shorthand cannot express longhands that differ, so the group only collapses
|
|
283
|
+
* when every longhand already holds the same value.
|
|
284
|
+
*
|
|
285
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
286
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
287
|
+
*/
|
|
288
|
+
function buildUniformValue ({ cleanValues, importantSuffix }) {
|
|
289
|
+
const isSharedByAll = cleanValues.every((value) => {
|
|
290
|
+
return value === cleanValues[0];
|
|
291
|
+
});
|
|
292
|
+
if (!isSharedByAll) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
return cleanValues[0] + importantSuffix;
|
|
296
|
+
}
|
|
297
|
+
|
|
278
298
|
/**
|
|
279
299
|
* Determines whether a shorthand takes a single width, style, and color, as
|
|
280
300
|
* `border` and `outline` do.
|
|
@@ -364,6 +384,9 @@ const NAMED_SHORTHAND_BUILDERS = {
|
|
|
364
384
|
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
365
385
|
*/
|
|
366
386
|
function buildShorthandValue (shorthandName, components) {
|
|
387
|
+
if (UNIFORM_VALUE_SHORTHANDS.has(shorthandName)) {
|
|
388
|
+
return buildUniformValue(components);
|
|
389
|
+
}
|
|
367
390
|
const namedBuilder = NAMED_SHORTHAND_BUILDERS[shorthandName];
|
|
368
391
|
if (namedBuilder) {
|
|
369
392
|
return namedBuilder(components);
|
package/src/position-try.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* @file Handles `@position-try` rule analysis, usage tracking, and dead-rule elimination during CSS minification.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { registersCustomProperty } from './rules/property.js';
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Scans top-level rules to register `@property` custom properties in the context and collect `@position-try` rule declarations and initial usage counts.
|
|
7
9
|
*
|
|
@@ -14,7 +16,7 @@ function collectRuleMetadata (rules, context) {
|
|
|
14
16
|
const positionTryUsage = new Map();
|
|
15
17
|
|
|
16
18
|
for (const rule of rules) {
|
|
17
|
-
if (rule.type === 'property' && rule.name) {
|
|
19
|
+
if (rule.type === 'property' && rule.name && registersCustomProperty(rule)) {
|
|
18
20
|
context.registeredCustomProperties.add(rule.name);
|
|
19
21
|
const syntaxDeclaration = (rule.declarations || []).find((declaration) => {
|
|
20
22
|
return declaration.type !== 'whitespace' && declaration.property === 'syntax';
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Analyzes `@property` at-rules, resolving each descriptor against the value the CSS engine assumes when the descriptor is absent, so redundant descriptors and pointless registrations can be dropped.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The descriptors an `@property` rule can declare. Anything else inside the
|
|
7
|
+
* rule is an unknown descriptor, which the CSS engine discards while parsing.
|
|
8
|
+
*
|
|
9
|
+
* @type {Set<string>}
|
|
10
|
+
*/
|
|
11
|
+
const PROPERTY_DESCRIPTORS = new Set([
|
|
12
|
+
'syntax',
|
|
13
|
+
'inherits',
|
|
14
|
+
'initial-value'
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The syntax that accepts any token sequence. It is also the syntax a
|
|
19
|
+
* registration falls back to when the rule omits the `syntax` descriptor.
|
|
20
|
+
*
|
|
21
|
+
* @type {string}
|
|
22
|
+
*/
|
|
23
|
+
const UNIVERSAL_SYNTAX = '*';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The inheritance a registration falls back to when the rule omits the
|
|
27
|
+
* `inherits` descriptor.
|
|
28
|
+
*
|
|
29
|
+
* @type {string}
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_INHERITS = 'true';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reads a descriptor value the way the CSS engine compares it, ignoring the
|
|
35
|
+
* whitespace that surrounds the value.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} declaration The descriptor declaration node.
|
|
38
|
+
* @return {string} The descriptor value without surrounding whitespace.
|
|
39
|
+
*/
|
|
40
|
+
function readDescriptorValue (declaration) {
|
|
41
|
+
return String(declaration.value ?? '').trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reads a `syntax` descriptor as the syntax it describes, rather than as the
|
|
46
|
+
* string it is written as, so it can be compared against the universal syntax.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} syntaxValue The raw `syntax` descriptor value, such as `"<length>"`.
|
|
49
|
+
* @return {string} The described syntax, such as `<length>`.
|
|
50
|
+
*/
|
|
51
|
+
function unquoteSyntax (syntaxValue) {
|
|
52
|
+
// A value wrapped in a matching pair of quotes, capturing the string contents
|
|
53
|
+
const quotedStringPattern = /^(["'])([\s\S]*)\1$/;
|
|
54
|
+
const quotedString = syntaxValue.match(quotedStringPattern);
|
|
55
|
+
if (quotedString) {
|
|
56
|
+
return quotedString[2].trim();
|
|
57
|
+
}
|
|
58
|
+
return syntaxValue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Collects the descriptors an `@property` rule declares, keyed by descriptor
|
|
63
|
+
* name and ordered by first appearance. Unknown descriptors are left out
|
|
64
|
+
* because the CSS engine ignores them, and a descriptor declared more than
|
|
65
|
+
* once resolves to its final declaration, which is the one the engine keeps.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} rule The `@property` AST rule node.
|
|
68
|
+
* @return {Map} Descriptor names mapped to the declaration that wins.
|
|
69
|
+
*/
|
|
70
|
+
function collectPropertyDescriptors (rule) {
|
|
71
|
+
const descriptors = new Map();
|
|
72
|
+
for (const declaration of rule.declarations || []) {
|
|
73
|
+
const descriptorName = String(declaration.property ?? '').toLowerCase();
|
|
74
|
+
const isKnownDescriptor = (
|
|
75
|
+
declaration.type === 'declaration' &&
|
|
76
|
+
PROPERTY_DESCRIPTORS.has(descriptorName)
|
|
77
|
+
);
|
|
78
|
+
if (isKnownDescriptor) {
|
|
79
|
+
descriptors.set(descriptorName, declaration);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return descriptors;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolves the syntax a set of descriptors registers, falling back to the
|
|
87
|
+
* universal syntax when the `syntax` descriptor is absent.
|
|
88
|
+
*
|
|
89
|
+
* @param {Map} descriptors Descriptor names mapped to their declarations.
|
|
90
|
+
* @return {string} The registered syntax.
|
|
91
|
+
*/
|
|
92
|
+
function resolveRegisteredSyntax (descriptors) {
|
|
93
|
+
const syntaxDeclaration = descriptors.get('syntax');
|
|
94
|
+
if (!syntaxDeclaration) {
|
|
95
|
+
return UNIVERSAL_SYNTAX;
|
|
96
|
+
}
|
|
97
|
+
return unquoteSyntax(readDescriptorValue(syntaxDeclaration));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Reports whether the CSS engine accepts the registration a set of descriptors
|
|
102
|
+
* describes. Registering anything narrower than the universal syntax requires
|
|
103
|
+
* an `initial-value`, since the engine has no valid value to start from
|
|
104
|
+
* otherwise, and a registration it rejects has no effect on the stylesheet.
|
|
105
|
+
*
|
|
106
|
+
* @param {Map} descriptors Descriptor names mapped to their declarations.
|
|
107
|
+
* @return {boolean} True when the registration is valid.
|
|
108
|
+
*/
|
|
109
|
+
function isValidRegistration (descriptors) {
|
|
110
|
+
if (resolveRegisteredSyntax(descriptors) === UNIVERSAL_SYNTAX) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return descriptors.has('initial-value');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Reports whether a descriptor declares exactly what the CSS engine already
|
|
118
|
+
* assumes, which makes writing the descriptor out pointless. An
|
|
119
|
+
* `initial-value` always says something, because the value it defaults to is
|
|
120
|
+
* the guaranteed-invalid value, which no declaration can spell out.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} descriptorName The lowercased descriptor name.
|
|
123
|
+
* @param {object} declaration The descriptor declaration node.
|
|
124
|
+
* @return {boolean} True when the descriptor restates a default.
|
|
125
|
+
*/
|
|
126
|
+
function isDefaultDescriptor (descriptorName, declaration) {
|
|
127
|
+
const value = readDescriptorValue(declaration);
|
|
128
|
+
if (descriptorName === 'syntax') {
|
|
129
|
+
return unquoteSyntax(value) === UNIVERSAL_SYNTAX;
|
|
130
|
+
}
|
|
131
|
+
if (descriptorName === 'inherits') {
|
|
132
|
+
return value.toLowerCase() === DEFAULT_INHERITS;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Reduces an `@property` rule to the descriptors worth writing out, meaning
|
|
139
|
+
* the ones that describe something other than what the CSS engine assumes on
|
|
140
|
+
* its own. An empty result means the entire rule can be dropped, either
|
|
141
|
+
* because the engine rejects the registration or because the registration
|
|
142
|
+
* matches an unregistered custom property in every way.
|
|
143
|
+
*
|
|
144
|
+
* @param {object} rule The `@property` AST rule node.
|
|
145
|
+
* @return {Array} The descriptor declarations to render.
|
|
146
|
+
*/
|
|
147
|
+
function resolvePropertyDescriptors (rule) {
|
|
148
|
+
const descriptors = collectPropertyDescriptors(rule);
|
|
149
|
+
if (!isValidRegistration(descriptors)) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
const meaningfulDescriptors = [];
|
|
153
|
+
for (const [descriptorName, declaration] of descriptors) {
|
|
154
|
+
if (!isDefaultDescriptor(descriptorName, declaration)) {
|
|
155
|
+
meaningfulDescriptors.push(declaration);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return meaningfulDescriptors;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Reports whether an `@property` rule survives minification, which is also
|
|
163
|
+
* what decides whether the rest of the stylesheet may rely on the custom
|
|
164
|
+
* property being registered. Rules that describe nothing beyond the defaults
|
|
165
|
+
* leave the custom property just as unregistered as never mentioning it.
|
|
166
|
+
*
|
|
167
|
+
* @param {object} rule The `@property` AST rule node.
|
|
168
|
+
* @return {boolean} True when the rule registers the custom property.
|
|
169
|
+
*/
|
|
170
|
+
function registersCustomProperty (rule) {
|
|
171
|
+
return resolvePropertyDescriptors(rule).length > 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export {
|
|
175
|
+
registersCustomProperty,
|
|
176
|
+
resolvePropertyDescriptors
|
|
177
|
+
};
|
package/src/rules/stringify.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
unescapeIdent,
|
|
18
18
|
unescapeSelector
|
|
19
19
|
} from './normalize.js';
|
|
20
|
+
import { resolvePropertyDescriptors } from './property.js';
|
|
20
21
|
import {
|
|
21
22
|
flattenNestingParentIsSelector,
|
|
22
23
|
mergeAdjacentWherePseudoClasses,
|
|
@@ -518,32 +519,7 @@ function stringifyRule (rule, context) {
|
|
|
518
519
|
}
|
|
519
520
|
|
|
520
521
|
if (rule.type === 'property') {
|
|
521
|
-
const
|
|
522
|
-
return declaration.type === 'declaration' && declaration.property;
|
|
523
|
-
});
|
|
524
|
-
const hasSyntaxDescriptor = propertyDeclarations.some((declaration) => {
|
|
525
|
-
return declaration.property === 'syntax';
|
|
526
|
-
});
|
|
527
|
-
const hasInheritsDescriptor = propertyDeclarations.some((declaration) => {
|
|
528
|
-
return declaration.property === 'inherits';
|
|
529
|
-
});
|
|
530
|
-
if (!hasSyntaxDescriptor || !hasInheritsDescriptor) {
|
|
531
|
-
return '';
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const syntaxDeclaration = propertyDeclarations.find((declaration) => {
|
|
535
|
-
return declaration.property === 'syntax';
|
|
536
|
-
});
|
|
537
|
-
const syntaxValue = (syntaxDeclaration.value || '').replace(/["']/g, '').trim();
|
|
538
|
-
const isUniversalSyntax = syntaxValue === '*';
|
|
539
|
-
const hasInitialValue = propertyDeclarations.some((declaration) => {
|
|
540
|
-
return declaration.property === 'initial-value';
|
|
541
|
-
});
|
|
542
|
-
if (!isUniversalSyntax && !hasInitialValue) {
|
|
543
|
-
return '';
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
let renderedDeclarations = stringifyDeclarations(rule.declarations || []);
|
|
522
|
+
const renderedDeclarations = stringifyDeclarations(resolvePropertyDescriptors(rule));
|
|
547
523
|
if (!renderedDeclarations) {
|
|
548
524
|
return '';
|
|
549
525
|
}
|