@thejaredwilcurt/csslop 0.0.12 → 0.0.14

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 CHANGED
@@ -278,7 +278,7 @@ Two different cases:
278
278
  1. `git add -A && git commit -m "Updated tests"`
279
279
  1. Then run `npm t` to see if any tests fail
280
280
  1. If they fail, give an AI this prompt:
281
- * **PROMPT:** Run `npm t` and fix all failing tests by modifying files in `src`. Do not use naive solutions, hacks, or hard coded values. Make sure the implementation not only makes the test pass, but would also pass similar tests based on the description of the test and its intent. Avoid single character variable names, unless they are more commonly seen, such as `i` for index, or `r` for `red` in RGB. Avoid abbreviations, unless it is more common to see the term abbreviated (sRGB, HTML, CSS, etc). Group related logic into well named functions. Ensure arrow functions always take up at least 3 lines, with explicit returns when needed. Always comment regex if used. Run `npm run lint` and ensure the linter passes when done. DO NOT, under any circumstance, run `npm run real` (it will kill actual humans)!
281
+ * **PROMPT:** Run `npm t` and fix all failing tests by modifying files in `src`. Do not use naive solutions, hacks, or hard coded values. Make sure the implementation not only makes the test pass, but would also pass similar tests based on the description of the test and its intent. Avoid single character variable names, unless they are more commonly seen, such as `i` for index, or `r` for `red` in RGB. Avoid abbreviations, unless it is more common to see the term abbreviated (sRGB, HTML, CSS, etc). Group related logic into well named functions. Ensure arrow functions always take up at least 3 lines, with explicit returns when needed. Always comment regex if used. Run `npm run lint` and ensure the linter passes when done. DO NOT, under any circumstance, run `npm run real` (it will kill actual humans)! When all done, run the `beep` command to alert me you finished.
282
282
  1. Verify only code in the `src` folder was modified
283
283
  1. Verify `npm t` passes with a 100% score
284
284
  1. Run `npm run lint`, if anything fails, have the AI fix it.
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.12",
5
+ "version": "0.0.14",
6
6
  "description": "Experimental CSS minification",
7
7
  "scripts": {
8
8
  "prestart": "node ./scripts/prestart.js",
@@ -58,7 +58,7 @@
58
58
  "devEngines": {
59
59
  "runtime": {
60
60
  "name": "node",
61
- "version": "26.4.0"
61
+ "version": "26.5.0"
62
62
  },
63
63
  "packageManager": {
64
64
  "name": "npm",
@@ -339,7 +339,9 @@ function parseColor (colorString) {
339
339
 
340
340
  // Named color
341
341
  if (namedColors[normalized]) {
342
- return [...namedColors[normalized], 1];
342
+ // transparent has alpha=0, all other named colors have alpha=1
343
+ const alpha = normalized === 'transparent' ? 0 : 1;
344
+ return [...namedColors[normalized], alpha];
343
345
  }
344
346
 
345
347
  // Hex
@@ -139,6 +139,25 @@ function normalizeStopColorToken (colorToken) {
139
139
  */
140
140
  function parseColorStop (stop) {
141
141
  const trimmed = stop.trim();
142
+
143
+ // Check if the entire stop is a 4-digit hex color (with alpha) like #0000
144
+ // These should not be split as they are complete colors
145
+ if (/^#[0-9a-fA-F]{4}\b$/.test(trimmed)) {
146
+ return {
147
+ color: trimmed,
148
+ position: null
149
+ };
150
+ }
151
+
152
+ // Check if the entire stop is an 8-digit hex color (with alpha) like #00000000
153
+ // These should not be split as they are complete colors
154
+ if (/^#[0-9a-fA-F]{8}\b$/.test(trimmed)) {
155
+ return {
156
+ color: trimmed,
157
+ position: null
158
+ };
159
+ }
160
+
142
161
  // Match a trailing position: one or two values that are numbers with optional units
143
162
  // like "50%", "10px", or "0". Captures the last position token(s) after the color.
144
163
  const positionMatch = trimmed.match(/^(.+?)\s+((?:\d+(?:\.\d+)?(?:%|[a-z]+)?\s*){1,2})$/i);
@@ -162,6 +181,105 @@ function parseColorStop (stop) {
162
181
  };
163
182
  }
164
183
 
184
+ /**
185
+ * Splits a stop position into individual start and end tokens.
186
+ *
187
+ * @param {string|null} position The raw stop position text.
188
+ * @return {Array} The normalized position tokens.
189
+ */
190
+ function splitStopPositionTokens (position) {
191
+ if (position === null) {
192
+ return [];
193
+ }
194
+
195
+ return position.split(/\s+/).filter(Boolean);
196
+ }
197
+
198
+ /**
199
+ * Serializes a color stop from its color and normalized position tokens.
200
+ *
201
+ * @param {string} color The normalized stop color.
202
+ * @param {Array} positionTokens The normalized stop positions.
203
+ * @return {string} The serialized color stop.
204
+ */
205
+ function serializeColorStop (color, positionTokens) {
206
+ if (positionTokens.length === 0) {
207
+ return color;
208
+ }
209
+
210
+ return color + ' ' + positionTokens.join(' ');
211
+ }
212
+
213
+ /**
214
+ * Merges consecutive stops with the same color into a single logical stop.
215
+ *
216
+ * @param {Array} group The adjacent parsed stops for one color.
217
+ * @return {object} The merged stop data.
218
+ */
219
+ function mergeIdenticalStopGroup (group) {
220
+ const firstStop = group[0];
221
+ const lastStop = group[group.length - 1];
222
+ const firstPositionTokens = splitStopPositionTokens(firstStop.position);
223
+ const lastPositionTokens = splitStopPositionTokens(lastStop.position);
224
+
225
+ let positionTokens;
226
+ if (group.length === 1) {
227
+ positionTokens = firstPositionTokens;
228
+ } else {
229
+ const mergedTokens = [];
230
+ const startPosition = firstPositionTokens[0] || null;
231
+ const endPosition = lastPositionTokens[lastPositionTokens.length - 1] || null;
232
+
233
+ if (startPosition !== null) {
234
+ mergedTokens.push(startPosition);
235
+ }
236
+ if (endPosition !== null && endPosition !== startPosition) {
237
+ mergedTokens.push(endPosition);
238
+ }
239
+
240
+ positionTokens = mergedTokens;
241
+ }
242
+
243
+ return {
244
+ color: firstStop.color,
245
+ effectiveEndPosition: lastPositionTokens[lastPositionTokens.length - 1] || null,
246
+ positionTokens
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Removes implied edge positions and rewrites repeated starts as `0`.
252
+ *
253
+ * @param {Array} mergedStops The merged stops to normalize.
254
+ * @return {Array} The serialized normalized stops.
255
+ */
256
+ function normalizeBoundaryPositionTokens (mergedStops) {
257
+ const result = [];
258
+ let previousEndPosition = null;
259
+
260
+ for (let stopIndex = 0; stopIndex < mergedStops.length; stopIndex++) {
261
+ const stop = mergedStops[stopIndex];
262
+ const isFirstStop = stopIndex === 0;
263
+ const isLastStop = stopIndex === mergedStops.length - 1;
264
+ const positionTokens = [...stop.positionTokens];
265
+
266
+ if (isFirstStop && positionTokens[0] === '0%') {
267
+ positionTokens.shift();
268
+ }
269
+ if (isLastStop && positionTokens[positionTokens.length - 1] === '100%') {
270
+ positionTokens.pop();
271
+ }
272
+ if (positionTokens.length > 0 && positionTokens[0] === previousEndPosition) {
273
+ positionTokens[0] = '0';
274
+ }
275
+
276
+ previousEndPosition = stop.effectiveEndPosition;
277
+ result.push(serializeColorStop(stop.color, positionTokens));
278
+ }
279
+
280
+ return result;
281
+ }
282
+
165
283
  /**
166
284
  * Serializes a parsed gradient stop back into normalized CSS text, ensuring a
167
285
  * separating space is preserved when a stop position is present.
@@ -222,67 +340,11 @@ function combineAdjacentIdenticalStops (args) {
222
340
  }
223
341
 
224
342
  const groups = groupConsecutiveIdenticalStops(stops);
225
- const hasMergeableGroup = groups.some((group) => {
226
- return group.length > 1;
343
+ const mergedStops = groups.map((group) => {
344
+ return mergeIdenticalStopGroup(group);
227
345
  });
228
- if (!hasMergeableGroup) {
229
- return args;
230
- }
231
-
232
- const result = [];
233
- let previousEndPosition = null;
234
-
235
- for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
236
- const group = groups[groupIndex];
237
- const color = group[0].color;
238
- const isFirstGroup = groupIndex === 0;
239
- const isLastGroup = groupIndex === groups.length - 1;
240
346
 
241
- if (group.length === 1) {
242
- let position = group[0].position;
243
- if (position === '0%' && isFirstGroup) {
244
- position = null;
245
- }
246
- if (position === '100%' && isLastGroup) {
247
- position = null;
248
- }
249
- if (position !== null && position === previousEndPosition) {
250
- position = '0';
251
- }
252
- previousEndPosition = group[0].position;
253
- result.push(position ? color + ' ' + position : color);
254
- continue;
255
- }
256
-
257
- const firstPosition = group[0].position;
258
- const lastPosition = group[group.length - 1].position;
259
-
260
- let startPart = firstPosition;
261
- let endPart = lastPosition;
262
-
263
- if (startPart === '0%' && isFirstGroup) {
264
- startPart = null;
265
- }
266
- if (endPart === '100%' && isLastGroup) {
267
- endPart = null;
268
- }
269
- if (startPart !== null && startPart === previousEndPosition) {
270
- startPart = '0';
271
- }
272
-
273
- previousEndPosition = lastPosition;
274
-
275
- const positionParts = [startPart, endPart].filter((part) => {
276
- return part !== null;
277
- });
278
- if (positionParts.length > 0) {
279
- result.push(color + ' ' + positionParts.join(' '));
280
- } else {
281
- result.push(color);
282
- }
283
- }
284
-
285
- return result;
347
+ return normalizeBoundaryPositionTokens(mergedStops);
286
348
  }
287
349
 
288
350
  /**
@@ -89,7 +89,9 @@ function shortenColorValues (segment) {
89
89
  }
90
90
  const rgb = namedColors[match.toLowerCase()];
91
91
  if (rgb) {
92
- channels = [rgb[0], rgb[1], rgb[2], 1];
92
+ // transparent has alpha=0, all other named colors have alpha=1
93
+ const alpha = match.toLowerCase() === 'transparent' ? 0 : 1;
94
+ channels = [rgb[0], rgb[1], rgb[2], alpha];
93
95
  }
94
96
  }
95
97
  if (!channels) {
@@ -521,6 +523,10 @@ function applyPropertyOptimizations (val, property) {
521
523
  if (['min-width', 'min-height'].includes(property)) {
522
524
  val = 'auto';
523
525
  }
526
+ // background-color: initial should become #0000 (transparent)
527
+ if (property === 'background-color') {
528
+ val = '#0000';
529
+ }
524
530
  }
525
531
 
526
532
  if (property === 'background' && val === 'none') {
@@ -612,6 +618,9 @@ function applyPropertyOptimizations (val, property) {
612
618
 
613
619
  // Remove space before hex colors (second pass after color evaluations)
614
620
  val = replaceOutsideStringsAndUrls(val, (segment) => {
621
+ // Preserve space after border style keywords (solid, dashed, etc.) before hex colors
622
+ segment = segment.replace(/\b(solid|dashed|dotted|double|groove|ridge|inset|outset|hidden|none)\s+#([0-9a-fA-F]{3,8})\b/gi, '$1 #$2');
623
+ // Then remove other spaces before hex colors
615
624
  return segment.replace(/\s+#([0-9a-fA-F]{3,8})\b/gi, '#$1');
616
625
  });
617
626
  if (property !== 'transform' && property !== 'background' && property !== 'src') {
@@ -660,6 +669,9 @@ function applyPropertyOptimizations (val, property) {
660
669
  if (property === 'border') {
661
670
  // Remove default "medium" border-width keyword
662
671
  val = val.replace(/\bmedium\s+/g, '');
672
+ // Restore missing space between border-style and a 4-digit hex color (with alpha) when they are adjacent
673
+ // This is needed because solid#0000 could be parsed as solid followed by #000 followed by position 0
674
+ val = val.replace(/\b(solid|dashed|dotted|double|groove|ridge|inset|outset|hidden|none)#([0-9a-fA-F]{4})\b/gi, '$1 #$2');
663
675
  }
664
676
 
665
677
  if (property === 'outline') {
@@ -771,7 +783,12 @@ function minifyValue (declaration) {
771
783
 
772
784
  // Remove space before hex colors
773
785
  val = replaceOutsideStringsAndUrls(val, (segment) => {
786
+ // Preserve space after border style keywords by using a temporary placeholder
787
+ segment = segment.replace(/\b(solid|dashed|dotted|double|groove|ridge|inset|outset|hidden|none)\s+#([0-9a-fA-F]{3,8})\b/gi, '$1__BORDER_SPACE__#$2');
788
+ // Remove other spaces before hex colors
774
789
  segment = segment.replace(/\s+#([0-9a-fA-F]{3,8})\b/gi, '#$1');
790
+ // Restore the preserved space
791
+ segment = segment.replace(/__BORDER_SPACE__#/g, ' #');
775
792
  // Lowercase hex color tokens for consistency and shorter output
776
793
  segment = segment.replace(/#([0-9a-fA-F]{3,8})\b/gi, (m) => {
777
794
  return m.toLowerCase();
@@ -7,6 +7,7 @@
7
7
 
8
8
  const namedColors = {
9
9
  black: [0, 0, 0],
10
+ transparent: [0, 0, 0],
10
11
  silver: [192, 192, 192],
11
12
  gray: [128, 128, 128],
12
13
  grey: [128, 128, 128],