@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.
- package/README.md +3 -4
- package/package.json +3 -3
- package/src/declarations/border.js +1 -1
- package/src/declarations/css-wide-keywords.js +37 -1
- package/src/declarations/fold.js +13 -3
- package/src/declarations/process.js +60 -7
- package/src/index.js +12 -7
- package/src/parser/source-search.js +6 -0
- package/src/rules/optimize/at-rules.js +113 -0
- package/src/rules/optimize/declaration-writes.js +70 -0
- package/src/rules/optimize/empty-rules.js +51 -0
- package/src/rules/optimize/expand-nesting.js +236 -0
- package/src/rules/optimize/factor-parents.js +343 -0
- package/src/rules/optimize/index.js +71 -0
- package/src/rules/optimize/merge-declarations.js +199 -0
- package/src/rules/optimize/merge-nested-rules.js +117 -0
- package/src/rules/optimize/merge-selectors.js +252 -0
- package/src/rules/optimize/nest-rules.js +202 -0
- package/src/rules/optimize/override-index.js +264 -0
- package/src/rules/optimize/prune-overridden.js +267 -0
- package/src/rules/optimize/selector-normalization.js +60 -0
- package/src/rules/optimize/specificity.js +280 -0
- package/src/{position-try.js → rules/position-try.js} +1 -1
- package/src/rules/selectors.js +9 -0
- package/src/rules/stringify.js +138 -76
- package/src/utilities.js +9 -8
- package/src/value/border.js +85 -0
- package/src/value/{colors.js → color/conversion.js} +3 -2
- package/src/value/color/functions.js +463 -0
- package/src/value/{light-dark.js → color/light-dark.js} +1 -1
- package/src/value/{color-mix.js → color/mix.js} +3 -3
- package/src/value/{relative-colors.js → color/relative.js} +3 -3
- package/src/value/color/shorten.js +93 -0
- package/src/value/gradients.js +1 -1
- package/src/value/hyphenate-limit-chars.js +72 -0
- package/src/value/initial-keywords.js +128 -0
- package/src/value/lengths.js +55 -0
- package/src/value/math/evaluate.js +36 -0
- package/src/value/math/fold.js +315 -0
- package/src/value/math/format.js +230 -0
- package/src/value/math/functions.js +302 -0
- package/src/value/math/index.js +219 -0
- package/src/value/math/parser.js +301 -0
- package/src/value/math/property-ranges.js +174 -0
- package/src/value/math/rewrite.js +222 -0
- package/src/value/math/tokenizer.js +160 -0
- package/src/value/math/unit-totals.js +194 -0
- package/src/value/minify.js +162 -1678
- package/src/value/numbers.js +28 -0
- package/src/value/positions.js +152 -0
- package/src/value/property-optimizations.js +485 -0
- package/src/value/segments.js +74 -0
- package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
- package/src/value/{svg-markup.js → svg/markup.js} +1 -1
- package/src/value/time.js +75 -0
- package/src/value/urls.js +242 -0
- package/src/value/whitespace.js +88 -0
- package/src/rules/optimize.js +0 -2031
- package/src/value/math-expression.js +0 -1156
- package/src/value/math.js +0 -886
- /package/src/value/{named-colors.js → color/named.js} +0 -0
|
@@ -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
|
+
};
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import {
|
|
6
6
|
findMatchingParenthesis,
|
|
7
7
|
splitTopLevelCommaList
|
|
8
|
-
} from '
|
|
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 './
|
|
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 '
|
|
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 './
|
|
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.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Rewrites hex colors and named color keywords in their shortest equivalent spelling.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { replaceOutsideStringsAndUrls } from '../segments.js';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
parseHex,
|
|
9
|
+
shortestColor
|
|
10
|
+
} from './conversion.js';
|
|
11
|
+
import { namedColors } from './named.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Regex matching hex color tokens (#rgb, #rgba, #rrggbb, #rrggbbaa) and CSS named
|
|
15
|
+
* color keywords. Hex patterns are ordered longest-first to avoid partial matches.
|
|
16
|
+
* Named colors are sorted longest-first so longer names like `darkslategray` are
|
|
17
|
+
* matched before shorter substrings. A named color only counts as a color keyword
|
|
18
|
+
* when it is a complete identifier, so hyphens on either side disqualify it (this
|
|
19
|
+
* keeps identifiers such as the custom property name in `var(--grey)` intact).
|
|
20
|
+
*
|
|
21
|
+
* @type {RegExp}
|
|
22
|
+
*/
|
|
23
|
+
const COLOR_TOKEN_PATTERN = new RegExp(
|
|
24
|
+
'#[0-9a-fA-F]{8}(?![0-9a-fA-F])|' +
|
|
25
|
+
'#[0-9a-fA-F]{6}(?![0-9a-fA-F])|' +
|
|
26
|
+
'#[0-9a-fA-F]{4}(?![0-9a-fA-F])|' +
|
|
27
|
+
'#[0-9a-fA-F]{3}(?![0-9a-fA-F])|' +
|
|
28
|
+
'(?<![\\w-])(?:' +
|
|
29
|
+
Object.keys(namedColors).sort((first, second) => {
|
|
30
|
+
return second.length - first.length;
|
|
31
|
+
}).join('|') +
|
|
32
|
+
')(?![\\w-])',
|
|
33
|
+
'gi'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Replaces every hex color and named color keyword in a CSS value segment with
|
|
38
|
+
* the shortest equivalent representation, comparing full hex, shortened hex,
|
|
39
|
+
* and any matching named color keyword.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} segment A CSS value segment (outside strings and urls).
|
|
42
|
+
* @param {boolean} rewritesEqualLengthSpelling Whether a spelling of the same length is worth switching to.
|
|
43
|
+
* @return {string} The segment with all colors shortened to their minimal form.
|
|
44
|
+
*/
|
|
45
|
+
function shortenColorValues (segment, rewritesEqualLengthSpelling = true) {
|
|
46
|
+
// Match "color-mix(" as a whole word, case-insensitive
|
|
47
|
+
const hasColorMix = /\bcolor-mix\(/i.test(segment);
|
|
48
|
+
return segment.replace(COLOR_TOKEN_PATTERN, (match) => {
|
|
49
|
+
let channels;
|
|
50
|
+
if (match.startsWith('#')) {
|
|
51
|
+
channels = parseHex(match);
|
|
52
|
+
} else {
|
|
53
|
+
if (hasColorMix) {
|
|
54
|
+
return match;
|
|
55
|
+
}
|
|
56
|
+
const rgb = namedColors[match.toLowerCase()];
|
|
57
|
+
if (rgb) {
|
|
58
|
+
// transparent has alpha=0, all other named colors have alpha=1
|
|
59
|
+
const alpha = match.toLowerCase() === 'transparent' ? 0 : 1;
|
|
60
|
+
channels = [rgb[0], rgb[1], rgb[2], alpha];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!channels) {
|
|
64
|
+
return match;
|
|
65
|
+
}
|
|
66
|
+
const shortest = shortestColor(channels[0], channels[1], channels[2], channels[3]);
|
|
67
|
+
if (!rewritesEqualLengthSpelling && shortest.length >= match.length) {
|
|
68
|
+
return match;
|
|
69
|
+
}
|
|
70
|
+
return shortest;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Lowercases every hex color token in a CSS value, since uppercase hex digits
|
|
76
|
+
* compress worse and are equivalent to their lowercase form.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} value The CSS value string.
|
|
79
|
+
* @return {string} The value with all hex color tokens lowercased.
|
|
80
|
+
*/
|
|
81
|
+
function lowercaseHexColors (value) {
|
|
82
|
+
return replaceOutsideStringsAndUrls(value, (segment) => {
|
|
83
|
+
// Match hex color tokens of 3 to 8 hex digits
|
|
84
|
+
return segment.replace(/#([0-9a-fA-F]{3,8})\b/gi, (hexColor) => {
|
|
85
|
+
return hexColor.toLowerCase();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
lowercaseHexColors,
|
|
92
|
+
shortenColorValues
|
|
93
|
+
};
|
package/src/value/gradients.js
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Collapses `hyphenate-limit-chars` values to their shortest equivalent form.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { hasSubstitutedParts } from './shared.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Matches a trailing `!important` flag, so the components in front of it can be
|
|
9
|
+
* rewritten without the flag being mistaken for part of the last one.
|
|
10
|
+
*
|
|
11
|
+
* @type {RegExp}
|
|
12
|
+
*/
|
|
13
|
+
const IMPORTANT_SUFFIX_PATTERN = /!important$/i;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Separates a value from the `!important` flag it may end with.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} value The declaration value, possibly flagged important.
|
|
19
|
+
* @return {object} The components in front of the flag, and the flag itself or an empty string.
|
|
20
|
+
*/
|
|
21
|
+
function splitImportantSuffix (value) {
|
|
22
|
+
const match = value.match(IMPORTANT_SUFFIX_PATTERN);
|
|
23
|
+
if (!match) {
|
|
24
|
+
return { components: value, importantSuffix: '' };
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
components: value.slice(0, match.index).trim(),
|
|
28
|
+
importantSuffix: match[0]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Collapses a `hyphenate-limit-chars` value to its shortest equivalent form.
|
|
34
|
+
* The property states up to three components: the shortest word that may be
|
|
35
|
+
* hyphenated, then the fewest characters allowed before a hyphen, then the
|
|
36
|
+
* fewest allowed after it. An unstated "before" component is read as `auto`,
|
|
37
|
+
* and an unstated "after" component repeats whatever "before" holds, so a
|
|
38
|
+
* trailing component that only restates the default it stands in for is
|
|
39
|
+
* redundant.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} value The `hyphenate-limit-chars` value.
|
|
42
|
+
* @return {string} The value with its redundant trailing components dropped.
|
|
43
|
+
*/
|
|
44
|
+
function collapseHyphenateLimitChars (value) {
|
|
45
|
+
const {
|
|
46
|
+
components,
|
|
47
|
+
importantSuffix
|
|
48
|
+
} = splitImportantSuffix(value);
|
|
49
|
+
const parts = components.split(' ').filter(Boolean);
|
|
50
|
+
if (hasSubstitutedParts(parts)) {
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
const isAfterMatchingBefore = (
|
|
54
|
+
parts.length === 3 &&
|
|
55
|
+
parts[2].toLowerCase() === parts[1].toLowerCase()
|
|
56
|
+
);
|
|
57
|
+
if (isAfterMatchingBefore) {
|
|
58
|
+
parts.pop();
|
|
59
|
+
}
|
|
60
|
+
const isBeforeLeftAtDefault = (
|
|
61
|
+
parts.length === 2 &&
|
|
62
|
+
parts[1].toLowerCase() === 'auto'
|
|
63
|
+
);
|
|
64
|
+
if (isBeforeLeftAtDefault) {
|
|
65
|
+
parts.pop();
|
|
66
|
+
}
|
|
67
|
+
return parts.join(' ') + importantSuffix;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
collapseHyphenateLimitChars
|
|
72
|
+
};
|