postcss-normalize-string 9.0.3 → 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 +2 -2
- package/src/index.js +291 -214
- package/types/index.d.ts +0 -27
- package/types/index.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postcss-normalize-string",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.5",
|
|
4
4
|
"description": "Normalize wrapping quotes for CSS string literals.",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"url": "git+https://github.com/cssnano/cssnano.git"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"
|
|
36
|
+
"@csstools/css-tokenizer": "^4.0.2"
|
|
37
37
|
},
|
|
38
38
|
"bugs": {
|
|
39
39
|
"url": "https://github.com/cssnano/cssnano/issues"
|
package/src/index.js
CHANGED
|
@@ -1,233 +1,311 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
* Constants (parser usage)
|
|
5
|
-
*/
|
|
1
|
+
import { tokenize, TokenType } from '@csstools/css-tokenizer';
|
|
6
2
|
|
|
7
3
|
const SINGLE_QUOTE = "'".charCodeAt(0);
|
|
8
4
|
const DOUBLE_QUOTE = '"'.charCodeAt(0);
|
|
9
5
|
const BACKSLASH = '\\'.charCodeAt(0);
|
|
10
6
|
const NEWLINE = '\n'.charCodeAt(0);
|
|
11
|
-
const SPACE = ' '.charCodeAt(0);
|
|
12
7
|
const FEED = '\f'.charCodeAt(0);
|
|
13
|
-
const TAB = '\t'.charCodeAt(0);
|
|
14
8
|
const CR = '\r'.charCodeAt(0);
|
|
9
|
+
const SPACE = ' '.charCodeAt(0);
|
|
10
|
+
const TAB = '\t'.charCodeAt(0);
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* Constants (node type strings)
|
|
12
|
+
/**
|
|
13
|
+
* @param {number} code
|
|
14
|
+
* @return {boolean}
|
|
20
15
|
*/
|
|
16
|
+
function isHexDigit(code) {
|
|
17
|
+
return (
|
|
18
|
+
(code >= 0x30 && code <= 0x39) ||
|
|
19
|
+
(code >= 0x41 && code <= 0x46) ||
|
|
20
|
+
(code >= 0x61 && code <= 0x66)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
}
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
*
|
|
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}
|
|
32
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
|
+
}
|
|
33
57
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
}
|
|
37
72
|
|
|
38
|
-
|
|
39
|
-
*
|
|
73
|
+
/**
|
|
74
|
+
* Verify whether a string token has matching delimiters and balanced trailing escapes.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} value
|
|
77
|
+
* @return {boolean}
|
|
40
78
|
*/
|
|
79
|
+
function isClosedString(value) {
|
|
80
|
+
const len = value.length;
|
|
81
|
+
if (len < 2 || value.charCodeAt(len - 1) !== value.charCodeAt(0)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
41
84
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
85
|
+
let backslashes = 0;
|
|
86
|
+
for (
|
|
87
|
+
let index = len - 2;
|
|
88
|
+
index >= 0 && value.charCodeAt(index) === BACKSLASH;
|
|
89
|
+
index--
|
|
90
|
+
) {
|
|
91
|
+
backslashes++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return backslashes % 2 === 0;
|
|
95
|
+
}
|
|
47
96
|
|
|
48
|
-
/** @typedef {typeof T_ESCAPED_SINGLE_QUOTE | typeof T_ESCAPED_DOUBLE_QUOTE | typeof T_SINGLE_QUOTE | typeof T_NEWLINE} StringAstNode */
|
|
49
97
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
98
|
+
* Return the length of an escaped line continuation, or 0 if not a newline escape.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} inner
|
|
101
|
+
* @param {number} pos
|
|
102
|
+
* @param {number} len
|
|
103
|
+
* @return {number}
|
|
53
104
|
*/
|
|
105
|
+
function getEscapedNewlineLength(inner, pos, len) {
|
|
106
|
+
const next = inner.charCodeAt(pos + 1);
|
|
107
|
+
if (next === NEWLINE || next === FEED) {
|
|
108
|
+
return 2;
|
|
109
|
+
}
|
|
110
|
+
if (next === CR) {
|
|
111
|
+
return pos + 2 < len && inner.charCodeAt(pos + 2) === NEWLINE ? 3 : 2;
|
|
112
|
+
}
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
54
115
|
|
|
55
116
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
117
|
+
* Scan string contents to count quotes and detect escaped newlines or redundant escapes.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} inner
|
|
120
|
+
* @param {string} currentQuote
|
|
121
|
+
* @return {{ singleCount: number, doubleCount: number, hasEscapedNewline: boolean, hasRedundantEscape: boolean }}
|
|
58
122
|
*/
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
123
|
+
function scanQuotes(inner, currentQuote) {
|
|
124
|
+
const len = inner.length;
|
|
125
|
+
let singleCount = 0;
|
|
126
|
+
let doubleCount = 0;
|
|
127
|
+
let hasEscapedNewline = false;
|
|
128
|
+
let hasRedundantEscape = false;
|
|
129
|
+
let pos = 0;
|
|
130
|
+
|
|
131
|
+
while (pos < len) {
|
|
132
|
+
const code = inner.charCodeAt(pos);
|
|
133
|
+
if (code !== BACKSLASH) {
|
|
134
|
+
if (code === SINGLE_QUOTE) {
|
|
135
|
+
singleCount++;
|
|
136
|
+
} else if (code === DOUBLE_QUOTE) {
|
|
137
|
+
doubleCount++;
|
|
138
|
+
}
|
|
139
|
+
pos++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const newlineLen = getEscapedNewlineLength(inner, pos, len);
|
|
143
|
+
if (newlineLen > 0) {
|
|
144
|
+
hasEscapedNewline = true;
|
|
145
|
+
pos += newlineLen;
|
|
146
|
+
continue;
|
|
65
147
|
}
|
|
148
|
+
const next = inner.charCodeAt(pos + 1);
|
|
149
|
+
if (next === SINGLE_QUOTE) {
|
|
150
|
+
singleCount++;
|
|
151
|
+
hasRedundantEscape ||= currentQuote === '"';
|
|
152
|
+
} else if (next === DOUBLE_QUOTE) {
|
|
153
|
+
doubleCount++;
|
|
154
|
+
hasRedundantEscape ||= currentQuote === "'";
|
|
155
|
+
}
|
|
156
|
+
pos += 2;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { singleCount, doubleCount, hasEscapedNewline, hasRedundantEscape };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Determine the optimal wrapping quote delimiter.
|
|
164
|
+
*
|
|
165
|
+
* @param {number} singleCount
|
|
166
|
+
* @param {number} doubleCount
|
|
167
|
+
* @param {'single' | 'double'} preferredQuote
|
|
168
|
+
* @param {string} currentQuote
|
|
169
|
+
* @return {string}
|
|
170
|
+
*/
|
|
171
|
+
function selectTargetQuote(
|
|
172
|
+
singleCount,
|
|
173
|
+
doubleCount,
|
|
174
|
+
preferredQuote,
|
|
175
|
+
currentQuote
|
|
176
|
+
) {
|
|
177
|
+
if (singleCount > doubleCount) {
|
|
178
|
+
return '"';
|
|
179
|
+
}
|
|
180
|
+
if (doubleCount > singleCount) {
|
|
181
|
+
return "'";
|
|
182
|
+
}
|
|
183
|
+
if (singleCount === 0 && doubleCount === 0) {
|
|
184
|
+
return preferredQuote === 'single' ? "'" : '"';
|
|
66
185
|
}
|
|
67
|
-
return
|
|
186
|
+
return currentQuote;
|
|
68
187
|
}
|
|
69
188
|
|
|
70
189
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
190
|
+
* Reconstruct string with the target quote, collapsing newlines and updating escapes.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} inner
|
|
193
|
+
* @param {string} targetQuote
|
|
194
|
+
* @return {string}
|
|
73
195
|
*/
|
|
74
|
-
function
|
|
75
|
-
|
|
196
|
+
function reconstructString(inner, targetQuote) {
|
|
197
|
+
const len = inner.length;
|
|
198
|
+
const targetCode = targetQuote.charCodeAt(0);
|
|
199
|
+
const otherQuote = targetQuote === "'" ? '"' : "'";
|
|
200
|
+
const otherCode = otherQuote.charCodeAt(0);
|
|
201
|
+
const escapedTargetQuote = '\\' + targetQuote;
|
|
202
|
+
const chunks = [targetQuote];
|
|
203
|
+
let cursor = 0;
|
|
76
204
|
let pos = 0;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
nodes: [],
|
|
82
|
-
types: {
|
|
83
|
-
escapedSingleQuote: 0,
|
|
84
|
-
escapedDoubleQuote: 0,
|
|
85
|
-
singleQuote: 0,
|
|
86
|
-
doubleQuote: 0,
|
|
87
|
-
},
|
|
88
|
-
quotes: false,
|
|
89
|
-
};
|
|
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;
|
|
90
209
|
|
|
91
210
|
while (pos < len) {
|
|
92
|
-
code =
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
case FEED:
|
|
99
|
-
next = pos;
|
|
100
|
-
|
|
101
|
-
do {
|
|
102
|
-
next += 1;
|
|
103
|
-
code = str.charCodeAt(next);
|
|
104
|
-
} while (
|
|
105
|
-
code === SPACE ||
|
|
106
|
-
code === NEWLINE ||
|
|
107
|
-
code === TAB ||
|
|
108
|
-
code === CR ||
|
|
109
|
-
code === FEED
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
ast.nodes.push({
|
|
113
|
-
type: 'space',
|
|
114
|
-
value: str.slice(pos, next),
|
|
115
|
-
});
|
|
116
|
-
pos = next - 1;
|
|
117
|
-
break;
|
|
118
|
-
case SINGLE_QUOTE:
|
|
119
|
-
ast.nodes.push(T_SINGLE_QUOTE);
|
|
120
|
-
ast.types[C_SINGLE_QUOTE]++;
|
|
121
|
-
ast.quotes = true;
|
|
122
|
-
break;
|
|
123
|
-
case DOUBLE_QUOTE:
|
|
124
|
-
ast.nodes.push(T_DOUBLE_QUOTE);
|
|
125
|
-
ast.types[C_DOUBLE_QUOTE]++;
|
|
126
|
-
ast.quotes = true;
|
|
127
|
-
break;
|
|
128
|
-
case BACKSLASH:
|
|
129
|
-
next = pos + 1;
|
|
130
|
-
|
|
131
|
-
if (str.charCodeAt(next) === SINGLE_QUOTE) {
|
|
132
|
-
ast.nodes.push(T_ESCAPED_SINGLE_QUOTE);
|
|
133
|
-
ast.types[C_ESCAPED_SINGLE_QUOTE]++;
|
|
134
|
-
ast.quotes = true;
|
|
135
|
-
pos = next;
|
|
136
|
-
break;
|
|
137
|
-
} else if (str.charCodeAt(next) === DOUBLE_QUOTE) {
|
|
138
|
-
ast.nodes.push(T_ESCAPED_DOUBLE_QUOTE);
|
|
139
|
-
ast.types[C_ESCAPED_DOUBLE_QUOTE]++;
|
|
140
|
-
ast.quotes = true;
|
|
141
|
-
pos = next;
|
|
142
|
-
break;
|
|
143
|
-
} else if (str.charCodeAt(next) === NEWLINE) {
|
|
144
|
-
ast.nodes.push(T_NEWLINE);
|
|
145
|
-
pos = next;
|
|
146
|
-
break;
|
|
211
|
+
const code = inner.charCodeAt(pos);
|
|
212
|
+
if (code === BACKSLASH) {
|
|
213
|
+
const newlineLen = getEscapedNewlineLength(inner, pos, len);
|
|
214
|
+
if (newlineLen > 0) {
|
|
215
|
+
if (pos > cursor) {
|
|
216
|
+
chunks.push(inner.slice(cursor, pos));
|
|
147
217
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
WORD_END.lastIndex = pos + 1;
|
|
155
|
-
WORD_END.test(str);
|
|
156
|
-
|
|
157
|
-
if (WORD_END.lastIndex === 0) {
|
|
158
|
-
next = len - 1;
|
|
159
|
-
} else {
|
|
160
|
-
next = WORD_END.lastIndex - 2;
|
|
218
|
+
const followsOpenHex = pos === openHexEnd;
|
|
219
|
+
pos += newlineLen;
|
|
220
|
+
cursor = pos;
|
|
221
|
+
if (followsOpenHex) {
|
|
222
|
+
openHexEnd = pos;
|
|
223
|
+
chunks.push(getHexEscapeDelimiter(inner, pos, openHexIsShort));
|
|
161
224
|
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
162
227
|
|
|
163
|
-
|
|
228
|
+
const next = inner.charCodeAt(pos + 1);
|
|
229
|
+
if (next === otherCode) {
|
|
230
|
+
if (pos > cursor) {
|
|
231
|
+
chunks.push(inner.slice(cursor, pos));
|
|
232
|
+
}
|
|
233
|
+
chunks.push(otherQuote);
|
|
234
|
+
pos += 2;
|
|
235
|
+
cursor = pos;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
164
238
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
169
253
|
|
|
170
|
-
|
|
254
|
+
if (code === targetCode) {
|
|
255
|
+
if (pos > cursor) {
|
|
256
|
+
chunks.push(inner.slice(cursor, pos));
|
|
257
|
+
}
|
|
258
|
+
chunks.push(escapedTargetQuote);
|
|
259
|
+
pos++;
|
|
260
|
+
cursor = pos;
|
|
261
|
+
continue;
|
|
171
262
|
}
|
|
263
|
+
|
|
172
264
|
pos++;
|
|
173
265
|
}
|
|
174
266
|
|
|
175
|
-
|
|
267
|
+
if (cursor < len) {
|
|
268
|
+
chunks.push(inner.slice(cursor));
|
|
269
|
+
}
|
|
270
|
+
chunks.push(targetQuote);
|
|
271
|
+
return chunks.join('');
|
|
176
272
|
}
|
|
177
273
|
|
|
178
274
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* @
|
|
275
|
+
* Normalizes quotes and line continuations in a single closed CSS string token.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} raw
|
|
278
|
+
* @param {'single' | 'double'} preferredQuote
|
|
279
|
+
* @return {string}
|
|
182
280
|
*/
|
|
183
|
-
function
|
|
184
|
-
const
|
|
281
|
+
function normalizeString(raw, preferredQuote) {
|
|
282
|
+
const currentQuote = raw[0];
|
|
283
|
+
const targetDelimiter = preferredQuote === 'single' ? "'" : '"';
|
|
284
|
+
const inner = raw.slice(1, -1);
|
|
185
285
|
|
|
186
|
-
if (
|
|
187
|
-
return;
|
|
286
|
+
if (currentQuote === targetDelimiter && !inner.includes('\\')) {
|
|
287
|
+
return raw;
|
|
188
288
|
}
|
|
189
289
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
290
|
+
const { singleCount, doubleCount, hasEscapedNewline, hasRedundantEscape } =
|
|
291
|
+
scanQuotes(inner, currentQuote);
|
|
292
|
+
|
|
293
|
+
const targetQuote = selectTargetQuote(
|
|
294
|
+
singleCount,
|
|
295
|
+
doubleCount,
|
|
296
|
+
preferredQuote,
|
|
297
|
+
currentQuote
|
|
298
|
+
);
|
|
197
299
|
|
|
198
300
|
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
!
|
|
301
|
+
targetQuote === currentQuote &&
|
|
302
|
+
!hasEscapedNewline &&
|
|
303
|
+
!hasRedundantEscape
|
|
202
304
|
) {
|
|
203
|
-
|
|
305
|
+
return raw;
|
|
204
306
|
}
|
|
205
307
|
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* @param {StringAstNode[]} childNodes
|
|
210
|
-
* @param {string} parentQuote
|
|
211
|
-
* @return {StringAstNode[]}
|
|
212
|
-
*/
|
|
213
|
-
function changeChildQuotes(childNodes, parentQuote) {
|
|
214
|
-
const updatedChildren = [];
|
|
215
|
-
for (const child of childNodes) {
|
|
216
|
-
if (
|
|
217
|
-
child.type === C_ESCAPED_DOUBLE_QUOTE &&
|
|
218
|
-
parentQuote === L_SINGLE_QUOTE
|
|
219
|
-
) {
|
|
220
|
-
updatedChildren.push(T_DOUBLE_QUOTE);
|
|
221
|
-
} else if (
|
|
222
|
-
child.type === C_ESCAPED_SINGLE_QUOTE &&
|
|
223
|
-
parentQuote === L_DOUBLE_QUOTE
|
|
224
|
-
) {
|
|
225
|
-
updatedChildren.push(T_SINGLE_QUOTE);
|
|
226
|
-
} else {
|
|
227
|
-
updatedChildren.push(child);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return updatedChildren;
|
|
308
|
+
return reconstructString(inner, targetQuote);
|
|
231
309
|
}
|
|
232
310
|
|
|
233
311
|
/**
|
|
@@ -236,29 +314,18 @@ function changeChildQuotes(childNodes, parentQuote) {
|
|
|
236
314
|
* @return {string}
|
|
237
315
|
*/
|
|
238
316
|
function normalize(value, preferredQuote) {
|
|
239
|
-
|
|
240
|
-
|
|
317
|
+
const chunks = [];
|
|
318
|
+
let cursor = 0;
|
|
319
|
+
for (const [type, raw, start, end] of tokenize({ css: value })) {
|
|
320
|
+
if (type !== TokenType.String || !isClosedString(raw)) continue;
|
|
321
|
+
const normalized = normalizeString(raw, preferredQuote);
|
|
322
|
+
if (normalized === raw) continue;
|
|
323
|
+
chunks.push(value.slice(cursor, start), normalized);
|
|
324
|
+
cursor = end + 1;
|
|
241
325
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (child.type !== C_STRING) {
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const ast = parse(child.value);
|
|
250
|
-
|
|
251
|
-
if (ast.quotes) {
|
|
252
|
-
changeWrappingQuotes(child, ast);
|
|
253
|
-
} else if (preferredQuote === C_SINGLE) {
|
|
254
|
-
child.quote = L_SINGLE_QUOTE;
|
|
255
|
-
} else {
|
|
256
|
-
child.quote = L_DOUBLE_QUOTE;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
child.value = stringify(ast);
|
|
260
|
-
})
|
|
261
|
-
.toString();
|
|
326
|
+
if (cursor === 0) return value;
|
|
327
|
+
chunks.push(value.slice(cursor));
|
|
328
|
+
return chunks.join('');
|
|
262
329
|
}
|
|
263
330
|
|
|
264
331
|
/**
|
|
@@ -268,28 +335,30 @@ function normalize(value, preferredQuote) {
|
|
|
268
335
|
* @return {string}
|
|
269
336
|
*/
|
|
270
337
|
function minify(original, cache, preferredQuote) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
338
|
+
if (!original || (!original.includes("'") && !original.includes('"'))) {
|
|
339
|
+
return original;
|
|
340
|
+
}
|
|
341
|
+
if (cache.has(original)) {
|
|
342
|
+
return /** @type {string} */ (cache.get(original));
|
|
274
343
|
}
|
|
275
344
|
const newValue = normalize(original, preferredQuote);
|
|
276
|
-
cache.set(
|
|
345
|
+
cache.set(original, newValue);
|
|
277
346
|
return newValue;
|
|
278
347
|
}
|
|
279
348
|
|
|
349
|
+
/** @param {import('postcss').Declaration} decl @param {string} value */
|
|
350
|
+
function assignValue(decl, value) {
|
|
351
|
+
decl.value = value;
|
|
352
|
+
if (decl.raws.value?.raw) decl.raws.value = { raw: value, value };
|
|
353
|
+
}
|
|
354
|
+
|
|
280
355
|
/** @typedef {{preferredQuote?: 'double' | 'single'}} Options */
|
|
281
356
|
/**
|
|
282
357
|
* @param {Options} opts
|
|
283
358
|
* @return {import('postcss').Plugin}
|
|
284
359
|
*/
|
|
285
360
|
function pluginCreator(opts) {
|
|
286
|
-
const { preferredQuote } =
|
|
287
|
-
{},
|
|
288
|
-
{
|
|
289
|
-
preferredQuote: 'double',
|
|
290
|
-
},
|
|
291
|
-
opts
|
|
292
|
-
);
|
|
361
|
+
const { preferredQuote = 'double' } = opts ?? {};
|
|
293
362
|
|
|
294
363
|
return {
|
|
295
364
|
postcssPlugin: 'postcss-normalize-string',
|
|
@@ -305,11 +374,19 @@ function pluginCreator(opts) {
|
|
|
305
374
|
case 'rule':
|
|
306
375
|
node.selector = minify(node.selector, cache, preferredQuote);
|
|
307
376
|
break;
|
|
308
|
-
case 'decl':
|
|
309
|
-
|
|
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));
|
|
310
384
|
break;
|
|
385
|
+
}
|
|
311
386
|
case 'atrule':
|
|
312
|
-
|
|
387
|
+
if (node.name.toLowerCase() !== 'charset') {
|
|
388
|
+
node.params = minify(node.params, cache, preferredQuote);
|
|
389
|
+
}
|
|
313
390
|
break;
|
|
314
391
|
}
|
|
315
392
|
});
|
package/types/index.d.ts
CHANGED
|
@@ -1,30 +1,3 @@
|
|
|
1
|
-
declare const T_ESCAPED_SINGLE_QUOTE: {
|
|
2
|
-
type: string;
|
|
3
|
-
value: string;
|
|
4
|
-
};
|
|
5
|
-
declare const T_ESCAPED_DOUBLE_QUOTE: {
|
|
6
|
-
type: string;
|
|
7
|
-
value: string;
|
|
8
|
-
};
|
|
9
|
-
declare const T_SINGLE_QUOTE: {
|
|
10
|
-
type: string;
|
|
11
|
-
value: string;
|
|
12
|
-
};
|
|
13
|
-
declare const T_NEWLINE: {
|
|
14
|
-
type: string;
|
|
15
|
-
value: string;
|
|
16
|
-
};
|
|
17
|
-
export type StringAstNode = typeof T_ESCAPED_SINGLE_QUOTE | typeof T_ESCAPED_DOUBLE_QUOTE | typeof T_SINGLE_QUOTE | typeof T_NEWLINE;
|
|
18
|
-
export type StringAst = {
|
|
19
|
-
nodes: StringAstNode[];
|
|
20
|
-
types: {
|
|
21
|
-
escapedSingleQuote: number;
|
|
22
|
-
escapedDoubleQuote: number;
|
|
23
|
-
singleQuote: number;
|
|
24
|
-
doubleQuote: number;
|
|
25
|
-
};
|
|
26
|
-
quotes: boolean;
|
|
27
|
-
};
|
|
28
1
|
export type Options = {
|
|
29
2
|
preferredQuote?: 'double' | 'single';
|
|
30
3
|
};
|
package/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"
|
|
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"}
|