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:
Justin Halsall
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 6de70cbf21
commit a6812827e0
18 changed files with 343 additions and 156 deletions

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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 {