@thejaredwilcurt/csslop 0.0.16 → 0.0.18
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 +22 -3
- package/package.json +8 -7
- package/src/index.js +3 -1
- package/src/rules/custom-properties.js +89 -0
- package/src/rules/optimize.js +294 -1
- package/src/rules/selectors.js +304 -0
- package/src/rules/stringify.js +61 -337
- package/src/value/color-mix.js +500 -0
- package/src/value/colors.js +54 -483
- package/src/value/light-dark.js +188 -0
- package/src/value/minify.js +85 -167
- package/src/value/relative-colors.js +336 -0
- package/src/value/syntax.js +56 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Simplifies CSS light-dark functions whose light and dark values are equivalent.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { findMatchingParenthesis } from './syntax.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Determines whether a character can appear inside a CSS identifier.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} character A single character.
|
|
11
|
+
* @return {boolean} True when the character is identifier-like.
|
|
12
|
+
*/
|
|
13
|
+
function isIdentifierCharacter (character) {
|
|
14
|
+
if (!character) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const codePoint = character.charCodeAt(0);
|
|
18
|
+
const isUppercaseLetter = codePoint >= 65 && codePoint <= 90;
|
|
19
|
+
const isLowercaseLetter = codePoint >= 97 && codePoint <= 122;
|
|
20
|
+
const isDigit = codePoint >= 48 && codePoint <= 57;
|
|
21
|
+
return isUppercaseLetter || isLowercaseLetter || isDigit || character === '_' || character === '-';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Detects a `light-dark(` function call at the provided position.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} value The CSS value being scanned.
|
|
28
|
+
* @param {number} index The candidate start index.
|
|
29
|
+
* @return {boolean} True when a light-dark function starts at index.
|
|
30
|
+
*/
|
|
31
|
+
function startsLightDarkFunction (value, index) {
|
|
32
|
+
if (value.slice(index, index + 11).toLowerCase() !== 'light-dark(') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return !isIdentifierCharacter(value[index - 1]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Splits a CSS function argument list at top-level commas while respecting
|
|
40
|
+
* nested parentheses and quoted strings.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} argumentString The raw content between function parentheses.
|
|
43
|
+
* @return {Array} The top-level argument strings.
|
|
44
|
+
*/
|
|
45
|
+
function splitTopLevelFunctionArguments (argumentString) {
|
|
46
|
+
const argumentsList = [];
|
|
47
|
+
let currentArgument = '';
|
|
48
|
+
let depth = 0;
|
|
49
|
+
let index = 0;
|
|
50
|
+
let activeQuote = '';
|
|
51
|
+
|
|
52
|
+
while (index < argumentString.length) {
|
|
53
|
+
const character = argumentString[index];
|
|
54
|
+
if (activeQuote) {
|
|
55
|
+
currentArgument += character;
|
|
56
|
+
if (character === '\\') {
|
|
57
|
+
if (index + 1 < argumentString.length) {
|
|
58
|
+
currentArgument += argumentString[index + 1];
|
|
59
|
+
index += 2;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
} else if (character === activeQuote) {
|
|
63
|
+
activeQuote = '';
|
|
64
|
+
}
|
|
65
|
+
index++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (character === '"' || character === '\'') {
|
|
70
|
+
activeQuote = character;
|
|
71
|
+
currentArgument += character;
|
|
72
|
+
index++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (character === '(') {
|
|
77
|
+
depth++;
|
|
78
|
+
currentArgument += character;
|
|
79
|
+
index++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (character === ')') {
|
|
84
|
+
if (depth > 0) {
|
|
85
|
+
depth--;
|
|
86
|
+
}
|
|
87
|
+
currentArgument += character;
|
|
88
|
+
index++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (character === ',' && depth === 0) {
|
|
93
|
+
argumentsList.push(currentArgument.trim());
|
|
94
|
+
currentArgument = '';
|
|
95
|
+
index++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
currentArgument += character;
|
|
100
|
+
index++;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
argumentsList.push(currentArgument.trim());
|
|
104
|
+
return argumentsList;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Simplifies `light-dark(a,b)` to `a` when both top-level arguments are identical
|
|
109
|
+
* after prior minification has normalized them.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} value The CSS value to simplify.
|
|
112
|
+
* @return {string} The value with redundant light-dark functions removed.
|
|
113
|
+
*/
|
|
114
|
+
function simplifyEquivalentLightDarkFunctions (value) {
|
|
115
|
+
let result = '';
|
|
116
|
+
let index = 0;
|
|
117
|
+
|
|
118
|
+
const consumeQuoted = (start) => {
|
|
119
|
+
const quote = value[start];
|
|
120
|
+
let end = start + 1;
|
|
121
|
+
while (end < value.length) {
|
|
122
|
+
if (value[end] === '\\') {
|
|
123
|
+
end += 2;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (value[end] === quote) {
|
|
127
|
+
end++;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
end++;
|
|
131
|
+
}
|
|
132
|
+
return end;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const startsUrl = (start) => {
|
|
136
|
+
return value.slice(start, start + 4).toLowerCase() === 'url(';
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
while (index < value.length) {
|
|
140
|
+
if (value[index] === '"' || value[index] === '\'') {
|
|
141
|
+
const end = consumeQuoted(index);
|
|
142
|
+
result += value.slice(index, end);
|
|
143
|
+
index = end;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (startsUrl(index)) {
|
|
148
|
+
const end = findMatchingParenthesis(value, index + 3);
|
|
149
|
+
if (end === -1) {
|
|
150
|
+
result += value.slice(index);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
result += value.slice(index, end + 1);
|
|
154
|
+
index = end + 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (startsLightDarkFunction(value, index)) {
|
|
159
|
+
const openParenIndex = index + 10;
|
|
160
|
+
const closingParenIndex = findMatchingParenthesis(value, openParenIndex);
|
|
161
|
+
if (closingParenIndex === -1) {
|
|
162
|
+
result += value.slice(index);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const argumentString = value.slice(openParenIndex + 1, closingParenIndex);
|
|
167
|
+
const argumentsList = splitTopLevelFunctionArguments(argumentString);
|
|
168
|
+
if (argumentsList.length === 2) {
|
|
169
|
+
const firstArgument = simplifyEquivalentLightDarkFunctions(argumentsList[0]);
|
|
170
|
+
const secondArgument = simplifyEquivalentLightDarkFunctions(argumentsList[1]);
|
|
171
|
+
if (firstArgument === secondArgument) {
|
|
172
|
+
result += firstArgument;
|
|
173
|
+
} else {
|
|
174
|
+
result += value.slice(index, openParenIndex + 1) + firstArgument + ',' + secondArgument + ')';
|
|
175
|
+
}
|
|
176
|
+
index = closingParenIndex + 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
result += value[index];
|
|
182
|
+
index++;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export { simplifyEquivalentLightDarkFunctions };
|
package/src/value/minify.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
import { isUnicodeCharset } from '../context.js';
|
|
6
6
|
import { resolveUnicodeEscape } from '../utilities.js';
|
|
7
7
|
|
|
8
|
+
import { evaluateColorMix } from './color-mix.js';
|
|
8
9
|
import {
|
|
10
|
+
convertLabToHex,
|
|
9
11
|
convertOklabToHex,
|
|
10
|
-
evaluateColorMix,
|
|
11
|
-
evaluateRelativeColor,
|
|
12
12
|
hslToRgbChannels,
|
|
13
13
|
hwbToRgbChannels,
|
|
14
14
|
parseHex,
|
|
@@ -16,18 +16,24 @@ import {
|
|
|
16
16
|
shortestColor
|
|
17
17
|
} from './colors.js';
|
|
18
18
|
import { minifyGradients } from './gradients.js';
|
|
19
|
+
import { simplifyEquivalentLightDarkFunctions } from './light-dark.js';
|
|
19
20
|
import {
|
|
20
21
|
normalizeMathFunctions,
|
|
21
22
|
simplifyStandaloneCalc
|
|
22
23
|
} from './math.js';
|
|
23
24
|
import { namedColors } from './named-colors.js';
|
|
24
25
|
import { isQuotesNoneEquivalent } from './quotes.js';
|
|
26
|
+
import {
|
|
27
|
+
evaluateRelativeColor,
|
|
28
|
+
minifyRelativeColorSyntax
|
|
29
|
+
} from './relative-colors.js';
|
|
25
30
|
import {
|
|
26
31
|
collapseShorthandParts,
|
|
27
32
|
normalizeScaleComponent,
|
|
28
33
|
parseAlphaString,
|
|
29
34
|
roundCompactNumber
|
|
30
35
|
} from './shared.js';
|
|
36
|
+
import { findMatchingParenthesis } from './syntax.js';
|
|
31
37
|
import { minifyTransformValue } from './transforms.js';
|
|
32
38
|
import { optimizeUnicodeRange } from './unicode-range.js';
|
|
33
39
|
|
|
@@ -173,164 +179,63 @@ function replaceOutsideStringsAndUrls (value, replacer) {
|
|
|
173
179
|
}
|
|
174
180
|
|
|
175
181
|
/**
|
|
176
|
-
*
|
|
182
|
+
* Chooses the shortest valid representation for the path inside a `url(...)`
|
|
183
|
+
* token, weighing an unquoted form, an escaped single space, and a quoted form.
|
|
177
184
|
*
|
|
178
|
-
* @param {string}
|
|
179
|
-
* @return {
|
|
185
|
+
* @param {string} path The resolved url path, without surrounding quotes.
|
|
186
|
+
* @return {string} The shortest valid url() content string.
|
|
180
187
|
*/
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
const isUppercaseLetter = codePoint >= 65 && codePoint <= 90;
|
|
187
|
-
const isLowercaseLetter = codePoint >= 97 && codePoint <= 122;
|
|
188
|
-
const isDigit = codePoint >= 48 && codePoint <= 57;
|
|
189
|
-
return isUppercaseLetter || isLowercaseLetter || isDigit || character === '_' || character === '-';
|
|
190
|
-
}
|
|
188
|
+
function formatUrlPath (path) {
|
|
189
|
+
// Parentheses and quote characters are invalid inside an unquoted url() token
|
|
190
|
+
const hasQuoteForcingCharacters = /[()"']/.test(path);
|
|
191
|
+
// Count spaces so escaping them can be compared against keeping the quotes
|
|
192
|
+
const spaceCount = (path.match(/ /g) || []).length;
|
|
191
193
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
* @param {string} value The CSS value being scanned.
|
|
196
|
-
* @param {number} index The candidate start index.
|
|
197
|
-
* @return {boolean} True when a light-dark function starts at index.
|
|
198
|
-
*/
|
|
199
|
-
function startsLightDarkFunction (value, index) {
|
|
200
|
-
if (value.slice(index, index + 11).toLowerCase() !== 'light-dark(') {
|
|
201
|
-
return false;
|
|
194
|
+
if (hasQuoteForcingCharacters || spaceCount >= 2) {
|
|
195
|
+
// Escape any embedded double quotes so the double-quoted wrapper stays valid
|
|
196
|
+
return '"' + path.replace(/"/g, '\\"') + '"';
|
|
202
197
|
}
|
|
203
|
-
return !isIdentifierCharacter(value[index - 1]);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Finds the closing parenthesis for an opening parenthesis while respecting
|
|
208
|
-
* nested parentheses and quoted strings.
|
|
209
|
-
*
|
|
210
|
-
* @param {string} value The CSS text being scanned.
|
|
211
|
-
* @param {number} openParenIndex The index of the opening `(` character.
|
|
212
|
-
* @return {number} The closing `)` index, or -1 if unmatched.
|
|
213
|
-
*/
|
|
214
|
-
function findMatchingParenthesis (value, openParenIndex) {
|
|
215
|
-
let depth = 1;
|
|
216
|
-
let index = openParenIndex + 1;
|
|
217
|
-
let activeQuote = '';
|
|
218
|
-
|
|
219
|
-
while (index < value.length) {
|
|
220
|
-
const character = value[index];
|
|
221
|
-
if (activeQuote) {
|
|
222
|
-
if (character === '\\') {
|
|
223
|
-
index += 2;
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (character === activeQuote) {
|
|
227
|
-
activeQuote = '';
|
|
228
|
-
}
|
|
229
|
-
index++;
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
198
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (character === '(') {
|
|
240
|
-
depth++;
|
|
241
|
-
index++;
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (character === ')') {
|
|
246
|
-
depth--;
|
|
247
|
-
if (depth === 0) {
|
|
248
|
-
return index;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
index++;
|
|
199
|
+
if (spaceCount === 1) {
|
|
200
|
+
// A lone space is one byte shorter to escape than to wrap the value in quotes
|
|
201
|
+
return path.replace(/ /g, '\\ ');
|
|
252
202
|
}
|
|
253
203
|
|
|
254
|
-
return
|
|
204
|
+
return path;
|
|
255
205
|
}
|
|
256
206
|
|
|
257
207
|
/**
|
|
258
|
-
*
|
|
259
|
-
*
|
|
208
|
+
* Produces the shortest valid contents for a single `url(...)` token from the
|
|
209
|
+
* raw text between its parentheses, stripping a leading current-directory
|
|
210
|
+
* indicator and normalizing quoting.
|
|
260
211
|
*
|
|
261
|
-
* @param {string}
|
|
262
|
-
* @return {
|
|
212
|
+
* @param {string} rawContent The trimmed text found between the url parentheses.
|
|
213
|
+
* @return {string} The minified url() content.
|
|
263
214
|
*/
|
|
264
|
-
function
|
|
265
|
-
const
|
|
266
|
-
let
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
while (index < argumentString.length) {
|
|
272
|
-
const character = argumentString[index];
|
|
273
|
-
if (activeQuote) {
|
|
274
|
-
currentArgument += character;
|
|
275
|
-
if (character === '\\') {
|
|
276
|
-
if (index + 1 < argumentString.length) {
|
|
277
|
-
currentArgument += argumentString[index + 1];
|
|
278
|
-
index += 2;
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
} else if (character === activeQuote) {
|
|
282
|
-
activeQuote = '';
|
|
283
|
-
}
|
|
284
|
-
index++;
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (character === '"' || character === '\'') {
|
|
289
|
-
activeQuote = character;
|
|
290
|
-
currentArgument += character;
|
|
291
|
-
index++;
|
|
292
|
-
continue;
|
|
215
|
+
function minifyUrlContent (rawContent) {
|
|
216
|
+
const wasQuoted = rawContent.startsWith('"') || rawContent.startsWith('\'');
|
|
217
|
+
let path = rawContent;
|
|
218
|
+
if (wasQuoted) {
|
|
219
|
+
const quote = rawContent[0];
|
|
220
|
+
if (rawContent.length >= 2 && rawContent.endsWith(quote)) {
|
|
221
|
+
path = rawContent.slice(1, -1);
|
|
293
222
|
}
|
|
294
|
-
|
|
295
|
-
if (character === '(') {
|
|
296
|
-
depth++;
|
|
297
|
-
currentArgument += character;
|
|
298
|
-
index++;
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (character === ')') {
|
|
303
|
-
if (depth > 0) {
|
|
304
|
-
depth--;
|
|
305
|
-
}
|
|
306
|
-
currentArgument += character;
|
|
307
|
-
index++;
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (character === ',' && depth === 0) {
|
|
312
|
-
argumentsList.push(currentArgument.trim());
|
|
313
|
-
currentArgument = '';
|
|
314
|
-
index++;
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
currentArgument += character;
|
|
319
|
-
index++;
|
|
320
223
|
}
|
|
321
224
|
|
|
322
|
-
|
|
323
|
-
|
|
225
|
+
// Remove a leading current-directory indicator (`./`); browsers resolve it implicitly
|
|
226
|
+
path = path.replace(/^\.\//, '');
|
|
227
|
+
|
|
228
|
+
return formatUrlPath(path);
|
|
324
229
|
}
|
|
325
230
|
|
|
326
231
|
/**
|
|
327
|
-
*
|
|
328
|
-
*
|
|
232
|
+
* Rewrites every `url(...)` token in a CSS value to its shortest valid form,
|
|
233
|
+
* skipping any quoted strings so an embedded `url(` inside a string is ignored.
|
|
329
234
|
*
|
|
330
|
-
* @param {string} value The CSS value
|
|
331
|
-
* @return {string} The value with
|
|
235
|
+
* @param {string} value The CSS value string potentially containing url() tokens.
|
|
236
|
+
* @return {string} The value with all url() tokens minified.
|
|
332
237
|
*/
|
|
333
|
-
function
|
|
238
|
+
function minifyUrls (value) {
|
|
334
239
|
let result = '';
|
|
335
240
|
let index = 0;
|
|
336
241
|
|
|
@@ -364,37 +269,15 @@ function simplifyEquivalentLightDarkFunctions (value) {
|
|
|
364
269
|
}
|
|
365
270
|
|
|
366
271
|
if (startsUrl(index)) {
|
|
367
|
-
const
|
|
368
|
-
if (end === -1) {
|
|
369
|
-
result += value.slice(index);
|
|
370
|
-
break;
|
|
371
|
-
}
|
|
372
|
-
result += value.slice(index, end + 1);
|
|
373
|
-
index = end + 1;
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (startsLightDarkFunction(value, index)) {
|
|
378
|
-
const openParenIndex = index + 10;
|
|
379
|
-
const closingParenIndex = findMatchingParenthesis(value, openParenIndex);
|
|
272
|
+
const closingParenIndex = findMatchingParenthesis(value, index + 3);
|
|
380
273
|
if (closingParenIndex === -1) {
|
|
381
274
|
result += value.slice(index);
|
|
382
275
|
break;
|
|
383
276
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const firstArgument = simplifyEquivalentLightDarkFunctions(argumentsList[0]);
|
|
389
|
-
const secondArgument = simplifyEquivalentLightDarkFunctions(argumentsList[1]);
|
|
390
|
-
if (firstArgument === secondArgument) {
|
|
391
|
-
result += firstArgument;
|
|
392
|
-
} else {
|
|
393
|
-
result += value.slice(index, openParenIndex + 1) + firstArgument + ',' + secondArgument + ')';
|
|
394
|
-
}
|
|
395
|
-
index = closingParenIndex + 1;
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
277
|
+
const inner = value.slice(index + 4, closingParenIndex).trim();
|
|
278
|
+
result += 'url(' + minifyUrlContent(inner) + ')';
|
|
279
|
+
index = closingParenIndex + 1;
|
|
280
|
+
continue;
|
|
398
281
|
}
|
|
399
282
|
|
|
400
283
|
result += value[index];
|
|
@@ -477,6 +360,24 @@ function convertColorsToHex (val) {
|
|
|
477
360
|
}
|
|
478
361
|
}
|
|
479
362
|
|
|
363
|
+
// Convert in-gamut lab() (CIE Lab, D50) to hex when it produces a shorter representation
|
|
364
|
+
val = val.replace(/\blab\(\s*(-?(?:\d+|\d*\.\d+)%?)\s+(-?(?:\d+|\d*\.\d+)%?)\s+(-?(?:\d+|\d*\.\d+)%?)(?:\s*\/\s*(-?(?:\d+|\d*\.\d+)%?))?\s*\)/gi, (match, lStr, aStr, bStr, alphaStr) => {
|
|
365
|
+
const alpha = parseAlphaString(alphaStr);
|
|
366
|
+
const l = parseFloat(lStr);
|
|
367
|
+
const aNumber = parseFloat(aStr);
|
|
368
|
+
const a = aStr.endsWith('%') ? aNumber * 1.25 : aNumber;
|
|
369
|
+
const bNumber = parseFloat(bStr);
|
|
370
|
+
const b = bStr.endsWith('%') ? bNumber * 1.25 : bNumber;
|
|
371
|
+
const hex = convertLabToHex(l, a, b, alpha);
|
|
372
|
+
if (!hex) {
|
|
373
|
+
return match; // out-of-gamut: keep native lab form
|
|
374
|
+
}
|
|
375
|
+
if (hex.length < match.length) {
|
|
376
|
+
return hex;
|
|
377
|
+
}
|
|
378
|
+
return match;
|
|
379
|
+
});
|
|
380
|
+
|
|
480
381
|
// Minify whitespace and numeric precision inside wide-gamut and functional color notations
|
|
481
382
|
val = val.replace(/\b(oklab|oklch|lch|lab|color|hwb)\((.*?)\)/gi, (match, func, inner) => {
|
|
482
383
|
// Collapse whitespace to single space
|
|
@@ -519,6 +420,14 @@ function convertColorsToHex (val) {
|
|
|
519
420
|
}
|
|
520
421
|
return before + rounded;
|
|
521
422
|
});
|
|
423
|
+
// Remove trailing ".0" from numbers so integer channel values stay integer
|
|
424
|
+
// (e.g. display-p3 1.0 0.0 0.0 becomes display-p3 1 0 0)
|
|
425
|
+
minified = minified.replace(/(-?\d*)\.0\b/g, (match, integer) => {
|
|
426
|
+
if (integer === '' || integer === '-' || integer === '-0') {
|
|
427
|
+
return '0';
|
|
428
|
+
}
|
|
429
|
+
return integer;
|
|
430
|
+
});
|
|
522
431
|
return func + '(' + minified.trim() + ')';
|
|
523
432
|
});
|
|
524
433
|
|
|
@@ -835,6 +744,11 @@ function applyPropertyOptimizations (val, property) {
|
|
|
835
744
|
});
|
|
836
745
|
}
|
|
837
746
|
|
|
747
|
+
if (property === 'syntax') {
|
|
748
|
+
// Remove whitespace around pipe separators in @property syntax descriptors
|
|
749
|
+
val = val.replace(/\s*\|\s*/g, '|');
|
|
750
|
+
}
|
|
751
|
+
|
|
838
752
|
// Simplify clamp() where all three arguments are identical (e.g. clamp(1rem,1rem,1rem) → 1rem)
|
|
839
753
|
val = val.replace(/\bclamp\(([^,]+),\1,\1\)/gi, '$1');
|
|
840
754
|
|
|
@@ -976,6 +890,7 @@ function minifyValue (declaration) {
|
|
|
976
890
|
if (typeof val === 'string') {
|
|
977
891
|
val = val.trim();
|
|
978
892
|
val = normalizeWhitespaceAndQuotes(val, declaration.property);
|
|
893
|
+
val = minifyUrls(val);
|
|
979
894
|
|
|
980
895
|
// Instead of unconditionally removing spaces around + and - and *, handle math vs non-math
|
|
981
896
|
// Collapse spaces around division operator
|
|
@@ -1039,6 +954,9 @@ function minifyValue (declaration) {
|
|
|
1039
954
|
|
|
1040
955
|
// Property-specific optimizations
|
|
1041
956
|
val = applyPropertyOptimizations(val, declaration.property);
|
|
957
|
+
|
|
958
|
+
// Minify relative color syntax (identity resolution and whitespace collapsing)
|
|
959
|
+
val = minifyRelativeColorSyntax(val);
|
|
1042
960
|
}
|
|
1043
961
|
|
|
1044
962
|
// Gradient optimizations
|