mirror of
https://github.com/denoland/deno.git
synced 2025-08-03 10:33:54 +00: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
|
@ -16,7 +16,6 @@
|
|||
/** @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").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").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() {
|
||||
this.value = "";
|
||||
|
||||
|
@ -378,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.
|
||||
|
@ -459,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;
|
||||
|
@ -617,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: [],
|
||||
|
@ -705,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`);
|
||||
|
@ -769,8 +822,11 @@ export function compileSelector(selector) {
|
|||
fn = matchNthChild(node, fn);
|
||||
break;
|
||||
case PSEUDO_HAS:
|
||||
// TODO(@marvinhagemeister)
|
||||
throw new Error("TODO: :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;
|
||||
|
@ -786,7 +842,7 @@ export function compileSelector(selector) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {NextFn} next
|
||||
* @param {MatcherFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchFirstChild(next) {
|
||||
|
@ -797,7 +853,7 @@ function matchFirstChild(next) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {NextFn} next
|
||||
* @param {MatcherFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchLastChild(next) {
|
||||
|
@ -829,7 +885,7 @@ function getNthAnB(node, i) {
|
|||
|
||||
/**
|
||||
* @param {PseudoNthChild} node
|
||||
* @param {NextFn} next
|
||||
* @param {MatcherFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchNthChild(node, next) {
|
||||
|
@ -868,7 +924,64 @@ function matchNthChild(node, next) {
|
|||
|
||||
/**
|
||||
* @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}
|
||||
*/
|
||||
function matchNot(selectors, next) {
|
||||
|
@ -880,20 +993,27 @@ function matchNot(selectors, next) {
|
|||
compiled.push(compileSelector(sel));
|
||||
}
|
||||
|
||||
/** @type {Map<number, boolean>} */
|
||||
const cache = new Map();
|
||||
|
||||
return (ctx, id) => {
|
||||
for (let i = 0; i < compiled.length; i++) {
|
||||
const fn = compiled[i];
|
||||
if (fn(ctx, id)) {
|
||||
return false;
|
||||
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 next(ctx, id);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NextFn} next
|
||||
* @param {MatcherFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchDescendant(next) {
|
||||
|
@ -913,7 +1033,7 @@ function matchDescendant(next) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {NextFn} next
|
||||
* @param {MatcherFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchChild(next) {
|
||||
|
@ -926,7 +1046,7 @@ function matchChild(next) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {NextFn} next
|
||||
* @param {MatcherFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchAdjacent(next) {
|
||||
|
@ -942,7 +1062,7 @@ function matchAdjacent(next) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {NextFn} next
|
||||
* @param {MatcherFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchFollowing(next) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue