@thejaredwilcurt/csslop 0.0.37 → 0.0.39
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 +8 -6
- 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 +19 -46
- 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/lang.js +330 -0
- package/src/rules/normalize.js +80 -0
- package/src/rules/optimize.js +260 -120
- package/src/rules/selectors.js +67 -223
- package/src/rules/stringify.js +18 -13
- 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 +163 -1
- package/src/value/minify.js +139 -52
- package/src/value/relative-colors.js +15 -78
- package/src/value/shared.js +1 -0
- package/src/value/tokens.js +4 -387
- package/src/value/transforms.js +20 -53
- package/src/value/syntax.js +0 -144
package/src/value/minify.js
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { isUnicodeCharset } from '../context.js';
|
|
6
|
+
import {
|
|
7
|
+
findMatchingParenthesis,
|
|
8
|
+
skipParenthesizedGroup,
|
|
9
|
+
skipQuotedString,
|
|
10
|
+
startsUrlToken
|
|
11
|
+
} from '../parser/source-search.js';
|
|
6
12
|
import { resolveUnicodeEscape } from '../utilities.js';
|
|
7
13
|
|
|
8
14
|
import { evaluateColorMix } from './color-mix.js';
|
|
@@ -31,12 +37,12 @@ import {
|
|
|
31
37
|
import {
|
|
32
38
|
collapseShorthandParts,
|
|
33
39
|
convertAbsoluteLengthToPx,
|
|
40
|
+
hasSubstitutedParts,
|
|
34
41
|
normalizeScaleComponent,
|
|
35
42
|
parseAlphaString,
|
|
36
43
|
parseAngleToDegrees,
|
|
37
44
|
roundCompactNumber
|
|
38
45
|
} from './shared.js';
|
|
39
|
-
import { findMatchingParenthesis } from './syntax.js';
|
|
40
46
|
import { elideRedundantSeparators } from './tokens.js';
|
|
41
47
|
import { minifyTransformValue } from './transforms.js';
|
|
42
48
|
import { optimizeUnicodeRange } from './unicode-range.js';
|
|
@@ -136,68 +142,24 @@ function splitValueSegments (value) {
|
|
|
136
142
|
const segments = [];
|
|
137
143
|
let index = 0;
|
|
138
144
|
|
|
139
|
-
const consumeQuoted = (start) => {
|
|
140
|
-
const quote = value[start];
|
|
141
|
-
let end = start + 1;
|
|
142
|
-
while (end < value.length) {
|
|
143
|
-
if (value[end] === '\\') {
|
|
144
|
-
end += 2;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (value[end] === quote) {
|
|
148
|
-
end++;
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
end++;
|
|
152
|
-
}
|
|
153
|
-
return end;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
const startsUrl = (start) => {
|
|
157
|
-
return value.slice(start, start + 4).toLowerCase() === 'url(';
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const consumeUrl = (start) => {
|
|
161
|
-
let depth = 1;
|
|
162
|
-
let end = start + 4;
|
|
163
|
-
while (end < value.length && depth > 0) {
|
|
164
|
-
if (value[end] === '"' || value[end] === '\'') {
|
|
165
|
-
end = consumeQuoted(end);
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
// An escaped character is url data, never the parenthesis that ends it
|
|
169
|
-
if (value[end] === '\\') {
|
|
170
|
-
end += 2;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (value[end] === '(') {
|
|
174
|
-
depth++;
|
|
175
|
-
}
|
|
176
|
-
if (value[end] === ')') {
|
|
177
|
-
depth--;
|
|
178
|
-
}
|
|
179
|
-
end++;
|
|
180
|
-
}
|
|
181
|
-
return end;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
145
|
while (index < value.length) {
|
|
185
146
|
if (value[index] === '"' || value[index] === '\'') {
|
|
186
|
-
const end =
|
|
147
|
+
const end = skipQuotedString(value, index);
|
|
187
148
|
segments.push({ text: value.slice(index, end), isLiteral: true });
|
|
188
149
|
index = end;
|
|
189
150
|
continue;
|
|
190
151
|
}
|
|
191
152
|
|
|
192
|
-
if (
|
|
193
|
-
|
|
153
|
+
if (startsUrlToken(value, index)) {
|
|
154
|
+
// The `url(` token opens its parenthesized group at its fourth character
|
|
155
|
+
const end = skipParenthesizedGroup(value, index + 3);
|
|
194
156
|
segments.push({ text: value.slice(index, end), isLiteral: true });
|
|
195
157
|
index = end;
|
|
196
158
|
continue;
|
|
197
159
|
}
|
|
198
160
|
|
|
199
161
|
const start = index;
|
|
200
|
-
while (index < value.length && value[index] !== '"' && value[index] !== '\'' && !
|
|
162
|
+
while (index < value.length && value[index] !== '"' && value[index] !== '\'' && !startsUrlToken(value, index)) {
|
|
201
163
|
index++;
|
|
202
164
|
}
|
|
203
165
|
segments.push({ text: value.slice(start, index), isLiteral: false });
|
|
@@ -836,7 +798,31 @@ const ZERO_INITIAL_PROPERTIES = new Set(['margin', 'padding']);
|
|
|
836
798
|
*
|
|
837
799
|
* @type {Set<string>}
|
|
838
800
|
*/
|
|
839
|
-
const AUTO_INITIAL_PROPERTIES = new Set([
|
|
801
|
+
const AUTO_INITIAL_PROPERTIES = new Set([
|
|
802
|
+
'min-width',
|
|
803
|
+
'min-height',
|
|
804
|
+
'hyphenate-limit-chars'
|
|
805
|
+
]);
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Properties whose initial value is a single keyword longer than the global
|
|
809
|
+
* `initial` keyword itself. These replacements are applied when serializing
|
|
810
|
+
* declarations, because shorthand builders reconstruct their value from the
|
|
811
|
+
* actual longhand keywords, and a CSS-wide keyword such as `initial` is only
|
|
812
|
+
* valid as a declaration's entire value.
|
|
813
|
+
*
|
|
814
|
+
* @type {Map<string, string>}
|
|
815
|
+
*/
|
|
816
|
+
const LENGTHY_INITIAL_KEYWORDS = new Map([
|
|
817
|
+
['background-clip', 'border-box'],
|
|
818
|
+
['background-origin', 'padding-box'],
|
|
819
|
+
['border-collapse', 'separate'],
|
|
820
|
+
['box-sizing', 'content-box'],
|
|
821
|
+
['mask-clip', 'border-box'],
|
|
822
|
+
['mask-origin', 'border-box'],
|
|
823
|
+
['vertical-align', 'baseline'],
|
|
824
|
+
['writing-mode', 'horizontal-tb']
|
|
825
|
+
]);
|
|
840
826
|
|
|
841
827
|
/**
|
|
842
828
|
* Properties whose value is a time, where a millisecond amount may be worth
|
|
@@ -988,6 +974,102 @@ function reorderBorderWidthBeforeStyle (value) {
|
|
|
988
974
|
return value.replace(BORDER_STYLE_BEFORE_WIDTH_PATTERN, '$2 $1');
|
|
989
975
|
}
|
|
990
976
|
|
|
977
|
+
/**
|
|
978
|
+
* Matches a trailing `!important` flag, so the components in front of it can be
|
|
979
|
+
* rewritten without the flag being mistaken for part of the last one.
|
|
980
|
+
*
|
|
981
|
+
* @type {RegExp}
|
|
982
|
+
*/
|
|
983
|
+
const IMPORTANT_SUFFIX_PATTERN = /!important$/i;
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Separates a value from the `!important` flag it may end with.
|
|
987
|
+
*
|
|
988
|
+
* @param {string} value The declaration value, possibly flagged important.
|
|
989
|
+
* @return {object} The components in front of the flag, and the flag itself or an empty string.
|
|
990
|
+
*/
|
|
991
|
+
function splitImportantSuffix (value) {
|
|
992
|
+
const match = value.match(IMPORTANT_SUFFIX_PATTERN);
|
|
993
|
+
if (!match) {
|
|
994
|
+
return { components: value, importantSuffix: '' };
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
components: value.slice(0, match.index).trim(),
|
|
998
|
+
importantSuffix: match[0]
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Collapses a `hyphenate-limit-chars` value to its shortest equivalent form.
|
|
1004
|
+
* The property states up to three components: the shortest word that may be
|
|
1005
|
+
* hyphenated, then the fewest characters allowed before a hyphen, then the
|
|
1006
|
+
* fewest allowed after it. An unstated "before" component is read as `auto`,
|
|
1007
|
+
* and an unstated "after" component repeats whatever "before" holds, so a
|
|
1008
|
+
* trailing component that only restates the default it stands in for is
|
|
1009
|
+
* redundant.
|
|
1010
|
+
*
|
|
1011
|
+
* @param {string} value The `hyphenate-limit-chars` value.
|
|
1012
|
+
* @return {string} The value with its redundant trailing components dropped.
|
|
1013
|
+
*/
|
|
1014
|
+
function collapseHyphenateLimitChars (value) {
|
|
1015
|
+
const {
|
|
1016
|
+
components,
|
|
1017
|
+
importantSuffix
|
|
1018
|
+
} = splitImportantSuffix(value);
|
|
1019
|
+
const parts = components.split(' ').filter(Boolean);
|
|
1020
|
+
if (hasSubstitutedParts(parts)) {
|
|
1021
|
+
return value;
|
|
1022
|
+
}
|
|
1023
|
+
const isAfterMatchingBefore = (
|
|
1024
|
+
parts.length === 3 &&
|
|
1025
|
+
parts[2].toLowerCase() === parts[1].toLowerCase()
|
|
1026
|
+
);
|
|
1027
|
+
if (isAfterMatchingBefore) {
|
|
1028
|
+
parts.pop();
|
|
1029
|
+
}
|
|
1030
|
+
const isBeforeLeftAtDefault = (
|
|
1031
|
+
parts.length === 2 &&
|
|
1032
|
+
parts[1].toLowerCase() === 'auto'
|
|
1033
|
+
);
|
|
1034
|
+
if (isBeforeLeftAtDefault) {
|
|
1035
|
+
parts.pop();
|
|
1036
|
+
}
|
|
1037
|
+
return parts.join(' ') + importantSuffix;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Minifies a declaration's value for output, rewriting its initial keyword to
|
|
1042
|
+
* the shorter global `initial` keyword after shorthand assembly. Both are
|
|
1043
|
+
* equivalent, but `initial` is shorter than these particular keywords and,
|
|
1044
|
+
* being repeated across properties, gives gzip more opportunities to deduplicate it.
|
|
1045
|
+
*
|
|
1046
|
+
* @param {object} declaration The CSS declaration object with property and value fields.
|
|
1047
|
+
* @return {string} The minified value with its initial keyword shortened to `initial` when that saves bytes.
|
|
1048
|
+
*/
|
|
1049
|
+
function minifyValueForOutput (declaration) {
|
|
1050
|
+
const value = minifyValue(declaration);
|
|
1051
|
+
const property = declaration.property;
|
|
1052
|
+
// Custom properties hold an arbitrary token stream that JavaScript can read
|
|
1053
|
+
// back, so their keywords are left exactly as written.
|
|
1054
|
+
if (isCustomProperty(property)) {
|
|
1055
|
+
return value;
|
|
1056
|
+
}
|
|
1057
|
+
// A keyword spelling the property's own initial value is shorter as the
|
|
1058
|
+
// global keyword (box-sizing: content-box -> box-sizing: initial)
|
|
1059
|
+
const initialKeyword = LENGTHY_INITIAL_KEYWORDS.get(property?.toLowerCase());
|
|
1060
|
+
if (!initialKeyword || 'initial'.length >= initialKeyword.length) {
|
|
1061
|
+
return value;
|
|
1062
|
+
}
|
|
1063
|
+
// A trailing `!important` rides along inside the value, so the keyword
|
|
1064
|
+
// comparison and the rewrite only concern what sits in front of it.
|
|
1065
|
+
const isImportant = value.endsWith('!important');
|
|
1066
|
+
const keyword = isImportant ? value.slice(0, value.length - '!important'.length) : value;
|
|
1067
|
+
if (keyword.toLowerCase() !== initialKeyword) {
|
|
1068
|
+
return value;
|
|
1069
|
+
}
|
|
1070
|
+
return 'initial' + (isImportant ? '!important' : '');
|
|
1071
|
+
}
|
|
1072
|
+
|
|
991
1073
|
/**
|
|
992
1074
|
* Applies property-specific optimizations to a CSS value (transition, flex, font,
|
|
993
1075
|
* background, display, scale, border-radius, shorthand collapsing, etc.).
|
|
@@ -1248,6 +1330,10 @@ function applyPropertyOptimizations (val, property) {
|
|
|
1248
1330
|
val = collapseShorthandParts(val.split(' ')).join(' ');
|
|
1249
1331
|
}
|
|
1250
1332
|
|
|
1333
|
+
if (property === 'hyphenate-limit-chars') {
|
|
1334
|
+
val = collapseHyphenateLimitChars(val);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1251
1337
|
if (property === 'border-radius') {
|
|
1252
1338
|
const segments = val.split('/').map((segment) => {
|
|
1253
1339
|
return segment.trim();
|
|
@@ -1426,5 +1512,6 @@ function minifyValue (declaration) {
|
|
|
1426
1512
|
|
|
1427
1513
|
export {
|
|
1428
1514
|
clearMinifiedValueCache,
|
|
1429
|
-
minifyValue
|
|
1515
|
+
minifyValue,
|
|
1516
|
+
minifyValueForOutput
|
|
1430
1517
|
};
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* @file Evaluates and minifies CSS relative color syntax.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
findMatchingParenthesis,
|
|
7
|
+
findTopLevelDelimiter,
|
|
8
|
+
splitTopLevelComponents
|
|
9
|
+
} from '../parser/source-search.js';
|
|
10
|
+
|
|
5
11
|
import {
|
|
6
12
|
parseColor,
|
|
7
13
|
rgbaToHex,
|
|
@@ -48,69 +54,6 @@ const RELATIVE_COLOR_CHANNELS = {
|
|
|
48
54
|
oklch: ['l', 'c', 'h']
|
|
49
55
|
};
|
|
50
56
|
|
|
51
|
-
/**
|
|
52
|
-
* Finds the closing parenthesis matching the opening one at the given index,
|
|
53
|
-
* accounting for nested parentheses.
|
|
54
|
-
*
|
|
55
|
-
* @param {string} value The string being scanned.
|
|
56
|
-
* @param {number} openIndex Index of the opening parenthesis.
|
|
57
|
-
* @return {number} Index of the matching close parenthesis, or -1.
|
|
58
|
-
*/
|
|
59
|
-
function findClosingParenthesis (value, openIndex) {
|
|
60
|
-
let depth = 1;
|
|
61
|
-
let index = openIndex + 1;
|
|
62
|
-
while (index < value.length) {
|
|
63
|
-
const character = value[index];
|
|
64
|
-
if (character === '(') {
|
|
65
|
-
depth++;
|
|
66
|
-
} else if (character === ')') {
|
|
67
|
-
depth--;
|
|
68
|
-
if (depth === 0) {
|
|
69
|
-
return index;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
index++;
|
|
73
|
-
}
|
|
74
|
-
return -1;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Splits a string on top-level whitespace while treating each parenthesized
|
|
79
|
-
* group as part of a single token, keeping nested function arguments intact.
|
|
80
|
-
*
|
|
81
|
-
* @param {string} text The text to split.
|
|
82
|
-
* @return {Array} The list of top-level tokens.
|
|
83
|
-
*/
|
|
84
|
-
function splitTopLevelWhitespace (text) {
|
|
85
|
-
const tokens = [];
|
|
86
|
-
let current = '';
|
|
87
|
-
let depth = 0;
|
|
88
|
-
for (let index = 0; index < text.length; index++) {
|
|
89
|
-
const character = text[index];
|
|
90
|
-
if (character === '(') {
|
|
91
|
-
depth++;
|
|
92
|
-
current += character;
|
|
93
|
-
} else if (character === ')') {
|
|
94
|
-
if (depth > 0) {
|
|
95
|
-
depth--;
|
|
96
|
-
}
|
|
97
|
-
current += character;
|
|
98
|
-
// Match any single whitespace character at the top level
|
|
99
|
-
} else if (depth === 0 && /\s/.test(character)) {
|
|
100
|
-
if (current) {
|
|
101
|
-
tokens.push(current);
|
|
102
|
-
current = '';
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
current += character;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (current) {
|
|
109
|
-
tokens.push(current);
|
|
110
|
-
}
|
|
111
|
-
return tokens;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
57
|
/**
|
|
115
58
|
* Splits a relative-color body into its channel portion and its optional alpha
|
|
116
59
|
* portion at the top-level `/` separator.
|
|
@@ -119,20 +62,14 @@ function splitTopLevelWhitespace (text) {
|
|
|
119
62
|
* @return {Array} A two-element array of [channelsPart, alphaPart|null].
|
|
120
63
|
*/
|
|
121
64
|
function splitRelativeAlpha (body) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (character === '(') {
|
|
126
|
-
depth++;
|
|
127
|
-
} else if (character === ')') {
|
|
128
|
-
if (depth > 0) {
|
|
129
|
-
depth--;
|
|
130
|
-
}
|
|
131
|
-
} else if (character === '/' && depth === 0) {
|
|
132
|
-
return [body.slice(0, index).trim(), body.slice(index + 1).trim()];
|
|
133
|
-
}
|
|
65
|
+
const separatorIndex = findTopLevelDelimiter(body, '/', 0);
|
|
66
|
+
if (separatorIndex === -1) {
|
|
67
|
+
return [body.trim(), null];
|
|
134
68
|
}
|
|
135
|
-
return [
|
|
69
|
+
return [
|
|
70
|
+
body.slice(0, separatorIndex).trim(),
|
|
71
|
+
body.slice(separatorIndex + 1).trim()
|
|
72
|
+
];
|
|
136
73
|
}
|
|
137
74
|
|
|
138
75
|
/**
|
|
@@ -276,7 +213,7 @@ function rewriteRelativeColor (functionName, inner) {
|
|
|
276
213
|
// Strip the leading `from` keyword, allowing it to abut the base color
|
|
277
214
|
const body = inner.replace(/^\s*from\b\s*/i, '').trim();
|
|
278
215
|
const [channelsPart, alphaRaw] = splitRelativeAlpha(body);
|
|
279
|
-
const tokens =
|
|
216
|
+
const tokens = splitTopLevelComponents(channelsPart);
|
|
280
217
|
if (tokens.length < 2) {
|
|
281
218
|
return functionName + '(' + inner + ')';
|
|
282
219
|
}
|
|
@@ -316,7 +253,7 @@ function minifyRelativeColorSyntax (value) {
|
|
|
316
253
|
if (match && !precededByIdentifier) {
|
|
317
254
|
const functionName = match[1].toLowerCase();
|
|
318
255
|
const openParenIndex = index + match[1].length;
|
|
319
|
-
const closingParenIndex =
|
|
256
|
+
const closingParenIndex = findMatchingParenthesis(value, openParenIndex);
|
|
320
257
|
if (closingParenIndex !== -1) {
|
|
321
258
|
const inner = value.slice(openParenIndex + 1, closingParenIndex);
|
|
322
259
|
result += rewriteRelativeColor(functionName, inner);
|