postcss-normalize-string 9.0.4 → 9.0.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-normalize-string",
3
- "version": "9.0.4",
3
+ "version": "9.0.5",
4
4
  "description": "Normalize wrapping quotes for CSS string literals.",
5
5
  "exports": {
6
6
  ".": {
package/src/index.js CHANGED
@@ -6,6 +6,69 @@ const BACKSLASH = '\\'.charCodeAt(0);
6
6
  const NEWLINE = '\n'.charCodeAt(0);
7
7
  const FEED = '\f'.charCodeAt(0);
8
8
  const CR = '\r'.charCodeAt(0);
9
+ const SPACE = ' '.charCodeAt(0);
10
+ const TAB = '\t'.charCodeAt(0);
11
+
12
+ /**
13
+ * @param {number} code
14
+ * @return {boolean}
15
+ */
16
+ function isHexDigit(code) {
17
+ return (
18
+ (code >= 0x30 && code <= 0x39) ||
19
+ (code >= 0x41 && code <= 0x46) ||
20
+ (code >= 0x61 && code <= 0x66)
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Return the index just past the hex digits of the escape whose backslash is at `pos`.
26
+ * CSS Syntax 3 allows one to six hex digits.
27
+ *
28
+ * @param {string} inner
29
+ * @param {number} pos
30
+ * @return {number}
31
+ */
32
+ function getHexDigitsEnd(inner, pos) {
33
+ const limit = Math.min(pos + 7, inner.length);
34
+ let end = pos + 2;
35
+ while (end < limit && isHexDigit(inner.charCodeAt(end))) {
36
+ end++;
37
+ }
38
+ return end;
39
+ }
40
+
41
+ /**
42
+ * Return the length of the whitespace at `pos` (CRLF counts as one newline), or 0.
43
+ *
44
+ * @param {string} inner
45
+ * @param {number} pos
46
+ * @return {number}
47
+ */
48
+ function getWhitespaceLength(inner, pos) {
49
+ const code = inner.charCodeAt(pos);
50
+ if (code === CR) {
51
+ return inner.charCodeAt(pos + 1) === NEWLINE ? 2 : 1;
52
+ }
53
+ return code === SPACE || code === TAB || code === NEWLINE || code === FEED
54
+ ? 1
55
+ : 0;
56
+ }
57
+
58
+ /**
59
+ * Return the space that keeps an open hex escape from absorbing the code point at `pos`.
60
+ *
61
+ * @param {string} inner
62
+ * @param {number} pos
63
+ * @param {boolean} acceptsMoreDigits
64
+ * @return {string}
65
+ */
66
+ function getHexEscapeDelimiter(inner, pos, acceptsMoreDigits) {
67
+ return getWhitespaceLength(inner, pos) > 0 ||
68
+ (acceptsMoreDigits && isHexDigit(inner.charCodeAt(pos)))
69
+ ? ' '
70
+ : '';
71
+ }
9
72
 
10
73
  /**
11
74
  * Verify whether a string token has matching delimiters and balanced trailing escapes.
@@ -14,20 +77,21 @@ const CR = '\r'.charCodeAt(0);
14
77
  * @return {boolean}
15
78
  */
16
79
  function isClosedString(value) {
17
- if (value.length < 2 || (value[0] !== "'" && value[0] !== '"')) {
80
+ const len = value.length;
81
+ if (len < 2 || value.charCodeAt(len - 1) !== value.charCodeAt(0)) {
18
82
  return false;
19
83
  }
20
84
 
21
85
  let backslashes = 0;
22
86
  for (
23
- let index = value.length - 2;
24
- index >= 0 && value[index] === '\\';
87
+ let index = len - 2;
88
+ index >= 0 && value.charCodeAt(index) === BACKSLASH;
25
89
  index--
26
90
  ) {
27
91
  backslashes++;
28
92
  }
29
93
 
30
- return value.at(-1) === value[0] && backslashes % 2 === 0;
94
+ return backslashes % 2 === 0;
31
95
  }
32
96
 
33
97
  /**
@@ -66,43 +130,30 @@ function scanQuotes(inner, currentQuote) {
66
130
 
67
131
  while (pos < len) {
68
132
  const code = inner.charCodeAt(pos);
69
- if (code === BACKSLASH) {
70
- if (pos + 1 < len) {
71
- const newlineLen = getEscapedNewlineLength(inner, pos, len);
72
- if (newlineLen > 0) {
73
- hasEscapedNewline = true;
74
- pos += newlineLen;
75
- continue;
76
- }
77
- const next = inner.charCodeAt(pos + 1);
78
- if (next === SINGLE_QUOTE) {
79
- singleCount++;
80
- if (currentQuote === '"') {
81
- hasRedundantEscape = true;
82
- }
83
- pos += 2;
84
- continue;
85
- }
86
- if (next === DOUBLE_QUOTE) {
87
- doubleCount++;
88
- if (currentQuote === "'") {
89
- hasRedundantEscape = true;
90
- }
91
- pos += 2;
92
- continue;
93
- }
94
- pos += 2;
95
- continue;
133
+ if (code !== BACKSLASH) {
134
+ if (code === SINGLE_QUOTE) {
135
+ singleCount++;
136
+ } else if (code === DOUBLE_QUOTE) {
137
+ doubleCount++;
96
138
  }
97
139
  pos++;
98
140
  continue;
99
141
  }
100
- if (code === SINGLE_QUOTE) {
142
+ const newlineLen = getEscapedNewlineLength(inner, pos, len);
143
+ if (newlineLen > 0) {
144
+ hasEscapedNewline = true;
145
+ pos += newlineLen;
146
+ continue;
147
+ }
148
+ const next = inner.charCodeAt(pos + 1);
149
+ if (next === SINGLE_QUOTE) {
101
150
  singleCount++;
102
- } else if (code === DOUBLE_QUOTE) {
151
+ hasRedundantEscape ||= currentQuote === '"';
152
+ } else if (next === DOUBLE_QUOTE) {
103
153
  doubleCount++;
154
+ hasRedundantEscape ||= currentQuote === "'";
104
155
  }
105
- pos++;
156
+ pos += 2;
106
157
  }
107
158
 
108
159
  return { singleCount, doubleCount, hasEscapedNewline, hasRedundantEscape };
@@ -151,35 +202,55 @@ function reconstructString(inner, targetQuote) {
151
202
  const chunks = [targetQuote];
152
203
  let cursor = 0;
153
204
  let pos = 0;
205
+ // A hex escape without a whitespace terminator would absorb a following hex
206
+ // digit or whitespace once the line continuations after it are removed.
207
+ let openHexEnd = -1;
208
+ let openHexIsShort = false;
154
209
 
155
210
  while (pos < len) {
156
211
  const code = inner.charCodeAt(pos);
157
212
  if (code === BACKSLASH) {
158
- if (pos + 1 < len) {
159
- const newlineLen = getEscapedNewlineLength(inner, pos, len);
160
- if (newlineLen > 0) {
161
- if (pos > cursor) {
162
- chunks.push(inner.slice(cursor, pos));
163
- }
164
- pos += newlineLen;
165
- cursor = pos;
166
- continue;
213
+ const newlineLen = getEscapedNewlineLength(inner, pos, len);
214
+ if (newlineLen > 0) {
215
+ if (pos > cursor) {
216
+ chunks.push(inner.slice(cursor, pos));
167
217
  }
168
- if (inner.charCodeAt(pos + 1) === otherCode) {
169
- if (pos > cursor) {
170
- chunks.push(inner.slice(cursor, pos));
171
- }
172
- chunks.push(otherQuote);
173
- pos += 2;
174
- cursor = pos;
175
- continue;
218
+ const followsOpenHex = pos === openHexEnd;
219
+ pos += newlineLen;
220
+ cursor = pos;
221
+ if (followsOpenHex) {
222
+ openHexEnd = pos;
223
+ chunks.push(getHexEscapeDelimiter(inner, pos, openHexIsShort));
224
+ }
225
+ continue;
226
+ }
227
+
228
+ const next = inner.charCodeAt(pos + 1);
229
+ if (next === otherCode) {
230
+ if (pos > cursor) {
231
+ chunks.push(inner.slice(cursor, pos));
176
232
  }
233
+ chunks.push(otherQuote);
177
234
  pos += 2;
235
+ cursor = pos;
178
236
  continue;
179
237
  }
180
- pos++;
238
+
239
+ if (isHexDigit(next)) {
240
+ const digitsEnd = getHexDigitsEnd(inner, pos);
241
+ const terminatorLen = getWhitespaceLength(inner, digitsEnd);
242
+ if (terminatorLen === 0) {
243
+ openHexEnd = digitsEnd;
244
+ openHexIsShort = digitsEnd - pos < 7;
245
+ }
246
+ pos = digitsEnd + terminatorLen;
247
+ continue;
248
+ }
249
+
250
+ pos += 2;
181
251
  continue;
182
252
  }
253
+
183
254
  if (code === targetCode) {
184
255
  if (pos > cursor) {
185
256
  chunks.push(inner.slice(cursor, pos));
@@ -189,6 +260,7 @@ function reconstructString(inner, targetQuote) {
189
260
  cursor = pos;
190
261
  continue;
191
262
  }
263
+
192
264
  pos++;
193
265
  }
194
266
 
@@ -208,7 +280,13 @@ function reconstructString(inner, targetQuote) {
208
280
  */
209
281
  function normalizeString(raw, preferredQuote) {
210
282
  const currentQuote = raw[0];
283
+ const targetDelimiter = preferredQuote === 'single' ? "'" : '"';
211
284
  const inner = raw.slice(1, -1);
285
+
286
+ if (currentQuote === targetDelimiter && !inner.includes('\\')) {
287
+ return raw;
288
+ }
289
+
212
290
  const { singleCount, doubleCount, hasEscapedNewline, hasRedundantEscape } =
213
291
  scanQuotes(inner, currentQuote);
214
292
 
@@ -236,25 +314,16 @@ function normalizeString(raw, preferredQuote) {
236
314
  * @return {string}
237
315
  */
238
316
  function normalize(value, preferredQuote) {
239
- if (!value || (!value.includes("'") && !value.includes('"'))) {
240
- return value;
241
- }
242
-
243
317
  const chunks = [];
244
318
  let cursor = 0;
245
- let changed = false;
246
319
  for (const [type, raw, start, end] of tokenize({ css: value })) {
247
- if (type !== TokenType.String) continue;
248
- if (!isClosedString(raw)) continue;
320
+ if (type !== TokenType.String || !isClosedString(raw)) continue;
249
321
  const normalized = normalizeString(raw, preferredQuote);
250
- if (normalized !== raw) {
251
- changed = true;
252
- }
253
- chunks.push(value.slice(cursor, start));
254
- chunks.push(normalized);
322
+ if (normalized === raw) continue;
323
+ chunks.push(value.slice(cursor, start), normalized);
255
324
  cursor = end + 1;
256
325
  }
257
- if (!changed) return value;
326
+ if (cursor === 0) return value;
258
327
  chunks.push(value.slice(cursor));
259
328
  return chunks.join('');
260
329
  }
@@ -266,12 +335,14 @@ function normalize(value, preferredQuote) {
266
335
  * @return {string}
267
336
  */
268
337
  function minify(original, cache, preferredQuote) {
269
- const key = original + '|' + preferredQuote;
270
- if (cache.has(key)) {
271
- return /** @type {string} */ (cache.get(key));
338
+ if (!original || (!original.includes("'") && !original.includes('"'))) {
339
+ return original;
340
+ }
341
+ if (cache.has(original)) {
342
+ return /** @type {string} */ (cache.get(original));
272
343
  }
273
344
  const newValue = normalize(original, preferredQuote);
274
- cache.set(key, newValue);
345
+ cache.set(original, newValue);
275
346
  return newValue;
276
347
  }
277
348
 
@@ -287,13 +358,7 @@ function assignValue(decl, value) {
287
358
  * @return {import('postcss').Plugin}
288
359
  */
289
360
  function pluginCreator(opts) {
290
- const { preferredQuote } = Object.assign(
291
- {},
292
- {
293
- preferredQuote: 'double',
294
- },
295
- opts
296
- );
361
+ const { preferredQuote = 'double' } = opts ?? {};
297
362
 
298
363
  return {
299
364
  postcssPlugin: 'postcss-normalize-string',
@@ -309,17 +374,19 @@ function pluginCreator(opts) {
309
374
  case 'rule':
310
375
  node.selector = minify(node.selector, cache, preferredQuote);
311
376
  break;
312
- case 'decl':
313
- {
314
- const value =
315
- node.raws.value?.value === node.value
316
- ? (node.raws.value.raw ?? node.value)
317
- : node.value;
318
- assignValue(node, minify(value, cache, preferredQuote));
319
- }
377
+ case 'decl': {
378
+ const rawValue = node.raws.value;
379
+ const value =
380
+ rawValue?.value === node.value
381
+ ? (rawValue.raw ?? node.value)
382
+ : node.value;
383
+ assignValue(node, minify(value, cache, preferredQuote));
320
384
  break;
385
+ }
321
386
  case 'atrule':
322
- node.params = minify(node.params, cache, preferredQuote);
387
+ if (node.name.toLowerCase() !== 'charset') {
388
+ node.params = minify(node.params, cache, preferredQuote);
389
+ }
323
390
  break;
324
391
  }
325
392
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AA2RI,YAAkD,OAAO,GAA/C;IAAC,cAAc,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;CAAC,CAAS;AAA7D,gEAAgE;AAChE;;;GAGG;AACH,iBAAS,aAAa,CAAC,IAAI,EAHhB,OAGgB,GAFf,OAAO,SAAS,EAAE,MAAM,CAyCnC;kBAvCQ,aAAa;iBAwCX,IAAI;;AAEf,QAAA,MAAM,aAAa,sBAAgB,CAAC;AAEpC,OAAO,EAAE,aAAa,IAAI,OAAO,EAAE,aAAa,IAAI,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AAkWI,YAAkD,OAAO,GAA/C;IAAC,cAAc,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;CAAC,CAAS;AAA7D,gEAAgE;AAChE;;;GAGG;AACH,iBAAS,aAAa,CAAC,IAAI,EAHhB,OAGgB,GAFf,OAAO,SAAS,EAAE,MAAM,CAqCnC;kBAnCQ,aAAa;iBAoCX,IAAI;;AAEf,QAAA,MAAM,aAAa,sBAAgB,CAAC;AAEpC,OAAO,EAAE,aAAa,IAAI,OAAO,EAAE,aAAa,IAAI,gBAAgB,EAAE,CAAC"}