diff --git a/.changeset/thirty-baboons-punch.md b/.changeset/thirty-baboons-punch.md new file mode 100644 index 0000000000..1dd1c36fad --- /dev/null +++ b/.changeset/thirty-baboons-punch.md @@ -0,0 +1,5 @@ +--- +'rrweb-snapshot': patch +--- + +Fix CSS rules captured in Safari diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 51f76f46f8..bb18542d26 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -22,6 +22,7 @@ import { getCssRulesString, getInputType, toLowerCase, + validateStringifiedCssRule, } from './utils'; let _id = 1; @@ -53,7 +54,9 @@ function getValidTagName(element: HTMLElement): Lowercase { function stringifyStyleSheet(sheet: CSSStyleSheet): string { return sheet.cssRules ? Array.from(sheet.cssRules) - .map((rule) => rule.cssText || '') + .map((rule) => + rule.cssText ? validateStringifiedCssRule(rule.cssText) : '', + ) .join('') : ''; } diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index b124680b57..06e3b7a010 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -76,6 +76,17 @@ export function getCssRuleString(rule: CSSRule): string { // ignore } } + return validateStringifiedCssRule(cssStringified); +} + +export function validateStringifiedCssRule(cssStringified: string): string { + // Safari does not escape selectors with : properly + if (cssStringified.includes(':')) { + // Replace e.g. [aa:bb] with [aa\\:bb] + const regex = /(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm; + return cssStringified.replace(regex, '$1\\$2'); + } + return cssStringified; } diff --git a/packages/rrweb-snapshot/test/css.test.ts b/packages/rrweb-snapshot/test/css.test.ts index 4461022beb..328ecc77d6 100644 --- a/packages/rrweb-snapshot/test/css.test.ts +++ b/packages/rrweb-snapshot/test/css.test.ts @@ -1,4 +1,5 @@ import { parse, Rule, Media } from '../src/css'; +import { validateStringifiedCssRule } from './../src/utils'; describe('css parser', () => { it('should save the filename and source', () => { @@ -106,4 +107,17 @@ describe('css parser', () => { decl = rule.declarations![0]; expect(decl.parent).toEqual(rule); }); + + it('parses : in attribute selectors correctly', () => { + const out1 = validateStringifiedCssRule('[data-foo] { color: red; }'); + expect(out1).toEqual('[data-foo] { color: red; }'); + + const out2 = validateStringifiedCssRule('[data-foo:other] { color: red; }'); + expect(out2).toEqual('[data-foo\\:other] { color: red; }'); + + const out3 = validateStringifiedCssRule( + '[data-aa\\:other] { color: red; }', + ); + expect(out3).toEqual('[data-aa\\:other] { color: red; }'); + }); });