Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/six-llamas-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rrweb-snapshot": patch
---

Fix `url()` rewrite for nested stylesheets by rewriting during stringification instead of after
1 change: 1 addition & 0 deletions packages/rrweb-snapshot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"test:watch": "vitest watch",
"retest:update": "vitest run --update",
"test:update": "yarn build && vitest run --update",
"bench": "vite build && vitest bench",
"dev": "vite build --watch",
"build": "yarn turbo prepublish -F rrweb-snapshot",
"check-types": "tsc --noEmit",
Expand Down
71 changes: 5 additions & 66 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
getInputType,
toLowerCase,
extractFileExtension,
absolutifyURLs,
} from './utils';

let _id = 1;
Expand Down Expand Up @@ -53,71 +54,9 @@ function getValidTagName(element: HTMLElement): Lowercase<string> {
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 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
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
Expand Down Expand Up @@ -254,7 +193,7 @@ export function transformAttribute(
} else if (name === 'srcset') {
return getAbsoluteSrcsetString(doc, value);
} else if (name === 'style') {
return absoluteToStylesheet(value, getHref(doc));
return absolutifyURLs(value, getHref(doc));
} else if (tagName === 'object' && name === 'data') {
return absoluteToDoc(doc, value);
}
Expand Down Expand Up @@ -584,7 +523,7 @@ function serializeTextNode(
n,
);
}
textContent = absoluteToStylesheet(textContent, getHref(options.doc));
textContent = absolutifyURLs(textContent, getHref(options.doc));
}
if (isScript) {
textContent = 'SCRIPT_PLACEHOLDER';
Expand Down Expand Up @@ -664,7 +603,7 @@ function serializeElementNode(
if (cssText) {
delete attributes.rel;
delete attributes.href;
attributes._cssText = absoluteToStylesheet(cssText, stylesheet!.href!);
attributes._cssText = cssText;
}
}
// dynamic stylesheet
Expand All @@ -678,7 +617,7 @@ function serializeElementNode(
(n as HTMLStyleElement).sheet as CSSStyleSheet,
);
if (cssText) {
attributes._cssText = absoluteToStylesheet(cssText, getHref(doc));
attributes._cssText = cssText;
}
}
// form fields
Expand Down
99 changes: 85 additions & 14 deletions packages/rrweb-snapshot/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,21 @@ export function escapeImportStatement(rule: CSSImportRule): string {
export function stringifyStylesheet(s: CSSStyleSheet): string | null {
try {
const rules = s.rules || s.cssRules;
return rules
? fixBrowserCompatibilityIssuesInCSS(
Array.from(rules, stringifyRule).join(''),
)
: null;
if (!rules) {
return null;
}
const stringifiedRules = Array.from(rules, (rule: CSSRule) =>
stringifyRule(rule, s.href),
).join('');
return fixBrowserCompatibilityIssuesInCSS(stringifiedRules);
} catch (error) {
return null;
}
}

export function stringifyRule(rule: CSSRule): string {
let importStringified;
export function stringifyRule(rule: CSSRule, sheetHref: string | null): string {
if (isCSSImportRule(rule)) {
let importStringified;
try {
importStringified =
// for same-origin stylesheets,
Expand All @@ -117,15 +119,25 @@ export function stringifyRule(rule: CSSRule): string {
// work around browser issues with the raw string `@import url(...)` statement
escapeImportStatement(rule);
} catch (error) {
// ignore
importStringified = rule.cssText;
}
if (rule.styleSheet.href) {
// url()s within the imported stylesheet are relative to _that_ sheet's href
return absolutifyURLs(importStringified, rule.styleSheet.href);
}
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);
}
} else if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) {
// Safari does not escape selectors with : properly
// see https://bugs.webkit.org/show_bug.cgi?id=184604
return fixSafariColons(rule.cssText);
if (sheetHref) {
return absolutifyURLs(ruleStringified, sheetHref);
}
return ruleStringified;
}

return importStringified || rule.cssText;
}

export function fixSafariColons(cssStringified: string): string {
Expand Down Expand Up @@ -351,3 +363,62 @@ export function extractFileExtension(
const match = url.pathname.match(regex);
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})`;
},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>"
`;
Expand All @@ -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>"
`;
Expand Down
12 changes: 12 additions & 0 deletions packages/rrweb-snapshot/test/alt-css/alt-style.css
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions packages/rrweb-snapshot/test/css/style-with-import.css
Original file line number Diff line number Diff line change
@@ -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';
4 changes: 2 additions & 2 deletions packages/rrweb-snapshot/test/css/style.css
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Loading