@thejaredwilcurt/csslop 0.0.49 → 0.0.51

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 (57) hide show
  1. package/README.md +0 -1
  2. package/package.json +2 -2
  3. package/src/declarations/border.js +1 -1
  4. package/src/declarations/process.js +155 -1
  5. package/src/index.js +7 -7
  6. package/src/rules/optimize/at-rules.js +113 -0
  7. package/src/rules/optimize/declaration-writes.js +70 -0
  8. package/src/rules/optimize/empty-rules.js +51 -0
  9. package/src/rules/optimize/expand-nesting.js +236 -0
  10. package/src/rules/optimize/factor-parents.js +343 -0
  11. package/src/rules/optimize/index.js +71 -0
  12. package/src/rules/optimize/merge-declarations.js +199 -0
  13. package/src/rules/optimize/merge-nested-rules.js +117 -0
  14. package/src/rules/optimize/merge-selectors.js +252 -0
  15. package/src/rules/optimize/nest-rules.js +202 -0
  16. package/src/rules/optimize/override-index.js +264 -0
  17. package/src/rules/optimize/prune-overridden.js +267 -0
  18. package/src/rules/optimize/selector-normalization.js +60 -0
  19. package/src/rules/optimize/specificity.js +280 -0
  20. package/src/{position-try.js → rules/position-try.js} +1 -1
  21. package/src/rules/stringify.js +13 -0
  22. package/src/utilities.js +14 -0
  23. package/src/value/border.js +85 -0
  24. package/src/value/{colors.js → color/conversion.js} +3 -2
  25. package/src/value/color/functions.js +463 -0
  26. package/src/value/{light-dark.js → color/light-dark.js} +1 -1
  27. package/src/value/{color-mix.js → color/mix.js} +3 -3
  28. package/src/value/{relative-colors.js → color/relative.js} +3 -3
  29. package/src/value/color/shorten.js +93 -0
  30. package/src/value/gradients.js +1 -1
  31. package/src/value/hyphenate-limit-chars.js +72 -0
  32. package/src/value/initial-keywords.js +128 -0
  33. package/src/value/lengths.js +55 -0
  34. package/src/value/math/evaluate.js +36 -0
  35. package/src/value/math/fold.js +315 -0
  36. package/src/value/math/format.js +230 -0
  37. package/src/value/math/functions.js +302 -0
  38. package/src/value/math/index.js +219 -0
  39. package/src/value/math/parser.js +301 -0
  40. package/src/value/math/property-ranges.js +174 -0
  41. package/src/value/math/rewrite.js +222 -0
  42. package/src/value/math/tokenizer.js +160 -0
  43. package/src/value/math/unit-totals.js +194 -0
  44. package/src/value/minify.js +127 -1669
  45. package/src/value/numbers.js +28 -0
  46. package/src/value/positions.js +152 -0
  47. package/src/value/property-optimizations.js +485 -0
  48. package/src/value/segments.js +74 -0
  49. package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
  50. package/src/value/{svg-markup.js → svg/markup.js} +1 -1
  51. package/src/value/time.js +75 -0
  52. package/src/value/urls.js +242 -0
  53. package/src/value/whitespace.js +88 -0
  54. package/src/rules/optimize.js +0 -2314
  55. package/src/value/math-expression.js +0 -1156
  56. package/src/value/math.js +0 -886
  57. /package/src/value/{named-colors.js → color/named.js} +0 -0
@@ -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.
@@ -0,0 +1,463 @@
1
+ /**
2
+ * @file Converts CSS color functions (rgb, hsl, hwb, lab, oklab, oklch, color-mix, and relative colors) to their shortest equivalent, and compacts the ones that have to stay functions.
3
+ */
4
+
5
+ import {
6
+ formatCompactNumber,
7
+ parseAlphaString,
8
+ parseAngleToDegrees
9
+ } from '../shared.js';
10
+
11
+ import {
12
+ convertLabToHex,
13
+ convertOklabToHex,
14
+ convertOklchToHex,
15
+ hslToRgbChannels,
16
+ hwbToRgbChannels,
17
+ rgbaToHex
18
+ } from './conversion.js';
19
+ import { evaluateColorMix } from './mix.js';
20
+ import { evaluateRelativeColor } from './relative.js';
21
+
22
+ /**
23
+ * The OKLCH chroma value that `100%` resolves to, per CSS Color Level 4.
24
+ *
25
+ * @type {number}
26
+ */
27
+ const OKLCH_CHROMA_PERCENT_REFERENCE = 0.4;
28
+
29
+ /**
30
+ * Regex matching an `oklch()` function with three space-separated components
31
+ * and an optional slash-delimited alpha. Lightness and chroma accept numbers
32
+ * or percentages, hue accepts a number with an optional CSS angle unit, and
33
+ * every component accepts the `none` keyword.
34
+ *
35
+ * @type {RegExp}
36
+ */
37
+ const OKLCH_FUNCTION_PATTERN = new RegExp(
38
+ '\\boklch\\(\\s*' +
39
+ '(none|-?(?:\\d+|\\d*\\.\\d+)%?)\\s+' +
40
+ '(none|-?(?:\\d+|\\d*\\.\\d+)%?)\\s+' +
41
+ '(none|-?(?:\\d+|\\d*\\.\\d+)(?:deg|grad|rad|turn)?)' +
42
+ '(?:\\s*/\\s*(none|-?(?:\\d+|\\d*\\.\\d+)%?))?' +
43
+ '\\s*\\)',
44
+ 'gi'
45
+ );
46
+
47
+ /**
48
+ * Parses an OKLCH lightness or chroma component into its numeric value.
49
+ * Missing components (`none`) resolve to zero, and percentages are scaled
50
+ * against the reference value for that component.
51
+ *
52
+ * @param {string} token The raw component token.
53
+ * @param {number} percentReference The value that `100%` represents for this component.
54
+ * @return {number|null} The numeric component value, or null when unparsable.
55
+ */
56
+ function parseOklchComponent (token, percentReference) {
57
+ const normalized = token.trim().toLowerCase();
58
+ if (normalized === 'none') {
59
+ return 0;
60
+ }
61
+ const numeric = parseFloat(normalized);
62
+ if (!Number.isFinite(numeric)) {
63
+ return null;
64
+ }
65
+ if (normalized.endsWith('%')) {
66
+ return numeric / 100 * percentReference;
67
+ }
68
+ return numeric;
69
+ }
70
+
71
+ /**
72
+ * Parses an OKLCH hue component into degrees, treating `none` as zero.
73
+ *
74
+ * @param {string} token The raw hue token, optionally carrying an angle unit.
75
+ * @return {number|null} The hue in degrees, or null when unparsable.
76
+ */
77
+ function parseOklchHue (token) {
78
+ const normalized = token.trim().toLowerCase();
79
+ if (normalized === 'none') {
80
+ return 0;
81
+ }
82
+ return parseAngleToDegrees(normalized);
83
+ }
84
+
85
+ /**
86
+ * Removes whitespace before a negative numeric channel in Lab-family colors.
87
+ * A preceding numeric token already separates the sign from the prior channel.
88
+ *
89
+ * @param {string} functionName The name of the color function.
90
+ * @param {string} value The minified function arguments.
91
+ * @return {string} The arguments with redundant channel spacing removed.
92
+ */
93
+ function compactNegativeColorChannelSpacing (functionName, value) {
94
+ const normalizedFunctionName = functionName.toLowerCase();
95
+ if (!['lab', 'lch', 'oklab', 'oklch'].includes(normalizedFunctionName)) {
96
+ return value;
97
+ }
98
+
99
+ let result = '';
100
+ let nestingDepth = 0;
101
+ for (let index = 0; index < value.length; index++) {
102
+ const character = value[index];
103
+ if (character === ' ' && nestingDepth === 0) {
104
+ const previousCharacter = result[result.length - 1];
105
+ const nextCharacter = value[index + 1];
106
+ const characterAfterSign = value[index + 2];
107
+ const characterAfterDecimal = value[index + 3];
108
+ const previousEndsNumber = (
109
+ isCssDigit(previousCharacter) ||
110
+ previousCharacter === '%'
111
+ );
112
+ const nextStartsNegativeNumber = (
113
+ nextCharacter === '-' &&
114
+ (
115
+ isCssDigit(characterAfterSign) ||
116
+ (
117
+ characterAfterSign === '.' &&
118
+ isCssDigit(characterAfterDecimal)
119
+ )
120
+ )
121
+ );
122
+ if (previousEndsNumber && nextStartsNegativeNumber) {
123
+ continue;
124
+ }
125
+ }
126
+ result += character;
127
+ if (character === '(') {
128
+ nestingDepth++;
129
+ } else if (character === ')') {
130
+ nestingDepth--;
131
+ }
132
+ }
133
+ return result;
134
+ }
135
+
136
+ /**
137
+ * Checks whether a character is an ASCII decimal digit.
138
+ *
139
+ * @param {string} character The character to inspect.
140
+ * @return {boolean} Whether the character is a digit.
141
+ */
142
+ function isCssDigit (character) {
143
+ return character >= '0' && character <= '9';
144
+ }
145
+
146
+ /**
147
+ * Converts OKLCH lightness and chroma percentages when doing so is shorter,
148
+ * including the separator each spelling needs before the hue.
149
+ *
150
+ * @param {string} value The normalized arguments of an `oklch()` function.
151
+ * @return {string} The arguments with shorter percentage spellings.
152
+ */
153
+ function minifyOklchPercentages (value) {
154
+ const slashIndex = value.indexOf('/');
155
+ const componentsText = slashIndex === -1 ? value : value.slice(0, slashIndex);
156
+ const alphaText = slashIndex === -1 ? '' : value.slice(slashIndex + 1);
157
+ const components = componentsText.trim().split(' ');
158
+ if (components.length !== 3) {
159
+ return value;
160
+ }
161
+
162
+ let [lightness, chroma, hue] = components;
163
+ if (lightness.endsWith('%')) {
164
+ lightness = formatCompactNumber(Number.parseFloat(lightness) / 100);
165
+ }
166
+ if (chroma.endsWith('%')) {
167
+ const convertedChroma = formatCompactNumber(
168
+ Number.parseFloat(chroma) / 100 * OKLCH_CHROMA_PERCENT_REFERENCE
169
+ );
170
+ const convertedWithHueSeparator = convertedChroma.length + 1;
171
+ if (chroma.length < convertedWithHueSeparator) {
172
+ return [lightness, chroma + hue].join(' ') + (slashIndex === -1 ? '' : '/' + alphaText);
173
+ }
174
+ chroma = convertedChroma;
175
+ }
176
+
177
+ return [lightness, chroma, hue].join(' ') + (slashIndex === -1 ? '' : '/' + alphaText);
178
+ }
179
+
180
+ /**
181
+ * Converts `oklch()` colors that fall inside the sRGB gamut to their hex
182
+ * equivalent when that is shorter. Out-of-gamut colors have no sRGB
183
+ * representation, so they are left in their native color space.
184
+ *
185
+ * @param {string} value The CSS value string that may contain oklch() colors.
186
+ * @return {string} The value with in-gamut oklch() colors replaced by hex.
187
+ */
188
+ function convertOklchFunctionsToHex (value) {
189
+ return value.replace(OKLCH_FUNCTION_PATTERN, (match, lightnessToken, chromaToken, hueToken, alphaToken) => {
190
+ const lightness = parseOklchComponent(lightnessToken, 1);
191
+ const chroma = parseOklchComponent(chromaToken, OKLCH_CHROMA_PERCENT_REFERENCE);
192
+ const hue = parseOklchHue(hueToken);
193
+ if (lightness === null || chroma === null || hue === null) {
194
+ return match;
195
+ }
196
+ const alpha = alphaToken?.trim().toLowerCase() === 'none' ? 0 : parseAlphaString(alphaToken);
197
+ const hex = convertOklchToHex(lightness, chroma, hue, alpha);
198
+ if (!hex) {
199
+ return match; // out-of-gamut: keep native oklch form
200
+ }
201
+ if (hex.length < match.length) {
202
+ return hex;
203
+ }
204
+ return match;
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Evaluates the colors that are computed from other colors, `color-mix()` and
210
+ * the `color(from ...)` relative color syntax, into the color they resolve to.
211
+ *
212
+ * @param {string} value The CSS value string with potential computed colors.
213
+ * @return {string} The value with computed colors resolved where possible.
214
+ */
215
+ function resolveComputedColors (value) {
216
+ // Evaluate color-mix() expressions (before space minification to avoid nested-paren issues)
217
+ if (/\bcolor-mix\(/i.test(value)) {
218
+ const result = evaluateColorMix(value);
219
+ if (result) {
220
+ value = result;
221
+ }
222
+ }
223
+
224
+ // Handle color(from ...) relative color syntax (identity case)
225
+ if (/\bcolor\(\s*from\b/i.test(value)) {
226
+ const result = evaluateRelativeColor(value);
227
+ if (result) {
228
+ value = result;
229
+ }
230
+ }
231
+ return value;
232
+ }
233
+
234
+ /**
235
+ * Converts in-gamut `lab()` colors (CIE Lab, D50) to hex when that produces a
236
+ * shorter representation. Out-of-gamut colors have no sRGB representation, so
237
+ * they are left in their native color space.
238
+ *
239
+ * @param {string} value The CSS value string that may contain lab() colors.
240
+ * @return {string} The value with in-gamut lab() colors replaced by hex where shorter.
241
+ */
242
+ function convertLabFunctionsToHex (value) {
243
+ // Match lab() with three numeric or percentage channels and an optional slash-delimited alpha
244
+ return value.replace(/\blab\(\s*(-?(?:\d+|\d*\.\d+)%?)\s+(-?(?:\d+|\d*\.\d+)%?)\s+(-?(?:\d+|\d*\.\d+)%?)(?:\s*\/\s*(-?(?:\d+|\d*\.\d+)%?))?\s*\)/gi, (match, lightnessText, aAxisText, bAxisText, alphaText) => {
245
+ const alpha = parseAlphaString(alphaText);
246
+ const lightness = parseFloat(lightnessText);
247
+ const aNumber = parseFloat(aAxisText);
248
+ const a = aAxisText.endsWith('%') ? aNumber * 1.25 : aNumber;
249
+ const bNumber = parseFloat(bAxisText);
250
+ const b = bAxisText.endsWith('%') ? bNumber * 1.25 : bNumber;
251
+ const hex = convertLabToHex(lightness, a, b, alpha);
252
+ if (!hex) {
253
+ return match; // out-of-gamut: keep native lab form
254
+ }
255
+ if (hex.length < match.length) {
256
+ return hex;
257
+ }
258
+ return match;
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Minifies the whitespace and numeric precision inside the wide-gamut and
264
+ * functional color notations, rounding each channel to the precision its color
265
+ * space needs.
266
+ *
267
+ * @param {string} value The CSS value string that may contain color functions.
268
+ * @return {string} The value with every color function's arguments compacted.
269
+ */
270
+ function compactColorFunctionArguments (value) {
271
+ // Match a wide-gamut or functional color notation, capturing its name and its arguments
272
+ return value.replace(/\b(oklab|oklch|lch|lab|color|hwb)\((.*?)\)/gi, (match, functionName, inner) => {
273
+ // Collapse whitespace to single space
274
+ let minified = inner.replace(/\s+/g, ' ');
275
+ // Remove space after commas
276
+ minified = minified.replace(/, /g, ',');
277
+ // Remove spaces around slash separator (alpha delimiter)
278
+ minified = minified.replace(/ \/ /g, '/');
279
+ // Strip leading zeros from decimal numbers (e.g. 0.5 → .5)
280
+ minified = minified.replace(/\b0+(\.[\d]+)/g, '$1');
281
+ // Strip leading zeros from decimals preceded by a keyword (e.g. srgb 0.5 → srgb .5)
282
+ minified = minified.replace(/([A-Za-z]) 0+(\.[\d]+)/g, '$1 $2');
283
+ // Check if function uses a wide-gamut color space requiring higher numeric precision
284
+ const useWidePrecision = functionName.toLowerCase() === 'color' && /\b(srgb-linear|xyz-d65|xyz-d50|xyz)\b/i.test(inner);
285
+ // Round numbers with 3+ decimal places, using context-aware precision
286
+ minified = minified.replace(/(^|[\s(,/-])(-?\d*\.\d{3,})/g, (match, before, number) => {
287
+ const isAlpha = before === '/';
288
+ const absoluteValue = Math.abs(parseFloat(number));
289
+ // Check if function is a Lab/LCH color notation with a large channel value (less precision needed)
290
+ const isLargeLabValue = /^(?:lch|lab|oklch|oklab)$/i.test(functionName) && absoluteValue >= 10;
291
+ let precision;
292
+ if (isAlpha) {
293
+ precision = 3;
294
+ } else if (isLargeLabValue) {
295
+ precision = 1;
296
+ } else if (useWidePrecision) {
297
+ precision = 4;
298
+ } else {
299
+ precision = 3;
300
+ }
301
+ const factor = Math.pow(10, precision);
302
+ const roundedNumber = Math.round(parseFloat(number) * factor) / factor;
303
+ // Strip trailing zeros and trailing decimal point from the rounded number
304
+ let rounded = roundedNumber.toFixed(precision).replace(/0+$/, '').replace(/\.$/, '');
305
+ if (rounded.startsWith('0.')) {
306
+ rounded = rounded.substring(1);
307
+ }
308
+ if (rounded.startsWith('-0.')) {
309
+ rounded = '-' + rounded.substring(2);
310
+ }
311
+ return before + rounded;
312
+ });
313
+ // Remove trailing ".0" from numbers so integer channel values stay integer
314
+ // (e.g. display-p3 1.0 0.0 0.0 becomes display-p3 1 0 0)
315
+ minified = minified.replace(/(-?\d*)\.0\b/g, (match, integer) => {
316
+ if (integer === '' || integer === '-' || integer === '-0') {
317
+ return '0';
318
+ }
319
+ return integer;
320
+ });
321
+ minified = compactNegativeColorChannelSpacing(functionName, minified);
322
+ if (functionName.toLowerCase() === 'oklch') {
323
+ minified = minifyOklchPercentages(minified);
324
+ }
325
+ return functionName + '(' + minified.trim() + ')';
326
+ });
327
+ }
328
+
329
+ /**
330
+ * Converts in-gamut `oklab()` colors to hex when that produces a shorter
331
+ * representation. Out-of-gamut colors have no sRGB representation, so they are
332
+ * left in their native color space.
333
+ *
334
+ * @param {string} value The CSS value string that may contain oklab() colors.
335
+ * @return {string} The value with in-gamut oklab() colors replaced by hex where shorter.
336
+ */
337
+ function convertOklabFunctionsToHex (value) {
338
+ // Match oklab() with three numeric channels and an optional slash-delimited alpha.
339
+ // A negative channel may touch the preceding number because its sign separates the CSS number tokens.
340
+ return value.replace(/\boklab\(\s*(-?(?:\d+|\d*\.\d+))(?:\s+|(?=-))(-?(?:\d+|\d*\.\d+))(?:\s+|(?=-))(-?(?:\d+|\d*\.\d+))(?:\s*\/\s*(-?(?:\d+|\d*\.\d+)%?))?\s*\)/gi, (match, lightnessText, aAxisText, bAxisText, alphaText) => {
341
+ const alpha = parseAlphaString(alphaText);
342
+ const hex = convertOklabToHex(parseFloat(lightnessText), parseFloat(aAxisText), parseFloat(bAxisText), alpha);
343
+ if (!hex) {
344
+ return match; // out-of-gamut: keep native oklab form
345
+ }
346
+ if (hex.length < match.length) {
347
+ return hex;
348
+ }
349
+ return match;
350
+ });
351
+ }
352
+
353
+ /**
354
+ * Replaces the `none` keyword inside `rgb()` and `hsl()` functions with zero,
355
+ * which is how CSS Color Level 4 treats a missing component.
356
+ *
357
+ * @param {string} value The CSS value string that may contain rgb() or hsl() colors.
358
+ * @return {string} The value with every `none` component of those functions written as 0.
359
+ */
360
+ function replaceNoneInLegacyColorFunctions (value) {
361
+ // Match an rgb(), rgba(), hsl(), or hsla() function up to its closing parenthesis
362
+ return value.replace(/\b(rgba?|hsla?)\([^)]*\)/gi, (match) => {
363
+ // Match the none keyword as a whole word
364
+ return match.replace(/\bnone\b/gi, '0');
365
+ });
366
+ }
367
+
368
+ /**
369
+ * Converts `hwb()` colors to hex. Whiteness and blackness are always read as
370
+ * percentages, so their percent signs are optional.
371
+ *
372
+ * @param {string} value The CSS value string that may contain hwb() colors.
373
+ * @return {string} The value with hwb() colors replaced by hex.
374
+ */
375
+ function convertHwbFunctionsToHex (value) {
376
+ // hwb() → hex (percent signs optional for whiteness/blackness, values always treated as percentages)
377
+ return value.replace(/\bhwb\(\s*(-?(?:\d+|\d*\.\d+))\s+((?:\d+|\d*\.\d+))%?\s+((?:\d+|\d*\.\d+))%?(?:\s*\/\s*(-?(?:\d+|\d*\.\d+)%?))?\s*\)/gi, (match, hueText, whitenessText, blacknessText, alphaText) => {
378
+ const [r, g, b] = hwbToRgbChannels(parseFloat(hueText), parseFloat(whitenessText) / 100, parseFloat(blacknessText) / 100);
379
+ return rgbaToHex(r, g, b, parseAlphaString(alphaText));
380
+ });
381
+ }
382
+
383
+ /**
384
+ * Converts the space-separated syntax of `rgb()` and `hsl()` colors to hex.
385
+ *
386
+ * @param {string} value The CSS value string that may contain rgb() or hsl() colors.
387
+ * @return {string} The value with space-separated rgb() and hsl() colors replaced by hex.
388
+ */
389
+ function convertSpaceSeparatedRgbAndHslToHex (value) {
390
+ // rgb()/rgba() space syntax → hex, case-insensitive (handles decimals and any alpha)
391
+ value = value.replace(/\brgba?\(\s*(-?(?:\d+|\d*\.\d+))\s+(-?(?:\d+|\d*\.\d+))\s+(-?(?:\d+|\d*\.\d+))(?:\s*\/\s*(-?(?:\d+|\d*\.\d+)%?))?\s*\)/gi, (match, redText, greenText, blueText, alphaText) => {
392
+ const r = Math.round(parseFloat(redText));
393
+ const g = Math.round(parseFloat(greenText));
394
+ const b = Math.round(parseFloat(blueText));
395
+ return rgbaToHex(r, g, b, parseAlphaString(alphaText));
396
+ });
397
+
398
+ // hsl()/hsla() space syntax → hex, case-insensitive (percent signs optional, values always treated as percentages)
399
+ value = value.replace(/\bhsla?\(\s*(-?(?:\d+|\d*\.\d+))\s+((?:\d+|\d*\.\d+))%?\s+((?:\d+|\d*\.\d+))%?(?:\s*\/\s*(-?(?:\d+|\d*\.\d+)%?))?\s*\)/gi, (match, hueText, saturationText, lightnessText, alphaText) => {
400
+ const [r, g, b] = hslToRgbChannels(parseFloat(hueText), parseFloat(saturationText) / 100, parseFloat(lightnessText) / 100);
401
+ return rgbaToHex(r, g, b, parseAlphaString(alphaText));
402
+ });
403
+ return value;
404
+ }
405
+
406
+ /**
407
+ * Converts the legacy comma-separated syntax of `rgb()` and `hsl()` colors to
408
+ * hex, with or without an alpha component.
409
+ *
410
+ * @param {string} value The CSS value string that may contain rgb() or hsl() colors.
411
+ * @return {string} The value with comma-separated rgb() and hsl() colors replaced by hex.
412
+ */
413
+ function convertCommaSeparatedRgbAndHslToHex (value) {
414
+ // rgba() comma syntax → hex, case-insensitive (handles any alpha)
415
+ value = value.replace(/\brgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?(?:\d+|\d*\.\d+)%?)\s*\)/gi, (match, redText, greenText, blueText, alphaText) => {
416
+ const r = parseInt(redText, 10);
417
+ const g = parseInt(greenText, 10);
418
+ const b = parseInt(blueText, 10);
419
+ return rgbaToHex(r, g, b, parseAlphaString(alphaText));
420
+ });
421
+
422
+ // hsla() comma syntax → hex, case-insensitive (percent signs optional, values always treated as percentages)
423
+ value = value.replace(/\bhsla\(\s*(-?(?:\d+|\d*\.\d+))\s*,\s*((?:\d+|\d*\.\d+))%?\s*,\s*((?:\d+|\d*\.\d+))%?\s*,\s*(-?(?:\d+|\d*\.\d+)%?)\s*\)/gi, (match, hueText, saturationText, lightnessText, alphaText) => {
424
+ const [r, g, b] = hslToRgbChannels(parseFloat(hueText), parseFloat(saturationText) / 100, parseFloat(lightnessText) / 100);
425
+ return rgbaToHex(r, g, b, parseAlphaString(alphaText));
426
+ });
427
+
428
+ // hsl()/hsla() comma syntax without alpha → hex, case-insensitive (percent signs optional, values always treated as percentages)
429
+ value = value.replace(/\bhsla?\(\s*(-?(?:\d+|\d*\.\d+))\s*,\s*((?:\d+|\d*\.\d+))%?\s*,\s*((?:\d+|\d*\.\d+))%?\s*\)/gi, (match, hueText, saturationText, lightnessText) => {
430
+ const [r, g, b] = hslToRgbChannels(parseFloat(hueText), parseFloat(saturationText) / 100, parseFloat(lightnessText) / 100);
431
+ return rgbaToHex(r, g, b, 1);
432
+ });
433
+
434
+ // rgb() comma syntax → hex, case-insensitive
435
+ value = value.replace(/\brgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/gi, (match, r, g, b) => {
436
+ return rgbaToHex(parseInt(r, 10), parseInt(g, 10), parseInt(b, 10), 1);
437
+ });
438
+ return value;
439
+ }
440
+
441
+ /**
442
+ * Converts CSS color functions (rgb, hsl, hwb, oklab, color-mix, etc.) to their
443
+ * shortest hex equivalents and applies hex shortening.
444
+ *
445
+ * @param {string} value The CSS value string with potential color functions.
446
+ * @return {string} The value with color functions converted to hex where shorter.
447
+ */
448
+ function convertColorsToHex (value) {
449
+ let converted = resolveComputedColors(value);
450
+ converted = convertLabFunctionsToHex(converted);
451
+ // Convert in-gamut oklch() to hex before precision rounding, so the full authored precision is used
452
+ converted = convertOklchFunctionsToHex(converted);
453
+ converted = compactColorFunctionArguments(converted);
454
+ converted = convertOklabFunctionsToHex(converted);
455
+ converted = replaceNoneInLegacyColorFunctions(converted);
456
+ converted = convertHwbFunctionsToHex(converted);
457
+ converted = convertSpaceSeparatedRgbAndHslToHex(converted);
458
+ return convertCommaSeparatedRgbAndHslToHex(converted);
459
+ }
460
+
461
+ export {
462
+ convertColorsToHex
463
+ };
@@ -7,7 +7,7 @@ import {
7
7
  skipQuotedString,
8
8
  splitTopLevelCommaList,
9
9
  startsUrlToken
10
- } from '../parser/source-search.js';
10
+ } from '../../parser/source-search.js';
11
11
 
12
12
  /**
13
13
  * Determines whether a character can appear inside a CSS identifier.
@@ -5,7 +5,8 @@
5
5
  import {
6
6
  findMatchingParenthesis,
7
7
  splitTopLevelCommaList
8
- } from '../parser/source-search.js';
8
+ } from '../../parser/source-search.js';
9
+ import { roundCompactNumber } from '../shared.js';
9
10
 
10
11
  import {
11
12
  convertOklchToHex,
@@ -14,8 +15,7 @@ import {
14
15
  rgbToOklab,
15
16
  rgbToOklch,
16
17
  rgbaToHex
17
- } from './colors.js';
18
- import { roundCompactNumber } from './shared.js';
18
+ } from './conversion.js';
19
19
 
20
20
  /**
21
21
  * Interpolates between two hue angles along the shorter arc, per the CSS Color specification.
@@ -6,14 +6,14 @@ import {
6
6
  findMatchingParenthesis,
7
7
  findTopLevelDelimiter,
8
8
  splitTopLevelComponents
9
- } from '../parser/source-search.js';
9
+ } from '../../parser/source-search.js';
10
+ import { parseAlphaString } from '../shared.js';
10
11
 
11
12
  import {
12
13
  parseColor,
13
14
  rgbaToHex,
14
15
  shortestColor
15
- } from './colors.js';
16
- import { parseAlphaString } from './shared.js';
16
+ } from './conversion.js';
17
17
 
18
18
  /**
19
19
  * Handle color(from ...) relative color syntax for simple identity cases.