@thejaredwilcurt/csslop 0.0.24 → 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.
@@ -103,6 +103,35 @@ function expandPureNestedRules (rules) {
103
103
  return result;
104
104
  }
105
105
 
106
+ /**
107
+ * Builds a reusable matcher that expresses child selectors as nested selectors
108
+ * relative to one parent. The combinator pattern is compiled once per parent so
109
+ * that testing many children against the same parent does not rebuild it.
110
+ *
111
+ * @param {string} parentSelector The parent selector string.
112
+ * @return {function(string): (string|null)} A matcher returning the nested selector, or null when the child cannot be nested.
113
+ */
114
+ function createNestedSelectorMatcher (parentSelector) {
115
+ const parent = parentSelector.trim();
116
+ // Match a child selector that starts with the parent followed by a combinator (>, +, ~)
117
+ const combinatorPattern = new RegExp('^' + escapeRegexString(parent) + '\\s*([>+~])\\s*(.+)$');
118
+
119
+ return (childSelector) => {
120
+ const child = childSelector.trim();
121
+ if (child.startsWith(parent + ':') || child.startsWith(parent + '::')) {
122
+ return '&' + child.slice(parent.length);
123
+ }
124
+ if (child.startsWith(parent + ' ')) {
125
+ return child.slice(parent.length + 1);
126
+ }
127
+ const combinatorMatch = child.match(combinatorPattern);
128
+ if (combinatorMatch) {
129
+ return combinatorMatch[1] + combinatorMatch[2];
130
+ }
131
+ return null;
132
+ };
133
+ }
134
+
106
135
  /**
107
136
  * Attempts to express a child selector as a nested selector relative to a parent, returning the nested form or null if nesting is not possible.
108
137
  *
@@ -111,22 +140,7 @@ function expandPureNestedRules (rules) {
111
140
  * @return {string|null} The nested selector using & syntax, or null if the child cannot be nested under the parent.
112
141
  */
113
142
  function tryNestSelector (parentSel, childSel) {
114
- const parent = parentSel.trim();
115
- const child = childSel.trim();
116
- if (child.startsWith(parent + ':') || child.startsWith(parent + '::')) {
117
- return '&' + child.slice(parent.length);
118
- }
119
- if (child.startsWith(parent + ' ')) {
120
- return child.slice(parent.length + 1);
121
- }
122
- // Match child selector that starts with the parent followed by a combinator (>, +, ~)
123
- const combinatorMatch = child.match(
124
- new RegExp('^' + escapeRegexString(parent) + '\\s*([>+~])\\s*(.+)$')
125
- );
126
- if (combinatorMatch) {
127
- return combinatorMatch[1] + combinatorMatch[2];
128
- }
129
- return null;
143
+ return createNestedSelectorMatcher(parentSel)(childSel);
130
144
  }
131
145
 
132
146
  /**
@@ -173,6 +187,87 @@ function removeEmptyRules (rules) {
173
187
  });
174
188
  }
175
189
 
190
+ /**
191
+ * Determines whether a character is CSS whitespace.
192
+ *
193
+ * @param {string} character A single character from a selector.
194
+ * @return {boolean} Whether the character is whitespace.
195
+ */
196
+ function isSelectorWhitespace (character) {
197
+ // Match a single whitespace character
198
+ return (/\s/).test(character);
199
+ }
200
+
201
+ /**
202
+ * Collects every prefix of a child selector that could act as its nesting
203
+ * parent. A parent always ends immediately before a pseudo-class colon, a
204
+ * descendant whitespace run, or the whitespace run leading into a combinator,
205
+ * so only those few positions can start a nestable remainder. Enumerating them
206
+ * turns "which earlier rule can host this one" into a set of key lookups
207
+ * instead of a comparison against every preceding rule.
208
+ *
209
+ * @param {string} childSelector The trimmed child selector.
210
+ * @return {Set} The candidate parent selector strings.
211
+ */
212
+ function collectCandidateParentSelectors (childSelector) {
213
+ const candidates = new Set();
214
+ for (let index = 1; index < childSelector.length; index++) {
215
+ const character = childSelector[index];
216
+ if (character === ':' || isSelectorWhitespace(character)) {
217
+ candidates.add(childSelector.slice(0, index));
218
+ continue;
219
+ }
220
+ if (character !== '>' && character !== '+' && character !== '~') {
221
+ continue;
222
+ }
223
+ // A combinator may be separated from its parent by whitespace, which
224
+ // belongs to neither side, so the parent ends where that run begins.
225
+ let boundary = index;
226
+ while (boundary > 0 && isSelectorWhitespace(childSelector[boundary - 1])) {
227
+ boundary--;
228
+ }
229
+ if (boundary > 0) {
230
+ candidates.add(childSelector.slice(0, boundary));
231
+ }
232
+ }
233
+ return candidates;
234
+ }
235
+
236
+ /**
237
+ * Finds the most recently emitted rule that can host a child selector as a
238
+ * nested rule, matching the behaviour of scanning backwards through the emitted
239
+ * rules but only visiting the handful of rules whose selector is a viable
240
+ * parent prefix.
241
+ *
242
+ * @param {Array} emittedRules The rules emitted so far.
243
+ * @param {Map} indexBySelector Map of each emitted single-selector rule's selector to its index.
244
+ * @param {string} childSelector The trimmed child selector to nest.
245
+ * @return {object|null} The `parentIndex` and `nestedSelector`, or null when nothing can host the child.
246
+ */
247
+ function findNestingParent (emittedRules, indexBySelector, childSelector) {
248
+ const parentIndexes = [];
249
+ for (const candidateParent of collectCandidateParentSelectors(childSelector)) {
250
+ const parentIndex = indexBySelector.get(candidateParent);
251
+ if (parentIndex !== undefined) {
252
+ parentIndexes.push(parentIndex);
253
+ }
254
+ }
255
+ // The nearest preceding parent wins, exactly as a backwards scan would.
256
+ parentIndexes.sort((first, second) => {
257
+ return second - first;
258
+ });
259
+ for (const parentIndex of parentIndexes) {
260
+ const nestedSelector = tryNestSelector(emittedRules[parentIndex].selectors[0], childSelector);
261
+ if (nestedSelector !== null) {
262
+ return {
263
+ nestedSelector,
264
+ parentIndex
265
+ };
266
+ }
267
+ }
268
+ return null;
269
+ }
270
+
176
271
  /**
177
272
  * Groups flat CSS rules into nested structures where a child selector can be expressed relative to a preceding parent, reducing output size through CSS nesting.
178
273
  *
@@ -181,29 +276,22 @@ function removeEmptyRules (rules) {
181
276
  */
182
277
  function nestFlatRules (rules) {
183
278
  const result = [];
279
+ const indexBySelector = new Map();
184
280
  for (const rule of rules) {
185
281
  if (rule.type !== 'rule' || rule.selectors?.length !== 1) {
186
282
  result.push(rule);
187
283
  continue;
188
284
  }
189
285
  const childSelector = rule.selectors[0].trim();
190
- let wasNested = false;
191
- for (let j = result.length - 1; j >= 0; j--) {
192
- const parentRule = result[j];
193
- if (parentRule.type !== 'rule' || parentRule.selectors?.length !== 1) {
194
- continue;
195
- }
196
- const nestedSelector = tryNestSelector(parentRule.selectors[0], childSelector);
197
- if (nestedSelector !== null) {
198
- parentRule.declarations = parentRule.declarations || [];
199
- parentRule.declarations.push({ ...rule, selectors: [nestedSelector] });
200
- wasNested = true;
201
- break;
202
- }
203
- }
204
- if (!wasNested) {
205
- result.push(rule);
286
+ const nesting = findNestingParent(result, indexBySelector, childSelector);
287
+ if (nesting) {
288
+ const parentRule = result[nesting.parentIndex];
289
+ parentRule.declarations = parentRule.declarations || [];
290
+ parentRule.declarations.push({ ...rule, selectors: [nesting.nestedSelector] });
291
+ continue;
206
292
  }
293
+ indexBySelector.set(childSelector, result.length);
294
+ result.push(rule);
207
295
  }
208
296
  for (const rule of result) {
209
297
  if (rule.type === 'rule' && rule.declarations) {
@@ -277,6 +365,7 @@ function factorCommonParents (rules) {
277
365
  continue;
278
366
  }
279
367
  // Collect the run of consecutive rules that can all nest under the candidate.
368
+ const matchNestedSelector = createNestedSelectorMatcher(candidateParent);
280
369
  const run = [];
281
370
  const nestedForms = [];
282
371
  let lookahead = index;
@@ -285,7 +374,7 @@ function factorCommonParents (rules) {
285
374
  if (sibling.type !== 'rule' || sibling.selectors?.length !== 1) {
286
375
  break;
287
376
  }
288
- const nestedSelector = tryNestSelector(candidateParent, sibling.selectors[0]);
377
+ const nestedSelector = matchNestedSelector(sibling.selectors[0]);
289
378
  if (nestedSelector === null) {
290
379
  break;
291
380
  }
@@ -745,8 +834,12 @@ function mergeIdenticalNestedRules (rules) {
745
834
  * @return {Array} A new array of rules with same-selector rules combined.
746
835
  */
747
836
  function mergeSelectorRules (rules) {
748
- let result = [];
749
- let selectorMap = new Map();
837
+ // A merged rule moves to the end of the output. Its old slot is emptied
838
+ // instead of spliced out so that every recorded position stays valid, and the
839
+ // position map locates that slot without searching the output.
840
+ const slots = [];
841
+ const positionByRule = new Map();
842
+ const selectorMap = new Map();
750
843
  for (const rule of rules) {
751
844
  if (rule.type === 'rule') {
752
845
  const selectorKey = rule.selectors ?
@@ -757,23 +850,26 @@ function mergeSelectorRules (rules) {
757
850
  if (selectorKey && selectorMap.has(selectorKey)) {
758
851
  const existingRule = selectorMap.get(selectorKey);
759
852
  existingRule.declarations.push(...(rule.declarations || []));
760
- result = result.filter((candidate) => {
761
- return candidate !== existingRule;
762
- });
763
- result.push(existingRule);
853
+ slots[positionByRule.get(existingRule)] = null;
854
+ positionByRule.set(existingRule, slots.length);
855
+ slots.push(existingRule);
764
856
  } else {
765
857
  selectorMap.set(selectorKey, rule);
766
- result.push(rule);
858
+ positionByRule.set(rule, slots.length);
859
+ slots.push(rule);
767
860
  }
768
861
  } else {
769
862
  if (rule.type === 'whitespace') {
770
863
  continue;
771
864
  }
772
- result.push(rule);
865
+ slots.push(rule);
773
866
  selectorMap.clear();
867
+ positionByRule.clear();
774
868
  }
775
869
  }
776
- return result;
870
+ return slots.filter((slot) => {
871
+ return slot !== null;
872
+ });
777
873
  }
778
874
 
779
875
  /**
@@ -835,6 +931,61 @@ function normalizeSelector (selector) {
835
931
  .replace(/::after\b/g, ':after');
836
932
  }
837
933
 
934
+ /**
935
+ * Indexes, for every normalized selector in the stylesheet, the last rule that
936
+ * declares each property under it. Comparing that index against a rule's own
937
+ * position answers "is this selector's property overridden later" in constant
938
+ * time, instead of rescanning and renormalizing every following rule.
939
+ *
940
+ * @param {Array} rules The flat list of AST rule nodes.
941
+ * @return {Map} Map of normalized selector to a map of property name to its last declaring rule index.
942
+ */
943
+ function indexLastDeclaringRuleBySelector (rules) {
944
+ const lastRuleIndexBySelector = new Map();
945
+ rules.forEach((rule, ruleIndex) => {
946
+ if (rule.type !== 'rule' || !rule.selectors) {
947
+ return;
948
+ }
949
+ const declaredProperties = (rule.declarations || []).filter((declaration) => {
950
+ return declaration.type === 'declaration';
951
+ }).map((declaration) => {
952
+ return declaration.property;
953
+ });
954
+ if (declaredProperties.length === 0) {
955
+ return;
956
+ }
957
+ for (const selector of rule.selectors) {
958
+ const normalizedSelector = normalizeSelector(selector);
959
+ let lastRuleIndexByProperty = lastRuleIndexBySelector.get(normalizedSelector);
960
+ if (!lastRuleIndexByProperty) {
961
+ lastRuleIndexByProperty = new Map();
962
+ lastRuleIndexBySelector.set(normalizedSelector, lastRuleIndexByProperty);
963
+ }
964
+ for (const property of declaredProperties) {
965
+ lastRuleIndexByProperty.set(property, ruleIndex);
966
+ }
967
+ }
968
+ });
969
+ return lastRuleIndexBySelector;
970
+ }
971
+
972
+ /**
973
+ * Checks whether a given selector has a specific property overridden
974
+ * by any later rule in the stylesheet. A property is considered
975
+ * overridden if a subsequent rule contains that selector (as its only
976
+ * selector or among its selectors) and declares the same property.
977
+ *
978
+ * @param {Map} lastRuleIndexBySelector The index built by `indexLastDeclaringRuleBySelector`.
979
+ * @param {number} startIndex The index of the current rule (search starts after this).
980
+ * @param {string} selector The normalized selector to check.
981
+ * @param {string} property The CSS property name to check.
982
+ * @return {boolean} True if a later rule overrides this selector+property.
983
+ */
984
+ function isSelectorPropertyOverriddenLater (lastRuleIndexBySelector, startIndex, selector, property) {
985
+ const lastRuleIndex = lastRuleIndexBySelector.get(selector)?.get(property);
986
+ return lastRuleIndex !== undefined && lastRuleIndex > startIndex;
987
+ }
988
+
838
989
  /**
839
990
  * Removes properties from multi-selector rules when every selector in
840
991
  * the rule has that property overridden by a later rule. For example,
@@ -847,6 +998,10 @@ function normalizeSelector (selector) {
847
998
  * @return {Array} The rules with overridden multi-selector properties removed.
848
999
  */
849
1000
  function removeOverriddenMultiSelectorProperties (rules) {
1001
+ // Only rules that follow the one being pruned are ever consulted, and pruning
1002
+ // never adds a declaration, so a single index built up front stays accurate.
1003
+ const lastRuleIndexBySelector = indexLastDeclaringRuleBySelector(rules);
1004
+
850
1005
  for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex++) {
851
1006
  const rule = rules[ruleIndex];
852
1007
  if (rule.type !== 'rule' || !rule.selectors || rule.selectors.length < 2) {
@@ -866,7 +1021,7 @@ function removeOverriddenMultiSelectorProperties (rules) {
866
1021
  for (const declaration of declarations) {
867
1022
  const property = declaration.property;
868
1023
  const allSelectorsOverridden = normalizedSelectors.every((selector) => {
869
- return isSelectorPropertyOverriddenLater(rules, ruleIndex, selector, property);
1024
+ return isSelectorPropertyOverriddenLater(lastRuleIndexBySelector, ruleIndex, selector, property);
870
1025
  });
871
1026
  if (allSelectorsOverridden) {
872
1027
  propertiesToRemove.add(property);
@@ -885,41 +1040,6 @@ function removeOverriddenMultiSelectorProperties (rules) {
885
1040
  return rules;
886
1041
  }
887
1042
 
888
- /**
889
- * Checks whether a given selector has a specific property overridden
890
- * by any later rule in the stylesheet. A property is considered
891
- * overridden if a subsequent rule contains that selector (as its only
892
- * selector or among its selectors) and declares the same property.
893
- *
894
- * @param {Array} rules The full list of AST rule nodes.
895
- * @param {number} startIndex The index of the current rule (search starts after this).
896
- * @param {string} selector The normalized selector to check.
897
- * @param {string} property The CSS property name to check.
898
- * @return {boolean} True if a later rule overrides this selector+property.
899
- */
900
- function isSelectorPropertyOverriddenLater (rules, startIndex, selector, property) {
901
- for (let laterIndex = startIndex + 1; laterIndex < rules.length; laterIndex++) {
902
- const laterRule = rules[laterIndex];
903
- if (laterRule.type !== 'rule' || !laterRule.selectors) {
904
- continue;
905
- }
906
- const laterSelectors = laterRule.selectors.map(normalizeSelector);
907
- if (!laterSelectors.includes(selector)) {
908
- continue;
909
- }
910
- const laterDeclarations = (laterRule.declarations || []).filter((declaration) => {
911
- return declaration.type === 'declaration';
912
- });
913
- const hasOverride = laterDeclarations.some((declaration) => {
914
- return declaration.property === property;
915
- });
916
- if (hasOverride) {
917
- return true;
918
- }
919
- }
920
- return false;
921
- }
922
-
923
1043
  export {
924
1044
  deduplicateKeyframes,
925
1045
  expandPureNestedRules,
@@ -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,
@@ -41,6 +42,14 @@ function stringifyDeclarations (declarations) {
41
42
  .join(';');
42
43
  }
43
44
 
45
+ /**
46
+ * The heading element selectors, which collapse into the `:heading`
47
+ * pseudo-class when a rule targets every one of them.
48
+ *
49
+ * @type {Set<string>}
50
+ */
51
+ const HEADING_SELECTORS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
52
+
44
53
  /**
45
54
  * Matches a complete `@layer` statement, which declares layer names without a
46
55
  * block and ends with the semicolon that separates it from the CSS that follows
@@ -263,12 +272,11 @@ function stringifyRule (rule, context) {
263
272
  }
264
273
  uniqueSelectors = uniqueSelectors.flatMap(processIsSelector);
265
274
  uniqueSelectors = [...new Set(uniqueSelectors)];
266
- const headingSet = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
267
275
  const isAllHeadings = (
268
- rule.selectors.length === 6 &&
269
- uniqueSelectors.length === 6 &&
276
+ rule.selectors.length === HEADING_SELECTORS.size &&
277
+ uniqueSelectors.length === HEADING_SELECTORS.size &&
270
278
  uniqueSelectors.every((selector) => {
271
- return headingSet.has(selector);
279
+ return HEADING_SELECTORS.has(selector);
272
280
  })
273
281
  );
274
282
  if (isAllHeadings) {
@@ -511,32 +519,7 @@ function stringifyRule (rule, context) {
511
519
  }
512
520
 
513
521
  if (rule.type === 'property') {
514
- const propertyDeclarations = (rule.declarations || []).filter((declaration) => {
515
- return declaration.type === 'declaration' && declaration.property;
516
- });
517
- const hasSyntaxDescriptor = propertyDeclarations.some((declaration) => {
518
- return declaration.property === 'syntax';
519
- });
520
- const hasInheritsDescriptor = propertyDeclarations.some((declaration) => {
521
- return declaration.property === 'inherits';
522
- });
523
- if (!hasSyntaxDescriptor || !hasInheritsDescriptor) {
524
- return '';
525
- }
526
-
527
- const syntaxDeclaration = propertyDeclarations.find((declaration) => {
528
- return declaration.property === 'syntax';
529
- });
530
- const syntaxValue = (syntaxDeclaration.value || '').replace(/["']/g, '').trim();
531
- const isUniversalSyntax = syntaxValue === '*';
532
- const hasInitialValue = propertyDeclarations.some((declaration) => {
533
- return declaration.property === 'initial-value';
534
- });
535
- if (!isUniversalSyntax && !hasInitialValue) {
536
- return '';
537
- }
538
-
539
- let renderedDeclarations = stringifyDeclarations(rule.declarations || []);
522
+ const renderedDeclarations = stringifyDeclarations(resolvePropertyDescriptors(rule));
540
523
  if (!renderedDeclarations) {
541
524
  return '';
542
525
  }