Skip to content

Commit 4313273

Browse files
committed
fix: stop normalizing url() quote style
print_url previously re-quoted every url() to double quotes (falling back to single when the value contained a double quote), and for data: URLs went further, escaping/re-picking quotes when the value contained both quote types. That's surprising for authors who deliberately chose a quote style, and the data: URL escaping could alter the URL's actual content (%22-encoding a literal double quote inside it). print_url now only lowercases a leading `url(` keyword and otherwise prints the node's raw text, leaving quote style (and unquoted url()s) exactly as written. Updated the tests that asserted the old quote-normalizing/escaping behavior.
1 parent 4fc121f commit 4313273

3 files changed

Lines changed: 18 additions & 31 deletions

File tree

‎src/lib/index.ts‎

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ export type FormatOptions = {
6161
}
6262

6363
const UNQUOTE_RE = /(?:^['"])|(?:['"]$)/g
64-
const DATA_URL_RE = /^['"]?data:/i
6564
const FONT_SLASH_RE = /\s*\/\s*/
6665
const ATRULE_COLON_COMMA_RE = /\s*([:,])/g
6766
const ATRULE_PAREN_TEXT_RE = /\)([a-zA-Z])/g
@@ -87,26 +86,14 @@ function print_string(str: string | number | null, quote?: '"' | "'"): string {
8786
return quote + inner + quote
8887
}
8988

89+
/** Prints a `url(...)`: lowercases a leading `url(` keyword but leaves quote
90+
* style untouched. */
9091
function print_url(node: Url): string {
91-
let value = node.value ?? ''
92-
let unquoted = unquote(value)
93-
94-
let inner: string
95-
if (DATA_URL_RE.test(value)) {
96-
let has_double = unquoted.includes('"')
97-
let has_single = unquoted.includes("'")
98-
if (has_double && has_single) {
99-
inner = print_string(unquoted.replaceAll('"', '%22'), '"')
100-
} else if (has_double || has_single) {
101-
inner = print_string(unquoted)
102-
} else {
103-
inner = unquoted
104-
}
105-
} else {
106-
inner = print_string(value)
92+
let text = node.text
93+
if (/^url\(/i.test(text)) {
94+
return 'url(' + text.slice(4)
10795
}
108-
109-
return 'url(' + inner + CLOSE_PARENTHESES
96+
return text
11097
}
11198

11299
function print_operator(node: Operator, optional_space = SPACE): string {

‎test/declarations.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ test('Declarations end with a semicolon (;)', () => {
2727
}
2828
`)
2929
let expected = `@font-face {
30-
src: url("test");
30+
src: url('test');
3131
font-family: Test;
3232
}
3333

‎test/values.test.ts‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ test('Does not mess up quotes inside `content`', () => {
272272
expect(actual).toBe(expected)
273273
})
274274

275-
test('adds quotes around strings in url()', () => {
275+
test('leaves url() quoting untouched', () => {
276276
let actual = format(`a {
277277
background-image: url("star.gif");
278278
list-style-image: url('../images/bullet.jpg');
@@ -285,12 +285,12 @@ test('adds quotes around strings in url()', () => {
285285
}`)
286286
let expected = `a {
287287
background-image: url("star.gif");
288-
list-style-image: url("../images/bullet.jpg");
288+
list-style-image: url('../images/bullet.jpg');
289289
content: url("pdficon.jpg");
290-
cursor: url("mycursor.cur");
291-
border-image-source: url("/media/diamonds.png");
292-
src: url("fantasticfont.woff");
293-
offset-path: url("#path");
290+
cursor: url(mycursor.cur);
291+
border-image-source: url(/media/diamonds.png);
292+
src: url('fantasticfont.woff');
293+
offset-path: url(#path);
294294
mask-image: url("masks.svg#mask1");
295295
}`
296296
expect(actual).toEqual(expected)
@@ -306,7 +306,7 @@ test.each([
306306
}`)
307307
let expected = `test {
308308
background-image: url('${input}');
309-
background-image: url('${input}');
309+
background-image: url(${input});
310310
}`
311311
expect(actual).toEqual(expected)
312312
})
@@ -329,7 +329,7 @@ test.each([
329329
expect(actual).toBe(expected)
330330
})
331331

332-
test('wraps data: URL in single quotes when it contains double quotes', () => {
332+
test('leaves a single-quoted data: URL containing double quotes untouched', () => {
333333
let input = `.a { background: url('data:image/svg+xml,%3Csvg fill="red"%3E%3C/svg%3E'); }`
334334
let actual = format(input)
335335
let expected = `.a {
@@ -338,7 +338,7 @@ test('wraps data: URL in single quotes when it contains double quotes', () => {
338338
expect(actual).toEqual(expected)
339339
})
340340

341-
test('wraps data: URL in double quotes when it contains single quotes', () => {
341+
test('leaves a double-quoted data: URL containing single quotes untouched', () => {
342342
let input = `.a { background: url("data:image/svg+xml,%3Csvg fill='red'%3E%3C/svg%3E"); }`
343343
let actual = format(input)
344344
let expected = `.a {
@@ -347,11 +347,11 @@ test('wraps data: URL in double quotes when it contains single quotes', () => {
347347
expect(actual).toEqual(expected)
348348
})
349349

350-
test('encodes double quotes when data: URL contains both quote types', () => {
350+
test('leaves a data: URL with both quote types untouched', () => {
351351
let input = `.a { background: url('data:image/svg+xml,%3Csvg fill="x" alt=\\'y\\'%3E'); }`
352352
let actual = format(input)
353353
let expected = `.a {
354-
background: url("data:image/svg+xml,%3Csvg fill=%22x%22 alt=\\'y\\'%3E");
354+
background: url('data:image/svg+xml,%3Csvg fill="x" alt=\\'y\\'%3E');
355355
}`
356356
expect(actual).toEqual(expected)
357357
})

0 commit comments

Comments
 (0)