@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 +1 -1
- package/package.json +2 -2
- package/src/value/colors.js +3 -1
- package/src/value/gradients.js +121 -59
- package/src/value/minify.js +18 -1
- package/src/value/named-colors.js +1 -0
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.
|
|
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.
|
|
61
|
+
"version": "26.5.0"
|
|
62
62
|
},
|
|
63
63
|
"packageManager": {
|
|
64
64
|
"name": "npm",
|
package/src/value/colors.js
CHANGED
|
@@ -339,7 +339,9 @@ function parseColor (colorString) {
|
|
|
339
339
|
|
|
340
340
|
// Named color
|
|
341
341
|
if (namedColors[normalized]) {
|
|
342
|
-
|
|
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
|
package/src/value/gradients.js
CHANGED
|
@@ -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
|
|
226
|
-
return group
|
|
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
|
-
|
|
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
|
/**
|
package/src/value/minify.js
CHANGED
|
@@ -89,7 +89,9 @@ function shortenColorValues (segment) {
|
|
|
89
89
|
}
|
|
90
90
|
const rgb = namedColors[match.toLowerCase()];
|
|
91
91
|
if (rgb) {
|
|
92
|
-
|
|
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();
|