fix: nested stylesheets should have absolute URLs (#1533)

* Replace relative URLs with absolute URLs when stringifying stylesheets

* Add test to show desired behavior for imported stylesheets from seperate directory

* Rename `absoluteToStylesheet` to `absolutifyURLs` and call it once after stringifying imported stylesheet

* Don't create the intermediary array of the spread operator

* Formalize that `stringifyRule` should expect a sheet href

* Ensure a <style> element can also import and gets it's url absolutized

* Handle case where non imported stylesheet has relative urls that need to be absolutified

* Clarify in test files where jpegs are expected to appear in absolutified urls

* Move absolutifyURLs call for import rules out of trycatch

* Add a benchmarking test for stringifyStylesheet

* Avoid the duplication on how to fall back

---------

Co-authored-by: Eoghan Murray <eoghan@getthere.ie>
Co-authored-by: eoghanmurray <eoghanmurray@users.noreply.github.com>
This commit is contained in:
Jeff Nguyen
2024-07-25 10:25:00 -04:00
committed by GitHub
parent 8059d96951
commit d350da8552
13 changed files with 175 additions and 108 deletions

View File

@@ -489,7 +489,7 @@ exports[`integration tests > [html file]: with-style-sheet.html 1`] = `
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" />
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\" />
<title>with style sheet</title>
<style>body { margin: 0px; background: url(\\"http://localhost:3030/a.jpg\\"); border-image: url(\\"data:image/svg+xml;utf8,&lt;svg xmlns=\\\\\\"http://www.w3.org/2000/svg\\\\\\" x=\\\\\\"0px\\\\\\" y=\\\\\\"0px\\\\\\" viewBox=\\\\\\"0 0 256 256\\\\\\"&gt;&lt;g&gt;&lt;g&gt;&lt;polygon points=\\\\\\"79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128\\\\\\"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/css/b.jpg\\"); }body &gt; p { color: yellow; }</style>
<style>body { margin: 0px; background: url(\\"http://localhost:3030/should-be-in-root-folder.jpg\\"); border-image: url(\\"data:image/svg+xml;utf8,&lt;svg xmlns=\\\\\\"http://www.w3.org/2000/svg\\\\\\" x=\\\\\\"0px\\\\\\" y=\\\\\\"0px\\\\\\" viewBox=\\\\\\"0 0 256 256\\\\\\"&gt;&lt;g&gt;&lt;g&gt;&lt;polygon points=\\\\\\"79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128\\\\\\"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/css/should-be-in-css-folder.jpg\\"); }body &gt; p { color: yellow; }</style>
</head><body>
</body></html>"
`;
@@ -500,7 +500,8 @@ exports[`integration tests > [html file]: with-style-sheet-with-import.html 1`]
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" />
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\" />
<title>with style sheet with import</title>
<style>@import url(\\"//fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&amp;family=Roboto:wght@100;300;400;500;700&amp;display=swap\\\\\\"\\");body { margin: 0px; background: url(\\"http://localhost:3030/a.jpg\\"); border-image: url(\\"data:image/svg+xml;utf8,&lt;svg xmlns=\\\\\\"http://www.w3.org/2000/svg\\\\\\" x=\\\\\\"0px\\\\\\" y=\\\\\\"0px\\\\\\" viewBox=\\\\\\"0 0 256 256\\\\\\"&gt;&lt;g&gt;&lt;g&gt;&lt;polygon points=\\\\\\"79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128\\\\\\"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/css/b.jpg\\"); }body &gt; p { color: yellow; }</style>
<style>@import url(\\"//fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&amp;family=Roboto:wght@100;300;400;500;700&amp;display=swap\\\\\\"\\");body { margin: 0px; background: url(\\"http://localhost:3030/should-be-in-root-folder.jpg\\"); border-image: url(\\"data:image/svg+xml;utf8,&lt;svg xmlns=\\\\\\"http://www.w3.org/2000/svg\\\\\\" x=\\\\\\"0px\\\\\\" y=\\\\\\"0px\\\\\\" viewBox=\\\\\\"0 0 256 256\\\\\\"&gt;&lt;g&gt;&lt;g&gt;&lt;polygon points=\\\\\\"79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128\\\\\\"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/css/should-be-in-css-folder.jpg\\"); }body &gt; p { color: yellow; }body { margin: 0px; background: url(\\"http://localhost:3030/should-be-in-root-folder.jpg\\"); border-image: url(\\"data:image/svg+xml;utf8,&lt;svg xmlns=\\\\\\"http://www.w3.org/2000/svg\\\\\\" x=\\\\\\"0px\\\\\\" y=\\\\\\"0px\\\\\\" viewBox=\\\\\\"0 0 256 256\\\\\\"&gt;&lt;g&gt;&lt;g&gt;&lt;polygon points=\\\\\\"79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128\\\\\\"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/alt-css/should-be-in-alt-css-folder.jpg\\"); }body &gt; p { color: yellow; }</style>
<style>body { margin: 0px; background: url(\\"http://localhost:3030/should-be-in-root-folder.jpg\\"); border-image: url(\\"data:image/svg+xml;utf8,&lt;svg xmlns=\\\\\\"http://www.w3.org/2000/svg\\\\\\" x=\\\\\\"0px\\\\\\" y=\\\\\\"0px\\\\\\" viewBox=\\\\\\"0 0 256 256\\\\\\"&gt;&lt;g&gt;&lt;g&gt;&lt;polygon points=\\\\\\"79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128\\\\\\"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/alt-css/should-be-in-alt-css-folder.jpg\\"); }body &gt; p { color: yellow; }section { background: url(\\"http://localhost:3030/should-be-in-root-folder.jpg\\"); }</style>
</head><body>
</body></html>"
`;

View File

@@ -0,0 +1,12 @@
body {
margin: 0;
background: url('../should-be-in-root-folder.jpg');
border-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 256 256"><g><g><polygon points="79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128"/></g></g></svg>');
}
p {
color: red;
background: url('./should-be-in-alt-css-folder.jpg');
}
body > p {
color: yellow;
}

View File

@@ -1,2 +1,3 @@
@import '//fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&family=Roboto:wght@100;300;400;500;700&display=swap"';
@import './style.css';
@import '../alt-css/alt-style.css';

View File

@@ -1,11 +1,11 @@
body {
margin: 0;
background: url('../a.jpg');
background: url('../should-be-in-root-folder.jpg');
border-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 256 256"><g><g><polygon points="79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128"/></g></g></svg>');
}
p {
color: red;
background: url('./b.jpg');
background: url('./should-be-in-css-folder.jpg');
}
body > p {
color: yellow;

View File

@@ -7,6 +7,10 @@
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>with style sheet with import</title>
<link rel="stylesheet" href="/css/style-with-import.css">
<style>
@import '../alt-css/alt-style.css';
section { background: url('./should-be-in-root-folder.jpg'); }
</style>
</head>
<body>

View File

@@ -3,63 +3,59 @@
*/
import { JSDOM } from 'jsdom';
import { describe, it, expect } from 'vitest';
import {
absoluteToStylesheet,
serializeNodeWithId,
_isBlockedElement,
} from '../src/snapshot';
import { serializeNodeWithId, _isBlockedElement } from '../src/snapshot';
import snapshot from '../src/snapshot';
import { serializedNodeWithId, elementNode } from '../src/types';
import { Mirror } from '../src/utils';
import { Mirror, absolutifyURLs } from '../src/utils';
describe('absolute url to stylesheet', () => {
const href = 'http://localhost/css/style.css';
it('can handle relative path', () => {
expect(absoluteToStylesheet('url(a.jpg)', href)).toEqual(
expect(absolutifyURLs('url(a.jpg)', href)).toEqual(
`url(http://localhost/css/a.jpg)`,
);
});
it('can handle same level path', () => {
expect(absoluteToStylesheet('url("./a.jpg")', href)).toEqual(
expect(absolutifyURLs('url("./a.jpg")', href)).toEqual(
`url("http://localhost/css/a.jpg")`,
);
});
it('can handle parent level path', () => {
expect(absoluteToStylesheet('url("../a.jpg")', href)).toEqual(
expect(absolutifyURLs('url("../a.jpg")', href)).toEqual(
`url("http://localhost/a.jpg")`,
);
});
it('can handle absolute path', () => {
expect(absoluteToStylesheet('url("/a.jpg")', href)).toEqual(
expect(absolutifyURLs('url("/a.jpg")', href)).toEqual(
`url("http://localhost/a.jpg")`,
);
});
it('can handle external path', () => {
expect(absoluteToStylesheet('url("http://localhost/a.jpg")', href)).toEqual(
expect(absolutifyURLs('url("http://localhost/a.jpg")', href)).toEqual(
`url("http://localhost/a.jpg")`,
);
});
it('can handle single quote path', () => {
expect(absoluteToStylesheet(`url('./a.jpg')`, href)).toEqual(
expect(absolutifyURLs(`url('./a.jpg')`, href)).toEqual(
`url('http://localhost/css/a.jpg')`,
);
});
it('can handle no quote path', () => {
expect(absoluteToStylesheet('url(./a.jpg)', href)).toEqual(
expect(absolutifyURLs('url(./a.jpg)', href)).toEqual(
`url(http://localhost/css/a.jpg)`,
);
});
it('can handle multiple no quote paths', () => {
expect(
absoluteToStylesheet(
absolutifyURLs(
'background-image: url(images/b.jpg);background: #aabbcc url(images/a.jpg) 50% 50% repeat;',
href,
),
@@ -70,11 +66,11 @@ describe('absolute url to stylesheet', () => {
});
it('can handle data url image', () => {
expect(absolutifyURLs('url(data:image/gif;base64,ABC)', href)).toEqual(
'url(data:image/gif;base64,ABC)',
);
expect(
absoluteToStylesheet('url(data:image/gif;base64,ABC)', href),
).toEqual('url(data:image/gif;base64,ABC)');
expect(
absoluteToStylesheet(
absolutifyURLs(
'url(data:application/font-woff;base64,d09GMgABAAAAAAm)',
href,
),
@@ -83,7 +79,7 @@ describe('absolute url to stylesheet', () => {
it('preserves quotes around inline svgs with spaces', () => {
expect(
absoluteToStylesheet(
absolutifyURLs(
"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,
),
@@ -91,7 +87,7 @@ describe('absolute url to stylesheet', () => {
"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(
absolutifyURLs(
'url(\'data:image/svg+xml;utf8,<svg width="28" height="32" viewBox="0 0 28 32" xmlns="http://www.w3.org/2000/svg"><path d="M27 14C28" fill="white"/></svg>\')',
href,
),
@@ -99,7 +95,7 @@ describe('absolute url to stylesheet', () => {
'url(\'data:image/svg+xml;utf8,<svg width="28" height="32" viewBox="0 0 28 32" xmlns="http://www.w3.org/2000/svg"><path d="M27 14C28" fill="white"/></svg>\')',
);
expect(
absoluteToStylesheet(
absolutifyURLs(
'url("data:image/svg+xml;utf8,<svg width="28" height="32" viewBox="0 0 28 32" xmlns="http://www.w3.org/2000/svg"><path d="M27 14C28" fill="white"/></svg>")',
href,
),
@@ -108,7 +104,7 @@ describe('absolute url to stylesheet', () => {
);
});
it('can handle empty path', () => {
expect(absoluteToStylesheet(`url('')`, href)).toEqual(`url('')`);
expect(absolutifyURLs(`url('')`, href)).toEqual(`url('')`);
});
});

View File

@@ -0,0 +1,37 @@
/**
* @vitest-environment jsdom
*/
import { bench } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import { stringifyStylesheet } from '../src/utils';
import * as CSSOM from 'cssom';
describe('stringifyStylesheet', () => {
let benchmarkStylesheet: CSSStyleSheet;
const cssText = fs.readFileSync(
path.resolve(__dirname, './css/benchmark.css'),
'utf8',
);
benchmarkStylesheet = CSSOM.parse(cssText);
benchmarkStylesheet.href = 'https://example.com/style.css';
it.skip('stringify', () => {
// written just to ensure it's working
const cssText = '.x { background: url(./relative.jpg) }';
const styleSheet = CSSOM.parse(cssText);
styleSheet.href = 'https://example.com/style.css';
expect(stringifyStylesheet(styleSheet)).toEqual(
'x {background: url(https://example.com/relative.jpg);}',
);
});
bench(
'stringify',
() => {
stringifyStylesheet(benchmarkStylesheet);
},
{ time: 1000 },
);
});