diff --git a/src/lib/index.ts b/src/lib/index.ts index fe2e005..81b2b9f 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 = ' ' @@ -61,9 +62,33 @@ 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 { + let value = node.value ?? '' + let unquoted = unquote(value) + + let inner: string + if (/^['"]?data:/i.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) { + inner = print_string(unquoted, "'") + } else if (has_single) { + inner = print_string(unquoted, '"') + } else { + inner = unquoted + } + } else { + inner = print_string(value) + } + + return 'url(' + inner + CLOSE_PARENTHESES } function print_operator(node: Operator, optional_space = SPACE): string { @@ -94,15 +119,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`,