@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
|
@@ -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,26 +6,37 @@ import {
|
|
|
6
6
|
shorthandMap,
|
|
7
7
|
shorthandOverrideMap
|
|
8
8
|
} from './config.js';
|
|
9
|
+
import { collectDeclaredProperties } from './lookup.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
-
* the first resets the second
|
|
13
|
-
* longhands it was built from.
|
|
12
|
+
* Builds the pairs of properties where the first has to be emitted before the
|
|
13
|
+
* second, because the first resets the second while a merged shorthand is
|
|
14
|
+
* appended after the longhands it was built from. Every property a shorthand
|
|
15
|
+
* resets but cannot express has to be restated after that shorthand, and a
|
|
16
|
+
* `margin` built from a mixed `!important` group leaves its important longhands
|
|
17
|
+
* behind for the same reason.
|
|
18
|
+
*
|
|
19
|
+
* @return {Array} The property pairs, each as a shorthand followed by the property it resets.
|
|
20
|
+
*/
|
|
21
|
+
function buildRequiredPropertyOrder () {
|
|
22
|
+
const orderedPairs = [];
|
|
23
|
+
for (const [shorthandProperty, resetProperties] of Object.entries(shorthandOverrideMap)) {
|
|
24
|
+
for (const resetProperty of resetProperties) {
|
|
25
|
+
orderedPairs.push([shorthandProperty, resetProperty]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const longhandProperty of shorthandMap.margin) {
|
|
29
|
+
orderedPairs.push(['margin', longhandProperty]);
|
|
30
|
+
}
|
|
31
|
+
return orderedPairs;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The property pairs whose relative order the output has to correct.
|
|
14
36
|
*
|
|
15
37
|
* @type {Array}
|
|
16
38
|
*/
|
|
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
|
-
];
|
|
39
|
+
const REQUIRED_PROPERTY_ORDER = buildRequiredPropertyOrder();
|
|
29
40
|
|
|
30
41
|
/**
|
|
31
42
|
* Reorders declarations so that shorthands appear before any related longhands they would override, preventing cascade issues in the minified output.
|
|
@@ -40,8 +51,16 @@ function orderDeclarations (declarations) {
|
|
|
40
51
|
return declaration?.property === property;
|
|
41
52
|
});
|
|
42
53
|
};
|
|
54
|
+
// Most rules declare neither half of any of these pairs, so the properties a
|
|
55
|
+
// rule does declare are gathered once rather than scanned for per pair.
|
|
56
|
+
// Reordering the declarations never changes which properties are declared,
|
|
57
|
+
// so the set stays accurate as the pairs are applied.
|
|
58
|
+
const declaredProperties = collectDeclaredProperties(ordered);
|
|
43
59
|
|
|
44
60
|
for (const [property, followingProperty] of REQUIRED_PROPERTY_ORDER) {
|
|
61
|
+
if (!declaredProperties.has(property) || !declaredProperties.has(followingProperty)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
45
64
|
const fromIndex = findPropertyIndex(property);
|
|
46
65
|
const toIndex = findPropertyIndex(followingProperty);
|
|
47
66
|
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
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
getOverriddenLonghands,
|
|
25
26
|
orderDeclarations
|
|
26
27
|
} from './order.js';
|
|
28
|
+
import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Shorthands that keep their non-important longhands in the output, so a mixed
|
|
@@ -347,6 +349,12 @@ function mergeLonghandsIntoShorthands (declarations, context) {
|
|
|
347
349
|
if (declaredProperties.has(shorthand)) {
|
|
348
350
|
continue;
|
|
349
351
|
}
|
|
352
|
+
// Assembling a shorthand also resets the properties it cannot express, so
|
|
353
|
+
// a rule only collapses into one when nothing else in the stylesheet
|
|
354
|
+
// relies on a value that reset would discard.
|
|
355
|
+
if (resetsPropertyDeclaredElsewhere(shorthand, declaredProperties, context)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
350
358
|
|
|
351
359
|
const mergeableProperties = getMergeProps(shorthand, longhands, declaredProperties);
|
|
352
360
|
if (!mergeableProperties) {
|
|
@@ -396,8 +404,9 @@ function processDeclarations (declarations, context) {
|
|
|
396
404
|
result = removeLonghandsOverriddenByShorthands(result);
|
|
397
405
|
result = absorbBackgroundLonghandsIntoShorthand(result);
|
|
398
406
|
result = mergeLonghandsIntoShorthands(result, context);
|
|
399
|
-
result =
|
|
400
|
-
result =
|
|
407
|
+
result = foldLonghandOverridesIntoShorthands(result, context);
|
|
408
|
+
result = hoistCssWideKeywordsIntoShorthands(result, context);
|
|
409
|
+
result = collapseBorderTrioWithPerEdgeColor(result, context);
|
|
401
410
|
|
|
402
411
|
return orderDeclarations(result);
|
|
403
412
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tracks the properties a shorthand resets without being able to express them, so that assembling a shorthand out of longhands never cancels a value another rule of the stylesheet set.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getOverridesOf,
|
|
7
|
+
shorthandOverrideMap
|
|
8
|
+
} from './config.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Collects every property that some shorthand resets without being able to
|
|
12
|
+
* express it, such as `border-image` for `border`. Only these properties can
|
|
13
|
+
* lose their value to a newly assembled shorthand, so only these are worth
|
|
14
|
+
* tracking across the stylesheet.
|
|
15
|
+
*
|
|
16
|
+
* @return {Set} The property names a shorthand resets but cannot express.
|
|
17
|
+
*/
|
|
18
|
+
function collectResettableProperties () {
|
|
19
|
+
const resettableProperties = new Set();
|
|
20
|
+
for (const resetProperties of Object.values(shorthandOverrideMap)) {
|
|
21
|
+
for (const property of resetProperties) {
|
|
22
|
+
resettableProperties.add(property);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return resettableProperties;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The properties a shorthand resets but cannot express. The shorthand tables
|
|
30
|
+
* never change, so the set is built once.
|
|
31
|
+
*
|
|
32
|
+
* @type {Set<string>}
|
|
33
|
+
*/
|
|
34
|
+
const RESETTABLE_PROPERTIES = collectResettableProperties();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Collects the rules nested inside a rule. The parser stores the children of an
|
|
38
|
+
* at-rule as a list of rules, and the children of a style rule that uses CSS
|
|
39
|
+
* nesting as declarations that carry rules of their own.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} rule The AST rule node to look inside.
|
|
42
|
+
* @return {Array} The rule nodes nested within it.
|
|
43
|
+
*/
|
|
44
|
+
function collectNestedRules (rule) {
|
|
45
|
+
const nestedRules = [...(rule.rules || [])];
|
|
46
|
+
for (const declaration of rule.declarations || []) {
|
|
47
|
+
if (declaration.rules || declaration.declarations) {
|
|
48
|
+
nestedRules.push(declaration);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return nestedRules;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Records which resettable properties the stylesheet declares, in any rule at
|
|
56
|
+
* any nesting depth, so that each rule can later ask whether assembling a
|
|
57
|
+
* shorthand would cancel a value another rule set.
|
|
58
|
+
*
|
|
59
|
+
* @param {Array} rules The AST rule nodes of the whole stylesheet.
|
|
60
|
+
* @param {object} context The minification context to populate.
|
|
61
|
+
*/
|
|
62
|
+
function recordStylesheetResetProperties (rules, context) {
|
|
63
|
+
const pendingRules = [...rules];
|
|
64
|
+
while (pendingRules.length) {
|
|
65
|
+
const rule = pendingRules.pop();
|
|
66
|
+
for (const declaration of rule.declarations || []) {
|
|
67
|
+
if (RESETTABLE_PROPERTIES.has(declaration.property)) {
|
|
68
|
+
context.stylesheetResetProperties.add(declaration.property);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
pendingRules.push(...collectNestedRules(rule));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Checks whether assembling a shorthand out of longhands would reset a property
|
|
77
|
+
* that the stylesheet sets somewhere else. The `border` shorthand resets
|
|
78
|
+
* `border-image`, so turning the border longhands of one rule into `border`
|
|
79
|
+
* cancels the `border-image` that another rule sets on the same element. A rule
|
|
80
|
+
* that states the reset property itself is safe, because that declaration is
|
|
81
|
+
* emitted after the shorthand and restates the value the shorthand discarded.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} shorthandName The shorthand that would be assembled.
|
|
84
|
+
* @param {Set} declaredProperties The property names the rule declares.
|
|
85
|
+
* @param {object} context The minification context with the stylesheet's reset properties.
|
|
86
|
+
* @return {boolean} Whether assembling the shorthand would discard another rule's value.
|
|
87
|
+
*/
|
|
88
|
+
function resetsPropertyDeclaredElsewhere (shorthandName, declaredProperties, context) {
|
|
89
|
+
const stylesheetResetProperties = context?.stylesheetResetProperties;
|
|
90
|
+
if (!stylesheetResetProperties?.size) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
for (const resetProperty of getOverridesOf(shorthandName)) {
|
|
94
|
+
const isSetElsewhere = (
|
|
95
|
+
stylesheetResetProperties.has(resetProperty) &&
|
|
96
|
+
!declaredProperties.has(resetProperty)
|
|
97
|
+
);
|
|
98
|
+
if (isSetElsewhere) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
recordStylesheetResetProperties,
|
|
107
|
+
resetsPropertyDeclaredElsewhere
|
|
108
|
+
};
|
|
@@ -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/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
createMinifyContext,
|
|
14
14
|
setActiveCharset
|
|
15
15
|
} from './context.js';
|
|
16
|
+
import { recordStylesheetResetProperties } from './declarations/reset-hazards.js';
|
|
16
17
|
import {
|
|
17
18
|
analyzePositionTryRules,
|
|
18
19
|
cleanPositionTryRules,
|
|
@@ -231,6 +232,10 @@ export const minifyCSS = function (input) {
|
|
|
231
232
|
const context = createMinifyContext();
|
|
232
233
|
|
|
233
234
|
if (ast?.stylesheet?.rules) {
|
|
235
|
+
// Which properties a shorthand may not silently reset is a question about
|
|
236
|
+
// the whole stylesheet, so it is answered before any rule is rewritten.
|
|
237
|
+
recordStylesheetResetProperties(ast.stylesheet.rules, context);
|
|
238
|
+
|
|
234
239
|
const {
|
|
235
240
|
positionTryRules,
|
|
236
241
|
positionTryUsage
|
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
|
}
|