mirror of
https://github.com/denoland/deno.git
synced 2025-03-10 06:07:03 -04:00
fix(unstable): lint plugin fix :has()
, :is/where/matches
and :not()
selectors (#28348)
This PR adds support for `:has/:is/:where()` and `:not()`. The latter was already present, but found a bunch of issues with it and I'd say that it didn't really work before this PR. Fixes https://github.com/denoland/deno/issues/28335
This commit is contained in:
parent
842a906295
commit
1f6f561979
5 changed files with 362 additions and 35 deletions
|
@ -82,6 +82,7 @@ const PropFlags = {
|
||||||
/** @typedef {import("./40_lint_types.d.ts").LintState} LintState */
|
/** @typedef {import("./40_lint_types.d.ts").LintState} LintState */
|
||||||
/** @typedef {import("./40_lint_types.d.ts").TransformFn} TransformFn */
|
/** @typedef {import("./40_lint_types.d.ts").TransformFn} TransformFn */
|
||||||
/** @typedef {import("./40_lint_types.d.ts").MatchContext} MatchContext */
|
/** @typedef {import("./40_lint_types.d.ts").MatchContext} MatchContext */
|
||||||
|
/** @typedef {import("./40_lint_types.d.ts").MatcherFn} MatcherFn */
|
||||||
|
|
||||||
/** @type {LintState} */
|
/** @type {LintState} */
|
||||||
const state = {
|
const state = {
|
||||||
|
@ -770,11 +771,15 @@ function getString(strTable, id) {
|
||||||
|
|
||||||
/** @implements {MatchContext} */
|
/** @implements {MatchContext} */
|
||||||
class MatchCtx {
|
class MatchCtx {
|
||||||
|
parentLimitIdx = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AstContext} ctx
|
* @param {AstContext} ctx
|
||||||
|
* @param {CancellationToken} cancellationToken
|
||||||
*/
|
*/
|
||||||
constructor(ctx) {
|
constructor(ctx, cancellationToken) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
|
this.cancellationToken = cancellationToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -782,6 +787,7 @@ class MatchCtx {
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
getParent(idx) {
|
getParent(idx) {
|
||||||
|
if (idx === this.parentLimitIdx) return AST_IDX_INVALID;
|
||||||
const parent = readParent(this.ctx.buf, idx);
|
const parent = readParent(this.ctx.buf, idx);
|
||||||
|
|
||||||
const parentType = readType(this.ctx.buf, parent);
|
const parentType = readType(this.ctx.buf, parent);
|
||||||
|
@ -953,13 +959,31 @@ class MatchCtx {
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for `:has()` and `:not()`
|
||||||
|
* @param {MatcherFn[]} selectors
|
||||||
|
* @param {number} idx
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
subSelect(selectors, idx) {
|
||||||
|
const prevLimit = this.parentLimitIdx;
|
||||||
|
this.parentLimitIdx = idx;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return subTraverse(this.ctx, selectors, idx, idx, this.cancellationToken);
|
||||||
|
} finally {
|
||||||
|
this.parentLimitIdx = prevLimit;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Uint8Array} buf
|
* @param {Uint8Array} buf
|
||||||
|
* @param {CancellationToken} token
|
||||||
* @returns {AstContext}
|
* @returns {AstContext}
|
||||||
*/
|
*/
|
||||||
function createAstContext(buf) {
|
function createAstContext(buf, token) {
|
||||||
/** @type {Map<number, string>} */
|
/** @type {Map<number, string>} */
|
||||||
const strTable = new Map();
|
const strTable = new Map();
|
||||||
|
|
||||||
|
@ -1039,7 +1063,7 @@ function createAstContext(buf) {
|
||||||
propByStr,
|
propByStr,
|
||||||
matcher: /** @type {*} */ (null),
|
matcher: /** @type {*} */ (null),
|
||||||
};
|
};
|
||||||
ctx.matcher = new MatchCtx(ctx);
|
ctx.matcher = new MatchCtx(ctx, token);
|
||||||
|
|
||||||
setNodeGetters(ctx);
|
setNodeGetters(ctx);
|
||||||
|
|
||||||
|
@ -1060,7 +1084,8 @@ const NOOP = (_node) => {};
|
||||||
* @param {Uint8Array} serializedAst
|
* @param {Uint8Array} serializedAst
|
||||||
*/
|
*/
|
||||||
export function runPluginsForFile(fileName, serializedAst) {
|
export function runPluginsForFile(fileName, serializedAst) {
|
||||||
const ctx = createAstContext(serializedAst);
|
const token = new CancellationToken();
|
||||||
|
const ctx = createAstContext(serializedAst, token);
|
||||||
|
|
||||||
/** @type {Map<string, CompiledVisitor["info"]>}>} */
|
/** @type {Map<string, CompiledVisitor["info"]>}>} */
|
||||||
const bySelector = new Map();
|
const bySelector = new Map();
|
||||||
|
@ -1169,7 +1194,6 @@ export function runPluginsForFile(fileName, serializedAst) {
|
||||||
visitors.push({ info, matcher });
|
visitors.push({ info, matcher });
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = new CancellationToken();
|
|
||||||
// Traverse ast with all visitors at the same time to avoid traversing
|
// Traverse ast with all visitors at the same time to avoid traversing
|
||||||
// multiple times.
|
// multiple times.
|
||||||
try {
|
try {
|
||||||
|
@ -1191,11 +1215,12 @@ export function runPluginsForFile(fileName, serializedAst) {
|
||||||
* @param {CancellationToken} cancellationToken
|
* @param {CancellationToken} cancellationToken
|
||||||
*/
|
*/
|
||||||
function traverse(ctx, visitors, idx, cancellationToken) {
|
function traverse(ctx, visitors, idx, cancellationToken) {
|
||||||
|
const { buf } = ctx;
|
||||||
|
|
||||||
while (idx !== AST_IDX_INVALID) {
|
while (idx !== AST_IDX_INVALID) {
|
||||||
if (cancellationToken.isCancellationRequested()) return;
|
if (cancellationToken.isCancellationRequested()) return;
|
||||||
|
|
||||||
const { buf } = ctx;
|
const nodeType = readType(buf, idx);
|
||||||
const nodeType = readType(ctx.buf, idx);
|
|
||||||
|
|
||||||
/** @type {VisitorFn[] | null} */
|
/** @type {VisitorFn[] | null} */
|
||||||
let exits = null;
|
let exits = null;
|
||||||
|
@ -1240,6 +1265,51 @@ function traverse(ctx, visitors, idx, cancellationToken) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for subqueries in `:has()` and `:not()`
|
||||||
|
* @param {AstContext} ctx
|
||||||
|
* @param {MatcherFn[]} selectors
|
||||||
|
* @param {number} rootIdx
|
||||||
|
* @param {number} idx
|
||||||
|
* @param {CancellationToken} cancellationToken
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function subTraverse(ctx, selectors, rootIdx, idx, cancellationToken) {
|
||||||
|
const { buf } = ctx;
|
||||||
|
|
||||||
|
while (idx > AST_IDX_INVALID) {
|
||||||
|
if (cancellationToken.isCancellationRequested()) return false;
|
||||||
|
|
||||||
|
const nodeType = readType(buf, idx);
|
||||||
|
|
||||||
|
if (nodeType !== AST_GROUP_TYPE) {
|
||||||
|
for (let i = 0; i < selectors.length; i++) {
|
||||||
|
const sel = selectors[i];
|
||||||
|
|
||||||
|
if (sel(ctx.matcher, idx)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const childIdx = readChild(buf, idx);
|
||||||
|
if (
|
||||||
|
childIdx > AST_IDX_INVALID &&
|
||||||
|
subTraverse(ctx, selectors, rootIdx, childIdx, cancellationToken)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idx === rootIdx) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
idx = readNext(buf, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is useful debugging helper to display the buffer's contents.
|
* This is useful debugging helper to display the buffer's contents.
|
||||||
* @param {AstContext} ctx
|
* @param {AstContext} ctx
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
/** @typedef {import("./40_lint_types.d.ts").Relation} SRelation */
|
/** @typedef {import("./40_lint_types.d.ts").Relation} SRelation */
|
||||||
/** @typedef {import("./40_lint_types.d.ts").Selector} Selector */
|
/** @typedef {import("./40_lint_types.d.ts").Selector} Selector */
|
||||||
/** @typedef {import("./40_lint_types.d.ts").SelectorParseCtx} SelectorParseCtx */
|
/** @typedef {import("./40_lint_types.d.ts").SelectorParseCtx} SelectorParseCtx */
|
||||||
/** @typedef {import("./40_lint_types.d.ts").NextFn} NextFn */
|
|
||||||
/** @typedef {import("./40_lint_types.d.ts").MatcherFn} MatcherFn */
|
/** @typedef {import("./40_lint_types.d.ts").MatcherFn} MatcherFn */
|
||||||
/** @typedef {import("./40_lint_types.d.ts").TransformFn} Transformer */
|
/** @typedef {import("./40_lint_types.d.ts").TransformFn} Transformer */
|
||||||
|
|
||||||
|
@ -173,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() {
|
next() {
|
||||||
this.value = "";
|
this.value = "";
|
||||||
|
|
||||||
|
@ -378,6 +402,7 @@ export const PSEUDO_NOT = 7;
|
||||||
export const PSEUDO_FIRST_CHILD = 8;
|
export const PSEUDO_FIRST_CHILD = 8;
|
||||||
export const PSEUDO_LAST_CHILD = 9;
|
export const PSEUDO_LAST_CHILD = 9;
|
||||||
export const FIELD_NODE = 10;
|
export const FIELD_NODE = 10;
|
||||||
|
export const PSEUDO_IS = 11;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse out all unique selectors of a selector list.
|
* Parse out all unique selectors of a selector list.
|
||||||
|
@ -459,6 +484,19 @@ export function parseSelector(input, toElem, toAttr) {
|
||||||
type: RELATION_NODE,
|
type: RELATION_NODE,
|
||||||
op: BinOp.Space,
|
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;
|
continue;
|
||||||
|
@ -617,14 +655,26 @@ export function parseSelector(input, toElem, toAttr) {
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "has":
|
|
||||||
case "where":
|
case "where":
|
||||||
|
case "matches":
|
||||||
case "is": {
|
case "is": {
|
||||||
lex.next();
|
lex.next();
|
||||||
lex.expect(Token.BraceOpen);
|
lex.expect(Token.BraceOpen);
|
||||||
lex.next();
|
lex.next();
|
||||||
|
|
||||||
|
current.push({
|
||||||
|
type: PSEUDO_IS,
|
||||||
|
selectors: [],
|
||||||
|
});
|
||||||
|
stack.push([]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case "has": {
|
||||||
|
lex.next();
|
||||||
|
lex.expect(Token.BraceOpen);
|
||||||
|
lex.next();
|
||||||
|
|
||||||
current.push({
|
current.push({
|
||||||
type: PSEUDO_HAS,
|
type: PSEUDO_HAS,
|
||||||
selectors: [],
|
selectors: [],
|
||||||
|
@ -705,7 +755,10 @@ function popSelector(result, stack) {
|
||||||
|
|
||||||
if (node.type === PSEUDO_NTH_CHILD) {
|
if (node.type === PSEUDO_NTH_CHILD) {
|
||||||
node.of = sel;
|
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);
|
node.selectors.push(sel);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Multiple selectors not allowed here`);
|
throw new Error(`Multiple selectors not allowed here`);
|
||||||
|
@ -769,8 +822,11 @@ export function compileSelector(selector) {
|
||||||
fn = matchNthChild(node, fn);
|
fn = matchNthChild(node, fn);
|
||||||
break;
|
break;
|
||||||
case PSEUDO_HAS:
|
case PSEUDO_HAS:
|
||||||
// TODO(@marvinhagemeister)
|
fn = matchHas(node.selectors, fn);
|
||||||
throw new Error("TODO: :has");
|
break;
|
||||||
|
case PSEUDO_IS:
|
||||||
|
fn = matchIs(node.selectors, fn);
|
||||||
|
break;
|
||||||
case PSEUDO_NOT:
|
case PSEUDO_NOT:
|
||||||
fn = matchNot(node.selectors, fn);
|
fn = matchNot(node.selectors, fn);
|
||||||
break;
|
break;
|
||||||
|
@ -786,7 +842,7 @@ export function compileSelector(selector) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {NextFn} next
|
* @param {MatcherFn} next
|
||||||
* @returns {MatcherFn}
|
* @returns {MatcherFn}
|
||||||
*/
|
*/
|
||||||
function matchFirstChild(next) {
|
function matchFirstChild(next) {
|
||||||
|
@ -797,7 +853,7 @@ function matchFirstChild(next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {NextFn} next
|
* @param {MatcherFn} next
|
||||||
* @returns {MatcherFn}
|
* @returns {MatcherFn}
|
||||||
*/
|
*/
|
||||||
function matchLastChild(next) {
|
function matchLastChild(next) {
|
||||||
|
@ -829,7 +885,7 @@ function getNthAnB(node, i) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {PseudoNthChild} node
|
* @param {PseudoNthChild} node
|
||||||
* @param {NextFn} next
|
* @param {MatcherFn} next
|
||||||
* @returns {MatcherFn}
|
* @returns {MatcherFn}
|
||||||
*/
|
*/
|
||||||
function matchNthChild(node, next) {
|
function matchNthChild(node, next) {
|
||||||
|
@ -868,7 +924,64 @@ function matchNthChild(node, next) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Selector[]} selectors
|
* @param {Selector[]} selectors
|
||||||
* @param {NextFn} next
|
* @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
|
||||||
|
* @returns {MatcherFn}
|
||||||
|
*/
|
||||||
|
function matchHas(selectors, next) {
|
||||||
|
/** @type {MatcherFn[]} */
|
||||||
|
const compiled = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < selectors.length; i++) {
|
||||||
|
const sel = selectors[i];
|
||||||
|
compiled.push(compileSelector(sel));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Map<number, boolean>} */
|
||||||
|
const cache = new Map();
|
||||||
|
|
||||||
|
return (ctx, id) => {
|
||||||
|
if (next(ctx, id)) {
|
||||||
|
const cached = cache.get(id);
|
||||||
|
if (cached !== undefined) return cached;
|
||||||
|
|
||||||
|
const match = ctx.subSelect(compiled, id);
|
||||||
|
cache.set(id, match);
|
||||||
|
if (match) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Selector[]} selectors
|
||||||
|
* @param {MatcherFn} next
|
||||||
* @returns {MatcherFn}
|
* @returns {MatcherFn}
|
||||||
*/
|
*/
|
||||||
function matchNot(selectors, next) {
|
function matchNot(selectors, next) {
|
||||||
|
@ -880,20 +993,27 @@ function matchNot(selectors, next) {
|
||||||
compiled.push(compileSelector(sel));
|
compiled.push(compileSelector(sel));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {Map<number, boolean>} */
|
||||||
|
const cache = new Map();
|
||||||
|
|
||||||
return (ctx, id) => {
|
return (ctx, id) => {
|
||||||
for (let i = 0; i < compiled.length; i++) {
|
if (next(ctx, id)) {
|
||||||
const fn = compiled[i];
|
const cached = cache.get(id);
|
||||||
if (fn(ctx, id)) {
|
if (cached !== undefined) return cached;
|
||||||
return false;
|
|
||||||
|
const match = ctx.subSelect(compiled, id);
|
||||||
|
cache.set(id, !match);
|
||||||
|
if (!match) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(ctx, id);
|
return false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {NextFn} next
|
* @param {MatcherFn} next
|
||||||
* @returns {MatcherFn}
|
* @returns {MatcherFn}
|
||||||
*/
|
*/
|
||||||
function matchDescendant(next) {
|
function matchDescendant(next) {
|
||||||
|
@ -913,7 +1033,7 @@ function matchDescendant(next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {NextFn} next
|
* @param {MatcherFn} next
|
||||||
* @returns {MatcherFn}
|
* @returns {MatcherFn}
|
||||||
*/
|
*/
|
||||||
function matchChild(next) {
|
function matchChild(next) {
|
||||||
|
@ -926,7 +1046,7 @@ function matchChild(next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {NextFn} next
|
* @param {MatcherFn} next
|
||||||
* @returns {MatcherFn}
|
* @returns {MatcherFn}
|
||||||
*/
|
*/
|
||||||
function matchAdjacent(next) {
|
function matchAdjacent(next) {
|
||||||
|
@ -942,7 +1062,7 @@ function matchAdjacent(next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {NextFn} next
|
* @param {MatcherFn} next
|
||||||
* @returns {MatcherFn}
|
* @returns {MatcherFn}
|
||||||
*/
|
*/
|
||||||
function matchFollowing(next) {
|
function matchFollowing(next) {
|
||||||
|
|
10
cli/js/40_lint_types.d.ts
vendored
10
cli/js/40_lint_types.d.ts
vendored
|
@ -25,7 +25,7 @@ export interface LintState {
|
||||||
export type VisitorFn = (node: unknown) => void;
|
export type VisitorFn = (node: unknown) => void;
|
||||||
|
|
||||||
export interface CompiledVisitor {
|
export interface CompiledVisitor {
|
||||||
matcher: (ctx: MatchContext, offset: number) => boolean;
|
matcher: MatcherFn;
|
||||||
info: { enter: VisitorFn; exit: VisitorFn };
|
info: { enter: VisitorFn; exit: VisitorFn };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,6 +68,10 @@ export interface PseudoHas {
|
||||||
type: 6;
|
type: 6;
|
||||||
selectors: Selector[];
|
selectors: Selector[];
|
||||||
}
|
}
|
||||||
|
export interface PseudoIs {
|
||||||
|
type: 11;
|
||||||
|
selectors: Selector[];
|
||||||
|
}
|
||||||
export interface PseudoNot {
|
export interface PseudoNot {
|
||||||
type: 7;
|
type: 7;
|
||||||
selectors: Selector[];
|
selectors: Selector[];
|
||||||
|
@ -93,6 +97,7 @@ export type Selector = Array<
|
||||||
| PseudoNthChild
|
| PseudoNthChild
|
||||||
| PseudoNot
|
| PseudoNot
|
||||||
| PseudoHas
|
| PseudoHas
|
||||||
|
| PseudoIs
|
||||||
| PseudoFirstChild
|
| PseudoFirstChild
|
||||||
| PseudoLastChild
|
| PseudoLastChild
|
||||||
>;
|
>;
|
||||||
|
@ -103,6 +108,8 @@ export interface SelectorParseCtx {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MatchContext {
|
export interface MatchContext {
|
||||||
|
/** Used for `:has()` and `:not()` */
|
||||||
|
subSelect(selectors: MatcherFn[], idx: number): boolean;
|
||||||
getFirstChild(id: number): number;
|
getFirstChild(id: number): number;
|
||||||
getLastChild(id: number): number;
|
getLastChild(id: number): number;
|
||||||
getSiblings(id: number): number[];
|
getSiblings(id: number): number[];
|
||||||
|
@ -112,7 +119,6 @@ export interface MatchContext {
|
||||||
getAttrPathValue(id: number, propIds: number[], idx: number): unknown;
|
getAttrPathValue(id: number, propIds: number[], idx: number): unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NextFn = (ctx: MatchContext, id: number) => boolean;
|
|
||||||
export type MatcherFn = (ctx: MatchContext, id: number) => boolean;
|
export type MatcherFn = (ctx: MatchContext, id: number) => boolean;
|
||||||
export type TransformFn = (value: string) => number;
|
export type TransformFn = (value: string) => number;
|
||||||
|
|
||||||
|
|
|
@ -361,6 +361,89 @@ Deno.test("Plugin - visitor :nth-child", () => {
|
||||||
assertEquals(result[1].node.name, "foobar");
|
assertEquals(result[1].node.name, "foobar");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("Plugin - visitor :has()", () => {
|
||||||
|
let result = testVisit(
|
||||||
|
"{ foo, bar }",
|
||||||
|
"BlockStatement:has(Identifier[name='bar'])",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.type, "BlockStatement");
|
||||||
|
|
||||||
|
// Multiple sub queries
|
||||||
|
result = testVisit(
|
||||||
|
"{ foo, bar }",
|
||||||
|
"BlockStatement:has(CallExpression, Identifier[name='bar'])",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.type, "BlockStatement");
|
||||||
|
|
||||||
|
// This should not match
|
||||||
|
result = testVisit(
|
||||||
|
"{ foo, bar }",
|
||||||
|
"BlockStatement:has(CallExpression, Identifier[name='baz'])",
|
||||||
|
);
|
||||||
|
assertEquals(result, []);
|
||||||
|
|
||||||
|
// Attr match
|
||||||
|
result = testVisit(
|
||||||
|
"{ foo, bar }",
|
||||||
|
"Identifier:has([name='bar'])",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.type, "Identifier");
|
||||||
|
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 }",
|
||||||
|
"BlockStatement:not(Identifier[name='baz'])",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.type, "BlockStatement");
|
||||||
|
|
||||||
|
// Multiple sub queries
|
||||||
|
result = testVisit(
|
||||||
|
"{ foo, bar }",
|
||||||
|
"BlockStatement:not(Identifier[name='baz'], CallExpression)",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.type, "BlockStatement");
|
||||||
|
|
||||||
|
// This should not match
|
||||||
|
result = testVisit(
|
||||||
|
"{ foo, bar }",
|
||||||
|
"BlockStatement:not(CallExpression, Identifier)",
|
||||||
|
);
|
||||||
|
assertEquals(result, []);
|
||||||
|
|
||||||
|
// Attr match
|
||||||
|
result = testVisit(
|
||||||
|
"{ foo, bar }",
|
||||||
|
"Identifier:not([name='foo'])",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.type, "Identifier");
|
||||||
|
assertEquals(result[0].node.name, "bar");
|
||||||
|
});
|
||||||
|
|
||||||
Deno.test("Plugin - Program", async (t) => {
|
Deno.test("Plugin - Program", async (t) => {
|
||||||
await testSnapshot(t, "", "Program");
|
await testSnapshot(t, "", "Program");
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
parseSelector,
|
parseSelector,
|
||||||
PSEUDO_FIRST_CHILD,
|
PSEUDO_FIRST_CHILD,
|
||||||
PSEUDO_HAS,
|
PSEUDO_HAS,
|
||||||
|
PSEUDO_IS,
|
||||||
PSEUDO_LAST_CHILD,
|
PSEUDO_LAST_CHILD,
|
||||||
PSEUDO_NOT,
|
PSEUDO_NOT,
|
||||||
PSEUDO_NTH_CHILD,
|
PSEUDO_NTH_CHILD,
|
||||||
|
@ -554,7 +555,7 @@ Deno.test("Parser - Pseudo nth-child", () => {
|
||||||
assertThrows(() => testParse(":nth-child(2n - 1 foo)"));
|
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)"), [[
|
assertEquals(testParse(":has(Foo:has(Foo), Bar)"), [[
|
||||||
{
|
{
|
||||||
type: PSEUDO_HAS,
|
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: [
|
selectors: [
|
||||||
[
|
[
|
||||||
{ type: ELEM_NODE, elem: 1, wildcard: false },
|
{ type: ELEM_NODE, elem: 1, wildcard: false },
|
||||||
{
|
{
|
||||||
type: PSEUDO_HAS,
|
type: PSEUDO_IS,
|
||||||
selectors: [
|
selectors: [
|
||||||
[{ type: ELEM_NODE, elem: 1, wildcard: false }],
|
[{ 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: [
|
selectors: [
|
||||||
[
|
[
|
||||||
{ type: ELEM_NODE, elem: 1, wildcard: false },
|
{ type: ELEM_NODE, elem: 1, wildcard: false },
|
||||||
{
|
{
|
||||||
type: PSEUDO_HAS,
|
type: PSEUDO_IS,
|
||||||
selectors: [
|
selectors: [
|
||||||
[{ type: ELEM_NODE, elem: 1, wildcard: false }],
|
[{ 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", () => {
|
Deno.test("Parser - Pseudo not", () => {
|
||||||
|
|
Loading…
Add table
Reference in a new issue