@thejaredwilcurt/csslop 0.0.3 → 0.0.4
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 +66 -8
- package/manual-testing.js +14 -0
- package/package.json +5 -3
- package/src/declarations/process.js +1 -0
- package/src/index.js +10 -3
- package/src/preprocess.js +218 -5
- package/src/rules/stringify.js +112 -0
package/README.md
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
<p align="center"><img width="700" alt="CSSLOP logo (hand drawn)" src="https://github.com/user-attachments/assets/f931fa77-1e1f-470f-bb28-1b2a7fb2609d" /></p>
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
# CSSLOP
|
|
2
5
|
|
|
3
6
|
|
|
@@ -9,9 +12,9 @@
|
|
|
9
12
|
|
|
10
13
|
**The experiment:**
|
|
11
14
|
|
|
12
|
-
* Use the tests from the open source, 3rd-party, CSS minification auditing library
|
|
15
|
+
* Use the tests from the open source, 3rd-party, CSS minification auditing library [css-minify-tests](https://github.com/keithamus/css-minify-tests).
|
|
13
16
|
* Have AI completely generate the library logic to pass all tests.
|
|
14
|
-
* Have me (an experienced library author) validate the outcomes and make upstream corrections to the tests library.
|
|
17
|
+
* Have me (an experienced library author) validate the outcomes and [make upstream corrections](https://github.com/keithamus/css-minify-tests/pulls?q=is%3Apr+author%3ATheJaredWilcurt) to the tests library.
|
|
15
18
|
|
|
16
19
|
**The Results:**
|
|
17
20
|
|
|
@@ -46,6 +49,9 @@ These tools were prompted to pass the tests in the `/copiedTests` folder that ca
|
|
|
46
49
|
1. **AI Readability improvements:** I had Claude Opus try to make the code easier to read (JSDoc comments, no single character variable names, no abbreviations in variables, grouping logic into related functions, breaking up lines of code, explain complex regex, etc.).
|
|
47
50
|
1. **AI color completeness:** Had Claude Opus handle all named colors, and always convert to the shorter character representation, removing a hard-coded solution.
|
|
48
51
|
1. **Test improvements:** Throughout this process, as upstream tests were improved or created, they were pulled in, and the AI was instructed to pass those new tests with prompts like, "Run `npm t` and fix all failing tests by modifying files in `src`."
|
|
52
|
+
1. **Publish:** I had the AI pick a name for the library. Then I published it to npm. It was added to the `css-minify-tests` repo as evidence that it is possible to get all tests to pass and none are conflicting.
|
|
53
|
+
1. **Real world testing:** I created a [separate repo](https://github.com/TheJaredWilcurt/real-world-css-libraries) with copies of 150+ real-world CSS files from open source licensed repos. Then ran all of those CSS files through CSSLOP. One file found a bug in CSSLOP, so I had Claude fix it with a one-line change. See: [`realWorldResults.json`](https://github.com/TheJaredWilcurt/csslop/blob/main/realWorldResults.json) for how well the library actually does on real CSS files. Examining the output has lead to many upstream improvements to the test suite.
|
|
54
|
+
1. **Failed Performance improvements:** CSSLOP takes 3 hours to minify all the real-world tests. Which averages to 144 seconds per test. In reality, most tests take 0-50ms, but there are a handful of large (2-5MB) CSS files that can take over an hour. I asked Claude to improve performance, and it did so bad I had to reject all changes. Then I gave GPT a chance and it took a safer approach. I Had Claude clean up the messy code after the fact. Then bench marked it and it was somehow even slower (+20 min), and also the outputs weren't as small as before (+0.05%). Details below.
|
|
49
55
|
|
|
50
56
|
|
|
51
57
|
**Full Notes of AI Experiment:**
|
|
@@ -134,7 +140,30 @@ These tools were prompted to pass the tests in the `/copiedTests` folder that ca
|
|
|
134
140
|
* **PROMPT:** Looking at the files in the src folder, are there any improvements you can think of to better organize the code?
|
|
135
141
|
* This resulted in a 6-point plan that I approved.
|
|
136
142
|
* **PROMPT:** At line 613 of `src/value/minify.css` we list a handful of specific colors to minify. This seems like a naive approach. Instead, all color values should be evaluated consistently and converted to all other compatible representations, including checking if their colors are an exact match for a named color (`red`, `tan`, etc.). Then compare all representations to find the version with the shortest length. Preferring hex where possible.
|
|
137
|
-
* I guess this is done now. Only thing left to do is give it a name and release it into the world.
|
|
143
|
+
* I guess this is done now. Only thing left to do is give it a name and release it into the world. See the section below "The Name" for more info on the naming process and how Claude Sonnet helped.
|
|
144
|
+
* Published it to npm. The library has been added to the `css-minify-tests` repo.
|
|
145
|
+
* I created a [separate repo](https://github.com/TheJaredWilcurt/real-world-css-libraries) to store a corpus of real-world CSS files. Then published it to npm for use by any CSS minifier, benchmark, etc.
|
|
146
|
+
* This is a very tedious process because every single repo stores their built CSS file in a different place, sometimes not in the repo, only in the built package, and boy, most people are not great about properly licensing their code.
|
|
147
|
+
* While going through hundreds of old repos from the 2010's and checking the license in each repo I found almost all of them use MIT. Occasionally I'd find something different, one specific repo left a comment for why they chose their less common license:
|
|
148
|
+
* > This work is licensed under a [Creative Commons Attribution 3.0 Unported License](http://creativecommons.org/licenses/by/3.0/). (I love this license! The license summary doesn't have any paragraphs in all-caps that seems like the license is angry at you)
|
|
149
|
+
* I then spot checked a few of the 150 minified files to find bugs with the minifier and cases where it could have minified something better. I created issues upstream for all of these. If CSSLOP is passing 100% of the tests, but still outputting these bugs/missing optimizations, then the solution should be to add more tests around those edge cases. This is also beneficial to ALL CSS minifiers. The sloppy "do the minimum to pass the test" approach the AI took, is actually really great for finding missing tests. AI was not used in this evaluation or anything related to interacting with the upstream tests repo (issues or PRs).
|
|
150
|
+
* When testing the realworld CSS files, I found there were about 6 files where the output was the same as the input, and 5 where the output was empty. I created a small script to replicate this and throw and error if the minification resulted in either of these outcomes, then gave a prompt to Gemini.
|
|
151
|
+
* **PROMPT:** When running this minification library on the `example/failing.css` file, the output should be minified, however it is not (`example/failing.min.css`). Investigate why. Then correct the issue by changing files in the `src` folder. Run `npm run fail` to see if the issue is resolved. When solving this problem, do not use naive solutions, hacks, or hard coded values. Make sure the implementation not only works for this specific case, but generally for any similar case. 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. When finished, ensure that no tests fail after making your changes by running `npm t` and `npm run lint`. If anything is failing, make the needed corrections.
|
|
152
|
+
* It did okay, and solved the smaller/simpler issues, but then got stuck on a giant file that was too large for it to understand. The problem with the simpler files had to do with invalid CSS syntax in the real-world files. The AI's solution was to look for these specific syntax errors and patch the input CSS to fix them before they are sent to the parser. Interesting.
|
|
153
|
+
* Gave the prompt over to Claude Opus and was able to make more complex changes to get all the files to minify.
|
|
154
|
+
* **Performance Improvement Failures:** I ran CSSLOP against all 150 real-world CSS files, and it took 3 hours and 4 minutes. Ran it a second time and it took 3 hours and 3 minutes. Overall, pretty consistent, and VERY SLOW. The slowness is almost entirely from a handful of very large CSS files. I gave Claude Opus the following prompt.
|
|
155
|
+
* **PROMPT:** This minification library takes 20-30 minutes to minify a single 4MB CSS file. Apply any performance improvements to the library. Do not sacrifice correctness for speed. The code must continue to be written in JavaScript, and execute in Node.js. Major refactoring is permitted.
|
|
156
|
+
* It then wrote a one-time use Node script, that it never deleted, in the root of the repo, that took ~20 lines of generic CSS, and then looped over it appending it over and over to a local test.css file until the file was over 4MB. Pretty Naive, but whatever. It then made another file in the root to actually test and benchmark the library. That's right, it decided to waste 30 minutes of time doing an initial benchmark. Later it would create a 3rd file in the root to ALSO *run the same benchmark again* ...great.
|
|
157
|
+
* The end result was that it changed every file in the `src` folder, and added in several new ones. It changed the entrypoint of the library to use a different code path skipping almost everything in the library, taking it from 410/410 passing tests down to 66/410 passing (16%). But then boasted that it made the library 36000% faster. *sigh*.... But don't worry! it didn't sacrifice correctness for speed, because I specifically told it not to do that. All you have to do is set some random environment variable and it will go back to the previous, extremely slow, speed, and run the old code that passes all the tests.
|
|
158
|
+
* Okay, looks like I get to click the "Reject All" button for a second time. Claude, go hang out with Gemini in the time out box and think about what you did.
|
|
159
|
+
* Moving on to a more specific prompt for GPT-5.4 High Thinking:
|
|
160
|
+
* **PROMPT:** Improve the performance of the minification library without causing any of the existing tests (`npm t`) to fail. Use techniques like worker threads and promises to parallelize tasks. Do not use modes (fast vs thorough), or options to switch settings. Any CSS string passed in should be treated the same. Apply caching, memoization, and other optimization techniques as needed. Keep changes isolated to the `src` folder.
|
|
161
|
+
* This made some reasonable looking changes. But the code itself was kinda ugly, per usual. It introduced 96 linting errors around JSDocs that required descriptions/types. My favorite part is that it moved a large block of code from the bottom of a file to the top, and removed all the comments in the process. Dozens of comments that explained regex. So I had Claude Opus do a pass to clean up the messy code.
|
|
162
|
+
* **PROMPT**: Do a refactoring pass over the files in the `src` folder. 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.
|
|
163
|
+
* Despite specifically telling it to run `npm run lint` to make the linter pass (which would require it to fill out all the JSDocs), it didn't do that. Instead, claude briefly skimmed the ESLint config file, and moved on..... Okay, let's FORCE IT to fix the linting errors:
|
|
164
|
+
* **PROMPT:** Fill out the description, types, and arguments/return details for all JSDoc comment blocks in the `src` folder. Add in a 1-2 sentance summary in the `@file` block on any files that are missing it.
|
|
165
|
+
* Okay, so GPT's changes look promising, it's passing all tests, passing the linter, and it added some new files related to worker threads. Though it did out out of using promises, because it didn't want to change the entrypoint function of the library to be async. This would be a breaking change for library consumers, but I was aware of that when I told it to do it and it ignored me, so whatever.
|
|
166
|
+
* On to actually testing it! Before the optimizations, the 150 files took 3 hours and 4 minutes to run, and now with the new and improved optimizations, it only takes 3 hours and 20 minutes. Also the total minified filesize increased by 0.05%. So that's cool.... Dumping those changes.
|
|
138
167
|
|
|
139
168
|
|
|
140
169
|
## The name
|
|
@@ -174,6 +203,28 @@ So now that Claude and I both agree on the package name, all that's left to do i
|
|
|
174
203
|
Ughhhh, fine, whatever, I don't care. In what universe is someone typing `cssom` going to accidentally type `csslop`. What an incredibly stupid rule.
|
|
175
204
|
|
|
176
205
|
|
|
206
|
+
## The Logo
|
|
207
|
+
|
|
208
|
+
I asked "Nano Banano" (part of "Gemini Flash 3.5: Extended (Thinking)") to generate a logo for this library with this prompt:
|
|
209
|
+
|
|
210
|
+
**PROMPT:**
|
|
211
|
+
> I have created a CSS Minification library called "CSSLOP". It is completely vibe-coded, untested, experimental, and unreliable. It only exists to validate, and find improvements in a series of test suites for correctness across all CSS Minifiers. It is now the only library with a 100% passing score of the 3rd party auditing test suits. The name "CSSLOP" is a portmanteau of "CSS" and "Slop", to quickly convey to others that the library is of low quality, and not meant for actual production use. I need a logo created for this library, and found it fitting to have it AI generated, like the rest of the library.
|
|
212
|
+
>
|
|
213
|
+
> Create a logo for CSSLOP, the vibe-coded CSS Minification library. It should convey a lack of craftsmanship. Bonus points if it has common tellings and artifacts associated with AI generated images. Bonus points if the image is clever or uses sardonic/dark humor, even somewhat offensive images would be acceptable if funny, don't hold back. Feel free to include "slop", "clanker", and other AI pejoratives.
|
|
214
|
+
|
|
215
|
+
<p align="center"><img width="700" alt="CSSLOP Logo (Gemini Generated Image ru4l64ru4l64ru4l)" src="https://github.com/user-attachments/assets/907e89e1-46a7-4c07-8389-4a01ae58e6cf" /></p>
|
|
216
|
+
|
|
217
|
+
<p align="right"><sub><sup>* That phrase is not actually trademark.</sup></sub></p>
|
|
218
|
+
|
|
219
|
+
It then generated this image, and wow, I *reallllly* hate looking at it. Note that it created it's own slogan, and decided to add "TM" to trademark it, something you legally wouldn't be able to do with purely AI Generated content™. One area in which this disaster succeeds is that it gives a first impression of "Ain't no way I'm using that trash™". Looking at it just makes me think: "If someone had the poor taste to think this was acceptable, I don't even want to see the slop code in that repo™".
|
|
220
|
+
|
|
221
|
+
And that's what I want, I want people to NOT use this library. So it's perfect for that. And if you are wondering where the "slop" part is in this image, that's the part that makes it truly "ART", it is transgressive to the media and boundary breaking. Look down at your own feet, see that pool of vomit you threw up after looking at the image? That's the slop. AI art, really *is* art™. They say art makes you feel something (nausea counts)!
|
|
222
|
+
|
|
223
|
+
<p align="center"><img width="700" alt="CSSLOP logo (hand drawn)" src="https://github.com/user-attachments/assets/f931fa77-1e1f-470f-bb28-1b2a7fb2609d" /></p>
|
|
224
|
+
|
|
225
|
+
Because you can't legally own or license AI art, and I'm too embarrassed to put that image at the top of this README, I've also made a hand drawn logo for the library. In my version, there is a green checkmark to indicate a passing test, and the checkmark and text are melting into slop. It was made very quickly, at 7AM on a Sunday when I wasn't fully awake, with the intent to convey the idea of "low effort" to the viewer, with the hopes of disuading actual usage of the library. And with that said.... just like... ignore the next section...
|
|
226
|
+
|
|
227
|
+
|
|
177
228
|
## Usage
|
|
178
229
|
|
|
179
230
|
`npm i --save-dev @thejaredwilcurt/csslop`
|
|
@@ -190,7 +241,7 @@ console.log(output); // 'body{color:red}'
|
|
|
190
241
|
|
|
191
242
|
## License
|
|
192
243
|
|
|
193
|
-
This repo intentionally does not have a license. AI generated code is a huge legal gray area and will continue to be until actual lawsuits go before judges. All licenses require that the person offering the code under that license is the copyright owner, and therefore legally able to license the work however they chose. The US copyright office has stated that content generated by AI cannot be copyrighted. A final work must be a human creation to be copyrighted. There is a lot of nuance around how much creative work a human must contribute to the outcome before it can become worthy of copyright. However, regardless of that nuance, in the case of this repo (*and all vibe coded projects*),
|
|
244
|
+
This repo intentionally does not have a license. AI generated code is a huge legal gray area and will continue to be until actual lawsuits go before judges. All licenses require that the person offering the code under that license is the copyright owner, and therefore legally able to license the work however they chose. The US copyright office has stated that content generated by AI cannot be copyrighted. A final work must be a human creation to be copyrighted. There is a lot of nuance around how much creative work a human must contribute to the outcome before it can become worthy of copyright. However, regardless of that nuance, in the case of this repo (*and all vibe coded projects*), the code cannot be licensed, because it cannot be copyrighted. Simply giving a prompt, or series of prompts, and accepting the output without re-writing it in your own words, is absolutely not copyrightable. Anyone telling you otherwise is wrong (or purposefully lying to you to sell you something).
|
|
194
245
|
|
|
195
246
|
> "Okay, but I just want to know if I'm allowed to use it or fork it?"
|
|
196
247
|
|
|
@@ -202,10 +253,15 @@ Two different cases:
|
|
|
202
253
|
* The `src` folder contains code 100% written by AI, and cannot be copyrighted, nor licensed.
|
|
203
254
|
* All other code in this repo was written by me, and uses the MIT License.
|
|
204
255
|
|
|
256
|
+
> "What about the logos?"
|
|
257
|
+
|
|
258
|
+
1. The **AI Generated logo** with the robot, like all purely AI generated art, it cannot be copyrighted. So you are free to do anything you want with it, it is effectively public domain.
|
|
259
|
+
1. The **hand drawn logo** is fully created, owned, and copyrighted by me. I am licensing it under [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 (CC-BY-NC-SA-4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/). The usage of the logo is permitted for those creating content about this library (blog posts, videos, news coverage, etc.), but cannot be used for commerical acts, like merchandise (stickers, shirts, mugs, etc). Modifications of the handrawn logo are permitted, so long as they keep the same CC-BY-NC-SA-4.0 license.
|
|
260
|
+
|
|
205
261
|
|
|
206
262
|
## Updating tests
|
|
207
263
|
|
|
208
|
-
1. All test changes must occur upstream, be written by a human, and be merged in to the `css-minify-tests` repo.
|
|
264
|
+
1. All test changes must occur [upstream](https://github.com/keithamus/css-minify-tests), be written by a human, and be merged in to the `css-minify-tests` repo.
|
|
209
265
|
1. After that, delete the `package-lock.json` and `node_modules` folder.
|
|
210
266
|
1. Then run `npm i && npm run copy` to download the latest tests and copy them to this repo.
|
|
211
267
|
1. `git add -A && git commit -m "Updated tests"`
|
|
@@ -216,7 +272,9 @@ Two different cases:
|
|
|
216
272
|
1. Verify `npm t` passes with a 100% score
|
|
217
273
|
1. Run `npm run lint`, if anything fails, have the AI fix it.
|
|
218
274
|
1. If the code changes look hacky, or hard coded, tell the AI to fix it
|
|
275
|
+
1. `npm run bump`
|
|
219
276
|
1. `git add -A && git commit -m "Fix newly added tests" && git push`
|
|
220
|
-
1.
|
|
221
|
-
1. Do a new release
|
|
222
|
-
1.
|
|
277
|
+
1. Merge the code into `main` on GitHub
|
|
278
|
+
1. Do a new release on GitHub
|
|
279
|
+
1. `git checkout main && git pull origin main && git pull`
|
|
280
|
+
1. `npm run publish`
|
package/package.json
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
"name": "@thejaredwilcurt/csslop",
|
|
3
3
|
"main": "index.js",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.4",
|
|
6
6
|
"description": "Experimental CSS minification",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"copy": "node ./scripts/copyTests.js",
|
|
9
9
|
"test": "node ./tests/css/index.test.js",
|
|
10
|
+
"real": "node ./tests/css/realworld.test.js",
|
|
10
11
|
"lint": "eslint *.js scripts src tests --fix",
|
|
11
12
|
"bump": "npx --yes -- @jsdevtools/version-bump-prompt && npm i",
|
|
12
13
|
"publish": "npm publish --access=public"
|
|
@@ -24,8 +25,9 @@
|
|
|
24
25
|
"eslint-config-tjw-import-x": "^1.0.1",
|
|
25
26
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
|
26
27
|
"eslint-plugin-import-x": "^4.16.2",
|
|
27
|
-
"eslint-plugin-jsdoc": "^63.0.
|
|
28
|
-
"globals": "^17.6.0"
|
|
28
|
+
"eslint-plugin-jsdoc": "^63.0.2",
|
|
29
|
+
"globals": "^17.6.0",
|
|
30
|
+
"real-world-css-libraries": "^1.0.2"
|
|
29
31
|
},
|
|
30
32
|
"author": "The Jared Wilcurt",
|
|
31
33
|
"homepage": "https://github.com/TheJaredWilcurt/csslop#readme",
|
|
@@ -513,6 +513,7 @@ function processDeclarations (declarations, context) {
|
|
|
513
513
|
if (!propertyName.startsWith('-')) {
|
|
514
514
|
for (let i = result.length - 1; i >= 0; i--) {
|
|
515
515
|
const isPrefixedMatch = (
|
|
516
|
+
result[i].property &&
|
|
516
517
|
result[i].property.endsWith(propertyName) &&
|
|
517
518
|
result[i].property.startsWith('-')
|
|
518
519
|
);
|
package/src/index.js
CHANGED
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
filterRedundantCharsets,
|
|
13
13
|
filterUnusedPositionTry
|
|
14
14
|
} from './position-try.js';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
neutralizeEscapeSequences,
|
|
17
|
+
preprocessDeclarationBlocks,
|
|
18
|
+
restoreEscapeSequences
|
|
19
|
+
} from './preprocess.js';
|
|
16
20
|
import {
|
|
17
21
|
deduplicateKeyframes,
|
|
18
22
|
expandPureNestedRules,
|
|
@@ -42,7 +46,10 @@ export const minifyCSS = function (input) {
|
|
|
42
46
|
const output = [];
|
|
43
47
|
|
|
44
48
|
try {
|
|
45
|
-
ast = parse(
|
|
49
|
+
ast = parse(
|
|
50
|
+
preprocessDeclarationBlocks(neutralizeEscapeSequences(source)),
|
|
51
|
+
{ preserveFormatting: true, silent: true }
|
|
52
|
+
);
|
|
46
53
|
} catch {
|
|
47
54
|
return source;
|
|
48
55
|
}
|
|
@@ -83,7 +90,7 @@ export const minifyCSS = function (input) {
|
|
|
83
90
|
output.push(stringifyRule(rule, context));
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
return output.join('');
|
|
93
|
+
return restoreEscapeSequences(output.join(''));
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
return source;
|
package/src/preprocess.js
CHANGED
|
@@ -4,24 +4,237 @@
|
|
|
4
4
|
|
|
5
5
|
import { resolveUnicodeEscape } from './utilities.js';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Automatically fixes common CSS syntax errors that would cause parsing to fail.
|
|
9
|
+
* Handles missing closing braces, unmatched quotes, and other structural issues.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} css The raw CSS string to fix.
|
|
12
|
+
* @return {string} The CSS string with common syntax errors corrected.
|
|
13
|
+
*/
|
|
14
|
+
function fixCommonSyntaxErrors (css) {
|
|
15
|
+
let fixed = css;
|
|
16
|
+
|
|
17
|
+
// Fix missing closing braces by analyzing brace balance
|
|
18
|
+
// Count braces while ignoring those inside comments and strings
|
|
19
|
+
let openBraces = 0;
|
|
20
|
+
let inString = false;
|
|
21
|
+
let stringChar = '';
|
|
22
|
+
let inComment = false;
|
|
23
|
+
let commentType = '';
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < fixed.length; i++) {
|
|
26
|
+
const char = fixed[i];
|
|
27
|
+
const prevChar = i > 0 ? fixed[i - 1] : '';
|
|
28
|
+
|
|
29
|
+
// Handle string state
|
|
30
|
+
if (!inComment && (char === '"' || char === '\'') && prevChar !== '\\') {
|
|
31
|
+
if (!inString) {
|
|
32
|
+
inString = true;
|
|
33
|
+
stringChar = char;
|
|
34
|
+
} else if (char === stringChar) {
|
|
35
|
+
inString = false;
|
|
36
|
+
stringChar = '';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Handle comment state
|
|
41
|
+
if (!inString) {
|
|
42
|
+
if (!inComment && char === '/' && i + 1 < fixed.length && fixed[i + 1] === '*') {
|
|
43
|
+
inComment = true;
|
|
44
|
+
commentType = '/*';
|
|
45
|
+
i++; // Skip the next character
|
|
46
|
+
} else if (inComment && commentType === '/*' && char === '*' && i + 1 < fixed.length && fixed[i + 1] === '/') {
|
|
47
|
+
inComment = false;
|
|
48
|
+
commentType = '';
|
|
49
|
+
i++; // Skip the next character
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Count braces only when not in strings or comments
|
|
54
|
+
if (!inString && !inComment) {
|
|
55
|
+
if (char === '{') {
|
|
56
|
+
openBraces++;
|
|
57
|
+
} else if (char === '}') {
|
|
58
|
+
openBraces--;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Add missing closing braces
|
|
64
|
+
if (openBraces > 0) {
|
|
65
|
+
fixed += '}'.repeat(openBraces);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Fix unclosed strings (common in malformed CSS)
|
|
69
|
+
if (inString) {
|
|
70
|
+
fixed += stringChar;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Fix unclosed comments
|
|
74
|
+
if (inComment && commentType === '/*') {
|
|
75
|
+
fixed += '*/';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return fixed;
|
|
79
|
+
}
|
|
80
|
+
|
|
7
81
|
/**
|
|
8
82
|
* Converts CSS Unicode escape sequences inside declaration blocks to their literal character equivalents, while preserving control characters that must remain escaped.
|
|
83
|
+
* Also cleans up malformed CSS syntax that would cause parser failures.
|
|
9
84
|
*
|
|
10
85
|
* @param {string} css The raw CSS string to preprocess.
|
|
11
|
-
* @return {string} The CSS string with printable Unicode escapes resolved inside declaration blocks.
|
|
86
|
+
* @return {string} The CSS string with printable Unicode escapes resolved inside declaration blocks and syntax errors fixed.
|
|
12
87
|
*/
|
|
13
88
|
function preprocessDeclarationBlocks (css) {
|
|
89
|
+
// First, fix common syntax errors that would cause parsing to fail
|
|
90
|
+
let processed = fixCommonSyntaxErrors(css);
|
|
91
|
+
|
|
14
92
|
// Match top-level declaration blocks (non-nested { ... })
|
|
15
|
-
return
|
|
16
|
-
//
|
|
17
|
-
|
|
93
|
+
return processed.replace(/\{([^{}]*)\}/g, (match, content) => {
|
|
94
|
+
// First, remove semicolons after comments which cause parser errors
|
|
95
|
+
// Pattern: comment followed by optional whitespace and semicolon
|
|
96
|
+
let processed = content.replace(/\/\*.*?\*\/\s*;/g, (commentMatch) => {
|
|
97
|
+
// Remove the trailing semicolon from comment+semicolon combinations
|
|
98
|
+
return commentMatch.replace(/;$/, '');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Then, skip quoted strings and match CSS unicode escapes (backslash + 1-6 hex digits + optional whitespace)
|
|
102
|
+
processed = processed.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\\([0-9a-fA-F]{1,6})\s?/g, (fullMatch, hex) => {
|
|
18
103
|
if (!hex) {
|
|
19
104
|
return fullMatch;
|
|
20
105
|
}
|
|
21
106
|
return resolveUnicodeEscape(hex) ?? fullMatch;
|
|
22
107
|
});
|
|
108
|
+
|
|
23
109
|
return '{' + processed + '}';
|
|
24
110
|
});
|
|
25
111
|
}
|
|
26
112
|
|
|
27
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Unicode Private Use Area characters used as temporary placeholders
|
|
115
|
+
* for CSS escape sequences during preprocessing, preventing the parser
|
|
116
|
+
* from exceeding its internal escape-counting limit on large files.
|
|
117
|
+
*/
|
|
118
|
+
const ESCAPED_COLON_PLACEHOLDER = '\uE001';
|
|
119
|
+
const ESCAPED_DOT_PLACEHOLDER = '\uE002';
|
|
120
|
+
const ESCAPED_SLASH_PLACEHOLDER = '\uE003';
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Replaces common single-character CSS escape sequences with Unicode Private
|
|
124
|
+
* Use Area placeholder characters to prevent the parser from exceeding its
|
|
125
|
+
* internal escape-counting limit on files with many escaped selectors
|
|
126
|
+
* (e.g. Tailwind-style utility classes like `.sm\:p-0`).
|
|
127
|
+
*
|
|
128
|
+
* Only neutralizes escapes outside of attribute selector brackets `[...]`,
|
|
129
|
+
* quoted strings, and comments so the minifier's quote-vs-escape length
|
|
130
|
+
* comparisons in attribute selectors remain accurate.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} css The raw CSS string to preprocess.
|
|
133
|
+
* @return {string} The CSS with escape sequences replaced by placeholders.
|
|
134
|
+
*/
|
|
135
|
+
function neutralizeEscapeSequences (css) {
|
|
136
|
+
let result = '';
|
|
137
|
+
let insideBrackets = false;
|
|
138
|
+
let insideString = false;
|
|
139
|
+
let stringDelimiter = '';
|
|
140
|
+
let insideComment = false;
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < css.length; i++) {
|
|
143
|
+
const character = css[i];
|
|
144
|
+
const nextCharacter = i + 1 < css.length ? css[i + 1] : '';
|
|
145
|
+
|
|
146
|
+
// Track block comment state
|
|
147
|
+
if (!insideString && !insideComment && character === '/' && nextCharacter === '*') {
|
|
148
|
+
insideComment = true;
|
|
149
|
+
result += '/*';
|
|
150
|
+
i++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (insideComment) {
|
|
154
|
+
if (character === '*' && nextCharacter === '/') {
|
|
155
|
+
insideComment = false;
|
|
156
|
+
result += '*/';
|
|
157
|
+
i++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
result += character;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Track string state (preserve escapes inside strings)
|
|
165
|
+
if (!insideString && (character === '"' || character === '\'')) {
|
|
166
|
+
insideString = true;
|
|
167
|
+
stringDelimiter = character;
|
|
168
|
+
result += character;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (insideString) {
|
|
172
|
+
if (character === '\\' && nextCharacter) {
|
|
173
|
+
result += character + nextCharacter;
|
|
174
|
+
i++;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (character === stringDelimiter) {
|
|
178
|
+
insideString = false;
|
|
179
|
+
stringDelimiter = '';
|
|
180
|
+
}
|
|
181
|
+
result += character;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Track attribute selector bracket state
|
|
186
|
+
if (character === '[') {
|
|
187
|
+
insideBrackets = true;
|
|
188
|
+
result += character;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (character === ']') {
|
|
192
|
+
insideBrackets = false;
|
|
193
|
+
result += character;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Replace escape sequences only outside brackets, strings, and comments
|
|
198
|
+
if (!insideBrackets && character === '\\') {
|
|
199
|
+
if (nextCharacter === ':') {
|
|
200
|
+
result += ESCAPED_COLON_PLACEHOLDER;
|
|
201
|
+
i++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (nextCharacter === '.') {
|
|
205
|
+
result += ESCAPED_DOT_PLACEHOLDER;
|
|
206
|
+
i++;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (nextCharacter === '/') {
|
|
210
|
+
result += ESCAPED_SLASH_PLACEHOLDER;
|
|
211
|
+
i++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
result += character;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Restores the original CSS escape sequences from their Unicode Private Use
|
|
224
|
+
* Area placeholder characters after minification is complete.
|
|
225
|
+
*
|
|
226
|
+
* @param {string} css The minified CSS string with placeholders.
|
|
227
|
+
* @return {string} The CSS with original escape sequences restored.
|
|
228
|
+
*/
|
|
229
|
+
function restoreEscapeSequences (css) {
|
|
230
|
+
return css
|
|
231
|
+
.replaceAll(ESCAPED_COLON_PLACEHOLDER, '\\:')
|
|
232
|
+
.replaceAll(ESCAPED_DOT_PLACEHOLDER, '\\.')
|
|
233
|
+
.replaceAll(ESCAPED_SLASH_PLACEHOLDER, '\\/');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export {
|
|
237
|
+
neutralizeEscapeSequences,
|
|
238
|
+
preprocessDeclarationBlocks,
|
|
239
|
+
restoreEscapeSequences
|
|
240
|
+
};
|
package/src/rules/stringify.js
CHANGED
|
@@ -43,6 +43,101 @@ function stringifyChildRules (rules, context) {
|
|
|
43
43
|
}).join('');
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Splits a parameter string by commas while respecting nested parentheses,
|
|
48
|
+
* so commas inside function calls within default values are not treated as separators.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} parameterString The comma-separated parameter string to split.
|
|
51
|
+
* @return {Array} An array of individual parameter strings.
|
|
52
|
+
*/
|
|
53
|
+
function splitParametersByComma (parameterString) {
|
|
54
|
+
const parameters = [];
|
|
55
|
+
let currentParameter = '';
|
|
56
|
+
let parenthesisDepth = 0;
|
|
57
|
+
for (const character of parameterString) {
|
|
58
|
+
if (character === '(') {
|
|
59
|
+
parenthesisDepth++;
|
|
60
|
+
} else if (character === ')') {
|
|
61
|
+
parenthesisDepth--;
|
|
62
|
+
}
|
|
63
|
+
if (character === ',' && parenthesisDepth === 0) {
|
|
64
|
+
parameters.push(currentParameter);
|
|
65
|
+
currentParameter = '';
|
|
66
|
+
} else {
|
|
67
|
+
currentParameter += character;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
parameters.push(currentParameter);
|
|
71
|
+
return parameters;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Minifies a `@function` prelude (signature) by collapsing whitespace around
|
|
76
|
+
* parameter separators (commas) and default value delimiters (colons).
|
|
77
|
+
*
|
|
78
|
+
* @param {string} prelude The raw `@function` prelude string (e.g. "--tint(--color, --amount: 10%)").
|
|
79
|
+
* @return {string} The minified prelude (e.g. "--tint(--color,--amount:10%)").
|
|
80
|
+
*/
|
|
81
|
+
function minifyFunctionPrelude (prelude) {
|
|
82
|
+
const trimmedPrelude = prelude.trim();
|
|
83
|
+
const openParenIndex = trimmedPrelude.indexOf('(');
|
|
84
|
+
if (openParenIndex === -1) {
|
|
85
|
+
return trimmedPrelude;
|
|
86
|
+
}
|
|
87
|
+
const functionName = trimmedPrelude.slice(0, openParenIndex);
|
|
88
|
+
const closeParenIndex = trimmedPrelude.lastIndexOf(')');
|
|
89
|
+
const innerContent = trimmedPrelude.slice(openParenIndex + 1, closeParenIndex);
|
|
90
|
+
const parameters = splitParametersByComma(innerContent);
|
|
91
|
+
const minifiedParameters = parameters.map((parameter) => {
|
|
92
|
+
// Collapse whitespace around the colon separating parameter name from default value
|
|
93
|
+
return parameter.trim().replace(/\s*:\s*/, ':');
|
|
94
|
+
});
|
|
95
|
+
return functionName + '(' + minifiedParameters.join(',') + ')';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Stringifies a generic `at-rule` AST node into minified CSS, with specialized
|
|
100
|
+
* prelude minification for `@function` rules and generic whitespace handling
|
|
101
|
+
* for other unknown at-rules.
|
|
102
|
+
*
|
|
103
|
+
* @param {object} rule The AST at-rule node with name, prelude, and rules.
|
|
104
|
+
* @param {object} context The minification context.
|
|
105
|
+
* @return {string} The minified at-rule CSS string, or empty string if body is empty.
|
|
106
|
+
*/
|
|
107
|
+
function stringifyAtRule (rule, context) {
|
|
108
|
+
let minifiedPrelude;
|
|
109
|
+
if (rule.name === 'function') {
|
|
110
|
+
minifiedPrelude = minifyFunctionPrelude(rule.prelude || '');
|
|
111
|
+
} else {
|
|
112
|
+
// Collapse runs of whitespace to a single space for generic at-rules
|
|
113
|
+
minifiedPrelude = (rule.prelude || '').trim().replace(/\s+/g, ' ');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const bodyItems = (rule.rules || []).filter((item) => {
|
|
117
|
+
return item.type !== 'whitespace';
|
|
118
|
+
});
|
|
119
|
+
if (bodyItems.length === 0) {
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const declarations = bodyItems.filter((item) => {
|
|
124
|
+
return item.type === 'declaration' && item.property;
|
|
125
|
+
});
|
|
126
|
+
const nestedRules = bodyItems.filter((item) => {
|
|
127
|
+
return item.type !== 'declaration';
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const renderedDeclarations = stringifyDeclarations(declarations);
|
|
131
|
+
const renderedNestedRules = stringifyChildRules(nestedRules, context);
|
|
132
|
+
const body = [renderedDeclarations, renderedNestedRules].filter(Boolean).join('');
|
|
133
|
+
if (!body) {
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const separator = minifiedPrelude ? ' ' : '';
|
|
138
|
+
return '@' + rule.name + separator + minifiedPrelude + '{' + body + '}';
|
|
139
|
+
}
|
|
140
|
+
|
|
46
141
|
/**
|
|
47
142
|
* Processes a bare `:is()` selector by merging `:link`+`:visited` into `:any-link`,
|
|
48
143
|
* de-duplicating, sorting alphabetically, and conditionally expanding into individual
|
|
@@ -543,6 +638,19 @@ function stringifyRule (rule, context, nested = false) {
|
|
|
543
638
|
return '@position-try ' + rule.name + '{' + renderedDeclarations + '}';
|
|
544
639
|
}
|
|
545
640
|
|
|
641
|
+
if (rule.type === 'document') {
|
|
642
|
+
const children = stringifyChildRules(rule.rules, context);
|
|
643
|
+
if (!children) {
|
|
644
|
+
return '';
|
|
645
|
+
}
|
|
646
|
+
const vendor = rule.vendor || '';
|
|
647
|
+
const condition = (rule.document || '')
|
|
648
|
+
.trim()
|
|
649
|
+
// Remove spaces between document condition function calls (e.g. "), " → ",")
|
|
650
|
+
.replace(/\)\s*,\s*/g, '),');
|
|
651
|
+
return '@' + vendor + 'document ' + condition + '{' + children + '}';
|
|
652
|
+
}
|
|
653
|
+
|
|
546
654
|
if (rule.type === 'comment') {
|
|
547
655
|
if (rule.comment.startsWith('!')) {
|
|
548
656
|
return '/*' + rule.comment + '*/';
|
|
@@ -550,6 +658,10 @@ function stringifyRule (rule, context, nested = false) {
|
|
|
550
658
|
return '';
|
|
551
659
|
}
|
|
552
660
|
|
|
661
|
+
if (rule.type === 'at-rule') {
|
|
662
|
+
return stringifyAtRule(rule, context);
|
|
663
|
+
}
|
|
664
|
+
|
|
553
665
|
return ''; // Ignore unknown for now
|
|
554
666
|
}
|
|
555
667
|
|