Add workaround for Chrome/Edge css import escaping bug (#1287)
* Upgrade to typescript 4.9.5 * Apply formatting changes * Add workaround for chrome incorrect escaping bug More info: https://bugs.chromium.org/p/chromium/issues/detail?id=1472259 * Apply formatting changes * Create itchy-dryers-double.md * Create rich-jars-remember.md * Apply formatting changes * Update packages/rrweb-snapshot/src/css.ts * Apply formatting changes * Update packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap * Apply formatting changes * Update snapshot * Apply formatting changes * Rename and refactor fixBrowserCompatibilityIssuesInCSSImports, getCssRulesString and getCssRuleString based on @eoghanmurray feedback * Apply formatting changes * Apply formatting changes
This commit is contained in:
@@ -17,9 +17,9 @@
|
||||
"rollup-plugin-typescript2": "^0.31.2",
|
||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||
"sirv-cli": "^0.4.4",
|
||||
"svelte": "^3.2.0",
|
||||
"svelte-check": "^1.4.0",
|
||||
"svelte-preprocess": "^4.0.0",
|
||||
"svelte": "^3.59.2",
|
||||
"svelte-check": "^3.0.1",
|
||||
"svelte-preprocess": "^5.0.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -345,7 +345,7 @@ export function parse(css: string, options: ParserOptions = {}) {
|
||||
whitespace();
|
||||
comments(rules);
|
||||
while (css.length && css.charAt(0) !== '}' && (node = atrule() || rule())) {
|
||||
if (node !== false) {
|
||||
if (node) {
|
||||
rules.push(node);
|
||||
comments(rules);
|
||||
}
|
||||
@@ -383,7 +383,7 @@ export function parse(css: string, options: ParserOptions = {}) {
|
||||
function comments(rules: Rule[] = []) {
|
||||
let c: Comment | void;
|
||||
while ((c = comment())) {
|
||||
if (c !== false) {
|
||||
if (c) {
|
||||
rules.push(c);
|
||||
}
|
||||
c = comment();
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
isShadowRoot,
|
||||
maskInputValue,
|
||||
isNativeShadowDom,
|
||||
getCssRulesString,
|
||||
stringifyStylesheet,
|
||||
getInputType,
|
||||
toLowerCase,
|
||||
validateStringifiedCssRule,
|
||||
@@ -554,7 +554,7 @@ function serializeTextNode(
|
||||
// to _only_ include the current rule(s) added by the text node.
|
||||
// So we'll be conservative and keep textContent as-is.
|
||||
} else if ((n.parentNode as HTMLStyleElement).sheet?.cssRules) {
|
||||
textContent = getCssRulesString(
|
||||
textContent = stringifyStylesheet(
|
||||
(n.parentNode as HTMLStyleElement).sheet!,
|
||||
);
|
||||
}
|
||||
@@ -644,7 +644,7 @@ function serializeElementNode(
|
||||
});
|
||||
let cssText: string | null = null;
|
||||
if (stylesheet) {
|
||||
cssText = getCssRulesString(stylesheet);
|
||||
cssText = stringifyStylesheet(stylesheet);
|
||||
}
|
||||
if (cssText) {
|
||||
delete attributes.rel;
|
||||
@@ -659,7 +659,7 @@ function serializeElementNode(
|
||||
// TODO: Currently we only try to get dynamic stylesheet when it is an empty style element
|
||||
!(n.innerText || n.textContent || '').trim().length
|
||||
) {
|
||||
const cssText = getCssRulesString(
|
||||
const cssText = stringifyStylesheet(
|
||||
(n as HTMLStyleElement).sheet as CSSStyleSheet,
|
||||
);
|
||||
if (cssText) {
|
||||
|
||||
@@ -54,12 +54,51 @@ function fixBrowserCompatibilityIssuesInCSS(cssText: string): string {
|
||||
return cssText;
|
||||
}
|
||||
|
||||
export function getCssRulesString(s: CSSStyleSheet): string | null {
|
||||
// Remove this declaration once typescript has added `CSSImportRule.supportsText` to the lib.
|
||||
declare interface CSSImportRule extends CSSRule {
|
||||
readonly href: string;
|
||||
readonly layerName: string | null;
|
||||
readonly media: MediaList;
|
||||
readonly styleSheet: CSSStyleSheet;
|
||||
/**
|
||||
* experimental API, currently only supported in firefox
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/CSSImportRule/supportsText
|
||||
*/
|
||||
readonly supportsText?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Browsers sometimes incorrectly escape `@import` on `.cssText` statements.
|
||||
* This function tries to correct the escaping.
|
||||
* more info: https://bugs.chromium.org/p/chromium/issues/detail?id=1472259
|
||||
* @param cssImportRule
|
||||
* @returns `cssText` with browser inconsistencies fixed, or null if not applicable.
|
||||
*/
|
||||
export function escapeImportStatement(rule: CSSImportRule): string {
|
||||
const { cssText } = rule;
|
||||
if (cssText.split('"').length < 3) return cssText;
|
||||
|
||||
const statement = ['@import', `url(${JSON.stringify(rule.href)})`];
|
||||
if (rule.layerName === '') {
|
||||
statement.push(`layer`);
|
||||
} else if (rule.layerName) {
|
||||
statement.push(`layer(${rule.layerName})`);
|
||||
}
|
||||
if (rule.supportsText) {
|
||||
statement.push(`supports(${rule.supportsText})`);
|
||||
}
|
||||
if (rule.media.length) {
|
||||
statement.push(rule.media.mediaText);
|
||||
}
|
||||
return statement.join(' ') + ';';
|
||||
}
|
||||
|
||||
export function stringifyStylesheet(s: CSSStyleSheet): string | null {
|
||||
try {
|
||||
const rules = s.rules || s.cssRules;
|
||||
return rules
|
||||
? fixBrowserCompatibilityIssuesInCSS(
|
||||
Array.from(rules).map(getCssRuleString).join(''),
|
||||
Array.from(rules).map(stringifyRule).join(''),
|
||||
)
|
||||
: null;
|
||||
} catch (error) {
|
||||
@@ -67,16 +106,22 @@ export function getCssRulesString(s: CSSStyleSheet): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function getCssRuleString(rule: CSSRule): string {
|
||||
let cssStringified = rule.cssText;
|
||||
export function stringifyRule(rule: CSSRule): string {
|
||||
let importStringified;
|
||||
if (isCSSImportRule(rule)) {
|
||||
try {
|
||||
cssStringified = getCssRulesString(rule.styleSheet) || cssStringified;
|
||||
} catch {
|
||||
importStringified =
|
||||
// for same-origin stylesheets,
|
||||
// we can access the imported stylesheet rules directly
|
||||
stringifyStylesheet(rule.styleSheet) ||
|
||||
// work around browser issues with the raw string `@import url(...)` statement
|
||||
escapeImportStatement(rule);
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return validateStringifiedCssRule(cssStringified);
|
||||
|
||||
return validateStringifiedCssRule(importStringified || rule.cssText);
|
||||
}
|
||||
|
||||
export function validateStringifiedCssRule(cssStringified: string): string {
|
||||
|
||||
@@ -490,7 +490,7 @@ 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>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/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>
|
||||
</head><body>
|
||||
</body></html>"
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { parse, Rule, Media } from '../src/css';
|
||||
import { validateStringifiedCssRule } from './../src/utils';
|
||||
import {
|
||||
validateStringifiedCssRule,
|
||||
escapeImportStatement,
|
||||
} from './../src/utils';
|
||||
|
||||
describe('css parser', () => {
|
||||
it('should save the filename and source', () => {
|
||||
@@ -120,4 +123,64 @@ describe('css parser', () => {
|
||||
);
|
||||
expect(out3).toEqual('[data-aa\\:other] { color: red; }');
|
||||
});
|
||||
|
||||
it('parses imports with quotes correctly', () => {
|
||||
const out1 = escapeImportStatement({
|
||||
cssText: `@import url("/foo.css;900;800"");`,
|
||||
href: '/foo.css;900;800"',
|
||||
media: {
|
||||
length: 0,
|
||||
},
|
||||
layerName: null,
|
||||
supportsText: null,
|
||||
} as unknown as CSSImportRule);
|
||||
expect(out1).toEqual(`@import url("/foo.css;900;800\\"");`);
|
||||
|
||||
const out2 = escapeImportStatement({
|
||||
cssText: `@import url("/foo.css;900;800"") supports(display: flex);`,
|
||||
href: '/foo.css;900;800"',
|
||||
media: {
|
||||
length: 0,
|
||||
},
|
||||
layerName: null,
|
||||
supportsText: 'display: flex',
|
||||
} as unknown as CSSImportRule);
|
||||
expect(out2).toEqual(
|
||||
`@import url("/foo.css;900;800\\"") supports(display: flex);`,
|
||||
);
|
||||
|
||||
const out3 = escapeImportStatement({
|
||||
cssText: `@import url("/foo.css;900;800"");`,
|
||||
href: '/foo.css;900;800"',
|
||||
media: {
|
||||
length: 1,
|
||||
mediaText: 'print, screen',
|
||||
},
|
||||
layerName: null,
|
||||
supportsText: null,
|
||||
} as unknown as CSSImportRule);
|
||||
expect(out3).toEqual(`@import url("/foo.css;900;800\\"") print, screen;`);
|
||||
|
||||
const out4 = escapeImportStatement({
|
||||
cssText: `@import url("/foo.css;900;800"") layer(layer-1);`,
|
||||
href: '/foo.css;900;800"',
|
||||
media: {
|
||||
length: 0,
|
||||
},
|
||||
layerName: 'layer-1',
|
||||
supportsText: null,
|
||||
} as unknown as CSSImportRule);
|
||||
expect(out4).toEqual(`@import url("/foo.css;900;800\\"") layer(layer-1);`);
|
||||
|
||||
const out5 = escapeImportStatement({
|
||||
cssText: `@import url("/foo.css;900;800"") layer;`,
|
||||
href: '/foo.css;900;800"',
|
||||
media: {
|
||||
length: 0,
|
||||
},
|
||||
layerName: '',
|
||||
supportsText: null,
|
||||
} as unknown as CSSImportRule);
|
||||
expect(out5).toEqual(`@import url("/foo.css;900;800\\"") layer;`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
@import "./style.css";
|
||||
@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';
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"@types/chai": "^4.1.6",
|
||||
"@types/dom-mediacapture-transform": "^0.1.3",
|
||||
"@types/dom-mediacapture-transform": "0.1.4",
|
||||
"@types/inquirer": "^8.2.1",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/jest-image-snapshot": "^6.1.0",
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// This informs the TS compiler about constructed stylesheets.
|
||||
// It can be removed when this is fixed: https://github.com/Microsoft/TypeScript/issues/30022
|
||||
declare interface DocumentOrShadowRoot {
|
||||
adoptedStyleSheets?: CSSStyleSheet[];
|
||||
}
|
||||
|
||||
declare interface CSSStyleSheet {
|
||||
replace?(text: string): Promise<CSSStyleSheet>;
|
||||
replaceSync?(text: string): void;
|
||||
}
|
||||
@@ -19,19 +19,19 @@ export default function initCanvasContextObserver(
|
||||
'getContext',
|
||||
function (
|
||||
original: (
|
||||
this: ICanvas,
|
||||
this: ICanvas | HTMLCanvasElement,
|
||||
contextType: string,
|
||||
...args: Array<unknown>
|
||||
) => void,
|
||||
) {
|
||||
return function (
|
||||
this: ICanvas,
|
||||
this: ICanvas | HTMLCanvasElement,
|
||||
contextType: string,
|
||||
...args: Array<unknown>
|
||||
) {
|
||||
if (!isBlocked(this, blockClass, blockSelector, true)) {
|
||||
const ctxName = getNormalizedContextName(contextType);
|
||||
if (!('__context' in this)) this.__context = ctxName;
|
||||
if (!('__context' in this)) (this as ICanvas).__context = ctxName;
|
||||
|
||||
if (
|
||||
setPreserveDrawingBufferToTrue &&
|
||||
|
||||
@@ -125,7 +125,7 @@ export function serializeArg(
|
||||
};
|
||||
}
|
||||
|
||||
return value as CanvasArg;
|
||||
return value as unknown as CanvasArg;
|
||||
}
|
||||
|
||||
export const serializeArgs = (
|
||||
|
||||
@@ -49,7 +49,10 @@ function patchGLPrototype(
|
||||
return function (this: typeof prototype, ...args: Array<unknown>) {
|
||||
const result = original.apply(this, args);
|
||||
saveWebGLVar(result, win, this);
|
||||
if (!isBlocked(this.canvas, blockClass, blockSelector, true)) {
|
||||
if (
|
||||
'tagName' in this.canvas &&
|
||||
!isBlocked(this.canvas, blockClass, blockSelector, true)
|
||||
) {
|
||||
const recordArgs = serializeArgs([...args], win, this);
|
||||
const mutation: canvasMutationWithType = {
|
||||
type,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { elementNode, serializedNodeWithId } from 'rrweb-snapshot';
|
||||
import { getCssRuleString } from 'rrweb-snapshot';
|
||||
import { stringifyRule } from 'rrweb-snapshot';
|
||||
import type {
|
||||
adoptedStyleSheetCallback,
|
||||
adoptedStyleSheetParam,
|
||||
@@ -66,7 +66,7 @@ export class StylesheetManager {
|
||||
styleId,
|
||||
rules: rules.map((r, index) => {
|
||||
return {
|
||||
rule: getCssRuleString(r),
|
||||
rule: stringifyRule(r),
|
||||
index,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"vite": "^3.2.0-beta.2",
|
||||
"vite-plugin-dts": "^1.6.6"
|
||||
"vite-plugin-dts": "^1.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"rrweb-snapshot": "^2.0.0-alpha.10"
|
||||
|
||||
Reference in New Issue
Block a user