1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-09 23:58:23 -05:00
denoland-deno/cli/js/40_lint_selector.js

1015 lines
22 KiB
JavaScript
Raw Normal View History

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// @ts-check
/** @typedef {import("./40_lint_types.d.ts").LintState} LintState */
/** @typedef {import("./40_lint_types.d.ts").AstContext} AstContext */
/** @typedef {import("./40_lint_types.d.ts").MatchContext} MatchCtx */
/** @typedef {import("./40_lint_types.d.ts").AttrExists} AttrExists */
/** @typedef {import("./40_lint_types.d.ts").AttrBin} AttrBin */
/** @typedef {import("./40_lint_types.d.ts").AttrSelector} AttrSelector */
/** @typedef {import("./40_lint_types.d.ts").ElemSelector} ElemSelector */
/** @typedef {import("./40_lint_types.d.ts").PseudoNthChild} PseudoNthChild */
/** @typedef {import("./40_lint_types.d.ts").PseudoHas} PseudoHas */
/** @typedef {import("./40_lint_types.d.ts").PseudoNot} PseudoNot */
/** @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 */
const Char = {
Tab: 9,
Space: 32,
Bang: 33,
DoubleQuote: 34,
Quote: 39,
BraceOpen: 40,
BraceClose: 41,
Plus: 43,
Comma: 44,
Minus: 45,
Dot: 46,
Slash: 47,
n0: 49,
n9: 57,
Colon: 58,
Less: 60,
Equal: 61,
Greater: 62,
A: 65,
Z: 90,
BracketOpen: 91,
BackSlash: 92,
BracketClose: 93,
Underscore: 95,
a: 97,
z: 122,
Tilde: 126,
};
export const Token = {
EOF: 0,
Word: 1,
Space: 2,
Op: 3,
Colon: 4,
Comma: 7,
BraceOpen: 8,
BraceClose: 9,
BracketOpen: 10,
BracketClose: 11,
String: 12,
Number: 13,
Bool: 14,
Null: 15,
Undefined: 16,
Dot: 17,
Minus: 17,
};
export const BinOp = {
/** [attr="value"] or [attr=value] */
Equal: 1,
/** [attr!="value"] or [attr!=value] */
NotEqual: 2,
/** [attr>1] */
Greater: 3,
/** [attr>=1] */
GreaterThan: 4,
/** [attr<1] */
Less: 5,
/** [attr<=1] */
LessThan: 6,
Tilde: 7,
Plus: 8,
Space: 9,
};
/**
* @param {string} s
* @returns {number}
*/
function getAttrOp(s) {
switch (s) {
case "=":
return BinOp.Equal;
case "!=":
return BinOp.NotEqual;
case ">":
return BinOp.Greater;
case ">=":
return BinOp.GreaterThan;
case "<":
return BinOp.Less;
case "<=":
return BinOp.LessThan;
case "~":
return BinOp.Tilde;
case "+":
return BinOp.Plus;
default:
throw new Error(`Unknown attribute operator: '${s}'`);
}
}
export class Lexer {
token = Token.Word;
start = 0;
end = 0;
ch = 0;
i = -1;
value = "";
/**
* @param {string} input
*/
constructor(input) {
this.input = input;
this.step();
this.next();
}
/**
* @param {number} token
*/
expect(token) {
if (this.token !== token) {
throw new Error(
`Expected token '${token}', but got '${this.token}'.\n\n${this.input}\n${
" ".repeat(this.i)
}^`,
);
}
}
/**
* @param {number} token
*/
readAsWordUntil(token) {
const s = this.i;
while (this.token !== Token.EOF && this.token !== token) {
this.next();
}
this.start = s;
this.end = this.i - 1;
this.value = this.getSlice();
}
getSlice() {
return this.input.slice(this.start, this.end);
}
step() {
this.i++;
if (this.i >= this.input.length) {
this.ch = -1;
} else {
this.ch = this.input.charCodeAt(this.i);
}
}
next() {
this.value = "";
if (this.i >= this.input.length) {
this.token = Token.EOF;
return;
}
// console.log(
// "NEXT",
// this.input,
// this.i,
// JSON.stringify(String.fromCharCode(this.ch)),
// );
while (true) {
switch (this.ch) {
case Char.Space:
while (this.isWhiteSpace()) {
this.step();
}
// Check if space preceeded operator
if (this.isOpContinue()) {
continue;
}
this.token = Token.Space;
return;
case Char.BracketOpen:
this.token = Token.BracketOpen;
this.step();
return;
case Char.BracketClose:
this.token = Token.BracketClose;
this.step();
return;
case Char.BraceOpen:
this.token = Token.BraceOpen;
this.step();
return;
case Char.BraceClose:
this.token = Token.BraceClose;
this.step();
return;
case Char.Colon:
this.token = Token.Colon;
this.step();
return;
case Char.Comma:
this.token = Token.Comma;
this.step();
return;
case Char.Dot:
this.token = Token.Dot;
this.step();
return;
case Char.Minus:
this.token = Token.Minus;
this.step();
return;
case Char.Plus:
case Char.Tilde:
case Char.Greater:
case Char.Equal:
case Char.Less:
case Char.Bang: {
this.token = Token.Op;
this.start = this.i;
this.step();
while (this.isOpContinue()) {
this.step();
}
this.end = this.i;
this.value = this.getSlice();
// Consume remaining space
while (this.isWhiteSpace()) {
this.step();
}
return;
}
case Char.Quote:
case Char.DoubleQuote: {
this.token = Token.String;
const ch = this.ch;
this.step();
this.start = this.i;
while (this.ch > 0 && this.ch !== ch) {
this.step();
}
this.end = this.i;
this.value = this.getSlice();
this.step();
return;
}
default:
this.start = this.i;
this.step();
while (this.isWordContinue()) {
this.step();
}
this.end = this.i;
this.value = this.getSlice();
this.token = Token.Word;
return;
}
}
}
isWordContinue() {
const ch = this.ch;
switch (ch) {
case Char.Minus:
case Char.Underscore:
return true;
default:
return (ch >= Char.a && ch <= Char.z) ||
(ch >= Char.A && ch <= Char.Z) ||
(ch >= Char.n0 && ch <= Char.n9);
}
}
isOpContinue() {
const ch = this.ch;
switch (ch) {
case Char.Equal:
case Char.Bang:
case Char.Greater:
case Char.Less:
case Char.Tilde:
case Char.Plus:
return true;
default:
return false;
}
}
isWhiteSpace() {
return this.ch === Char.Space || this.ch === Char.Tab;
}
}
const NUMBER_REG = /^(\d+\.)?\d+$/;
const BIGINT_REG = /^\d+n$/;
/**
* @param {string} raw
* @returns {any}
*/
function getFromRawValue(raw) {
switch (raw) {
case "true":
return true;
case "false":
return false;
case "null":
return null;
case "undefined":
return undefined;
default:
if (raw.startsWith("'") && raw.endsWith("'")) {
if (raw.length === 2) return "";
return raw.slice(1, -1);
} else if (raw.startsWith('"') && raw.endsWith('"')) {
if (raw.length === 2) return "";
return raw.slice(1, -1);
} else if (raw.startsWith("/")) {
const end = raw.lastIndexOf("/");
if (end === -1) throw new Error(`Invalid RegExp pattern: ${raw}`);
const pattern = raw.slice(1, end);
const flags = end < raw.length - 1 ? raw.slice(end + 1) : undefined;
return new RegExp(pattern, flags);
} else if (NUMBER_REG.test(raw)) {
return Number(raw);
} else if (BIGINT_REG.test(raw)) {
return BigInt(raw.slice(0, -1));
}
return raw;
}
}
export const ELEM_NODE = 1;
export const RELATION_NODE = 2;
export const ATTR_EXISTS_NODE = 3;
export const ATTR_BIN_NODE = 4;
export const PSEUDO_NTH_CHILD = 5;
export const PSEUDO_HAS = 6;
export const PSEUDO_NOT = 7;
export const PSEUDO_FIRST_CHILD = 8;
export const PSEUDO_LAST_CHILD = 9;
/**
* Parse out all unique selectors of a selector list.
* @param {string} input
* @returns {string[]}
*/
export function splitSelectors(input) {
/** @type {string[]} */
const out = [];
let last = 0;
let depth = 0;
for (let i = 0; i < input.length; i++) {
const ch = input.charCodeAt(i);
switch (ch) {
case Char.BraceOpen:
depth++;
break;
case Char.BraceClose:
depth--;
break;
case Char.Comma:
if (depth === 0) {
out.push(input.slice(last, i).trim());
last = i + 1;
}
break;
}
}
if (last < input.length - 1) {
out.push(input.slice(last).trim());
}
return out;
}
/**
* @param {string} input
* @param {Transformer} toElem
* @param {Transformer} toAttr
* @returns {Selector[]}
*/
export function parseSelector(input, toElem, toAttr) {
/** @type {Selector[]} */
const result = [];
/** @type {Selector[]} */
const stack = [[]];
const lex = new Lexer(input);
// Some subselectors like `:nth-child(.. of <selector>)` must have
// a single selector instead of selector list.
let throwOnComma = false;
while (lex.token !== Token.EOF) {
const current = /** @type {Selector} */ (stack.at(-1));
if (lex.token === Token.Word) {
const value = lex.value;
const wildcard = value === "*";
const elem = !wildcard ? toElem(value) : 0;
current.push({
type: ELEM_NODE,
elem,
wildcard,
});
lex.next();
continue;
} else if (lex.token === Token.Space) {
lex.next();
if (lex.token === Token.Word) {
current.push({
type: RELATION_NODE,
op: BinOp.Space,
});
}
continue;
} else if (lex.token === Token.BracketOpen) {
lex.next();
lex.expect(Token.Word);
// Check for value comparison
const prop = [toAttr(lex.value)];
lex.next();
while (lex.token === Token.Dot) {
lex.next();
lex.expect(Token.Word);
prop.push(toAttr(lex.value));
lex.next();
}
if (lex.token === Token.Op) {
const op = getAttrOp(lex.value);
lex.readAsWordUntil(Token.BracketClose);
const value = getFromRawValue(lex.value);
current.push({ type: ATTR_BIN_NODE, prop, op, value });
} else {
current.push({
type: ATTR_EXISTS_NODE,
prop,
});
}
lex.expect(Token.BracketClose);
lex.next();
continue;
} else if (lex.token === Token.Colon) {
lex.next();
lex.expect(Token.Word);
switch (lex.value) {
case "first-child":
current.push({
type: PSEUDO_FIRST_CHILD,
});
break;
case "last-child":
current.push({
type: PSEUDO_LAST_CHILD,
});
break;
case "nth-child": {
lex.next();
lex.expect(Token.BraceOpen);
lex.next();
let mul = 1;
let repeat = false;
let step = 0;
if (lex.token === Token.Minus) {
mul = -1;
lex.next();
}
lex.expect(Token.Word);
const value = lex.getSlice();
if (value.endsWith("n")) {
repeat = true;
step = +value.slice(0, -1) * mul;
} else {
step = +value * mul;
}
lex.next();
/** @type {PseudoNthChild} */
const node = {
type: PSEUDO_NTH_CHILD,
of: null,
op: null,
step,
stepOffset: 0,
repeat,
};
current.push(node);
if (lex.token === Token.Space) lex.next();
if (lex.token !== Token.BraceClose) {
if (lex.token === Token.Op) {
node.op = lex.value;
lex.next();
if (lex.token === Token.Space) lex.next();
} else if (lex.token === Token.Minus) {
node.op = "-";
lex.next();
if (lex.token === Token.Space) {
lex.next();
}
}
lex.expect(Token.Word);
node.stepOffset = +lex.value;
lex.next();
if (lex.token !== Token.BraceClose) {
lex.next(); // Space
if (lex.token === Token.Word) {
if (/** @type {string} */ (lex.value) !== "of") {
throw new Error(
`Expected 'of' keyword in ':nth-child' but got: ${lex.value}`,
);
}
lex.next();
lex.expect(Token.Space);
lex.next();
throwOnComma = true;
stack.push([]);
}
continue;
}
lex.expect(Token.BraceClose);
} else if (!node.repeat) {
// :nth-child(2) -> step is actually stepOffset
node.stepOffset = node.step - 1;
node.step = 0;
}
lex.next();
continue;
}
case "has":
case "where":
case "is": {
lex.next();
lex.expect(Token.BraceOpen);
lex.next();
current.push({
type: PSEUDO_HAS,
selectors: [],
});
stack.push([]);
continue;
}
case "not": {
lex.next();
lex.expect(Token.BraceOpen);
lex.next();
current.push({
type: PSEUDO_NOT,
selectors: [],
});
stack.push([]);
continue;
}
default:
throw new Error(`Unknown pseudo selector: '${lex.value}'`);
}
} else if (lex.token === Token.Comma) {
if (throwOnComma) {
throw new Error(`Multiple selector arguments not supported here`);
}
lex.next();
if (lex.token === Token.Space) {
lex.next();
}
popSelector(result, stack);
stack.push([]);
continue;
} else if (lex.token === Token.BraceClose) {
throwOnComma = false;
popSelector(result, stack);
} else if (lex.token === Token.Op) {
current.push({
type: RELATION_NODE,
op: getAttrOp(lex.value),
});
}
lex.next();
}
if (stack.length > 0) {
result.push(stack[0]);
}
return result;
}
/**
* @param {Selector[]} result
* @param {Selector[]} stack
*/
function popSelector(result, stack) {
const sel = /** @type {Selector} */ (stack.pop());
if (stack.length === 0) {
result.push(sel);
stack.push([]);
} else {
const prev = /** @type {Selector} */ (stack.at(-1));
if (prev.length === 0) {
throw new Error(`Empty selector`);
}
const node = prev.at(-1);
if (node === undefined) {
throw new Error(`Empty node`);
}
if (node.type === PSEUDO_NTH_CHILD) {
node.of = sel;
} else if (node.type === PSEUDO_HAS || node.type === PSEUDO_NOT) {
node.selectors.push(sel);
} else {
throw new Error(`Multiple selectors not allowed here`);
}
}
}
const TRUE_FN = () => {
return true;
};
/**
* @param {Selector} selector
* @returns {MatcherFn}
*/
export function compileSelector(selector) {
/** @type {MatcherFn} */
let fn = TRUE_FN;
for (let i = 0; i < selector.length; i++) {
const node = selector[i];
switch (node.type) {
case ELEM_NODE:
fn = matchElem(node, fn);
break;
case RELATION_NODE:
switch (node.op) {
case BinOp.Space:
fn = matchDescendant(fn);
break;
case BinOp.Greater:
fn = matchChild(fn);
break;
case BinOp.Plus:
fn = matchAdjacent(fn);
break;
case BinOp.Tilde:
fn = matchFollowing(fn);
break;
default:
throw new Error(`Unknown relation op ${node.op}`);
}
break;
case ATTR_EXISTS_NODE:
fn = matchAttrExists(node, fn);
break;
case ATTR_BIN_NODE:
fn = matchAttrBin(node, fn);
break;
case PSEUDO_FIRST_CHILD:
fn = matchFirstChild(fn);
break;
case PSEUDO_LAST_CHILD:
fn = matchLastChild(fn);
break;
case PSEUDO_NTH_CHILD:
fn = matchNthChild(node, fn);
break;
case PSEUDO_HAS:
// FIXME
// fn = matchIs(part, fn);
throw new Error("TODO: :has");
case PSEUDO_NOT:
fn = matchNot(node.selectors, fn);
break;
default:
// @ts-ignore error handling
// deno-lint-ignore no-console
console.log(node);
throw new Error(`Unknown selector node`);
}
}
return fn;
}
/**
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchFirstChild(next) {
return (ctx, id) => {
const parent = ctx.getParent(id);
const first = ctx.getFirstChild(parent);
return first === id && next(ctx, first);
};
}
/**
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchLastChild(next) {
return (ctx, id) => {
const parent = ctx.getParent(id);
const last = ctx.getLastChild(parent);
return last === id && next(ctx, id);
};
}
/**
* @param {PseudoNthChild} node
* @param {number} i
* @returns {number}
*/
function getNthAnB(node, i) {
const n = node.step * i;
if (node.op === null) return n;
switch (node.op) {
case "+":
return n + node.stepOffset;
case "-":
return n - node.stepOffset;
default:
throw new Error("Not supported nth-child operator: " + node.op);
}
}
/**
* @param {PseudoNthChild} node
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchNthChild(node, next) {
const ofSelector = node.of !== null ? compileSelector(node.of) : TRUE_FN;
// TODO(@marvinhagemeister): we should probably cache results here
return (ctx, id) => {
const siblings = ctx.getSiblings(id);
const idx = siblings.indexOf(id);
if (!node.repeat) {
return idx === node.stepOffset && next(ctx, id);
}
for (let i = 0; i < siblings.length; i++) {
const n = getNthAnB(node, i);
if (n > siblings.length - 1) return false;
const search = siblings[n];
if (id === search) {
if (node.of !== null && !ofSelector(ctx, id)) {
continue;
} else if (next(ctx, id)) {
return true;
}
} else if (n > idx) {
return false;
}
}
return false;
};
}
/**
* @param {Selector[]} selectors
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchNot(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 fn = compiled[i];
if (fn(ctx, id)) {
return false;
}
}
return next(ctx, id);
};
}
/**
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchDescendant(next) {
// TODO(@marvinhagemeister): we should probably cache results here
return (ctx, id) => {
let current = ctx.getParent(id);
while (current > 0) {
if (next(ctx, current)) {
return true;
}
current = ctx.getParent(current);
}
return false;
};
}
/**
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchChild(next) {
return (ctx, id) => {
const parent = ctx.getParent(id);
if (parent < 0) return false;
return next(ctx, parent);
};
}
/**
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchAdjacent(next) {
return (ctx, id) => {
const siblings = ctx.getSiblings(id);
const idx = siblings.indexOf(id) - 1;
if (idx < 0) return false;
const prev = siblings[idx];
return next(ctx, prev);
};
}
/**
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchFollowing(next) {
return (ctx, id) => {
const siblings = ctx.getSiblings(id);
const idx = siblings.indexOf(id) - 1;
if (idx < 0) return false;
for (let i = idx; i >= 0; i--) {
const sib = siblings[i];
if (next(ctx, sib)) return true;
}
return false;
};
}
/**
* @param {ElemSelector} part
* @param {MatcherFn} next
* @returns {MatcherFn}
*/
function matchElem(part, next) {
return (ctx, id) => {
// Placeholder node cannot be matched
if (id === 0) return false;
// Wildcard always matches
else if (part.wildcard) return next(ctx, id);
// 0 means it's the placeholder node which
// can never be matched.
else if (part.elem === 0) return false;
const type = ctx.getType(id);
if (type > 0 && type === part.elem) return next(ctx, id);
return false;
};
}
/**
* @param {AttrExists} attr
* @param {MatcherFn} next
* @returns {MatcherFn}
*/
function matchAttrExists(attr, next) {
return (ctx, id) => {
return ctx.hasAttrPath(id, attr.prop, 0) ? next(ctx, id) : false;
};
}
/**
* @param {AttrBin} attr
* @param {MatcherFn} next
* @returns {MatcherFn}
*/
function matchAttrBin(attr, next) {
return (ctx, id) => {
if (!ctx.hasAttrPath(id, attr.prop, 0)) return false;
const value = ctx.getAttrPathValue(id, attr.prop, 0);
if (!matchAttrValue(attr, value)) return false;
return next(ctx, id);
};
}
/**
* @param {AttrBin} attr
* @param {*} value
* @returns {boolean}
*/
function matchAttrValue(attr, value) {
switch (attr.op) {
case BinOp.Equal:
return value === attr.value;
case BinOp.NotEqual:
return value !== attr.value;
case BinOp.Greater:
return typeof value === "number" && typeof attr.value === "number" &&
value > attr.value;
case BinOp.GreaterThan:
return typeof value === "number" && typeof attr.value === "number" &&
value >= attr.value;
case BinOp.Less:
return typeof value === "number" && typeof attr.value === "number" &&
value < attr.value;
case BinOp.LessThan:
return typeof value === "number" && typeof attr.value === "number" &&
value <= attr.value;
default:
return false;
}
}