mirror of
https://github.com/denoland/deno.git
synced 2025-01-21 13:00:36 -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
|
// @ts-check
|
||||||
|
|
||||||
import { core } from "ext:core/mod.js";
|
import { core } from "ext:core/mod.js";
|
||||||
|
import { compileSelector, parseSelector } from "ext:cli/lint/selector.js";
|
||||||
|
|
||||||
|
console.log({ compileSelector, foo: "foo" });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
op_lint_get_rule,
|
op_lint_get_rule,
|
||||||
op_lint_get_source,
|
op_lint_get_source,
|
||||||
|
|
|
@ -1,66 +1,75 @@
|
||||||
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
/** @typedef {import("./internal.d.ts").LintState} LintState */
|
/** @typedef {import("./40_lint_types.d.ts").LintState} LintState */
|
||||||
/** @typedef {import("./internal.d.ts").AstContext} AstContext */
|
/** @typedef {import("./40_lint_types.d.ts").AstContext} AstContext */
|
||||||
/** @typedef {import("./internal.d.ts").MatchCtx} MatchCtx */
|
/** @typedef {import("./40_lint_types.d.ts").MatchCtx} MatchCtx */
|
||||||
/** @typedef {import("./internal.d.ts").AttrOp} AttrOp */
|
/** @typedef {import("./40_lint_types.d.ts").AttrExists} AttrExists */
|
||||||
/** @typedef {import("./internal.d.ts").AttrExists} AttrExists */
|
/** @typedef {import("./40_lint_types.d.ts").AttrBin} AttrBin */
|
||||||
/** @typedef {import("./internal.d.ts").AttrBin} AttrBin */
|
/** @typedef {import("./40_lint_types.d.ts").AttrSelector} AttrSelector */
|
||||||
/** @typedef {import("./internal.d.ts").AttrSelector} AttrSelector */
|
/** @typedef {import("./40_lint_types.d.ts").ElemSelector} ElemSelector */
|
||||||
/** @typedef {import("./internal.d.ts").Elem} SelectorPart */
|
/** @typedef {import("./40_lint_types.d.ts").PseudoNthChild} PseudoNthChild */
|
||||||
/** @typedef {import("./internal.d.ts").PseudoNthChild} PseudoNthChild */
|
/** @typedef {import("./40_lint_types.d.ts").PseudoHas} PseudoHas */
|
||||||
/** @typedef {import("./internal.d.ts").PseudoHas} PseudoHas */
|
/** @typedef {import("./40_lint_types.d.ts").PseudoNot} PseudoNot */
|
||||||
/** @typedef {import("./internal.d.ts").PseudoNot} PseudoNot */
|
/** @typedef {import("./40_lint_types.d.ts").Relation} SRelation */
|
||||||
/** @typedef {import("./internal.d.ts").Relation} SRelation */
|
/** @typedef {import("./40_lint_types.d.ts").Selector} Selector */
|
||||||
/** @typedef {import("./internal.d.ts").Selector} Selector */
|
/** @typedef {import("./40_lint_types.d.ts").SelectorParseCtx} SelectorParseCtx */
|
||||||
/** @typedef {import("./internal.d.ts").SelectorParseCtx} SelectorParseCtx */
|
/** @typedef {import("./40_lint_types.d.ts").NextFn} NextFn */
|
||||||
/** @typedef {import("./internal.d.ts").ILexer} ILexer */
|
/** @typedef {import("./40_lint_types.d.ts").MatcherFn} MatcherFn */
|
||||||
/** @typedef {import("./internal.d.ts").NextFn} NextFn */
|
/** @typedef {import("./40_lint_types.d.ts").Transformer} Transformer */
|
||||||
/** @typedef {import("./internal.d.ts").MatcherFn} MatcherFn */
|
|
||||||
/** @typedef {import("./internal.d.ts").AttrRegex} AttrRegex */
|
|
||||||
|
|
||||||
const Char = {
|
const Char = {
|
||||||
/** */
|
Tab: 9,
|
||||||
Space: 32,
|
Space: 32,
|
||||||
/** ' */
|
|
||||||
Bang: 33,
|
Bang: 33,
|
||||||
/** " */
|
|
||||||
DoubleQuote: 34,
|
DoubleQuote: 34,
|
||||||
/** ' */
|
|
||||||
Quote: 39,
|
Quote: 39,
|
||||||
/** ( */
|
|
||||||
BraceOpen: 40,
|
BraceOpen: 40,
|
||||||
/** ) */
|
|
||||||
BraceClose: 41,
|
BraceClose: 41,
|
||||||
/** + */
|
|
||||||
Plus: 43,
|
Plus: 43,
|
||||||
/** , */
|
|
||||||
Comma: 44,
|
Comma: 44,
|
||||||
/** : */
|
Minus: 45,
|
||||||
|
Dot: 46,
|
||||||
|
Slash: 47,
|
||||||
|
n0: 49,
|
||||||
|
n9: 57,
|
||||||
Colon: 58,
|
Colon: 58,
|
||||||
/** < */
|
|
||||||
Less: 60,
|
Less: 60,
|
||||||
/** = */
|
|
||||||
Equal: 61,
|
Equal: 61,
|
||||||
/** > */
|
|
||||||
Greater: 62,
|
Greater: 62,
|
||||||
/** [ */
|
A: 65,
|
||||||
|
Z: 90,
|
||||||
BracketOpen: 91,
|
BracketOpen: 91,
|
||||||
/** ] */
|
BackSlash: 92,
|
||||||
BracketClose: 93,
|
BracketClose: 93,
|
||||||
/** ~ */
|
Underscore: 95,
|
||||||
|
a: 97,
|
||||||
|
z: 122,
|
||||||
Tilde: 126,
|
Tilde: 126,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Token = {
|
export const Token = {
|
||||||
Value: 0,
|
EOF: 0,
|
||||||
Char: 1,
|
Word: 1,
|
||||||
Attr: 2,
|
Space: 2,
|
||||||
Pseudo: 3,
|
Op: 3,
|
||||||
EOF: 4,
|
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] */
|
/** [attr="value"] or [attr=value] */
|
||||||
Equal: 1,
|
Equal: 1,
|
||||||
/** [attr!="value"] or [attr!=value] */
|
/** [attr!="value"] or [attr!=value] */
|
||||||
|
@ -73,6 +82,8 @@ const AttrOp = {
|
||||||
Less: 5,
|
Less: 5,
|
||||||
/** [attr<=1] */
|
/** [attr<=1] */
|
||||||
LessThan: 6,
|
LessThan: 6,
|
||||||
|
Tilde: 7,
|
||||||
|
Plus: 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,242 +93,241 @@ const AttrOp = {
|
||||||
function getAttrOp(s) {
|
function getAttrOp(s) {
|
||||||
switch (s) {
|
switch (s) {
|
||||||
case "=":
|
case "=":
|
||||||
return AttrOp.Equal;
|
return BinOp.Equal;
|
||||||
case "!=":
|
case "!=":
|
||||||
return AttrOp.NotEqual;
|
return BinOp.NotEqual;
|
||||||
case ">":
|
case ">":
|
||||||
return AttrOp.Greater;
|
return BinOp.Greater;
|
||||||
case ">=":
|
case ">=":
|
||||||
return AttrOp.GreaterThan;
|
return BinOp.GreaterThan;
|
||||||
case "<":
|
case "<":
|
||||||
return AttrOp.Less;
|
return BinOp.Less;
|
||||||
case "<=":
|
case "<=":
|
||||||
return AttrOp.LessThan;
|
return BinOp.LessThan;
|
||||||
|
case "~":
|
||||||
|
return BinOp.Tilde;
|
||||||
|
case "+":
|
||||||
|
return BinOp.Plus;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown attribute operator: '${s}'`);
|
throw new Error(`Unknown attribute operator: '${s}'`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @implements ILexer */
|
export class Lexer {
|
||||||
class Lexer {
|
token = Token.Word;
|
||||||
token = Token.Value;
|
|
||||||
start = 0;
|
start = 0;
|
||||||
end = 0;
|
end = 0;
|
||||||
ch = 0;
|
ch = 0;
|
||||||
i = 0;
|
i = -1;
|
||||||
|
|
||||||
value = "";
|
value = "";
|
||||||
value2 = "";
|
|
||||||
op = 0;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} input
|
* @param {string} input
|
||||||
*/
|
*/
|
||||||
constructor(input) {
|
constructor(input) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
|
this.step();
|
||||||
|
this.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} char
|
* @param {number} token
|
||||||
*/
|
*/
|
||||||
expect(char) {
|
expect(token) {
|
||||||
if (this.i >= this.input.length) {
|
if (this.token !== token) {
|
||||||
|
console.log(this, JSON.stringify(String.fromCharCode(this.ch)));
|
||||||
throw new Error(
|
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() {
|
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() {
|
next() {
|
||||||
this.value = "";
|
this.value = "";
|
||||||
this.value2 = "";
|
|
||||||
this.op = 0;
|
|
||||||
|
|
||||||
if (this.i >= this.input.length) {
|
if (this.i >= this.input.length) {
|
||||||
this.token = Token.EOF;
|
this.token = Token.EOF;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ch = this.input.charCodeAt(this.i);
|
console.log(
|
||||||
console.log("NEXT", JSON.stringify(String.fromCharCode(ch)));
|
"NEXT",
|
||||||
switch (ch) {
|
this.input,
|
||||||
case Char.Space:
|
this.i,
|
||||||
while (ch === Char.Space) {
|
JSON.stringify(String.fromCharCode(this.ch)),
|
||||||
ch = this.getChar();
|
);
|
||||||
this.i++;
|
if (this.i > 30) throw new Error("infininte #2");
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this a sibling/descendant selector
|
let j = 0;
|
||||||
if (ch === Char.Plus || ch === Char.Tilde || ch === Char.Greater) {
|
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.start = this.i;
|
||||||
|
this.step();
|
||||||
|
|
||||||
|
while (this.isOpContinue()) {
|
||||||
|
this.step();
|
||||||
|
}
|
||||||
|
|
||||||
this.end = this.i;
|
this.end = this.i;
|
||||||
this.ch = ch;
|
this.value = this.getSlice();
|
||||||
this.token = Token.Char;
|
|
||||||
|
|
||||||
console.log("--> yeah");
|
// Consume remaining space
|
||||||
this.i++;
|
while (this.isWhiteSpace()) {
|
||||||
ch = this.getChar();
|
this.step();
|
||||||
|
|
||||||
while (ch === Char.Space) {
|
|
||||||
ch = this.getChar();
|
|
||||||
this.i++;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Char.Quote:
|
||||||
|
case Char.DoubleQuote: {
|
||||||
|
this.token = Token.String;
|
||||||
|
const ch = this.ch;
|
||||||
|
|
||||||
|
this.step();
|
||||||
this.start = this.i;
|
this.start = this.i;
|
||||||
|
|
||||||
|
while (this.ch > 0 && this.ch !== ch) {
|
||||||
|
this.step();
|
||||||
|
}
|
||||||
|
|
||||||
this.end = this.i;
|
this.end = this.i;
|
||||||
this.ch = Char.Space;
|
this.value = this.getSlice();
|
||||||
this.token = Token.Char;
|
this.step();
|
||||||
this.i--;
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
default:
|
||||||
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) {
|
|
||||||
this.start = this.i;
|
this.start = this.i;
|
||||||
while (
|
this.step();
|
||||||
ch === Char.Equal || ch === Char.Greater || ch === Char.Less ||
|
|
||||||
ch === Char.Bang
|
while (this.isWordContinue()) {
|
||||||
) {
|
this.step();
|
||||||
this.i++;
|
|
||||||
ch = this.getChar();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.op = getAttrOp(this.getSlice());
|
this.end = this.i;
|
||||||
|
this.value = this.getSlice();
|
||||||
this.start = this.i;
|
this.token = Token.Word;
|
||||||
|
return;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
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+$/;
|
const NUMBER_REG = /^(\d+\.)?\d+$/;
|
||||||
|
@ -345,10 +355,10 @@ function getFromRawValue(raw) {
|
||||||
if (raw.length === 2) return "";
|
if (raw.length === 2) return "";
|
||||||
return raw.slice(1, -1);
|
return raw.slice(1, -1);
|
||||||
} else if (raw.startsWith("/")) {
|
} else if (raw.startsWith("/")) {
|
||||||
const end = raw.lastIndexOf("/", 1);
|
const end = raw.lastIndexOf("/");
|
||||||
if (end === -1) throw new Error(`Invalid RegExp pattern: ${raw}`);
|
if (end === -1) throw new Error(`Invalid RegExp pattern: ${raw}`);
|
||||||
const pattern = raw.slice(0, end);
|
const pattern = raw.slice(1, end);
|
||||||
const flags = end < raw.length - 1 ? raw.slice(end) : undefined;
|
const flags = end < raw.length - 1 ? raw.slice(end + 1) : undefined;
|
||||||
return new RegExp(pattern, flags);
|
return new RegExp(pattern, flags);
|
||||||
} else if (NUMBER_REG.test(raw)) {
|
} else if (NUMBER_REG.test(raw)) {
|
||||||
return Number(raw);
|
return Number(raw);
|
||||||
|
@ -360,24 +370,23 @@ function getFromRawValue(raw) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ELEM_NODE = 1;
|
export const ELEM_NODE = 1;
|
||||||
const RELATION_NODE = 2;
|
export const RELATION_NODE = 2;
|
||||||
const ATTR_EXISTS_NODE = 3;
|
export const ATTR_EXISTS_NODE = 3;
|
||||||
const ATTR_BIN_NODE = 4;
|
export const ATTR_BIN_NODE = 4;
|
||||||
const ATTR_REGEX_NODE = 5;
|
export const PSEUDO_NTH_CHILD = 5;
|
||||||
const PSEUDO_NODE_NTH_CHILD = 6;
|
export const PSEUDO_HAS = 6;
|
||||||
const PSEUDO_NODE_HAS = 7;
|
export const PSEUDO_NODE_NOT = 7;
|
||||||
const PSEUDO_NODE_NOT = 8;
|
export const PSEUDO_FIRST_CHILD = 8;
|
||||||
const PSEUDO_FIRST_CHILD = 9;
|
export const PSEUDO_LAST_CHILD = 9;
|
||||||
const PSEUDO_LAST_CHILD = 10;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} input
|
* @param {string} input
|
||||||
* @param {Record<string, number>} astNodes
|
* @param {Transformer} toElem
|
||||||
* @param {Record<string, number>} astAttrs
|
* @param {Transformer} toAttr
|
||||||
* @returns {Selector[]}
|
* @returns {Selector[]}
|
||||||
*/
|
*/
|
||||||
export function parseSelector(input, astNodes, astAttrs) {
|
export function parseSelector(input, toElem, toAttr) {
|
||||||
/** @type {Selector[]} */
|
/** @type {Selector[]} */
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
|
@ -385,95 +394,155 @@ export function parseSelector(input, astNodes, astAttrs) {
|
||||||
let current = [];
|
let current = [];
|
||||||
|
|
||||||
const lex = new Lexer(input);
|
const lex = new Lexer(input);
|
||||||
lex.next();
|
|
||||||
|
|
||||||
while (lex.token !== Token.EOF) {
|
while (lex.token !== Token.EOF) {
|
||||||
console.log(
|
console.log(
|
||||||
lex.token,
|
lex.token,
|
||||||
JSON.stringify(String.fromCharCode(lex.ch)),
|
|
||||||
Token,
|
|
||||||
result,
|
result,
|
||||||
current,
|
current,
|
||||||
);
|
);
|
||||||
if (lex.token === Token.Value) {
|
if (lex.token === Token.Word) {
|
||||||
const name = lex.value;
|
const value = lex.value;
|
||||||
const wildcard = name === "*";
|
const wildcard = value === "*";
|
||||||
|
|
||||||
let elem = 0;
|
|
||||||
if (!wildcard) {
|
|
||||||
elem = astNodes[name];
|
|
||||||
if (elem === undefined) {
|
|
||||||
throw new Error(`Unkown element: ${name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const elem = !wildcard ? toElem(value) : 0;
|
||||||
current.push({
|
current.push({
|
||||||
type: ELEM_NODE,
|
type: ELEM_NODE,
|
||||||
elem,
|
elem,
|
||||||
debug: name,
|
|
||||||
wildcard,
|
wildcard,
|
||||||
});
|
});
|
||||||
} else if (lex.token === Token.Attr) {
|
lex.next();
|
||||||
const name = lex.value;
|
continue;
|
||||||
const id = astAttrs[name];
|
} else if (lex.token === Token.BracketOpen) {
|
||||||
if (id === undefined) {
|
lex.next();
|
||||||
console.log(lex);
|
lex.expect(Token.Word);
|
||||||
throw new Error(`Unknown attribute: ${name}`);
|
|
||||||
|
// 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 === "") {
|
if (lex.token === Token.Op) {
|
||||||
current.push({
|
const op = getAttrOp(lex.value);
|
||||||
type: ATTR_EXISTS_NODE,
|
lex.readAsWordUntil(Token.BracketClose);
|
||||||
prop: id,
|
|
||||||
debug: lex.value,
|
const value = getFromRawValue(lex.value);
|
||||||
});
|
current.push({ type: ATTR_BIN_NODE, prop, op, value });
|
||||||
} else {
|
} else {
|
||||||
current.push({
|
current.push({
|
||||||
type: ATTR_BIN_NODE,
|
type: ATTR_EXISTS_NODE,
|
||||||
prop: id,
|
prop,
|
||||||
op: lex.op,
|
|
||||||
debug: lex.value,
|
|
||||||
value: lex.value2,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} 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);
|
console.log("PSEUDO", lex);
|
||||||
switch (lex.value) {
|
switch (lex.value) {
|
||||||
case ":first-child":
|
case "first-child":
|
||||||
current.push({
|
current.push({
|
||||||
type: PSEUDO_FIRST_CHILD,
|
type: PSEUDO_FIRST_CHILD,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case ":last-child":
|
case "last-child":
|
||||||
current.push({
|
current.push({
|
||||||
type: PSEUDO_LAST_CHILD,
|
type: PSEUDO_LAST_CHILD,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case ":nth-child":
|
case "nth-child": {
|
||||||
lex.expect(Char.BraceOpen);
|
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("nth", lex);
|
||||||
console.log(lex.getSlice());
|
console.log(lex.getSlice());
|
||||||
|
|
||||||
current.push({
|
current.push({
|
||||||
type: PSEUDO_NODE_NTH_CHILD,
|
type: PSEUDO_NTH_CHILD,
|
||||||
|
backwards,
|
||||||
|
of: null,
|
||||||
|
step,
|
||||||
|
stepOffset,
|
||||||
|
repeat,
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown pseudo selector: '${lex.value}'`);
|
throw new Error(`Unknown pseudo selector: '${lex.value}'`);
|
||||||
}
|
}
|
||||||
} else if (lex.ch === Char.Comma) {
|
} else if (lex.ch === Char.Comma) {
|
||||||
result.push(current);
|
result.push(current);
|
||||||
current = [];
|
current = [];
|
||||||
} else if (
|
} else if (lex.token === Token.Op) {
|
||||||
lex.ch === Char.Space || lex.ch === Char.Plus || lex.ch === Char.Tilde ||
|
|
||||||
lex.ch === Char.Greater
|
|
||||||
) {
|
|
||||||
current.push({
|
current.push({
|
||||||
type: RELATION_NODE,
|
type: RELATION_NODE,
|
||||||
op: lex.ch,
|
op: getAttrOp(lex.value),
|
||||||
debug: JSON.stringify(String.fromCharCode(lex.ch)),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -531,19 +600,16 @@ export function compileSelector(selector) {
|
||||||
case ATTR_BIN_NODE:
|
case ATTR_BIN_NODE:
|
||||||
fn = matchAttrBin(node, fn);
|
fn = matchAttrBin(node, fn);
|
||||||
break;
|
break;
|
||||||
case ATTR_REGEX_NODE:
|
|
||||||
fn = matchAttrRegex(node, fn);
|
|
||||||
break;
|
|
||||||
case PSEUDO_FIRST_CHILD:
|
case PSEUDO_FIRST_CHILD:
|
||||||
fn = matchFirstChild(fn);
|
fn = matchFirstChild(fn);
|
||||||
break;
|
break;
|
||||||
case PSEUDO_LAST_CHILD:
|
case PSEUDO_LAST_CHILD:
|
||||||
fn = matchLastChild(fn);
|
fn = matchLastChild(fn);
|
||||||
break;
|
break;
|
||||||
case PSEUDO_NODE_NTH_CHILD:
|
case PSEUDO_NTH_CHILD:
|
||||||
fn = matchNthChild(node, fn);
|
fn = matchNthChild(node, fn);
|
||||||
break;
|
break;
|
||||||
case PSEUDO_NODE_HAS:
|
case PSEUDO_HAS:
|
||||||
// FIXME
|
// FIXME
|
||||||
// fn = matchIs(part, fn);
|
// fn = matchIs(part, fn);
|
||||||
throw new Error("TODO: :has");
|
throw new Error("TODO: :has");
|
||||||
|
@ -724,7 +790,7 @@ function matchFollowing(next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {SelectorPart} part
|
* @param {ElemSelector} part
|
||||||
* @param {MatcherFn} next
|
* @param {MatcherFn} next
|
||||||
* @returns {MatcherFn}
|
* @returns {MatcherFn}
|
||||||
*/
|
*/
|
||||||
|
@ -770,39 +836,23 @@ function matchAttrBin(attr, next) {
|
||||||
*/
|
*/
|
||||||
function matchAttrValue(attr, value) {
|
function matchAttrValue(attr, value) {
|
||||||
switch (attr.op) {
|
switch (attr.op) {
|
||||||
case AttrOp.Equal:
|
case BinOp.Equal:
|
||||||
return value === attr.value;
|
return value === attr.value;
|
||||||
case AttrOp.NotEqual:
|
case BinOp.NotEqual:
|
||||||
return value !== attr.value;
|
return value !== attr.value;
|
||||||
case AttrOp.Greater:
|
case BinOp.Greater:
|
||||||
return typeof value === "number" && typeof attr.value === "number" &&
|
return typeof value === "number" && typeof attr.value === "number" &&
|
||||||
value > attr.value;
|
value > attr.value;
|
||||||
case AttrOp.GreaterThan:
|
case BinOp.GreaterThan:
|
||||||
return typeof value === "number" && typeof attr.value === "number" &&
|
return typeof value === "number" && typeof attr.value === "number" &&
|
||||||
value >= attr.value;
|
value >= attr.value;
|
||||||
case AttrOp.Less:
|
case BinOp.Less:
|
||||||
return typeof value === "number" && typeof attr.value === "number" &&
|
return typeof value === "number" && typeof attr.value === "number" &&
|
||||||
value < attr.value;
|
value < attr.value;
|
||||||
case AttrOp.LessThan:
|
case BinOp.LessThan:
|
||||||
return typeof value === "number" && typeof attr.value === "number" &&
|
return typeof value === "number" && typeof attr.value === "number" &&
|
||||||
value <= attr.value;
|
value <= attr.value;
|
||||||
default:
|
default:
|
||||||
return false;
|
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 { compileSelector, parseSelector } from "./40_lint_selector.js";
|
||||||
import { expect } from "@std/expect";
|
import { expect } from "@std/expect";
|
||||||
import { MatchCtx, MatcherFn, Selector } from "./internal.d.ts";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TS eslint selector Examples
|
* TS eslint selector Examples
|
||||||
|
@ -277,7 +276,7 @@ function testSelector(
|
||||||
return visit(ctx, sel, 0);
|
return visit(ctx, sel, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Deno.test("select descendant: A B", () => {
|
Deno.test.only("select descendant: A B", () => {
|
||||||
const ast: TestNode = {
|
const ast: TestNode = {
|
||||||
type: "Foo",
|
type: "Foo",
|
||||||
children: [{ type: "Bar" }, { type: "Baz" }],
|
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 {
|
export interface AstContext {
|
||||||
buf: Uint8Array;
|
buf: Uint8Array;
|
||||||
strTable: Map<number, string>;
|
strTable: Map<number, string>;
|
||||||
|
@ -11,84 +13,60 @@ export interface LintState {
|
||||||
installedPlugins: Set<string>;
|
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 {
|
export interface AttrExists {
|
||||||
type: 3;
|
type: 3;
|
||||||
prop: number;
|
prop: number[];
|
||||||
debug?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AttrBin {
|
export interface AttrBin {
|
||||||
type: 4;
|
type: 4;
|
||||||
prop: number;
|
prop: number[];
|
||||||
op: AttrOp;
|
op: number;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AttrRegex {
|
export type AttrSelector = AttrExists | AttrBin;
|
||||||
type: 5;
|
|
||||||
prop: number;
|
|
||||||
value: RegExp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AttrSelector = AttrExists | AttrBin | AttrRegex;
|
export interface ElemSelector {
|
||||||
|
|
||||||
export interface Elem {
|
|
||||||
type: 1;
|
type: 1;
|
||||||
wildcard: boolean;
|
wildcard: boolean;
|
||||||
elem: number;
|
elem: number;
|
||||||
debug?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PseudoNthChild {
|
export interface PseudoNthChild {
|
||||||
type: 6;
|
type: 5;
|
||||||
backward: boolean;
|
backwards: boolean;
|
||||||
step: number;
|
step: number;
|
||||||
stepOffset: number;
|
stepOffset: number;
|
||||||
of: Selector | null;
|
of: Selector | null;
|
||||||
|
repeat: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PseudoHas {
|
export interface PseudoHas {
|
||||||
type: 7;
|
type: 6;
|
||||||
selector: Selector[];
|
selector: Selector[];
|
||||||
}
|
}
|
||||||
export interface PseudoNot {
|
export interface PseudoNot {
|
||||||
type: 8;
|
type: 7;
|
||||||
selector: Selector[];
|
selector: Selector[];
|
||||||
}
|
}
|
||||||
export interface PseudoFirstChild {
|
export interface PseudoFirstChild {
|
||||||
type: 9;
|
type: 8;
|
||||||
}
|
}
|
||||||
export interface PseudoLastChild {
|
export interface PseudoLastChild {
|
||||||
type: 10;
|
type: 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Relation {
|
export interface Relation {
|
||||||
type: 2;
|
type: 2;
|
||||||
op: number;
|
op: number;
|
||||||
debug?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Selector = Array<
|
export type Selector = Array<
|
||||||
| Elem
|
| ElemSelector
|
||||||
| Relation
|
| Relation
|
||||||
| AttrExists
|
| AttrExists
|
||||||
| AttrBin
|
| AttrBin
|
||||||
| AttrRegex
|
|
||||||
| PseudoNthChild
|
| PseudoNthChild
|
||||||
| PseudoNot
|
| PseudoNot
|
||||||
| PseudoHas
|
| PseudoHas
|
||||||
|
@ -101,10 +79,10 @@ export interface SelectorParseCtx {
|
||||||
current: Selector;
|
current: Selector;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SelToken {
|
export const enum SelToken {
|
||||||
Value = 0,
|
Value,
|
||||||
Char = 1,
|
Char,
|
||||||
EOF = 2,
|
EOF,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILexer {
|
export interface ILexer {
|
||||||
|
@ -124,3 +102,6 @@ export interface MatchCtx {
|
||||||
|
|
||||||
export type NextFn = (ctx: MatchCtx, id: number) => boolean;
|
export type NextFn = (ctx: MatchCtx, id: number) => boolean;
|
||||||
export type MatcherFn = (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");
|
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(
|
let obj_result = runtime.lazy_load_es_module_with_code(
|
||||||
"ext:cli/lint.js",
|
"ext:cli/lint.js",
|
||||||
deno_core::ascii_str_include!(concat!("../../js/40_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_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/telemetry.ts": "../ext/deno_telemetry/telemetry.ts",
|
||||||
"ext:deno_telemetry/util.ts": "../ext/deno_telemetry/util.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": "../tests/util/std/archive/mod.ts",
|
||||||
"@std/archive/tar": "../tests/util/std/archive/tar.ts",
|
"@std/archive/tar": "../tests/util/std/archive/tar.ts",
|
||||||
"@std/archive/untar": "../tests/util/std/archive/untar.ts",
|
"@std/archive/untar": "../tests/util/std/archive/untar.ts",
|
||||||
|
|
Loading…
Add table
Reference in a new issue