@thejaredwilcurt/csslop 0.0.38 → 0.0.40
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 +7 -2
- package/package.json +12 -11
- package/src/declarations/background.js +29 -37
- package/src/declarations/border.js +1 -1
- package/src/declarations/fold.js +1 -1
- package/src/declarations/process.js +17 -8
- package/src/declarations/shorthand-values.js +1 -1
- package/src/index.js +15 -44
- package/src/parser/at-rules.js +291 -0
- package/src/parser/index.js +826 -0
- package/src/parser/positions.js +71 -0
- package/src/parser/scanner.js +229 -0
- package/src/parser/source-search.js +287 -0
- package/src/parser/tokenizer.js +396 -0
- package/src/preprocess.js +20 -228
- package/src/rules/custom-properties.js +86 -9
- package/src/rules/lang.js +20 -56
- package/src/rules/normalize.js +80 -0
- package/src/rules/optimize.js +271 -111
- package/src/rules/selectors.js +56 -221
- package/src/rules/stringify.js +4 -5
- package/src/value/color-mix.js +22 -47
- package/src/value/gradients.js +44 -71
- package/src/value/light-dark.js +9 -94
- package/src/value/math.js +271 -76
- package/src/value/minify.js +89 -54
- package/src/value/relative-colors.js +15 -78
- package/src/value/shared.js +53 -10
- package/src/value/tokens.js +4 -387
- package/src/value/transforms.js +20 -53
- package/src/value/syntax.js +0 -144
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* @file Custom property value whitespace and comment processing for CSS minification.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { tokenizeCssValue } from '../parser/tokenizer.js';
|
|
6
|
+
import { formatCompactNumber } from '../value/shared.js';
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* Removes spaces after commas only inside parenthesized groups (function
|
|
7
10
|
* calls like `var()`, `calc()`), leaving top-level comma spacing intact.
|
|
@@ -34,15 +37,89 @@ function removeSpacesAfterCommasInsideFunctions (value) {
|
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
40
|
+
* The token types whose text begins with a number, and so may hold digits that
|
|
41
|
+
* are written out longer than they need to be.
|
|
42
|
+
*
|
|
43
|
+
* @type {Set<string>}
|
|
44
|
+
*/
|
|
45
|
+
const NUMERIC_TOKEN_TYPES = new Set(['number', 'dimension', 'percentage']);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Splits a numeric token into the decimal number it states and the unit
|
|
49
|
+
* written on it. A number carrying a scientific exponent does not match, since
|
|
50
|
+
* its digits cannot be restated without reading the exponent along with them,
|
|
51
|
+
* and neither does a whole number, which has no zeros to shed.
|
|
52
|
+
*
|
|
53
|
+
* @type {RegExp}
|
|
54
|
+
*/
|
|
55
|
+
const DECIMAL_NUMBER_TOKEN = /^([+-]?(?:\d+\.\d+|\.\d+))([a-zA-Z%]*)$/;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Restates a numeric token as the shortest way of writing the same number,
|
|
59
|
+
* dropping the zeros that lead the decimal point and the ones that trail the
|
|
60
|
+
* digits behind it (`-0.020em` becomes `-.02em`). A token that is not a plain
|
|
61
|
+
* decimal, or whose number is only expressible in scientific notation, is left
|
|
62
|
+
* exactly as it was written.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} tokenText The text of a number, dimension, or percentage token.
|
|
65
|
+
* @return {string} The shortest way of writing that same token.
|
|
66
|
+
*/
|
|
67
|
+
function shortenNumericToken (tokenText) {
|
|
68
|
+
const decimalNumber = tokenText.match(DECIMAL_NUMBER_TOKEN);
|
|
69
|
+
if (!decimalNumber) {
|
|
70
|
+
return tokenText;
|
|
71
|
+
}
|
|
72
|
+
const [, number, unit] = decimalNumber;
|
|
73
|
+
const shortened = formatCompactNumber(number);
|
|
74
|
+
// A number too small or too large to write out plainly comes back in
|
|
75
|
+
// scientific notation, which reads as a different token once a unit follows
|
|
76
|
+
if (shortened.includes('e')) {
|
|
77
|
+
return tokenText;
|
|
78
|
+
}
|
|
79
|
+
return shortened + unit;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Reports whether two token lists say the same thing, which is what tells a
|
|
84
|
+
* rewritten value apart from one whose tokens have run together. Shortening
|
|
85
|
+
* the numbers of `1.0.0` would leave `10`, a single number where two stood.
|
|
86
|
+
*
|
|
87
|
+
* @param {Array} tokens The tokens the value was read as.
|
|
88
|
+
* @param {Array} rewrittenTokens The tokens the rewritten value reads as.
|
|
89
|
+
* @return {boolean} Whether the rewrite left every token boundary in place.
|
|
90
|
+
*/
|
|
91
|
+
function hasSameTokenBoundaries (tokens, rewrittenTokens) {
|
|
92
|
+
if (tokens.length !== rewrittenTokens.length) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return tokens.every((token, index) => {
|
|
96
|
+
return token.type === rewrittenTokens[index].type;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Writes every number in a custom property value the shortest way it can be
|
|
102
|
+
* written. A custom property holds an arbitrary token sequence, so the value
|
|
103
|
+
* is walked token by token: only the tokens that state a number are rewritten,
|
|
104
|
+
* leaving strings, urls, and hash tokens exactly as the author wrote them.
|
|
39
105
|
*
|
|
40
106
|
* @param {string} value The custom property value string.
|
|
41
|
-
* @return {string} The value with
|
|
107
|
+
* @return {string} The value with its numbers shortened.
|
|
42
108
|
*/
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
109
|
+
function shortenNumbers (value) {
|
|
110
|
+
const tokens = tokenizeCssValue(value);
|
|
111
|
+
const rewritten = tokens
|
|
112
|
+
.map((token) => {
|
|
113
|
+
if (!NUMERIC_TOKEN_TYPES.has(token.type)) {
|
|
114
|
+
return token.text;
|
|
115
|
+
}
|
|
116
|
+
return shortenNumericToken(token.text);
|
|
117
|
+
})
|
|
118
|
+
.join('');
|
|
119
|
+
if (!hasSameTokenBoundaries(tokens, tokenizeCssValue(rewritten))) {
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
return rewritten;
|
|
46
123
|
}
|
|
47
124
|
|
|
48
125
|
/**
|
|
@@ -69,7 +146,7 @@ function processCustomPropertyComments (value) {
|
|
|
69
146
|
* Collapses whitespace in a custom property value while preserving
|
|
70
147
|
* token boundaries. Each whitespace sequence is reduced to a single
|
|
71
148
|
* space, spaces after commas inside function calls are removed, and
|
|
72
|
-
*
|
|
149
|
+
* every number is written the shortest way it can be.
|
|
73
150
|
*
|
|
74
151
|
* @param {string} value The raw custom property value string.
|
|
75
152
|
* @return {string} The minified custom property value.
|
|
@@ -79,8 +156,8 @@ function collapseCustomPropertyWhitespace (value) {
|
|
|
79
156
|
let collapsed = value.replace(/\s+/g, ' ');
|
|
80
157
|
// Remove spaces after commas only inside function calls (e.g. var(--bar, 1.5) → var(--bar,1.5))
|
|
81
158
|
collapsed = removeSpacesAfterCommasInsideFunctions(collapsed);
|
|
82
|
-
//
|
|
83
|
-
collapsed =
|
|
159
|
+
// Shorten decimals (e.g. 0.5 → .5, -0.020em → -.02em)
|
|
160
|
+
collapsed = shortenNumbers(collapsed);
|
|
84
161
|
return collapsed;
|
|
85
162
|
}
|
|
86
163
|
export {
|
package/src/rules/lang.js
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
findMatchingParenthesis,
|
|
9
|
+
skipParenthesizedGroup,
|
|
10
|
+
splitTopLevel
|
|
11
|
+
} from '../parser/source-search.js';
|
|
12
|
+
|
|
13
|
+
import { findNextFunctionCallOutsideStrings } from './selectors.js';
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* The `:lang()` pseudo-class opening, as it is written in minified output.
|
|
@@ -79,49 +82,6 @@ function requiresEscape (character) {
|
|
|
79
82
|
);
|
|
80
83
|
}
|
|
81
84
|
|
|
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
85
|
/**
|
|
126
86
|
* Reads the plain language-code out of one `:lang()` argument, stripping the
|
|
127
87
|
* quotes or escapes that were only there to let it be written. Arguments this
|
|
@@ -191,7 +151,7 @@ function formatLanguageCode (code) {
|
|
|
191
151
|
*/
|
|
192
152
|
function minifyLanguageArguments (argumentList) {
|
|
193
153
|
const codes = [];
|
|
194
|
-
for (const argument of
|
|
154
|
+
for (const argument of splitTopLevel(argumentList, ',')) {
|
|
195
155
|
const code = readLanguageCode(argument);
|
|
196
156
|
if (code === null) {
|
|
197
157
|
return null;
|
|
@@ -217,7 +177,7 @@ function findLanguageFunctions (selector) {
|
|
|
217
177
|
break;
|
|
218
178
|
}
|
|
219
179
|
const openIndex = startIndex + LANGUAGE_FUNCTION.length - 1;
|
|
220
|
-
const closeIndex =
|
|
180
|
+
const closeIndex = findMatchingParenthesis(selector, openIndex);
|
|
221
181
|
if (closeIndex === -1) {
|
|
222
182
|
break;
|
|
223
183
|
}
|
|
@@ -271,15 +231,19 @@ function minifyLanguageSelector (selector) {
|
|
|
271
231
|
* @return {boolean} True when an enclosing parenthesis is still open at that position.
|
|
272
232
|
*/
|
|
273
233
|
function isInsideFunction (selector, index) {
|
|
274
|
-
let
|
|
275
|
-
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
234
|
+
let position = 0;
|
|
235
|
+
while (position < index) {
|
|
236
|
+
if (selector[position] !== '(') {
|
|
237
|
+
position++;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const afterGroupIndex = skipParenthesizedGroup(selector, position);
|
|
241
|
+
if (afterGroupIndex > index) {
|
|
242
|
+
return true;
|
|
280
243
|
}
|
|
244
|
+
position = afterGroupIndex;
|
|
281
245
|
}
|
|
282
|
-
return
|
|
246
|
+
return false;
|
|
283
247
|
}
|
|
284
248
|
|
|
285
249
|
/**
|
|
@@ -305,7 +269,7 @@ function splitAroundLanguageCodes (selector) {
|
|
|
305
269
|
if (isInsideFunction(selector, startIndex)) {
|
|
306
270
|
return null;
|
|
307
271
|
}
|
|
308
|
-
const codes =
|
|
272
|
+
const codes = splitTopLevel(selector.slice(openIndex + 1, closeIndex), ',').map((code) => {
|
|
309
273
|
return code.trim();
|
|
310
274
|
});
|
|
311
275
|
if (codes.some((code) => {
|
package/src/rules/normalize.js
CHANGED
|
@@ -48,6 +48,85 @@ function unescapeSelector (selector) {
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Matches a single whitespace character.
|
|
53
|
+
*
|
|
54
|
+
* @type {RegExp}
|
|
55
|
+
*/
|
|
56
|
+
const WHITESPACE_CHARACTER = /\s/;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Collapses every run of whitespace in a selector down to the single space a
|
|
60
|
+
* descendant combinator is written with, and trims the ends. Whitespace held
|
|
61
|
+
* inside a quoted string, or written as an escape, is copied through untouched:
|
|
62
|
+
* there it is part of the value being matched rather than a combinator.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} selector The raw selector string.
|
|
65
|
+
* @return {string} The selector with its combinator whitespace normalized.
|
|
66
|
+
*/
|
|
67
|
+
function normalizeSelectorWhitespace (selector) {
|
|
68
|
+
let normalized = '';
|
|
69
|
+
let quoteDelimiter = '';
|
|
70
|
+
let index = 0;
|
|
71
|
+
while (index < selector.length) {
|
|
72
|
+
const character = selector[index];
|
|
73
|
+
if (quoteDelimiter) {
|
|
74
|
+
if (character === '\\') {
|
|
75
|
+
normalized += selector.slice(index, index + 2);
|
|
76
|
+
index += 2;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (character === quoteDelimiter) {
|
|
80
|
+
quoteDelimiter = '';
|
|
81
|
+
}
|
|
82
|
+
normalized += character;
|
|
83
|
+
index++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (character === '"' || character === '\'') {
|
|
87
|
+
quoteDelimiter = character;
|
|
88
|
+
normalized += character;
|
|
89
|
+
index++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (character === '\\') {
|
|
93
|
+
normalized += selector.slice(index, index + 2);
|
|
94
|
+
index += 2;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (WHITESPACE_CHARACTER.test(character)) {
|
|
98
|
+
normalized += ' ';
|
|
99
|
+
while (index < selector.length && WHITESPACE_CHARACTER.test(selector[index])) {
|
|
100
|
+
index++;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
normalized += character;
|
|
105
|
+
index++;
|
|
106
|
+
}
|
|
107
|
+
return normalized.trim();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Rewrites every selector in a stylesheet into its whitespace-normalized form,
|
|
112
|
+
* descending into nested rules and the bodies of at-rules. Running this before
|
|
113
|
+
* any rule is rewritten means each later pass compares, nests, and prints
|
|
114
|
+
* selectors that are already written the one canonical way, so a selector that
|
|
115
|
+
* happens to be spaced out in the source cannot leak that spacing into a
|
|
116
|
+
* selector the minifier builds from it.
|
|
117
|
+
*
|
|
118
|
+
* @param {Array} rules The AST rule nodes to normalize.
|
|
119
|
+
*/
|
|
120
|
+
function normalizeRuleSelectors (rules) {
|
|
121
|
+
for (const rule of rules || []) {
|
|
122
|
+
if (rule?.selectors?.length) {
|
|
123
|
+
rule.selectors = rule.selectors.map(normalizeSelectorWhitespace);
|
|
124
|
+
}
|
|
125
|
+
normalizeRuleSelectors(rule?.rules);
|
|
126
|
+
normalizeRuleSelectors(rule?.declarations);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
51
130
|
/**
|
|
52
131
|
* Normalizes a `@layer` cascade layer name list by trimming it and removing the
|
|
53
132
|
* optional whitespace that may surround the commas separating the layer names.
|
|
@@ -126,6 +205,7 @@ export {
|
|
|
126
205
|
canUnwrapSupports,
|
|
127
206
|
normalizeLayerNames,
|
|
128
207
|
normalizeMedia,
|
|
208
|
+
normalizeRuleSelectors,
|
|
129
209
|
normalizeSupports,
|
|
130
210
|
unescapeIdent,
|
|
131
211
|
unescapeSelector
|