@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.
@@ -1,9 +1,42 @@
1
1
  /**
2
- * @file Builds keyed lookups over a rule's declarations, so the shorthand
3
- * passes can ask which properties a rule sets without rescanning its
4
- * declarations once per property they are interested in.
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
- if (shorthand === 'font') {
41
- const hasRequiredFontProps = declaredProperties.has('font-size') && declaredProperties.has('font-family');
42
- if (hasRequiredFontProps) {
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
- * Pairs of properties where the first must be emitted before the second, because
12
- * the first resets the second and a merged shorthand is appended after the
13
- * longhands it was built from.
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 = hoistCssWideKeywordsIntoShorthands(result);
400
- result = collapseBorderTrioWithPerEdgeColor(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
@@ -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
+ };
@@ -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 propertyDeclarations = (rule.declarations || []).filter((declaration) => {
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
  }