2024-12-31 14:12:39 -05:00
|
|
|
// Copyright 2018-2025 the Deno authors. MIT license.
|
2024-12-23 02:45:47 -05:00
|
|
|
|
|
|
|
// @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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-07 18:21:50 -05:00
|
|
|
const remaining = input.slice(last).trim();
|
|
|
|
if (remaining.length > 0) {
|
|
|
|
out.push(remaining);
|
2024-12-23 02:45:47 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|