fix(css parser): support nested builtin functions (#27841)

Things like `:nth-child(1 of :has(span:nth-last-child(3)))`.

Fixes #27743.
This commit is contained in:
Dmitry Gozman 2023-10-27 13:16:12 -07:00 committed by GitHub
parent 88f30d1ce2
commit 100d3b2601
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 43 additions and 3 deletions

View File

@ -98,10 +98,18 @@ export function parseCSS(selector: string, customNames: Set<string>): { selector
return tokens[p] instanceof css.CommaToken;
}
function isOpenParen(p = pos) {
return tokens[p] instanceof css.OpenParenToken;
}
function isCloseParen(p = pos) {
return tokens[p] instanceof css.CloseParenToken;
}
function isFunction(p = pos) {
return tokens[p] instanceof css.FunctionToken;
}
function isStar(p = pos) {
return (tokens[p] instanceof css.DelimToken) && tokens[p].value === '*';
}
@ -186,7 +194,7 @@ export function parseCSS(selector: string, customNames: Set<string>): { selector
functions.push({ name, args: [] });
names.add(name);
}
} else if (tokens[pos] instanceof css.FunctionToken) {
} else if (isFunction()) {
const name = (tokens[pos++].value as string).toLowerCase();
if (!customNames.has(name)) {
rawCSSString += `:${name}(${consumeBuiltinFunctionArguments()})`;
@ -221,14 +229,22 @@ export function parseCSS(selector: string, customNames: Set<string>): { selector
function consumeBuiltinFunctionArguments(): string {
let s = '';
while (!isCloseParen() && !isEOF())
let balance = 1; // First open paren is a part of a function token.
while (!isEOF()) {
if (isOpenParen() || isFunction())
balance++;
if (isCloseParen())
balance--;
if (!balance)
break;
s += tokens[pos++].toSource();
}
return s;
}
const result = consumeFunctionArguments();
if (!isEOF())
throw new InvalidSelectorError(`Error while parsing selector "${selector}"`);
throw unexpected();
if (result.some(arg => typeof arg !== 'object' || !('simples' in arg)))
throw new InvalidSelectorError(`Error while parsing selector "${selector}"`);
return { selector: result as CSSComplexSelector[], names: Array.from(names) };

View File

@ -274,6 +274,30 @@ it('should work with :nth-child', async ({ page, server }) => {
expect(await page.$$eval(`css=span:nth-child(23n+2)`, els => els.length)).toBe(1);
});
it('should work with :nth-child(of) notation with nested functions', async ({ page, browserName }) => {
it.fixme(browserName === 'firefox', 'Should enable once Firefox supports this syntax');
await page.setContent(`
<div>
<span>span1</span>
<span class=foo>span2<dd></dd></span>
<span class=foo>span3<dd class=marker></dd></span>
<span class=foo>span4<dd class=marker></dd></span>
<span class=foo>span5<dd></dd></span>
<span>span6</span>
</div>
`);
expect(await page.$$eval(`css=span:nth-child(1)`, els => els.map(e => e.textContent))).toEqual(['span1']);
expect(await page.$$eval(`css=span:nth-child(1 of .foo)`, els => els.map(e => e.textContent))).toEqual(['span2']);
expect(await page.$$eval(`css=span:nth-child(1 of .foo:has(dd.marker))`, els => els.map(e => e.textContent))).toEqual(['span3']);
expect(await page.$$eval(`css=span:nth-last-child(1 of .foo:has(dd.marker))`, els => els.map(e => e.textContent))).toEqual(['span4']);
expect(await page.$$eval(`css=span:nth-last-child(1 of .foo)`, els => els.map(e => e.textContent))).toEqual(['span5']);
expect(await page.$$eval(`css=span:nth-last-child( 1 )`, els => els.map(e => e.textContent))).toEqual(['span6']);
expect(await page.$$eval(`css=span:nth-child(1 of .foo:nth-child(3))`, els => els.map(e => e.textContent))).toEqual(['span3']);
expect(await page.$$eval(`css=span:nth-child(1 of .foo:nth-child(6))`, els => els.map(e => e.textContent))).toEqual([]);
});
it('should work with :not', async ({ page, server }) => {
await page.goto(server.PREFIX + '/deep-shadow.html');
expect(await page.$$eval(`css=div:not(#root1)`, els => els.length)).toBe(2);