Skip to content

Commit fedff7c

Browse files
committed
Reapply "fix: Incorrect parsing of functional pseudo class css selector (#169)" (#182)
This reverts commit de6cd2b, reversing changes made to 9e07245.
1 parent 7700d91 commit fedff7c

File tree

4 files changed

+87
-147
lines changed

4 files changed

+87
-147
lines changed

.changeset/modern-doors-watch.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

.changeset/rich-dots-lay.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/rrweb-snapshot/src/css.ts

Lines changed: 79 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -431,81 +431,102 @@ export function parse(css: string, options: ParserOptions = {}) {
431431
*/
432432

433433
function selector() {
434-
whitespace();
435-
while (css[0] == '}') {
436-
error('extra closing bracket');
437-
css = css.slice(1);
438-
whitespace();
439-
}
440-
441-
// Use match logic from https://github.com/NxtChg/pieces/blob/3eb39c8287a97632e9347a24f333d52d916bc816/js/css_parser/css_parse.js#L46C1-L47C1
442-
const m = match(/^(("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|[^{])+)/);
434+
const m = match(/^([^{]+)/);
443435
if (!m) {
444436
return;
445437
}
446-
447438
/* @fix Remove all comments from selectors
448439
* http://ostermiller.org/findcomment.html */
449-
const cleanedInput = m[0]
450-
.trim()
440+
const splitSelectors = trim(m[0])
451441
.replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '')
452-
453-
// Handle strings by replacing commas inside them
454442
.replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, (m) => {
455443
return m.replace(/,/g, '\u200C');
456-
});
457-
458-
// Split using a custom function and restore commas in strings
459-
return customSplit(cleanedInput).map((s) =>
460-
s.replace(/\u200C/g, ',').trim(),
461-
);
462-
}
444+
})
445+
.split(/\s*(?![^(]*\)),\s*/);
463446

464-
/**
465-
* Split selector correctly, ensuring not to split on comma if inside ().
466-
*/
467-
468-
function customSplit(input: string) {
469-
const result = [];
470-
let currentSegment = '';
471-
let depthParentheses = 0; // Track depth of parentheses
472-
let depthBrackets = 0; // Track depth of square brackets
473-
let currentStringChar = null;
447+
if (splitSelectors.length <= 1) {
448+
return splitSelectors.map((s) => {
449+
return s.replace(/\u200C/g, ',');
450+
});
451+
}
474452

475-
for (const char of input) {
476-
const hasStringEscape = currentSegment.endsWith('\\');
453+
// For each selector, need to check if we properly split on `,`
454+
// Example case where selector is:
455+
// .bar:has(input:is(:disabled), button:is(:disabled))
456+
let i = 0;
457+
let j = 0;
458+
const len = splitSelectors.length;
459+
const finalSelectors = [];
460+
while (i < len) {
461+
// Look for selectors with opening parens - `(` and search rest of
462+
// selectors for the first one with matching number of closing
463+
// parens `)`
464+
const openingParensCount = (splitSelectors[i].match(/\(/g) || []).length;
465+
const closingParensCount = (splitSelectors[i].match(/\)/g) || []).length;
466+
let unbalancedParens = openingParensCount - closingParensCount;
467+
468+
if (unbalancedParens >= 1) {
469+
// At least one opening parens was found, prepare to look through
470+
// rest of selectors
471+
let foundClosingSelector = false;
472+
473+
// Loop starting with next item in array, until we find matching
474+
// number of ending parens
475+
j = i + 1;
476+
while (j < len) {
477+
// peek into next item to count the number of closing brackets
478+
const nextOpeningParensCount = (splitSelectors[j].match(/\(/g) || [])
479+
.length;
480+
const nextClosingParensCount = (splitSelectors[j].match(/\)/g) || [])
481+
.length;
482+
const nextUnbalancedParens =
483+
nextClosingParensCount - nextOpeningParensCount;
484+
485+
if (nextUnbalancedParens === unbalancedParens) {
486+
// Matching # of closing parens was found, join all elements
487+
// from i to j
488+
finalSelectors.push(splitSelectors.slice(i, j + 1).join(','));
489+
490+
// we will want to skip the items that we have joined together
491+
i = j + 1;
492+
493+
// Use to continue the outer loop
494+
foundClosingSelector = true;
495+
496+
// break out of inner loop so we found matching closing parens
497+
break;
498+
}
499+
500+
// No matching closing parens found, keep moving through index, but
501+
// update the # of unbalanced parents still outstanding
502+
j++;
503+
unbalancedParens -= nextUnbalancedParens;
504+
}
477505

478-
if (currentStringChar) {
479-
if (currentStringChar === char && !hasStringEscape) {
480-
currentStringChar = null;
506+
if (foundClosingSelector) {
507+
// Matching closing selector was found, move to next selector
508+
continue;
481509
}
482-
} else if (char === '(') {
483-
depthParentheses++;
484-
} else if (char === ')') {
485-
depthParentheses--;
486-
} else if (char === '[') {
487-
depthBrackets++;
488-
} else if (char === ']') {
489-
depthBrackets--;
490-
} else if ('\'"'.includes(char)) {
491-
currentStringChar = char;
492-
}
493510

494-
// Split point is a comma that is not inside parentheses or square brackets
495-
if (char === ',' && depthParentheses === 0 && depthBrackets === 0) {
496-
result.push(currentSegment);
497-
currentSegment = '';
498-
} else {
499-
currentSegment += char;
511+
// No matching closing selector was found, either invalid CSS,
512+
// or unbalanced number of opening parens were used as CSS
513+
// selectors. Assume that rest of the list of selectors are
514+
// selectors and break to avoid iterating through the list of
515+
// selectors again.
516+
splitSelectors
517+
.slice(i, len)
518+
.forEach((selector) => selector && finalSelectors.push(selector));
519+
break;
500520
}
501-
}
502521

503-
// Add the last segment
504-
if (currentSegment) {
505-
result.push(currentSegment);
522+
// No opening parens found, contiue looking through list
523+
splitSelectors[i] && finalSelectors.push(splitSelectors[i]);
524+
i++;
506525
}
507526

508-
return result;
527+
return finalSelectors.map((s) => {
528+
return s.replace(/\u200C/g, ',');
529+
});
509530
}
510531

511532
/**

packages/rrweb-snapshot/test/css.test.ts

Lines changed: 8 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -78,35 +78,6 @@ describe('css parser', () => {
7878
expect(errors[0].filename).toEqual('foo.css');
7979
});
8080

81-
it('should parse selector with comma nested inside ()', () => {
82-
const result = parse(
83-
'[_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); }',
84-
);
85-
86-
expect(result.parent).toEqual(null);
87-
88-
const rules = result.stylesheet!.rules;
89-
expect(rules.length).toEqual(1);
90-
91-
let rule = rules[0] as Rule;
92-
expect(rule.parent).toEqual(result);
93-
expect(rule.selectors?.length).toEqual(1);
94-
95-
let decl = rule.declarations![0];
96-
expect(decl.parent).toEqual(rule);
97-
});
98-
99-
it('parses { and } in attribute selectors correctly', () => {
100-
const result = parse('foo[someAttr~="{someId}"] { color: red; }');
101-
const rules = result.stylesheet!.rules;
102-
103-
expect(rules.length).toEqual(1);
104-
105-
const rule = rules[0] as Rule;
106-
107-
expect(rule.selectors![0]).toEqual('foo[someAttr~="{someId}"]');
108-
});
109-
11081
it('should set parent property', () => {
11182
const result = parse(
11283
'thing { test: value; }\n' +
@@ -148,50 +119,6 @@ describe('css parser', () => {
148119
expect(out3).toEqual('[data-aa\\:other] { color: red; }');
149120
});
150121

151-
it('parses nested commas in selectors correctly', () => {
152-
const result = parse(
153-
`
154-
body > ul :is(li:not(:first-of-type) a:hover, li:not(:first-of-type).active a) {
155-
background: red;
156-
}
157-
`,
158-
);
159-
expect((result.stylesheet!.rules[0] as Rule)!.selectors!.length).toEqual(1);
160-
161-
const trickresult = parse(
162-
`
163-
li[attr="weirdly("] a:hover, li[attr="weirdly)"] a {
164-
background-color: red;
165-
}
166-
`,
167-
);
168-
expect(
169-
(trickresult.stylesheet!.rules[0] as Rule)!.selectors!.length,
170-
).toEqual(2);
171-
172-
const weirderresult = parse(
173-
`
174-
li[attr="weirder\\"("] a:hover, li[attr="weirder\\")"] a {
175-
background-color: red;
176-
}
177-
`,
178-
);
179-
expect(
180-
(weirderresult.stylesheet!.rules[0] as Rule)!.selectors!.length,
181-
).toEqual(2);
182-
183-
const commainstrresult = parse(
184-
`
185-
li[attr="has,comma"] a:hover {
186-
background-color: red;
187-
}
188-
`,
189-
);
190-
expect(
191-
(commainstrresult.stylesheet!.rules[0] as Rule)!.selectors!.length,
192-
).toEqual(1);
193-
});
194-
195122
it.each([
196123
['.foo,.bar {}', ['.foo', '.bar']],
197124
['.bar:has(:disabled) {}', ['.bar:has(:disabled)']],
@@ -202,11 +129,11 @@ li[attr="has,comma"] a:hover {
202129
],
203130
[
204131
'.bar:has(div, input:is(:disabled), button) {}',
205-
['.bar:has(div, input:is(:disabled), button)'],
132+
['.bar:has(div,input:is(:disabled), button)'],
206133
],
207134
[
208135
'.bar:has(div, input:is(:disabled),button:has(:disabled,.baz)) {}',
209-
['.bar:has(div, input:is(:disabled),button:has(:disabled,.baz))'],
136+
['.bar:has(div,input:is(:disabled),button:has(:disabled,.baz))'],
210137
],
211138
[
212139
'.bar:has(input), .foo:has(input, button), .baz {}',
@@ -215,15 +142,17 @@ li[attr="has,comma"] a:hover {
215142
[
216143
'.bar:has(input:is(:disabled),button:has(:disabled,.baz), div:has(:disabled,.baz)){color: red;}',
217144
[
218-
'.bar:has(input:is(:disabled),button:has(:disabled,.baz), div:has(:disabled,.baz))',
145+
'.bar:has(input:is(:disabled),button:has(:disabled,.baz),div:has(:disabled,.baz))',
219146
],
220147
],
148+
['.bar((( {}', ['.bar(((']],
221149
[
222150
'.bar:has(:has(:has(a), :has(:has(:has(b, :has(a), c), e))), input:is(:disabled), button) {}',
223151
[
224-
'.bar:has(:has(:has(a), :has(:has(:has(b, :has(a), c), e))), input:is(:disabled), button)',
152+
'.bar:has(:has(:has(a),:has(:has(:has(b,:has(a), c), e))),input:is(:disabled), button)',
225153
],
226154
],
155+
['.foo,.bar(((,.baz {}', ['.foo', '.bar(((', '.baz']],
227156
[
228157
'.foo,.bar:has(input:is(:disabled)){color: red;}',
229158
['.foo', '.bar:has(input:is(:disabled))'],
@@ -236,14 +165,14 @@ li[attr="has,comma"] a:hover {
236165
'.foo,.bar:has(input:is(:disabled),button:has(:disabled), div:has(:disabled,.baz)){color: red;}',
237166
[
238167
'.foo',
239-
'.bar:has(input:is(:disabled),button:has(:disabled), div:has(:disabled,.baz))',
168+
'.bar:has(input:is(:disabled),button:has(:disabled),div:has(:disabled,.baz))',
240169
],
241170
],
242171
[
243172
'.foo,.bar:has(input:is(:disabled),button:has(:disabled,.baz), div:has(:disabled,.baz)){color: red;}',
244173
[
245174
'.foo',
246-
'.bar:has(input:is(:disabled),button:has(:disabled,.baz), div:has(:disabled,.baz))',
175+
'.bar:has(input:is(:disabled),button:has(:disabled,.baz),div:has(:disabled,.baz))',
247176
],
248177
],
249178
['.bar:has(:disabled), .foo {}', ['.bar:has(:disabled)', '.foo']],

0 commit comments

Comments
 (0)