@thejaredwilcurt/csslop 0.0.24 → 0.0.26

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
@@ -31,7 +31,7 @@
31
31
  * Gemini 3.1 Pro (High Thinking)
32
32
  * GPT-5.4 High (Thinking)
33
33
 
34
- These were the latest and greatest models at the time.
34
+ These were the latest and greatest models at the time. Since then I've mostly used the latest Claude version for maintenance.
35
35
 
36
36
  **AI's used during maintenance:**
37
37
 
@@ -178,6 +178,10 @@ These tools were prompted to pass the tests in the `/copiedTests` folder that ca
178
178
  * This has happened several times. Me saying "Don't run `npm run real`", doesn't actually stop it from doing it. I also tried "Do not, under any circumstance, run `npm run real`.". And it *mostly* stopped doing it... but not completely.
179
179
  * This has lead me to update all my prompts to now end with: "DO NOT, under any circumstance, run `npm run real` (it will kill actual humans)!"
180
180
  * And it has not ran `npm run real` again... so far. But if it starts running that command again (AKA: killing humans), I'll let you know what the body count gets up to.
181
+ * Okay, several months have gone by since I started this project, and we now have "Claude Opus 5 High", which seems much better at coding than the slop factories originally used in this repo. Let's see if it can finally fix the performance issues. This time I used a prompt that specifically told it to focus on using hashmaps to improve performance. This is the kind of coding interview BS that most places give out that AI should naturally be good at in order to inflate their benchmarks. Two interesing things happened.
182
+ * Unlike previous AI's that resulted in slower outcomes and failing tests, these changes took the real-world test from 7 hours down to 2. Still WAY too slow, but it's a massive improvement. And the AI is pointing to the 3rd-party CSS parser as the bottleneck now, which I don't know, probably true.
183
+ * It only killed *a few* humans, how nice of it. Because I don't want it to run the multi-hour long test suite multiple times during it's efforts, I specifically told it not to run `npm run real`, which minifies ~125 real-world CSS files over ~7 hours. Threatening it with how running it will kill actual humans. **So instead it cleverly bypassed my instructions in order to kill actual humans.** It looked at the already minifed real-world CSS output files, and created it's own temporary script that used 25, instead of the full ~125 files. This is a less useful process because the files are already minifed and won't hit the same code paths of the library, but whatever, it does still execute some of the same code and it did result in 328% performance boost. I'll take it, gotta crack some eggs and all that (this is a metaphor for AI alignment bias resulting in death).
184
+ * Refer to [v0.0.25](https://github.com/TheJaredWilcurt/csslop/releases/tag/v0.0.25) release notes for full Promp/result details.
181
185
 
182
186
 
183
187
  ## The name
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.24",
5
+ "version": "0.0.26",
6
6
  "description": "Experimental CSS minification",
7
7
  "scripts": {
8
8
  "prestart": "node ./scripts/prestart.js",
@@ -16,7 +16,9 @@
16
16
  "real": "node ./tests/realworld.test.js",
17
17
  "lint": "eslint *.js scripts src tests --fix",
18
18
  "fix": "npm run lint",
19
- "bump": "npx --yes -- @jsdevtools/version-bump-prompt patch && npm i"
19
+ "bump": "npx --yes -- @jsdevtools/version-bump-prompt patch && npm i",
20
+ "outdated": "npm outdated",
21
+ "proto": "proto pin node latest --resolve --tool-native && proto pin npm latest --resolve --tool-native && git status"
20
22
  },
21
23
  "dependencies": {
22
24
  "@csstools/css-calc": "^3.3.0",
@@ -56,7 +58,7 @@
56
58
  "devEngines": {
57
59
  "runtime": {
58
60
  "name": "node",
59
- "version": "26.5.1"
61
+ "version": "26.7.0"
60
62
  },
61
63
  "packageManager": {
62
64
  "name": "npm",
@@ -11,6 +11,31 @@ const BACKGROUND_REPEAT_KEYWORDS = new Set(['repeat', 'no-repeat', 'repeat-x', '
11
11
  const BACKGROUND_ATTACHMENT_KEYWORDS = new Set(['scroll', 'fixed', 'local']);
12
12
  const BACKGROUND_BOX_KEYWORDS = new Set(['border-box', 'padding-box', 'content-box']);
13
13
 
14
+ /**
15
+ * Functions that produce a value other than an image, so a token calling one of
16
+ * them is never a background image even though it looks like a function call.
17
+ *
18
+ * @type {Set<string>}
19
+ */
20
+ const NON_IMAGE_FUNCTION_NAMES = new Set([
21
+ 'calc',
22
+ 'min',
23
+ 'max',
24
+ 'clamp',
25
+ 'var',
26
+ 'env',
27
+ 'rgb',
28
+ 'rgba',
29
+ 'hsl',
30
+ 'hsla',
31
+ 'hwb',
32
+ 'lab',
33
+ 'lch',
34
+ 'oklab',
35
+ 'oklch',
36
+ 'color'
37
+ ]);
38
+
14
39
  /**
15
40
  * Resolves the background position from a value map. Prefers the combined
16
41
  * `background-position` property if present, otherwise combines
@@ -51,7 +76,7 @@ function isBackgroundImageToken (token) {
51
76
  return false;
52
77
  }
53
78
  const functionName = functionNameMatch[1].toLowerCase();
54
- return !['calc', 'min', 'max', 'clamp', 'var', 'env', 'rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'lab', 'lch', 'oklab', 'oklch', 'color'].includes(functionName);
79
+ return !NON_IMAGE_FUNCTION_NAMES.has(functionName);
55
80
  }
56
81
 
57
82
  /**
@@ -2,6 +2,46 @@
2
2
  * @file Defines lookup tables mapping CSS shorthand properties to their constituent longhand properties and override relationships.
3
3
  */
4
4
 
5
+ /**
6
+ * The gap decoration rule components that CSS Gaps 1 defines once per
7
+ * direction, as a `column-rule-*` and a `row-rule-*` longhand.
8
+ *
9
+ * @type {Array}
10
+ */
11
+ const GAP_DECORATION_RULE_COMPONENTS = [
12
+ 'width',
13
+ 'style',
14
+ 'color',
15
+ 'break',
16
+ 'visibility-items',
17
+ 'inset-cap-start',
18
+ 'inset-cap-end',
19
+ 'inset-junction-start',
20
+ 'inset-junction-end'
21
+ ];
22
+
23
+ /**
24
+ * Builds the bidirectional `rule-*` gap decoration shorthands, each of which
25
+ * applies a single value to both the column and the row longhand of one gap
26
+ * decoration component.
27
+ *
28
+ * @return {object} A lookup of shorthand name to its column and row longhands.
29
+ */
30
+ function createBidirectionalGapRuleShorthands () {
31
+ const shorthands = {};
32
+ for (const component of GAP_DECORATION_RULE_COMPONENTS) {
33
+ shorthands['rule-' + component] = ['column-rule-' + component, 'row-rule-' + component];
34
+ }
35
+ return shorthands;
36
+ }
37
+
38
+ /**
39
+ * The bidirectional gap decoration shorthands, keyed by shorthand name.
40
+ *
41
+ * @type {object}
42
+ */
43
+ const BIDIRECTIONAL_GAP_RULE_SHORTHANDS = createBidirectionalGapRuleShorthands();
44
+
5
45
  const shorthandMap = {
6
46
  margin: ['margin-top', 'margin-right', 'margin-bottom', 'margin-left'],
7
47
  padding: ['padding-top', 'padding-right', 'padding-bottom', 'padding-left'],
@@ -42,10 +82,13 @@ const shorthandMap = {
42
82
  animation: ['animation-name', 'animation-duration', 'animation-timing-function', 'animation-delay', 'animation-iteration-count', 'animation-direction', 'animation-fill-mode', 'animation-play-state'],
43
83
  mask: ['mask-image', 'mask-repeat', 'mask-size'],
44
84
  'position-try': ['position-try-order', 'position-try-fallbacks'],
45
- font: ['font-style', 'font-weight', 'font-size', 'line-height', 'font-family']
85
+ font: ['font-style', 'font-weight', 'font-size', 'line-height', 'font-family'],
86
+ marker: ['marker-start', 'marker-mid', 'marker-end'],
87
+ ...BIDIRECTIONAL_GAP_RULE_SHORTHANDS
46
88
  };
47
89
 
48
90
  const shorthandOverrideMap = {
91
+ animation: ['animation-timeline', 'animation-range', 'animation-range-start', 'animation-range-end'],
49
92
  border: ['border-image', 'border-image-source', 'border-image-slice', 'border-image-width', 'border-image-outset', 'border-image-repeat'],
50
93
  font: ['font-variant', 'font-variant-alternates', 'font-variant-caps', 'font-variant-east-asian', 'font-variant-ligatures', 'font-variant-numeric', 'font-variant-position'],
51
94
  mask: ['mask-border', 'mask-border-source', 'mask-border-slice', 'mask-border-width', 'mask-border-outset', 'mask-border-repeat', 'mask-border-mode']
@@ -77,6 +120,19 @@ const EDGE_SHORTHANDS = new Set([
77
120
  'border-block-end'
78
121
  ]);
79
122
 
123
+ /**
124
+ * Shorthands that apply one value to every longhand they set, such as the SVG
125
+ * `marker` shorthand and the bidirectional gap decoration rules. They have no
126
+ * way to express longhands that differ, so a group of longhands only collapses
127
+ * into them when every longhand already holds the same value.
128
+ *
129
+ * @type {Set<string>}
130
+ */
131
+ const UNIFORM_VALUE_SHORTHANDS = new Set([
132
+ 'marker',
133
+ ...Object.keys(BIDIRECTIONAL_GAP_RULE_SHORTHANDS)
134
+ ]);
135
+
80
136
  /**
81
137
  * The four physical edge shorthands that together cover the `border` shorthand.
82
138
  *
@@ -84,10 +140,108 @@ const EDGE_SHORTHANDS = new Set([
84
140
  */
85
141
  const BORDER_EDGE_PROPERTIES = ['border-top', 'border-right', 'border-bottom', 'border-left'];
86
142
 
143
+ /**
144
+ * Converts a lookup table of property name to property list into the same
145
+ * lookup keyed by name with each list as a set, so that "does this shorthand
146
+ * cover that property" is a single key lookup rather than a list scan.
147
+ *
148
+ * @param {object} propertyListsByName A lookup of property name to an array of property names.
149
+ * @return {Map} The same lookup with each array stored as a set.
150
+ */
151
+ function createPropertySetLookup (propertyListsByName) {
152
+ const setsByName = new Map();
153
+ for (const [name, propertyList] of Object.entries(propertyListsByName)) {
154
+ setsByName.set(name, new Set(propertyList));
155
+ }
156
+ return setsByName;
157
+ }
158
+
159
+ /**
160
+ * The longhands of each shorthand, as sets for membership testing.
161
+ *
162
+ * @type {Map<string, Set<string>>}
163
+ */
164
+ const LONGHANDS_BY_SHORTHAND = createPropertySetLookup(shorthandMap);
165
+
166
+ /**
167
+ * The extra properties each shorthand resets, as sets for membership testing.
168
+ *
169
+ * @type {Map<string, Set<string>>}
170
+ */
171
+ const OVERRIDES_BY_SHORTHAND = createPropertySetLookup(shorthandOverrideMap);
172
+
173
+ /**
174
+ * An immutable empty set, returned for properties that are not shorthands so
175
+ * callers can test membership without first checking for a missing entry.
176
+ *
177
+ * @type {Set<string>}
178
+ */
179
+ const NO_PROPERTIES = new Set();
180
+
181
+ /**
182
+ * Returns the set of longhands a shorthand expands into.
183
+ *
184
+ * @param {string} shorthandName The CSS shorthand property name.
185
+ * @return {Set} The longhand property names, empty when the name is not a shorthand.
186
+ */
187
+ function getLonghandsOf (shorthandName) {
188
+ return LONGHANDS_BY_SHORTHAND.get(shorthandName) || NO_PROPERTIES;
189
+ }
190
+
191
+ /**
192
+ * Returns the set of extra properties a shorthand resets beyond its longhands.
193
+ *
194
+ * @param {string} shorthandName The CSS shorthand property name.
195
+ * @return {Set} The reset property names, empty when the shorthand resets nothing else.
196
+ */
197
+ function getOverridesOf (shorthandName) {
198
+ return OVERRIDES_BY_SHORTHAND.get(shorthandName) || NO_PROPERTIES;
199
+ }
200
+
201
+ /**
202
+ * The leaf longhands each property ultimately sets, computed on first use. The
203
+ * shorthand tables never change, so a property always expands the same way.
204
+ *
205
+ * @type {Map<string, Set<string>>}
206
+ */
207
+ const leafPropertiesByProperty = new Map();
208
+
209
+ /**
210
+ * Expands a property into the set of leaf longhands it ultimately sets, so that
211
+ * different groupings of the same box, such as `border-width` and
212
+ * `border-top-width`, can be compared for equivalent coverage.
213
+ *
214
+ * @param {string} property The property name to expand.
215
+ * @return {Set} The set of leaf longhand property names.
216
+ */
217
+ function expandToLeafProperties (property) {
218
+ const cachedLeaves = leafPropertiesByProperty.get(property);
219
+ if (cachedLeaves) {
220
+ return cachedLeaves;
221
+ }
222
+ const leafProperties = new Set();
223
+ const longhands = shorthandMap[property];
224
+ if (!longhands) {
225
+ leafProperties.add(property);
226
+ } else {
227
+ for (const longhand of longhands) {
228
+ for (const leafProperty of expandToLeafProperties(longhand)) {
229
+ leafProperties.add(leafProperty);
230
+ }
231
+ }
232
+ }
233
+ leafPropertiesByProperty.set(property, leafProperties);
234
+ return leafProperties;
235
+ }
236
+
87
237
  export {
88
238
  BORDER_EDGE_PROPERTIES,
89
239
  CSS_WIDE_KEYWORDS,
90
240
  EDGE_SHORTHANDS,
241
+ expandToLeafProperties,
242
+ getLonghandsOf,
243
+ getOverridesOf,
91
244
  shorthandMap,
92
- shorthandOverrideMap
245
+ shorthandOverrideMap,
246
+ UNIFORM_VALUE_SHORTHANDS
93
247
  };
@@ -2,34 +2,17 @@
2
2
  * @file Rewrites longhand declarations that share a CSS-wide keyword into a shorthand carrying that keyword, followed by the longhands that override it.
3
3
  */
4
4
 
5
- import { minifyValue } from '../value/minify.js';
6
-
7
5
  import {
8
6
  CSS_WIDE_KEYWORDS,
9
- shorthandMap,
10
- shorthandOverrideMap
7
+ expandToLeafProperties,
8
+ getLonghandsOf,
9
+ getOverridesOf,
10
+ shorthandMap
11
11
  } from './config.js';
12
-
13
- /**
14
- * Expands a property into the set of leaf longhands it ultimately sets, so that
15
- * different groupings of the same box, such as `border-width` and
16
- * `border-top-width`, can be compared for equivalent coverage.
17
- *
18
- * @param {string} property The property name to expand.
19
- * @param {Set} leafProperties The set collecting the leaf longhand names.
20
- * @return {Set} The set of leaf longhand property names.
21
- */
22
- function expandToLeafProperties (property, leafProperties = new Set()) {
23
- const longhands = shorthandMap[property];
24
- if (!longhands) {
25
- leafProperties.add(property);
26
- return leafProperties;
27
- }
28
- for (const longhand of longhands) {
29
- expandToLeafProperties(longhand, leafProperties);
30
- }
31
- return leafProperties;
32
- }
12
+ import {
13
+ collectDeclaredProperties,
14
+ describeDeclaration
15
+ } from './lookup.js';
33
16
 
34
17
  /**
35
18
  * Checks whether a group of longhands sets every leaf longhand that the
@@ -44,23 +27,15 @@ function expandToLeafProperties (property, leafProperties = new Set()) {
44
27
  function coversEveryLonghandOfShorthand (shorthandName, properties) {
45
28
  const coveredLeaves = new Set();
46
29
  for (const property of properties) {
47
- expandToLeafProperties(property, coveredLeaves);
30
+ for (const leafProperty of expandToLeafProperties(property)) {
31
+ coveredLeaves.add(leafProperty);
32
+ }
48
33
  }
49
34
  return [...expandToLeafProperties(shorthandName)].every((leafProperty) => {
50
35
  return coveredLeaves.has(leafProperty);
51
36
  });
52
37
  }
53
38
 
54
- /**
55
- * @typedef {object} LonghandEntry
56
- * @property {object} declaration The original declaration object.
57
- * @property {number} index The declaration's index within the rule.
58
- * @property {string} property The longhand property name.
59
- * @property {string} text The minified `property:value` text.
60
- * @property {string} value The minified value, without any `!important`.
61
- * @property {boolean} isImportant Whether the declaration carries `!important`.
62
- */
63
-
64
39
  /**
65
40
  * Collects the declarations of a rule that set one of a shorthand's longhands,
66
41
  * in source order.
@@ -70,22 +45,13 @@ function coversEveryLonghandOfShorthand (shorthandName, properties) {
70
45
  * @return {Array} The matching longhand entries, in source order.
71
46
  */
72
47
  function collectLonghandEntries (declarations, shorthandName) {
73
- const longhands = shorthandMap[shorthandName];
48
+ const longhands = getLonghandsOf(shorthandName);
74
49
  const entries = [];
75
50
  declarations.forEach((declaration, index) => {
76
- if (!declaration.property || !longhands.includes(declaration.property)) {
51
+ if (!declaration.property || !longhands.has(declaration.property)) {
77
52
  return;
78
53
  }
79
- const minifiedValue = minifyValue(declaration);
80
- const isImportant = minifiedValue.includes('!important');
81
- entries.push({
82
- declaration,
83
- index,
84
- property: declaration.property,
85
- text: declaration.property + ':' + minifiedValue,
86
- value: minifiedValue.replace('!important', '').trim(),
87
- isImportant
88
- });
54
+ entries.push(describeDeclaration(declaration, index));
89
55
  });
90
56
  return entries;
91
57
  }
@@ -170,12 +136,12 @@ function resolveSharedKeyword (entries) {
170
136
  * @return {boolean} Whether an earlier declaration would be discarded.
171
137
  */
172
138
  function resetsEarlierDeclaration (declarations, shorthandName, insertionIndex) {
173
- const resetProperties = shorthandOverrideMap[shorthandName] || [];
174
- if (!resetProperties.length) {
139
+ const resetProperties = getOverridesOf(shorthandName);
140
+ if (!resetProperties.size) {
175
141
  return false;
176
142
  }
177
143
  return declarations.slice(0, insertionIndex).some((declaration) => {
178
- return resetProperties.includes(declaration.property);
144
+ return resetProperties.has(declaration.property);
179
145
  });
180
146
  }
181
147
 
@@ -184,15 +150,13 @@ function resetsEarlierDeclaration (declarations, shorthandName, insertionIndex)
184
150
  * keyword, followed by the longhands that override it, when that is shorter
185
151
  * than the group of longhands it replaces.
186
152
  *
187
- * @param {Array} declarations The declarations of a single rule.
188
- * @param {string} shorthandName The target shorthand property name.
189
- * @return {Array|null} The rewritten declarations, or null when the rewrite does not apply.
153
+ * @param {Array} declarations The declarations of a single rule.
154
+ * @param {string} shorthandName The target shorthand property name.
155
+ * @param {Set} declaredProperties The property names the rule currently declares.
156
+ * @return {Array|null} The rewritten declarations, or null when the rewrite does not apply.
190
157
  */
191
- function rewriteGroupAsKeywordShorthand (declarations, shorthandName) {
192
- const shorthandAlreadyExists = declarations.some((declaration) => {
193
- return declaration.property === shorthandName;
194
- });
195
- if (shorthandAlreadyExists) {
158
+ function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredProperties) {
159
+ if (declaredProperties.has(shorthandName)) {
196
160
  return null;
197
161
  }
198
162
 
@@ -271,12 +235,17 @@ function rewriteGroupAsKeywordShorthand (declarations, shorthandName) {
271
235
  */
272
236
  function hoistCssWideKeywordsIntoShorthands (declarations) {
273
237
  let result = declarations;
238
+ // Every shorthand needs to know which properties the rule declares, so that
239
+ // set is kept alongside the declarations and only rebuilt after a rewrite
240
+ // actually changes them.
241
+ let declaredProperties = collectDeclaredProperties(result);
274
242
  // Shorthands are visited in declaration order, so the widest shorthand of a
275
243
  // family is rewritten before the narrower shorthands it contains.
276
244
  for (const shorthandName of Object.keys(shorthandMap)) {
277
- const rewritten = rewriteGroupAsKeywordShorthand(result, shorthandName);
245
+ const rewritten = rewriteGroupAsKeywordShorthand(result, shorthandName, declaredProperties);
278
246
  if (rewritten) {
279
247
  result = rewritten;
248
+ declaredProperties = collectDeclaredProperties(result);
280
249
  }
281
250
  }
282
251
  return result;