@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.
Files changed (55) hide show
  1. package/README.md +0 -1
  2. package/package.json +1 -1
  3. package/src/declarations/border.js +1 -1
  4. package/src/index.js +7 -7
  5. package/src/rules/optimize/at-rules.js +113 -0
  6. package/src/rules/optimize/declaration-writes.js +70 -0
  7. package/src/rules/optimize/empty-rules.js +51 -0
  8. package/src/rules/optimize/expand-nesting.js +236 -0
  9. package/src/rules/optimize/factor-parents.js +343 -0
  10. package/src/rules/optimize/index.js +71 -0
  11. package/src/rules/optimize/merge-declarations.js +199 -0
  12. package/src/rules/optimize/merge-nested-rules.js +117 -0
  13. package/src/rules/optimize/merge-selectors.js +252 -0
  14. package/src/rules/optimize/nest-rules.js +202 -0
  15. package/src/rules/optimize/override-index.js +264 -0
  16. package/src/rules/optimize/prune-overridden.js +267 -0
  17. package/src/rules/optimize/selector-normalization.js +60 -0
  18. package/src/rules/optimize/specificity.js +280 -0
  19. package/src/{position-try.js → rules/position-try.js} +1 -1
  20. package/src/utilities.js +14 -0
  21. package/src/value/border.js +85 -0
  22. package/src/value/{colors.js → color/conversion.js} +3 -2
  23. package/src/value/color/functions.js +463 -0
  24. package/src/value/{light-dark.js → color/light-dark.js} +1 -1
  25. package/src/value/{color-mix.js → color/mix.js} +3 -3
  26. package/src/value/{relative-colors.js → color/relative.js} +3 -3
  27. package/src/value/color/shorten.js +93 -0
  28. package/src/value/gradients.js +1 -1
  29. package/src/value/hyphenate-limit-chars.js +72 -0
  30. package/src/value/initial-keywords.js +128 -0
  31. package/src/value/lengths.js +55 -0
  32. package/src/value/math/evaluate.js +36 -0
  33. package/src/value/math/fold.js +315 -0
  34. package/src/value/math/format.js +230 -0
  35. package/src/value/math/functions.js +302 -0
  36. package/src/value/math/index.js +219 -0
  37. package/src/value/math/parser.js +301 -0
  38. package/src/value/math/property-ranges.js +174 -0
  39. package/src/value/math/rewrite.js +222 -0
  40. package/src/value/math/tokenizer.js +160 -0
  41. package/src/value/math/unit-totals.js +194 -0
  42. package/src/value/minify.js +127 -1669
  43. package/src/value/numbers.js +28 -0
  44. package/src/value/positions.js +152 -0
  45. package/src/value/property-optimizations.js +485 -0
  46. package/src/value/segments.js +74 -0
  47. package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
  48. package/src/value/{svg-markup.js → svg/markup.js} +1 -1
  49. package/src/value/time.js +75 -0
  50. package/src/value/urls.js +242 -0
  51. package/src/value/whitespace.js +88 -0
  52. package/src/rules/optimize.js +0 -2314
  53. package/src/value/math-expression.js +0 -1156
  54. package/src/value/math.js +0 -886
  55. /package/src/value/{named-colors.js → color/named.js} +0 -0
@@ -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 './rules/property.js';
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/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
  };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @file Rewrites `border` and `outline` values into their shortest canonical form, stating the width before the style and dropping the default width.
3
+ */
4
+
5
+ /**
6
+ * Matches the keywords that state a border line style.
7
+ *
8
+ * @type {string}
9
+ */
10
+ const BORDER_STYLE_KEYWORD_SOURCE = 'solid|dashed|dotted|double|groove|ridge|inset|outset|hidden|none';
11
+
12
+ /**
13
+ * Matches the ways a border line width is stated: one of its three keywords, a
14
+ * length, or a math function that resolves to one.
15
+ *
16
+ * @type {string}
17
+ */
18
+ const BORDER_WIDTH_SOURCE = 'thin|medium|thick|[+-]?(?:\\d+|\\d*\\.\\d+)(?:e[+-]?\\d+)?[a-z]*|(?:calc|min|max|clamp)\\([^()]*\\)';
19
+
20
+ /**
21
+ * Matches a `border` value that states its line style in front of its line
22
+ * width, with the two captured so they can be swapped.
23
+ *
24
+ * @type {RegExp}
25
+ */
26
+ const BORDER_STYLE_BEFORE_WIDTH_PATTERN = new RegExp(
27
+ '^(' + BORDER_STYLE_KEYWORD_SOURCE + ')\\s+(' + BORDER_WIDTH_SOURCE + ')(?=\\s|$)',
28
+ 'i'
29
+ );
30
+
31
+ /**
32
+ * Rewrites a `border` value that leads with its line style into the canonical
33
+ * width-style-color order. The width has to be recognized before the two are
34
+ * swapped: `border` states its components in any order, so the component behind
35
+ * the style may be a color instead, as in `solid #8aadf4 1px`, and that one
36
+ * belongs behind the width rather than in front of it.
37
+ *
38
+ * @param {string} value The `border` value.
39
+ * @return {string} The value with its width stated before its style.
40
+ */
41
+ function reorderBorderWidthBeforeStyle (value) {
42
+ return value.replace(BORDER_STYLE_BEFORE_WIDTH_PATTERN, '$2 $1');
43
+ }
44
+
45
+ /**
46
+ * Removes the default `medium` line width from a `border` value, since the
47
+ * shorthand resets an unstated width to it anyway.
48
+ *
49
+ * @param {string} value The `border` value.
50
+ * @return {string} The value without its default line width.
51
+ */
52
+ function removeDefaultBorderWidth (value) {
53
+ // Match the medium keyword and the whitespace that separates it from the next component
54
+ return value.replace(/\bmedium\s+/g, '');
55
+ }
56
+
57
+ /**
58
+ * Matches a line style keyword written directly against one of the color
59
+ * keywords that can follow it, capturing both so a space can be put back
60
+ * between them.
61
+ *
62
+ * @type {RegExp}
63
+ */
64
+ const STYLE_TOUCHING_COLOR_PATTERN = new RegExp(
65
+ '\\b(' + BORDER_STYLE_KEYWORD_SOURCE + ')(red|green|olive|tan|transparent)\\b',
66
+ 'g'
67
+ );
68
+
69
+ /**
70
+ * Restores the space between an `outline` value's line style and a color
71
+ * keyword when the two have been written against each other, since together
72
+ * they would read as one unknown keyword.
73
+ *
74
+ * @param {string} value The `outline` value.
75
+ * @return {string} The value with its style and color keywords separated.
76
+ */
77
+ function separateOutlineStyleFromColor (value) {
78
+ return value.replace(STYLE_TOUCHING_COLOR_PATTERN, '$1 $2');
79
+ }
80
+
81
+ export {
82
+ removeDefaultBorderWidth,
83
+ reorderBorderWidthBeforeStyle,
84
+ separateOutlineStyleFromColor
85
+ };
@@ -2,8 +2,9 @@
2
2
  * @file Provides color parsing, color space conversion (sRGB, OKLab, OKLCH, HSL, HWB), and hex formatting for CSS value minification.
3
3
  */
4
4
 
5
- import { namedColors } from './named-colors.js';
6
- import { parseAlphaString } from './shared.js';
5
+ import { parseAlphaString } from '../shared.js';
6
+
7
+ import { namedColors } from './named.js';
7
8
 
8
9
  /**
9
10
  * Reverse lookup from "r,g,b" to the shortest CSS named color keyword.