@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.
- package/README.md +5 -1
- package/package.json +5 -3
- package/src/declarations/background.js +26 -1
- package/src/declarations/config.js +156 -2
- package/src/declarations/css-wide-keywords.js +29 -60
- package/src/declarations/fold.js +342 -0
- package/src/declarations/lookup.js +78 -0
- package/src/declarations/merge.js +59 -54
- package/src/declarations/order.js +45 -6
- package/src/declarations/process.js +148 -55
- package/src/declarations/shorthand-values.js +23 -0
- package/src/index.js +29 -5
- package/src/position-try.js +3 -1
- package/src/rules/optimize.js +198 -78
- package/src/rules/property.js +177 -0
- package/src/rules/stringify.js +13 -30
- package/src/value/gradients.js +87 -46
- package/src/value/math.js +17 -2
- package/src/value/minify.js +110 -14
package/src/rules/optimize.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
const parentRule = result[
|
|
193
|
-
|
|
194
|
-
|
|
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 =
|
|
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
|
-
|
|
749
|
-
|
|
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
|
-
|
|
761
|
-
|
|
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
|
-
|
|
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
|
-
|
|
865
|
+
slots.push(rule);
|
|
773
866
|
selectorMap.clear();
|
|
867
|
+
positionByRule.clear();
|
|
774
868
|
}
|
|
775
869
|
}
|
|
776
|
-
return
|
|
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(
|
|
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
|
+
};
|
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,
|
|
@@ -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 ===
|
|
269
|
-
uniqueSelectors.length ===
|
|
276
|
+
rule.selectors.length === HEADING_SELECTORS.size &&
|
|
277
|
+
uniqueSelectors.length === HEADING_SELECTORS.size &&
|
|
270
278
|
uniqueSelectors.every((selector) => {
|
|
271
|
-
return
|
|
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
|
|
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
|
}
|