From 635adfc9fd4bc8ca05b8937b5c0ad48124de4c00 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 19:03:33 +0000 Subject: [PATCH 1/4] fix: preserve quotes in data: URLs when inner quotes are present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #217. Extracts URL printing into print_url(node: Url) and safely chooses a quote style — single, double, or %22-encoded — when the unquoted data: value contains quote characters that would be illegal in an unquoted CSS URL token. https://claude.ai/code/session_01PXdqvCto3sc2bUrPNQ4pdk --- src/lib/index.ts | 34 +++++++++++++++++++++++++--------- test/values.test.ts | 31 +++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index fe2e005..efe6cf5 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -35,6 +35,7 @@ import { type Atrule, type StyleSheet, type CSSNode, + type Url, } from '@projectwallace/css-parser' const SPACE = ' ' @@ -66,6 +67,29 @@ function print_string(str: string | number | null): string { return QUOTE + unquote(str) + QUOTE } +function print_url(node: Url): string { + let unquoted = unquote(node.value) + let has_double = unquoted.includes('"') + let has_single = unquoted.includes("'") + + let inner: string + if (/^['"]?data:/i.test(node.value)) { + if (!has_double && !has_single) { + inner = unquoted + } else if (!has_double) { + inner = '"' + unquoted + '"' + } else if (!has_single) { + inner = "'" + unquoted + "'" + } else { + inner = '"' + unquoted.replaceAll('"', '%22') + '"' + } + } else { + inner = print_string(node.value) + } + + return 'url(' + inner + CLOSE_PARENTHESES +} + function print_operator(node: Operator, optional_space = SPACE): string { // https://developer.mozilla.org/en-US/docs/Web/CSS/calc#notes // The + and - operators must be surrounded by whitespace @@ -94,15 +118,7 @@ function print_list(nodes: CSSNode[], optional_space = SPACE): string { } else if (is_parenthesis(node)) { parts.push(OPEN_PARENTHESES, print_list(node.children, optional_space), CLOSE_PARENTHESES) } else if (is_url(node) && node.value) { - parts.push('url(') - let { value } = node - // if the value starts with data:, 'data:, "data: - if (/^['"]?data:/i.test(value)) { - parts.push(unquote(value)) - } else { - parts.push(print_string(value)) - } - parts.push(CLOSE_PARENTHESES) + parts.push(print_url(node)) } else { parts.push(node.text) } diff --git a/test/values.test.ts b/test/values.test.ts index b03965b..9b5cb42 100644 --- a/test/values.test.ts +++ b/test/values.test.ts @@ -293,8 +293,8 @@ test.each([ background-image: url(${input}); }`) let expected = `test { - background-image: url(${input}); - background-image: url(${input}); + background-image: url('${input}'); + background-image: url('${input}'); }` expect(actual).toEqual(expected) }) @@ -317,6 +317,33 @@ test.each([ expect(actual).toBe(expected) }) +test('wraps data: URL in single quotes when it contains double quotes', () => { + let input = `.a { background: url('data:image/svg+xml,%3Csvg fill="red"%3E%3C/svg%3E'); }` + let actual = format(input) + let expected = `.a { + background: url('data:image/svg+xml,%3Csvg fill="red"%3E%3C/svg%3E'); +}` + expect(actual).toEqual(expected) +}) + +test('wraps data: URL in double quotes when it contains single quotes', () => { + let input = `.a { background: url("data:image/svg+xml,%3Csvg fill='red'%3E%3C/svg%3E"); }` + let actual = format(input) + let expected = `.a { + background: url("data:image/svg+xml,%3Csvg fill='red'%3E%3C/svg%3E"); +}` + expect(actual).toEqual(expected) +}) + +test('encodes double quotes when data: URL contains both quote types', () => { + 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"); +}` + expect(actual).toEqual(expected) +}) + test.each([ `U+26`, // single code point `U+0-7F`, From c8868772e2f8a5718b57d36dc0ad3d102dd5fc2f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 19:05:48 +0000 Subject: [PATCH 2/4] refactor: add quote parameter to print_string https://claude.ai/code/session_01PXdqvCto3sc2bUrPNQ4pdk --- src/lib/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index efe6cf5..b435718 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -62,9 +62,9 @@ export function unquote(str: string): string { return str.replaceAll(/(?:^['"])|(?:['"]$)/g, EMPTY_STRING) } -function print_string(str: string | number | null): string { +function print_string(str: string | number | null, quote: '"' | "'" = '"'): string { str = str?.toString() || '' - return QUOTE + unquote(str) + QUOTE + return quote + unquote(str) + quote } function print_url(node: Url): string { @@ -77,11 +77,11 @@ function print_url(node: Url): string { if (!has_double && !has_single) { inner = unquoted } else if (!has_double) { - inner = '"' + unquoted + '"' + inner = print_string(unquoted, '"') } else if (!has_single) { - inner = "'" + unquoted + "'" + inner = print_string(unquoted, "'") } else { - inner = '"' + unquoted.replaceAll('"', '%22') + '"' + inner = print_string(unquoted.replaceAll('"', '%22'), '"') } } else { inner = print_string(node.value) From f9d60b5502231a87af3c0dbc31cac9ba1bb6743f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 19:07:43 +0000 Subject: [PATCH 3/4] refactor: move has_double/has_single inside data: branch https://claude.ai/code/session_01PXdqvCto3sc2bUrPNQ4pdk --- src/lib/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index b435718..ca07dea 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -69,11 +69,11 @@ function print_string(str: string | number | null, quote: '"' | "'" = '"'): stri function print_url(node: Url): string { let unquoted = unquote(node.value) - let has_double = unquoted.includes('"') - let has_single = unquoted.includes("'") let inner: string if (/^['"]?data:/i.test(node.value)) { + let has_double = unquoted.includes('"') + let has_single = unquoted.includes("'") if (!has_double && !has_single) { inner = unquoted } else if (!has_double) { From 4db93595f13504dbd42e247c1a454aac515cc15c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 19:30:33 +0000 Subject: [PATCH 4/4] fix: resolve type errors and linting in print_url - Use `node.value ?? ''` to handle `string | null` - Reorder data: URL conditions to avoid negated-condition lint errors https://claude.ai/code/session_01PXdqvCto3sc2bUrPNQ4pdk --- src/lib/index.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index ca07dea..81b2b9f 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -68,23 +68,24 @@ function print_string(str: string | number | null, quote: '"' | "'" = '"'): stri } function print_url(node: Url): string { - let unquoted = unquote(node.value) + let value = node.value ?? '' + let unquoted = unquote(value) let inner: string - if (/^['"]?data:/i.test(node.value)) { + if (/^['"]?data:/i.test(value)) { let has_double = unquoted.includes('"') let has_single = unquoted.includes("'") - if (!has_double && !has_single) { - inner = unquoted - } else if (!has_double) { - inner = print_string(unquoted, '"') - } else if (!has_single) { + if (has_double && has_single) { + inner = print_string(unquoted.replaceAll('"', '%22'), '"') + } else if (has_double) { inner = print_string(unquoted, "'") + } else if (has_single) { + inner = print_string(unquoted, '"') } else { - inner = print_string(unquoted.replaceAll('"', '%22'), '"') + inner = unquoted } } else { - inner = print_string(node.value) + inner = print_string(value) } return 'url(' + inner + CLOSE_PARENTHESES