@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.
- package/README.md +0 -1
- package/package.json +2 -2
- package/src/declarations/border.js +1 -1
- package/src/declarations/process.js +155 -1
- package/src/index.js +7 -7
- 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/stringify.js +13 -0
- package/src/utilities.js +14 -0
- 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 +127 -1669
- 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 -2314
- 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,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 {
|
|
6
|
-
|
|
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
|
+
};
|
|
@@ -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.
|