diff --git a/.changeset/smooth-ghosts-wonder.md b/.changeset/smooth-ghosts-wonder.md new file mode 100644 index 0000000000..87286e8b80 --- /dev/null +++ b/.changeset/smooth-ghosts-wonder.md @@ -0,0 +1,5 @@ +--- +"@rrweb/record": patch +--- + +fix 'CssSyntaxError: Unclosed string' for invalid css rules diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 418ce8230a..c0aa43850b 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -150,7 +150,9 @@ export function stringifyRule(rule: CSSRule, sheetHref: string | null): string { } return importStringified; } else { - let ruleStringified = rule.cssText; + // Removes incomprehensible empty .cssText rules + // see https://github.com/rrweb-io/rrweb/issues/1734 + let ruleStringified = fixEmptyCssStyles(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 @@ -169,6 +171,18 @@ export function fixSafariColons(cssStringified: string): string { return cssStringified.replace(regex, '$1\\$2'); } +export function fixEmptyCssStyles(cssStringified: string) { + const quickTestRegExp = /:\s*;/; + + if (quickTestRegExp.test(cssStringified)) { + // Remove e.g. "border-top-style: ;" + const regex = /(?<=^|\{|;)\s*[_a-zA-Z][_a-zA-Z0-9-]*:\s*;/gm; + return cssStringified.replace(regex, ''); + } + + return cssStringified; +} + export function isCSSImportRule(rule: CSSRule): rule is CSSImportRule { return 'styleSheet' in rule; } diff --git a/packages/rrweb-snapshot/test/utils.test.ts b/packages/rrweb-snapshot/test/utils.test.ts index f4b68245cc..2284b9ec66 100644 --- a/packages/rrweb-snapshot/test/utils.test.ts +++ b/packages/rrweb-snapshot/test/utils.test.ts @@ -6,6 +6,7 @@ import { escapeImportStatement, extractFileExtension, fixSafariColons, + fixEmptyCssStyles, isNodeMetaEqual, stringifyStylesheet, } from '../src/utils'; @@ -282,6 +283,19 @@ describe('utils', () => { }); }); + describe('fixEmptyCssStyles', () => { + it('removes empty css styles', () => { + const input = + '.cl { font-family: sans-serif; font-size: 13px; color: var(--bug-text); border-top-style: ; border-top-width: ; border-right-style: ; border-right-width: ; border-bottom-style: ; border-bottom-width: ; border-left-style: ; border-left-width: ; border-image-source: ; border-image-slice: ; border-image-width: ; border-image-outset: ; border-image-repeat: ; border-color: var(--bug-border); background-color: var(--bug-background-primary); }'; + + const out1 = fixEmptyCssStyles(input); + + expect(out1).toEqual( + '.cl { font-family: sans-serif; font-size: 13px; color: var(--bug-text); border-color: var(--bug-border); background-color: var(--bug-background-primary); }', + ); + }); + }); + describe('stringifyStylesheet', () => { it('returns null if rules are missing', () => { const mockSheet = {