@thejaredwilcurt/csslop 0.0.19 → 0.0.21
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/package.json +8 -10
- package/src/declarations/background.js +322 -0
- package/src/declarations/border.js +120 -0
- package/src/declarations/config.js +36 -0
- package/src/declarations/merge.js +323 -0
- package/src/declarations/order.js +77 -0
- package/src/declarations/process.js +186 -869
- package/src/declarations/shorthand-values.js +374 -0
- package/src/rules/normalize.js +16 -5
- package/src/rules/selectors.js +71 -8
- package/src/rules/stringify.js +10 -11
- package/src/value/color-mix.js +6 -0
- package/src/value/colors.js +15 -0
- package/src/value/minify.js +149 -31
- package/src/value/shared.js +31 -0
- package/src/value/syntax.js +82 -1
package/src/value/minify.js
CHANGED
|
@@ -9,6 +9,7 @@ import { evaluateColorMix } from './color-mix.js';
|
|
|
9
9
|
import {
|
|
10
10
|
convertLabToHex,
|
|
11
11
|
convertOklabToHex,
|
|
12
|
+
convertOklchToHex,
|
|
12
13
|
hslToRgbChannels,
|
|
13
14
|
hwbToRgbChannels,
|
|
14
15
|
parseHex,
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
collapseShorthandParts,
|
|
32
33
|
normalizeScaleComponent,
|
|
33
34
|
parseAlphaString,
|
|
35
|
+
parseAngleToDegrees,
|
|
34
36
|
roundCompactNumber
|
|
35
37
|
} from './shared.js';
|
|
36
38
|
import { findMatchingParenthesis } from './syntax.js';
|
|
@@ -57,7 +59,9 @@ const POSITION_AREA_SHORTHANDS = {
|
|
|
57
59
|
* Regex matching hex color tokens (#rgb, #rgba, #rrggbb, #rrggbbaa) and CSS named
|
|
58
60
|
* color keywords. Hex patterns are ordered longest-first to avoid partial matches.
|
|
59
61
|
* Named colors are sorted longest-first so longer names like `darkslategray` are
|
|
60
|
-
* matched before shorter substrings.
|
|
62
|
+
* matched before shorter substrings. A named color only counts as a color keyword
|
|
63
|
+
* when it is a complete identifier, so hyphens on either side disqualify it (this
|
|
64
|
+
* keeps identifiers such as the custom property name in `var(--grey)` intact).
|
|
61
65
|
*
|
|
62
66
|
* @type {RegExp}
|
|
63
67
|
*/
|
|
@@ -66,11 +70,11 @@ const COLOR_TOKEN_PATTERN = new RegExp(
|
|
|
66
70
|
'#[0-9a-fA-F]{6}(?![0-9a-fA-F])|' +
|
|
67
71
|
'#[0-9a-fA-F]{4}(?![0-9a-fA-F])|' +
|
|
68
72
|
'#[0-9a-fA-F]{3}(?![0-9a-fA-F])|' +
|
|
69
|
-
'\\
|
|
73
|
+
'(?<![\\w-])(?:' +
|
|
70
74
|
Object.keys(namedColors).sort((a, b) => {
|
|
71
75
|
return b.length - a.length;
|
|
72
76
|
}).join('|') +
|
|
73
|
-
')\\
|
|
77
|
+
')(?![\\w-])',
|
|
74
78
|
'gi'
|
|
75
79
|
);
|
|
76
80
|
|
|
@@ -178,6 +182,37 @@ function replaceOutsideStringsAndUrls (value, replacer) {
|
|
|
178
182
|
return result;
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Lowercases every hex color token in a CSS value, since uppercase hex digits
|
|
187
|
+
* compress worse and are equivalent to their lowercase form.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} value The CSS value string.
|
|
190
|
+
* @return {string} The value with all hex color tokens lowercased.
|
|
191
|
+
*/
|
|
192
|
+
function lowercaseHexColors (value) {
|
|
193
|
+
return replaceOutsideStringsAndUrls(value, (segment) => {
|
|
194
|
+
// Match hex color tokens of 3 to 8 hex digits
|
|
195
|
+
return segment.replace(/#([0-9a-fA-F]{3,8})\b/gi, (hexColor) => {
|
|
196
|
+
return hexColor.toLowerCase();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Removes whitespace that precedes a hex color token. A `#` unambiguously starts
|
|
203
|
+
* a hash token in CSS, so no separator is required between it and a preceding
|
|
204
|
+
* ident, keyword, or number (e.g. `1px solid #f00` becomes `1px solid#f00`).
|
|
205
|
+
*
|
|
206
|
+
* @param {string} value The CSS value string.
|
|
207
|
+
* @return {string} The value with spaces before hex colors removed.
|
|
208
|
+
*/
|
|
209
|
+
function elideSpaceBeforeHexColors (value) {
|
|
210
|
+
return replaceOutsideStringsAndUrls(value, (segment) => {
|
|
211
|
+
// Match whitespace followed by a hex color token of 3 to 8 hex digits
|
|
212
|
+
return segment.replace(/\s+#([0-9a-fA-F]{3,8})\b/gi, '#$1');
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
181
216
|
/**
|
|
182
217
|
* Chooses the shortest valid representation for the path inside a `url(...)`
|
|
183
218
|
* token, weighing an unquoted form, an escaped single space, and a quoted form.
|
|
@@ -336,6 +371,97 @@ function normalizeWhitespaceAndQuotes (val, property) {
|
|
|
336
371
|
return val;
|
|
337
372
|
}
|
|
338
373
|
|
|
374
|
+
/**
|
|
375
|
+
* The OKLCH chroma value that `100%` resolves to, per CSS Color Level 4.
|
|
376
|
+
*
|
|
377
|
+
* @type {number}
|
|
378
|
+
*/
|
|
379
|
+
const OKLCH_CHROMA_PERCENT_REFERENCE = 0.4;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Regex matching an `oklch()` function with three space-separated components
|
|
383
|
+
* and an optional slash-delimited alpha. Lightness and chroma accept numbers
|
|
384
|
+
* or percentages, hue accepts a number with an optional CSS angle unit, and
|
|
385
|
+
* every component accepts the `none` keyword.
|
|
386
|
+
*
|
|
387
|
+
* @type {RegExp}
|
|
388
|
+
*/
|
|
389
|
+
const OKLCH_FUNCTION_PATTERN = new RegExp(
|
|
390
|
+
'\\boklch\\(\\s*' +
|
|
391
|
+
'(none|-?(?:\\d+|\\d*\\.\\d+)%?)\\s+' +
|
|
392
|
+
'(none|-?(?:\\d+|\\d*\\.\\d+)%?)\\s+' +
|
|
393
|
+
'(none|-?(?:\\d+|\\d*\\.\\d+)(?:deg|grad|rad|turn)?)' +
|
|
394
|
+
'(?:\\s*/\\s*(none|-?(?:\\d+|\\d*\\.\\d+)%?))?' +
|
|
395
|
+
'\\s*\\)',
|
|
396
|
+
'gi'
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Parses an OKLCH lightness or chroma component into its numeric value.
|
|
401
|
+
* Missing components (`none`) resolve to zero, and percentages are scaled
|
|
402
|
+
* against the reference value for that component.
|
|
403
|
+
*
|
|
404
|
+
* @param {string} token The raw component token.
|
|
405
|
+
* @param {number} percentReference The value that `100%` represents for this component.
|
|
406
|
+
* @return {number|null} The numeric component value, or null when unparsable.
|
|
407
|
+
*/
|
|
408
|
+
function parseOklchComponent (token, percentReference) {
|
|
409
|
+
const normalized = token.trim().toLowerCase();
|
|
410
|
+
if (normalized === 'none') {
|
|
411
|
+
return 0;
|
|
412
|
+
}
|
|
413
|
+
const numeric = parseFloat(normalized);
|
|
414
|
+
if (!Number.isFinite(numeric)) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
if (normalized.endsWith('%')) {
|
|
418
|
+
return numeric / 100 * percentReference;
|
|
419
|
+
}
|
|
420
|
+
return numeric;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Parses an OKLCH hue component into degrees, treating `none` as zero.
|
|
425
|
+
*
|
|
426
|
+
* @param {string} token The raw hue token, optionally carrying an angle unit.
|
|
427
|
+
* @return {number|null} The hue in degrees, or null when unparsable.
|
|
428
|
+
*/
|
|
429
|
+
function parseOklchHue (token) {
|
|
430
|
+
const normalized = token.trim().toLowerCase();
|
|
431
|
+
if (normalized === 'none') {
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
return parseAngleToDegrees(normalized);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Converts `oklch()` colors that fall inside the sRGB gamut to their hex
|
|
439
|
+
* equivalent when that is shorter. Out-of-gamut colors have no sRGB
|
|
440
|
+
* representation, so they are left in their native color space.
|
|
441
|
+
*
|
|
442
|
+
* @param {string} value The CSS value string that may contain oklch() colors.
|
|
443
|
+
* @return {string} The value with in-gamut oklch() colors replaced by hex.
|
|
444
|
+
*/
|
|
445
|
+
function convertOklchFunctionsToHex (value) {
|
|
446
|
+
return value.replace(OKLCH_FUNCTION_PATTERN, (match, lightnessToken, chromaToken, hueToken, alphaToken) => {
|
|
447
|
+
const lightness = parseOklchComponent(lightnessToken, 1);
|
|
448
|
+
const chroma = parseOklchComponent(chromaToken, OKLCH_CHROMA_PERCENT_REFERENCE);
|
|
449
|
+
const hue = parseOklchHue(hueToken);
|
|
450
|
+
if (lightness === null || chroma === null || hue === null) {
|
|
451
|
+
return match;
|
|
452
|
+
}
|
|
453
|
+
const alpha = alphaToken?.trim().toLowerCase() === 'none' ? 0 : parseAlphaString(alphaToken);
|
|
454
|
+
const hex = convertOklchToHex(lightness, chroma, hue, alpha);
|
|
455
|
+
if (!hex) {
|
|
456
|
+
return match; // out-of-gamut: keep native oklch form
|
|
457
|
+
}
|
|
458
|
+
if (hex.length < match.length) {
|
|
459
|
+
return hex;
|
|
460
|
+
}
|
|
461
|
+
return match;
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
339
465
|
/**
|
|
340
466
|
* Converts CSS color functions (rgb, hsl, hwb, oklab, color-mix, etc.) to their
|
|
341
467
|
* shortest hex equivalents and applies hex shortening.
|
|
@@ -378,6 +504,9 @@ function convertColorsToHex (val) {
|
|
|
378
504
|
return match;
|
|
379
505
|
});
|
|
380
506
|
|
|
507
|
+
// Convert in-gamut oklch() to hex before precision rounding, so the full authored precision is used
|
|
508
|
+
val = convertOklchFunctionsToHex(val);
|
|
509
|
+
|
|
381
510
|
// Minify whitespace and numeric precision inside wide-gamut and functional color notations
|
|
382
511
|
val = val.replace(/\b(oklab|oklch|lch|lab|color|hwb)\((.*?)\)/gi, (match, func, inner) => {
|
|
383
512
|
// Collapse whitespace to single space
|
|
@@ -586,11 +715,12 @@ function convertMillisecondsToSeconds (value) {
|
|
|
586
715
|
* Applies property-specific optimizations to a CSS value (transition, flex, font,
|
|
587
716
|
* background, display, scale, border-radius, shorthand collapsing, etc.).
|
|
588
717
|
*
|
|
589
|
-
* @param {string}
|
|
590
|
-
* @param {string}
|
|
591
|
-
* @
|
|
718
|
+
* @param {string} val The CSS value string after generic minification.
|
|
719
|
+
* @param {string} property The CSS property name.
|
|
720
|
+
* @param {boolean} allowsHexSpaceElision Whether the space preceding a hex color may be removed.
|
|
721
|
+
* @return {string} The value with property-specific optimizations applied.
|
|
592
722
|
*/
|
|
593
|
-
function applyPropertyOptimizations (val, property) {
|
|
723
|
+
function applyPropertyOptimizations (val, property, allowsHexSpaceElision) {
|
|
594
724
|
if (property === 'font-weight' && isUnicodeCharset()) {
|
|
595
725
|
// Replace font-weight keyword "bold" with its numeric equivalent
|
|
596
726
|
val = val.replace(/\bbold\b/gi, '700');
|
|
@@ -763,12 +893,9 @@ function applyPropertyOptimizations (val, property) {
|
|
|
763
893
|
val = replaceOutsideStringsAndUrls(val, shortenColorValues);
|
|
764
894
|
|
|
765
895
|
// Remove space before hex colors (second pass after color evaluations)
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
// Then remove other spaces before hex colors
|
|
770
|
-
return segment.replace(/\s+#([0-9a-fA-F]{3,8})\b/gi, '#$1');
|
|
771
|
-
});
|
|
896
|
+
if (allowsHexSpaceElision) {
|
|
897
|
+
val = elideSpaceBeforeHexColors(val);
|
|
898
|
+
}
|
|
772
899
|
if (property !== 'transform' && property !== 'background' && property !== 'src') {
|
|
773
900
|
// Restore space after close-paren when followed by an alphanumeric, hash, or hyphen
|
|
774
901
|
val = val.replace(/\)(?=[0-9a-zA-Z#-])/g, ') ');
|
|
@@ -815,9 +942,6 @@ function applyPropertyOptimizations (val, property) {
|
|
|
815
942
|
if (property === 'border') {
|
|
816
943
|
// Remove default "medium" border-width keyword
|
|
817
944
|
val = val.replace(/\bmedium\s+/g, '');
|
|
818
|
-
// Restore missing space between border-style and a 4-digit hex color (with alpha) when they are adjacent
|
|
819
|
-
// This is needed because solid#0000 could be parsed as solid followed by #000 followed by position 0
|
|
820
|
-
val = val.replace(/\b(solid|dashed|dotted|double|groove|ridge|inset|outset|hidden|none)#([0-9a-fA-F]{4})\b/gi, '$1 #$2');
|
|
821
945
|
}
|
|
822
946
|
|
|
823
947
|
if (property === 'outline') {
|
|
@@ -886,6 +1010,10 @@ function minifyValue (declaration) {
|
|
|
886
1010
|
return 'none';
|
|
887
1011
|
}
|
|
888
1012
|
let val = declaration.value;
|
|
1013
|
+
// Values assembled from already-minified longhands keep the separator spaces
|
|
1014
|
+
// between their components, because those spaces delimit the shorthand's
|
|
1015
|
+
// parts rather than the authored whitespace of a single written value.
|
|
1016
|
+
const allowsHexSpaceElision = !declaration.isAssembledShorthand;
|
|
889
1017
|
|
|
890
1018
|
if (typeof val === 'string') {
|
|
891
1019
|
val = val.trim();
|
|
@@ -928,20 +1056,10 @@ function minifyValue (declaration) {
|
|
|
928
1056
|
val = roundCompactNumber(rawNumber, 4) + rawUnit;
|
|
929
1057
|
}
|
|
930
1058
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
// Remove other spaces before hex colors
|
|
936
|
-
segment = segment.replace(/\s+#([0-9a-fA-F]{3,8})\b/gi, '#$1');
|
|
937
|
-
// Restore the preserved space
|
|
938
|
-
segment = segment.replace(/__BORDER_SPACE__#/g, ' #');
|
|
939
|
-
// Lowercase hex color tokens for consistency and shorter output
|
|
940
|
-
segment = segment.replace(/#([0-9a-fA-F]{3,8})\b/gi, (m) => {
|
|
941
|
-
return m.toLowerCase();
|
|
942
|
-
});
|
|
943
|
-
return segment;
|
|
944
|
-
});
|
|
1059
|
+
val = lowercaseHexColors(val);
|
|
1060
|
+
if (allowsHexSpaceElision) {
|
|
1061
|
+
val = elideSpaceBeforeHexColors(val);
|
|
1062
|
+
}
|
|
945
1063
|
|
|
946
1064
|
// Convert color functions to hex equivalents
|
|
947
1065
|
val = convertColorsToHex(val);
|
|
@@ -953,7 +1071,7 @@ function minifyValue (declaration) {
|
|
|
953
1071
|
val = simplifyEquivalentLightDarkFunctions(val);
|
|
954
1072
|
|
|
955
1073
|
// Property-specific optimizations
|
|
956
|
-
val = applyPropertyOptimizations(val, declaration.property);
|
|
1074
|
+
val = applyPropertyOptimizations(val, declaration.property, allowsHexSpaceElision);
|
|
957
1075
|
|
|
958
1076
|
// Minify relative color syntax (identity resolution and whitespace collapsing)
|
|
959
1077
|
val = minifyRelativeColorSyntax(val);
|
package/src/value/shared.js
CHANGED
|
@@ -113,6 +113,36 @@ function parseAlphaString (alphaStr, fallback = 1) {
|
|
|
113
113
|
return parseFloat(alphaStr);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Conversion factors from each CSS angle unit to degrees.
|
|
118
|
+
*
|
|
119
|
+
* @type {{[key: string]: number}}
|
|
120
|
+
*/
|
|
121
|
+
const ANGLE_UNIT_TO_DEGREES = {
|
|
122
|
+
deg: 1,
|
|
123
|
+
grad: 360 / 400,
|
|
124
|
+
rad: 180 / Math.PI,
|
|
125
|
+
turn: 360
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parses a CSS angle token (e.g. "90", "90deg", ".25turn") into degrees.
|
|
130
|
+
* Unitless values are treated as degrees, per the CSS Color specification's
|
|
131
|
+
* handling of hue components.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} angleToken The angle token, with or without a unit suffix.
|
|
134
|
+
* @return {number|null} The angle in degrees, or null if the token is not a valid angle.
|
|
135
|
+
*/
|
|
136
|
+
function parseAngleToDegrees (angleToken) {
|
|
137
|
+
// Capture the numeric portion and an optional CSS angle unit suffix
|
|
138
|
+
const match = String(angleToken).trim().match(/^(-?(?:\d+|\d*\.\d+))(deg|grad|rad|turn)?$/i);
|
|
139
|
+
if (!match) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const unit = match[2] ? match[2].toLowerCase() : 'deg';
|
|
143
|
+
return parseFloat(match[1]) * ANGLE_UNIT_TO_DEGREES[unit];
|
|
144
|
+
}
|
|
145
|
+
|
|
116
146
|
/**
|
|
117
147
|
* Collapses redundant CSS shorthand parts using the standard box-model
|
|
118
148
|
* reduction rules: 4-value → 3-value → 2-value → 1-value.
|
|
@@ -142,5 +172,6 @@ export {
|
|
|
142
172
|
formatDimension,
|
|
143
173
|
normalizeScaleComponent,
|
|
144
174
|
parseAlphaString,
|
|
175
|
+
parseAngleToDegrees,
|
|
145
176
|
roundCompactNumber
|
|
146
177
|
};
|
package/src/value/syntax.js
CHANGED
|
@@ -53,4 +53,85 @@ function findMatchingParenthesis (value, openParenIndex) {
|
|
|
53
53
|
return -1;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Splits a CSS value into its top-level components, keeping parenthesized
|
|
58
|
+
* function arguments and quoted strings intact. Components are separated by
|
|
59
|
+
* whitespace, or by a `#`, which always starts a hash token and therefore ends
|
|
60
|
+
* any component already in progress.
|
|
61
|
+
*
|
|
62
|
+
* For example, `rgb(0 0 0) red` yields `["rgb(0 0 0)", "red"]`, and the
|
|
63
|
+
* minified `red#00f` yields `["red", "#00f"]`.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} value The CSS value to split.
|
|
66
|
+
* @return {Array} The top-level components of the value.
|
|
67
|
+
*/
|
|
68
|
+
function splitTopLevelComponents (value) {
|
|
69
|
+
const components = [];
|
|
70
|
+
let current = '';
|
|
71
|
+
let depth = 0;
|
|
72
|
+
let activeQuote = '';
|
|
73
|
+
let index = 0;
|
|
74
|
+
|
|
75
|
+
while (index < value.length) {
|
|
76
|
+
const character = value[index];
|
|
77
|
+
|
|
78
|
+
if (activeQuote) {
|
|
79
|
+
current += character;
|
|
80
|
+
if (character === '\\') {
|
|
81
|
+
current += value[index + 1] ?? '';
|
|
82
|
+
index += 2;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (character === activeQuote) {
|
|
86
|
+
activeQuote = '';
|
|
87
|
+
}
|
|
88
|
+
index++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (character === '"' || character === '\'') {
|
|
93
|
+
activeQuote = character;
|
|
94
|
+
current += character;
|
|
95
|
+
index++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (character === '(') {
|
|
100
|
+
depth++;
|
|
101
|
+
}
|
|
102
|
+
if (character === ')' && depth > 0) {
|
|
103
|
+
depth--;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Match any whitespace character, which separates components at depth zero
|
|
107
|
+
const isSeparator = depth === 0 && /\s/.test(character);
|
|
108
|
+
if (isSeparator) {
|
|
109
|
+
if (current) {
|
|
110
|
+
components.push(current);
|
|
111
|
+
current = '';
|
|
112
|
+
}
|
|
113
|
+
index++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const startsHashToken = character === '#' && depth === 0 && current !== '';
|
|
118
|
+
if (startsHashToken) {
|
|
119
|
+
components.push(current);
|
|
120
|
+
current = '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
current += character;
|
|
124
|
+
index++;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (current) {
|
|
128
|
+
components.push(current);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return components;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
findMatchingParenthesis,
|
|
136
|
+
splitTopLevelComponents
|
|
137
|
+
};
|