From 6da1ec4c653fd667454db9de0ff860f9e4f23e9c Mon Sep 17 00:00:00 2001 From: Daniel Engelke Date: Wed, 24 Jan 2024 15:21:36 +0800 Subject: [PATCH 1/5] Fix known issues --- packages/rrweb-snapshot/src/css.ts | 56 +++++++++++++++++++++--- packages/rrweb-snapshot/test/css.test.ts | 29 ++++++++++++ 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts index d7a413eb67..b2dd70cece 100644 --- a/packages/rrweb-snapshot/src/css.ts +++ b/packages/rrweb-snapshot/src/css.ts @@ -431,22 +431,64 @@ export function parse(css: string, options: ParserOptions = {}) { */ function selector() { - const m = match(/^([^{]+)/); + + whitespace(); while(css[0] == '}'){ error('extra closing bracket'); css = css.slice(1); whitespace(); } + + const m = match(/^(("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|[^{])+)/); if (!m) { return; } /* @fix Remove all comments from selectors * http://ostermiller.org/findcomment.html */ - return trim(m[0]) + const cleanedInput = m[0].trim() .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '') + + // Handle strings by replacing commas inside them .replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, (m) => { return m.replace(/,/g, '\u200C'); - }) - .split(/\s*(?![^(]*\)),\s*/) - .map((s) => { - return s.replace(/\u200C/g, ','); }); - } + + // Split using a custom function and restore commas in strings + return customSplit(cleanedInput).map(s => s.replace(/\u200C/g, ',').trim()); + } + + /** + * Split selector correctly, ensuring not to split on comma if inside (). + */ + + function customSplit(input: string) { + let result = []; + let currentSegment = ''; + let depthParentheses = 0; // Track depth of parentheses + let depthBrackets = 0; // Track depth of square brackets + + for (let char of input) { + if (char === '(') { + depthParentheses++; + } else if (char === ')') { + depthParentheses--; + } else if (char === '[') { + depthBrackets++; + } else if (char === ']') { + depthBrackets--; + } + + // Split point is a comma that is not inside parentheses or square brackets + if (char === ',' && depthParentheses === 0 && depthBrackets === 0) { + result.push(currentSegment); + currentSegment = ''; + } else { + currentSegment += char; + } + } + + // Add the last segment + if (currentSegment) { + result.push(currentSegment); + } + + return result; + } /** * Parse declaration. diff --git a/packages/rrweb-snapshot/test/css.test.ts b/packages/rrweb-snapshot/test/css.test.ts index 2818386071..bce7285079 100644 --- a/packages/rrweb-snapshot/test/css.test.ts +++ b/packages/rrweb-snapshot/test/css.test.ts @@ -78,6 +78,35 @@ describe('css parser', () => { expect(errors[0].filename).toEqual('foo.css'); }); + it('should parse selector with comma nested inside ()', () => { + const result = parse( + '[_nghost-ng-c4172599085]:not(.fit-content).aim-select:hover:not(:disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--invalid, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--active) { border-color: rgb(84, 84, 84); }', + ); + + expect(result.parent).toEqual(null); + + const rules = result.stylesheet!.rules; + expect(rules.length).toEqual(1); + + let rule = rules[0] as Rule; + expect(rule.parent).toEqual(result); + expect(rule.selectors?.length).toEqual(1); + + let decl = rule.declarations![0]; + expect(decl.parent).toEqual(rule); + }); + + it('parses { and } in attribute selectors correctly', () => { + const result = parse('foo[someAttr~="{someId}"] { color: red; }'); + const rules = result.stylesheet!.rules; + + expect(rules.length).toEqual(1); + + const rule = rules[0] as Rule; + + expect(rule.selectors![0]).toEqual('foo[someAttr~="{someId}"]'); + }); + it('should set parent property', () => { const result = parse( 'thing { test: value; }\n' + From 4f30d3acbde59e4e04fc4dc8cdf6bca91810cc9b Mon Sep 17 00:00:00 2001 From: Daniel Engelke Date: Wed, 24 Jan 2024 15:28:25 +0800 Subject: [PATCH 2/5] Run format --- packages/rrweb-snapshot/src/css.ts | 89 +++++++++++++----------- packages/rrweb-snapshot/test/css.test.ts | 6 +- 2 files changed, 51 insertions(+), 44 deletions(-) diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts index b2dd70cece..30c58fbd4f 100644 --- a/packages/rrweb-snapshot/src/css.ts +++ b/packages/rrweb-snapshot/src/css.ts @@ -431,16 +431,21 @@ export function parse(css: string, options: ParserOptions = {}) { */ function selector() { + whitespace(); + while (css[0] == '}') { + error('extra closing bracket'); + css = css.slice(1); + whitespace(); + } - whitespace(); while(css[0] == '}'){ error('extra closing bracket'); css = css.slice(1); whitespace(); } - - const m = match(/^(("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|[^{])+)/); + const m = match(/^(("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|[^{])+)/); if (!m) { return; } /* @fix Remove all comments from selectors * http://ostermiller.org/findcomment.html */ - const cleanedInput = m[0].trim() + const cleanedInput = m[0] + .trim() .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '') // Handle strings by replacing commas inside them @@ -448,48 +453,50 @@ export function parse(css: string, options: ParserOptions = {}) { return m.replace(/,/g, '\u200C'); }); - // Split using a custom function and restore commas in strings - return customSplit(cleanedInput).map(s => s.replace(/\u200C/g, ',').trim()); - } - - /** - * Split selector correctly, ensuring not to split on comma if inside (). - */ - - function customSplit(input: string) { - let result = []; - let currentSegment = ''; - let depthParentheses = 0; // Track depth of parentheses - let depthBrackets = 0; // Track depth of square brackets - - for (let char of input) { - if (char === '(') { - depthParentheses++; - } else if (char === ')') { - depthParentheses--; - } else if (char === '[') { - depthBrackets++; - } else if (char === ']') { - depthBrackets--; - } - - // Split point is a comma that is not inside parentheses or square brackets - if (char === ',' && depthParentheses === 0 && depthBrackets === 0) { - result.push(currentSegment); - currentSegment = ''; - } else { - currentSegment += char; - } + // Split using a custom function and restore commas in strings + return customSplit(cleanedInput).map((s) => + s.replace(/\u200C/g, ',').trim(), + ); + } + + /** + * Split selector correctly, ensuring not to split on comma if inside (). + */ + + function customSplit(input: string) { + let result = []; + let currentSegment = ''; + let depthParentheses = 0; // Track depth of parentheses + let depthBrackets = 0; // Track depth of square brackets + + for (let char of input) { + if (char === '(') { + depthParentheses++; + } else if (char === ')') { + depthParentheses--; + } else if (char === '[') { + depthBrackets++; + } else if (char === ']') { + depthBrackets--; } - - // Add the last segment - if (currentSegment) { + + // Split point is a comma that is not inside parentheses or square brackets + if (char === ',' && depthParentheses === 0 && depthBrackets === 0) { result.push(currentSegment); + currentSegment = ''; + } else { + currentSegment += char; } - - return result; } + // Add the last segment + if (currentSegment) { + result.push(currentSegment); + } + + return result; + } + /** * Parse declaration. */ diff --git a/packages/rrweb-snapshot/test/css.test.ts b/packages/rrweb-snapshot/test/css.test.ts index bce7285079..d3c9482a90 100644 --- a/packages/rrweb-snapshot/test/css.test.ts +++ b/packages/rrweb-snapshot/test/css.test.ts @@ -99,11 +99,11 @@ describe('css parser', () => { it('parses { and } in attribute selectors correctly', () => { const result = parse('foo[someAttr~="{someId}"] { color: red; }'); const rules = result.stylesheet!.rules; - + expect(rules.length).toEqual(1); - + const rule = rules[0] as Rule; - + expect(rule.selectors![0]).toEqual('foo[someAttr~="{someId}"]'); }); From 1d6d0b863471236f09047c146212314384b29e2a Mon Sep 17 00:00:00 2001 From: Daniel Engelke Date: Wed, 24 Jan 2024 15:34:15 +0800 Subject: [PATCH 3/5] Fix linting errors --- packages/rrweb-snapshot/src/css.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts index 30c58fbd4f..4113e293b7 100644 --- a/packages/rrweb-snapshot/src/css.ts +++ b/packages/rrweb-snapshot/src/css.ts @@ -464,12 +464,12 @@ export function parse(css: string, options: ParserOptions = {}) { */ function customSplit(input: string) { - let result = []; + const result = []; let currentSegment = ''; let depthParentheses = 0; // Track depth of parentheses let depthBrackets = 0; // Track depth of square brackets - for (let char of input) { + for (const char of input) { if (char === '(') { depthParentheses++; } else if (char === ')') { From 2d43ffdf82b24c27158395bdfd114506ef6e7552 Mon Sep 17 00:00:00 2001 From: Daniel Engelke Date: Sat, 3 Feb 2024 21:04:45 +0800 Subject: [PATCH 4/5] Add comment in code for source of match logic --- packages/rrweb-snapshot/src/css.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts index 4113e293b7..82b4c41f96 100644 --- a/packages/rrweb-snapshot/src/css.ts +++ b/packages/rrweb-snapshot/src/css.ts @@ -438,6 +438,7 @@ export function parse(css: string, options: ParserOptions = {}) { whitespace(); } + // Use match logic from https://github.com/NxtChg/pieces/blob/3eb39c8287a97632e9347a24f333d52d916bc816/js/css_parser/css_parse.js#L46C1-L47C1 const m = match(/^(("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|[^{])+)/); if (!m) { return; From 1f0316cf3b07c4a71b3f9aeab4a2d0d9611a34dd Mon Sep 17 00:00:00 2001 From: Daniel Engelke Date: Sat, 3 Feb 2024 21:04:55 +0800 Subject: [PATCH 5/5] Add changeset --- .changeset/rich-dots-lay.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rich-dots-lay.md diff --git a/.changeset/rich-dots-lay.md b/.changeset/rich-dots-lay.md new file mode 100644 index 0000000000..ba889e446e --- /dev/null +++ b/.changeset/rich-dots-lay.md @@ -0,0 +1,5 @@ +--- +'rrweb-snapshot': patch +--- + +Fix css parsing errors