@thejaredwilcurt/csslop 0.0.24 → 0.0.25
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 +4 -0
- package/package.json +5 -3
- package/src/declarations/background.js +26 -1
- package/src/declarations/config.js +60 -0
- package/src/declarations/css-wide-keywords.js +47 -25
- package/src/declarations/lookup.js +44 -0
- package/src/declarations/merge.js +27 -30
- package/src/declarations/order.js +32 -6
- package/src/declarations/process.js +146 -55
- package/src/index.js +29 -5
- package/src/rules/optimize.js +198 -78
- package/src/rules/stringify.js +11 -4
- 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,
|
package/src/rules/stringify.js
CHANGED
|
@@ -41,6 +41,14 @@ function stringifyDeclarations (declarations) {
|
|
|
41
41
|
.join(';');
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The heading element selectors, which collapse into the `:heading`
|
|
46
|
+
* pseudo-class when a rule targets every one of them.
|
|
47
|
+
*
|
|
48
|
+
* @type {Set<string>}
|
|
49
|
+
*/
|
|
50
|
+
const HEADING_SELECTORS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
|
|
51
|
+
|
|
44
52
|
/**
|
|
45
53
|
* Matches a complete `@layer` statement, which declares layer names without a
|
|
46
54
|
* block and ends with the semicolon that separates it from the CSS that follows
|
|
@@ -263,12 +271,11 @@ function stringifyRule (rule, context) {
|
|
|
263
271
|
}
|
|
264
272
|
uniqueSelectors = uniqueSelectors.flatMap(processIsSelector);
|
|
265
273
|
uniqueSelectors = [...new Set(uniqueSelectors)];
|
|
266
|
-
const headingSet = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
|
|
267
274
|
const isAllHeadings = (
|
|
268
|
-
rule.selectors.length ===
|
|
269
|
-
uniqueSelectors.length ===
|
|
275
|
+
rule.selectors.length === HEADING_SELECTORS.size &&
|
|
276
|
+
uniqueSelectors.length === HEADING_SELECTORS.size &&
|
|
270
277
|
uniqueSelectors.every((selector) => {
|
|
271
|
-
return
|
|
278
|
+
return HEADING_SELECTORS.has(selector);
|
|
272
279
|
})
|
|
273
280
|
);
|
|
274
281
|
if (isAllHeadings) {
|
package/src/value/gradients.js
CHANGED
|
@@ -350,6 +350,88 @@ function combineAdjacentIdenticalStops (args) {
|
|
|
350
350
|
return normalizeBoundaryPositionTokens(mergedStops);
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
/**
|
|
354
|
+
* The `to <side>` linear-gradient directions that are shorter to write as an
|
|
355
|
+
* angle, keyed by the normalized keyword form. Corner directions are listed
|
|
356
|
+
* under both keyword orders, since either spells the same corner.
|
|
357
|
+
*
|
|
358
|
+
* @type {Map<string, string>}
|
|
359
|
+
*/
|
|
360
|
+
const LINEAR_DIRECTION_ANGLES = new Map([
|
|
361
|
+
['to top', '0deg'],
|
|
362
|
+
['to right', '90deg'],
|
|
363
|
+
['to left', '270deg'],
|
|
364
|
+
['to top right', '45deg'],
|
|
365
|
+
['to right top', '45deg'],
|
|
366
|
+
['to bottom right', '135deg'],
|
|
367
|
+
['to right bottom', '135deg'],
|
|
368
|
+
['to bottom left', '225deg'],
|
|
369
|
+
['to left bottom', '225deg'],
|
|
370
|
+
['to top left', '315deg'],
|
|
371
|
+
['to left top', '315deg']
|
|
372
|
+
]);
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The linear-gradient directions that are already the default, so writing them
|
|
376
|
+
* out adds nothing.
|
|
377
|
+
*
|
|
378
|
+
* @type {Set<string>}
|
|
379
|
+
*/
|
|
380
|
+
const DEFAULT_LINEAR_DIRECTIONS = new Set(['to bottom', '180deg']);
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* The radial-gradient shapes that are already the default.
|
|
384
|
+
*
|
|
385
|
+
* @type {Set<string>}
|
|
386
|
+
*/
|
|
387
|
+
const DEFAULT_RADIAL_SHAPES = new Set(['ellipse at center', 'circle at center']);
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Rewrites the leading direction argument of a linear gradient into its
|
|
391
|
+
* shortest form, dropping it when it is the default.
|
|
392
|
+
*
|
|
393
|
+
* @param {Array} args The gradient arguments, rewritten in place.
|
|
394
|
+
* @return {number} The number of leading arguments that are not color stops.
|
|
395
|
+
*/
|
|
396
|
+
function normalizeLinearGradientDirection (args) {
|
|
397
|
+
const firstDirection = args[0].toLowerCase().replace(/\s+/g, ' ').trim();
|
|
398
|
+
if (DEFAULT_LINEAR_DIRECTIONS.has(firstDirection)) {
|
|
399
|
+
args.shift();
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
402
|
+
const angle = LINEAR_DIRECTION_ANGLES.get(firstDirection);
|
|
403
|
+
if (angle) {
|
|
404
|
+
args[0] = angle;
|
|
405
|
+
return 1;
|
|
406
|
+
}
|
|
407
|
+
// Check if first arg looks like a direction (angle or "to ..." keyword)
|
|
408
|
+
const looksLikeDirection = /^\d+(\.\d+)?deg$/i.test(firstDirection) || firstDirection.startsWith('to ');
|
|
409
|
+
if (looksLikeDirection) {
|
|
410
|
+
return 1;
|
|
411
|
+
}
|
|
412
|
+
return 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Drops the leading shape argument of a radial gradient when it is the default.
|
|
417
|
+
*
|
|
418
|
+
* @param {Array} args The gradient arguments, rewritten in place.
|
|
419
|
+
* @return {number} The number of leading arguments that are not color stops.
|
|
420
|
+
*/
|
|
421
|
+
function normalizeRadialGradientShape (args) {
|
|
422
|
+
const firstShape = args[0].toLowerCase().replace(/\s+/g, ' ').trim();
|
|
423
|
+
if (DEFAULT_RADIAL_SHAPES.has(firstShape)) {
|
|
424
|
+
args.shift();
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
// Check if first arg is a radial shape/size descriptor
|
|
428
|
+
const looksLikeShape = /\b(circle|ellipse|closest|farthest|at)\b/i.test(firstShape);
|
|
429
|
+
if (looksLikeShape) {
|
|
430
|
+
return 1;
|
|
431
|
+
}
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
|
|
353
435
|
/**
|
|
354
436
|
* Optimizes gradient arguments by removing default direction or shape keywords, combining adjacent identical color stops, and trimming redundant 0% or 100% stop positions from the first and last stops.
|
|
355
437
|
*
|
|
@@ -363,52 +445,11 @@ function processGradientArgs (func, argsStr) {
|
|
|
363
445
|
|
|
364
446
|
let directionArgCount = 0;
|
|
365
447
|
|
|
366
|
-
if (
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
} else if (firstDirection === 'to top') {
|
|
372
|
-
args[0] = '0deg';
|
|
373
|
-
directionArgCount = 1;
|
|
374
|
-
} else if (firstDirection === 'to right') {
|
|
375
|
-
args[0] = '90deg';
|
|
376
|
-
directionArgCount = 1;
|
|
377
|
-
} else if (firstDirection === 'to left') {
|
|
378
|
-
args[0] = '270deg';
|
|
379
|
-
directionArgCount = 1;
|
|
380
|
-
} else if (firstDirection === 'to top right' || firstDirection === 'to right top') {
|
|
381
|
-
args[0] = '45deg';
|
|
382
|
-
directionArgCount = 1;
|
|
383
|
-
} else if (firstDirection === 'to bottom right' || firstDirection === 'to right bottom') {
|
|
384
|
-
args[0] = '135deg';
|
|
385
|
-
directionArgCount = 1;
|
|
386
|
-
} else if (firstDirection === 'to bottom left' || firstDirection === 'to left bottom') {
|
|
387
|
-
args[0] = '225deg';
|
|
388
|
-
directionArgCount = 1;
|
|
389
|
-
} else if (firstDirection === 'to top left' || firstDirection === 'to left top') {
|
|
390
|
-
args[0] = '315deg';
|
|
391
|
-
directionArgCount = 1;
|
|
392
|
-
} else {
|
|
393
|
-
// Check if first arg looks like a direction (angle or "to ..." keyword)
|
|
394
|
-
const looksLikeDirection = /^\d+(\.\d+)?deg$/i.test(firstDirection) || firstDirection.startsWith('to ');
|
|
395
|
-
if (looksLikeDirection) {
|
|
396
|
-
directionArgCount = 1;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
} else if (functionLower.includes('radial')) {
|
|
401
|
-
if (args.length > 1) {
|
|
402
|
-
const firstShape = args[0].toLowerCase().replace(/\s+/g, ' ').trim();
|
|
403
|
-
if (firstShape === 'ellipse at center' || firstShape === 'circle at center') {
|
|
404
|
-
args.shift();
|
|
405
|
-
} else {
|
|
406
|
-
// Check if first arg is a radial shape/size descriptor
|
|
407
|
-
const looksLikeShape = /\b(circle|ellipse|closest|farthest|at)\b/i.test(firstShape);
|
|
408
|
-
if (looksLikeShape) {
|
|
409
|
-
directionArgCount = 1;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
448
|
+
if (args.length > 1) {
|
|
449
|
+
if (functionLower.includes('linear')) {
|
|
450
|
+
directionArgCount = normalizeLinearGradientDirection(args);
|
|
451
|
+
} else if (functionLower.includes('radial')) {
|
|
452
|
+
directionArgCount = normalizeRadialGradientShape(args);
|
|
412
453
|
}
|
|
413
454
|
}
|
|
414
455
|
|
package/src/value/math.js
CHANGED
|
@@ -10,6 +10,21 @@ import {
|
|
|
10
10
|
roundCompactNumber
|
|
11
11
|
} from './shared.js';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The units a folded calc() expression leads with, in the order they are
|
|
15
|
+
* written. Every other unit follows them alphabetically.
|
|
16
|
+
*
|
|
17
|
+
* @type {Array}
|
|
18
|
+
*/
|
|
19
|
+
const PREFERRED_UNIT_ORDER = ['%', '', 'px'];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The same leading units, for excluding them from the alphabetical remainder.
|
|
23
|
+
*
|
|
24
|
+
* @type {Set<string>}
|
|
25
|
+
*/
|
|
26
|
+
const PREFERRED_UNITS = new Set(PREFERRED_UNIT_ORDER);
|
|
27
|
+
|
|
13
28
|
/**
|
|
14
29
|
* Attempts to simplify a calc() expression by combining like-unit terms and evaluating pure arithmetic, returning the simplified string or null if folding is not possible.
|
|
15
30
|
*
|
|
@@ -89,8 +104,8 @@ function tryFoldCalcExpression (expression) {
|
|
|
89
104
|
totals.set(unit, (totals.get(unit) || 0) + number);
|
|
90
105
|
}
|
|
91
106
|
|
|
92
|
-
const orderedUnits = [
|
|
93
|
-
return !
|
|
107
|
+
const orderedUnits = [...PREFERRED_UNIT_ORDER, ...[...totals.keys()].filter((unit) => {
|
|
108
|
+
return !PREFERRED_UNITS.has(unit);
|
|
94
109
|
}).sort()];
|
|
95
110
|
const outputTerms = [];
|
|
96
111
|
|