From 28917d1c9a63238b403b452cdd2df918f5c75bbf Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] Preserve url quotes (#47) * Preserve original quotes when rewriting CSS url() paths - important for inline SVG files which often have spaces * Found an example in the wild with the 'charset=' part left off. This is supported by https://css-tricks.com/lodge/svg/09-svg-data-uris/ ... not sure why we aren't just testing for the 'data:' prefix here? * Not sure why this is now coming back with a double quote after recent changes here; it's supposed to preserve the single quote from style.css?? --- src/snapshot.ts | 15 ++++++++------- test/__snapshots__/integration.ts.snap | 4 ++-- test/snapshot.test.ts | 24 ++++++++++++++++-------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/snapshot.ts b/src/snapshot.ts index 17150d0a..9c9a64c3 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -66,28 +66,29 @@ function extractOrigin(url: string): string { return origin; } -const URL_IN_CSS_REF = /url\((?:'([^']*)'|"([^"]*)"|([^)]*))\)/gm; +const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")([^"]*)"|([^)]*))\)/gm; const RELATIVE_PATH = /^(?!www\.|(?:http|ftp)s?:\/\/|[A-Za-z]:\\|\/\/).*/; -const DATA_URI = /^(data:)([\w\/\+\-]+);(charset=[\w-]+|base64).*,(.*)/i; +const DATA_URI = /^(data:)([\w\/\+\-]+);(charset=[\w-]+|base64|utf-?8).*,(.*)/i; export function absoluteToStylesheet( cssText: string | null, href: string, ): string { return (cssText || '').replace( URL_IN_CSS_REF, - (origin, path1, path2, path3) => { + (origin, quote1, path1, quote2, path2, path3) => { const filePath = path1 || path2 || path3; + const maybe_quote = quote1 || quote2 || ''; if (!filePath) { return origin; } if (!RELATIVE_PATH.test(filePath)) { - return `url('${filePath}')`; + return `url(${maybe_quote}${filePath}${maybe_quote})`; } if (DATA_URI.test(filePath)) { - return `url(${filePath})`; + return `url(${maybe_quote}${filePath}${maybe_quote})`; } if (filePath[0] === '/') { - return `url('${extractOrigin(href) + filePath}')`; + return `url(${maybe_quote}${extractOrigin(href) + filePath}${maybe_quote})`; } const stack = href.split('/'); const parts = filePath.split('/'); @@ -101,7 +102,7 @@ export function absoluteToStylesheet( stack.push(part); } } - return `url('${stack.join('/')}')`; + return `url(${maybe_quote}${stack.join('/')}${maybe_quote})`; }, ); } diff --git a/test/__snapshots__/integration.ts.snap b/test/__snapshots__/integration.ts.snap index 855d73eb..f5c84e18 100644 --- a/test/__snapshots__/integration.ts.snap +++ b/test/__snapshots__/integration.ts.snap @@ -258,7 +258,7 @@ exports[`[html file]: with-style-sheet.html 1`] = ` with style sheet - + " `; @@ -269,7 +269,7 @@ exports[`[html file]: with-style-sheet-with-import.html 1`] = ` with style sheet with import - + " `; diff --git a/test/snapshot.test.ts b/test/snapshot.test.ts index 9c58dca7..2116813a 100644 --- a/test/snapshot.test.ts +++ b/test/snapshot.test.ts @@ -7,32 +7,32 @@ describe('absolute url to stylesheet', () => { it('can handle relative path', () => { expect(absoluteToStylesheet('url(a.jpg)', href)).to.equal( - `url('http://localhost/css/a.jpg')`, + `url(http://localhost/css/a.jpg)`, ); }); it('can handle same level path', () => { expect(absoluteToStylesheet('url("./a.jpg")', href)).to.equal( - `url('http://localhost/css/a.jpg')`, + `url("http://localhost/css/a.jpg")`, ); }); it('can handle parent level path', () => { expect(absoluteToStylesheet('url("../a.jpg")', href)).to.equal( - `url('http://localhost/a.jpg')`, + `url("http://localhost/a.jpg")`, ); }); it('can handle absolute path', () => { expect(absoluteToStylesheet('url("/a.jpg")', href)).to.equal( - `url('http://localhost/a.jpg')`, + `url("http://localhost/a.jpg")`, ); }); it('can handle external path', () => { expect( absoluteToStylesheet('url("http://localhost/a.jpg")', href), - ).to.equal(`url('http://localhost/a.jpg')`); + ).to.equal(`url("http://localhost/a.jpg")`); }); it('can handle single quote path', () => { @@ -43,7 +43,7 @@ describe('absolute url to stylesheet', () => { it('can handle no quote path', () => { expect(absoluteToStylesheet('url(./a.jpg)', href)).to.equal( - `url('http://localhost/css/a.jpg')`, + `url(http://localhost/css/a.jpg)`, ); }); @@ -54,8 +54,8 @@ describe('absolute url to stylesheet', () => { href, ), ).to.equal( - `background-image: url('http://localhost/css/images/b.jpg');` + - `background: #aabbcc url('http://localhost/css/images/a.jpg') 50% 50% repeat;`, + `background-image: url(http://localhost/css/images/b.jpg);` + + `background: #aabbcc url(http://localhost/css/images/a.jpg) 50% 50% repeat;`, ); }); @@ -71,6 +71,14 @@ describe('absolute url to stylesheet', () => { ).to.equal('url(data:application/font-woff;base64,d09GMgABAAAAAAm)'); }); + it('preserves quotes around inline svgs with spaces', () => { + expect( + absoluteToStylesheet("url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2328a745' d='M3'/%3E%3C/svg%3E\")", href), + ).to.equal("url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2328a745' d='M3'/%3E%3C/svg%3E\")"); + expect( + absoluteToStylesheet('url(\'data:image/svg+xml;utf8,\')', href), + ).to.equal('url(\'data:image/svg+xml;utf8,\')'); + }); it('can handle empty path', () => { expect(absoluteToStylesheet(`url('')`, href)).to.equal(`url('')`); });