@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
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Reads a CSS value as the sequence of tokens that the CSS Syntax tokenizer reads out of it, so that what a value is made of can be judged token by token rather than character by character.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Matches one CSS whitespace character. CSS counts a form feed and a carriage
|
|
7
|
+
* return as whitespace, so the shorter `\s` class would be too broad.
|
|
8
|
+
*
|
|
9
|
+
* @type {RegExp}
|
|
10
|
+
*/
|
|
11
|
+
const WHITESPACE_CHARACTER = /[ \t\n\r\f]/;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Matches one decimal digit, which is what starts the numeric tokens.
|
|
15
|
+
*
|
|
16
|
+
* @type {RegExp}
|
|
17
|
+
*/
|
|
18
|
+
const DIGIT_CHARACTER = /[0-9]/;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Matches one hexadecimal digit, which is what a unicode escape holds.
|
|
22
|
+
*
|
|
23
|
+
* @type {RegExp}
|
|
24
|
+
*/
|
|
25
|
+
const HEX_DIGIT_CHARACTER = /[0-9a-fA-F]/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Matches one character that may start an identifier: a letter, an underscore,
|
|
29
|
+
* or anything outside ASCII.
|
|
30
|
+
*
|
|
31
|
+
* @type {RegExp}
|
|
32
|
+
*/
|
|
33
|
+
const IDENT_START_CHARACTER = /[a-zA-Z_\u0080-\uFFFF]/;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Matches one character that may continue an identifier, which is everything an
|
|
37
|
+
* identifier may start with plus the digits and the hyphen.
|
|
38
|
+
*
|
|
39
|
+
* @type {RegExp}
|
|
40
|
+
*/
|
|
41
|
+
const NAME_CHARACTER = /[-a-zA-Z0-9_\u0080-\uFFFF]/;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {object} CssToken
|
|
45
|
+
* @property {string} type The kind of token, such as `ident`, `number`, or `whitespace`.
|
|
46
|
+
* @property {string} text The exact slice of the value that the token spans.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reports whether the two characters at an index form a valid escape sequence.
|
|
51
|
+
* A backslash escapes the character after it, unless that character is the
|
|
52
|
+
* newline that ends the line.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} text The CSS value being read.
|
|
55
|
+
* @param {number} index The index of the possible backslash.
|
|
56
|
+
* @return {boolean} Whether an escape sequence starts at the index.
|
|
57
|
+
*/
|
|
58
|
+
function startsEscape (text, index) {
|
|
59
|
+
return text[index] === '\\' && text[index + 1] !== '\n';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reads past an escape sequence, which is either up to six hexadecimal digits
|
|
64
|
+
* and the single whitespace character that may end them, or one escaped
|
|
65
|
+
* character.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} text The CSS value being read.
|
|
68
|
+
* @param {number} index The index of the backslash that starts the escape.
|
|
69
|
+
* @return {number} The index just past the escape sequence.
|
|
70
|
+
*/
|
|
71
|
+
function readPastEscape (text, index) {
|
|
72
|
+
let end = index + 1;
|
|
73
|
+
let hexDigitCount = 0;
|
|
74
|
+
while (end < text.length && hexDigitCount < 6 && HEX_DIGIT_CHARACTER.test(text[end])) {
|
|
75
|
+
end++;
|
|
76
|
+
hexDigitCount++;
|
|
77
|
+
}
|
|
78
|
+
if (!hexDigitCount) {
|
|
79
|
+
return Math.min(end + 1, text.length);
|
|
80
|
+
}
|
|
81
|
+
if (end < text.length && WHITESPACE_CHARACTER.test(text[end])) {
|
|
82
|
+
end++;
|
|
83
|
+
}
|
|
84
|
+
return end;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reads past a name, the run of identifier characters and escapes that makes up
|
|
89
|
+
* an identifier, the body of a hash token, or the unit of a dimension.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} text The CSS value being read.
|
|
92
|
+
* @param {number} index The index the name starts at.
|
|
93
|
+
* @return {number} The index just past the name.
|
|
94
|
+
*/
|
|
95
|
+
function readPastName (text, index) {
|
|
96
|
+
let end = index;
|
|
97
|
+
while (end < text.length) {
|
|
98
|
+
if (NAME_CHARACTER.test(text[end])) {
|
|
99
|
+
end++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (startsEscape(text, end)) {
|
|
103
|
+
end = readPastEscape(text, end);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
return end;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Reports whether an identifier starts at an index. A hyphen starts one only
|
|
113
|
+
* when a second hyphen, an identifier character, or an escape follows it, which
|
|
114
|
+
* is what makes `--custom` an identifier and a lone `-` a delimiter.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} text The CSS value being read.
|
|
117
|
+
* @param {number} index The index to test.
|
|
118
|
+
* @return {boolean} Whether an identifier starts at the index.
|
|
119
|
+
*/
|
|
120
|
+
function startsIdent (text, index) {
|
|
121
|
+
const character = text[index];
|
|
122
|
+
if (character === undefined) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (character === '-') {
|
|
126
|
+
const nextCharacter = text[index + 1];
|
|
127
|
+
if (nextCharacter === '-') {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return Boolean(nextCharacter) && (IDENT_START_CHARACTER.test(nextCharacter) || startsEscape(text, index + 1));
|
|
131
|
+
}
|
|
132
|
+
if (IDENT_START_CHARACTER.test(character)) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return startsEscape(text, index);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Reports whether a number starts at an index. A sign or a decimal point only
|
|
140
|
+
* starts one when a digit follows close enough behind it.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} text The CSS value being read.
|
|
143
|
+
* @param {number} index The index to test.
|
|
144
|
+
* @return {boolean} Whether a number starts at the index.
|
|
145
|
+
*/
|
|
146
|
+
function startsNumber (text, index) {
|
|
147
|
+
const character = text[index];
|
|
148
|
+
if (character === undefined) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
if (character === '+' || character === '-') {
|
|
152
|
+
const nextCharacter = text[index + 1] || '';
|
|
153
|
+
const followingCharacter = text[index + 2] || '';
|
|
154
|
+
return DIGIT_CHARACTER.test(nextCharacter) || (nextCharacter === '.' && DIGIT_CHARACTER.test(followingCharacter));
|
|
155
|
+
}
|
|
156
|
+
if (character === '.') {
|
|
157
|
+
return DIGIT_CHARACTER.test(text[index + 1] || '');
|
|
158
|
+
}
|
|
159
|
+
return DIGIT_CHARACTER.test(character);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Reads past the run of digits that starts at an index.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} text The CSS value being read.
|
|
166
|
+
* @param {number} index The index the digits start at.
|
|
167
|
+
* @return {number} The index just past the digits.
|
|
168
|
+
*/
|
|
169
|
+
function readPastDigits (text, index) {
|
|
170
|
+
let end = index;
|
|
171
|
+
while (end < text.length && DIGIT_CHARACTER.test(text[end])) {
|
|
172
|
+
end++;
|
|
173
|
+
}
|
|
174
|
+
return end;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Reads past a number: an optional sign, an integer part, an optional fraction,
|
|
179
|
+
* and an optional scientific exponent.
|
|
180
|
+
*
|
|
181
|
+
* @param {string} text The CSS value being read.
|
|
182
|
+
* @param {number} index The index the number starts at.
|
|
183
|
+
* @return {number} The index just past the number.
|
|
184
|
+
*/
|
|
185
|
+
function readPastNumber (text, index) {
|
|
186
|
+
let end = index;
|
|
187
|
+
if (text[end] === '+' || text[end] === '-') {
|
|
188
|
+
end++;
|
|
189
|
+
}
|
|
190
|
+
end = readPastDigits(text, end);
|
|
191
|
+
if (text[end] === '.' && DIGIT_CHARACTER.test(text[end + 1] || '')) {
|
|
192
|
+
end = readPastDigits(text, end + 1);
|
|
193
|
+
}
|
|
194
|
+
const exponentCharacter = text[end];
|
|
195
|
+
if (exponentCharacter === 'e' || exponentCharacter === 'E') {
|
|
196
|
+
let exponentEnd = end + 1;
|
|
197
|
+
if (text[exponentEnd] === '+' || text[exponentEnd] === '-') {
|
|
198
|
+
exponentEnd++;
|
|
199
|
+
}
|
|
200
|
+
if (DIGIT_CHARACTER.test(text[exponentEnd] || '')) {
|
|
201
|
+
end = readPastDigits(text, exponentEnd);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return end;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Reads the numeric token at an index. A number takes a unit when an identifier
|
|
209
|
+
* follows it and becomes a percentage when a percent sign does.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} text The CSS value being read.
|
|
212
|
+
* @param {number} index The index the number starts at.
|
|
213
|
+
* @return {CssToken} The dimension, percentage, or number token.
|
|
214
|
+
*/
|
|
215
|
+
function readNumericToken (text, index) {
|
|
216
|
+
const numberEnd = readPastNumber(text, index);
|
|
217
|
+
if (startsIdent(text, numberEnd)) {
|
|
218
|
+
const unitEnd = readPastName(text, numberEnd);
|
|
219
|
+
return { type: 'dimension', text: text.slice(index, unitEnd) };
|
|
220
|
+
}
|
|
221
|
+
if (text[numberEnd] === '%') {
|
|
222
|
+
return { type: 'percentage', text: text.slice(index, numberEnd + 1) };
|
|
223
|
+
}
|
|
224
|
+
return { type: 'number', text: text.slice(index, numberEnd) };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Reports whether the contents of a `url()` are quoted, which makes it an
|
|
229
|
+
* ordinary function token holding a string rather than a single url token.
|
|
230
|
+
*
|
|
231
|
+
* @param {string} text The CSS value being read.
|
|
232
|
+
* @param {number} index The index just past the opening parenthesis.
|
|
233
|
+
* @return {boolean} Whether a quoted string opens the parentheses.
|
|
234
|
+
*/
|
|
235
|
+
function holdsQuotedUrl (text, index) {
|
|
236
|
+
let end = index;
|
|
237
|
+
while (end < text.length && WHITESPACE_CHARACTER.test(text[end])) {
|
|
238
|
+
end++;
|
|
239
|
+
}
|
|
240
|
+
return text[end] === '"' || text[end] === '\'';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Reads past the body of an unquoted url token, which runs to the parenthesis
|
|
245
|
+
* that closes it. An unquoted url holds no nested parentheses, so the first
|
|
246
|
+
* closing one ends the token.
|
|
247
|
+
*
|
|
248
|
+
* @param {string} text The CSS value being read.
|
|
249
|
+
* @param {number} index The index just past the opening parenthesis.
|
|
250
|
+
* @return {number} The index just past the closing parenthesis.
|
|
251
|
+
*/
|
|
252
|
+
function readPastUrlBody (text, index) {
|
|
253
|
+
let end = index;
|
|
254
|
+
while (end < text.length) {
|
|
255
|
+
if (startsEscape(text, end)) {
|
|
256
|
+
end = readPastEscape(text, end);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (text[end] === ')') {
|
|
260
|
+
return end + 1;
|
|
261
|
+
}
|
|
262
|
+
end++;
|
|
263
|
+
}
|
|
264
|
+
return end;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Reads the token that an identifier starts. An identifier directly followed by
|
|
269
|
+
* an opening parenthesis is a function token instead, and the `url()` written
|
|
270
|
+
* without quotes is a single token that swallows its own contents.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} text The CSS value being read.
|
|
273
|
+
* @param {number} index The index the identifier starts at.
|
|
274
|
+
* @return {CssToken} The ident, function, or url token.
|
|
275
|
+
*/
|
|
276
|
+
function readIdentLikeToken (text, index) {
|
|
277
|
+
const nameEnd = readPastName(text, index);
|
|
278
|
+
const name = text.slice(index, nameEnd);
|
|
279
|
+
if (text[nameEnd] !== '(') {
|
|
280
|
+
return { type: 'ident', text: name };
|
|
281
|
+
}
|
|
282
|
+
if (name.toLowerCase() === 'url' && !holdsQuotedUrl(text, nameEnd + 1)) {
|
|
283
|
+
const urlEnd = readPastUrlBody(text, nameEnd + 1);
|
|
284
|
+
return { type: 'url', text: text.slice(index, urlEnd) };
|
|
285
|
+
}
|
|
286
|
+
return { type: 'function', text: text.slice(index, nameEnd + 1) };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Reads the string token at an index, which runs to the matching quote and
|
|
291
|
+
* takes escaped quotes in its stride.
|
|
292
|
+
*
|
|
293
|
+
* @param {string} text The CSS value being read.
|
|
294
|
+
* @param {number} index The index of the opening quote.
|
|
295
|
+
* @return {CssToken} The string token.
|
|
296
|
+
*/
|
|
297
|
+
function readStringToken (text, index) {
|
|
298
|
+
const quote = text[index];
|
|
299
|
+
let end = index + 1;
|
|
300
|
+
while (end < text.length) {
|
|
301
|
+
if (startsEscape(text, end)) {
|
|
302
|
+
end = readPastEscape(text, end);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (text[end] === quote) {
|
|
306
|
+
end++;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
if (text[end] === '\n') {
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
end++;
|
|
313
|
+
}
|
|
314
|
+
return { type: 'string', text: text.slice(index, end) };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Reads the run of whitespace at an index as the single token that CSS treats
|
|
319
|
+
* it as.
|
|
320
|
+
*
|
|
321
|
+
* @param {string} text The CSS value being read.
|
|
322
|
+
* @param {number} index The index the whitespace starts at.
|
|
323
|
+
* @return {CssToken} The whitespace token.
|
|
324
|
+
*/
|
|
325
|
+
function readWhitespaceToken (text, index) {
|
|
326
|
+
let end = index;
|
|
327
|
+
while (end < text.length && WHITESPACE_CHARACTER.test(text[end])) {
|
|
328
|
+
end++;
|
|
329
|
+
}
|
|
330
|
+
return { type: 'whitespace', text: text.slice(index, end) };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The characters that stand on their own as structural punctuation, none of
|
|
335
|
+
* which can ever merge with the token beside it.
|
|
336
|
+
*
|
|
337
|
+
* @type {Set<string>}
|
|
338
|
+
*/
|
|
339
|
+
const PUNCTUATION_CHARACTERS = new Set(['(', ')', '[', ']', '{', '}', ',', ':', ';']);
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Reads the single token that starts at an index.
|
|
343
|
+
*
|
|
344
|
+
* @param {string} text The CSS value being read.
|
|
345
|
+
* @param {number} index The index the token starts at.
|
|
346
|
+
* @return {CssToken} The token found at the index.
|
|
347
|
+
*/
|
|
348
|
+
function readToken (text, index) {
|
|
349
|
+
const character = text[index];
|
|
350
|
+
|
|
351
|
+
if (WHITESPACE_CHARACTER.test(character)) {
|
|
352
|
+
return readWhitespaceToken(text, index);
|
|
353
|
+
}
|
|
354
|
+
if (character === '"' || character === '\'') {
|
|
355
|
+
return readStringToken(text, index);
|
|
356
|
+
}
|
|
357
|
+
if (PUNCTUATION_CHARACTERS.has(character)) {
|
|
358
|
+
return { type: 'punctuation', text: character };
|
|
359
|
+
}
|
|
360
|
+
if (character === '#') {
|
|
361
|
+
const startsHash = NAME_CHARACTER.test(text[index + 1] || '') || startsEscape(text, index + 1);
|
|
362
|
+
if (startsHash) {
|
|
363
|
+
return { type: 'hash', text: text.slice(index, readPastName(text, index + 1)) };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (character === '@' && startsIdent(text, index + 1)) {
|
|
367
|
+
return { type: 'at-keyword', text: text.slice(index, readPastName(text, index + 1)) };
|
|
368
|
+
}
|
|
369
|
+
if (startsNumber(text, index)) {
|
|
370
|
+
return readNumericToken(text, index);
|
|
371
|
+
}
|
|
372
|
+
if (startsIdent(text, index)) {
|
|
373
|
+
return readIdentLikeToken(text, index);
|
|
374
|
+
}
|
|
375
|
+
return { type: 'delim', text: character };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Splits a CSS value into the tokens that the CSS Syntax tokenizer would read
|
|
380
|
+
* out of it.
|
|
381
|
+
*
|
|
382
|
+
* @param {string} value The CSS value to tokenize.
|
|
383
|
+
* @return {Array} The tokens the value holds, in the order they appear.
|
|
384
|
+
*/
|
|
385
|
+
function tokenizeCssValue (value) {
|
|
386
|
+
const tokens = [];
|
|
387
|
+
let index = 0;
|
|
388
|
+
while (index < value.length) {
|
|
389
|
+
const token = readToken(value, index);
|
|
390
|
+
tokens.push(token);
|
|
391
|
+
index += Math.max(token.text.length, 1);
|
|
392
|
+
}
|
|
393
|
+
return tokens;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export { tokenizeCssValue };
|
package/src/preprocess.js
CHANGED
|
@@ -6,239 +6,31 @@ import { isUnicodeCharset } from './context.js';
|
|
|
6
6
|
import { resolveUnicodeEscape } from './utilities.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @return {string} The CSS string with common syntax errors corrected.
|
|
14
|
-
*/
|
|
15
|
-
function fixCommonSyntaxErrors (css) {
|
|
16
|
-
let fixed = css;
|
|
17
|
-
|
|
18
|
-
// Fix missing closing braces by analyzing brace balance
|
|
19
|
-
// Count braces while ignoring those inside comments and strings
|
|
20
|
-
let openBraces = 0;
|
|
21
|
-
let inString = false;
|
|
22
|
-
let stringChar = '';
|
|
23
|
-
let inComment = false;
|
|
24
|
-
let commentType = '';
|
|
25
|
-
|
|
26
|
-
for (let i = 0; i < fixed.length; i++) {
|
|
27
|
-
const char = fixed[i];
|
|
28
|
-
const prevChar = i > 0 ? fixed[i - 1] : '';
|
|
29
|
-
|
|
30
|
-
// Handle string state
|
|
31
|
-
if (!inComment && (char === '"' || char === '\'') && prevChar !== '\\') {
|
|
32
|
-
if (!inString) {
|
|
33
|
-
inString = true;
|
|
34
|
-
stringChar = char;
|
|
35
|
-
} else if (char === stringChar) {
|
|
36
|
-
inString = false;
|
|
37
|
-
stringChar = '';
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Handle comment state
|
|
42
|
-
if (!inString) {
|
|
43
|
-
if (!inComment && char === '/' && i + 1 < fixed.length && fixed[i + 1] === '*') {
|
|
44
|
-
inComment = true;
|
|
45
|
-
commentType = '/*';
|
|
46
|
-
i++; // Skip the next character
|
|
47
|
-
} else if (inComment && commentType === '/*' && char === '*' && i + 1 < fixed.length && fixed[i + 1] === '/') {
|
|
48
|
-
inComment = false;
|
|
49
|
-
commentType = '';
|
|
50
|
-
i++; // Skip the next character
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Count braces only when not in strings or comments
|
|
55
|
-
if (!inString && !inComment) {
|
|
56
|
-
if (char === '{') {
|
|
57
|
-
openBraces++;
|
|
58
|
-
} else if (char === '}') {
|
|
59
|
-
openBraces--;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Add missing closing braces
|
|
65
|
-
if (openBraces > 0) {
|
|
66
|
-
fixed += '}'.repeat(openBraces);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Fix unclosed strings (common in malformed CSS)
|
|
70
|
-
if (inString) {
|
|
71
|
-
fixed += stringChar;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Fix unclosed comments
|
|
75
|
-
if (inComment && commentType === '/*') {
|
|
76
|
-
fixed += '*/';
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return fixed;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Converts CSS Unicode escape sequences inside declaration blocks to their literal character equivalents, while preserving control characters that must remain escaped.
|
|
84
|
-
* Also cleans up malformed CSS syntax that would cause parser failures.
|
|
9
|
+
* Converts CSS Unicode escape sequences inside declaration blocks to their
|
|
10
|
+
* literal character equivalents, while preserving the control characters that
|
|
11
|
+
* must remain escaped. A property name is read as written, so an escape in one
|
|
12
|
+
* is resolved before the parse rather than after it.
|
|
85
13
|
*
|
|
86
14
|
* @param {string} css The raw CSS string to preprocess.
|
|
87
|
-
* @return {string} The CSS string with printable Unicode escapes resolved inside declaration blocks
|
|
15
|
+
* @return {string} The CSS string with printable Unicode escapes resolved inside declaration blocks.
|
|
88
16
|
*/
|
|
89
17
|
function preprocessDeclarationBlocks (css) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// Match
|
|
94
|
-
return
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
18
|
+
if (!isUnicodeCharset()) {
|
|
19
|
+
return css;
|
|
20
|
+
}
|
|
21
|
+
// Match the innermost declaration blocks, which hold no block of their own
|
|
22
|
+
return css.replace(/\{([^{}]*)\}/g, (blockMatch, blockContents) => {
|
|
23
|
+
// Match a quoted string, whose escapes are data, or a unicode escape
|
|
24
|
+
// outside one: a backslash, one to six hexadecimal digits, and the
|
|
25
|
+
// optional whitespace that ends the escape
|
|
26
|
+
const resolved = blockContents.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\\([0-9a-fA-F]{1,6})\s?/g, (escapeMatch, hex) => {
|
|
27
|
+
if (!hex) {
|
|
28
|
+
return escapeMatch;
|
|
29
|
+
}
|
|
30
|
+
return resolveUnicodeEscape(hex) ?? escapeMatch;
|
|
100
31
|
});
|
|
101
|
-
|
|
102
|
-
// Then, skip quoted strings and match CSS unicode escapes (backslash + 1-6 hex digits + optional whitespace).
|
|
103
|
-
// Only resolve when the charset is unicode-compatible.
|
|
104
|
-
if (isUnicodeCharset()) {
|
|
105
|
-
processed = processed.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\\([0-9a-fA-F]{1,6})\s?/g, (fullMatch, hex) => {
|
|
106
|
-
if (!hex) {
|
|
107
|
-
return fullMatch;
|
|
108
|
-
}
|
|
109
|
-
return resolveUnicodeEscape(hex) ?? fullMatch;
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return '{' + processed + '}';
|
|
32
|
+
return '{' + resolved + '}';
|
|
114
33
|
});
|
|
115
34
|
}
|
|
116
35
|
|
|
117
|
-
|
|
118
|
-
* Unicode Private Use Area characters used as temporary placeholders
|
|
119
|
-
* for CSS escape sequences during preprocessing, preventing the parser
|
|
120
|
-
* from exceeding its internal escape-counting limit on large files.
|
|
121
|
-
*/
|
|
122
|
-
const ESCAPED_COLON_PLACEHOLDER = '\uE001';
|
|
123
|
-
const ESCAPED_DOT_PLACEHOLDER = '\uE002';
|
|
124
|
-
const ESCAPED_SLASH_PLACEHOLDER = '\uE003';
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Replaces common single-character CSS escape sequences with Unicode Private
|
|
128
|
-
* Use Area placeholder characters to prevent the parser from exceeding its
|
|
129
|
-
* internal escape-counting limit on files with many escaped selectors
|
|
130
|
-
* (e.g. Tailwind-style utility classes like `.sm\:p-0`).
|
|
131
|
-
*
|
|
132
|
-
* Only neutralizes escapes outside of attribute selector brackets `[...]`,
|
|
133
|
-
* quoted strings, and comments so the minifier's quote-vs-escape length
|
|
134
|
-
* comparisons in attribute selectors remain accurate.
|
|
135
|
-
*
|
|
136
|
-
* @param {string} css The raw CSS string to preprocess.
|
|
137
|
-
* @return {string} The CSS with escape sequences replaced by placeholders.
|
|
138
|
-
*/
|
|
139
|
-
function neutralizeEscapeSequences (css) {
|
|
140
|
-
let result = '';
|
|
141
|
-
let insideBrackets = false;
|
|
142
|
-
let insideString = false;
|
|
143
|
-
let stringDelimiter = '';
|
|
144
|
-
let insideComment = false;
|
|
145
|
-
|
|
146
|
-
for (let i = 0; i < css.length; i++) {
|
|
147
|
-
const character = css[i];
|
|
148
|
-
const nextCharacter = i + 1 < css.length ? css[i + 1] : '';
|
|
149
|
-
|
|
150
|
-
// Track block comment state
|
|
151
|
-
if (!insideString && !insideComment && character === '/' && nextCharacter === '*') {
|
|
152
|
-
insideComment = true;
|
|
153
|
-
result += '/*';
|
|
154
|
-
i++;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
if (insideComment) {
|
|
158
|
-
if (character === '*' && nextCharacter === '/') {
|
|
159
|
-
insideComment = false;
|
|
160
|
-
result += '*/';
|
|
161
|
-
i++;
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
result += character;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Track string state (preserve escapes inside strings)
|
|
169
|
-
if (!insideString && (character === '"' || character === '\'')) {
|
|
170
|
-
insideString = true;
|
|
171
|
-
stringDelimiter = character;
|
|
172
|
-
result += character;
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (insideString) {
|
|
176
|
-
if (character === '\\' && nextCharacter) {
|
|
177
|
-
result += character + nextCharacter;
|
|
178
|
-
i++;
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
if (character === stringDelimiter) {
|
|
182
|
-
insideString = false;
|
|
183
|
-
stringDelimiter = '';
|
|
184
|
-
}
|
|
185
|
-
result += character;
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Track attribute selector bracket state
|
|
190
|
-
if (character === '[') {
|
|
191
|
-
insideBrackets = true;
|
|
192
|
-
result += character;
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
if (character === ']') {
|
|
196
|
-
insideBrackets = false;
|
|
197
|
-
result += character;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Replace escape sequences only outside brackets, strings, and comments
|
|
202
|
-
if (!insideBrackets && character === '\\') {
|
|
203
|
-
if (nextCharacter === ':') {
|
|
204
|
-
result += ESCAPED_COLON_PLACEHOLDER;
|
|
205
|
-
i++;
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
if (nextCharacter === '.') {
|
|
209
|
-
result += ESCAPED_DOT_PLACEHOLDER;
|
|
210
|
-
i++;
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
if (nextCharacter === '/') {
|
|
214
|
-
result += ESCAPED_SLASH_PLACEHOLDER;
|
|
215
|
-
i++;
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
result += character;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return result;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Restores the original CSS escape sequences from their Unicode Private Use
|
|
228
|
-
* Area placeholder characters after minification is complete.
|
|
229
|
-
*
|
|
230
|
-
* @param {string} css The minified CSS string with placeholders.
|
|
231
|
-
* @return {string} The CSS with original escape sequences restored.
|
|
232
|
-
*/
|
|
233
|
-
function restoreEscapeSequences (css) {
|
|
234
|
-
return css
|
|
235
|
-
.replaceAll(ESCAPED_COLON_PLACEHOLDER, '\\:')
|
|
236
|
-
.replaceAll(ESCAPED_DOT_PLACEHOLDER, '\\.')
|
|
237
|
-
.replaceAll(ESCAPED_SLASH_PLACEHOLDER, '\\/');
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export {
|
|
241
|
-
neutralizeEscapeSequences,
|
|
242
|
-
preprocessDeclarationBlocks,
|
|
243
|
-
restoreEscapeSequences
|
|
244
|
-
};
|
|
36
|
+
export { preprocessDeclarationBlocks };
|