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:
parent
88f30d1ce2
commit
100d3b2601
|
|
@ -98,10 +98,18 @@ export function parseCSS(selector: string, customNames: Set<string>): { selector
|
||||||
return tokens[p] instanceof css.CommaToken;
|
return tokens[p] instanceof css.CommaToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOpenParen(p = pos) {
|
||||||
|
return tokens[p] instanceof css.OpenParenToken;
|
||||||
|
}
|
||||||
|
|
||||||
function isCloseParen(p = pos) {
|
function isCloseParen(p = pos) {
|
||||||
return tokens[p] instanceof css.CloseParenToken;
|
return tokens[p] instanceof css.CloseParenToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFunction(p = pos) {
|
||||||
|
return tokens[p] instanceof css.FunctionToken;
|
||||||
|
}
|
||||||
|
|
||||||
function isStar(p = pos) {
|
function isStar(p = pos) {
|
||||||
return (tokens[p] instanceof css.DelimToken) && tokens[p].value === '*';
|
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: [] });
|
functions.push({ name, args: [] });
|
||||||
names.add(name);
|
names.add(name);
|
||||||
}
|
}
|
||||||
} else if (tokens[pos] instanceof css.FunctionToken) {
|
} else if (isFunction()) {
|
||||||
const name = (tokens[pos++].value as string).toLowerCase();
|
const name = (tokens[pos++].value as string).toLowerCase();
|
||||||
if (!customNames.has(name)) {
|
if (!customNames.has(name)) {
|
||||||
rawCSSString += `:${name}(${consumeBuiltinFunctionArguments()})`;
|
rawCSSString += `:${name}(${consumeBuiltinFunctionArguments()})`;
|
||||||
|
|
@ -221,14 +229,22 @@ export function parseCSS(selector: string, customNames: Set<string>): { selector
|
||||||
|
|
||||||
function consumeBuiltinFunctionArguments(): string {
|
function consumeBuiltinFunctionArguments(): string {
|
||||||
let s = '';
|
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();
|
s += tokens[pos++].toSource();
|
||||||
|
}
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = consumeFunctionArguments();
|
const result = consumeFunctionArguments();
|
||||||
if (!isEOF())
|
if (!isEOF())
|
||||||
throw new InvalidSelectorError(`Error while parsing selector "${selector}"`);
|
throw unexpected();
|
||||||
if (result.some(arg => typeof arg !== 'object' || !('simples' in arg)))
|
if (result.some(arg => typeof arg !== 'object' || !('simples' in arg)))
|
||||||
throw new InvalidSelectorError(`Error while parsing selector "${selector}"`);
|
throw new InvalidSelectorError(`Error while parsing selector "${selector}"`);
|
||||||
return { selector: result as CSSComplexSelector[], names: Array.from(names) };
|
return { selector: result as CSSComplexSelector[], names: Array.from(names) };
|
||||||
|
|
|
||||||
|
|
@ -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);
|
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 }) => {
|
it('should work with :not', async ({ page, server }) => {
|
||||||
await page.goto(server.PREFIX + '/deep-shadow.html');
|
await page.goto(server.PREFIX + '/deep-shadow.html');
|
||||||
expect(await page.$$eval(`css=div:not(#root1)`, els => els.length)).toBe(2);
|
expect(await page.$$eval(`css=div:not(#root1)`, els => els.length)).toBe(2);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue