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:
5
.changeset/six-llamas-brush.md
Normal file
5
.changeset/six-llamas-brush.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"rrweb-snapshot": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix `url()` rewrite for nested stylesheets by rewriting during stringification instead of after
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"test:watch": "vitest watch",
|
"test:watch": "vitest watch",
|
||||||
"retest:update": "vitest run --update",
|
"retest:update": "vitest run --update",
|
||||||
"test:update": "yarn build && vitest run --update",
|
"test:update": "yarn build && vitest run --update",
|
||||||
|
"bench": "vite build && vitest bench",
|
||||||
"dev": "vite build --watch",
|
"dev": "vite build --watch",
|
||||||
"build": "yarn turbo prepublish -F rrweb-snapshot",
|
"build": "yarn turbo prepublish -F rrweb-snapshot",
|
||||||
"check-types": "tsc --noEmit",
|
"check-types": "tsc --noEmit",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
getInputType,
|
getInputType,
|
||||||
toLowerCase,
|
toLowerCase,
|
||||||
extractFileExtension,
|
extractFileExtension,
|
||||||
|
absolutifyURLs,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
let _id = 1;
|
let _id = 1;
|
||||||
@@ -53,71 +54,9 @@ function getValidTagName(element: HTMLElement): Lowercase<string> {
|
|||||||
return processedTagName;
|
return processedTagName;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractOrigin(url: string): string {
|
|
||||||
let origin = '';
|
|
||||||
if (url.indexOf('//') > -1) {
|
|
||||||
origin = url.split('/').slice(0, 3).join('/');
|
|
||||||
} else {
|
|
||||||
origin = url.split('/')[0];
|
|
||||||
}
|
|
||||||
origin = origin.split('?')[0];
|
|
||||||
return origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
let canvasService: HTMLCanvasElement | null;
|
let canvasService: HTMLCanvasElement | null;
|
||||||
let canvasCtx: CanvasRenderingContext2D | null;
|
let canvasCtx: CanvasRenderingContext2D | null;
|
||||||
|
|
||||||
const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm;
|
|
||||||
const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i;
|
|
||||||
const URL_WWW_MATCH = /^www\..*/i;
|
|
||||||
const DATA_URI = /^(data:)([^,]*),(.*)/i;
|
|
||||||
export function absoluteToStylesheet(
|
|
||||||
cssText: string | null,
|
|
||||||
href: string,
|
|
||||||
): string {
|
|
||||||
return (cssText || '').replace(
|
|
||||||
URL_IN_CSS_REF,
|
|
||||||
(
|
|
||||||
origin: string,
|
|
||||||
quote1: string,
|
|
||||||
path1: string,
|
|
||||||
quote2: string,
|
|
||||||
path2: string,
|
|
||||||
path3: string,
|
|
||||||
) => {
|
|
||||||
const filePath = path1 || path2 || path3;
|
|
||||||
const maybeQuote = quote1 || quote2 || '';
|
|
||||||
if (!filePath) {
|
|
||||||
return origin;
|
|
||||||
}
|
|
||||||
if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) {
|
|
||||||
return `url(${maybeQuote}${filePath}${maybeQuote})`;
|
|
||||||
}
|
|
||||||
if (DATA_URI.test(filePath)) {
|
|
||||||
return `url(${maybeQuote}${filePath}${maybeQuote})`;
|
|
||||||
}
|
|
||||||
if (filePath[0] === '/') {
|
|
||||||
return `url(${maybeQuote}${
|
|
||||||
extractOrigin(href) + filePath
|
|
||||||
}${maybeQuote})`;
|
|
||||||
}
|
|
||||||
const stack = href.split('/');
|
|
||||||
const parts = filePath.split('/');
|
|
||||||
stack.pop();
|
|
||||||
for (const part of parts) {
|
|
||||||
if (part === '.') {
|
|
||||||
continue;
|
|
||||||
} else if (part === '..') {
|
|
||||||
stack.pop();
|
|
||||||
} else {
|
|
||||||
stack.push(part);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/; // Don't use \s, to avoid matching non-breaking space
|
const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/; // Don't use \s, to avoid matching non-breaking space
|
||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
@@ -254,7 +193,7 @@ export function transformAttribute(
|
|||||||
} else if (name === 'srcset') {
|
} else if (name === 'srcset') {
|
||||||
return getAbsoluteSrcsetString(doc, value);
|
return getAbsoluteSrcsetString(doc, value);
|
||||||
} else if (name === 'style') {
|
} else if (name === 'style') {
|
||||||
return absoluteToStylesheet(value, getHref(doc));
|
return absolutifyURLs(value, getHref(doc));
|
||||||
} else if (tagName === 'object' && name === 'data') {
|
} else if (tagName === 'object' && name === 'data') {
|
||||||
return absoluteToDoc(doc, value);
|
return absoluteToDoc(doc, value);
|
||||||
}
|
}
|
||||||
@@ -584,7 +523,7 @@ function serializeTextNode(
|
|||||||
n,
|
n,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
textContent = absoluteToStylesheet(textContent, getHref(options.doc));
|
textContent = absolutifyURLs(textContent, getHref(options.doc));
|
||||||
}
|
}
|
||||||
if (isScript) {
|
if (isScript) {
|
||||||
textContent = 'SCRIPT_PLACEHOLDER';
|
textContent = 'SCRIPT_PLACEHOLDER';
|
||||||
@@ -664,7 +603,7 @@ function serializeElementNode(
|
|||||||
if (cssText) {
|
if (cssText) {
|
||||||
delete attributes.rel;
|
delete attributes.rel;
|
||||||
delete attributes.href;
|
delete attributes.href;
|
||||||
attributes._cssText = absoluteToStylesheet(cssText, stylesheet!.href!);
|
attributes._cssText = cssText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// dynamic stylesheet
|
// dynamic stylesheet
|
||||||
@@ -678,7 +617,7 @@ function serializeElementNode(
|
|||||||
(n as HTMLStyleElement).sheet as CSSStyleSheet,
|
(n as HTMLStyleElement).sheet as CSSStyleSheet,
|
||||||
);
|
);
|
||||||
if (cssText) {
|
if (cssText) {
|
||||||
attributes._cssText = absoluteToStylesheet(cssText, getHref(doc));
|
attributes._cssText = cssText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// form fields
|
// form fields
|
||||||
|
|||||||
@@ -96,19 +96,21 @@ export function escapeImportStatement(rule: CSSImportRule): string {
|
|||||||
export function stringifyStylesheet(s: CSSStyleSheet): string | null {
|
export function stringifyStylesheet(s: CSSStyleSheet): string | null {
|
||||||
try {
|
try {
|
||||||
const rules = s.rules || s.cssRules;
|
const rules = s.rules || s.cssRules;
|
||||||
return rules
|
if (!rules) {
|
||||||
? fixBrowserCompatibilityIssuesInCSS(
|
return null;
|
||||||
Array.from(rules, stringifyRule).join(''),
|
}
|
||||||
)
|
const stringifiedRules = Array.from(rules, (rule: CSSRule) =>
|
||||||
: null;
|
stringifyRule(rule, s.href),
|
||||||
|
).join('');
|
||||||
|
return fixBrowserCompatibilityIssuesInCSS(stringifiedRules);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringifyRule(rule: CSSRule): string {
|
export function stringifyRule(rule: CSSRule, sheetHref: string | null): string {
|
||||||
let importStringified;
|
|
||||||
if (isCSSImportRule(rule)) {
|
if (isCSSImportRule(rule)) {
|
||||||
|
let importStringified;
|
||||||
try {
|
try {
|
||||||
importStringified =
|
importStringified =
|
||||||
// for same-origin stylesheets,
|
// for same-origin stylesheets,
|
||||||
@@ -117,15 +119,25 @@ export function stringifyRule(rule: CSSRule): string {
|
|||||||
// work around browser issues with the raw string `@import url(...)` statement
|
// work around browser issues with the raw string `@import url(...)` statement
|
||||||
escapeImportStatement(rule);
|
escapeImportStatement(rule);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ignore
|
importStringified = rule.cssText;
|
||||||
}
|
}
|
||||||
} else if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) {
|
if (rule.styleSheet.href) {
|
||||||
// Safari does not escape selectors with : properly
|
// url()s within the imported stylesheet are relative to _that_ sheet's href
|
||||||
// see https://bugs.webkit.org/show_bug.cgi?id=184604
|
return absolutifyURLs(importStringified, rule.styleSheet.href);
|
||||||
return fixSafariColons(rule.cssText);
|
}
|
||||||
|
return importStringified;
|
||||||
|
} else {
|
||||||
|
let ruleStringified = rule.cssText;
|
||||||
|
if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) {
|
||||||
|
// Safari does not escape selectors with : properly
|
||||||
|
// see https://bugs.webkit.org/show_bug.cgi?id=184604
|
||||||
|
ruleStringified = fixSafariColons(ruleStringified);
|
||||||
|
}
|
||||||
|
if (sheetHref) {
|
||||||
|
return absolutifyURLs(ruleStringified, sheetHref);
|
||||||
|
}
|
||||||
|
return ruleStringified;
|
||||||
}
|
}
|
||||||
|
|
||||||
return importStringified || rule.cssText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fixSafariColons(cssStringified: string): string {
|
export function fixSafariColons(cssStringified: string): string {
|
||||||
@@ -351,3 +363,62 @@ export function extractFileExtension(
|
|||||||
const match = url.pathname.match(regex);
|
const match = url.pathname.match(regex);
|
||||||
return match?.[1] ?? null;
|
return match?.[1] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractOrigin(url: string): string {
|
||||||
|
let origin = '';
|
||||||
|
if (url.indexOf('//') > -1) {
|
||||||
|
origin = url.split('/').slice(0, 3).join('/');
|
||||||
|
} else {
|
||||||
|
origin = url.split('/')[0];
|
||||||
|
}
|
||||||
|
origin = origin.split('?')[0];
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm;
|
||||||
|
const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i;
|
||||||
|
const URL_WWW_MATCH = /^www\..*/i;
|
||||||
|
const DATA_URI = /^(data:)([^,]*),(.*)/i;
|
||||||
|
export function absolutifyURLs(cssText: string | null, href: string): string {
|
||||||
|
return (cssText || '').replace(
|
||||||
|
URL_IN_CSS_REF,
|
||||||
|
(
|
||||||
|
origin: string,
|
||||||
|
quote1: string,
|
||||||
|
path1: string,
|
||||||
|
quote2: string,
|
||||||
|
path2: string,
|
||||||
|
path3: string,
|
||||||
|
) => {
|
||||||
|
const filePath = path1 || path2 || path3;
|
||||||
|
const maybeQuote = quote1 || quote2 || '';
|
||||||
|
if (!filePath) {
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) {
|
||||||
|
return `url(${maybeQuote}${filePath}${maybeQuote})`;
|
||||||
|
}
|
||||||
|
if (DATA_URI.test(filePath)) {
|
||||||
|
return `url(${maybeQuote}${filePath}${maybeQuote})`;
|
||||||
|
}
|
||||||
|
if (filePath[0] === '/') {
|
||||||
|
return `url(${maybeQuote}${
|
||||||
|
extractOrigin(href) + filePath
|
||||||
|
}${maybeQuote})`;
|
||||||
|
}
|
||||||
|
const stack = href.split('/');
|
||||||
|
const parts = filePath.split('/');
|
||||||
|
stack.pop();
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part === '.') {
|
||||||
|
continue;
|
||||||
|
} else if (part === '..') {
|
||||||
|
stack.pop();
|
||||||
|
} else {
|
||||||
|
stack.push(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" />
|
||||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\" />
|
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\" />
|
||||||
<title>with style sheet</title>
|
<title>with style sheet</title>
|
||||||
<style>body { margin: 0px; background: url(\\"http://localhost:3030/a.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>\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/css/b.jpg\\"); }body > 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,<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>\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/css/should-be-in-css-folder.jpg\\"); }body > p { color: yellow; }</style>
|
||||||
</head><body>
|
</head><body>
|
||||||
</body></html>"
|
</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 name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" />
|
||||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\" />
|
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\" />
|
||||||
<title>with style sheet with import</title>
|
<title>with style sheet with import</title>
|
||||||
<style>@import url(\\"//fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&family=Roboto:wght@100;300;400;500;700&display=swap\\\\\\"\\");body { margin: 0px; background: url(\\"http://localhost:3030/a.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>\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/css/b.jpg\\"); }body > p { color: yellow; }</style>
|
<style>@import url(\\"//fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&family=Roboto:wght@100;300;400;500;700&display=swap\\\\\\"\\");body { margin: 0px; background: url(\\"http://localhost:3030/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>\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/css/should-be-in-css-folder.jpg\\"); }body > 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,<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>\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/alt-css/should-be-in-alt-css-folder.jpg\\"); }body > 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,<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>\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/alt-css/should-be-in-alt-css-folder.jpg\\"); }body > p { color: yellow; }section { background: url(\\"http://localhost:3030/should-be-in-root-folder.jpg\\"); }</style>
|
||||||
</head><body>
|
</head><body>
|
||||||
</body></html>"
|
</body></html>"
|
||||||
`;
|
`;
|
||||||
|
|||||||
12
packages/rrweb-snapshot/test/alt-css/alt-style.css
Normal file
12
packages/rrweb-snapshot/test/alt-css/alt-style.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -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 '//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 './style.css';
|
||||||
|
@import '../alt-css/alt-style.css';
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
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>');
|
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 {
|
p {
|
||||||
color: red;
|
color: red;
|
||||||
background: url('./b.jpg');
|
background: url('./should-be-in-css-folder.jpg');
|
||||||
}
|
}
|
||||||
body > p {
|
body > p {
|
||||||
color: yellow;
|
color: yellow;
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<title>with style sheet with import</title>
|
<title>with style sheet with import</title>
|
||||||
<link rel="stylesheet" href="/css/style-with-import.css">
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -3,63 +3,59 @@
|
|||||||
*/
|
*/
|
||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import {
|
import { serializeNodeWithId, _isBlockedElement } from '../src/snapshot';
|
||||||
absoluteToStylesheet,
|
|
||||||
serializeNodeWithId,
|
|
||||||
_isBlockedElement,
|
|
||||||
} from '../src/snapshot';
|
|
||||||
import snapshot from '../src/snapshot';
|
import snapshot from '../src/snapshot';
|
||||||
import { serializedNodeWithId, elementNode } from '../src/types';
|
import { serializedNodeWithId, elementNode } from '../src/types';
|
||||||
import { Mirror } from '../src/utils';
|
import { Mirror, absolutifyURLs } from '../src/utils';
|
||||||
|
|
||||||
describe('absolute url to stylesheet', () => {
|
describe('absolute url to stylesheet', () => {
|
||||||
const href = 'http://localhost/css/style.css';
|
const href = 'http://localhost/css/style.css';
|
||||||
|
|
||||||
it('can handle relative path', () => {
|
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)`,
|
`url(http://localhost/css/a.jpg)`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can handle same level path', () => {
|
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")`,
|
`url("http://localhost/css/a.jpg")`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can handle parent level path', () => {
|
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")`,
|
`url("http://localhost/a.jpg")`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can handle absolute path', () => {
|
it('can handle absolute path', () => {
|
||||||
expect(absoluteToStylesheet('url("/a.jpg")', href)).toEqual(
|
expect(absolutifyURLs('url("/a.jpg")', href)).toEqual(
|
||||||
`url("http://localhost/a.jpg")`,
|
`url("http://localhost/a.jpg")`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can handle external path', () => {
|
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")`,
|
`url("http://localhost/a.jpg")`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can handle single quote path', () => {
|
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')`,
|
`url('http://localhost/css/a.jpg')`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can handle no quote path', () => {
|
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)`,
|
`url(http://localhost/css/a.jpg)`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can handle multiple no quote paths', () => {
|
it('can handle multiple no quote paths', () => {
|
||||||
expect(
|
expect(
|
||||||
absoluteToStylesheet(
|
absolutifyURLs(
|
||||||
'background-image: url(images/b.jpg);background: #aabbcc url(images/a.jpg) 50% 50% repeat;',
|
'background-image: url(images/b.jpg);background: #aabbcc url(images/a.jpg) 50% 50% repeat;',
|
||||||
href,
|
href,
|
||||||
),
|
),
|
||||||
@@ -70,11 +66,11 @@ describe('absolute url to stylesheet', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle data url image', () => {
|
it('can handle data url image', () => {
|
||||||
|
expect(absolutifyURLs('url(data:image/gif;base64,ABC)', href)).toEqual(
|
||||||
|
'url(data:image/gif;base64,ABC)',
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
absoluteToStylesheet('url(data:image/gif;base64,ABC)', href),
|
absolutifyURLs(
|
||||||
).toEqual('url(data:image/gif;base64,ABC)');
|
|
||||||
expect(
|
|
||||||
absoluteToStylesheet(
|
|
||||||
'url(data:application/font-woff;base64,d09GMgABAAAAAAm)',
|
'url(data:application/font-woff;base64,d09GMgABAAAAAAm)',
|
||||||
href,
|
href,
|
||||||
),
|
),
|
||||||
@@ -83,7 +79,7 @@ describe('absolute url to stylesheet', () => {
|
|||||||
|
|
||||||
it('preserves quotes around inline svgs with spaces', () => {
|
it('preserves quotes around inline svgs with spaces', () => {
|
||||||
expect(
|
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\")",
|
"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,
|
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\")",
|
"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(
|
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>\')',
|
'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,
|
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>\')',
|
'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(
|
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>")',
|
'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,
|
href,
|
||||||
),
|
),
|
||||||
@@ -108,7 +104,7 @@ describe('absolute url to stylesheet', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can handle empty path', () => {
|
it('can handle empty path', () => {
|
||||||
expect(absoluteToStylesheet(`url('')`, href)).toEqual(`url('')`);
|
expect(absolutifyURLs(`url('')`, href)).toEqual(`url('')`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
37
packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts
Normal file
37
packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts
Normal 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 },
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -6,7 +6,7 @@ export default mergeConfig(
|
|||||||
configShared,
|
configShared,
|
||||||
defineProject({
|
defineProject({
|
||||||
test: {
|
test: {
|
||||||
// ... custom test config here
|
globals: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export class StylesheetManager {
|
|||||||
styles.push({
|
styles.push({
|
||||||
styleId,
|
styleId,
|
||||||
rules: Array.from(sheet.rules || CSSRule, (r, index) => ({
|
rules: Array.from(sheet.rules || CSSRule, (r, index) => ({
|
||||||
rule: stringifyRule(r),
|
rule: stringifyRule(r, sheet.href),
|
||||||
index,
|
index,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user