@thejaredwilcurt/csslop 0.0.37 → 0.0.38
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/package.json +4 -3
- package/src/index.js +4 -2
- package/src/rules/lang.js +366 -0
- package/src/rules/optimize.js +77 -10
- package/src/rules/selectors.js +12 -3
- package/src/rules/stringify.js +14 -8
- package/src/value/math.js +183 -1
- package/src/value/minify.js +55 -1
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.38",
|
|
6
6
|
"description": "Experimental CSS minification",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"prestart": "node ./scripts/prestart.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"copy": "node ./scripts/copyTests.js",
|
|
15
15
|
"test": "node ./tests/index.test.js",
|
|
16
16
|
"real": "node ./tests/realworld.test.js",
|
|
17
|
+
"idem": "node ./tests/idempotency.test.js",
|
|
17
18
|
"lint": "eslint *.js scripts src tests --fix",
|
|
18
19
|
"fix": "npm run lint",
|
|
19
20
|
"bump": "npx --yes -- @jsdevtools/version-bump-prompt patch && npm i",
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
"proto": "proto pin node latest --resolve --tool-native && proto pin npm latest --resolve --tool-native && git status"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@csstools/css-calc": "^3.
|
|
25
|
+
"@csstools/css-calc": "^3.4.0",
|
|
25
26
|
"@node-projects/css-parser": "^5.2.0"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
"eslint-config-tjw-import-x": "^1.0.1",
|
|
37
38
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
|
38
39
|
"eslint-plugin-import-x": "^4.17.0",
|
|
39
|
-
"eslint-plugin-jsdoc": "^64.3.
|
|
40
|
+
"eslint-plugin-jsdoc": "^64.3.10",
|
|
40
41
|
"fflate": "^0.8.3",
|
|
41
42
|
"globals": "^17.12.0",
|
|
42
43
|
"pretty-ms": "^9.3.1",
|
package/src/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
preprocessDeclarationBlocks,
|
|
26
26
|
restoreEscapeSequences
|
|
27
27
|
} from './preprocess.js';
|
|
28
|
+
import { combineLanguageSelectors } from './rules/lang.js';
|
|
28
29
|
import {
|
|
29
30
|
deduplicateKeyframes,
|
|
30
31
|
expandPureNestedRules,
|
|
@@ -83,7 +84,8 @@ function splitSelectorList (selectorList) {
|
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
86
|
* Deduplicates a minified CSS selector list string by splitting at top-level
|
|
86
|
-
* commas, removing duplicate selectors,
|
|
87
|
+
* commas, removing duplicate selectors, combining the selectors that differ
|
|
88
|
+
* only in language-code, and rejoining with commas.
|
|
87
89
|
*
|
|
88
90
|
* @param {string} selectorList The comma-separated selector list string.
|
|
89
91
|
* @return {string} The deduplicated selector list string.
|
|
@@ -98,7 +100,7 @@ function deduplicateSelectorList (selectorList) {
|
|
|
98
100
|
seen.add(selector);
|
|
99
101
|
return true;
|
|
100
102
|
});
|
|
101
|
-
return unique.join(',');
|
|
103
|
+
return combineLanguageSelectors(unique).join(',');
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
/**
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Minifies the `:lang()` pseudo-class, by writing each language-code in
|
|
3
|
+
* its shortest valid form and by combining selectors that differ only in the
|
|
4
|
+
* language-code they ask for.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
findMatchingCloseParenthesis,
|
|
9
|
+
findNextFunctionCallOutsideStrings
|
|
10
|
+
} from './selectors.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The `:lang()` pseudo-class opening, as it is written in minified output.
|
|
14
|
+
* Pseudo-class names are case-insensitive, so the scan that looks for this
|
|
15
|
+
* token ignores case, while everything written back out uses this spelling.
|
|
16
|
+
*
|
|
17
|
+
* @type {string}
|
|
18
|
+
*/
|
|
19
|
+
const LANGUAGE_FUNCTION = ':lang(';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Matches a backslash escape of a character that cannot be read as the start
|
|
23
|
+
* of a hexadecimal code point escape, and captures the character it escapes.
|
|
24
|
+
*
|
|
25
|
+
* @type {RegExp}
|
|
26
|
+
*/
|
|
27
|
+
const SINGLE_CHARACTER_ESCAPE = /\\([^0-9a-fA-F])/g;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Matches a backslash followed by a hexadecimal digit, which starts a code
|
|
31
|
+
* point escape whose meaning cannot be judged without decoding it.
|
|
32
|
+
*
|
|
33
|
+
* @type {RegExp}
|
|
34
|
+
*/
|
|
35
|
+
const CODE_POINT_ESCAPE = /\\[0-9a-fA-F]/;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Matches a language-code holding a character that would end the code, or the
|
|
39
|
+
* selector, if the code were written without quotes or escapes.
|
|
40
|
+
*
|
|
41
|
+
* @type {RegExp}
|
|
42
|
+
*/
|
|
43
|
+
const UNWRITABLE_CHARACTER = /[\s"'()\\]/;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Matches a language-code whose first characters read as the beginning of a
|
|
47
|
+
* number, which no unquoted identifier may do.
|
|
48
|
+
*
|
|
49
|
+
* @type {RegExp}
|
|
50
|
+
*/
|
|
51
|
+
const NUMERIC_START = /^-?[0-9]/;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Matches a single character an identifier may hold as written.
|
|
55
|
+
*
|
|
56
|
+
* @type {RegExp}
|
|
57
|
+
*/
|
|
58
|
+
const IDENTIFIER_CHARACTER = /[a-zA-Z0-9_-]/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The first code point that is an identifier character in its own right, so
|
|
62
|
+
* that letters beyond ASCII never need escaping.
|
|
63
|
+
*
|
|
64
|
+
* @type {number}
|
|
65
|
+
*/
|
|
66
|
+
const FIRST_NON_ASCII_CODE_POINT = 0x80;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reports whether a character has to be escaped to survive in an unquoted
|
|
70
|
+
* language-code.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} character A single character of a language-code.
|
|
73
|
+
* @return {boolean} True when the character needs a backslash in front of it.
|
|
74
|
+
*/
|
|
75
|
+
function requiresEscape (character) {
|
|
76
|
+
return (
|
|
77
|
+
character.codePointAt(0) < FIRST_NON_ASCII_CODE_POINT &&
|
|
78
|
+
!IDENTIFIER_CHARACTER.test(character)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Splits the argument list of a `:lang()` into its individual language-codes,
|
|
84
|
+
* treating only the commas outside of a quoted code as separators.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} argumentList The text between the parentheses of a `:lang()`.
|
|
87
|
+
* @return {Array} The language-codes, still in the form they were written in.
|
|
88
|
+
*/
|
|
89
|
+
function splitLanguageCodes (argumentList) {
|
|
90
|
+
const codes = [];
|
|
91
|
+
let current = '';
|
|
92
|
+
let openQuote = '';
|
|
93
|
+
for (let index = 0; index < argumentList.length; index++) {
|
|
94
|
+
const character = argumentList[index];
|
|
95
|
+
const nextCharacter = argumentList[index + 1] ?? '';
|
|
96
|
+
if (character === '\\') {
|
|
97
|
+
// An escape carries the character behind it along, whatever it may be
|
|
98
|
+
current += character + nextCharacter;
|
|
99
|
+
index++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (openQuote) {
|
|
103
|
+
current += character;
|
|
104
|
+
if (character === openQuote) {
|
|
105
|
+
openQuote = '';
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (character === '"' || character === '\'') {
|
|
110
|
+
openQuote = character;
|
|
111
|
+
current += character;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (character === ',') {
|
|
115
|
+
codes.push(current);
|
|
116
|
+
current = '';
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
current += character;
|
|
120
|
+
}
|
|
121
|
+
codes.push(current);
|
|
122
|
+
return codes;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Reads the plain language-code out of one `:lang()` argument, stripping the
|
|
127
|
+
* quotes or escapes that were only there to let it be written. Arguments this
|
|
128
|
+
* minifier cannot read with certainty, such as one holding a code point
|
|
129
|
+
* escape, report no code so they are left exactly as their author wrote them.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} argument One argument of a `:lang()`, as written.
|
|
132
|
+
* @return {string|null} The language-code itself, or null when it cannot be read.
|
|
133
|
+
*/
|
|
134
|
+
function readLanguageCode (argument) {
|
|
135
|
+
const text = argument.trim();
|
|
136
|
+
if (!text || CODE_POINT_ESCAPE.test(text)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const firstCharacter = text[0];
|
|
140
|
+
const isQuoted = (
|
|
141
|
+
(firstCharacter === '"' || firstCharacter === '\'') &&
|
|
142
|
+
text.length > 1 &&
|
|
143
|
+
text.endsWith(firstCharacter)
|
|
144
|
+
);
|
|
145
|
+
let code = text;
|
|
146
|
+
if (isQuoted) {
|
|
147
|
+
code = text.slice(1, -1);
|
|
148
|
+
}
|
|
149
|
+
code = code.replace(SINGLE_CHARACTER_ESCAPE, '$1');
|
|
150
|
+
if (!code || UNWRITABLE_CHARACTER.test(code)) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return code;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Writes a language-code in the shortest form a browser still reads as that
|
|
158
|
+
* same code. A code of plain identifier characters needs nothing around it; a
|
|
159
|
+
* code holding others is either escaped character by character or wrapped in
|
|
160
|
+
* quotes once, whichever costs fewer characters.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} code The language-code itself.
|
|
163
|
+
* @return {string} The shortest way to write the code inside a `:lang()`.
|
|
164
|
+
*/
|
|
165
|
+
function formatLanguageCode (code) {
|
|
166
|
+
const quoted = '"' + code + '"';
|
|
167
|
+
// A leading digit can only be escaped as a code point, which is never short
|
|
168
|
+
if (NUMERIC_START.test(code)) {
|
|
169
|
+
return quoted;
|
|
170
|
+
}
|
|
171
|
+
const escaped = [...code].map((character) => {
|
|
172
|
+
if (requiresEscape(character)) {
|
|
173
|
+
return '\\' + character;
|
|
174
|
+
}
|
|
175
|
+
return character;
|
|
176
|
+
}).join('');
|
|
177
|
+
if (escaped.length <= quoted.length) {
|
|
178
|
+
return escaped;
|
|
179
|
+
}
|
|
180
|
+
return quoted;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Rewrites the argument list of one `:lang()`, lowercasing every code because
|
|
185
|
+
* language-codes are matched case-insensitively (one casing throughout a
|
|
186
|
+
* stylesheet compresses better), dropping repeated codes, and writing each
|
|
187
|
+
* remaining code in its shortest form.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} argumentList The text between the parentheses of a `:lang()`.
|
|
190
|
+
* @return {string|null} The minified argument list, or null when an argument could not be read.
|
|
191
|
+
*/
|
|
192
|
+
function minifyLanguageArguments (argumentList) {
|
|
193
|
+
const codes = [];
|
|
194
|
+
for (const argument of splitLanguageCodes(argumentList)) {
|
|
195
|
+
const code = readLanguageCode(argument);
|
|
196
|
+
if (code === null) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
codes.push(code.toLowerCase());
|
|
200
|
+
}
|
|
201
|
+
return [...new Set(codes)].map(formatLanguageCode).join(',');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Locates every `:lang()` of a selector, reporting where each one starts,
|
|
206
|
+
* where its argument list starts, and where it ends.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} selector A minified CSS selector string.
|
|
209
|
+
* @return {Array} One entry per `:lang()`, holding its `startIndex`, `openIndex`, and `closeIndex`.
|
|
210
|
+
*/
|
|
211
|
+
function findLanguageFunctions (selector) {
|
|
212
|
+
const languageFunctions = [];
|
|
213
|
+
let position = 0;
|
|
214
|
+
while (position < selector.length) {
|
|
215
|
+
const startIndex = findNextFunctionCallOutsideStrings(selector, LANGUAGE_FUNCTION, position);
|
|
216
|
+
if (startIndex === -1) {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
const openIndex = startIndex + LANGUAGE_FUNCTION.length - 1;
|
|
220
|
+
const closeIndex = findMatchingCloseParenthesis(selector, openIndex);
|
|
221
|
+
if (closeIndex === -1) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
languageFunctions.push({
|
|
225
|
+
closeIndex,
|
|
226
|
+
openIndex,
|
|
227
|
+
startIndex
|
|
228
|
+
});
|
|
229
|
+
position = closeIndex + 1;
|
|
230
|
+
}
|
|
231
|
+
return languageFunctions;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Minifies every `:lang()` a selector holds.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} selector A minified CSS selector string.
|
|
238
|
+
* @return {string} The selector with each `:lang()` written in its shortest form.
|
|
239
|
+
*/
|
|
240
|
+
function minifyLanguageSelector (selector) {
|
|
241
|
+
const languageFunctions = findLanguageFunctions(selector);
|
|
242
|
+
if (!languageFunctions.length) {
|
|
243
|
+
return selector;
|
|
244
|
+
}
|
|
245
|
+
let result = '';
|
|
246
|
+
let index = 0;
|
|
247
|
+
for (const languageFunction of languageFunctions) {
|
|
248
|
+
const {
|
|
249
|
+
closeIndex,
|
|
250
|
+
openIndex,
|
|
251
|
+
startIndex
|
|
252
|
+
} = languageFunction;
|
|
253
|
+
const minifiedArguments = minifyLanguageArguments(selector.slice(openIndex + 1, closeIndex));
|
|
254
|
+
result += selector.slice(index, startIndex);
|
|
255
|
+
if (minifiedArguments === null) {
|
|
256
|
+
result += selector.slice(startIndex, closeIndex + 1);
|
|
257
|
+
} else {
|
|
258
|
+
result += LANGUAGE_FUNCTION + minifiedArguments + ')';
|
|
259
|
+
}
|
|
260
|
+
index = closeIndex + 1;
|
|
261
|
+
}
|
|
262
|
+
return result + selector.slice(index);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Reports whether a position in a selector sits inside a functional
|
|
267
|
+
* pseudo-class, such as the `:lang()` of `:not(:lang(en))`.
|
|
268
|
+
*
|
|
269
|
+
* @param {string} selector A minified CSS selector string.
|
|
270
|
+
* @param {number} index The position to judge.
|
|
271
|
+
* @return {boolean} True when an enclosing parenthesis is still open at that position.
|
|
272
|
+
*/
|
|
273
|
+
function isInsideFunction (selector, index) {
|
|
274
|
+
let depth = 0;
|
|
275
|
+
for (const character of selector.slice(0, index)) {
|
|
276
|
+
if (character === '(') {
|
|
277
|
+
depth++;
|
|
278
|
+
} else if (character === ')') {
|
|
279
|
+
depth--;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return depth > 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Splits a selector into the text around its one and only `:lang()` and the
|
|
287
|
+
* language-codes that `:lang()` asks for. A selector holding no `:lang()`, or
|
|
288
|
+
* more than one, has no single list of codes to vary, and a `:lang()` nested
|
|
289
|
+
* in another functional pseudo-class cannot have codes added to it without
|
|
290
|
+
* changing what the pseudo-class it sits in means.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} selector A minified CSS selector string.
|
|
293
|
+
* @return {object|null} The `prefix`, `codes`, and `suffix` of the selector, or null when it has no lone top-level `:lang()`.
|
|
294
|
+
*/
|
|
295
|
+
function splitAroundLanguageCodes (selector) {
|
|
296
|
+
const languageFunctions = findLanguageFunctions(selector);
|
|
297
|
+
if (languageFunctions.length !== 1) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const {
|
|
301
|
+
closeIndex,
|
|
302
|
+
openIndex,
|
|
303
|
+
startIndex
|
|
304
|
+
} = languageFunctions[0];
|
|
305
|
+
if (isInsideFunction(selector, startIndex)) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const codes = splitLanguageCodes(selector.slice(openIndex + 1, closeIndex)).map((code) => {
|
|
309
|
+
return code.trim();
|
|
310
|
+
});
|
|
311
|
+
if (codes.some((code) => {
|
|
312
|
+
return !code;
|
|
313
|
+
})) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
codes,
|
|
318
|
+
prefix: selector.slice(0, startIndex),
|
|
319
|
+
suffix: selector.slice(closeIndex + 1)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Combines the selectors of one rule that are identical but for the
|
|
325
|
+
* language-codes of a single `:lang()`. A `:lang()` takes a comma-separated
|
|
326
|
+
* list of codes and matches an element written in any of them, so the several
|
|
327
|
+
* selectors say as one what they used to say apart, at the same specificity.
|
|
328
|
+
*
|
|
329
|
+
* @param {Array} selectors The rule's minified selector strings.
|
|
330
|
+
* @return {Array} The selector list with language-only variants combined.
|
|
331
|
+
*/
|
|
332
|
+
function combineLanguageSelectors (selectors) {
|
|
333
|
+
const groupsBySurroundings = new Map();
|
|
334
|
+
const result = [];
|
|
335
|
+
for (const selector of selectors) {
|
|
336
|
+
const parts = splitAroundLanguageCodes(selector);
|
|
337
|
+
if (!parts) {
|
|
338
|
+
result.push(selector);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
// The prefix and suffix are joined by a character no selector may hold, so
|
|
342
|
+
// that text moving from one side to the other cannot look like a match
|
|
343
|
+
const surroundings = parts.prefix + '\u0000' + parts.suffix;
|
|
344
|
+
const existingGroup = groupsBySurroundings.get(surroundings);
|
|
345
|
+
if (existingGroup) {
|
|
346
|
+
existingGroup.codes.push(...parts.codes);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
groupsBySurroundings.set(surroundings, {
|
|
350
|
+
codes: [...parts.codes],
|
|
351
|
+
parts,
|
|
352
|
+
position: result.length
|
|
353
|
+
});
|
|
354
|
+
result.push(selector);
|
|
355
|
+
}
|
|
356
|
+
for (const group of groupsBySurroundings.values()) {
|
|
357
|
+
const codes = [...new Set(group.codes)];
|
|
358
|
+
result[group.position] = group.parts.prefix + LANGUAGE_FUNCTION + codes.join(',') + ')' + group.parts.suffix;
|
|
359
|
+
}
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export {
|
|
364
|
+
combineLanguageSelectors,
|
|
365
|
+
minifyLanguageSelector
|
|
366
|
+
};
|
package/src/rules/optimize.js
CHANGED
|
@@ -13,13 +13,82 @@ import {
|
|
|
13
13
|
normalizeMedia
|
|
14
14
|
} from './normalize.js';
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Reports whether an entry of a rule's body is itself a nested rule rather
|
|
18
|
+
* than a plain declaration.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} entry An entry of a rule's declaration list.
|
|
21
|
+
* @return {boolean} True when the entry is a nested rule.
|
|
22
|
+
*/
|
|
23
|
+
function isNestedRule (entry) {
|
|
24
|
+
return entry.type === 'rule';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Expands the rules nested inside each rule's body, so a chain of wrappers
|
|
29
|
+
* collapses from the inside out and the outermost wrapper is then judged
|
|
30
|
+
* against an already flattened child. A rule's body holds the same kinds of
|
|
31
|
+
* entries a stylesheet does, so the body is expanded by the same rules, and
|
|
32
|
+
* the plain declarations it also holds pass through untouched.
|
|
33
|
+
*
|
|
34
|
+
* @param {Array} rules The AST rule nodes whose bodies may hold nested rules.
|
|
35
|
+
* @return {Array} The rules, each with its body expanded.
|
|
36
|
+
*/
|
|
37
|
+
function expandNestedRuleBodies (rules) {
|
|
38
|
+
return rules.map((rule) => {
|
|
39
|
+
if (rule.type !== 'rule' || !(rule.declarations || []).some(isNestedRule)) {
|
|
40
|
+
return rule;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
...rule,
|
|
44
|
+
declarations: expandPureNestedRules(rule.declarations)
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reports whether a nested child selector still means the same thing once it
|
|
51
|
+
* is written out in full under its parent.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} childSelector The selector of a rule nested inside another.
|
|
54
|
+
* @return {boolean} True when the child can be written as a flat selector.
|
|
55
|
+
*/
|
|
56
|
+
function canFlattenChildSelector (childSelector) {
|
|
57
|
+
const child = childSelector.trim();
|
|
58
|
+
if (!child) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
// A `&` anywhere but the very start puts the parent where flattening cannot:
|
|
62
|
+
// `.b &` means the parent follows the child rather than leading it
|
|
63
|
+
return !child.slice(1).includes('&');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Writes a nested child selector out in full, as the selector it stands for
|
|
68
|
+
* when read through its parent.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} parentSelector The selector of the rule the child is nested in.
|
|
71
|
+
* @param {string} childSelector The selector of the nested child rule.
|
|
72
|
+
* @return {string} The flat selector the nested pair stands for.
|
|
73
|
+
*/
|
|
74
|
+
function combineNestedSelector (parentSelector, childSelector) {
|
|
75
|
+
const parent = parentSelector.trim();
|
|
76
|
+
const child = childSelector.trim();
|
|
77
|
+
// A leading `&` names the parent itself, so it is what the parent replaces
|
|
78
|
+
if (child.startsWith('&')) {
|
|
79
|
+
return parent + child.slice(1);
|
|
80
|
+
}
|
|
81
|
+
return parent + ' ' + child;
|
|
82
|
+
}
|
|
83
|
+
|
|
16
84
|
/**
|
|
17
85
|
* Expands rules that contain only nested sub-rules into flat rules with combined selectors, enabling further merging when the combined selectors already exist elsewhere.
|
|
18
86
|
*
|
|
19
|
-
* @param {Array}
|
|
20
|
-
* @return {Array}
|
|
87
|
+
* @param {Array} inputRules The AST rule nodes to process.
|
|
88
|
+
* @return {Array} A new array of rules with pure-nested rules expanded.
|
|
21
89
|
*/
|
|
22
|
-
function expandPureNestedRules (
|
|
90
|
+
function expandPureNestedRules (inputRules) {
|
|
91
|
+
const rules = expandNestedRuleBodies(inputRules);
|
|
23
92
|
const flatSelectors = new Set();
|
|
24
93
|
for (const rule of rules) {
|
|
25
94
|
if (rule.type !== 'rule' || !rule.selectors?.length) {
|
|
@@ -77,16 +146,14 @@ function expandPureNestedRules (rules) {
|
|
|
77
146
|
canExpand = false;
|
|
78
147
|
break;
|
|
79
148
|
}
|
|
149
|
+
if (!nestedRule.selectors.every(canFlattenChildSelector)) {
|
|
150
|
+
canExpand = false;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
80
153
|
const combinedSelectors = [];
|
|
81
154
|
for (const parentSelector of rule.selectors) {
|
|
82
155
|
for (const childSelector of nestedRule.selectors) {
|
|
83
|
-
const
|
|
84
|
-
let combinedSelector;
|
|
85
|
-
if (trimmedChild.startsWith('&')) {
|
|
86
|
-
combinedSelector = trimmedChild.replace(/^&/, parentSelector.trim());
|
|
87
|
-
} else {
|
|
88
|
-
combinedSelector = parentSelector.trim() + ' ' + trimmedChild;
|
|
89
|
-
}
|
|
156
|
+
const combinedSelector = combineNestedSelector(parentSelector, childSelector);
|
|
90
157
|
combinedSelectors.push(combinedSelector);
|
|
91
158
|
if (flatSelectors.has(combinedSelector)) {
|
|
92
159
|
anyMatch = true;
|
package/src/rules/selectors.js
CHANGED
|
@@ -118,10 +118,11 @@ function findMatchingCloseParenthesis (text, openIndex) {
|
|
|
118
118
|
/**
|
|
119
119
|
* Finds the next occurrence of a pseudo-class function token (e.g. `:is(`)
|
|
120
120
|
* that sits at the top level of a selector, outside any quoted string or
|
|
121
|
-
* attribute selector.
|
|
121
|
+
* attribute selector. Pseudo-class names are case-insensitive, so the token is
|
|
122
|
+
* matched without regard to case and must be given in lowercase.
|
|
122
123
|
*
|
|
123
124
|
* @param {string} text The selector string to scan.
|
|
124
|
-
* @param {string} functionCall The function token to find, including its opening parenthesis.
|
|
125
|
+
* @param {string} functionCall The lowercase function token to find, including its opening parenthesis.
|
|
125
126
|
* @param {number} start The index to start scanning from.
|
|
126
127
|
* @return {number} The index of the next top-level occurrence, or -1 if none remains.
|
|
127
128
|
*/
|
|
@@ -141,7 +142,13 @@ function findNextFunctionCallOutsideStrings (text, functionCall, start) {
|
|
|
141
142
|
index += 2;
|
|
142
143
|
continue;
|
|
143
144
|
}
|
|
144
|
-
|
|
145
|
+
// The token's first character is compared before its whole length is read,
|
|
146
|
+
// so only the few positions that could start it are lowercased
|
|
147
|
+
const startsToken = (
|
|
148
|
+
character === functionCall[0] &&
|
|
149
|
+
text.slice(index, index + functionCall.length).toLowerCase() === functionCall
|
|
150
|
+
);
|
|
151
|
+
if (startsToken) {
|
|
145
152
|
return index;
|
|
146
153
|
}
|
|
147
154
|
index++;
|
|
@@ -876,6 +883,8 @@ function flattenNestingParentIsSelector (selector) {
|
|
|
876
883
|
return parts;
|
|
877
884
|
}
|
|
878
885
|
export {
|
|
886
|
+
findMatchingCloseParenthesis,
|
|
887
|
+
findNextFunctionCallOutsideStrings,
|
|
879
888
|
flattenNestingParentIsSelector,
|
|
880
889
|
mergeAdjacentWherePseudoClasses,
|
|
881
890
|
processIsSelector,
|
package/src/rules/stringify.js
CHANGED
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { processDeclarations } from '../declarations/process.js';
|
|
6
|
-
import {
|
|
6
|
+
import { minifyValueForOutput } from '../value/minify.js';
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
collapseCustomPropertyWhitespace,
|
|
10
10
|
processCustomPropertyComments
|
|
11
11
|
} from './custom-properties.js';
|
|
12
|
+
import {
|
|
13
|
+
combineLanguageSelectors,
|
|
14
|
+
minifyLanguageSelector
|
|
15
|
+
} from './lang.js';
|
|
12
16
|
import {
|
|
13
17
|
canUnwrapSupports,
|
|
14
18
|
normalizeLayerNames,
|
|
@@ -37,7 +41,7 @@ function stringifyDeclarations (declarations) {
|
|
|
37
41
|
return declaration.type !== 'whitespace' && declaration.type !== 'comment' && declaration.property;
|
|
38
42
|
})
|
|
39
43
|
.map((declaration) => {
|
|
40
|
-
return [declaration.property, ':',
|
|
44
|
+
return [declaration.property, ':', minifyValueForOutput(declaration)].join('');
|
|
41
45
|
})
|
|
42
46
|
.join(';');
|
|
43
47
|
}
|
|
@@ -251,6 +255,7 @@ function stringifyRule (rule, context) {
|
|
|
251
255
|
// Remove redundant leading "& " nesting selector
|
|
252
256
|
minified = minified.replace(/^& /, '');
|
|
253
257
|
minified = mergeAdjacentWherePseudoClasses(minified);
|
|
258
|
+
minified = minifyLanguageSelector(minified);
|
|
254
259
|
return minified;
|
|
255
260
|
});
|
|
256
261
|
// When this rule is a nesting parent, its whole selector list is treated
|
|
@@ -267,6 +272,7 @@ function stringifyRule (rule, context) {
|
|
|
267
272
|
return processIsSelector(selector, hasSiblingSelectors);
|
|
268
273
|
});
|
|
269
274
|
uniqueSelectors = [...new Set(uniqueSelectors)];
|
|
275
|
+
uniqueSelectors = combineLanguageSelectors(uniqueSelectors);
|
|
270
276
|
output.push(uniqueSelectors.join(','));
|
|
271
277
|
}
|
|
272
278
|
output.push('{');
|
|
@@ -295,7 +301,7 @@ function stringifyRule (rule, context) {
|
|
|
295
301
|
if (property.startsWith('--')) {
|
|
296
302
|
const syntax = context.registeredCustomPropertySyntax.get(property);
|
|
297
303
|
if (syntax === '"<color>"') {
|
|
298
|
-
value =
|
|
304
|
+
value = minifyValueForOutput(declaration);
|
|
299
305
|
} else {
|
|
300
306
|
const rawValue = declaration.rawValue || declaration.value || '';
|
|
301
307
|
const commentProcessedValue = processCustomPropertyComments(rawValue);
|
|
@@ -323,7 +329,7 @@ function stringifyRule (rule, context) {
|
|
|
323
329
|
}
|
|
324
330
|
}
|
|
325
331
|
} else {
|
|
326
|
-
value =
|
|
332
|
+
value = minifyValueForOutput(declaration);
|
|
327
333
|
}
|
|
328
334
|
return [property, ':', value].join('');
|
|
329
335
|
})
|
|
@@ -362,7 +368,7 @@ function stringifyRule (rule, context) {
|
|
|
362
368
|
return item.type !== 'declaration';
|
|
363
369
|
});
|
|
364
370
|
const renderedDeclarations = mediaDeclarations.map((declaration) => {
|
|
365
|
-
return [unescapeIdent(declaration.property), ':',
|
|
371
|
+
return [unescapeIdent(declaration.property), ':', minifyValueForOutput(declaration)].join('');
|
|
366
372
|
}).join(';');
|
|
367
373
|
const renderedRules = stringifyChildRules(subRules, context);
|
|
368
374
|
const children = [renderedDeclarations, renderedRules].filter(Boolean).join('');
|
|
@@ -440,7 +446,7 @@ function stringifyRule (rule, context) {
|
|
|
440
446
|
return declaration.type !== 'whitespace' && declaration.type !== 'comment';
|
|
441
447
|
})
|
|
442
448
|
?.map((declaration) => {
|
|
443
|
-
return [declaration.property, ':',
|
|
449
|
+
return [declaration.property, ':', minifyValueForOutput(declaration)].join('');
|
|
444
450
|
})
|
|
445
451
|
.join(';') || '';
|
|
446
452
|
output.push(renderedKeyframeDeclarations);
|
|
@@ -558,7 +564,7 @@ function stringifyRule (rule, context) {
|
|
|
558
564
|
return innerDeclaration.type !== 'whitespace' && innerDeclaration.property;
|
|
559
565
|
})
|
|
560
566
|
.map((innerDeclaration) => {
|
|
561
|
-
return [unescapeIdent(innerDeclaration.property), ':',
|
|
567
|
+
return [unescapeIdent(innerDeclaration.property), ':', minifyValueForOutput(innerDeclaration)].join('');
|
|
562
568
|
})
|
|
563
569
|
.join(';');
|
|
564
570
|
if (innerDeclarations) {
|
|
@@ -567,7 +573,7 @@ function stringifyRule (rule, context) {
|
|
|
567
573
|
return [];
|
|
568
574
|
}
|
|
569
575
|
if (declaration.property) {
|
|
570
|
-
return [[unescapeIdent(declaration.property), ':',
|
|
576
|
+
return [[unescapeIdent(declaration.property), ':', minifyValueForOutput(declaration)].join('')];
|
|
571
577
|
}
|
|
572
578
|
return [];
|
|
573
579
|
});
|
package/src/value/math.js
CHANGED
|
@@ -83,6 +83,188 @@ function keepSeparatorAfterDissolvedFunction (replacer) {
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The functions whose arguments are a math expression, where `*` and `/` are
|
|
88
|
+
* arithmetic operators rather than characters of some other syntax.
|
|
89
|
+
*
|
|
90
|
+
* @type {Set<string>}
|
|
91
|
+
*/
|
|
92
|
+
const MATH_FUNCTION_NAMES = new Set([
|
|
93
|
+
'abs',
|
|
94
|
+
'calc',
|
|
95
|
+
'clamp',
|
|
96
|
+
'hypot',
|
|
97
|
+
'max',
|
|
98
|
+
'min',
|
|
99
|
+
'mod',
|
|
100
|
+
'rem',
|
|
101
|
+
'round',
|
|
102
|
+
'sign'
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Matches a single character that may appear in a CSS function name.
|
|
107
|
+
*
|
|
108
|
+
* @type {RegExp}
|
|
109
|
+
*/
|
|
110
|
+
const FUNCTION_NAME_CHARACTER = /[a-zA-Z-]/;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Finds where the function name that ends at an opening parenthesis begins.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} value The CSS value string being scanned.
|
|
116
|
+
* @param {number} openIndex The index of the opening parenthesis.
|
|
117
|
+
* @return {number} The index of the function name's first character.
|
|
118
|
+
*/
|
|
119
|
+
function findFunctionNameStart (value, openIndex) {
|
|
120
|
+
let index = openIndex;
|
|
121
|
+
while (index > 0 && FUNCTION_NAME_CHARACTER.test(value[index - 1])) {
|
|
122
|
+
index--;
|
|
123
|
+
}
|
|
124
|
+
return index;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Finds the parenthesis closing the one at the given index.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} value The CSS value string being scanned.
|
|
131
|
+
* @param {number} openIndex The index of the opening parenthesis.
|
|
132
|
+
* @return {number} The index of the matching closing parenthesis, or -1 when it never closes.
|
|
133
|
+
*/
|
|
134
|
+
function findClosingParenthesis (value, openIndex) {
|
|
135
|
+
let depth = 0;
|
|
136
|
+
for (let index = openIndex; index < value.length; index++) {
|
|
137
|
+
if (value[index] === '(') {
|
|
138
|
+
depth++;
|
|
139
|
+
} else if (value[index] === ')') {
|
|
140
|
+
depth--;
|
|
141
|
+
if (depth === 0) {
|
|
142
|
+
return index;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return -1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Rewrites the contents of every math function in a value, leaving the rest of
|
|
151
|
+
* the value untouched. A `*` only means multiplication inside these functions,
|
|
152
|
+
* so an arithmetic rewrite may only reach the text between their parentheses.
|
|
153
|
+
* The whole of a math function is handed over at once, so a math function
|
|
154
|
+
* nested in another is rewritten as part of its parent.
|
|
155
|
+
*
|
|
156
|
+
* @param {function(string): string} rewriteExpression Receives a math function's contents and returns its replacement.
|
|
157
|
+
* @param {string} value The CSS value string to rewrite.
|
|
158
|
+
* @return {string} The value with every math function's contents rewritten.
|
|
159
|
+
*/
|
|
160
|
+
function rewriteMathFunctionContents (rewriteExpression, value) {
|
|
161
|
+
let result = '';
|
|
162
|
+
let index = 0;
|
|
163
|
+
while (index < value.length) {
|
|
164
|
+
const openIndex = value.indexOf('(', index);
|
|
165
|
+
if (openIndex === -1) {
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
const functionName = value.slice(findFunctionNameStart(value, openIndex), openIndex).toLowerCase();
|
|
169
|
+
const closeIndex = findClosingParenthesis(value, openIndex);
|
|
170
|
+
if (!MATH_FUNCTION_NAMES.has(functionName) || closeIndex === -1) {
|
|
171
|
+
result += value.slice(index, openIndex + 1);
|
|
172
|
+
index = openIndex + 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
result += value.slice(index, openIndex + 1);
|
|
176
|
+
result += rewriteExpression(value.slice(openIndex + 1, closeIndex));
|
|
177
|
+
index = closeIndex;
|
|
178
|
+
}
|
|
179
|
+
return result + value.slice(index);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Matches a multiplication by a decimal below one, capturing the digits after
|
|
184
|
+
* its decimal point and the unit written on it. The trailing lookahead keeps
|
|
185
|
+
* the match away from numbers that only begin this way, such as the exponent
|
|
186
|
+
* of `.5e3` or a unit name that is still being read.
|
|
187
|
+
*
|
|
188
|
+
* @type {RegExp}
|
|
189
|
+
*/
|
|
190
|
+
const FRACTIONAL_MULTIPLIER = /\s*\*\s*0*\.(\d+)([a-z%]*)(?![\w.%(])/gi;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The most decimal places a fraction may be built from while the power of ten
|
|
194
|
+
* it needs is still exactly representable as an integer.
|
|
195
|
+
*
|
|
196
|
+
* @type {number}
|
|
197
|
+
*/
|
|
198
|
+
const MAXIMUM_FRACTION_DIGITS = 15;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Computes the greatest common divisor of two non-negative integers, which is
|
|
202
|
+
* what a fraction has to be divided by to be stated in lowest terms.
|
|
203
|
+
*
|
|
204
|
+
* @param {number} first The first integer.
|
|
205
|
+
* @param {number} second The second integer.
|
|
206
|
+
* @return {number} The greatest common divisor of both integers.
|
|
207
|
+
*/
|
|
208
|
+
function greatestCommonDivisor (first, second) {
|
|
209
|
+
let dividend = first;
|
|
210
|
+
let divisor = second;
|
|
211
|
+
while (divisor) {
|
|
212
|
+
const remainder = dividend % divisor;
|
|
213
|
+
dividend = divisor;
|
|
214
|
+
divisor = remainder;
|
|
215
|
+
}
|
|
216
|
+
return dividend;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Converts the digits behind a decimal point into the whole number that
|
|
221
|
+
* dividing by scales a value exactly as multiplying by that decimal does.
|
|
222
|
+
* The digits `25` (of `.25`) state the fraction 25/100, which reduces to 1/4,
|
|
223
|
+
* so the divisor is `4`. A fraction that does not reduce to a numerator of one
|
|
224
|
+
* cannot be stated as a single division, and so has no divisor to give.
|
|
225
|
+
*
|
|
226
|
+
* @param {string} fractionDigits The digits written after the decimal point.
|
|
227
|
+
* @return {number|null} The equivalent whole number divisor, or null when there is none.
|
|
228
|
+
*/
|
|
229
|
+
function reciprocalOfDecimalFraction (fractionDigits) {
|
|
230
|
+
if (fractionDigits.length > MAXIMUM_FRACTION_DIGITS) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const numerator = Number(fractionDigits);
|
|
234
|
+
const denominator = 10 ** fractionDigits.length;
|
|
235
|
+
const commonFactor = greatestCommonDivisor(numerator, denominator);
|
|
236
|
+
if (numerator / commonFactor !== 1) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return denominator / commonFactor;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Rewrites a multiplication by a decimal below one as a division by the whole
|
|
244
|
+
* number that decimal is the reciprocal of, which is never longer to write and
|
|
245
|
+
* usually shorter (`calc(var(--x)*.25)` → `calc(var(--x)/4)`). A unit written
|
|
246
|
+
* on the multiplier moves onto the divisor, since only how the operand is
|
|
247
|
+
* stated changes.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} expression The contents of a math function.
|
|
250
|
+
* @return {string} The expression with reciprocal multipliers stated as divisions.
|
|
251
|
+
*/
|
|
252
|
+
function convertReciprocalMultiplicationToDivision (expression) {
|
|
253
|
+
return expression.replace(FRACTIONAL_MULTIPLIER, (match, fractionDigits, unit) => {
|
|
254
|
+
const divisor = reciprocalOfDecimalFraction(fractionDigits);
|
|
255
|
+
if (divisor === null) {
|
|
256
|
+
return match;
|
|
257
|
+
}
|
|
258
|
+
const division = '/' + divisor + unit;
|
|
259
|
+
// Compared against the shortest the multiplication could have been written
|
|
260
|
+
const multiplication = '*.' + fractionDigits + unit;
|
|
261
|
+
if (division.length > multiplication.length) {
|
|
262
|
+
return match;
|
|
263
|
+
}
|
|
264
|
+
return division;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
86
268
|
/**
|
|
87
269
|
* The units a folded calc() expression leads with, in the order they are
|
|
88
270
|
* written. Every other unit follows them alphabetically.
|
|
@@ -223,7 +405,7 @@ function tryFoldCalcExpression (expression) {
|
|
|
223
405
|
* @return {string} The value with math functions simplified where possible.
|
|
224
406
|
*/
|
|
225
407
|
function normalizeMathFunctions (value, property, originalValue = '') {
|
|
226
|
-
let result = value;
|
|
408
|
+
let result = rewriteMathFunctionContents(convertReciprocalMultiplicationToDivision, value);
|
|
227
409
|
|
|
228
410
|
// Unwrap calc(1 / (1 / x)) → x (double-reciprocal identity)
|
|
229
411
|
result = result.replace(/calc\(\s*1\s*\/\s*\(\s*1\s*\/\s*([^()]+)\s*\)\s*\)/gi, keepSeparatorAfterDissolvedFunction((match, inner) => {
|
package/src/value/minify.js
CHANGED
|
@@ -838,6 +838,26 @@ const ZERO_INITIAL_PROPERTIES = new Set(['margin', 'padding']);
|
|
|
838
838
|
*/
|
|
839
839
|
const AUTO_INITIAL_PROPERTIES = new Set(['min-width', 'min-height']);
|
|
840
840
|
|
|
841
|
+
/**
|
|
842
|
+
* Properties whose initial value is a single keyword longer than the global
|
|
843
|
+
* `initial` keyword itself. These replacements are applied when serializing
|
|
844
|
+
* declarations, because shorthand builders reconstruct their value from the
|
|
845
|
+
* actual longhand keywords, and a CSS-wide keyword such as `initial` is only
|
|
846
|
+
* valid as a declaration's entire value.
|
|
847
|
+
*
|
|
848
|
+
* @type {Map<string, string>}
|
|
849
|
+
*/
|
|
850
|
+
const LENGTHY_INITIAL_KEYWORDS = new Map([
|
|
851
|
+
['background-clip', 'border-box'],
|
|
852
|
+
['background-origin', 'padding-box'],
|
|
853
|
+
['border-collapse', 'separate'],
|
|
854
|
+
['box-sizing', 'content-box'],
|
|
855
|
+
['mask-clip', 'border-box'],
|
|
856
|
+
['mask-origin', 'border-box'],
|
|
857
|
+
['vertical-align', 'baseline'],
|
|
858
|
+
['writing-mode', 'horizontal-tb']
|
|
859
|
+
]);
|
|
860
|
+
|
|
841
861
|
/**
|
|
842
862
|
* Properties whose value is a time, where a millisecond amount may be worth
|
|
843
863
|
* rewriting in seconds.
|
|
@@ -988,6 +1008,39 @@ function reorderBorderWidthBeforeStyle (value) {
|
|
|
988
1008
|
return value.replace(BORDER_STYLE_BEFORE_WIDTH_PATTERN, '$2 $1');
|
|
989
1009
|
}
|
|
990
1010
|
|
|
1011
|
+
/**
|
|
1012
|
+
* Minifies a declaration's value for output, rewriting its initial keyword to
|
|
1013
|
+
* the shorter global `initial` keyword after shorthand assembly. Both are
|
|
1014
|
+
* equivalent, but `initial` is shorter than these particular keywords and,
|
|
1015
|
+
* being repeated across properties, gives gzip more opportunities to deduplicate it.
|
|
1016
|
+
*
|
|
1017
|
+
* @param {object} declaration The CSS declaration object with property and value fields.
|
|
1018
|
+
* @return {string} The minified value with its initial keyword shortened to `initial` when that saves bytes.
|
|
1019
|
+
*/
|
|
1020
|
+
function minifyValueForOutput (declaration) {
|
|
1021
|
+
const value = minifyValue(declaration);
|
|
1022
|
+
const property = declaration.property;
|
|
1023
|
+
// Custom properties hold an arbitrary token stream that JavaScript can read
|
|
1024
|
+
// back, so their keywords are left exactly as written.
|
|
1025
|
+
if (isCustomProperty(property)) {
|
|
1026
|
+
return value;
|
|
1027
|
+
}
|
|
1028
|
+
// A keyword spelling the property's own initial value is shorter as the
|
|
1029
|
+
// global keyword (box-sizing: content-box -> box-sizing: initial)
|
|
1030
|
+
const initialKeyword = LENGTHY_INITIAL_KEYWORDS.get(property?.toLowerCase());
|
|
1031
|
+
if (!initialKeyword || 'initial'.length >= initialKeyword.length) {
|
|
1032
|
+
return value;
|
|
1033
|
+
}
|
|
1034
|
+
// A trailing `!important` rides along inside the value, so the keyword
|
|
1035
|
+
// comparison and the rewrite only concern what sits in front of it.
|
|
1036
|
+
const isImportant = value.endsWith('!important');
|
|
1037
|
+
const keyword = isImportant ? value.slice(0, value.length - '!important'.length) : value;
|
|
1038
|
+
if (keyword.toLowerCase() !== initialKeyword) {
|
|
1039
|
+
return value;
|
|
1040
|
+
}
|
|
1041
|
+
return 'initial' + (isImportant ? '!important' : '');
|
|
1042
|
+
}
|
|
1043
|
+
|
|
991
1044
|
/**
|
|
992
1045
|
* Applies property-specific optimizations to a CSS value (transition, flex, font,
|
|
993
1046
|
* background, display, scale, border-radius, shorthand collapsing, etc.).
|
|
@@ -1426,5 +1479,6 @@ function minifyValue (declaration) {
|
|
|
1426
1479
|
|
|
1427
1480
|
export {
|
|
1428
1481
|
clearMinifiedValueCache,
|
|
1429
|
-
minifyValue
|
|
1482
|
+
minifyValue,
|
|
1483
|
+
minifyValueForOutput
|
|
1430
1484
|
};
|