From c2abcb9a3de77d3c8ee502d12ec5a27c55e17c9c Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Mon, 3 Mar 2025 09:27:00 +0100 Subject: [PATCH] fix(unstable): :is()/:where()/:matches --- cli/js/40_lint.js | 2 +- cli/js/40_lint_selector.js | 87 +++++++++++++++++++++++++++++-- cli/js/40_lint_types.d.ts | 5 ++ tests/unit/lint_plugin_test.ts | 37 ++++++++----- tests/unit/lint_selectors_test.ts | 62 +++++++++++++++++++--- 5 files changed, 169 insertions(+), 24 deletions(-) diff --git a/cli/js/40_lint.js b/cli/js/40_lint.js index e13db38ba4..1dc512c96f 100644 --- a/cli/js/40_lint.js +++ b/cli/js/40_lint.js @@ -954,7 +954,7 @@ class MatchCtx { } /** - * Used for `:has/:is/:where` and `:not` + * Used for `:has()` and `:not()` * @param {MatcherFn[]} selectors * @param {number} idx * @returns {boolean} diff --git a/cli/js/40_lint_selector.js b/cli/js/40_lint_selector.js index f4df584210..bd0ad223d9 100644 --- a/cli/js/40_lint_selector.js +++ b/cli/js/40_lint_selector.js @@ -172,6 +172,31 @@ export class Lexer { } } + peek() { + const value = this.value; + const start = this.start; + const end = this.end; + const i = this.i; + const ch = this.ch; + const token = this.token; + + this.next(); + + const result = { + token: this.token, + value: this.value, + }; + + this.vaue = value; + this.start = start; + this.end = end; + this.i = i; + this.ch = ch; + this.token = token; + + return result; + } + next() { this.value = ""; @@ -377,6 +402,7 @@ export const PSEUDO_NOT = 7; export const PSEUDO_FIRST_CHILD = 8; export const PSEUDO_LAST_CHILD = 9; export const FIELD_NODE = 10; +export const PSEUDO_IS = 11; /** * Parse out all unique selectors of a selector list. @@ -458,6 +484,19 @@ export function parseSelector(input, toElem, toAttr) { type: RELATION_NODE, op: BinOp.Space, }); + } else if (lex.token === Token.Colon) { + const peeked = lex.peek(); + + if ( + peeked.token === Token.Word && + (peeked.value === "is" || peeked.value === "where" || + peeked.value === "matches") + ) { + current.push({ + type: RELATION_NODE, + op: BinOp.Space, + }); + } } continue; @@ -616,14 +655,26 @@ export function parseSelector(input, toElem, toAttr) { continue; } - - case "has": case "where": + case "matches": case "is": { lex.next(); lex.expect(Token.BraceOpen); lex.next(); + current.push({ + type: PSEUDO_IS, + selectors: [], + }); + stack.push([]); + + continue; + } + case "has": { + lex.next(); + lex.expect(Token.BraceOpen); + lex.next(); + current.push({ type: PSEUDO_HAS, selectors: [], @@ -704,7 +755,10 @@ function popSelector(result, stack) { if (node.type === PSEUDO_NTH_CHILD) { node.of = sel; - } else if (node.type === PSEUDO_HAS || node.type === PSEUDO_NOT) { + } else if ( + node.type === PSEUDO_HAS || node.type === PSEUDO_IS || + node.type === PSEUDO_NOT + ) { node.selectors.push(sel); } else { throw new Error(`Multiple selectors not allowed here`); @@ -770,6 +824,9 @@ export function compileSelector(selector) { case PSEUDO_HAS: fn = matchHas(node.selectors, fn); break; + case PSEUDO_IS: + fn = matchIs(node.selectors, fn); + break; case PSEUDO_NOT: fn = matchNot(node.selectors, fn); break; @@ -865,6 +922,30 @@ function matchNthChild(node, next) { }; } +/** + * @param {Selector[]} selectors + * @param {MatcherFn} next + * @returns {MatcherFn} + */ +function matchIs(selectors, next) { + /** @type {MatcherFn[]} */ + const compiled = []; + + for (let i = 0; i < selectors.length; i++) { + const sel = selectors[i]; + compiled.push(compileSelector(sel)); + } + + return (ctx, id) => { + for (let i = 0; i < compiled.length; i++) { + const sel = compiled[i]; + if (sel(ctx, id)) return next(ctx, id); + } + + return false; + }; +} + /** * @param {Selector[]} selectors * @param {MatcherFn} next diff --git a/cli/js/40_lint_types.d.ts b/cli/js/40_lint_types.d.ts index 93af061611..3d55773175 100644 --- a/cli/js/40_lint_types.d.ts +++ b/cli/js/40_lint_types.d.ts @@ -68,6 +68,10 @@ export interface PseudoHas { type: 6; selectors: Selector[]; } +export interface PseudoIs { + type: 11; + selectors: Selector[]; +} export interface PseudoNot { type: 7; selectors: Selector[]; @@ -93,6 +97,7 @@ export type Selector = Array< | PseudoNthChild | PseudoNot | PseudoHas + | PseudoIs | PseudoFirstChild | PseudoLastChild >; diff --git a/tests/unit/lint_plugin_test.ts b/tests/unit/lint_plugin_test.ts index 4df1803f4d..731cc585a2 100644 --- a/tests/unit/lint_plugin_test.ts +++ b/tests/unit/lint_plugin_test.ts @@ -355,25 +355,13 @@ Deno.test("Plugin - visitor :nth-child", () => { assertEquals(result[1].node.name, "foobar"); }); -Deno.test("Plugin - visitor :has/:is/:where", () => { +Deno.test("Plugin - visitor :has()", () => { let result = testVisit( "{ foo, bar }", "BlockStatement:has(Identifier[name='bar'])", ); assertEquals(result[0].node.type, "BlockStatement"); - result = testVisit( - "{ foo, bar }", - "BlockStatement:is(Identifier[name='bar'])", - ); - assertEquals(result[0].node.type, "BlockStatement"); - - result = testVisit( - "{ foo, bar }", - "BlockStatement:where(Identifier[name='bar'])", - ); - assertEquals(result[0].node.type, "BlockStatement"); - // Multiple sub queries result = testVisit( "{ foo, bar }", @@ -397,6 +385,29 @@ Deno.test("Plugin - visitor :has/:is/:where", () => { assertEquals(result[0].node.name, "bar"); }); +Deno.test("Plugin - visitor :is()/:where()/:matches()", () => { + let result = testVisit( + "{ foo, bar }", + "BlockStatement :is(Identifier[name='bar'])", + ); + assertEquals(result[0].node.type, "Identifier"); + assertEquals(result[0].node.name, "bar"); + + result = testVisit( + "{ foo, bar }", + "BlockStatement :where(Identifier[name='bar'])", + ); + assertEquals(result[0].node.type, "Identifier"); + assertEquals(result[0].node.name, "bar"); + + result = testVisit( + "{ foo, bar }", + "BlockStatement :matches(Identifier[name='bar'])", + ); + assertEquals(result[0].node.type, "Identifier"); + assertEquals(result[0].node.name, "bar"); +}); + Deno.test("Plugin - visitor :not", () => { let result = testVisit( "{ foo, bar }", diff --git a/tests/unit/lint_selectors_test.ts b/tests/unit/lint_selectors_test.ts index 9828343a79..566ee81e30 100644 --- a/tests/unit/lint_selectors_test.ts +++ b/tests/unit/lint_selectors_test.ts @@ -11,6 +11,7 @@ import { parseSelector, PSEUDO_FIRST_CHILD, PSEUDO_HAS, + PSEUDO_IS, PSEUDO_LAST_CHILD, PSEUDO_NOT, PSEUDO_NTH_CHILD, @@ -554,7 +555,7 @@ Deno.test("Parser - Pseudo nth-child", () => { assertThrows(() => testParse(":nth-child(2n - 1 foo)")); }); -Deno.test("Parser - Pseudo has/is/where", () => { +Deno.test("Parser - Pseudo :has()", () => { assertEquals(testParse(":has(Foo:has(Foo), Bar)"), [[ { type: PSEUDO_HAS, @@ -574,14 +575,17 @@ Deno.test("Parser - Pseudo has/is/where", () => { ], }, ]]); - assertEquals(testParse(":where(Foo:where(Foo), Bar)"), [[ +}); + +Deno.test("Parser - Pseudo :is()/:where()/:matches()", () => { + assertEquals(testParse(":is(Foo:is(Foo), Bar)"), [[ { - type: PSEUDO_HAS, + type: PSEUDO_IS, selectors: [ [ { type: ELEM_NODE, elem: 1, wildcard: false }, { - type: PSEUDO_HAS, + type: PSEUDO_IS, selectors: [ [{ type: ELEM_NODE, elem: 1, wildcard: false }], ], @@ -593,14 +597,14 @@ Deno.test("Parser - Pseudo has/is/where", () => { ], }, ]]); - assertEquals(testParse(":is(Foo:is(Foo), Bar)"), [[ + assertEquals(testParse(":where(Foo:where(Foo), Bar)"), [[ { - type: PSEUDO_HAS, + type: PSEUDO_IS, selectors: [ [ { type: ELEM_NODE, elem: 1, wildcard: false }, { - type: PSEUDO_HAS, + type: PSEUDO_IS, selectors: [ [{ type: ELEM_NODE, elem: 1, wildcard: false }], ], @@ -612,6 +616,50 @@ Deno.test("Parser - Pseudo has/is/where", () => { ], }, ]]); + assertEquals(testParse(":matches(Foo:matches(Foo), Bar)"), [[ + { + type: PSEUDO_IS, + selectors: [ + [ + { type: ELEM_NODE, elem: 1, wildcard: false }, + { + type: PSEUDO_IS, + selectors: [ + [{ type: ELEM_NODE, elem: 1, wildcard: false }], + ], + }, + ], + [ + { type: ELEM_NODE, elem: 2, wildcard: false }, + ], + ], + }, + ]]); + + assertEquals(testParse("Foo:is(Bar)"), [[ + { type: ELEM_NODE, elem: 1, wildcard: false }, + { + type: PSEUDO_IS, + selectors: [ + [ + { type: ELEM_NODE, elem: 2, wildcard: false }, + ], + ], + }, + ]]); + + assertEquals(testParse("Foo :is(Bar)"), [[ + { type: ELEM_NODE, elem: 1, wildcard: false }, + { type: RELATION_NODE, op: BinOp.Space }, + { + type: PSEUDO_IS, + selectors: [ + [ + { type: ELEM_NODE, elem: 2, wildcard: false }, + ], + ], + }, + ]]); }); Deno.test("Parser - Pseudo not", () => {