@thejaredwilcurt/csslop 0.0.15 → 0.0.16
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/package.json +1 -1
- package/src/value/minify.js +235 -0
package/package.json
CHANGED
package/src/value/minify.js
CHANGED
|
@@ -172,6 +172,238 @@ function replaceOutsideStringsAndUrls (value, replacer) {
|
|
|
172
172
|
return result;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Determines whether a character can appear inside a CSS identifier.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} character A single character.
|
|
179
|
+
* @return {boolean} True when the character is identifier-like.
|
|
180
|
+
*/
|
|
181
|
+
function isIdentifierCharacter (character) {
|
|
182
|
+
if (!character) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const codePoint = character.charCodeAt(0);
|
|
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
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Detects a `light-dark(` function call at the provided position.
|
|
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;
|
|
202
|
+
}
|
|
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
|
+
|
|
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++;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return -1;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Splits a CSS function argument list at top-level commas while respecting
|
|
259
|
+
* nested parentheses and quoted strings.
|
|
260
|
+
*
|
|
261
|
+
* @param {string} argumentString The raw content between function parentheses.
|
|
262
|
+
* @return {Array} The top-level argument strings.
|
|
263
|
+
*/
|
|
264
|
+
function splitTopLevelFunctionArguments (argumentString) {
|
|
265
|
+
const argumentsList = [];
|
|
266
|
+
let currentArgument = '';
|
|
267
|
+
let depth = 0;
|
|
268
|
+
let index = 0;
|
|
269
|
+
let activeQuote = '';
|
|
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;
|
|
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
|
+
}
|
|
321
|
+
|
|
322
|
+
argumentsList.push(currentArgument.trim());
|
|
323
|
+
return argumentsList;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Simplifies `light-dark(a,b)` to `a` when both top-level arguments are identical
|
|
328
|
+
* after prior minification has normalized them.
|
|
329
|
+
*
|
|
330
|
+
* @param {string} value The CSS value to simplify.
|
|
331
|
+
* @return {string} The value with redundant light-dark functions removed.
|
|
332
|
+
*/
|
|
333
|
+
function simplifyEquivalentLightDarkFunctions (value) {
|
|
334
|
+
let result = '';
|
|
335
|
+
let index = 0;
|
|
336
|
+
|
|
337
|
+
const consumeQuoted = (start) => {
|
|
338
|
+
const quote = value[start];
|
|
339
|
+
let end = start + 1;
|
|
340
|
+
while (end < value.length) {
|
|
341
|
+
if (value[end] === '\\') {
|
|
342
|
+
end += 2;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (value[end] === quote) {
|
|
346
|
+
end++;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
end++;
|
|
350
|
+
}
|
|
351
|
+
return end;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const startsUrl = (start) => {
|
|
355
|
+
return value.slice(start, start + 4).toLowerCase() === 'url(';
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
while (index < value.length) {
|
|
359
|
+
if (value[index] === '"' || value[index] === '\'') {
|
|
360
|
+
const end = consumeQuoted(index);
|
|
361
|
+
result += value.slice(index, end);
|
|
362
|
+
index = end;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (startsUrl(index)) {
|
|
367
|
+
const end = findMatchingParenthesis(value, index + 3);
|
|
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);
|
|
380
|
+
if (closingParenIndex === -1) {
|
|
381
|
+
result += value.slice(index);
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const argumentString = value.slice(openParenIndex + 1, closingParenIndex);
|
|
386
|
+
const argumentsList = splitTopLevelFunctionArguments(argumentString);
|
|
387
|
+
if (argumentsList.length === 2) {
|
|
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
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
result += value[index];
|
|
401
|
+
index++;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
|
|
175
407
|
/**
|
|
176
408
|
* Normalizes whitespace, quotes, and unicode escapes in a CSS value string.
|
|
177
409
|
*
|
|
@@ -802,6 +1034,9 @@ function minifyValue (declaration) {
|
|
|
802
1034
|
// Shorten all color tokens (hex and named) to their shortest representation
|
|
803
1035
|
val = replaceOutsideStringsAndUrls(val, shortenColorValues);
|
|
804
1036
|
|
|
1037
|
+
// Collapse light-dark() when both normalized branches are identical
|
|
1038
|
+
val = simplifyEquivalentLightDarkFunctions(val);
|
|
1039
|
+
|
|
805
1040
|
// Property-specific optimizations
|
|
806
1041
|
val = applyPropertyOptimizations(val, declaration.property);
|
|
807
1042
|
}
|