From 43132732eca24fbb101c013cd802617d93554df6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 13:51:30 +0000 Subject: [PATCH 1/2] 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. --- src/lib/index.ts | 25 ++++++------------------- test/declarations.test.ts | 2 +- test/values.test.ts | 22 +++++++++++----------- 3 files changed, 18 insertions(+), 31 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index 67434db..1a666bc 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -61,7 +61,6 @@ export type FormatOptions = { } const UNQUOTE_RE = /(?:^['"])|(?:['"]$)/g -const DATA_URL_RE = /^['"]?data:/i const FONT_SLASH_RE = /\s*\/\s*/ const ATRULE_COLON_COMMA_RE = /\s*([:,])/g const ATRULE_PAREN_TEXT_RE = /\)([a-zA-Z])/g @@ -87,26 +86,14 @@ function print_string(str: string | number | null, quote?: '"' | "'"): string { return quote + inner + quote } +/** Prints a `url(...)`: lowercases a leading `url(` keyword but leaves quote + * style untouched. */ function print_url(node: Url): string { - let value = node.value ?? '' - let unquoted = unquote(value) - - let inner: string - if (DATA_URL_RE.test(value)) { - let has_double = unquoted.includes('"') - let has_single = unquoted.includes("'") - if (has_double && has_single) { - inner = print_string(unquoted.replaceAll('"', '%22'), '"') - } else if (has_double || has_single) { - inner = print_string(unquoted) - } else { - inner = unquoted - } - } else { - inner = print_string(value) + let text = node.text + if (/^url\(/i.test(text)) { + return 'url(' + text.slice(4) } - - return 'url(' + inner + CLOSE_PARENTHESES + return text } function print_operator(node: Operator, optional_space = SPACE): string { diff --git a/test/declarations.test.ts b/test/declarations.test.ts index d915342..75c55fe 100644 --- a/test/declarations.test.ts +++ b/test/declarations.test.ts @@ -27,7 +27,7 @@ test('Declarations end with a semicolon (;)', () => { } `) let expected = `@font-face { - src: url("test"); + src: url('test'); font-family: Test; } diff --git a/test/values.test.ts b/test/values.test.ts index 749f037..c977adb 100644 --- a/test/values.test.ts +++ b/test/values.test.ts @@ -272,7 +272,7 @@ test('Does not mess up quotes inside `content`', () => { expect(actual).toBe(expected) }) -test('adds quotes around strings in url()', () => { +test('leaves url() quoting untouched', () => { let actual = format(`a { background-image: url("star.gif"); list-style-image: url('../images/bullet.jpg'); @@ -285,12 +285,12 @@ test('adds quotes around strings in url()', () => { }`) let expected = `a { background-image: url("star.gif"); - list-style-image: url("../images/bullet.jpg"); + list-style-image: url('../images/bullet.jpg'); content: url("pdficon.jpg"); - cursor: url("mycursor.cur"); - border-image-source: url("/media/diamonds.png"); - src: url("fantasticfont.woff"); - offset-path: url("#path"); + cursor: url(mycursor.cur); + border-image-source: url(/media/diamonds.png); + src: url('fantasticfont.woff'); + offset-path: url(#path); mask-image: url("masks.svg#mask1"); }` expect(actual).toEqual(expected) @@ -306,7 +306,7 @@ test.each([ }`) let expected = `test { background-image: url('${input}'); - background-image: url('${input}'); + background-image: url(${input}); }` expect(actual).toEqual(expected) }) @@ -329,7 +329,7 @@ test.each([ expect(actual).toBe(expected) }) -test('wraps data: URL in single quotes when it contains double quotes', () => { +test('leaves a single-quoted data: URL containing double quotes untouched', () => { let input = `.a { background: url('data:image/svg+xml,%3Csvg fill="red"%3E%3C/svg%3E'); }` let actual = format(input) let expected = `.a { @@ -338,7 +338,7 @@ test('wraps data: URL in single quotes when it contains double quotes', () => { expect(actual).toEqual(expected) }) -test('wraps data: URL in double quotes when it contains single quotes', () => { +test('leaves a double-quoted data: URL containing single quotes untouched', () => { let input = `.a { background: url("data:image/svg+xml,%3Csvg fill='red'%3E%3C/svg%3E"); }` let actual = format(input) let expected = `.a { @@ -347,11 +347,11 @@ test('wraps data: URL in double quotes when it contains single quotes', () => { expect(actual).toEqual(expected) }) -test('encodes double quotes when data: URL contains both quote types', () => { +test('leaves a data: URL with both quote types untouched', () => { let input = `.a { background: url('data:image/svg+xml,%3Csvg fill="x" alt=\\'y\\'%3E'); }` let actual = format(input) let expected = `.a { - background: url("data:image/svg+xml,%3Csvg fill=%22x%22 alt=\\'y\\'%3E"); + background: url('data:image/svg+xml,%3Csvg fill="x" alt=\\'y\\'%3E'); }` expect(actual).toEqual(expected) }) From b47345b24380040de9ba05e9d744fb41111bf0d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 18:18:30 +0000 Subject: [PATCH 2/2] refactor: simplify print_url, the url( prefix check is always true A Url node's text always starts with url( (any casing) by construction -- that's what makes the parser tag it as a Url node in the first place, confirmed empirically across url(), URL(), quoted and unquoted forms. The regex test guarding the slice was dead code. --- src/lib/index.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index 2237fa2..a6b2fc2 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -92,14 +92,11 @@ function print_string(str: string | number | null, quote?: '"' | "'"): string { return quote + inner + quote } -/** Prints a `url(...)`: lowercases a leading `url(` keyword but leaves quote - * style untouched. */ +/** Prints a `url(...)`: lowercases the `url(` keyword but leaves quote style + * untouched. A Url node's text always starts with `url(` (any casing) — it's + * how the parser identifies the node as a Url in the first place. */ function print_url(node: Url): string { - let text = node.text - if (/^url\(/i.test(text)) { - return 'url(' + text.slice(4) - } - return text + return 'url(' + node.text.slice(4) } function print_operator(node: Operator, optional_space = SPACE): string {