mirror of
https://github.com/denoland/deno.git
synced 2025-01-21 04:52:26 -05:00
WIP
This commit is contained in:
parent
f167277561
commit
86fc6e64cb
7 changed files with 880 additions and 349 deletions
|
@ -3,6 +3,10 @@
|
|||
// @ts-check
|
||||
|
||||
import { core } from "ext:core/mod.js";
|
||||
import { compileSelector, parseSelector } from "ext:cli/lint/selector.js";
|
||||
|
||||
console.log({ compileSelector, foo: "foo" });
|
||||
|
||||
const {
|
||||
op_lint_get_rule,
|
||||
op_lint_get_source,
|
||||
|
|
|
@ -1,66 +1,75 @@
|
|||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
// @ts-check
|
||||
|
||||
/** @typedef {import("./internal.d.ts").LintState} LintState */
|
||||
/** @typedef {import("./internal.d.ts").AstContext} AstContext */
|
||||
/** @typedef {import("./internal.d.ts").MatchCtx} MatchCtx */
|
||||
/** @typedef {import("./internal.d.ts").AttrOp} AttrOp */
|
||||
/** @typedef {import("./internal.d.ts").AttrExists} AttrExists */
|
||||
/** @typedef {import("./internal.d.ts").AttrBin} AttrBin */
|
||||
/** @typedef {import("./internal.d.ts").AttrSelector} AttrSelector */
|
||||
/** @typedef {import("./internal.d.ts").Elem} SelectorPart */
|
||||
/** @typedef {import("./internal.d.ts").PseudoNthChild} PseudoNthChild */
|
||||
/** @typedef {import("./internal.d.ts").PseudoHas} PseudoHas */
|
||||
/** @typedef {import("./internal.d.ts").PseudoNot} PseudoNot */
|
||||
/** @typedef {import("./internal.d.ts").Relation} SRelation */
|
||||
/** @typedef {import("./internal.d.ts").Selector} Selector */
|
||||
/** @typedef {import("./internal.d.ts").SelectorParseCtx} SelectorParseCtx */
|
||||
/** @typedef {import("./internal.d.ts").ILexer} ILexer */
|
||||
/** @typedef {import("./internal.d.ts").NextFn} NextFn */
|
||||
/** @typedef {import("./internal.d.ts").MatcherFn} MatcherFn */
|
||||
/** @typedef {import("./internal.d.ts").AttrRegex} AttrRegex */
|
||||
/** @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").MatchCtx} 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").Transformer} 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,
|
||||
};
|
||||
|
||||
const Token = {
|
||||
Value: 0,
|
||||
Char: 1,
|
||||
Attr: 2,
|
||||
Pseudo: 3,
|
||||
EOF: 4,
|
||||
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,
|
||||
};
|
||||
|
||||
const AttrOp = {
|
||||
const BinOp = {
|
||||
/** [attr="value"] or [attr=value] */
|
||||
Equal: 1,
|
||||
/** [attr!="value"] or [attr!=value] */
|
||||
|
@ -73,6 +82,8 @@ const AttrOp = {
|
|||
Less: 5,
|
||||
/** [attr<=1] */
|
||||
LessThan: 6,
|
||||
Tilde: 7,
|
||||
Plus: 8,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -82,242 +93,241 @@ const AttrOp = {
|
|||
function getAttrOp(s) {
|
||||
switch (s) {
|
||||
case "=":
|
||||
return AttrOp.Equal;
|
||||
return BinOp.Equal;
|
||||
case "!=":
|
||||
return AttrOp.NotEqual;
|
||||
return BinOp.NotEqual;
|
||||
case ">":
|
||||
return AttrOp.Greater;
|
||||
return BinOp.Greater;
|
||||
case ">=":
|
||||
return AttrOp.GreaterThan;
|
||||
return BinOp.GreaterThan;
|
||||
case "<":
|
||||
return AttrOp.Less;
|
||||
return BinOp.Less;
|
||||
case "<=":
|
||||
return AttrOp.LessThan;
|
||||
return BinOp.LessThan;
|
||||
case "~":
|
||||
return BinOp.Tilde;
|
||||
case "+":
|
||||
return BinOp.Plus;
|
||||
default:
|
||||
throw new Error(`Unknown attribute operator: '${s}'`);
|
||||
}
|
||||
}
|
||||
|
||||
/** @implements ILexer */
|
||||
class Lexer {
|
||||
token = Token.Value;
|
||||
export class Lexer {
|
||||
token = Token.Word;
|
||||
start = 0;
|
||||
end = 0;
|
||||
ch = 0;
|
||||
i = 0;
|
||||
i = -1;
|
||||
|
||||
value = "";
|
||||
value2 = "";
|
||||
op = 0;
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
*/
|
||||
constructor(input) {
|
||||
this.input = input;
|
||||
this.step();
|
||||
this.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} char
|
||||
* @param {number} token
|
||||
*/
|
||||
expect(char) {
|
||||
if (this.i >= this.input.length) {
|
||||
expect(token) {
|
||||
if (this.token !== token) {
|
||||
console.log(this, JSON.stringify(String.fromCharCode(this.ch)));
|
||||
throw new Error(
|
||||
`Unterminated selector:\n\n${this.input}\n${" ".repeat(this.i)}^`,
|
||||
`Expected token '${token}', but got '${this.token}'.\n\n${this.input}\n${
|
||||
" ".repeat(this.i)
|
||||
}^`,
|
||||
);
|
||||
}
|
||||
const ch = this.input.charCodeAt(this.i);
|
||||
if (ch !== char) {
|
||||
throw new Error(
|
||||
`Expected character '${
|
||||
String.fromCharCode(ch)
|
||||
}', but got '${ch}'.\n\n${this.input}\n${" ".repeat(this.i)}^`,
|
||||
);
|
||||
}
|
||||
|
||||
this.i++;
|
||||
}
|
||||
|
||||
getChar() {
|
||||
return this.input.charCodeAt(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.i);
|
||||
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 = "";
|
||||
this.value2 = "";
|
||||
this.op = 0;
|
||||
|
||||
if (this.i >= this.input.length) {
|
||||
this.token = Token.EOF;
|
||||
return;
|
||||
}
|
||||
|
||||
let ch = this.input.charCodeAt(this.i);
|
||||
console.log("NEXT", JSON.stringify(String.fromCharCode(ch)));
|
||||
switch (ch) {
|
||||
case Char.Space:
|
||||
while (ch === Char.Space) {
|
||||
ch = this.getChar();
|
||||
this.i++;
|
||||
}
|
||||
console.log(
|
||||
"NEXT",
|
||||
this.input,
|
||||
this.i,
|
||||
JSON.stringify(String.fromCharCode(this.ch)),
|
||||
);
|
||||
if (this.i > 30) throw new Error("infininte #2");
|
||||
|
||||
// Check if this a sibling/descendant selector
|
||||
if (ch === Char.Plus || ch === Char.Tilde || ch === Char.Greater) {
|
||||
let j = 0;
|
||||
while (true) {
|
||||
if (j++ > 50) throw new Error("infininte");
|
||||
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.ch = ch;
|
||||
this.token = Token.Char;
|
||||
this.value = this.getSlice();
|
||||
|
||||
console.log("--> yeah");
|
||||
this.i++;
|
||||
ch = this.getChar();
|
||||
|
||||
while (ch === Char.Space) {
|
||||
ch = this.getChar();
|
||||
this.i++;
|
||||
// Consume remaining space
|
||||
while (this.isWhiteSpace()) {
|
||||
this.step();
|
||||
}
|
||||
} else {
|
||||
|
||||
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.ch = Char.Space;
|
||||
this.token = Token.Char;
|
||||
this.i--;
|
||||
this.value = this.getSlice();
|
||||
this.step();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
case Char.BracketOpen: {
|
||||
this.i++;
|
||||
this.start = this.i;
|
||||
|
||||
let hasValue = false;
|
||||
while (this.i < this.input.length) {
|
||||
ch = this.getChar();
|
||||
if (
|
||||
ch === Char.Equal || ch === Char.Greater || ch === Char.Less ||
|
||||
ch === Char.Bang
|
||||
) {
|
||||
this.value = this.getSlice().trim();
|
||||
hasValue = true;
|
||||
break;
|
||||
} else if (ch === Char.BracketClose) {
|
||||
this.value = this.getSlice();
|
||||
break;
|
||||
}
|
||||
|
||||
this.i++;
|
||||
}
|
||||
|
||||
if (hasValue) {
|
||||
default:
|
||||
this.start = this.i;
|
||||
while (
|
||||
ch === Char.Equal || ch === Char.Greater || ch === Char.Less ||
|
||||
ch === Char.Bang
|
||||
) {
|
||||
this.i++;
|
||||
ch = this.getChar();
|
||||
this.step();
|
||||
|
||||
while (this.isWordContinue()) {
|
||||
this.step();
|
||||
}
|
||||
|
||||
this.op = getAttrOp(this.getSlice());
|
||||
|
||||
this.start = this.i;
|
||||
|
||||
while (this.i < this.input.length) {
|
||||
ch = this.input.charCodeAt(this.i);
|
||||
if (ch === Char.BracketClose) {
|
||||
const raw = this.getSlice().trim();
|
||||
this.value2 = getFromRawValue(raw);
|
||||
break;
|
||||
}
|
||||
|
||||
this.i++;
|
||||
}
|
||||
}
|
||||
|
||||
this.expect(Char.BracketClose);
|
||||
|
||||
this.end = this.i;
|
||||
this.token = Token.Attr;
|
||||
|
||||
break;
|
||||
this.end = this.i;
|
||||
this.value = this.getSlice();
|
||||
this.token = Token.Word;
|
||||
return;
|
||||
}
|
||||
case Char.Greater:
|
||||
case Char.Plus:
|
||||
case Char.Tilde:
|
||||
case Char.Comma:
|
||||
case Char.BraceClose: {
|
||||
const original = ch;
|
||||
this.start = this.i;
|
||||
this.i++;
|
||||
while (this.i < this.input.length) {
|
||||
ch = this.getChar();
|
||||
if (ch !== Char.Space) {
|
||||
break;
|
||||
}
|
||||
|
||||
this.i++;
|
||||
}
|
||||
console.log("char", String.fromCharCode(original));
|
||||
this.end = this.i;
|
||||
this.token = Token.Char;
|
||||
this.ch = original;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Pseudo
|
||||
case Char.Colon:
|
||||
this.start = this.i;
|
||||
this.i++;
|
||||
while (this.i < this.input.length) {
|
||||
ch = this.getChar();
|
||||
if (
|
||||
ch === Char.Space || ch === Char.BracketOpen ||
|
||||
ch === Char.BraceOpen
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
this.i++;
|
||||
}
|
||||
|
||||
this.end = this.i;
|
||||
this.token = Token.Pseudo;
|
||||
this.value = this.getSlice();
|
||||
break;
|
||||
|
||||
default:
|
||||
this.start = this.i;
|
||||
this.i++;
|
||||
|
||||
loop: while (this.i < this.input.length) {
|
||||
ch = this.getChar();
|
||||
|
||||
switch (ch) {
|
||||
case Char.Space:
|
||||
case Char.Comma:
|
||||
case Char.Colon:
|
||||
case Char.BracketOpen:
|
||||
case Char.BraceClose:
|
||||
case Char.Greater:
|
||||
case Char.Plus:
|
||||
case Char.Tilde:
|
||||
console.log("BREAK", this.getSlice());
|
||||
break loop;
|
||||
default:
|
||||
this.i++;
|
||||
}
|
||||
}
|
||||
|
||||
this.end = this.i;
|
||||
this.value = this.getSlice();
|
||||
console.log({ v: this.value });
|
||||
this.token = Token.Value;
|
||||
}
|
||||
}
|
||||
|
||||
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+$/;
|
||||
|
@ -345,10 +355,10 @@ function getFromRawValue(raw) {
|
|||
if (raw.length === 2) return "";
|
||||
return raw.slice(1, -1);
|
||||
} else if (raw.startsWith("/")) {
|
||||
const end = raw.lastIndexOf("/", 1);
|
||||
const end = raw.lastIndexOf("/");
|
||||
if (end === -1) throw new Error(`Invalid RegExp pattern: ${raw}`);
|
||||
const pattern = raw.slice(0, end);
|
||||
const flags = end < raw.length - 1 ? raw.slice(end) : undefined;
|
||||
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);
|
||||
|
@ -360,24 +370,23 @@ function getFromRawValue(raw) {
|
|||
}
|
||||
}
|
||||
|
||||
const ELEM_NODE = 1;
|
||||
const RELATION_NODE = 2;
|
||||
const ATTR_EXISTS_NODE = 3;
|
||||
const ATTR_BIN_NODE = 4;
|
||||
const ATTR_REGEX_NODE = 5;
|
||||
const PSEUDO_NODE_NTH_CHILD = 6;
|
||||
const PSEUDO_NODE_HAS = 7;
|
||||
const PSEUDO_NODE_NOT = 8;
|
||||
const PSEUDO_FIRST_CHILD = 9;
|
||||
const PSEUDO_LAST_CHILD = 10;
|
||||
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_NODE_NOT = 7;
|
||||
export const PSEUDO_FIRST_CHILD = 8;
|
||||
export const PSEUDO_LAST_CHILD = 9;
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Record<string, number>} astNodes
|
||||
* @param {Record<string, number>} astAttrs
|
||||
* @param {Transformer} toElem
|
||||
* @param {Transformer} toAttr
|
||||
* @returns {Selector[]}
|
||||
*/
|
||||
export function parseSelector(input, astNodes, astAttrs) {
|
||||
export function parseSelector(input, toElem, toAttr) {
|
||||
/** @type {Selector[]} */
|
||||
const result = [];
|
||||
|
||||
|
@ -385,95 +394,155 @@ export function parseSelector(input, astNodes, astAttrs) {
|
|||
let current = [];
|
||||
|
||||
const lex = new Lexer(input);
|
||||
lex.next();
|
||||
|
||||
while (lex.token !== Token.EOF) {
|
||||
console.log(
|
||||
lex.token,
|
||||
JSON.stringify(String.fromCharCode(lex.ch)),
|
||||
Token,
|
||||
result,
|
||||
current,
|
||||
);
|
||||
if (lex.token === Token.Value) {
|
||||
const name = lex.value;
|
||||
const wildcard = name === "*";
|
||||
|
||||
let elem = 0;
|
||||
if (!wildcard) {
|
||||
elem = astNodes[name];
|
||||
if (elem === undefined) {
|
||||
throw new Error(`Unkown element: ${name}`);
|
||||
}
|
||||
}
|
||||
if (lex.token === Token.Word) {
|
||||
const value = lex.value;
|
||||
const wildcard = value === "*";
|
||||
|
||||
const elem = !wildcard ? toElem(value) : 0;
|
||||
current.push({
|
||||
type: ELEM_NODE,
|
||||
elem,
|
||||
debug: name,
|
||||
wildcard,
|
||||
});
|
||||
} else if (lex.token === Token.Attr) {
|
||||
const name = lex.value;
|
||||
const id = astAttrs[name];
|
||||
if (id === undefined) {
|
||||
console.log(lex);
|
||||
throw new Error(`Unknown attribute: ${name}`);
|
||||
lex.next();
|
||||
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.value2 === "") {
|
||||
current.push({
|
||||
type: ATTR_EXISTS_NODE,
|
||||
prop: id,
|
||||
debug: lex.value,
|
||||
});
|
||||
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_BIN_NODE,
|
||||
prop: id,
|
||||
op: lex.op,
|
||||
debug: lex.value,
|
||||
value: lex.value2,
|
||||
type: ATTR_EXISTS_NODE,
|
||||
prop,
|
||||
});
|
||||
}
|
||||
} else if (lex.token === Token.Pseudo) {
|
||||
|
||||
lex.expect(Token.BracketClose);
|
||||
lex.next();
|
||||
continue;
|
||||
} else if (lex.token === Token.Colon) {
|
||||
lex.next();
|
||||
lex.expect(Token.Word);
|
||||
|
||||
console.log("PSEUDO", lex);
|
||||
switch (lex.value) {
|
||||
case ":first-child":
|
||||
case "first-child":
|
||||
current.push({
|
||||
type: PSEUDO_FIRST_CHILD,
|
||||
});
|
||||
break;
|
||||
case ":last-child":
|
||||
case "last-child":
|
||||
current.push({
|
||||
type: PSEUDO_LAST_CHILD,
|
||||
});
|
||||
break;
|
||||
case ":nth-child":
|
||||
lex.expect(Char.BraceOpen);
|
||||
case "nth-child": {
|
||||
lex.next();
|
||||
lex.expect(Token.BraceOpen);
|
||||
lex.next();
|
||||
|
||||
let backwards = false;
|
||||
let repeat = false;
|
||||
let step = 0;
|
||||
if (lex.token === Token.Minus) {
|
||||
backwards = true;
|
||||
lex.next();
|
||||
}
|
||||
|
||||
lex.expect(Token.Word);
|
||||
const value = lex.getSlice();
|
||||
|
||||
if (value.endsWith("n")) {
|
||||
repeat = true;
|
||||
step = +value.slice(0, -1);
|
||||
} else {
|
||||
step = +value;
|
||||
}
|
||||
|
||||
let stepOffset = 0;
|
||||
lex.next();
|
||||
|
||||
if (lex.token !== Token.BraceClose) {
|
||||
lex.expect(Token.Op);
|
||||
if (lex.value !== "+") {
|
||||
throw new Error(
|
||||
`Unknown operator in ':nth-child' selector: '${lex.value}'`,
|
||||
);
|
||||
}
|
||||
|
||||
lex.next();
|
||||
lex.expect(Token.Word);
|
||||
stepOffset = +lex.value;
|
||||
lex.next();
|
||||
|
||||
lex.next(); // Space
|
||||
|
||||
if (lex.token === Token.Word) {
|
||||
if (lex.value !== "of") {
|
||||
throw new Error(
|
||||
`Expected 'of' keyword in ':nth-child' but got: ${lex.value}`,
|
||||
);
|
||||
}
|
||||
|
||||
lex.next();
|
||||
lex.expect(Token.Space);
|
||||
lex.next();
|
||||
}
|
||||
|
||||
lex.expect(Token.BraceClose);
|
||||
//
|
||||
console.log("CLOSE", Token.Op, lex.token);
|
||||
}
|
||||
|
||||
console.log("nth", lex);
|
||||
console.log(lex.getSlice());
|
||||
|
||||
current.push({
|
||||
type: PSEUDO_NODE_NTH_CHILD,
|
||||
type: PSEUDO_NTH_CHILD,
|
||||
backwards,
|
||||
of: null,
|
||||
step,
|
||||
stepOffset,
|
||||
repeat,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown pseudo selector: '${lex.value}'`);
|
||||
}
|
||||
} else if (lex.ch === Char.Comma) {
|
||||
result.push(current);
|
||||
current = [];
|
||||
} else if (
|
||||
lex.ch === Char.Space || lex.ch === Char.Plus || lex.ch === Char.Tilde ||
|
||||
lex.ch === Char.Greater
|
||||
) {
|
||||
} else if (lex.token === Token.Op) {
|
||||
current.push({
|
||||
type: RELATION_NODE,
|
||||
op: lex.ch,
|
||||
debug: JSON.stringify(String.fromCharCode(lex.ch)),
|
||||
op: getAttrOp(lex.value),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -531,19 +600,16 @@ export function compileSelector(selector) {
|
|||
case ATTR_BIN_NODE:
|
||||
fn = matchAttrBin(node, fn);
|
||||
break;
|
||||
case ATTR_REGEX_NODE:
|
||||
fn = matchAttrRegex(node, fn);
|
||||
break;
|
||||
case PSEUDO_FIRST_CHILD:
|
||||
fn = matchFirstChild(fn);
|
||||
break;
|
||||
case PSEUDO_LAST_CHILD:
|
||||
fn = matchLastChild(fn);
|
||||
break;
|
||||
case PSEUDO_NODE_NTH_CHILD:
|
||||
case PSEUDO_NTH_CHILD:
|
||||
fn = matchNthChild(node, fn);
|
||||
break;
|
||||
case PSEUDO_NODE_HAS:
|
||||
case PSEUDO_HAS:
|
||||
// FIXME
|
||||
// fn = matchIs(part, fn);
|
||||
throw new Error("TODO: :has");
|
||||
|
@ -724,7 +790,7 @@ function matchFollowing(next) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {SelectorPart} part
|
||||
* @param {ElemSelector} part
|
||||
* @param {MatcherFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
|
@ -770,39 +836,23 @@ function matchAttrBin(attr, next) {
|
|||
*/
|
||||
function matchAttrValue(attr, value) {
|
||||
switch (attr.op) {
|
||||
case AttrOp.Equal:
|
||||
case BinOp.Equal:
|
||||
return value === attr.value;
|
||||
case AttrOp.NotEqual:
|
||||
case BinOp.NotEqual:
|
||||
return value !== attr.value;
|
||||
case AttrOp.Greater:
|
||||
case BinOp.Greater:
|
||||
return typeof value === "number" && typeof attr.value === "number" &&
|
||||
value > attr.value;
|
||||
case AttrOp.GreaterThan:
|
||||
case BinOp.GreaterThan:
|
||||
return typeof value === "number" && typeof attr.value === "number" &&
|
||||
value >= attr.value;
|
||||
case AttrOp.Less:
|
||||
case BinOp.Less:
|
||||
return typeof value === "number" && typeof attr.value === "number" &&
|
||||
value < attr.value;
|
||||
case AttrOp.LessThan:
|
||||
case BinOp.LessThan:
|
||||
return typeof value === "number" && typeof attr.value === "number" &&
|
||||
value <= attr.value;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AttrRegex} attr
|
||||
* @param {MatcherFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchAttrRegex(attr, next) {
|
||||
return (ctx, id) => {
|
||||
const value = ctx.getAttrValue(id, attr.prop);
|
||||
if (!attr.value.test(String(value))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return next(ctx, id);
|
||||
};
|
||||
}
|
||||
|
|
484
cli/js/40_lint_selector_parser_test.ts
Normal file
484
cli/js/40_lint_selector_parser_test.ts
Normal file
|
@ -0,0 +1,484 @@
|
|||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
import {
|
||||
ATTR_BIN_NODE,
|
||||
ATTR_EXISTS_NODE,
|
||||
ELEM_NODE,
|
||||
Lexer,
|
||||
parseSelector,
|
||||
PSEUDO_FIRST_CHILD,
|
||||
PSEUDO_HAS,
|
||||
PSEUDO_LAST_CHILD,
|
||||
PSEUDO_NTH_CHILD,
|
||||
RELATION_NODE,
|
||||
Token,
|
||||
} from "./40_lint_selector.js";
|
||||
import { expect } from "@std/expect";
|
||||
|
||||
interface LexState {
|
||||
token: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function testLexer(input: string): LexState[] {
|
||||
const out: LexState[] = [];
|
||||
const l = new Lexer(input);
|
||||
|
||||
while (l.token !== Token.EOF) {
|
||||
out.push({ token: l.token, value: l.value });
|
||||
l.next();
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
const Tags: Record<string, number> = { Foo: 1, Bar: 2, FooBar: 3 };
|
||||
const Attrs: Record<string, number> = { foo: 1, bar: 2, foobar: 3 };
|
||||
const toTag = (name: string): number => Tags[name];
|
||||
const toAttr = (name: string): number => Attrs[name];
|
||||
|
||||
Deno.test("Lexer - Elem", () => {
|
||||
expect(testLexer("Foo")).toEqual([
|
||||
{ token: Token.Word, value: "Foo" },
|
||||
]);
|
||||
expect(testLexer("foo-bar")).toEqual([
|
||||
{ token: Token.Word, value: "foo-bar" },
|
||||
]);
|
||||
expect(testLexer("foo_bar")).toEqual([
|
||||
{ token: Token.Word, value: "foo_bar" },
|
||||
]);
|
||||
expect(testLexer("Foo Bar Baz")).toEqual([
|
||||
{ token: Token.Word, value: "Foo" },
|
||||
{ token: Token.Space, value: "" },
|
||||
{ token: Token.Word, value: "Bar" },
|
||||
{ token: Token.Space, value: "" },
|
||||
{ token: Token.Word, value: "Baz" },
|
||||
]);
|
||||
expect(testLexer("Foo Bar Baz")).toEqual([
|
||||
{ token: Token.Word, value: "Foo" },
|
||||
{ token: Token.Space, value: "" },
|
||||
{ token: Token.Word, value: "Bar" },
|
||||
{ token: Token.Space, value: "" },
|
||||
{ token: Token.Word, value: "Baz" },
|
||||
]);
|
||||
});
|
||||
|
||||
Deno.test("Lexer - Relation >", () => {
|
||||
expect(testLexer("Foo > Bar")).toEqual([
|
||||
{ token: Token.Word, value: "Foo" },
|
||||
{ token: Token.Op, value: ">" },
|
||||
{ token: Token.Word, value: "Bar" },
|
||||
]);
|
||||
expect(testLexer("Foo>Bar")).toEqual([
|
||||
{ token: Token.Word, value: "Foo" },
|
||||
{ token: Token.Op, value: ">" },
|
||||
{ token: Token.Word, value: "Bar" },
|
||||
]);
|
||||
expect(testLexer(">Bar")).toEqual([
|
||||
{ token: Token.Op, value: ">" },
|
||||
{ token: Token.Word, value: "Bar" },
|
||||
]);
|
||||
});
|
||||
|
||||
Deno.test("Lexer - Relation +", () => {
|
||||
expect(testLexer("Foo + Bar")).toEqual([
|
||||
{ token: Token.Word, value: "Foo" },
|
||||
{ token: Token.Op, value: "+" },
|
||||
{ token: Token.Word, value: "Bar" },
|
||||
]);
|
||||
expect(testLexer("Foo+Bar")).toEqual([
|
||||
{ token: Token.Word, value: "Foo" },
|
||||
{ token: Token.Op, value: "+" },
|
||||
{ token: Token.Word, value: "Bar" },
|
||||
]);
|
||||
expect(testLexer("+Bar")).toEqual([
|
||||
{ token: Token.Op, value: "+" },
|
||||
{ token: Token.Word, value: "Bar" },
|
||||
]);
|
||||
});
|
||||
|
||||
Deno.test("Lexer - Relation ~", () => {
|
||||
expect(testLexer("Foo ~ Bar")).toEqual([
|
||||
{ token: Token.Word, value: "Foo" },
|
||||
{ token: Token.Op, value: "~" },
|
||||
{ token: Token.Word, value: "Bar" },
|
||||
]);
|
||||
expect(testLexer("Foo~Bar")).toEqual([
|
||||
{ token: Token.Word, value: "Foo" },
|
||||
{ token: Token.Op, value: "~" },
|
||||
{ token: Token.Word, value: "Bar" },
|
||||
]);
|
||||
expect(testLexer("~Bar")).toEqual([
|
||||
{ token: Token.Op, value: "~" },
|
||||
{ token: Token.Word, value: "Bar" },
|
||||
]);
|
||||
});
|
||||
|
||||
Deno.test("Lexer - Attr", () => {
|
||||
expect(testLexer("[attr]")).toEqual([
|
||||
{ token: Token.BracketOpen, value: "" },
|
||||
{ token: Token.Word, value: "attr" },
|
||||
{ token: Token.BracketClose, value: "" },
|
||||
]);
|
||||
expect(testLexer("[attr=1]")).toEqual([
|
||||
{ token: Token.BracketOpen, value: "" },
|
||||
{ token: Token.Word, value: "attr" },
|
||||
{ token: Token.Op, value: "=" },
|
||||
{ token: Token.Word, value: "1" },
|
||||
{ token: Token.BracketClose, value: "" },
|
||||
]);
|
||||
expect(testLexer("[attr='foo']")).toEqual([
|
||||
{ token: Token.BracketOpen, value: "" },
|
||||
{ token: Token.Word, value: "attr" },
|
||||
{ token: Token.Op, value: "=" },
|
||||
{ token: Token.String, value: "foo" },
|
||||
{ token: Token.BracketClose, value: "" },
|
||||
]);
|
||||
expect(testLexer("[attr>=2]")).toEqual([
|
||||
{ token: Token.BracketOpen, value: "" },
|
||||
{ token: Token.Word, value: "attr" },
|
||||
{ token: Token.Op, value: ">=" },
|
||||
{ token: Token.Word, value: "2" },
|
||||
{ token: Token.BracketClose, value: "" },
|
||||
]);
|
||||
expect(testLexer("[attr<=2]")).toEqual([
|
||||
{ token: Token.BracketOpen, value: "" },
|
||||
{ token: Token.Word, value: "attr" },
|
||||
{ token: Token.Op, value: "<=" },
|
||||
{ token: Token.Word, value: "2" },
|
||||
{ token: Token.BracketClose, value: "" },
|
||||
]);
|
||||
expect(testLexer("[attr>2]")).toEqual([
|
||||
{ token: Token.BracketOpen, value: "" },
|
||||
{ token: Token.Word, value: "attr" },
|
||||
{ token: Token.Op, value: ">" },
|
||||
{ token: Token.Word, value: "2" },
|
||||
{ token: Token.BracketClose, value: "" },
|
||||
]);
|
||||
expect(testLexer("[attr<2]")).toEqual([
|
||||
{ token: Token.BracketOpen, value: "" },
|
||||
{ token: Token.Word, value: "attr" },
|
||||
{ token: Token.Op, value: "<" },
|
||||
{ token: Token.Word, value: "2" },
|
||||
{ token: Token.BracketClose, value: "" },
|
||||
]);
|
||||
expect(testLexer("[attr!=2]")).toEqual([
|
||||
{ token: Token.BracketOpen, value: "" },
|
||||
{ token: Token.Word, value: "attr" },
|
||||
{ token: Token.Op, value: "!=" },
|
||||
{ token: Token.Word, value: "2" },
|
||||
{ token: Token.BracketClose, value: "" },
|
||||
]);
|
||||
expect(testLexer("[attr.foo=1]")).toEqual([
|
||||
{ token: Token.BracketOpen, value: "" },
|
||||
{ token: Token.Word, value: "attr" },
|
||||
{ token: Token.Dot, value: "" },
|
||||
{ token: Token.Word, value: "foo" },
|
||||
{ token: Token.Op, value: "=" },
|
||||
{ token: Token.Word, value: "1" },
|
||||
{ token: Token.BracketClose, value: "" },
|
||||
]);
|
||||
expect(testLexer("[attr] [attr]")).toEqual([
|
||||
{ token: Token.BracketOpen, value: "" },
|
||||
{ token: Token.Word, value: "attr" },
|
||||
{ token: Token.BracketClose, value: "" },
|
||||
{ token: Token.Space, value: "" },
|
||||
{ token: Token.BracketOpen, value: "" },
|
||||
{ token: Token.Word, value: "attr" },
|
||||
{ token: Token.BracketClose, value: "" },
|
||||
]);
|
||||
expect(testLexer("Foo[attr][attr2=1]")).toEqual([
|
||||
{ token: Token.Word, value: "Foo" },
|
||||
{ token: Token.BracketOpen, value: "" },
|
||||
{ token: Token.Word, value: "attr" },
|
||||
{ token: Token.BracketClose, value: "" },
|
||||
{ token: Token.BracketOpen, value: "" },
|
||||
{ token: Token.Word, value: "attr2" },
|
||||
{ token: Token.Op, value: "=" },
|
||||
{ token: Token.Word, value: "1" },
|
||||
{ token: Token.BracketClose, value: "" },
|
||||
]);
|
||||
});
|
||||
|
||||
Deno.test("Lexer - Pseudo", () => {
|
||||
expect(testLexer(":foo-bar")).toEqual([
|
||||
{ token: Token.Colon, value: "" },
|
||||
{ token: Token.Word, value: "foo-bar" },
|
||||
]);
|
||||
expect(testLexer("Foo:foo-bar")).toEqual([
|
||||
{ token: Token.Word, value: "Foo" },
|
||||
{ token: Token.Colon, value: "" },
|
||||
{ token: Token.Word, value: "foo-bar" },
|
||||
]);
|
||||
expect(testLexer(":foo-bar(baz)")).toEqual([
|
||||
{ token: Token.Colon, value: "" },
|
||||
{ token: Token.Word, value: "foo-bar" },
|
||||
{ token: Token.BraceOpen, value: "" },
|
||||
{ token: Token.Word, value: "baz" },
|
||||
{ token: Token.BraceClose, value: "" },
|
||||
]);
|
||||
expect(testLexer(":foo-bar(2n + 1)")).toEqual([
|
||||
{ token: Token.Colon, value: "" },
|
||||
{ token: Token.Word, value: "foo-bar" },
|
||||
{ token: Token.BraceOpen, value: "" },
|
||||
{ token: Token.Word, value: "2n" },
|
||||
{ token: Token.Op, value: "+" },
|
||||
{ token: Token.Word, value: "1" },
|
||||
{ token: Token.BraceClose, value: "" },
|
||||
]);
|
||||
});
|
||||
|
||||
Deno.test("Parser", () => {
|
||||
expect(parseSelector("Foo", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 1,
|
||||
wildcard: false,
|
||||
},
|
||||
]]);
|
||||
expect(parseSelector("Foo Bar", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 1,
|
||||
wildcard: false,
|
||||
},
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 2,
|
||||
wildcard: false,
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
Deno.test("Parser - Relation", () => {
|
||||
expect(parseSelector("Foo > Bar", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 1,
|
||||
wildcard: false,
|
||||
},
|
||||
{
|
||||
type: RELATION_NODE,
|
||||
op: 3,
|
||||
},
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 2,
|
||||
wildcard: false,
|
||||
},
|
||||
]]);
|
||||
|
||||
expect(parseSelector("Foo ~ Bar", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 1,
|
||||
wildcard: false,
|
||||
},
|
||||
{
|
||||
type: RELATION_NODE,
|
||||
op: 7,
|
||||
},
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 2,
|
||||
wildcard: false,
|
||||
},
|
||||
]]);
|
||||
|
||||
expect(parseSelector("Foo + Bar", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 1,
|
||||
wildcard: false,
|
||||
},
|
||||
{
|
||||
type: RELATION_NODE,
|
||||
op: 8,
|
||||
},
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 2,
|
||||
wildcard: false,
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
Deno.test("Parser - Attr", () => {
|
||||
expect(parseSelector("[foo]", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ATTR_EXISTS_NODE,
|
||||
prop: [1],
|
||||
},
|
||||
]]);
|
||||
|
||||
expect(parseSelector("[foo][bar]", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ATTR_EXISTS_NODE,
|
||||
prop: [1],
|
||||
},
|
||||
{
|
||||
type: ATTR_EXISTS_NODE,
|
||||
prop: [2],
|
||||
},
|
||||
]]);
|
||||
|
||||
expect(parseSelector("[foo=1]", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: 1,
|
||||
prop: [1],
|
||||
value: 1,
|
||||
},
|
||||
]]);
|
||||
expect(parseSelector("[foo=true]", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: 1,
|
||||
prop: [1],
|
||||
value: true,
|
||||
},
|
||||
]]);
|
||||
expect(parseSelector("[foo=false]", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: 1,
|
||||
prop: [1],
|
||||
value: false,
|
||||
},
|
||||
]]);
|
||||
expect(parseSelector("[foo=null]", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: 1,
|
||||
prop: [1],
|
||||
value: null,
|
||||
},
|
||||
]]);
|
||||
expect(parseSelector("[foo='str']", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: 1,
|
||||
prop: [1],
|
||||
value: "str",
|
||||
},
|
||||
]]);
|
||||
expect(parseSelector('[foo="str"]', toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: 1,
|
||||
prop: [1],
|
||||
value: "str",
|
||||
},
|
||||
]]);
|
||||
expect(parseSelector("[foo=/str/]", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: 1,
|
||||
prop: [1],
|
||||
value: /str/,
|
||||
},
|
||||
]]);
|
||||
expect(parseSelector("[foo=/str/g]", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: 1,
|
||||
prop: [1],
|
||||
value: /str/g,
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
Deno.test("Parser - Attr nested", () => {
|
||||
expect(parseSelector("[foo.bar]", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ATTR_EXISTS_NODE,
|
||||
prop: [1, 2],
|
||||
},
|
||||
]]);
|
||||
|
||||
expect(parseSelector("[foo.bar = 2]", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: 1,
|
||||
prop: [1, 2],
|
||||
value: 2,
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
Deno.test("Parser - Pseudo no value", () => {
|
||||
expect(parseSelector(":first-child", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: PSEUDO_FIRST_CHILD,
|
||||
},
|
||||
]]);
|
||||
expect(parseSelector(":last-child", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: PSEUDO_LAST_CHILD,
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
Deno.test.only("Parser - Pseudo nth-child", () => {
|
||||
expect(parseSelector(":nth-child(2)", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: PSEUDO_NTH_CHILD,
|
||||
of: null,
|
||||
backwards: false,
|
||||
step: 2,
|
||||
stepOffset: 0,
|
||||
repeat: false,
|
||||
},
|
||||
]]);
|
||||
expect(parseSelector(":nth-child(2n)", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: PSEUDO_NTH_CHILD,
|
||||
of: null,
|
||||
backwards: false,
|
||||
step: 2,
|
||||
stepOffset: 0,
|
||||
repeat: true,
|
||||
},
|
||||
]]);
|
||||
expect(parseSelector(":nth-child(-2n)", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: PSEUDO_NTH_CHILD,
|
||||
of: null,
|
||||
backwards: true,
|
||||
step: 2,
|
||||
stepOffset: 0,
|
||||
repeat: true,
|
||||
},
|
||||
]]);
|
||||
expect(parseSelector(":nth-child(2n + 1)", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: PSEUDO_NTH_CHILD,
|
||||
of: null,
|
||||
backwards: false,
|
||||
step: 2,
|
||||
stepOffset: 1,
|
||||
repeat: true,
|
||||
},
|
||||
]]);
|
||||
expect(parseSelector(":nth-child(2n + 1 of Foo[attr])", toTag, toAttr))
|
||||
.toEqual([[
|
||||
{
|
||||
type: PSEUDO_NTH_CHILD,
|
||||
of: [],
|
||||
backwards: false,
|
||||
step: 2,
|
||||
stepOffset: 1,
|
||||
repeat: true,
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
Deno.test("Parser - Pseudo has/is/where", () => {
|
||||
expect(parseSelector(":has(Foo:has(Foo), Bar)", toTag, toAttr)).toEqual([[
|
||||
{
|
||||
type: PSEUDO_HAS,
|
||||
op: 1,
|
||||
selectors: [{}],
|
||||
},
|
||||
]]);
|
||||
});
|
|
@ -1,6 +1,5 @@
|
|||
import { compileSelector, parseSelector } from "./40_lint_selector.js";
|
||||
import { expect } from "@std/expect";
|
||||
import { MatchCtx, MatcherFn, Selector } from "./internal.d.ts";
|
||||
|
||||
/**
|
||||
* TS eslint selector Examples
|
||||
|
@ -277,7 +276,7 @@ function testSelector(
|
|||
return visit(ctx, sel, 0);
|
||||
}
|
||||
|
||||
Deno.test("select descendant: A B", () => {
|
||||
Deno.test.only("select descendant: A B", () => {
|
||||
const ast: TestNode = {
|
||||
type: "Foo",
|
||||
children: [{ type: "Bar" }, { type: "Baz" }],
|
63
cli/js/internal.d.ts → cli/js/40_lint_types.d.ts
vendored
63
cli/js/internal.d.ts → cli/js/40_lint_types.d.ts
vendored
|
@ -1,3 +1,5 @@
|
|||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
export interface AstContext {
|
||||
buf: Uint8Array;
|
||||
strTable: Map<number, string>;
|
||||
|
@ -11,84 +13,60 @@ export interface LintState {
|
|||
installedPlugins: Set<string>;
|
||||
}
|
||||
|
||||
export enum AttrOp {
|
||||
/** [attr="value"] or [attr=value] */
|
||||
Equal,
|
||||
/** [attr!="value"] or [attr!=value] */
|
||||
NotEqual,
|
||||
/** [attr>1] */
|
||||
Greater,
|
||||
/** [attr>=1] */
|
||||
GreaterThan,
|
||||
/** [attr<1] */
|
||||
Less,
|
||||
/** [attr<=1] */
|
||||
LessThan,
|
||||
}
|
||||
|
||||
export interface AttrExists {
|
||||
type: 3;
|
||||
prop: number;
|
||||
debug?: string;
|
||||
prop: number[];
|
||||
}
|
||||
|
||||
export interface AttrBin {
|
||||
type: 4;
|
||||
prop: number;
|
||||
op: AttrOp;
|
||||
prop: number[];
|
||||
op: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface AttrRegex {
|
||||
type: 5;
|
||||
prop: number;
|
||||
value: RegExp;
|
||||
}
|
||||
export type AttrSelector = AttrExists | AttrBin;
|
||||
|
||||
export type AttrSelector = AttrExists | AttrBin | AttrRegex;
|
||||
|
||||
export interface Elem {
|
||||
export interface ElemSelector {
|
||||
type: 1;
|
||||
wildcard: boolean;
|
||||
elem: number;
|
||||
debug?: string;
|
||||
}
|
||||
|
||||
export interface PseudoNthChild {
|
||||
type: 6;
|
||||
backward: boolean;
|
||||
type: 5;
|
||||
backwards: boolean;
|
||||
step: number;
|
||||
stepOffset: number;
|
||||
of: Selector | null;
|
||||
repeat: boolean;
|
||||
}
|
||||
|
||||
export interface PseudoHas {
|
||||
type: 7;
|
||||
type: 6;
|
||||
selector: Selector[];
|
||||
}
|
||||
export interface PseudoNot {
|
||||
type: 8;
|
||||
type: 7;
|
||||
selector: Selector[];
|
||||
}
|
||||
export interface PseudoFirstChild {
|
||||
type: 9;
|
||||
type: 8;
|
||||
}
|
||||
export interface PseudoLastChild {
|
||||
type: 10;
|
||||
type: 9;
|
||||
}
|
||||
|
||||
export interface Relation {
|
||||
type: 2;
|
||||
op: number;
|
||||
debug?: string;
|
||||
}
|
||||
|
||||
export type Selector = Array<
|
||||
| Elem
|
||||
| ElemSelector
|
||||
| Relation
|
||||
| AttrExists
|
||||
| AttrBin
|
||||
| AttrRegex
|
||||
| PseudoNthChild
|
||||
| PseudoNot
|
||||
| PseudoHas
|
||||
|
@ -101,10 +79,10 @@ export interface SelectorParseCtx {
|
|||
current: Selector;
|
||||
}
|
||||
|
||||
export enum SelToken {
|
||||
Value = 0,
|
||||
Char = 1,
|
||||
EOF = 2,
|
||||
export const enum SelToken {
|
||||
Value,
|
||||
Char,
|
||||
EOF,
|
||||
}
|
||||
|
||||
export interface ILexer {
|
||||
|
@ -124,3 +102,6 @@ export interface MatchCtx {
|
|||
|
||||
export type NextFn = (ctx: MatchCtx, id: number) => boolean;
|
||||
export type MatcherFn = (ctx: MatchCtx, id: number) => boolean;
|
||||
export type Transformer = (value: string) => number;
|
||||
|
||||
export {};
|
|
@ -115,6 +115,18 @@ impl PluginRunner {
|
|||
|
||||
log::debug!("before loaded");
|
||||
|
||||
match runtime.lazy_load_es_module_with_code(
|
||||
"ext:cli/lint/selector.js",
|
||||
deno_core::ascii_str_include!(concat!(
|
||||
"../../js/40_lint_selector.js"
|
||||
)),
|
||||
) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
eprintln!("after load error {:#?}", err);
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
let obj_result = runtime.lazy_load_es_module_with_code(
|
||||
"ext:cli/lint.js",
|
||||
deno_core::ascii_str_include!(concat!("../../js/40_lint.js")),
|
||||
|
|
|
@ -250,6 +250,7 @@
|
|||
"ext:deno_node/_util/std_fmt_colors.ts": "../ext/node/polyfills/_util/std_fmt_colors.ts",
|
||||
"ext:deno_telemetry/telemetry.ts": "../ext/deno_telemetry/telemetry.ts",
|
||||
"ext:deno_telemetry/util.ts": "../ext/deno_telemetry/util.ts",
|
||||
"ext:cli/lint/selector.js": "../cli/js/40_lint_selector.ts",
|
||||
"@std/archive": "../tests/util/std/archive/mod.ts",
|
||||
"@std/archive/tar": "../tests/util/std/archive/tar.ts",
|
||||
"@std/archive/untar": "../tests/util/std/archive/untar.ts",
|
||||
|
|
Loading…
Add table
Reference in a new issue