@thejaredwilcurt/csslop 0.0.38 → 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.
@@ -1,397 +1,14 @@
1
1
  /**
2
- * @file Tokenizes a CSS value the way the CSS Syntax tokenizer does, so that the whitespace which only separated two tokens that already end themselves can be told apart from the whitespace that holds two tokens apart.
2
+ * @file Drops the whitespace of a minified CSS value that only separates two tokens which already mark the boundary between them.
3
3
  */
4
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
- }
5
+ import { tokenizeCssValue } from '../parser/tokenizer.js';
137
6
 
138
7
  /**
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.
8
+ * One token of a value, as the tokenizer reads it.
141
9
  *
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.
10
+ * @typedef {import('../parser/tokenizer.js').CssToken} CssToken
145
11
  */
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
12
 
396
13
  /**
397
14
  * The operators that a math function requires whitespace on both sides of. Both
@@ -2,6 +2,11 @@
2
2
  * @file Minifies CSS transform values by simplifying individual transform functions, collapsing axes, and removing identity components.
3
3
  */
4
4
 
5
+ import {
6
+ findMatchingParenthesis,
7
+ splitTopLevelCommaList
8
+ } from '../parser/source-search.js';
9
+
5
10
  import {
6
11
  normalizeScaleComponent,
7
12
  roundCompactNumber
@@ -17,41 +22,11 @@ function isZeroTransformValue (value) {
17
22
  return /^[-+]?0(?:[a-z%]+)?$/i.test(value.trim());
18
23
  }
19
24
 
20
- /**
21
- * Splits a CSS function's comma-separated argument string into an array, correctly handling nested parentheses.
22
- *
23
- * @param {string} value The raw arguments string inside the function parentheses.
24
- * @return {Array} An array of trimmed argument strings.
25
- */
26
- function splitFunctionArguments (value) {
27
- const parts = [];
28
- let depth = 0;
29
- let current = '';
30
- for (const character of value) {
31
- if (character === '(') {
32
- depth++;
33
- }
34
- if (character === ')') {
35
- depth--;
36
- }
37
- if (character === ',' && depth === 0) {
38
- parts.push(current.trim());
39
- current = '';
40
- } else {
41
- current += character;
42
- }
43
- }
44
- if (current.trim() || parts.length) {
45
- parts.push(current.trim());
46
- }
47
- return parts;
48
- }
49
-
50
25
  /**
51
26
  * Parses a CSS transform value string into an array of objects, each containing a function name and its raw arguments string.
52
27
  *
53
28
  * @param {string} value The full CSS transform value string.
54
- * @return {Array|null} An array of { name, args } objects, or null if the string cannot be parsed.
29
+ * @return {Array|null} An array of { name, arguments } objects, or null if the string cannot be parsed.
55
30
  */
56
31
  function splitTransformFunctions (value) {
57
32
  const parts = [];
@@ -72,23 +47,15 @@ function splitTransformFunctions (value) {
72
47
  if (nameStart === position || value[position] !== '(') {
73
48
  return null;
74
49
  }
75
- const name = value.slice(nameStart, position);
76
- let depth = 1;
77
- let end = position + 1;
78
- while (end < value.length && depth > 0) {
79
- if (value[end] === '(') {
80
- depth++;
81
- }
82
- if (value[end] === ')') {
83
- depth--;
84
- }
85
- end++;
86
- }
87
- if (depth !== 0) {
50
+ const closeIndex = findMatchingParenthesis(value, position);
51
+ if (closeIndex === -1) {
88
52
  return null;
89
53
  }
90
- parts.push({ name, args: value.slice(position + 1, end - 1) });
91
- position = end;
54
+ parts.push({
55
+ name: value.slice(nameStart, position),
56
+ argumentText: value.slice(position + 1, closeIndex)
57
+ });
58
+ position = closeIndex + 1;
92
59
  }
93
60
  return parts;
94
61
  }
@@ -96,13 +63,13 @@ function splitTransformFunctions (value) {
96
63
  /**
97
64
  * Minifies a single CSS transform function by simplifying 3D functions to 2D equivalents, collapsing redundant axes, and normalizing scale percentages.
98
65
  *
99
- * @param {string} name The transform function name (e.g. "translate3d", "scale").
100
- * @param {string} args The raw comma-separated arguments string.
101
- * @return {string} The minified transform function call string.
66
+ * @param {string} name The transform function name (e.g. "translate3d", "scale").
67
+ * @param {string} argumentText The raw comma-separated arguments string.
68
+ * @return {string} The minified transform function call string.
102
69
  */
103
- function minifyTransformFunction (name, args) {
70
+ function minifyTransformFunction (name, argumentText) {
104
71
  const lowerName = name.toLowerCase();
105
- const parts = splitFunctionArguments(args);
72
+ const parts = splitTopLevelCommaList(argumentText);
106
73
 
107
74
  if (lowerName === 'rotate' && parts.length === 1) {
108
75
  // Convert turn units to degrees (e.g. 0.5turn → 180deg)
@@ -214,8 +181,8 @@ function minifyTransformValue (value) {
214
181
  if (!functions) {
215
182
  return value;
216
183
  }
217
- return functions.map(({ name, args }) => {
218
- return minifyTransformFunction(name, args);
184
+ return functions.map(({ name, argumentText }) => {
185
+ return minifyTransformFunction(name, argumentText);
219
186
  }).join(' ');
220
187
  }
221
188
 
@@ -1,144 +0,0 @@
1
- /**
2
- * @file Provides syntax-aware CSS string scanning utilities.
3
- */
4
-
5
- /**
6
- * Finds the closing parenthesis for an opening parenthesis while respecting
7
- * nested parentheses and quoted strings.
8
- *
9
- * @param {string} value The CSS text being scanned.
10
- * @param {number} openParenIndex The index of the opening `(` character.
11
- * @return {number} The closing `)` index, or -1 if unmatched.
12
- */
13
- function findMatchingParenthesis (value, openParenIndex) {
14
- let depth = 1;
15
- let index = openParenIndex + 1;
16
- let activeQuote = '';
17
-
18
- while (index < value.length) {
19
- const character = value[index];
20
- if (activeQuote) {
21
- if (character === '\\') {
22
- index += 2;
23
- continue;
24
- }
25
- if (character === activeQuote) {
26
- activeQuote = '';
27
- }
28
- index++;
29
- continue;
30
- }
31
-
32
- // An escaped character is data rather than a delimiter, even outside a
33
- // quoted string, as in the `\)` of an unquoted url()
34
- if (character === '\\') {
35
- index += 2;
36
- continue;
37
- }
38
-
39
- if (character === '"' || character === '\'') {
40
- activeQuote = character;
41
- index++;
42
- continue;
43
- }
44
-
45
- if (character === '(') {
46
- depth++;
47
- index++;
48
- continue;
49
- }
50
-
51
- if (character === ')') {
52
- depth--;
53
- if (depth === 0) {
54
- return index;
55
- }
56
- }
57
- index++;
58
- }
59
-
60
- return -1;
61
- }
62
-
63
- /**
64
- * Splits a CSS value into its top-level components, keeping parenthesized
65
- * function arguments and quoted strings intact. Components are separated by
66
- * whitespace, or by a `#`, which always starts a hash token and therefore ends
67
- * any component already in progress.
68
- *
69
- * For example, `rgb(0 0 0) red` yields `["rgb(0 0 0)", "red"]`, and the
70
- * minified `red#00f` yields `["red", "#00f"]`.
71
- *
72
- * @param {string} value The CSS value to split.
73
- * @return {Array} The top-level components of the value.
74
- */
75
- function splitTopLevelComponents (value) {
76
- const components = [];
77
- let current = '';
78
- let depth = 0;
79
- let activeQuote = '';
80
- let index = 0;
81
-
82
- while (index < value.length) {
83
- const character = value[index];
84
-
85
- if (activeQuote) {
86
- current += character;
87
- if (character === '\\') {
88
- current += value[index + 1] ?? '';
89
- index += 2;
90
- continue;
91
- }
92
- if (character === activeQuote) {
93
- activeQuote = '';
94
- }
95
- index++;
96
- continue;
97
- }
98
-
99
- if (character === '"' || character === '\'') {
100
- activeQuote = character;
101
- current += character;
102
- index++;
103
- continue;
104
- }
105
-
106
- if (character === '(') {
107
- depth++;
108
- }
109
- if (character === ')' && depth > 0) {
110
- depth--;
111
- }
112
-
113
- // Match any whitespace character, which separates components at depth zero
114
- const isSeparator = depth === 0 && /\s/.test(character);
115
- if (isSeparator) {
116
- if (current) {
117
- components.push(current);
118
- current = '';
119
- }
120
- index++;
121
- continue;
122
- }
123
-
124
- const startsHashToken = character === '#' && depth === 0 && current !== '';
125
- if (startsHashToken) {
126
- components.push(current);
127
- current = '';
128
- }
129
-
130
- current += character;
131
- index++;
132
- }
133
-
134
- if (current) {
135
- components.push(current);
136
- }
137
-
138
- return components;
139
- }
140
-
141
- export {
142
- findMatchingParenthesis,
143
- splitTopLevelComponents
144
- };