@thejaredwilcurt/csslop 0.0.20 → 0.0.22
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 +12 -5
- package/package.json +5 -7
- package/src/declarations/background.js +322 -0
- package/src/declarations/border.js +120 -0
- package/src/declarations/config.js +36 -0
- package/src/declarations/css-wide-keywords.js +285 -0
- package/src/declarations/merge.js +323 -0
- package/src/declarations/order.js +77 -0
- package/src/declarations/process.js +188 -869
- package/src/declarations/shorthand-values.js +374 -0
- package/src/index.js +5 -2
- package/src/rules/normalize.js +29 -5
- package/src/rules/optimize.js +5 -2
- package/src/rules/stringify.js +49 -18
- package/src/value/minify.js +102 -36
- package/src/value/syntax.js +82 -1
package/README.md
CHANGED
|
@@ -20,16 +20,23 @@
|
|
|
20
20
|
|
|
21
21
|
* All tests pass.
|
|
22
22
|
* Several new tests were created upstream, and some existing tests were improved/fixed upstream (all manually by me, no AI used).
|
|
23
|
-
* The `src` folder is 100% vibe coded, and despite passing all tests, is
|
|
23
|
+
* The `src` folder is 100% vibe coded, and despite passing all tests, is absolutely not worth using as CSSLOP often outputs invalid CSS syntax. But that's the point, any time it outputs something wrong, that's an indication of a missing test upstream.
|
|
24
|
+
* I've made [an online playground](https://TheJaredWilcurt.com/playground) of all modern CSS minifiers so you can compare and isolate the problem, then use that to create a [new test upstream](https://github.com/keithamus/css-minify-tests/issues) for the AI's to be forced to pass.
|
|
24
25
|
* No AI generated code exists outside of the `src` folder, this README, for example, is 100% human crafted.
|
|
25
26
|
|
|
26
|
-
**AI's used:**
|
|
27
|
+
**AI's used during initial library creation:**
|
|
27
28
|
|
|
28
29
|
* Claude Opus 4.6 (Thinking)
|
|
29
30
|
* Claude Sonnet 4.6 (Thinking)
|
|
30
31
|
* Gemini 3.1 Pro (High Thinking)
|
|
31
32
|
* GPT-5.4 High (Thinking)
|
|
32
33
|
|
|
34
|
+
These were the latest and greatest models at the time.
|
|
35
|
+
|
|
36
|
+
**AI's used during maintenance:**
|
|
37
|
+
|
|
38
|
+
As new tests are created upstream I use whatever the latest models are, like "Claude 5 High Thinking".
|
|
39
|
+
|
|
33
40
|
These tools were prompted to pass the tests in the `/copiedTests` folder that came from `keithamus/css-minify-tests`.
|
|
34
41
|
|
|
35
42
|
**Summary of project phases:**
|
|
@@ -51,8 +58,8 @@ These tools were prompted to pass the tests in the `/copiedTests` folder that ca
|
|
|
51
58
|
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
59
|
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
60
|
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
|
|
55
|
-
1. **Playground:** Created an [online playground](https://TheJaredWilcurt.com/csslop) (no AI used) for others to more easily test things out and find
|
|
61
|
+
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 benchmarked it and it was somehow even slower (+20 min), and also the outputs weren't as small as before (+0.05%). Details below. **Update:** The "Real world" library has grown. It now takes over 7 hours to complete.
|
|
62
|
+
1. **Playground:** Created an [online playground](https://TheJaredWilcurt.com/csslop) (no AI used) for others to more easily test things out and find and create new upstream tests.
|
|
56
63
|
|
|
57
64
|
|
|
58
65
|
**Full Notes of AI Experiment:**
|
|
@@ -166,7 +173,7 @@ These tools were prompted to pass the tests in the `/copiedTests` folder that ca
|
|
|
166
173
|
* 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.
|
|
167
174
|
* 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.
|
|
168
175
|
* **KILL ALL HUMANS:**
|
|
169
|
-
* More libraries were added to the "Real World CSS Libraries". Now when I run `npm run real` to minify all of them, it ends up taking about 7 hours to run. This is because the optimizations CSSLOP performs do not scale linearly with more CSS rules, they scale exponentially. There are a
|
|
176
|
+
* More libraries were added to the "Real World CSS Libraries". Now when I run `npm run real` to minify all of them, it ends up taking about 7 hours to run. This is because the optimizations CSSLOP performs do not scale linearly with more CSS rules, they scale exponentially. There are a handful of large CSS files that each take about an hour to complete, where as the smaller files all go by pretty fast.
|
|
170
177
|
* When the upstream `css-minify-tests` adds a new test to their suite, I pull it in and have the AI update the library to pass that new test. For some reason, the AI notices `/tests/realworld.test.js`, sees that `npm run real` will execute that, then sees it has permission to run `npm run *`, so it takes it upon itself to run this SEVEN HOUR LONG COMMAND, without anyone asking it to...
|
|
171
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.
|
|
172
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)!"
|
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.22",
|
|
6
6
|
"description": "Experimental CSS minification",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"prestart": "node ./scripts/prestart.js",
|
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
"prebuild": "node ./scripts/prebuild.js",
|
|
12
12
|
"build": "vite build",
|
|
13
13
|
"postbuild": "node ./scripts/postbuild.js",
|
|
14
|
-
"copy": "node ./scripts/
|
|
15
|
-
"copy-repo": "node ./scripts/copyTestsFromRepo.js",
|
|
14
|
+
"copy": "node ./scripts/copyTests.js",
|
|
16
15
|
"test": "node ./tests/index.test.js",
|
|
17
16
|
"real": "node ./tests/realworld.test.js",
|
|
18
17
|
"lint": "eslint *.js scripts src tests --fix",
|
|
@@ -30,17 +29,16 @@
|
|
|
30
29
|
"@eslint/js": "^10.0.1",
|
|
31
30
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
32
31
|
"codemirror": "^6.0.2",
|
|
33
|
-
"css-minify-tests": "github:keithamus/css-minify-tests",
|
|
34
32
|
"eslint": "^10.8.1",
|
|
35
33
|
"eslint-config-tjw-base": "^5.0.0",
|
|
36
34
|
"eslint-config-tjw-import-x": "^1.0.1",
|
|
37
35
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
|
38
36
|
"eslint-plugin-import-x": "^4.17.0",
|
|
39
|
-
"eslint-plugin-jsdoc": "^64.0
|
|
37
|
+
"eslint-plugin-jsdoc": "^64.1.0",
|
|
40
38
|
"fflate": "^0.8.3",
|
|
41
|
-
"globals": "^17.
|
|
39
|
+
"globals": "^17.11.0",
|
|
42
40
|
"pretty-ms": "^9.3.0",
|
|
43
|
-
"real-world-css-libraries": "^1.0.
|
|
41
|
+
"real-world-css-libraries": "^1.0.7",
|
|
44
42
|
"vite": "^8.2.1"
|
|
45
43
|
},
|
|
46
44
|
"author": "The Jared Wilcurt",
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Classifies background shorthand tokens and rebuilds the `background` shorthand from its component values.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { minifyValue } from '../value/minify.js';
|
|
6
|
+
|
|
7
|
+
import { shorthandMap } from './config.js';
|
|
8
|
+
|
|
9
|
+
const BACKGROUND_POSITION_KEYWORDS = new Set(['left', 'center', 'right', 'top', 'bottom']);
|
|
10
|
+
const BACKGROUND_REPEAT_KEYWORDS = new Set(['repeat', 'no-repeat', 'repeat-x', 'repeat-y', 'space', 'round']);
|
|
11
|
+
const BACKGROUND_ATTACHMENT_KEYWORDS = new Set(['scroll', 'fixed', 'local']);
|
|
12
|
+
const BACKGROUND_BOX_KEYWORDS = new Set(['border-box', 'padding-box', 'content-box']);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the background position from a value map. Prefers the combined
|
|
16
|
+
* `background-position` property if present, otherwise combines
|
|
17
|
+
* `background-position-x` and `background-position-y` into a single value.
|
|
18
|
+
*
|
|
19
|
+
* @param {Map} valueMap A map of property names to their minified values.
|
|
20
|
+
* @return {string|null} The resolved position string, or null if no position data is available.
|
|
21
|
+
*/
|
|
22
|
+
function resolveBackgroundPosition (valueMap) {
|
|
23
|
+
const position = valueMap.get('background-position');
|
|
24
|
+
if (position) {
|
|
25
|
+
return position;
|
|
26
|
+
}
|
|
27
|
+
const positionX = valueMap.get('background-position-x');
|
|
28
|
+
const positionY = valueMap.get('background-position-y');
|
|
29
|
+
if (positionX && positionY) {
|
|
30
|
+
return positionX + ' ' + positionY;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Determines whether a token is a background image component such as `none`,
|
|
37
|
+
* `url(...)`, or an image-producing function like `linear-gradient(...)`.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} token The token to classify.
|
|
40
|
+
* @return {boolean} Whether the token is a background image token.
|
|
41
|
+
*/
|
|
42
|
+
function isBackgroundImageToken (token) {
|
|
43
|
+
if (token === 'none' || token.startsWith('url(')) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (!token.endsWith(')')) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const functionNameMatch = token.match(/^([a-z-]+)\(/i);
|
|
50
|
+
if (!functionNameMatch) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
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);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Determines whether a token can participate in a background-position value.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} token The token to classify.
|
|
61
|
+
* @return {boolean} Whether the token is a valid background-position token.
|
|
62
|
+
*/
|
|
63
|
+
function isBackgroundPositionToken (token) {
|
|
64
|
+
const lowercaseToken = token.toLowerCase();
|
|
65
|
+
if (BACKGROUND_POSITION_KEYWORDS.has(lowercaseToken)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (/^[+-]?(?:\d+|\d*\.\d+)(?:%|[a-z]+)?$/i.test(token)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return /^(?:calc|min|max|clamp|var|env)\(/i.test(token);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Determines whether a token is a background color component after excluding
|
|
76
|
+
* known image, position, repeat, attachment, and box tokens.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} token The token to classify.
|
|
79
|
+
* @return {boolean} Whether the token is a background color token.
|
|
80
|
+
*/
|
|
81
|
+
function isBackgroundColorToken (token) {
|
|
82
|
+
if (token === '/' || isBackgroundImageToken(token) || isBackgroundPositionToken(token)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const lowercaseToken = token.toLowerCase();
|
|
86
|
+
if (BACKGROUND_REPEAT_KEYWORDS.has(lowercaseToken) || BACKGROUND_ATTACHMENT_KEYWORDS.has(lowercaseToken) || BACKGROUND_BOX_KEYWORDS.has(lowercaseToken)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return /^#/i.test(token) || /^[a-z-]+$/i.test(token) || /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\(/i.test(token);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Splits a token like `linear-gradient(...)100%` into separate image and
|
|
94
|
+
* position tokens when the function output is immediately followed by a
|
|
95
|
+
* background-position token.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} token The token to inspect.
|
|
98
|
+
* @return {Array} The original token, or separate image/position tokens.
|
|
99
|
+
*/
|
|
100
|
+
function splitAttachedBackgroundImageToken (token) {
|
|
101
|
+
const lastCloseParenthesis = token.lastIndexOf(')');
|
|
102
|
+
if (lastCloseParenthesis === -1 || lastCloseParenthesis === token.length - 1) {
|
|
103
|
+
return [token];
|
|
104
|
+
}
|
|
105
|
+
const imageToken = token.slice(0, lastCloseParenthesis + 1);
|
|
106
|
+
const followingToken = token.slice(lastCloseParenthesis + 1);
|
|
107
|
+
if (!isBackgroundImageToken(imageToken) || !isBackgroundPositionToken(followingToken)) {
|
|
108
|
+
return [token];
|
|
109
|
+
}
|
|
110
|
+
return [imageToken, followingToken];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Splits a single-layer background shorthand into top-level tokens while
|
|
115
|
+
* respecting nested parentheses and preserving `/` as its own token.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} value The background shorthand value.
|
|
118
|
+
* @return {Array} The extracted top-level tokens.
|
|
119
|
+
*/
|
|
120
|
+
function splitBackgroundTokens (value) {
|
|
121
|
+
const tokens = [];
|
|
122
|
+
let currentToken = '';
|
|
123
|
+
let parenthesisDepth = 0;
|
|
124
|
+
|
|
125
|
+
for (const character of value) {
|
|
126
|
+
if (character === '(') {
|
|
127
|
+
parenthesisDepth++;
|
|
128
|
+
} else if (character === ')') {
|
|
129
|
+
parenthesisDepth--;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (parenthesisDepth === 0 && character === '/') {
|
|
133
|
+
if (currentToken) {
|
|
134
|
+
tokens.push(currentToken);
|
|
135
|
+
currentToken = '';
|
|
136
|
+
}
|
|
137
|
+
tokens.push('/');
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (parenthesisDepth === 0 && /\s/.test(character)) {
|
|
142
|
+
if (currentToken) {
|
|
143
|
+
tokens.push(currentToken);
|
|
144
|
+
currentToken = '';
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
currentToken += character;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (currentToken) {
|
|
153
|
+
tokens.push(currentToken);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const normalizedTokens = [];
|
|
157
|
+
for (const token of tokens) {
|
|
158
|
+
normalizedTokens.push(...splitAttachedBackgroundImageToken(token));
|
|
159
|
+
}
|
|
160
|
+
return normalizedTokens;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Extracts the simple image/color base from an existing background shorthand.
|
|
165
|
+
* Returns null for shorthands that already contain size, position, or any
|
|
166
|
+
* token that cannot be safely reconstructed by the background builder.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} value The background shorthand value.
|
|
169
|
+
* @return {Map|null} A component map for safe reconstruction, or null.
|
|
170
|
+
*/
|
|
171
|
+
function extractSimpleBackgroundBase (value) {
|
|
172
|
+
const tokens = splitBackgroundTokens(value);
|
|
173
|
+
const componentMap = new Map();
|
|
174
|
+
|
|
175
|
+
for (const token of tokens) {
|
|
176
|
+
if (token === '/') {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (isBackgroundImageToken(token)) {
|
|
180
|
+
if (componentMap.has('background-image')) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
componentMap.set('background-image', token);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (isBackgroundColorToken(token)) {
|
|
187
|
+
if (componentMap.has('background-color')) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
componentMap.set('background-color', token);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return componentMap;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Serializes normalized background components into a minified background
|
|
201
|
+
* shorthand value while omitting default sub-values.
|
|
202
|
+
*
|
|
203
|
+
* @param {Map} valueMap The normalized background component map.
|
|
204
|
+
* @param {string} importantSuffix A trailing `!important` suffix, if needed.
|
|
205
|
+
* @return {string|null} The minified background shorthand, or null.
|
|
206
|
+
*/
|
|
207
|
+
function buildBackgroundShorthandValue (valueMap, importantSuffix) {
|
|
208
|
+
const color = valueMap.get('background-color');
|
|
209
|
+
const image = valueMap.get('background-image');
|
|
210
|
+
const repeat = valueMap.get('background-repeat');
|
|
211
|
+
const attachment = valueMap.get('background-attachment');
|
|
212
|
+
const size = valueMap.get('background-size');
|
|
213
|
+
const origin = valueMap.get('background-origin');
|
|
214
|
+
const clip = valueMap.get('background-clip');
|
|
215
|
+
const position = resolveBackgroundPosition(valueMap);
|
|
216
|
+
|
|
217
|
+
const result = [];
|
|
218
|
+
if (color && color !== 'transparent') {
|
|
219
|
+
result.push(color);
|
|
220
|
+
}
|
|
221
|
+
if (image && image !== 'none') {
|
|
222
|
+
result.push(image);
|
|
223
|
+
}
|
|
224
|
+
if (position && position !== '0 0' && position !== '0% 0%') {
|
|
225
|
+
result.push(position);
|
|
226
|
+
}
|
|
227
|
+
if (size && size !== 'auto') {
|
|
228
|
+
if (position && position !== '0 0' && position !== '0% 0%') {
|
|
229
|
+
result.push('/' + size);
|
|
230
|
+
} else {
|
|
231
|
+
result.push('0 0/' + size);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (repeat && repeat !== 'repeat') {
|
|
235
|
+
result.push(repeat);
|
|
236
|
+
}
|
|
237
|
+
if (attachment && attachment !== 'scroll') {
|
|
238
|
+
result.push(attachment);
|
|
239
|
+
}
|
|
240
|
+
const hasNonDefaultOrigin = origin && origin !== 'padding-box';
|
|
241
|
+
const hasNonDefaultClip = clip && clip !== 'border-box';
|
|
242
|
+
if (hasNonDefaultOrigin && hasNonDefaultClip) {
|
|
243
|
+
result.push(origin);
|
|
244
|
+
result.push(clip);
|
|
245
|
+
} else if (hasNonDefaultOrigin || hasNonDefaultClip) {
|
|
246
|
+
if (origin) {
|
|
247
|
+
result.push(origin);
|
|
248
|
+
}
|
|
249
|
+
if (clip) {
|
|
250
|
+
result.push(clip);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!result.length) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
return result.join(' ') + importantSuffix;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Merges later background longhands into an earlier simple background shorthand
|
|
261
|
+
* when their combined value can be reconstructed without changing semantics.
|
|
262
|
+
*
|
|
263
|
+
* @param {Array} declarations The declarations in source order.
|
|
264
|
+
* @return {Array} The updated declarations with absorbed longhands.
|
|
265
|
+
*/
|
|
266
|
+
function absorbBackgroundLonghandsIntoShorthand (declarations) {
|
|
267
|
+
const backgroundIndex = declarations.findIndex((declaration) => {
|
|
268
|
+
return declaration.property === 'background';
|
|
269
|
+
});
|
|
270
|
+
if (backgroundIndex === -1) {
|
|
271
|
+
return declarations;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const backgroundDeclaration = declarations[backgroundIndex];
|
|
275
|
+
const backgroundValue = minifyValue(backgroundDeclaration);
|
|
276
|
+
const backgroundIsImportant = backgroundValue.includes('!important');
|
|
277
|
+
const simpleBase = extractSimpleBackgroundBase(backgroundValue.replace('!important', '').trim());
|
|
278
|
+
if (!simpleBase) {
|
|
279
|
+
return declarations;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const absorbableProperties = new Set(shorthandMap.background.filter((property) => {
|
|
283
|
+
return property !== 'background';
|
|
284
|
+
}));
|
|
285
|
+
const relevantDeclarations = declarations.filter((declaration, index) => {
|
|
286
|
+
return index > backgroundIndex && absorbableProperties.has(declaration.property);
|
|
287
|
+
});
|
|
288
|
+
if (!relevantDeclarations.length) {
|
|
289
|
+
return declarations;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const sharesImportance = relevantDeclarations.every((declaration) => {
|
|
293
|
+
return minifyValue(declaration).includes('!important') === backgroundIsImportant;
|
|
294
|
+
});
|
|
295
|
+
if (!sharesImportance) {
|
|
296
|
+
return declarations;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const declaration of relevantDeclarations) {
|
|
300
|
+
simpleBase.set(declaration.property, minifyValue(declaration).replace('!important', '').trim());
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const mergedValue = buildBackgroundShorthandValue(simpleBase, backgroundIsImportant ? '!important' : '');
|
|
304
|
+
if (!mergedValue) {
|
|
305
|
+
return declarations;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return declarations.flatMap((declaration, index) => {
|
|
309
|
+
if (index === backgroundIndex) {
|
|
310
|
+
return [{ ...declaration, value: mergedValue }];
|
|
311
|
+
}
|
|
312
|
+
if (index > backgroundIndex && absorbableProperties.has(declaration.property)) {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
return [declaration];
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export {
|
|
320
|
+
absorbBackgroundLonghandsIntoShorthand,
|
|
321
|
+
buildBackgroundShorthandValue
|
|
322
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Rewrites border longhand declarations that cannot be expressed as a single `border` shorthand into the shortest valid shorthand plus override pair.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { minifyValue } from '../value/minify.js';
|
|
6
|
+
import { splitTopLevelComponents } from '../value/syntax.js';
|
|
7
|
+
|
|
8
|
+
import { CSS_WIDE_KEYWORDS } from './config.js';
|
|
9
|
+
|
|
10
|
+
const BORDER_TRIO_PROPERTIES = ['border-width', 'border-style', 'border-color'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Finds the index of the last declaration for a property, which is the one that
|
|
14
|
+
* wins the cascade within a rule.
|
|
15
|
+
*
|
|
16
|
+
* @param {Array} declarations The declarations to search.
|
|
17
|
+
* @param {string} property The property name to look for.
|
|
18
|
+
* @return {number} The index of the matching declaration, or -1 when absent.
|
|
19
|
+
*/
|
|
20
|
+
function findLastDeclarationIndex (declarations, property) {
|
|
21
|
+
for (let index = declarations.length - 1; index >= 0; index--) {
|
|
22
|
+
if (declarations[index].property === property) {
|
|
23
|
+
return index;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return -1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Rewrites a `border-width` + `border-style` + `border-color` trio whose color
|
|
31
|
+
* differs per edge into a `border` shorthand built from the first color, followed
|
|
32
|
+
* by a `border-color` override that restores the per-edge colors. The rewrite is
|
|
33
|
+
* only applied when the resulting pair is shorter than the three longhands.
|
|
34
|
+
*
|
|
35
|
+
* @param {Array} declarations The declarations of a single rule.
|
|
36
|
+
* @return {Array} The declarations, with the trio rewritten when it is shorter.
|
|
37
|
+
*/
|
|
38
|
+
function collapseBorderTrioWithPerEdgeColor (declarations) {
|
|
39
|
+
const trioIndexes = BORDER_TRIO_PROPERTIES.map((property) => {
|
|
40
|
+
return findLastDeclarationIndex(declarations, property);
|
|
41
|
+
});
|
|
42
|
+
const hasWholeTrio = trioIndexes.every((index) => {
|
|
43
|
+
return index !== -1;
|
|
44
|
+
});
|
|
45
|
+
if (!hasWholeTrio) {
|
|
46
|
+
return declarations;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const trioValues = trioIndexes.map((index) => {
|
|
50
|
+
return minifyValue(declarations[index]);
|
|
51
|
+
});
|
|
52
|
+
const allImportant = trioValues.every((value) => {
|
|
53
|
+
return value.includes('!important');
|
|
54
|
+
});
|
|
55
|
+
const noneImportant = trioValues.every((value) => {
|
|
56
|
+
return !value.includes('!important');
|
|
57
|
+
});
|
|
58
|
+
if (!allImportant && !noneImportant) {
|
|
59
|
+
return declarations;
|
|
60
|
+
}
|
|
61
|
+
const importantSuffix = allImportant ? '!important' : '';
|
|
62
|
+
|
|
63
|
+
const [width, style, color] = trioValues.map((value) => {
|
|
64
|
+
return value.replace('!important', '').trim();
|
|
65
|
+
});
|
|
66
|
+
const colorComponents = splitTopLevelComponents(color);
|
|
67
|
+
const isSingleComponent = (value) => {
|
|
68
|
+
return splitTopLevelComponents(value).length === 1;
|
|
69
|
+
};
|
|
70
|
+
const canBuildShorthand = (
|
|
71
|
+
isSingleComponent(width) &&
|
|
72
|
+
isSingleComponent(style) &&
|
|
73
|
+
colorComponents.length > 1
|
|
74
|
+
);
|
|
75
|
+
if (!canBuildShorthand) {
|
|
76
|
+
return declarations;
|
|
77
|
+
}
|
|
78
|
+
const usesCssWideKeyword = [width, style, ...colorComponents].some((value) => {
|
|
79
|
+
return CSS_WIDE_KEYWORDS.has(value.toLowerCase());
|
|
80
|
+
});
|
|
81
|
+
if (usesCssWideKeyword) {
|
|
82
|
+
return declarations;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const borderValue = [width, style, colorComponents[0]].join(' ') + importantSuffix;
|
|
86
|
+
const colorValue = color + importantSuffix;
|
|
87
|
+
const rewrittenLength = ('border:' + borderValue + ';border-color:' + colorValue).length;
|
|
88
|
+
const longhandLength = (
|
|
89
|
+
'border-width:' + trioValues[0] +
|
|
90
|
+
';border-style:' + trioValues[1] +
|
|
91
|
+
';border-color:' + trioValues[2]
|
|
92
|
+
).length;
|
|
93
|
+
if (rewrittenLength >= longhandLength) {
|
|
94
|
+
return declarations;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const replacedIndexes = new Set(trioIndexes);
|
|
98
|
+
const insertionIndex = Math.min(...trioIndexes);
|
|
99
|
+
return declarations.flatMap((declaration, index) => {
|
|
100
|
+
if (index === insertionIndex) {
|
|
101
|
+
return [
|
|
102
|
+
{
|
|
103
|
+
property: 'border',
|
|
104
|
+
value: borderValue,
|
|
105
|
+
isAssembledShorthand: true
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
property: 'border-color',
|
|
109
|
+
value: colorValue
|
|
110
|
+
}
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
if (replacedIndexes.has(index)) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
return [declaration];
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export { collapseBorderTrioWithPerEdgeColor };
|
|
@@ -51,7 +51,43 @@ const shorthandOverrideMap = {
|
|
|
51
51
|
mask: ['mask-border', 'mask-border-source', 'mask-border-slice', 'mask-border-width', 'mask-border-outset', 'mask-border-repeat', 'mask-border-mode']
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* CSS-wide keywords, which are only valid as the entire value of a declaration
|
|
56
|
+
* and never as an individual component of a shorthand value.
|
|
57
|
+
*
|
|
58
|
+
* @type {Set<string>}
|
|
59
|
+
*/
|
|
60
|
+
const CSS_WIDE_KEYWORDS = new Set(['inherit', 'initial', 'unset', 'revert', 'revert-layer']);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Shorthand properties that themselves hold a full width/style/color value for a
|
|
64
|
+
* single edge of a box, so they can only collapse into their parent shorthand
|
|
65
|
+
* when every edge carries the exact same value.
|
|
66
|
+
*
|
|
67
|
+
* @type {Set<string>}
|
|
68
|
+
*/
|
|
69
|
+
const EDGE_SHORTHANDS = new Set([
|
|
70
|
+
'border-top',
|
|
71
|
+
'border-right',
|
|
72
|
+
'border-bottom',
|
|
73
|
+
'border-left',
|
|
74
|
+
'border-inline-start',
|
|
75
|
+
'border-inline-end',
|
|
76
|
+
'border-block-start',
|
|
77
|
+
'border-block-end'
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The four physical edge shorthands that together cover the `border` shorthand.
|
|
82
|
+
*
|
|
83
|
+
* @type {Array}
|
|
84
|
+
*/
|
|
85
|
+
const BORDER_EDGE_PROPERTIES = ['border-top', 'border-right', 'border-bottom', 'border-left'];
|
|
86
|
+
|
|
54
87
|
export {
|
|
88
|
+
BORDER_EDGE_PROPERTIES,
|
|
89
|
+
CSS_WIDE_KEYWORDS,
|
|
90
|
+
EDGE_SHORTHANDS,
|
|
55
91
|
shorthandMap,
|
|
56
92
|
shorthandOverrideMap
|
|
57
93
|
};
|