@thejaredwilcurt/csslop 0.0.16 → 0.0.17
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 +6 -5
- package/src/index.js +3 -1
- package/src/rules/optimize.js +294 -1
- package/src/rules/stringify.js +69 -1
- package/src/value/colors.js +0 -21
- package/src/value/light-dark.js +188 -0
- package/src/value/minify.js +52 -166
- 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
|
@@ -8,7 +8,6 @@ import { resolveUnicodeEscape } from '../utilities.js';
|
|
|
8
8
|
import {
|
|
9
9
|
convertOklabToHex,
|
|
10
10
|
evaluateColorMix,
|
|
11
|
-
evaluateRelativeColor,
|
|
12
11
|
hslToRgbChannels,
|
|
13
12
|
hwbToRgbChannels,
|
|
14
13
|
parseHex,
|
|
@@ -16,18 +15,24 @@ import {
|
|
|
16
15
|
shortestColor
|
|
17
16
|
} from './colors.js';
|
|
18
17
|
import { minifyGradients } from './gradients.js';
|
|
18
|
+
import { simplifyEquivalentLightDarkFunctions } from './light-dark.js';
|
|
19
19
|
import {
|
|
20
20
|
normalizeMathFunctions,
|
|
21
21
|
simplifyStandaloneCalc
|
|
22
22
|
} from './math.js';
|
|
23
23
|
import { namedColors } from './named-colors.js';
|
|
24
24
|
import { isQuotesNoneEquivalent } from './quotes.js';
|
|
25
|
+
import {
|
|
26
|
+
evaluateRelativeColor,
|
|
27
|
+
minifyRelativeColorSyntax
|
|
28
|
+
} from './relative-colors.js';
|
|
25
29
|
import {
|
|
26
30
|
collapseShorthandParts,
|
|
27
31
|
normalizeScaleComponent,
|
|
28
32
|
parseAlphaString,
|
|
29
33
|
roundCompactNumber
|
|
30
34
|
} from './shared.js';
|
|
35
|
+
import { findMatchingParenthesis } from './syntax.js';
|
|
31
36
|
import { minifyTransformValue } from './transforms.js';
|
|
32
37
|
import { optimizeUnicodeRange } from './unicode-range.js';
|
|
33
38
|
|
|
@@ -173,164 +178,63 @@ function replaceOutsideStringsAndUrls (value, replacer) {
|
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
/**
|
|
176
|
-
*
|
|
181
|
+
* Chooses the shortest valid representation for the path inside a `url(...)`
|
|
182
|
+
* token, weighing an unquoted form, an escaped single space, and a quoted form.
|
|
177
183
|
*
|
|
178
|
-
* @param {string}
|
|
179
|
-
* @return {
|
|
184
|
+
* @param {string} path The resolved url path, without surrounding quotes.
|
|
185
|
+
* @return {string} The shortest valid url() content string.
|
|
180
186
|
*/
|
|
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
|
-
}
|
|
187
|
+
function formatUrlPath (path) {
|
|
188
|
+
// Parentheses and quote characters are invalid inside an unquoted url() token
|
|
189
|
+
const hasQuoteForcingCharacters = /[()"']/.test(path);
|
|
190
|
+
// Count spaces so escaping them can be compared against keeping the quotes
|
|
191
|
+
const spaceCount = (path.match(/ /g) || []).length;
|
|
191
192
|
|
|
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;
|
|
193
|
+
if (hasQuoteForcingCharacters || spaceCount >= 2) {
|
|
194
|
+
// Escape any embedded double quotes so the double-quoted wrapper stays valid
|
|
195
|
+
return '"' + path.replace(/"/g, '\\"') + '"';
|
|
202
196
|
}
|
|
203
|
-
return !isIdentifierCharacter(value[index - 1]);
|
|
204
|
-
}
|
|
205
197
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
233
|
-
if (character === '"' || character === '\'') {
|
|
234
|
-
activeQuote = character;
|
|
235
|
-
index++;
|
|
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++;
|
|
198
|
+
if (spaceCount === 1) {
|
|
199
|
+
// A lone space is one byte shorter to escape than to wrap the value in quotes
|
|
200
|
+
return path.replace(/ /g, '\\ ');
|
|
252
201
|
}
|
|
253
202
|
|
|
254
|
-
return
|
|
203
|
+
return path;
|
|
255
204
|
}
|
|
256
205
|
|
|
257
206
|
/**
|
|
258
|
-
*
|
|
259
|
-
*
|
|
207
|
+
* Produces the shortest valid contents for a single `url(...)` token from the
|
|
208
|
+
* raw text between its parentheses, stripping a leading current-directory
|
|
209
|
+
* indicator and normalizing quoting.
|
|
260
210
|
*
|
|
261
|
-
* @param {string}
|
|
262
|
-
* @return {
|
|
211
|
+
* @param {string} rawContent The trimmed text found between the url parentheses.
|
|
212
|
+
* @return {string} The minified url() content.
|
|
263
213
|
*/
|
|
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;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (character === '(') {
|
|
296
|
-
depth++;
|
|
297
|
-
currentArgument += character;
|
|
298
|
-
index++;
|
|
299
|
-
continue;
|
|
214
|
+
function minifyUrlContent (rawContent) {
|
|
215
|
+
const wasQuoted = rawContent.startsWith('"') || rawContent.startsWith('\'');
|
|
216
|
+
let path = rawContent;
|
|
217
|
+
if (wasQuoted) {
|
|
218
|
+
const quote = rawContent[0];
|
|
219
|
+
if (rawContent.length >= 2 && rawContent.endsWith(quote)) {
|
|
220
|
+
path = rawContent.slice(1, -1);
|
|
300
221
|
}
|
|
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
222
|
}
|
|
321
223
|
|
|
322
|
-
|
|
323
|
-
|
|
224
|
+
// Remove a leading current-directory indicator (`./`); browsers resolve it implicitly
|
|
225
|
+
path = path.replace(/^\.\//, '');
|
|
226
|
+
|
|
227
|
+
return formatUrlPath(path);
|
|
324
228
|
}
|
|
325
229
|
|
|
326
230
|
/**
|
|
327
|
-
*
|
|
328
|
-
*
|
|
231
|
+
* Rewrites every `url(...)` token in a CSS value to its shortest valid form,
|
|
232
|
+
* skipping any quoted strings so an embedded `url(` inside a string is ignored.
|
|
329
233
|
*
|
|
330
|
-
* @param {string} value The CSS value
|
|
331
|
-
* @return {string} The value with
|
|
234
|
+
* @param {string} value The CSS value string potentially containing url() tokens.
|
|
235
|
+
* @return {string} The value with all url() tokens minified.
|
|
332
236
|
*/
|
|
333
|
-
function
|
|
237
|
+
function minifyUrls (value) {
|
|
334
238
|
let result = '';
|
|
335
239
|
let index = 0;
|
|
336
240
|
|
|
@@ -364,37 +268,15 @@ function simplifyEquivalentLightDarkFunctions (value) {
|
|
|
364
268
|
}
|
|
365
269
|
|
|
366
270
|
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);
|
|
271
|
+
const closingParenIndex = findMatchingParenthesis(value, index + 3);
|
|
380
272
|
if (closingParenIndex === -1) {
|
|
381
273
|
result += value.slice(index);
|
|
382
274
|
break;
|
|
383
275
|
}
|
|
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
|
-
}
|
|
276
|
+
const inner = value.slice(index + 4, closingParenIndex).trim();
|
|
277
|
+
result += 'url(' + minifyUrlContent(inner) + ')';
|
|
278
|
+
index = closingParenIndex + 1;
|
|
279
|
+
continue;
|
|
398
280
|
}
|
|
399
281
|
|
|
400
282
|
result += value[index];
|
|
@@ -976,6 +858,7 @@ function minifyValue (declaration) {
|
|
|
976
858
|
if (typeof val === 'string') {
|
|
977
859
|
val = val.trim();
|
|
978
860
|
val = normalizeWhitespaceAndQuotes(val, declaration.property);
|
|
861
|
+
val = minifyUrls(val);
|
|
979
862
|
|
|
980
863
|
// Instead of unconditionally removing spaces around + and - and *, handle math vs non-math
|
|
981
864
|
// Collapse spaces around division operator
|
|
@@ -1039,6 +922,9 @@ function minifyValue (declaration) {
|
|
|
1039
922
|
|
|
1040
923
|
// Property-specific optimizations
|
|
1041
924
|
val = applyPropertyOptimizations(val, declaration.property);
|
|
925
|
+
|
|
926
|
+
// Minify relative color syntax (identity resolution and whitespace collapsing)
|
|
927
|
+
val = minifyRelativeColorSyntax(val);
|
|
1042
928
|
}
|
|
1043
929
|
|
|
1044
930
|
// Gradient optimizations
|