@thejaredwilcurt/csslop 0.0.49 → 0.0.50
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 +1 -1
- package/src/declarations/border.js +1 -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/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,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Indexes which properties each rule writes to and how strongly, so a merge can ask whether moving declarations past later rules would change the cascade.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { isCustomProperty } from '../../utilities.js';
|
|
6
|
+
|
|
7
|
+
import { expandToOverridableProperties } from './declaration-writes.js';
|
|
8
|
+
import {
|
|
9
|
+
addSpecificity,
|
|
10
|
+
compareSpecificity,
|
|
11
|
+
maximumRuleSpecificity,
|
|
12
|
+
minimumRuleSpecificity
|
|
13
|
+
} from './specificity.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The property that rewrites every other standard property, and so conflicts
|
|
17
|
+
* with any declaration it is ordered against.
|
|
18
|
+
*
|
|
19
|
+
* @type {string}
|
|
20
|
+
*/
|
|
21
|
+
const RESET_ALL_PROPERTY = 'all';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Index key standing for "declares anything at all", used to order a rule that
|
|
25
|
+
* sets `all` against every other declaration.
|
|
26
|
+
*
|
|
27
|
+
* @type {symbol}
|
|
28
|
+
*/
|
|
29
|
+
const ANY_PROPERTY = Symbol('any property');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {object} RuleWriteProfile
|
|
33
|
+
* @property {Map} specificityByProperty The strongest specificity each leaf property is written with.
|
|
34
|
+
* @property {Array} weakestSpecificity The weakest specificity the rule's declarations are applied with.
|
|
35
|
+
* @property {Array} strongestSpecificity The strongest specificity anything in the rule is written with.
|
|
36
|
+
* @property {boolean} writesStandardProperty Whether the rule writes any property other than a custom property.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Notes in a profile that a leaf property is written with a specificity,
|
|
41
|
+
* keeping only the strongest specificity seen for it.
|
|
42
|
+
*
|
|
43
|
+
* @param {RuleWriteProfile} profile The profile to extend.
|
|
44
|
+
* @param {string} property The leaf property name.
|
|
45
|
+
* @param {Array} specificity The specificity the property is written with.
|
|
46
|
+
*/
|
|
47
|
+
function addWrittenProperty (profile, property, specificity) {
|
|
48
|
+
const strongestSoFar = profile.specificityByProperty.get(property);
|
|
49
|
+
if (!strongestSoFar || compareSpecificity(specificity, strongestSoFar) > 0) {
|
|
50
|
+
profile.specificityByProperty.set(property, specificity);
|
|
51
|
+
}
|
|
52
|
+
if (compareSpecificity(specificity, profile.strongestSpecificity) > 0) {
|
|
53
|
+
profile.strongestSpecificity = specificity;
|
|
54
|
+
}
|
|
55
|
+
profile.writesStandardProperty = profile.writesStandardProperty || !isCustomProperty(property);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Adds every leaf property a rule's declarations write to into a profile,
|
|
60
|
+
* descending into nested rules, whose declarations only ever match through the
|
|
61
|
+
* parent and so carry the specificity of both selectors.
|
|
62
|
+
*
|
|
63
|
+
* @param {RuleWriteProfile} profile The profile to extend.
|
|
64
|
+
* @param {object} rule The AST rule node to read declarations from.
|
|
65
|
+
* @param {Array} specificity The specificity the rule's own declarations apply with.
|
|
66
|
+
*/
|
|
67
|
+
function addWrittenProperties (profile, rule, specificity) {
|
|
68
|
+
for (const declaration of rule.declarations || []) {
|
|
69
|
+
if (declaration.type === 'rule') {
|
|
70
|
+
const nestedSpecificity = addSpecificity(specificity, maximumRuleSpecificity(declaration.selectors));
|
|
71
|
+
addWrittenProperties(profile, declaration, nestedSpecificity);
|
|
72
|
+
} else if (declaration.type === 'declaration' && declaration.property) {
|
|
73
|
+
for (const leafProperty of expandToOverridableProperties(declaration.property)) {
|
|
74
|
+
addWrittenProperty(profile, leafProperty, specificity);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Summarizes which properties a rule writes to and how strongly, which is
|
|
82
|
+
* everything the cascade needs to know about a rule to decide whether it may be
|
|
83
|
+
* reordered against another one.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} rule The AST rule node.
|
|
86
|
+
* @return {RuleWriteProfile} The rule's write profile.
|
|
87
|
+
*/
|
|
88
|
+
function buildRuleWriteProfile (rule) {
|
|
89
|
+
const profile = {
|
|
90
|
+
specificityByProperty: new Map(),
|
|
91
|
+
weakestSpecificity: minimumRuleSpecificity(rule.selectors),
|
|
92
|
+
strongestSpecificity: [0, 0, 0],
|
|
93
|
+
writesStandardProperty: false
|
|
94
|
+
};
|
|
95
|
+
addWrittenProperties(profile, rule, maximumRuleSpecificity(rule.selectors));
|
|
96
|
+
return profile;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Folds one rule's writes into another's profile, which is how a profile stays
|
|
101
|
+
* current after a merge appends declarations to a rule. Both rules share the
|
|
102
|
+
* same selectors, so the absorbed writes keep their recorded specificity.
|
|
103
|
+
*
|
|
104
|
+
* @param {RuleWriteProfile} profile The profile of the rule that grew.
|
|
105
|
+
* @param {RuleWriteProfile} absorbedWrites The profile of the rule whose declarations were appended.
|
|
106
|
+
*/
|
|
107
|
+
function absorbWriteProfile (profile, absorbedWrites) {
|
|
108
|
+
for (const [property, specificity] of absorbedWrites.specificityByProperty) {
|
|
109
|
+
addWrittenProperty(profile, property, specificity);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Tracks which properties each rule writes to and where it sits, so that a
|
|
115
|
+
* merge can ask whether anything after a position could override the
|
|
116
|
+
* declarations it wants to move, instead of rescanning the stylesheet once per
|
|
117
|
+
* merge.
|
|
118
|
+
*
|
|
119
|
+
* Entries for a property are appended in increasing position order, and each
|
|
120
|
+
* new entry discards the earlier ones it dominates, since a declaration that is
|
|
121
|
+
* both later and at least as specific blocks everything a weaker, earlier one
|
|
122
|
+
* would. What remains is a list whose specificity strictly decreases as
|
|
123
|
+
* position grows, so the first entry past a queried position is also the
|
|
124
|
+
* strongest one past it.
|
|
125
|
+
*
|
|
126
|
+
* @param {Map} profileByRule The write profiles already built for rules, shared by every pass of one merge.
|
|
127
|
+
* @return {object} An index exposing `recordRule` and `blocksRelocation`.
|
|
128
|
+
*/
|
|
129
|
+
function createOverrideIndex (profileByRule) {
|
|
130
|
+
const entriesByProperty = new Map();
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Records that a property is written at a position with a given specificity.
|
|
134
|
+
*
|
|
135
|
+
* @param {string|symbol} property The leaf property name, or an index key.
|
|
136
|
+
* @param {number} position The slot the writing rule occupies.
|
|
137
|
+
* @param {Array} specificity The specificity the declaration applies with.
|
|
138
|
+
*/
|
|
139
|
+
function addEntry (property, position, specificity) {
|
|
140
|
+
let entries = entriesByProperty.get(property);
|
|
141
|
+
if (!entries) {
|
|
142
|
+
entries = [];
|
|
143
|
+
entriesByProperty.set(property, entries);
|
|
144
|
+
}
|
|
145
|
+
while (entries.length && compareSpecificity(entries[entries.length - 1].specificity, specificity) <= 0) {
|
|
146
|
+
entries.pop();
|
|
147
|
+
}
|
|
148
|
+
entries.push({ position, specificity });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Whether some rule after a position writes to a property with at least a
|
|
153
|
+
* given specificity, and could therefore win the cascade against it.
|
|
154
|
+
*
|
|
155
|
+
* @param {string|symbol} property The leaf property name, or an index key.
|
|
156
|
+
* @param {number} position The position to search after.
|
|
157
|
+
* @param {Array} specificity The specificity to compare against.
|
|
158
|
+
* @return {boolean} True when a later rule could override the property.
|
|
159
|
+
*/
|
|
160
|
+
function hasStrongerEntryAfter (property, position, specificity) {
|
|
161
|
+
const entries = entriesByProperty.get(property);
|
|
162
|
+
if (!entries) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
let low = 0;
|
|
166
|
+
let high = entries.length;
|
|
167
|
+
while (low < high) {
|
|
168
|
+
const middle = Math.floor((low + high) / 2);
|
|
169
|
+
if (entries[middle].position > position) {
|
|
170
|
+
high = middle;
|
|
171
|
+
} else {
|
|
172
|
+
low = middle + 1;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (low === entries.length) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return compareSpecificity(entries[low].specificity, specificity) >= 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Returns a rule's write profile, building it the first time it is needed.
|
|
183
|
+
* Rules only ever grow here, by absorbing another rule's declarations, and
|
|
184
|
+
* `absorbRule` folds those into the profile, so a profile stays valid for
|
|
185
|
+
* every pass of the merge.
|
|
186
|
+
*
|
|
187
|
+
* @param {object} rule The AST rule node.
|
|
188
|
+
* @return {RuleWriteProfile} The rule's write profile.
|
|
189
|
+
*/
|
|
190
|
+
function getWriteProfile (rule) {
|
|
191
|
+
let profile = profileByRule.get(rule);
|
|
192
|
+
if (!profile) {
|
|
193
|
+
profile = buildRuleWriteProfile(rule);
|
|
194
|
+
profileByRule.set(rule, profile);
|
|
195
|
+
}
|
|
196
|
+
return profile;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Records every property a rule writes to at the position it occupies.
|
|
201
|
+
*
|
|
202
|
+
* @param {object} rule The AST rule node.
|
|
203
|
+
* @param {number} position The slot the rule occupies.
|
|
204
|
+
*/
|
|
205
|
+
function recordRule (rule, position) {
|
|
206
|
+
const profile = getWriteProfile(rule);
|
|
207
|
+
if (!profile.specificityByProperty.size) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
for (const [property, specificity] of profile.specificityByProperty) {
|
|
211
|
+
addEntry(property, position, specificity);
|
|
212
|
+
}
|
|
213
|
+
addEntry(ANY_PROPERTY, position, profile.strongestSpecificity);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Folds a rule's writes into the rule that just absorbed its declarations, so
|
|
218
|
+
* the grown rule is treated as writing to both sets of properties.
|
|
219
|
+
*
|
|
220
|
+
* @param {object} rule The rule that grew.
|
|
221
|
+
* @param {object} absorbedRule The rule whose declarations were appended to it.
|
|
222
|
+
*/
|
|
223
|
+
function absorbRule (rule, absorbedRule) {
|
|
224
|
+
absorbWriteProfile(getWriteProfile(rule), getWriteProfile(absorbedRule));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Whether moving a rule's declarations past everything recorded after a
|
|
229
|
+
* position would change which declaration wins the cascade. Only a rule that
|
|
230
|
+
* is strictly less specific is guaranteed to lose either way, so anything of
|
|
231
|
+
* equal or greater specificity that writes to the same properties blocks the
|
|
232
|
+
* move.
|
|
233
|
+
*
|
|
234
|
+
* @param {object} movingRule The rule whose declarations would relocate.
|
|
235
|
+
* @param {number} afterPosition The position the declarations would move across.
|
|
236
|
+
* @return {boolean} True when the relocation is unsafe.
|
|
237
|
+
*/
|
|
238
|
+
function blocksRelocation (movingRule, afterPosition) {
|
|
239
|
+
const profile = getWriteProfile(movingRule);
|
|
240
|
+
const specificity = profile.weakestSpecificity;
|
|
241
|
+
for (const property of profile.specificityByProperty.keys()) {
|
|
242
|
+
if (hasStrongerEntryAfter(property, afterPosition, specificity)) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (profile.writesStandardProperty && hasStrongerEntryAfter(RESET_ALL_PROPERTY, afterPosition, specificity)) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
return (
|
|
250
|
+
profile.specificityByProperty.has(RESET_ALL_PROPERTY) &&
|
|
251
|
+
hasStrongerEntryAfter(ANY_PROPERTY, afterPosition, specificity)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
absorbRule,
|
|
257
|
+
blocksRelocation,
|
|
258
|
+
recordRule
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export {
|
|
263
|
+
createOverrideIndex
|
|
264
|
+
};
|
|
@@ -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
|
+
};
|