diff --git a/.changeset/modern-doors-watch.md b/.changeset/modern-doors-watch.md new file mode 100644 index 0000000000..b74bfbad45 --- /dev/null +++ b/.changeset/modern-doors-watch.md @@ -0,0 +1,5 @@ +--- +'rrweb-snapshot': patch +--- + +better nested css selector splitting when commas or brackets happen to be in quoted text diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts index 4645deae4f..1a3157d40f 100644 --- a/packages/rrweb-snapshot/src/css.ts +++ b/packages/rrweb-snapshot/src/css.ts @@ -437,6 +437,7 @@ export function parse(css: string, options: ParserOptions = {}): Stylesheet { if (!m) { return; } + /* @fix Remove all comments from selectors * http://ostermiller.org/findcomment.html */ const cleanedInput = m[0] @@ -463,9 +464,16 @@ export function parse(css: string, options: ParserOptions = {}): Stylesheet { let currentSegment = ''; let depthParentheses = 0; // Track depth of parentheses let depthBrackets = 0; // Track depth of square brackets + let currentStringChar = null; for (const char of input) { - if (char === '(') { + const hasStringEscape = currentSegment.endsWith('\\'); + + if (currentStringChar) { + if (currentStringChar === char && !hasStringEscape) { + currentStringChar = null; + } + } else if (char === '(') { depthParentheses++; } else if (char === ')') { depthParentheses--; @@ -473,6 +481,8 @@ export function parse(css: string, options: ParserOptions = {}): Stylesheet { depthBrackets++; } else if (char === ']') { depthBrackets--; + } else if ('\'"'.includes(char)) { + currentStringChar = char; } // Split point is a comma that is not inside parentheses or square brackets diff --git a/packages/rrweb-snapshot/test/css.test.ts b/packages/rrweb-snapshot/test/css.test.ts index d3c9482a90..6f10f6e569 100644 --- a/packages/rrweb-snapshot/test/css.test.ts +++ b/packages/rrweb-snapshot/test/css.test.ts @@ -148,6 +148,50 @@ describe('css parser', () => { expect(out3).toEqual('[data-aa\\:other] { color: red; }'); }); + it('parses nested commas in selectors correctly', () => { + const result = parse( + ` +body > ul :is(li:not(:first-of-type) a:hover, li:not(:first-of-type).active a) { + background: red; +} +`, + ); + expect((result.stylesheet!.rules[0] as Rule)!.selectors!.length).toEqual(1); + + const trickresult = parse( + ` +li[attr="weirdly("] a:hover, li[attr="weirdly)"] a { + background-color: red; +} +`, + ); + expect( + (trickresult.stylesheet!.rules[0] as Rule)!.selectors!.length, + ).toEqual(2); + + const weirderresult = parse( + ` +li[attr="weirder\\"("] a:hover, li[attr="weirder\\")"] a { + background-color: red; +} +`, + ); + expect( + (weirderresult.stylesheet!.rules[0] as Rule)!.selectors!.length, + ).toEqual(2); + + const commainstrresult = parse( + ` +li[attr="has,comma"] a:hover { + background-color: red; +} +`, + ); + expect( + (commainstrresult.stylesheet!.rules[0] as Rule)!.selectors!.length, + ).toEqual(1); + }); + it('parses imports with quotes correctly', () => { const out1 = escapeImportStatement({ cssText: `@import url("/foo.css;900;800"");`,