@thejaredwilcurt/csslop 0.0.49 → 0.0.51
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 +0 -1
- package/package.json +2 -2
- package/src/declarations/border.js +1 -1
- package/src/declarations/process.js +155 -1
- package/src/index.js +7 -7
- package/src/rules/optimize/at-rules.js +113 -0
- package/src/rules/optimize/declaration-writes.js +70 -0
- package/src/rules/optimize/empty-rules.js +51 -0
- package/src/rules/optimize/expand-nesting.js +236 -0
- package/src/rules/optimize/factor-parents.js +343 -0
- package/src/rules/optimize/index.js +71 -0
- package/src/rules/optimize/merge-declarations.js +199 -0
- package/src/rules/optimize/merge-nested-rules.js +117 -0
- package/src/rules/optimize/merge-selectors.js +252 -0
- package/src/rules/optimize/nest-rules.js +202 -0
- package/src/rules/optimize/override-index.js +264 -0
- package/src/rules/optimize/prune-overridden.js +267 -0
- package/src/rules/optimize/selector-normalization.js +60 -0
- package/src/rules/optimize/specificity.js +280 -0
- package/src/{position-try.js → rules/position-try.js} +1 -1
- package/src/rules/stringify.js +13 -0
- package/src/utilities.js +14 -0
- package/src/value/border.js +85 -0
- package/src/value/{colors.js → color/conversion.js} +3 -2
- package/src/value/color/functions.js +463 -0
- package/src/value/{light-dark.js → color/light-dark.js} +1 -1
- package/src/value/{color-mix.js → color/mix.js} +3 -3
- package/src/value/{relative-colors.js → color/relative.js} +3 -3
- package/src/value/color/shorten.js +93 -0
- package/src/value/gradients.js +1 -1
- package/src/value/hyphenate-limit-chars.js +72 -0
- package/src/value/initial-keywords.js +128 -0
- package/src/value/lengths.js +55 -0
- package/src/value/math/evaluate.js +36 -0
- package/src/value/math/fold.js +315 -0
- package/src/value/math/format.js +230 -0
- package/src/value/math/functions.js +302 -0
- package/src/value/math/index.js +219 -0
- package/src/value/math/parser.js +301 -0
- package/src/value/math/property-ranges.js +174 -0
- package/src/value/math/rewrite.js +222 -0
- package/src/value/math/tokenizer.js +160 -0
- package/src/value/math/unit-totals.js +194 -0
- package/src/value/minify.js +127 -1669
- package/src/value/numbers.js +28 -0
- package/src/value/positions.js +152 -0
- package/src/value/property-optimizations.js +485 -0
- package/src/value/segments.js +74 -0
- package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
- package/src/value/{svg-markup.js → svg/markup.js} +1 -1
- package/src/value/time.js +75 -0
- package/src/value/urls.js +242 -0
- package/src/value/whitespace.js +88 -0
- package/src/rules/optimize.js +0 -2314
- package/src/value/math-expression.js +0 -1156
- package/src/value/math.js +0 -886
- /package/src/value/{named-colors.js → color/named.js} +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Removes the selectors and declarations of multi-selector rules that later rules override in full, since they no longer decide anything.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
expandToOverridableProperties,
|
|
7
|
+
isImportantDeclaration
|
|
8
|
+
} from './declaration-writes.js';
|
|
9
|
+
import { normalizeSelector } from './selector-normalization.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Indexes, for every normalized selector in the stylesheet, the last rule that
|
|
13
|
+
* declares each property under it. Comparing that index against a rule's own
|
|
14
|
+
* position answers "is this selector's property overridden later" in constant
|
|
15
|
+
* time, instead of rescanning and renormalizing every following rule.
|
|
16
|
+
*
|
|
17
|
+
* @param {Array} rules The flat list of AST rule nodes.
|
|
18
|
+
* @return {Map} Map of normalized selector to a map of property name to its last declaring rule index.
|
|
19
|
+
*/
|
|
20
|
+
function indexLastDeclaringRuleBySelector (rules) {
|
|
21
|
+
const lastRuleIndexBySelector = new Map();
|
|
22
|
+
rules.forEach((rule, ruleIndex) => {
|
|
23
|
+
if (rule.type !== 'rule' || !rule.selectors) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const declaredProperties = (rule.declarations || []).filter((declaration) => {
|
|
27
|
+
return declaration.type === 'declaration';
|
|
28
|
+
}).map((declaration) => {
|
|
29
|
+
return declaration.property;
|
|
30
|
+
});
|
|
31
|
+
if (declaredProperties.length === 0) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
for (const selector of rule.selectors) {
|
|
35
|
+
const normalizedSelector = normalizeSelector(selector);
|
|
36
|
+
let lastRuleIndexByProperty = lastRuleIndexBySelector.get(normalizedSelector);
|
|
37
|
+
if (!lastRuleIndexByProperty) {
|
|
38
|
+
lastRuleIndexByProperty = new Map();
|
|
39
|
+
lastRuleIndexBySelector.set(normalizedSelector, lastRuleIndexByProperty);
|
|
40
|
+
}
|
|
41
|
+
for (const property of declaredProperties) {
|
|
42
|
+
lastRuleIndexByProperty.set(property, ruleIndex);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return lastRuleIndexBySelector;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Checks whether a given selector has a specific property overridden
|
|
51
|
+
* by any later rule in the stylesheet. A property is considered
|
|
52
|
+
* overridden if a subsequent rule contains that selector (as its only
|
|
53
|
+
* selector or among its selectors) and declares the same property.
|
|
54
|
+
*
|
|
55
|
+
* @param {Map} lastRuleIndexBySelector The index built by `indexLastDeclaringRuleBySelector`.
|
|
56
|
+
* @param {number} startIndex The index of the current rule (search starts after this).
|
|
57
|
+
* @param {string} selector The normalized selector to check.
|
|
58
|
+
* @param {string} property The CSS property name to check.
|
|
59
|
+
* @return {boolean} True if a later rule overrides this selector+property.
|
|
60
|
+
*/
|
|
61
|
+
function isSelectorPropertyOverriddenLater (lastRuleIndexBySelector, startIndex, selector, property) {
|
|
62
|
+
const lastRuleIndex = lastRuleIndexBySelector.get(selector)?.get(property);
|
|
63
|
+
return lastRuleIndex !== undefined && lastRuleIndex > startIndex;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Indexes, for every normalized selector in the stylesheet, the last rule that
|
|
68
|
+
* covers each leaf property under it, where coverage follows the same "writes
|
|
69
|
+
* to" model as `expandToOverridableProperties` (a later `border` declaration
|
|
70
|
+
* on the same selector also overrides an earlier `border-color` on it), along
|
|
71
|
+
* with whether that covering declaration is important. A last-covering entry
|
|
72
|
+
* is consulted to answer "is every declaration this selector still gets from
|
|
73
|
+
* the rule at position X re-declared later".
|
|
74
|
+
*
|
|
75
|
+
* @param {Array} rules The flat list of AST rule nodes.
|
|
76
|
+
* @return {Map} Map of normalized selector to a map of covered leaf property name to `{ ruleIndex, important }`.
|
|
77
|
+
*/
|
|
78
|
+
function indexFinalCoveringRuleBySelector (rules) {
|
|
79
|
+
const coverageBySelector = new Map();
|
|
80
|
+
rules.forEach((rule, ruleIndex) => {
|
|
81
|
+
if (rule.type !== 'rule' || !rule.selectors?.length) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const declarations = (rule.declarations || []).filter((declaration) => {
|
|
85
|
+
return declaration.type === 'declaration' && declaration.property;
|
|
86
|
+
});
|
|
87
|
+
if (!declarations.length) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const selector of rule.selectors) {
|
|
91
|
+
const normalizedSelector = normalizeSelector(selector);
|
|
92
|
+
let coverageByProperty = coverageBySelector.get(normalizedSelector);
|
|
93
|
+
if (!coverageByProperty) {
|
|
94
|
+
coverageByProperty = new Map();
|
|
95
|
+
coverageBySelector.set(normalizedSelector, coverageByProperty);
|
|
96
|
+
}
|
|
97
|
+
for (const declaration of declarations) {
|
|
98
|
+
const important = isImportantDeclaration(declaration);
|
|
99
|
+
for (const leafProperty of expandToOverridableProperties(declaration.property)) {
|
|
100
|
+
coverageByProperty.set(leafProperty, { important, ruleIndex });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return coverageBySelector;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Determines whether a later rule overrides every leaf property an earlier
|
|
110
|
+
* declaration writes to for the same selector, including via shorthand
|
|
111
|
+
* coverage, with an important earlier write requiring an important later
|
|
112
|
+
* write to beat it.
|
|
113
|
+
*
|
|
114
|
+
* @param {Map} coverageByProperty The leaf property coverage index for the selector.
|
|
115
|
+
* @param {object} declaration The earlier declaration to test.
|
|
116
|
+
* @param {number} ruleIndex The position of the rule holding the declaration.
|
|
117
|
+
* @param {boolean} declarationIsImportant Whether the earlier write of this property is important.
|
|
118
|
+
* @return {boolean} True when the declaration is conclusively overridden later.
|
|
119
|
+
*/
|
|
120
|
+
function isDeclarationOverriddenLater (coverageByProperty, declaration, ruleIndex, declarationIsImportant) {
|
|
121
|
+
for (const leafProperty of expandToOverridableProperties(declaration.property)) {
|
|
122
|
+
const coverage = coverageByProperty.get(leafProperty);
|
|
123
|
+
if (!coverage || coverage.ruleIndex <= ruleIndex) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
// An important declaration outlives a later non-important one
|
|
127
|
+
if (declarationIsImportant && !coverage.important) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Removes a selector from a multi-selector rule's list when every declaration
|
|
136
|
+
* the rule would still give that selector is overridden by a later rule that
|
|
137
|
+
* contains the same selector. For example, if `h1,h2{color:red}` is followed
|
|
138
|
+
* by `h2{color:tan}`, the `h2` entry contributes nothing and can be dropped,
|
|
139
|
+
* leaving `h1{color:red}`. If the list empties out entirely, the rule is
|
|
140
|
+
* stripped of declarations so `removeEmptyRules` discards it.
|
|
141
|
+
*
|
|
142
|
+
* @param {Array} rules The flat list of AST rule nodes.
|
|
143
|
+
* @return {Array} The rules with fully overridden selectors removed from their lists.
|
|
144
|
+
*/
|
|
145
|
+
function removeFullyOverriddenSelectors (rules) {
|
|
146
|
+
// Pruning a selector only ever shrinks lists: a covering entry that wins
|
|
147
|
+
// today is supplied by a later rule, and pruning earlier rules never moves
|
|
148
|
+
// coverage forward, so a single index built up front stays accurate.
|
|
149
|
+
const coverageBySelector = indexFinalCoveringRuleBySelector(rules);
|
|
150
|
+
|
|
151
|
+
for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex++) {
|
|
152
|
+
const rule = rules[ruleIndex];
|
|
153
|
+
if (rule.type !== 'rule' || !rule.selectors || rule.selectors.length < 2) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const entries = rule.declarations || [];
|
|
157
|
+
// Removing a selector would also un-scope nested rules, which may depend
|
|
158
|
+
// on it, so only rules made purely of declarations qualify
|
|
159
|
+
const hasNestedContent = entries.some((entry) => {
|
|
160
|
+
return entry.type !== 'declaration' && entry.type !== 'whitespace' && entry.type !== 'comment';
|
|
161
|
+
});
|
|
162
|
+
if (hasNestedContent) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const declarations = entries.filter((entry) => {
|
|
166
|
+
return entry.type === 'declaration' && entry.property;
|
|
167
|
+
});
|
|
168
|
+
if (!declarations.length) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// Within a rule, any important declaration of a property makes the rule's
|
|
172
|
+
// write of that property important (a conservative stand-in for tracking
|
|
173
|
+
// which of several same-property declarations survives inside the rule)
|
|
174
|
+
const importantByProperty = new Map();
|
|
175
|
+
for (const declaration of declarations) {
|
|
176
|
+
if (isImportantDeclaration(declaration)) {
|
|
177
|
+
importantByProperty.set(declaration.property, true);
|
|
178
|
+
} else if (!importantByProperty.has(declaration.property)) {
|
|
179
|
+
importantByProperty.set(declaration.property, false);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const keptSelectors = rule.selectors.filter((selector) => {
|
|
183
|
+
const coverageByProperty = coverageBySelector.get(normalizeSelector(selector));
|
|
184
|
+
if (!coverageByProperty) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
// Keep the selector while at least one declaration is not conclusively
|
|
188
|
+
// overridden for it by a later rule
|
|
189
|
+
return declarations.some((declaration) => {
|
|
190
|
+
return !isDeclarationOverriddenLater(
|
|
191
|
+
coverageByProperty,
|
|
192
|
+
declaration,
|
|
193
|
+
ruleIndex,
|
|
194
|
+
importantByProperty.get(declaration.property)
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
if (keptSelectors.length !== rule.selectors.length) {
|
|
199
|
+
if (!keptSelectors.length) {
|
|
200
|
+
rule.declarations = entries.filter((entry) => {
|
|
201
|
+
return entry.type !== 'declaration';
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
rule.selectors = keptSelectors;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return rules;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Removes properties from multi-selector rules when every selector in
|
|
212
|
+
* the rule has that property overridden by a later rule. For example,
|
|
213
|
+
* if `h1,h2{color:red}` is followed by `h1{color:blue}` and
|
|
214
|
+
* `h2{color:green}`, the `color` in the first rule is redundant and
|
|
215
|
+
* can be removed. If all properties are removed, the empty rule will
|
|
216
|
+
* be cleaned up by `removeEmptyRules`.
|
|
217
|
+
*
|
|
218
|
+
* @param {Array} rules The flat list of AST rule nodes.
|
|
219
|
+
* @return {Array} The rules with overridden multi-selector properties removed.
|
|
220
|
+
*/
|
|
221
|
+
function removeOverriddenMultiSelectorProperties (rules) {
|
|
222
|
+
// Only rules that follow the one being pruned are ever consulted, and pruning
|
|
223
|
+
// never adds a declaration, so a single index built up front stays accurate.
|
|
224
|
+
const lastRuleIndexBySelector = indexLastDeclaringRuleBySelector(rules);
|
|
225
|
+
|
|
226
|
+
for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex++) {
|
|
227
|
+
const rule = rules[ruleIndex];
|
|
228
|
+
if (rule.type !== 'rule' || !rule.selectors || rule.selectors.length < 2) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const normalizedSelectors = rule.selectors.map(normalizeSelector);
|
|
232
|
+
const declarations = (rule.declarations || []).filter((declaration) => {
|
|
233
|
+
return declaration.type === 'declaration';
|
|
234
|
+
});
|
|
235
|
+
if (declarations.length === 0) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// For each property in this multi-selector rule, check if ALL selectors
|
|
240
|
+
// get that property overridden in later rules
|
|
241
|
+
const propertiesToRemove = new Set();
|
|
242
|
+
for (const declaration of declarations) {
|
|
243
|
+
const property = declaration.property;
|
|
244
|
+
const allSelectorsOverridden = normalizedSelectors.every((selector) => {
|
|
245
|
+
return isSelectorPropertyOverriddenLater(lastRuleIndexBySelector, ruleIndex, selector, property);
|
|
246
|
+
});
|
|
247
|
+
if (allSelectorsOverridden) {
|
|
248
|
+
propertiesToRemove.add(property);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (propertiesToRemove.size > 0) {
|
|
253
|
+
rule.declarations = (rule.declarations || []).filter((declaration) => {
|
|
254
|
+
if (declaration.type !== 'declaration') {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
return !propertiesToRemove.has(declaration.property);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return rules;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export {
|
|
265
|
+
removeFullyOverriddenSelectors,
|
|
266
|
+
removeOverriddenMultiSelectorProperties
|
|
267
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Normalizes selectors into one comparable spelling, memoized per stylesheet, so rules that select the same elements are recognized as such.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Memoizes the normalized form of each selector seen during the current
|
|
7
|
+
* stylesheet.
|
|
8
|
+
*
|
|
9
|
+
* @type {Map<string, string>}
|
|
10
|
+
*/
|
|
11
|
+
const normalizedSelectorCache = new Map();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Discards every memoized normalized selector. The minifier calls this around
|
|
15
|
+
* each stylesheet, so the cache does not outlive the pass that filled it.
|
|
16
|
+
*/
|
|
17
|
+
function clearNormalizedSelectorCache () {
|
|
18
|
+
normalizedSelectorCache.clear();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalizes a selector string for consistent comparison by trimming
|
|
23
|
+
* and collapsing internal whitespace.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} selector The raw selector string.
|
|
26
|
+
* @return {string} The normalized selector.
|
|
27
|
+
*/
|
|
28
|
+
function computeNormalizedSelector (selector) {
|
|
29
|
+
// Collapse each whitespace run into a single space
|
|
30
|
+
const normalized = selector.trim().replace(/\s+/g, ' ');
|
|
31
|
+
if (!normalized.includes('::')) {
|
|
32
|
+
return normalized;
|
|
33
|
+
}
|
|
34
|
+
return normalized
|
|
35
|
+
// Convert double-colon ::before/::after to single-colon legacy form
|
|
36
|
+
.replace(/::before\b/g, ':before')
|
|
37
|
+
.replace(/::after\b/g, ':after');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Normalizes a selector string for consistent comparison, reusing the result
|
|
42
|
+
* when the same selector was already normalized during this stylesheet. Every
|
|
43
|
+
* merge pass compares the same selectors again, so each is only rewritten once.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} selector The raw selector string.
|
|
46
|
+
* @return {string} The normalized selector.
|
|
47
|
+
*/
|
|
48
|
+
function normalizeSelector (selector) {
|
|
49
|
+
let normalized = normalizedSelectorCache.get(selector);
|
|
50
|
+
if (normalized === undefined) {
|
|
51
|
+
normalized = computeNormalizedSelector(selector);
|
|
52
|
+
normalizedSelectorCache.set(selector, normalized);
|
|
53
|
+
}
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
clearNormalizedSelectorCache,
|
|
59
|
+
normalizeSelector
|
|
60
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Measures the specificity of selectors and selector lists, so rules can be regrouped and reordered without changing which declaration wins the cascade.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
findMatchingBracket,
|
|
7
|
+
findMatchingParenthesis,
|
|
8
|
+
splitTopLevelCommaList
|
|
9
|
+
} from '../../parser/source-search.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Advances past a CSS identifier (name) starting at the given index.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} text The selector text.
|
|
15
|
+
* @param {number} start The index to start scanning from.
|
|
16
|
+
* @return {number} The index just after the identifier.
|
|
17
|
+
*/
|
|
18
|
+
function skipSelectorName (text, start) {
|
|
19
|
+
let index = start;
|
|
20
|
+
// Advance over identifier characters (letters, digits, hyphen, underscore)
|
|
21
|
+
while (index < text.length && isSelectorNameCharacter(text.charCodeAt(index))) {
|
|
22
|
+
index++;
|
|
23
|
+
}
|
|
24
|
+
return index;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reports whether a character code is an ASCII letter.
|
|
29
|
+
*
|
|
30
|
+
* @param {number} characterCode The UTF-16 code unit of the character.
|
|
31
|
+
* @return {boolean} True for `a`-`z` and `A`-`Z`.
|
|
32
|
+
*/
|
|
33
|
+
function isAsciiLetter (characterCode) {
|
|
34
|
+
return (
|
|
35
|
+
(characterCode >= 97 && characterCode <= 122) ||
|
|
36
|
+
(characterCode >= 65 && characterCode <= 90)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Reports whether a character code can appear in a selector name, which here
|
|
42
|
+
* means an ASCII letter, digit, hyphen, or underscore. Comparing character
|
|
43
|
+
* codes avoids running a regular expression once per character.
|
|
44
|
+
*
|
|
45
|
+
* @param {number} characterCode The UTF-16 code unit of the character.
|
|
46
|
+
* @return {boolean} True when the character continues a name.
|
|
47
|
+
*/
|
|
48
|
+
function isSelectorNameCharacter (characterCode) {
|
|
49
|
+
const isDigit = characterCode >= 48 && characterCode <= 57;
|
|
50
|
+
const isHyphen = characterCode === 45;
|
|
51
|
+
const isUnderscore = characterCode === 95;
|
|
52
|
+
return isAsciiLetter(characterCode) || isDigit || isHyphen || isUnderscore;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Compares two specificity tuples [ids, classes, types] lexicographically.
|
|
57
|
+
*
|
|
58
|
+
* @param {Array} first The first specificity tuple.
|
|
59
|
+
* @param {Array} second The second specificity tuple.
|
|
60
|
+
* @return {number} Negative, zero, or positive per standard comparison.
|
|
61
|
+
*/
|
|
62
|
+
function compareSpecificity (first, second) {
|
|
63
|
+
for (let index = 0; index < 3; index++) {
|
|
64
|
+
if (first[index] !== second[index]) {
|
|
65
|
+
return first[index] - second[index];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Computes the CSS specificity of a single complex selector as an
|
|
73
|
+
* [ids, classes, types] tuple. The nesting selector `&` is ignored because its
|
|
74
|
+
* contribution comes from the shared parent when comparing sibling selectors.
|
|
75
|
+
* Functional pseudo-classes `:is()`, `:not()`, `:has()`, and `:matches()` add
|
|
76
|
+
* the maximum specificity of their arguments; `:where()` adds nothing.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} selector A single complex selector string.
|
|
79
|
+
* @return {Array} The [ids, classes, types] specificity tuple.
|
|
80
|
+
*/
|
|
81
|
+
function computeSpecificity (selector) {
|
|
82
|
+
const specificity = [0, 0, 0];
|
|
83
|
+
const text = selector.trim();
|
|
84
|
+
let index = 0;
|
|
85
|
+
while (index < text.length) {
|
|
86
|
+
const character = text[index];
|
|
87
|
+
if (character === '#') {
|
|
88
|
+
specificity[0]++;
|
|
89
|
+
index = skipSelectorName(text, index + 1);
|
|
90
|
+
} else if (character === '.') {
|
|
91
|
+
specificity[1]++;
|
|
92
|
+
index = skipSelectorName(text, index + 1);
|
|
93
|
+
} else if (character === '[') {
|
|
94
|
+
specificity[1]++;
|
|
95
|
+
// An attribute selector that never closes runs to the end of the selector
|
|
96
|
+
const closeBracketIndex = findMatchingBracket(text, index);
|
|
97
|
+
index = closeBracketIndex === -1 ? text.length : closeBracketIndex + 1;
|
|
98
|
+
} else if (character === ':') {
|
|
99
|
+
if (text[index + 1] === ':') {
|
|
100
|
+
specificity[2]++;
|
|
101
|
+
index = skipSelectorName(text, index + 2);
|
|
102
|
+
} else {
|
|
103
|
+
const nameStart = index + 1;
|
|
104
|
+
const nameEnd = skipSelectorName(text, nameStart);
|
|
105
|
+
const name = text.slice(nameStart, nameEnd).toLowerCase();
|
|
106
|
+
if (text[nameEnd] === '(') {
|
|
107
|
+
// An argument list that never closes runs to the end of the selector
|
|
108
|
+
const closeIndex = findMatchingParenthesis(text, nameEnd);
|
|
109
|
+
const argumentsEnd = closeIndex === -1 ? text.length : closeIndex;
|
|
110
|
+
const inner = text.slice(nameEnd + 1, argumentsEnd);
|
|
111
|
+
if (name === 'where') {
|
|
112
|
+
// :where() contributes zero specificity
|
|
113
|
+
} else if (name === 'is' || name === 'not' || name === 'has' || name === 'matches') {
|
|
114
|
+
const innerMax = maxSelectorSpecificity(inner);
|
|
115
|
+
specificity[0] += innerMax[0];
|
|
116
|
+
specificity[1] += innerMax[1];
|
|
117
|
+
specificity[2] += innerMax[2];
|
|
118
|
+
} else {
|
|
119
|
+
specificity[1]++;
|
|
120
|
+
}
|
|
121
|
+
index = argumentsEnd + 1;
|
|
122
|
+
} else {
|
|
123
|
+
// Legacy single-colon pseudo-elements count as pseudo-elements
|
|
124
|
+
if (name === 'before' || name === 'after' || name === 'first-line' || name === 'first-letter') {
|
|
125
|
+
specificity[2]++;
|
|
126
|
+
} else {
|
|
127
|
+
specificity[1]++;
|
|
128
|
+
}
|
|
129
|
+
index = nameEnd;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} else if (character === '*' || character === '&') {
|
|
133
|
+
// Universal selector and nesting selector add no counted specificity here
|
|
134
|
+
index++;
|
|
135
|
+
} else if (isAsciiLetter(text.charCodeAt(index))) {
|
|
136
|
+
specificity[2]++;
|
|
137
|
+
index = skipSelectorName(text, index);
|
|
138
|
+
} else {
|
|
139
|
+
// Combinators and whitespace do not affect specificity
|
|
140
|
+
index++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return specificity;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Memoizes the strongest and weakest specificity of each selector list seen
|
|
148
|
+
* during the current stylesheet. The same rules are measured again on every
|
|
149
|
+
* merge pass, and a selector list always has the same specificity, so each
|
|
150
|
+
* distinct list is only split and scanned once per stylesheet.
|
|
151
|
+
*
|
|
152
|
+
* @type {Map<string, {strongest: Array, weakest: Array}>}
|
|
153
|
+
*/
|
|
154
|
+
const specificityRangeCache = new Map();
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Discards every memoized specificity range. The minifier calls this around
|
|
158
|
+
* each stylesheet, so the cache does not outlive the pass that filled it.
|
|
159
|
+
*/
|
|
160
|
+
function clearSpecificityCache () {
|
|
161
|
+
specificityRangeCache.clear();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Scans every selector of a comma-separated list for the strongest and the
|
|
166
|
+
* weakest specificity among them. The returned tuples are frozen because they
|
|
167
|
+
* are shared by everything that asks about the same list.
|
|
168
|
+
*
|
|
169
|
+
* @param {string} selectorList The selector list string.
|
|
170
|
+
* @return {object} The `strongest` and `weakest` [ids, classes, types] tuples.
|
|
171
|
+
*/
|
|
172
|
+
function computeSpecificityRange (selectorList) {
|
|
173
|
+
let strongest = [0, 0, 0];
|
|
174
|
+
let weakest = null;
|
|
175
|
+
for (const selector of splitTopLevelCommaList(selectorList)) {
|
|
176
|
+
const specificity = computeSpecificity(selector);
|
|
177
|
+
if (compareSpecificity(specificity, strongest) > 0) {
|
|
178
|
+
strongest = specificity;
|
|
179
|
+
}
|
|
180
|
+
if (!weakest || compareSpecificity(specificity, weakest) < 0) {
|
|
181
|
+
weakest = specificity;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
strongest: Object.freeze(strongest),
|
|
186
|
+
weakest: Object.freeze(weakest || [0, 0, 0])
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Returns the strongest and weakest specificity of a selector list, measuring
|
|
192
|
+
* it the first time it is asked about.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} selectorList The selector list string.
|
|
195
|
+
* @return {object} The `strongest` and `weakest` [ids, classes, types] tuples.
|
|
196
|
+
*/
|
|
197
|
+
function getSpecificityRange (selectorList) {
|
|
198
|
+
let range = specificityRangeCache.get(selectorList);
|
|
199
|
+
if (!range) {
|
|
200
|
+
range = computeSpecificityRange(selectorList);
|
|
201
|
+
specificityRangeCache.set(selectorList, range);
|
|
202
|
+
}
|
|
203
|
+
return range;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Returns the maximum specificity tuple across a comma-separated selector list,
|
|
208
|
+
* matching how a comma-separated group behaves as `:is()`.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} selectorList The selector list string.
|
|
211
|
+
* @return {Array} The maximum [ids, classes, types] tuple.
|
|
212
|
+
*/
|
|
213
|
+
function maxSelectorSpecificity (selectorList) {
|
|
214
|
+
return getSpecificityRange(selectorList).strongest;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Returns the strongest specificity a rule can match with, which is the highest
|
|
219
|
+
* of its individual selectors.
|
|
220
|
+
*
|
|
221
|
+
* @param {Array} selectors The rule's selector strings.
|
|
222
|
+
* @return {Array} The [ids, classes, types] specificity tuple.
|
|
223
|
+
*/
|
|
224
|
+
function maximumRuleSpecificity (selectors) {
|
|
225
|
+
return maxSelectorSpecificity((selectors || []).join(','));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Returns the weakest specificity a rule's declarations are applied with. Every
|
|
230
|
+
* selector of a top-level rule applies the same declarations under its own
|
|
231
|
+
* specificity, so the weakest selector decides whether another rule is able to
|
|
232
|
+
* override the rule as a whole.
|
|
233
|
+
*
|
|
234
|
+
* @param {Array} selectors The rule's selector strings.
|
|
235
|
+
* @return {Array} The [ids, classes, types] specificity tuple.
|
|
236
|
+
*/
|
|
237
|
+
function minimumRuleSpecificity (selectors) {
|
|
238
|
+
return getSpecificityRange((selectors || []).join(',')).weakest;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Adds two specificity tuples together, which is how a nested selector combines
|
|
243
|
+
* with the parent selector it only ever matches through.
|
|
244
|
+
*
|
|
245
|
+
* @param {Array} first The first [ids, classes, types] tuple.
|
|
246
|
+
* @param {Array} second The second [ids, classes, types] tuple.
|
|
247
|
+
* @return {Array} The summed specificity tuple.
|
|
248
|
+
*/
|
|
249
|
+
function addSpecificity (first, second) {
|
|
250
|
+
return [
|
|
251
|
+
first[0] + second[0],
|
|
252
|
+
first[1] + second[1],
|
|
253
|
+
first[2] + second[2]
|
|
254
|
+
];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Reports whether a rule that nests blocks may take on further selectors. A
|
|
259
|
+
* nested selector resolves against its parent's whole selector list as `:is()`,
|
|
260
|
+
* which matches with the specificity of the strongest selector in that list, so
|
|
261
|
+
* the list may only grow while every selector in it weighs the same.
|
|
262
|
+
*
|
|
263
|
+
* @param {Array} selectors The selector list the merged rule would carry.
|
|
264
|
+
* @return {boolean} Whether the nested blocks keep their current weight.
|
|
265
|
+
*/
|
|
266
|
+
function selectorsShareSpecificity (selectors) {
|
|
267
|
+
const strongest = maximumRuleSpecificity(selectors);
|
|
268
|
+
const weakest = minimumRuleSpecificity(selectors);
|
|
269
|
+
return compareSpecificity(strongest, weakest) === 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export {
|
|
273
|
+
addSpecificity,
|
|
274
|
+
clearSpecificityCache,
|
|
275
|
+
compareSpecificity,
|
|
276
|
+
maximumRuleSpecificity,
|
|
277
|
+
maxSelectorSpecificity,
|
|
278
|
+
minimumRuleSpecificity,
|
|
279
|
+
selectorsShareSpecificity
|
|
280
|
+
};
|
|
@@ -2,7 +2,7 @@
|
|
|
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 './
|
|
5
|
+
import { registersCustomProperty } from './property.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Scans top-level rules to register `@property` custom properties in the context and collect `@position-try` rule declarations and initial usage counts.
|
package/src/rules/stringify.js
CHANGED
|
@@ -465,6 +465,19 @@ function stringifyRule (rule, context) {
|
|
|
465
465
|
|
|
466
466
|
declarations = processDeclarations(declarations, context);
|
|
467
467
|
|
|
468
|
+
// Check if the rule is now effectively empty after processing declarations
|
|
469
|
+
const isEffectivelyEmptyAfterProcessing = (
|
|
470
|
+
declarations.length === 0 ||
|
|
471
|
+
declarations.every((declaration) => {
|
|
472
|
+
return (
|
|
473
|
+
declaration.type === 'comment' && !declaration.comment?.startsWith('!')
|
|
474
|
+
);
|
|
475
|
+
})
|
|
476
|
+
);
|
|
477
|
+
if (isEffectivelyEmptyAfterProcessing) {
|
|
478
|
+
return '';
|
|
479
|
+
}
|
|
480
|
+
|
|
468
481
|
// We need to properly output nested rules vs normal declarations.
|
|
469
482
|
// Wait, the processDeclarations will now correctly keep 'rule' types because we push them in processDeclarations.
|
|
470
483
|
// If we have `.foo { .bar { color: red; } }`, the inner rule is a declaration of type 'rule' with selectors: ['.bar'].
|
package/src/utilities.js
CHANGED
|
@@ -19,6 +19,20 @@ function resolveUnicodeEscape (hex) {
|
|
|
19
19
|
return String.fromCodePoint(codePoint);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Reports whether a property is a custom property. A custom property holds an
|
|
24
|
+
* arbitrary token stream rather than a typed value, so every one of its tokens,
|
|
25
|
+
* whitespace included, is part of what it stores, and neither shorthands nor
|
|
26
|
+
* `all` ever reset it.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} property The CSS property name.
|
|
29
|
+
* @return {boolean} Whether the property is a custom property.
|
|
30
|
+
*/
|
|
31
|
+
function isCustomProperty (property) {
|
|
32
|
+
return Boolean(property) && property.startsWith('--');
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
export {
|
|
36
|
+
isCustomProperty,
|
|
23
37
|
resolveUnicodeEscape
|
|
24
38
|
};
|