1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-21 13:00:36 -05:00
This commit is contained in:
Marvin Hagemeister 2024-12-16 16:17:02 +01:00
parent f167277561
commit 86fc6e64cb
7 changed files with 880 additions and 349 deletions

View file

@ -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,

View file

@ -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);
};
}

View 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: [{}],
},
]]);
});

View file

@ -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" }],

View file

@ -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 {};

View file

@ -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")),

View file

@ -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",