@thejaredwilcurt/csslop 0.0.14 → 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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@thejaredwilcurt/csslop",
3
3
  "main": "index.js",
4
4
  "type": "module",
5
- "version": "0.0.14",
5
+ "version": "0.0.16",
6
6
  "description": "Experimental CSS minification",
7
7
  "scripts": {
8
8
  "prestart": "node ./scripts/prestart.js",
@@ -269,6 +269,9 @@ function normalizeBoundaryPositionTokens (mergedStops) {
269
269
  if (isLastStop && positionTokens[positionTokens.length - 1] === '100%') {
270
270
  positionTokens.pop();
271
271
  }
272
+ if (isLastStop && positionTokens.length === 1 && positionTokens[0] === '100%') {
273
+ positionTokens.pop();
274
+ }
272
275
  if (positionTokens.length > 0 && positionTokens[0] === previousEndPosition) {
273
276
  positionTokens[0] = '0';
274
277
  }
@@ -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
  }