@thejaredwilcurt/csslop 0.0.48 → 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 (61) hide show
  1. package/README.md +3 -4
  2. package/package.json +3 -3
  3. package/src/declarations/border.js +1 -1
  4. package/src/declarations/css-wide-keywords.js +37 -1
  5. package/src/declarations/fold.js +13 -3
  6. package/src/declarations/process.js +60 -7
  7. package/src/index.js +12 -7
  8. package/src/parser/source-search.js +6 -0
  9. package/src/rules/optimize/at-rules.js +113 -0
  10. package/src/rules/optimize/declaration-writes.js +70 -0
  11. package/src/rules/optimize/empty-rules.js +51 -0
  12. package/src/rules/optimize/expand-nesting.js +236 -0
  13. package/src/rules/optimize/factor-parents.js +343 -0
  14. package/src/rules/optimize/index.js +71 -0
  15. package/src/rules/optimize/merge-declarations.js +199 -0
  16. package/src/rules/optimize/merge-nested-rules.js +117 -0
  17. package/src/rules/optimize/merge-selectors.js +252 -0
  18. package/src/rules/optimize/nest-rules.js +202 -0
  19. package/src/rules/optimize/override-index.js +264 -0
  20. package/src/rules/optimize/prune-overridden.js +267 -0
  21. package/src/rules/optimize/selector-normalization.js +60 -0
  22. package/src/rules/optimize/specificity.js +280 -0
  23. package/src/{position-try.js → rules/position-try.js} +1 -1
  24. package/src/rules/selectors.js +9 -0
  25. package/src/rules/stringify.js +138 -76
  26. package/src/utilities.js +9 -8
  27. package/src/value/border.js +85 -0
  28. package/src/value/{colors.js → color/conversion.js} +3 -2
  29. package/src/value/color/functions.js +463 -0
  30. package/src/value/{light-dark.js → color/light-dark.js} +1 -1
  31. package/src/value/{color-mix.js → color/mix.js} +3 -3
  32. package/src/value/{relative-colors.js → color/relative.js} +3 -3
  33. package/src/value/color/shorten.js +93 -0
  34. package/src/value/gradients.js +1 -1
  35. package/src/value/hyphenate-limit-chars.js +72 -0
  36. package/src/value/initial-keywords.js +128 -0
  37. package/src/value/lengths.js +55 -0
  38. package/src/value/math/evaluate.js +36 -0
  39. package/src/value/math/fold.js +315 -0
  40. package/src/value/math/format.js +230 -0
  41. package/src/value/math/functions.js +302 -0
  42. package/src/value/math/index.js +219 -0
  43. package/src/value/math/parser.js +301 -0
  44. package/src/value/math/property-ranges.js +174 -0
  45. package/src/value/math/rewrite.js +222 -0
  46. package/src/value/math/tokenizer.js +160 -0
  47. package/src/value/math/unit-totals.js +194 -0
  48. package/src/value/minify.js +162 -1678
  49. package/src/value/numbers.js +28 -0
  50. package/src/value/positions.js +152 -0
  51. package/src/value/property-optimizations.js +485 -0
  52. package/src/value/segments.js +74 -0
  53. package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
  54. package/src/value/{svg-markup.js → svg/markup.js} +1 -1
  55. package/src/value/time.js +75 -0
  56. package/src/value/urls.js +242 -0
  57. package/src/value/whitespace.js +88 -0
  58. package/src/rules/optimize.js +0 -2031
  59. package/src/value/math-expression.js +0 -1156
  60. package/src/value/math.js +0 -886
  61. /package/src/value/{named-colors.js → color/named.js} +0 -0
@@ -276,6 +276,143 @@ function stringifyAtRule (rule, context) {
276
276
  const separator = minifiedPrelude ? ' ' : '';
277
277
  return '@' + rule.name + separator + minifiedPrelude + '{' + body + '}';
278
278
  }
279
+
280
+ /**
281
+ * Rewrites `:nth-*()` pseudo-classes into their shortest equivalent forms.
282
+ *
283
+ * @param {string} selector A selector with its whitespace already collapsed.
284
+ * @return {string} The selector with simplified `:nth-*()` pseudo-classes.
285
+ */
286
+ function simplifyNthPseudoClasses (selector) {
287
+ let minified = selector;
288
+ // Simplify :nth-child(2n+1) to :nth-child(odd)
289
+ minified = minified.replace(/:nth-child\(2n\s*\+\s*1\)/g, ':nth-child(odd)');
290
+ // Simplify :nth-child(2n+0) to :nth-child(2n)
291
+ minified = minified.replace(/:nth-child\(2n\s*\+\s*0\)/g, ':nth-child(2n)');
292
+ // Simplify :nth-child(1n) to :nth-child(n)
293
+ minified = minified.replace(/:nth-child\(1n\)/g, ':nth-child(n)');
294
+ // Remove unnecessary leading + sign from :nth-child()
295
+ minified = minified.replace(/:nth-child\(\+\s*(\d+)\)/g, ':nth-child($1)');
296
+ // Simplify :nth-child(0n+N) to :nth-child(N)
297
+ minified = minified.replace(/:nth-child\(0n\s*\+\s*(\d+)\)/g, ':nth-child($1)');
298
+ // Replace :nth-child(1) with :first-child
299
+ minified = minified.replace(/:nth-child\(1\)/g, ':first-child');
300
+ // Replace :nth-last-child(1) with :last-child
301
+ minified = minified.replace(/:nth-last-child\(1\)/g, ':last-child');
302
+ // Replace :nth-of-type(1) with :first-of-type
303
+ minified = minified.replace(/:nth-of-type\(1\)/g, ':first-of-type');
304
+ // Replace :nth-last-of-type(1) with :last-of-type
305
+ minified = minified.replace(/:nth-last-of-type\(1\)/g, ':last-of-type');
306
+ return minified;
307
+ }
308
+
309
+ /**
310
+ * Writes each attribute selector's value quoted or escaped, whichever is
311
+ * shorter, and removes the whitespace inside its brackets.
312
+ *
313
+ * @param {string} selector A selector with its whitespace already collapsed.
314
+ * @return {string} The selector with minified attribute selectors.
315
+ */
316
+ function minifyAttributeSelectors (selector) {
317
+ let minified = selector;
318
+ // Minify double-quoted attribute selectors: remove inner whitespace and escape when shorter
319
+ minified = minified.replace(/\[\s*([^=]+)\s*=\s*"(.*?)"\s*\]/g, (match, attribute, value) => {
320
+ // Escape special characters that require quoting (spaces, #, ., :, /, ;), and compare lengths
321
+ let escaped = value.replace(/([ #.:/;])/g, '\\$1');
322
+ if (escaped.length < value.length + 2) {
323
+ return '[' + attribute + '=' + escaped + ']';
324
+ }
325
+ return '[' + attribute + '="' + value + '"]';
326
+ });
327
+ // Minify single-quoted attribute selectors: remove inner whitespace and escape when shorter
328
+ minified = minified.replace(/\[\s*([^=]+)\s*=\s*'(.*?)'\s*\]/g, (match, attribute, value) => {
329
+ // Escape special characters that require quoting (spaces, #, ., :, /, ;), and compare lengths
330
+ let escaped = value.replace(/([ #.:/;])/g, '\\$1');
331
+ if (escaped.length < value.length + 2) {
332
+ return '[' + attribute + '=' + escaped + ']';
333
+ }
334
+ return '[' + attribute + '="' + value + '"]';
335
+ });
336
+ // Minify unquoted attribute selectors: quote when unescaping produces a shorter result
337
+ minified = minified.replace(/\[\s*([^=]+)\s*=\s*([^"'].*?)\s*\]/g, (match, attribute, value) => {
338
+ // Unescape special characters (spaces, #, ., :, /, ;) and compare with quoted form
339
+ let unescaped = value.replace(/\\([ #.:/;])/g, '$1');
340
+ if (unescaped.length + 2 < value.length) {
341
+ return '[' + attribute + '="' + unescaped + '"]';
342
+ }
343
+ return '[' + attribute + '=' + value + ']';
344
+ });
345
+ return minified;
346
+ }
347
+
348
+ /**
349
+ * Rewrites `:not()` pseudo-classes that negate a pseudo-class with a direct
350
+ * opposite, such as `:not(:enabled)`, into that opposite.
351
+ *
352
+ * @param {string} selector A selector with its whitespace already collapsed.
353
+ * @return {string} The selector with simplified negations.
354
+ */
355
+ function simplifyNegatedPseudoClasses (selector) {
356
+ let minified = selector;
357
+ // Minify logical combinations
358
+ // Replace :not(:invalid) with :valid, only directly after a form element type selector
359
+ minified = minified.replace(/(?<=\b(?:button|fieldset|form|input|select|textarea)):not\(:invalid\)/g, ':valid');
360
+ // Replace :not(:dir(ltr)) with :dir(rtl)
361
+ minified = minified.replace(/:not\(:dir\(ltr\)\)/g, ':dir(rtl)');
362
+ // Replace a double negation, :not(:not(X)), with the X it wraps
363
+ minified = minified.replace(/:not\(:not\((.*?)\)\)/g, '$1');
364
+ // Replace :not(:enabled) with :disabled
365
+ minified = minified.replace(/:not\(:enabled\)/g, ':disabled');
366
+ // Replace :not(:required) with :optional
367
+ minified = minified.replace(/:not\(:required\)/g, ':optional');
368
+ // Match a link element type selector at the start or after a combinator, with any attribute selectors and an optional :where( wrapper, followed by :not(:link)
369
+ minified = minified.replace(/(^|[\s,>+~])(a|area|link)(?:\[.*?\])*(?::where\()?:not\(:link\)\)?/g, (match) => {
370
+ return match.replace(':not(:link)', ':visited');
371
+ });
372
+ return minified;
373
+ }
374
+
375
+ /**
376
+ * Minifies a single selector's spacing, pseudo-classes, pseudo-elements, and
377
+ * attribute selectors. Most selectors hold none of the patterns a group of
378
+ * rewrites looks for, so each group only runs when its marker text is present.
379
+ *
380
+ * @param {string} selector A single selector from a rule's selector list.
381
+ * @return {string} The minified selector.
382
+ */
383
+ function minifySelector (selector) {
384
+ let minified = unescapeSelector(selector);
385
+ // Collapse whitespace to single space
386
+ minified = minified.replace(/\s+/g, ' ');
387
+ // Strip whitespace around selector combinators and commas
388
+ minified = minified.replace(/\s*([,>+~])\s*/g, '$1');
389
+ // Strip whitespace inside parentheses for pseudo-class arguments
390
+ minified = minified.replace(/\(\s+/g, '(').replace(/\s+\)/g, ')');
391
+ if (minified.includes(':nth-')) {
392
+ minified = simplifyNthPseudoClasses(minified);
393
+ }
394
+ if (minified.includes('::')) {
395
+ // Convert double-colon pseudo-elements to single-colon legacy form
396
+ minified = minified.replace(/::before/g, ':before');
397
+ minified = minified.replace(/::after/g, ':after');
398
+ }
399
+ if (minified.includes('*')) {
400
+ // Strip redundant universal selector `*` when it precedes an ID, class, or attribute selector
401
+ minified = minified.replace(/\*([#.[])/g, '$1');
402
+ }
403
+ if (minified.includes('[') && minified.includes('=')) {
404
+ minified = minifyAttributeSelectors(minified);
405
+ }
406
+ if (minified.includes(':not(')) {
407
+ minified = simplifyNegatedPseudoClasses(minified);
408
+ }
409
+ // Remove redundant leading "& " nesting selector
410
+ minified = minified.replace(/^& /, '');
411
+ minified = mergeAdjacentWherePseudoClasses(minified);
412
+ minified = minifyLanguageSelector(minified);
413
+ return minified;
414
+ }
415
+
279
416
  /**
280
417
  * Converts a parsed CSS AST rule node into a minified CSS string, dispatching to specialized handlers for each rule type including selectors, `@media`, `@keyframes`, `@layer`, and other at-rules.
281
418
  *
@@ -305,82 +442,7 @@ function stringifyRule (rule, context) {
305
442
  if (rule.selectors?.length) {
306
443
  let uniqueSelectors = [...new Set(rule.selectors)];
307
444
  // Minify spacing within selectors (e.g. inside :is(), :where(), etc)
308
- uniqueSelectors = uniqueSelectors.map((selector) => {
309
- let minified = unescapeSelector(selector);
310
- // Collapse whitespace to single space
311
- minified = minified.replace(/\s+/g, ' ');
312
- // Strip whitespace around selector combinators and commas
313
- minified = minified.replace(/\s*([,>+~])\s*/g, '$1');
314
- // Strip whitespace inside parentheses for pseudo-class arguments
315
- minified = minified.replace(/\(\s+/g, '(').replace(/\s+\)/g, ')');
316
- // Simplify :nth-child(2n+1) to :nth-child(odd)
317
- minified = minified.replace(/:nth-child\(2n\s*\+\s*1\)/g, ':nth-child(odd)');
318
- // Simplify :nth-child(2n+0) to :nth-child(2n)
319
- minified = minified.replace(/:nth-child\(2n\s*\+\s*0\)/g, ':nth-child(2n)');
320
- // Simplify :nth-child(1n) to :nth-child(n)
321
- minified = minified.replace(/:nth-child\(1n\)/g, ':nth-child(n)');
322
- // Remove unnecessary leading + sign from :nth-child()
323
- minified = minified.replace(/:nth-child\(\+\s*(\d+)\)/g, ':nth-child($1)');
324
- // Simplify :nth-child(0n+N) to :nth-child(N)
325
- minified = minified.replace(/:nth-child\(0n\s*\+\s*(\d+)\)/g, ':nth-child($1)');
326
- // Replace :nth-child(1) with :first-child
327
- minified = minified.replace(/:nth-child\(1\)/g, ':first-child');
328
- // Replace :nth-last-child(1) with :last-child
329
- minified = minified.replace(/:nth-last-child\(1\)/g, ':last-child');
330
- // Replace :nth-of-type(1) with :first-of-type
331
- minified = minified.replace(/:nth-of-type\(1\)/g, ':first-of-type');
332
- // Replace :nth-last-of-type(1) with :last-of-type
333
- minified = minified.replace(/:nth-last-of-type\(1\)/g, ':last-of-type');
334
- // Convert double-colon pseudo-elements to single-colon legacy form
335
- minified = minified.replace(/::before/g, ':before');
336
- minified = minified.replace(/::after/g, ':after');
337
-
338
- // Strip redundant universal selector `*` when it precedes an ID, class, or attribute selector
339
- minified = minified.replace(/\*([#.[])/g, '$1');
340
-
341
- // Minify double-quoted attribute selectors: remove inner whitespace and escape when shorter
342
- minified = minified.replace(/\[\s*([^=]+)\s*=\s*"(.*?)"\s*\]/g, (match, attribute, value) => {
343
- // Escape special characters that require quoting (spaces, #, ., :, /, ;), and compare lengths
344
- let escaped = value.replace(/([ #.:/;])/g, '\\$1');
345
- if (escaped.length < value.length + 2) {
346
- return '[' + attribute + '=' + escaped + ']';
347
- }
348
- return '[' + attribute + '="' + value + '"]';
349
- });
350
- // Minify single-quoted attribute selectors: remove inner whitespace and escape when shorter
351
- minified = minified.replace(/\[\s*([^=]+)\s*=\s*'(.*?)'\s*\]/g, (match, attribute, value) => {
352
- // Escape special characters that require quoting (spaces, #, ., :, /, ;), and compare lengths
353
- let escaped = value.replace(/([ #.:/;])/g, '\\$1');
354
- if (escaped.length < value.length + 2) {
355
- return '[' + attribute + '=' + escaped + ']';
356
- }
357
- return '[' + attribute + '="' + value + '"]';
358
- });
359
- // Minify unquoted attribute selectors: quote when unescaping produces a shorter result
360
- minified = minified.replace(/\[\s*([^=]+)\s*=\s*([^"'].*?)\s*\]/g, (match, attribute, value) => {
361
- // Unescape special characters (spaces, #, ., :, /, ;) and compare with quoted form
362
- let unescaped = value.replace(/\\([ #.:/;])/g, '$1');
363
- if (unescaped.length + 2 < value.length) {
364
- return '[' + attribute + '="' + unescaped + '"]';
365
- }
366
- return '[' + attribute + '=' + value + ']';
367
- });
368
-
369
- // Minify logical combinations
370
- minified = minified.replace(/(?<=\b(?:button|fieldset|form|input|select|textarea)):not\(:invalid\)/g, ':valid');
371
- minified = minified.replace(/:not\(:dir\(ltr\)\)/g, ':dir(rtl)');
372
- minified = minified.replace(/:not\(:not\((.*?)\)\)/g, '$1');
373
- minified = minified.replace(/:not\(:enabled\)/g, ':disabled');
374
- minified = minified.replace(/:not\(:required\)/g, ':optional');
375
- minified = minified.replace(/(^|[\s,>+~])(a|area|link)(?:\[.*?\])*(?::where\()?:not\(:link\)\)?/g, (match) => {
376
- return match.replace(':not(:link)', ':visited');
377
- });
378
- // Remove redundant leading "& " nesting selector
379
- minified = minified.replace(/^& /, '');
380
- minified = mergeAdjacentWherePseudoClasses(minified);
381
- minified = minifyLanguageSelector(minified);
382
- return minified;
383
- });
445
+ uniqueSelectors = uniqueSelectors.map(minifySelector);
384
446
  uniqueSelectors = combineLegacyAnySelectors(uniqueSelectors);
385
447
  // When this rule is a nesting parent, its whole selector list is treated
386
448
  // as :is() for the children's specificity, so a top-level :is() can be
package/src/utilities.js CHANGED
@@ -20,18 +20,19 @@ function resolveUnicodeEscape (hex) {
20
20
  }
21
21
 
22
22
  /**
23
- * Escapes special regex metacharacters in a string so it can be safely used
24
- * as a literal pattern in a RegExp constructor.
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.
25
27
  *
26
- * @param {string} input The string to escape.
27
- * @return {string} The escaped string safe for use in a RegExp.
28
+ * @param {string} property The CSS property name.
29
+ * @return {boolean} Whether the property is a custom property.
28
30
  */
29
- function escapeRegexString (input) {
30
- // Escape all regex metacharacters: . * + ? ^ $ { } ( ) | [ ] backslash
31
- return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
31
+ function isCustomProperty (property) {
32
+ return Boolean(property) && property.startsWith('--');
32
33
  }
33
34
 
34
35
  export {
35
- escapeRegexString,
36
+ isCustomProperty,
36
37
  resolveUnicodeEscape
37
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.