@thejaredwilcurt/csslop 0.0.35 → 0.0.36

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
@@ -290,7 +290,7 @@ Two different cases:
290
290
  1. `git add -A && git commit -m "Updated tests"`
291
291
  1. Then run `npm t` to see if any tests fail
292
292
  1. If they fail, give an AI this prompt:
293
- * **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 correct any linter warnings/errors that occur in the `/src` folder. 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.
293
+ * **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. Any test fails related to idempotency should be handled last. Run `npm run lint` and correct any linter warnings/errors that occur in the `/src` folder. 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.
294
294
  1. Verify only code in the `src` folder was modified
295
295
  1. Verify `npm t` passes with a 100% score
296
296
  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.35",
5
+ "version": "0.0.36",
6
6
  "description": "Experimental CSS minification",
7
7
  "scripts": {
8
8
  "prestart": "node ./scripts/prestart.js",
@@ -36,7 +36,7 @@
36
36
  "eslint-config-tjw-import-x": "^1.0.1",
37
37
  "eslint-config-tjw-jsdoc": "^2.0.1",
38
38
  "eslint-plugin-import-x": "^4.17.0",
39
- "eslint-plugin-jsdoc": "^64.3.5",
39
+ "eslint-plugin-jsdoc": "^64.3.6",
40
40
  "fflate": "^0.8.3",
41
41
  "globals": "^17.12.0",
42
42
  "pretty-ms": "^9.3.1",
@@ -199,93 +199,6 @@ function getOverridesOf (shorthandName) {
199
199
  return OVERRIDES_BY_SHORTHAND.get(shorthandName) || NO_PROPERTIES;
200
200
  }
201
201
 
202
- /**
203
- * The words a longhand adds to its shorthand's name to say which side, axis,
204
- * corner, or alignment dimension of the box that longhand applies to, such as
205
- * the `top` of `padding-top` or the `row` of `row-gap`.
206
- *
207
- * @type {Set<string>}
208
- */
209
- const BOX_PART_WORDS = new Set([
210
- 'align',
211
- 'block',
212
- 'bottom',
213
- 'column',
214
- 'end',
215
- 'inline',
216
- 'justify',
217
- 'left',
218
- 'right',
219
- 'row',
220
- 'start',
221
- 'top',
222
- 'x',
223
- 'y'
224
- ]);
225
-
226
- /**
227
- * Removes each of the shorthand's own words from a longhand's words, leaving
228
- * only the words the longhand adds to name the part of the box it covers. Each
229
- * shared word is removed once, so the `border` and the `radius` of
230
- * `border-radius` leave `top` and `left` behind in `border-top-left-radius`.
231
- *
232
- * @param {Array} longhandWords The hyphen-separated words of the longhand's name.
233
- * @param {Array} shorthandWords The hyphen-separated words of the shorthand's name.
234
- * @return {Array} The words the longhand adds on top of the shorthand's.
235
- */
236
- function subtractSharedWords (longhandWords, shorthandWords) {
237
- const remainingWords = [...longhandWords];
238
- for (const shorthandWord of shorthandWords) {
239
- const wordIndex = remainingWords.indexOf(shorthandWord);
240
- if (wordIndex !== -1) {
241
- remainingWords.splice(wordIndex, 1);
242
- }
243
- }
244
- return remainingWords;
245
- }
246
-
247
- /**
248
- * Whether each shorthand takes a positional list of components, computed on
249
- * first use, since the shorthand tables never change.
250
- *
251
- * @type {Map<string, boolean>}
252
- */
253
- const positionalComponentsByShorthand = new Map();
254
-
255
- /**
256
- * Reports whether a shorthand's value is a positional list of same-typed
257
- * components rather than an unordered set of components that its grammar tells
258
- * apart by type. A shorthand is positional when its longhands are the very same
259
- * property repeated for each part of the box, as `padding` repeats a length for
260
- * each side and `gap` repeats one for each axis. Nothing but the order the
261
- * components are written in says which part of the box each one lands on, so
262
- * the whitespace between them delimits the list. A shorthand such as `border`
263
- * or `font`, whose longhands each hold a different kind of value, is not
264
- * positional: its grammar reads each component by type, in any order.
265
- *
266
- * @param {string} shorthandName The CSS property name to test.
267
- * @return {boolean} Whether the shorthand's components are positional.
268
- */
269
- function hasPositionalComponents (shorthandName) {
270
- const cachedAnswer = positionalComponentsByShorthand.get(shorthandName);
271
- if (cachedAnswer !== undefined) {
272
- return cachedAnswer;
273
- }
274
- const longhands = shorthandMap[shorthandName];
275
- let isPositional = false;
276
- if (Array.isArray(longhands)) {
277
- const shorthandWords = shorthandName.split('-');
278
- isPositional = longhands.every((longhand) => {
279
- const addedWords = subtractSharedWords(longhand.split('-'), shorthandWords);
280
- return Boolean(addedWords.length) && addedWords.every((word) => {
281
- return BOX_PART_WORDS.has(word);
282
- });
283
- });
284
- }
285
- positionalComponentsByShorthand.set(shorthandName, isPositional);
286
- return isPositional;
287
- }
288
-
289
202
  /**
290
203
  * The leaf longhands each property ultimately sets, computed on first use. The
291
204
  * shorthand tables never change, so a property always expands the same way.
@@ -329,7 +242,6 @@ export {
329
242
  expandToLeafProperties,
330
243
  getLonghandsOf,
331
244
  getOverridesOf,
332
- hasPositionalComponents,
333
245
  shorthandMap,
334
246
  shorthandOverrideMap,
335
247
  UNIFORM_VALUE_SHORTHANDS
@@ -68,6 +68,15 @@ function expandPureNestedRules (rules) {
68
68
  canExpand = false;
69
69
  break;
70
70
  }
71
+ // A child written as a comma-separated selector list, such as `.b,.c`,
72
+ // repeats the parent in front of every item when expanded (`.a .b,.a .c`),
73
+ // while the nested form states the parent once. Expanding is always
74
+ // longer, and leaving the rule nested keeps one pass from expanding what
75
+ // the next pass would then keep, so the output stays idempotent.
76
+ if (nestedRule.selectors.length > 1) {
77
+ canExpand = false;
78
+ break;
79
+ }
71
80
  const combinedSelectors = [];
72
81
  for (const parentSelector of rule.selectors) {
73
82
  for (const childSelector of nestedRule.selectors) {
@@ -3,7 +3,6 @@
3
3
  */
4
4
 
5
5
  import { isUnicodeCharset } from '../context.js';
6
- import { hasPositionalComponents } from '../declarations/config.js';
7
6
  import { resolveUnicodeEscape } from '../utilities.js';
8
7
 
9
8
  import { evaluateColorMix } from './color-mix.js';
@@ -261,8 +260,12 @@ function restoreSpaceBeforeMathOperators (value) {
261
260
  function formatUrlPath (path) {
262
261
  // Parentheses and quote characters are invalid inside an unquoted url() token
263
262
  const hasQuoteForcingCharacters = /[()"']/.test(path);
264
- // Count spaces so escaping them can be compared against keeping the quotes
265
- const spaceCount = (path.match(/ /g) || []).length;
263
+ // An escaped space is already the shortest representation of a space, so it
264
+ // is counted apart from unescaped ones to keep from being escaped twice
265
+ const escapedSpaceCount = (path.match(/\\ /g) || []).length;
266
+ // A space only needs escaping when no escaping backslash precedes it
267
+ const unescapedSpaceCount = (path.match(/(?<!\\) /g) || []).length;
268
+ const spaceCount = escapedSpaceCount + unescapedSpaceCount;
266
269
 
267
270
  if (hasQuoteForcingCharacters || spaceCount >= 2) {
268
271
  // Escape any embedded double quotes so the double-quoted wrapper stays valid
@@ -270,8 +273,12 @@ function formatUrlPath (path) {
270
273
  }
271
274
 
272
275
  if (spaceCount === 1) {
273
- // A lone space is one byte shorter to escape than to wrap the value in quotes
274
- return path.replace(/ /g, '\\ ');
276
+ // A lone space is one byte shorter to escape than to wrap the value in
277
+ // quotes, and one that arrived already escaped stays exactly as it is
278
+ if (!unescapedSpaceCount) {
279
+ return path;
280
+ }
281
+ return path.replace(/(?<!\\) /g, '\\ ');
275
282
  }
276
283
 
277
284
  return path;
@@ -976,12 +983,11 @@ function reorderBorderWidthBeforeStyle (value) {
976
983
  * Applies property-specific optimizations to a CSS value (transition, flex, font,
977
984
  * background, display, scale, border-radius, shorthand collapsing, etc.).
978
985
  *
979
- * @param {string} val The CSS value string after generic minification.
980
- * @param {string} property The CSS property name.
981
- * @param {boolean} allowsSeparatorElision Whether redundant separator whitespace may be removed.
982
- * @return {string} The value with property-specific optimizations applied.
986
+ * @param {string} val The CSS value string after generic minification.
987
+ * @param {string} property The CSS property name.
988
+ * @return {string} The value with property-specific optimizations applied.
983
989
  */
984
- function applyPropertyOptimizations (val, property, allowsSeparatorElision) {
990
+ function applyPropertyOptimizations (val, property) {
985
991
  if (property === 'font-weight' && isUnicodeCharset()) {
986
992
  // Replace font-weight keyword "bold" with its numeric equivalent
987
993
  val = val.replace(/\bbold\b/gi, '700');
@@ -1023,8 +1029,11 @@ function applyPropertyOptimizations (val, property, allowsSeparatorElision) {
1023
1029
  val = val.replace(/\s+0px/g, ' ');
1024
1030
  // Remove leading zero-pixel value
1025
1031
  val = val.replace(/^0px\s*/, '');
1026
- // Remove trailing zero
1027
- val = val.replace(/\s+0$/, '');
1032
+ // Remove an explicit zero basis: a trailing zero is only a basis when grow
1033
+ // and shrink precede it, and the shorthand already reads an unwritten basis
1034
+ // as 0. In `0 0` the trailing zero is the shrink, which the shorthand
1035
+ // defaults to 1 instead, so dropping it would size the element differently.
1036
+ val = val.replace(/^(\S+\s+\S+)\s+0$/, '$1');
1028
1037
  // Remove standalone zero-pixel value
1029
1038
  val = val.replace(/^0px$/, '');
1030
1039
  val = val.trim();
@@ -1141,7 +1150,7 @@ function applyPropertyOptimizations (val, property, allowsSeparatorElision) {
1141
1150
 
1142
1151
  // Shorten all color tokens (second pass after property-specific color evaluations)
1143
1152
  val = replaceOutsideStringsAndUrls(val, (segment) => {
1144
- return shortenColorValues(segment, allowsSeparatorElision);
1153
+ return shortenColorValues(segment);
1145
1154
  });
1146
1155
 
1147
1156
  if (!PUNCTUATED_COMPONENT_PROPERTIES.has(property)) {
@@ -1243,21 +1252,6 @@ function applyPropertyOptimizations (val, property, allowsSeparatorElision) {
1243
1252
  return val;
1244
1253
  }
1245
1254
 
1246
- /**
1247
- * Reports whether a declaration holds a shorthand that was assembled by joining
1248
- * already-minified longhand values with a separator, and whose grammar reads
1249
- * those components by their position in the list. Nothing but that separator
1250
- * says where one component ends and the next begins, so it is kept even where
1251
- * the two components happen to be tokens that would survive being written
1252
- * together.
1253
- *
1254
- * @param {object} declaration The CSS declaration object with property and value fields.
1255
- * @return {boolean} Whether the assembled components keep their separators.
1256
- */
1257
- function keepsAssembledComponentSeparators (declaration) {
1258
- return Boolean(declaration.isAssembledShorthand) && hasPositionalComponents(declaration.property);
1259
- }
1260
-
1261
1255
  /**
1262
1256
  * Minifies a CSS declaration's value by applying color conversion, math simplification, shorthand compression, gradient optimization, and other property-specific optimizations.
1263
1257
  *
@@ -1275,7 +1269,6 @@ function computeMinifiedValue (declaration) {
1275
1269
  return 'none';
1276
1270
  }
1277
1271
  let val = declaration.value;
1278
- const allowsSeparatorElision = !keepsAssembledComponentSeparators(declaration);
1279
1272
 
1280
1273
  if (typeof val === 'string') {
1281
1274
  val = val.trim();
@@ -1323,19 +1316,16 @@ function computeMinifiedValue (declaration) {
1323
1316
  // Convert color functions to hex equivalents
1324
1317
  val = convertColorsToHex(val);
1325
1318
 
1326
- // Shorten all color tokens (hex and named) to their shortest representation.
1327
- // A value that keeps the whitespace between its components saves nothing by
1328
- // switching to a spelling of the same length, so its colors keep the
1329
- // spelling they were written with.
1319
+ // Shorten all color tokens (hex and named) to their shortest representation
1330
1320
  val = replaceOutsideStringsAndUrls(val, (segment) => {
1331
- return shortenColorValues(segment, allowsSeparatorElision);
1321
+ return shortenColorValues(segment);
1332
1322
  });
1333
1323
 
1334
1324
  // Collapse light-dark() when both normalized branches are identical
1335
1325
  val = simplifyEquivalentLightDarkFunctions(val);
1336
1326
 
1337
1327
  // Property-specific optimizations
1338
- val = applyPropertyOptimizations(val, declaration.property, allowsSeparatorElision);
1328
+ val = applyPropertyOptimizations(val, declaration.property);
1339
1329
 
1340
1330
  // Minify relative color syntax (identity resolution and whitespace collapsing)
1341
1331
  val = minifyRelativeColorSyntax(val);
@@ -1356,7 +1346,6 @@ function computeMinifiedValue (declaration) {
1356
1346
  // so the ones that turned out to be redundant are only dropped at the end.
1357
1347
  const elidesRedundantSeparators = (
1358
1348
  typeof val === 'string' &&
1359
- allowsSeparatorElision &&
1360
1349
  !isCustomProperty(declaration.property)
1361
1350
  );
1362
1351
  if (elidesRedundantSeparators) {