mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 17:34:47 -05:00
revert selector changes
This commit is contained in:
parent
6e28ae5425
commit
c831535ac9
6 changed files with 48 additions and 2374 deletions
|
@ -3,11 +3,6 @@
|
|||
// @ts-check
|
||||
|
||||
import { core, internals } from "ext:core/mod.js";
|
||||
import {
|
||||
compileSelector,
|
||||
parseSelector,
|
||||
splitSelectors,
|
||||
} from "ext:cli/40_lint_selector.js";
|
||||
|
||||
const {
|
||||
op_lint_get_rule,
|
||||
|
@ -391,238 +386,6 @@ function getString(strTable, id) {
|
|||
return name;
|
||||
}
|
||||
|
||||
/** @implements {MatchContext} */
|
||||
class MatchCtx {
|
||||
/**
|
||||
* @param {AstContext["buf"]} buf
|
||||
* @param {AstContext["strTable"]} strTable
|
||||
*/
|
||||
constructor(buf, strTable) {
|
||||
this.buf = buf;
|
||||
this.strTable = strTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @returns {number}
|
||||
*/
|
||||
getParent(offset) {
|
||||
return readU32(this.buf, offset + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @returns {number}
|
||||
*/
|
||||
getType(offset) {
|
||||
return this.buf[offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @param {number[]} propIds
|
||||
* @param {number} idx
|
||||
* @returns {unknown}
|
||||
*/
|
||||
getAttrPathValue(offset, propIds, idx) {
|
||||
const { buf } = this;
|
||||
|
||||
offset = findPropOffset(buf, offset, propIds[idx]);
|
||||
if (offset === -1) return undefined;
|
||||
const _prop = buf[offset++];
|
||||
const kind = buf[offset++];
|
||||
|
||||
if (kind === PropFlags.Ref) {
|
||||
const value = readU32(buf, offset);
|
||||
// Checks need to end with a value, not a node
|
||||
if (idx === propIds.length - 1) return undefined;
|
||||
return this.getAttrPathValue(value, propIds, idx + 1);
|
||||
} else if (kind === PropFlags.RefArr) {
|
||||
// FIXME
|
||||
const _count = readU32(buf, offset);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
// Cannot traverse into primitives further
|
||||
if (idx < propIds.length - 1) return undefined;
|
||||
|
||||
if (kind === PropFlags.String) {
|
||||
const s = readU32(buf, offset);
|
||||
return getString(this.strTable, s);
|
||||
} else if (kind === PropFlags.Bool) {
|
||||
return buf[offset] === 1;
|
||||
} else if (kind === PropFlags.Null) {
|
||||
return null;
|
||||
} else if (kind === PropFlags.Undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @param {number[]} propIds
|
||||
* @param {number} idx
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasAttrPath(offset, propIds, idx) {
|
||||
const { buf } = this;
|
||||
|
||||
offset = findPropOffset(buf, offset, propIds[idx]);
|
||||
// console.log("attr path", offset, propIds, idx);
|
||||
if (offset === -1) return false;
|
||||
if (idx === propIds.length - 1) return true;
|
||||
|
||||
const prop = buf[offset++];
|
||||
const kind = buf[offset++];
|
||||
if (kind === PropFlags.Ref) {
|
||||
const value = readU32(buf, offset);
|
||||
return this.hasAttrPath(value, propIds, idx + 1);
|
||||
} else if (kind === PropFlags.RefArr) {
|
||||
const count = readU32(buf, offset);
|
||||
offset += 4;
|
||||
|
||||
// FIXME
|
||||
}
|
||||
|
||||
// Primitives cannot be traversed further. This means we
|
||||
// didn't found the attribute.
|
||||
if (idx < propIds.length - 1) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @returns {number}
|
||||
*/
|
||||
getFirstChild(offset) {
|
||||
const { buf } = this;
|
||||
|
||||
// type + parentId + SpanLo + SpanHi
|
||||
offset += 1 + 4 + 4 + 4;
|
||||
|
||||
const count = buf[offset++];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const _prop = buf[offset++];
|
||||
const kind = buf[offset++];
|
||||
|
||||
switch (kind) {
|
||||
case PropFlags.Ref: {
|
||||
const v = readU32(buf, offset);
|
||||
offset += 4;
|
||||
return v;
|
||||
}
|
||||
case PropFlags.RefArr: {
|
||||
const len = readU32(buf, offset);
|
||||
offset += 4;
|
||||
for (let j = 0; j < len; j++) {
|
||||
const v = readU32(buf, offset);
|
||||
offset += 4;
|
||||
return v;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
case PropFlags.String:
|
||||
offset += 4;
|
||||
break;
|
||||
case PropFlags.Bool:
|
||||
offset++;
|
||||
break;
|
||||
case PropFlags.Null:
|
||||
case PropFlags.Undefined:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @returns {number}
|
||||
*/
|
||||
getLastChild(offset) {
|
||||
const { buf } = this;
|
||||
|
||||
// type + parentId + SpanLo + SpanHi
|
||||
offset += 1 + 4 + 4 + 4;
|
||||
|
||||
let last = -1;
|
||||
|
||||
const count = buf[offset++];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const _prop = buf[offset++];
|
||||
const kind = buf[offset++];
|
||||
|
||||
switch (kind) {
|
||||
case PropFlags.Ref: {
|
||||
const v = readU32(buf, offset);
|
||||
offset += 4;
|
||||
last = v;
|
||||
break;
|
||||
}
|
||||
case PropFlags.RefArr: {
|
||||
const len = readU32(buf, offset);
|
||||
offset += 4;
|
||||
for (let j = 0; j < len; j++) {
|
||||
const v = readU32(buf, offset);
|
||||
last = v;
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PropFlags.String:
|
||||
offset += 4;
|
||||
break;
|
||||
case PropFlags.Bool:
|
||||
offset++;
|
||||
break;
|
||||
case PropFlags.Null:
|
||||
case PropFlags.Undefined:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return last;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @returns {number[]}
|
||||
*/
|
||||
getSiblings(id) {
|
||||
const { buf } = this;
|
||||
|
||||
const result = findChildOffset(buf, id);
|
||||
// Happens for program nodes
|
||||
if (result === null) return [];
|
||||
|
||||
if (result[1] === -1) {
|
||||
return [id];
|
||||
}
|
||||
|
||||
let offset = result[0];
|
||||
const count = readU32(buf, offset);
|
||||
offset += 4;
|
||||
|
||||
/** @type {number[]} */
|
||||
const out = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const v = readU32(buf, offset);
|
||||
offset += 4;
|
||||
out.push(v);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} buf
|
||||
* @param {AstContext} buf
|
||||
|
@ -703,7 +466,6 @@ function createAstContext(buf) {
|
|||
strByType,
|
||||
typeByStr,
|
||||
propByStr,
|
||||
matcher: new MatchCtx(buf, strTable),
|
||||
};
|
||||
|
||||
setNodeGetters(ctx);
|
||||
|
@ -756,37 +518,33 @@ export function runPluginsForFile(fileName, serializedAst) {
|
|||
isExit = true;
|
||||
key = key.slice(0, -":exit".length);
|
||||
}
|
||||
const selectors = splitSelectors(key);
|
||||
|
||||
for (let j = 0; j < selectors.length; j++) {
|
||||
const fnKey = selectors[j] + (isExit ? ":exit" : "");
|
||||
let info = bySelector.get(fnKey);
|
||||
if (info === undefined) {
|
||||
info = { enter: NOOP, exit: NOOP };
|
||||
bySelector.set(fnKey, info);
|
||||
let info = bySelector.get(key);
|
||||
if (info === undefined) {
|
||||
info = { enter: NOOP, exit: NOOP };
|
||||
bySelector.set(key, info);
|
||||
}
|
||||
const prevFn = isExit ? info.exit : info.enter;
|
||||
|
||||
/**
|
||||
* @param {*} node
|
||||
*/
|
||||
const wrapped = (node) => {
|
||||
prevFn(node);
|
||||
|
||||
try {
|
||||
fn(node);
|
||||
} catch (err) {
|
||||
throw new Error(`Visitor "${name}" of plugin "${id}" errored`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
const prevFn = isExit ? info.exit : info.enter;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {*} node
|
||||
*/
|
||||
const wrapped = (node) => {
|
||||
prevFn(node);
|
||||
|
||||
try {
|
||||
fn(node);
|
||||
} catch (err) {
|
||||
throw new Error(`Visitor "${name}" of plugin "${id}" errored`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isExit) {
|
||||
info.exit = wrapped;
|
||||
} else {
|
||||
info.enter = wrapped;
|
||||
}
|
||||
if (isExit) {
|
||||
info.exit = wrapped;
|
||||
} else {
|
||||
info.enter = wrapped;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -803,31 +561,24 @@ export function runPluginsForFile(fileName, serializedAst) {
|
|||
}
|
||||
}
|
||||
|
||||
// Create selectors
|
||||
/** @type {TransformerFn} */
|
||||
const toElem = (str) => {
|
||||
const id = ctx.typeByStr.get(str);
|
||||
if (id === undefined) throw new Error(`Unknown elem: ${str}`);
|
||||
return id;
|
||||
};
|
||||
/** @type {TransformerFn} */
|
||||
const toAttr = (str) => {
|
||||
const id = ctx.propByStr.get(str);
|
||||
if (id === undefined) throw new Error(`Unknown elem: ${str}`);
|
||||
return id;
|
||||
};
|
||||
|
||||
/** @type {CompiledVisitor[]} */
|
||||
const visitors = [];
|
||||
for (const [sel, info] of bySelector.entries()) {
|
||||
// Selectors are already split here.
|
||||
// TODO: Avoid array allocation (not sure if that matters)
|
||||
const parsed = parseSelector(sel, toElem, toAttr)[0];
|
||||
const matcher = compileSelector(parsed);
|
||||
// This will make more sense once selectors land as it's faster
|
||||
// to precompile them once upfront.
|
||||
|
||||
// Convert the visiting element name to a number. This number
|
||||
// is part of the serialized buffer and comparing a single number
|
||||
// is quicker than strings.
|
||||
const elemId = ctx.typeByStr.get(sel) ?? -1;
|
||||
|
||||
visitors.push({
|
||||
info,
|
||||
matcher,
|
||||
// Check if we should call this visitor
|
||||
matcher: (offset) => {
|
||||
const type = ctx.buf[offset];
|
||||
return type === elemId;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -862,18 +613,18 @@ function traverse(ctx, visitors, offset) {
|
|||
for (let i = 0; i < visitors.length; i++) {
|
||||
const v = visitors[i];
|
||||
|
||||
if (v.info.enter === NOOP) {
|
||||
continue;
|
||||
}
|
||||
if (v.matcher(offset)) {
|
||||
if (v.info.exit !== NOOP) {
|
||||
if (exits === null) {
|
||||
exits = [v.info.exit];
|
||||
} else {
|
||||
exits.push(v.info.exit);
|
||||
}
|
||||
}
|
||||
|
||||
if (v.matcher(ctx.matcher, offset)) {
|
||||
const node = /** @type {*} */ (getNode(ctx, offset));
|
||||
v.info.enter(node);
|
||||
|
||||
if (exits === null) {
|
||||
exits = [v.info.exit];
|
||||
} else {
|
||||
exits.push(v.info.exit);
|
||||
if (v.info.enter !== NOOP) {
|
||||
const node = /** @type {*} */ (getNode(ctx, offset));
|
||||
v.info.enter(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,999 +0,0 @@
|
|||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
// @ts-check
|
||||
|
||||
/** @typedef {import("./40_lint_types.d.ts").LintState} LintState */
|
||||
/** @typedef {import("./40_lint_types.d.ts").AstContext} AstContext */
|
||||
/** @typedef {import("./40_lint_types.d.ts").MatchContext} MatchCtx */
|
||||
/** @typedef {import("./40_lint_types.d.ts").AttrExists} AttrExists */
|
||||
/** @typedef {import("./40_lint_types.d.ts").AttrBin} AttrBin */
|
||||
/** @typedef {import("./40_lint_types.d.ts").AttrSelector} AttrSelector */
|
||||
/** @typedef {import("./40_lint_types.d.ts").ElemSelector} ElemSelector */
|
||||
/** @typedef {import("./40_lint_types.d.ts").PseudoNthChild} PseudoNthChild */
|
||||
/** @typedef {import("./40_lint_types.d.ts").PseudoHas} PseudoHas */
|
||||
/** @typedef {import("./40_lint_types.d.ts").PseudoNot} PseudoNot */
|
||||
/** @typedef {import("./40_lint_types.d.ts").Relation} SRelation */
|
||||
/** @typedef {import("./40_lint_types.d.ts").Selector} Selector */
|
||||
/** @typedef {import("./40_lint_types.d.ts").SelectorParseCtx} SelectorParseCtx */
|
||||
/** @typedef {import("./40_lint_types.d.ts").NextFn} NextFn */
|
||||
/** @typedef {import("./40_lint_types.d.ts").MatcherFn} MatcherFn */
|
||||
/** @typedef {import("./40_lint_types.d.ts").TransformFn} Transformer */
|
||||
|
||||
const Char = {
|
||||
Tab: 9,
|
||||
Space: 32,
|
||||
Bang: 33,
|
||||
DoubleQuote: 34,
|
||||
Quote: 39,
|
||||
BraceOpen: 40,
|
||||
BraceClose: 41,
|
||||
Plus: 43,
|
||||
Comma: 44,
|
||||
Minus: 45,
|
||||
Dot: 46,
|
||||
Slash: 47,
|
||||
n0: 49,
|
||||
n9: 57,
|
||||
Colon: 58,
|
||||
Less: 60,
|
||||
Equal: 61,
|
||||
Greater: 62,
|
||||
A: 65,
|
||||
Z: 90,
|
||||
BracketOpen: 91,
|
||||
BackSlash: 92,
|
||||
BracketClose: 93,
|
||||
Underscore: 95,
|
||||
a: 97,
|
||||
z: 122,
|
||||
Tilde: 126,
|
||||
};
|
||||
|
||||
export const Token = {
|
||||
EOF: 0,
|
||||
Word: 1,
|
||||
Space: 2,
|
||||
Op: 3,
|
||||
Colon: 4,
|
||||
Comma: 7,
|
||||
BraceOpen: 8,
|
||||
BraceClose: 9,
|
||||
BracketOpen: 10,
|
||||
BracketClose: 11,
|
||||
String: 12,
|
||||
Number: 13,
|
||||
Bool: 14,
|
||||
Null: 15,
|
||||
Undefined: 16,
|
||||
Dot: 17,
|
||||
Minus: 17,
|
||||
};
|
||||
|
||||
export const BinOp = {
|
||||
/** [attr="value"] or [attr=value] */
|
||||
Equal: 1,
|
||||
/** [attr!="value"] or [attr!=value] */
|
||||
NotEqual: 2,
|
||||
/** [attr>1] */
|
||||
Greater: 3,
|
||||
/** [attr>=1] */
|
||||
GreaterThan: 4,
|
||||
/** [attr<1] */
|
||||
Less: 5,
|
||||
/** [attr<=1] */
|
||||
LessThan: 6,
|
||||
Tilde: 7,
|
||||
Plus: 8,
|
||||
Space: 9,
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} s
|
||||
* @returns {number}
|
||||
*/
|
||||
function getAttrOp(s) {
|
||||
switch (s) {
|
||||
case "=":
|
||||
return BinOp.Equal;
|
||||
case "!=":
|
||||
return BinOp.NotEqual;
|
||||
case ">":
|
||||
return BinOp.Greater;
|
||||
case ">=":
|
||||
return BinOp.GreaterThan;
|
||||
case "<":
|
||||
return BinOp.Less;
|
||||
case "<=":
|
||||
return BinOp.LessThan;
|
||||
case "~":
|
||||
return BinOp.Tilde;
|
||||
case "+":
|
||||
return BinOp.Plus;
|
||||
default:
|
||||
throw new Error(`Unknown attribute operator: '${s}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export class Lexer {
|
||||
token = Token.Word;
|
||||
start = 0;
|
||||
end = 0;
|
||||
ch = 0;
|
||||
i = -1;
|
||||
|
||||
value = "";
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
*/
|
||||
constructor(input) {
|
||||
this.input = input;
|
||||
this.step();
|
||||
this.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} token
|
||||
*/
|
||||
expect(token) {
|
||||
if (this.token !== token) {
|
||||
throw new Error(
|
||||
`Expected token '${token}', but got '${this.token}'.\n\n${this.input}\n${
|
||||
" ".repeat(this.i)
|
||||
}^`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} token
|
||||
*/
|
||||
readAsWordUntil(token) {
|
||||
const s = this.i;
|
||||
while (this.token !== Token.EOF && this.token !== token) {
|
||||
this.next();
|
||||
}
|
||||
|
||||
this.start = s;
|
||||
this.end = this.i - 1;
|
||||
this.value = this.getSlice();
|
||||
}
|
||||
|
||||
getSlice() {
|
||||
return this.input.slice(this.start, this.end);
|
||||
}
|
||||
|
||||
step() {
|
||||
this.i++;
|
||||
if (this.i >= this.input.length) {
|
||||
this.ch = -1;
|
||||
} else {
|
||||
this.ch = this.input.charCodeAt(this.i);
|
||||
}
|
||||
}
|
||||
|
||||
next() {
|
||||
this.value = "";
|
||||
|
||||
if (this.i >= this.input.length) {
|
||||
this.token = Token.EOF;
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log(
|
||||
// "NEXT",
|
||||
// this.input,
|
||||
// this.i,
|
||||
// JSON.stringify(String.fromCharCode(this.ch)),
|
||||
// );
|
||||
|
||||
while (true) {
|
||||
switch (this.ch) {
|
||||
case Char.Space:
|
||||
while (this.isWhiteSpace()) {
|
||||
this.step();
|
||||
}
|
||||
|
||||
// Check if space preceeded operator
|
||||
if (this.isOpContinue()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.token = Token.Space;
|
||||
return;
|
||||
case Char.BracketOpen:
|
||||
this.token = Token.BracketOpen;
|
||||
this.step();
|
||||
return;
|
||||
case Char.BracketClose:
|
||||
this.token = Token.BracketClose;
|
||||
this.step();
|
||||
return;
|
||||
case Char.BraceOpen:
|
||||
this.token = Token.BraceOpen;
|
||||
this.step();
|
||||
return;
|
||||
case Char.BraceClose:
|
||||
this.token = Token.BraceClose;
|
||||
this.step();
|
||||
return;
|
||||
case Char.Colon:
|
||||
this.token = Token.Colon;
|
||||
this.step();
|
||||
return;
|
||||
case Char.Comma:
|
||||
this.token = Token.Comma;
|
||||
this.step();
|
||||
return;
|
||||
case Char.Dot:
|
||||
this.token = Token.Dot;
|
||||
this.step();
|
||||
return;
|
||||
case Char.Minus:
|
||||
this.token = Token.Minus;
|
||||
this.step();
|
||||
return;
|
||||
|
||||
case Char.Plus:
|
||||
case Char.Tilde:
|
||||
case Char.Greater:
|
||||
case Char.Equal:
|
||||
case Char.Less:
|
||||
case Char.Bang: {
|
||||
this.token = Token.Op;
|
||||
this.start = this.i;
|
||||
this.step();
|
||||
|
||||
while (this.isOpContinue()) {
|
||||
this.step();
|
||||
}
|
||||
|
||||
this.end = this.i;
|
||||
this.value = this.getSlice();
|
||||
|
||||
// Consume remaining space
|
||||
while (this.isWhiteSpace()) {
|
||||
this.step();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
case Char.Quote:
|
||||
case Char.DoubleQuote: {
|
||||
this.token = Token.String;
|
||||
const ch = this.ch;
|
||||
|
||||
this.step();
|
||||
this.start = this.i;
|
||||
|
||||
while (this.ch > 0 && this.ch !== ch) {
|
||||
this.step();
|
||||
}
|
||||
|
||||
this.end = this.i;
|
||||
this.value = this.getSlice();
|
||||
this.step();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
this.start = this.i;
|
||||
this.step();
|
||||
|
||||
while (this.isWordContinue()) {
|
||||
this.step();
|
||||
}
|
||||
|
||||
this.end = this.i;
|
||||
this.value = this.getSlice();
|
||||
this.token = Token.Word;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isWordContinue() {
|
||||
const ch = this.ch;
|
||||
switch (ch) {
|
||||
case Char.Minus:
|
||||
case Char.Underscore:
|
||||
return true;
|
||||
default:
|
||||
return (ch >= Char.a && ch <= Char.z) ||
|
||||
(ch >= Char.A && ch <= Char.Z) ||
|
||||
(ch >= Char.n0 && ch <= Char.n9);
|
||||
}
|
||||
}
|
||||
|
||||
isOpContinue() {
|
||||
const ch = this.ch;
|
||||
switch (ch) {
|
||||
case Char.Equal:
|
||||
case Char.Bang:
|
||||
case Char.Greater:
|
||||
case Char.Less:
|
||||
case Char.Tilde:
|
||||
case Char.Plus:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
isWhiteSpace() {
|
||||
return this.ch === Char.Space || this.ch === Char.Tab;
|
||||
}
|
||||
}
|
||||
|
||||
const NUMBER_REG = /^(\d+\.)?\d+$/;
|
||||
const BIGINT_REG = /^\d+n$/;
|
||||
|
||||
/**
|
||||
* @param {string} raw
|
||||
* @returns {any}
|
||||
*/
|
||||
function getFromRawValue(raw) {
|
||||
switch (raw) {
|
||||
case "true":
|
||||
return true;
|
||||
case "false":
|
||||
return false;
|
||||
case "null":
|
||||
return null;
|
||||
case "undefined":
|
||||
return undefined;
|
||||
default:
|
||||
if (raw.startsWith("'") && raw.endsWith("'")) {
|
||||
if (raw.length === 2) return "";
|
||||
return raw.slice(1, -1);
|
||||
} else if (raw.startsWith('"') && raw.endsWith('"')) {
|
||||
if (raw.length === 2) return "";
|
||||
return raw.slice(1, -1);
|
||||
} else if (raw.startsWith("/")) {
|
||||
const end = raw.lastIndexOf("/");
|
||||
if (end === -1) throw new Error(`Invalid RegExp pattern: ${raw}`);
|
||||
const pattern = raw.slice(1, end);
|
||||
const flags = end < raw.length - 1 ? raw.slice(end + 1) : undefined;
|
||||
return new RegExp(pattern, flags);
|
||||
} else if (NUMBER_REG.test(raw)) {
|
||||
return Number(raw);
|
||||
} else if (BIGINT_REG.test(raw)) {
|
||||
return BigInt(raw.slice(0, -1));
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
export const ELEM_NODE = 1;
|
||||
export const RELATION_NODE = 2;
|
||||
export const ATTR_EXISTS_NODE = 3;
|
||||
export const ATTR_BIN_NODE = 4;
|
||||
export const PSEUDO_NTH_CHILD = 5;
|
||||
export const PSEUDO_HAS = 6;
|
||||
export const PSEUDO_NOT = 7;
|
||||
export const PSEUDO_FIRST_CHILD = 8;
|
||||
export const PSEUDO_LAST_CHILD = 9;
|
||||
|
||||
/**
|
||||
* Parse out all unique selectors of a selector list.
|
||||
* @param {string} input
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function splitSelectors(input) {
|
||||
/** @type {string[]} */
|
||||
const out = [];
|
||||
|
||||
let last = 0;
|
||||
let depth = 0;
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const ch = input.charCodeAt(i);
|
||||
switch (ch) {
|
||||
case Char.BraceOpen:
|
||||
depth++;
|
||||
break;
|
||||
case Char.BraceClose:
|
||||
depth--;
|
||||
break;
|
||||
case Char.Comma:
|
||||
if (depth === 0) {
|
||||
out.push(input.slice(last, i).trim());
|
||||
last = i + 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (last < input.length - 1) {
|
||||
out.push(input.slice(last).trim());
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Transformer} toElem
|
||||
* @param {Transformer} toAttr
|
||||
* @returns {Selector[]}
|
||||
*/
|
||||
export function parseSelector(input, toElem, toAttr) {
|
||||
/** @type {Selector[]} */
|
||||
const result = [];
|
||||
|
||||
/** @type {Selector[]} */
|
||||
const stack = [[]];
|
||||
|
||||
const lex = new Lexer(input);
|
||||
|
||||
// Some subselectors like `:nth-child(.. of <selector>)` must have
|
||||
// a single selector instead of selector list.
|
||||
let throwOnComma = false;
|
||||
|
||||
while (lex.token !== Token.EOF) {
|
||||
const current = /** @type {Selector} */ (stack.at(-1));
|
||||
|
||||
if (lex.token === Token.Word) {
|
||||
const value = lex.value;
|
||||
const wildcard = value === "*";
|
||||
|
||||
const elem = !wildcard ? toElem(value) : 0;
|
||||
current.push({
|
||||
type: ELEM_NODE,
|
||||
elem,
|
||||
wildcard,
|
||||
});
|
||||
lex.next();
|
||||
|
||||
continue;
|
||||
} else if (lex.token === Token.Space) {
|
||||
lex.next();
|
||||
|
||||
if (lex.token === Token.Word) {
|
||||
current.push({
|
||||
type: RELATION_NODE,
|
||||
op: BinOp.Space,
|
||||
});
|
||||
}
|
||||
|
||||
continue;
|
||||
} else if (lex.token === Token.BracketOpen) {
|
||||
lex.next();
|
||||
lex.expect(Token.Word);
|
||||
|
||||
// Check for value comparison
|
||||
const prop = [toAttr(lex.value)];
|
||||
lex.next();
|
||||
|
||||
while (lex.token === Token.Dot) {
|
||||
lex.next();
|
||||
lex.expect(Token.Word);
|
||||
|
||||
prop.push(toAttr(lex.value));
|
||||
lex.next();
|
||||
}
|
||||
|
||||
if (lex.token === Token.Op) {
|
||||
const op = getAttrOp(lex.value);
|
||||
lex.readAsWordUntil(Token.BracketClose);
|
||||
|
||||
const value = getFromRawValue(lex.value);
|
||||
current.push({ type: ATTR_BIN_NODE, prop, op, value });
|
||||
} else {
|
||||
current.push({
|
||||
type: ATTR_EXISTS_NODE,
|
||||
prop,
|
||||
});
|
||||
}
|
||||
|
||||
lex.expect(Token.BracketClose);
|
||||
lex.next();
|
||||
continue;
|
||||
} else if (lex.token === Token.Colon) {
|
||||
lex.next();
|
||||
lex.expect(Token.Word);
|
||||
|
||||
switch (lex.value) {
|
||||
case "first-child":
|
||||
current.push({
|
||||
type: PSEUDO_FIRST_CHILD,
|
||||
});
|
||||
break;
|
||||
case "last-child":
|
||||
current.push({
|
||||
type: PSEUDO_LAST_CHILD,
|
||||
});
|
||||
break;
|
||||
case "nth-child": {
|
||||
lex.next();
|
||||
lex.expect(Token.BraceOpen);
|
||||
lex.next();
|
||||
|
||||
let mul = 1;
|
||||
let repeat = false;
|
||||
let step = 0;
|
||||
if (lex.token === Token.Minus) {
|
||||
mul = -1;
|
||||
lex.next();
|
||||
}
|
||||
|
||||
lex.expect(Token.Word);
|
||||
const value = lex.getSlice();
|
||||
|
||||
if (value.endsWith("n")) {
|
||||
repeat = true;
|
||||
step = +value.slice(0, -1) * mul;
|
||||
} else {
|
||||
step = +value * mul;
|
||||
}
|
||||
|
||||
lex.next();
|
||||
|
||||
/** @type {PseudoNthChild} */
|
||||
const node = {
|
||||
type: PSEUDO_NTH_CHILD,
|
||||
of: null,
|
||||
op: null,
|
||||
step,
|
||||
stepOffset: 0,
|
||||
repeat,
|
||||
};
|
||||
current.push(node);
|
||||
|
||||
if (lex.token === Token.Space) lex.next();
|
||||
|
||||
if (lex.token !== Token.BraceClose) {
|
||||
if (lex.token === Token.Op) {
|
||||
node.op = lex.value;
|
||||
lex.next();
|
||||
|
||||
if (lex.token === Token.Space) lex.next();
|
||||
} else if (lex.token === Token.Minus) {
|
||||
node.op = "-";
|
||||
lex.next();
|
||||
|
||||
if (lex.token === Token.Space) {
|
||||
lex.next();
|
||||
}
|
||||
}
|
||||
|
||||
lex.expect(Token.Word);
|
||||
node.stepOffset = +lex.value;
|
||||
lex.next();
|
||||
|
||||
if (lex.token !== Token.BraceClose) {
|
||||
lex.next(); // Space
|
||||
|
||||
if (lex.token === Token.Word) {
|
||||
if (/** @type {string} */ (lex.value) !== "of") {
|
||||
throw new Error(
|
||||
`Expected 'of' keyword in ':nth-child' but got: ${lex.value}`,
|
||||
);
|
||||
}
|
||||
|
||||
lex.next();
|
||||
lex.expect(Token.Space);
|
||||
lex.next();
|
||||
throwOnComma = true;
|
||||
stack.push([]);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
lex.expect(Token.BraceClose);
|
||||
} else if (!node.repeat) {
|
||||
// :nth-child(2) -> step is actually stepOffset
|
||||
node.stepOffset = node.step - 1;
|
||||
node.step = 0;
|
||||
}
|
||||
|
||||
lex.next();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
case "has":
|
||||
case "where":
|
||||
case "is": {
|
||||
lex.next();
|
||||
lex.expect(Token.BraceOpen);
|
||||
lex.next();
|
||||
|
||||
current.push({
|
||||
type: PSEUDO_HAS,
|
||||
selectors: [],
|
||||
});
|
||||
stack.push([]);
|
||||
|
||||
continue;
|
||||
}
|
||||
case "not": {
|
||||
lex.next();
|
||||
lex.expect(Token.BraceOpen);
|
||||
lex.next();
|
||||
|
||||
current.push({
|
||||
type: PSEUDO_NOT,
|
||||
selectors: [],
|
||||
});
|
||||
stack.push([]);
|
||||
|
||||
continue;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown pseudo selector: '${lex.value}'`);
|
||||
}
|
||||
} else if (lex.token === Token.Comma) {
|
||||
if (throwOnComma) {
|
||||
throw new Error(`Multiple selector arguments not supported here`);
|
||||
}
|
||||
|
||||
lex.next();
|
||||
if (lex.token === Token.Space) {
|
||||
lex.next();
|
||||
}
|
||||
|
||||
popSelector(result, stack);
|
||||
stack.push([]);
|
||||
continue;
|
||||
} else if (lex.token === Token.BraceClose) {
|
||||
throwOnComma = false;
|
||||
popSelector(result, stack);
|
||||
} else if (lex.token === Token.Op) {
|
||||
current.push({
|
||||
type: RELATION_NODE,
|
||||
op: getAttrOp(lex.value),
|
||||
});
|
||||
}
|
||||
|
||||
lex.next();
|
||||
}
|
||||
|
||||
if (stack.length > 0) {
|
||||
result.push(stack[0]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Selector[]} result
|
||||
* @param {Selector[]} stack
|
||||
*/
|
||||
function popSelector(result, stack) {
|
||||
const sel = /** @type {Selector} */ (stack.pop());
|
||||
|
||||
if (stack.length === 0) {
|
||||
result.push(sel);
|
||||
stack.push([]);
|
||||
} else {
|
||||
const prev = /** @type {Selector} */ (stack.at(-1));
|
||||
if (prev.length === 0) {
|
||||
throw new Error(`Empty selector`);
|
||||
}
|
||||
|
||||
const node = prev.at(-1);
|
||||
if (node === undefined) {
|
||||
throw new Error(`Empty node`);
|
||||
}
|
||||
|
||||
if (node.type === PSEUDO_NTH_CHILD) {
|
||||
node.of = sel;
|
||||
} else if (node.type === PSEUDO_HAS || node.type === PSEUDO_NOT) {
|
||||
node.selectors.push(sel);
|
||||
} else {
|
||||
throw new Error(`Multiple selectors not allowed here`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TRUE_FN = () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Selector} selector
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
export function compileSelector(selector) {
|
||||
/** @type {MatcherFn} */
|
||||
let fn = TRUE_FN;
|
||||
|
||||
for (let i = 0; i < selector.length; i++) {
|
||||
const node = selector[i];
|
||||
|
||||
switch (node.type) {
|
||||
case ELEM_NODE:
|
||||
fn = matchElem(node, fn);
|
||||
break;
|
||||
case RELATION_NODE:
|
||||
switch (node.op) {
|
||||
case BinOp.Space:
|
||||
fn = matchDescendant(fn);
|
||||
break;
|
||||
case BinOp.Greater:
|
||||
fn = matchChild(fn);
|
||||
break;
|
||||
case BinOp.Plus:
|
||||
fn = matchAdjacent(fn);
|
||||
break;
|
||||
case BinOp.Tilde:
|
||||
fn = matchFollowing(fn);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown relation op ${node.op}`);
|
||||
}
|
||||
break;
|
||||
case ATTR_EXISTS_NODE:
|
||||
fn = matchAttrExists(node, fn);
|
||||
break;
|
||||
case ATTR_BIN_NODE:
|
||||
fn = matchAttrBin(node, fn);
|
||||
break;
|
||||
case PSEUDO_FIRST_CHILD:
|
||||
fn = matchFirstChild(fn);
|
||||
break;
|
||||
case PSEUDO_LAST_CHILD:
|
||||
fn = matchLastChild(fn);
|
||||
break;
|
||||
case PSEUDO_NTH_CHILD:
|
||||
fn = matchNthChild(node, fn);
|
||||
break;
|
||||
case PSEUDO_HAS:
|
||||
// FIXME
|
||||
// fn = matchIs(part, fn);
|
||||
throw new Error("TODO: :has");
|
||||
case PSEUDO_NOT:
|
||||
fn = matchNot(node.selectors, fn);
|
||||
break;
|
||||
default:
|
||||
// @ts-ignore error handling
|
||||
console.log(node);
|
||||
throw new Error(`Unknown selector node`);
|
||||
}
|
||||
}
|
||||
|
||||
return fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NextFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchFirstChild(next) {
|
||||
return (ctx, id) => {
|
||||
const parent = ctx.getParent(id);
|
||||
const first = ctx.getFirstChild(parent);
|
||||
return first === id && next(ctx, first);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NextFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchLastChild(next) {
|
||||
return (ctx, id) => {
|
||||
const parent = ctx.getParent(id);
|
||||
const last = ctx.getLastChild(parent);
|
||||
return last === id && next(ctx, id);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PseudoNthChild} node
|
||||
* @param {number} i
|
||||
* @returns {number}
|
||||
*/
|
||||
function getNthAnB(node, i) {
|
||||
const n = node.step * i;
|
||||
|
||||
if (node.op === null) return n;
|
||||
|
||||
switch (node.op) {
|
||||
case "+":
|
||||
return n + node.stepOffset;
|
||||
case "-":
|
||||
return n - node.stepOffset;
|
||||
default:
|
||||
throw new Error("Not supported nth-child operator: " + node.op);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PseudoNthChild} node
|
||||
* @param {NextFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchNthChild(node, next) {
|
||||
const ofSelector = node.of !== null ? compileSelector(node.of) : TRUE_FN;
|
||||
|
||||
return (ctx, id) => {
|
||||
const siblings = ctx.getSiblings(id);
|
||||
const idx = siblings.indexOf(id);
|
||||
|
||||
if (!node.repeat) {
|
||||
return idx === node.stepOffset && next(ctx, id);
|
||||
}
|
||||
|
||||
for (let i = 0; i < siblings.length; i++) {
|
||||
const n = getNthAnB(node, i);
|
||||
if (id === n) {
|
||||
if (node.of !== null && !ofSelector(ctx, id)) {
|
||||
continue;
|
||||
} else if (next(ctx, id)) {
|
||||
return true;
|
||||
}
|
||||
} else if (n > idx) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Selector[]} selectors
|
||||
* @param {NextFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchNot(selectors, next) {
|
||||
/** @type {MatcherFn[]} */
|
||||
const compiled = [];
|
||||
|
||||
for (let i = 0; i < selectors.length; i++) {
|
||||
const sel = selectors[i];
|
||||
compiled.push(compileSelector(sel));
|
||||
}
|
||||
|
||||
return (ctx, id) => {
|
||||
for (let i = 0; i < compiled.length; i++) {
|
||||
const fn = compiled[i];
|
||||
if (fn(ctx, id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return next(ctx, id);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NextFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchDescendant(next) {
|
||||
return (ctx, id) => {
|
||||
let current = ctx.getParent(id);
|
||||
while (current > -1) {
|
||||
if (next(ctx, current)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
current = ctx.getParent(current);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NextFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchChild(next) {
|
||||
return (ctx, id) => {
|
||||
const parent = ctx.getParent(id);
|
||||
if (parent < 0) return false;
|
||||
|
||||
return next(ctx, parent);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NextFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchAdjacent(next) {
|
||||
return (ctx, id) => {
|
||||
const siblings = ctx.getSiblings(id);
|
||||
const idx = siblings.indexOf(id) - 1;
|
||||
|
||||
if (idx < 0) return false;
|
||||
|
||||
const prev = siblings[idx];
|
||||
return next(ctx, prev);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NextFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchFollowing(next) {
|
||||
return (ctx, id) => {
|
||||
const siblings = ctx.getSiblings(id);
|
||||
const idx = siblings.indexOf(id) - 1;
|
||||
|
||||
if (idx < 0) return false;
|
||||
|
||||
for (let i = idx; i >= 0; i--) {
|
||||
const sib = siblings[i];
|
||||
if (next(ctx, sib)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ElemSelector} part
|
||||
* @param {MatcherFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchElem(part, next) {
|
||||
return (ctx, id) => {
|
||||
if (part.wildcard) return next(ctx, id);
|
||||
const type = ctx.getType(id);
|
||||
if (type > -1 && type === part.elem) return next(ctx, id);
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AttrExists} attr
|
||||
* @param {MatcherFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchAttrExists(attr, next) {
|
||||
return (ctx, id) => {
|
||||
return ctx.hasAttrPath(id, attr.prop, 0) ? next(ctx, id) : false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AttrBin} attr
|
||||
* @param {MatcherFn} next
|
||||
* @returns {MatcherFn}
|
||||
*/
|
||||
function matchAttrBin(attr, next) {
|
||||
return (ctx, id) => {
|
||||
if (!ctx.hasAttrPath(id, attr.prop, 0)) return false;
|
||||
const value = ctx.getAttrPathValue(id, attr.prop, 0);
|
||||
if (!matchAttrValue(attr, value)) return false;
|
||||
return next(ctx, id);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AttrBin} attr
|
||||
* @param {*} value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function matchAttrValue(attr, value) {
|
||||
switch (attr.op) {
|
||||
case BinOp.Equal:
|
||||
return value === attr.value;
|
||||
case BinOp.NotEqual:
|
||||
return value !== attr.value;
|
||||
case BinOp.Greater:
|
||||
return typeof value === "number" && typeof attr.value === "number" &&
|
||||
value > attr.value;
|
||||
case BinOp.GreaterThan:
|
||||
return typeof value === "number" && typeof attr.value === "number" &&
|
||||
value >= attr.value;
|
||||
case BinOp.Less:
|
||||
return typeof value === "number" && typeof attr.value === "number" &&
|
||||
value < attr.value;
|
||||
case BinOp.LessThan:
|
||||
return typeof value === "number" && typeof attr.value === "number" &&
|
||||
value <= attr.value;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,600 +0,0 @@
|
|||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
import {
|
||||
ATTR_BIN_NODE,
|
||||
ATTR_EXISTS_NODE,
|
||||
BinOp,
|
||||
ELEM_NODE,
|
||||
Lexer,
|
||||
parseSelector,
|
||||
PSEUDO_FIRST_CHILD,
|
||||
PSEUDO_HAS,
|
||||
PSEUDO_LAST_CHILD,
|
||||
PSEUDO_NOT,
|
||||
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, attr: 4 };
|
||||
const toTag = (name: string): number => Tags[name];
|
||||
const toAttr = (name: string): number => Attrs[name];
|
||||
|
||||
const testParse = (input: string) => parseSelector(input, toTag, toAttr);
|
||||
|
||||
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" },
|
||||
]);
|
||||
|
||||
expect(testLexer("Foo Bar ~ Bar")).toEqual([
|
||||
{ token: Token.Word, value: "Foo" },
|
||||
{ token: Token.Space, value: "" },
|
||||
{ token: Token.Word, value: "Bar" },
|
||||
{ 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 - Elem", () => {
|
||||
expect(testParse("Foo")).toEqual([[
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 1,
|
||||
wildcard: false,
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
Deno.test("Parser - Relation (descendant)", () => {
|
||||
expect(testParse("Foo Bar")).toEqual([[
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 1,
|
||||
wildcard: false,
|
||||
},
|
||||
{
|
||||
type: RELATION_NODE,
|
||||
op: BinOp.Space,
|
||||
},
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 2,
|
||||
wildcard: false,
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
Deno.test("Parser - Relation", () => {
|
||||
expect(testParse("Foo > Bar")).toEqual([[
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 1,
|
||||
wildcard: false,
|
||||
},
|
||||
{
|
||||
type: RELATION_NODE,
|
||||
op: BinOp.Greater,
|
||||
},
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 2,
|
||||
wildcard: false,
|
||||
},
|
||||
]]);
|
||||
|
||||
expect(testParse("Foo ~ Bar")).toEqual([[
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 1,
|
||||
wildcard: false,
|
||||
},
|
||||
{
|
||||
type: RELATION_NODE,
|
||||
op: BinOp.Tilde,
|
||||
},
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 2,
|
||||
wildcard: false,
|
||||
},
|
||||
]]);
|
||||
|
||||
expect(testParse("Foo + Bar")).toEqual([[
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 1,
|
||||
wildcard: false,
|
||||
},
|
||||
{
|
||||
type: RELATION_NODE,
|
||||
op: BinOp.Plus,
|
||||
},
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 2,
|
||||
wildcard: false,
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
Deno.test("Parser - Attr", () => {
|
||||
expect(testParse("[foo]")).toEqual([[
|
||||
{
|
||||
type: ATTR_EXISTS_NODE,
|
||||
prop: [1],
|
||||
},
|
||||
]]);
|
||||
|
||||
expect(testParse("[foo][bar]")).toEqual([[
|
||||
{
|
||||
type: ATTR_EXISTS_NODE,
|
||||
prop: [1],
|
||||
},
|
||||
{
|
||||
type: ATTR_EXISTS_NODE,
|
||||
prop: [2],
|
||||
},
|
||||
]]);
|
||||
|
||||
expect(testParse("[foo=1]")).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: BinOp.Equal,
|
||||
prop: [1],
|
||||
value: 1,
|
||||
},
|
||||
]]);
|
||||
expect(testParse("[foo=true]")).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: BinOp.Equal,
|
||||
prop: [1],
|
||||
value: true,
|
||||
},
|
||||
]]);
|
||||
expect(testParse("[foo=false]")).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: BinOp.Equal,
|
||||
prop: [1],
|
||||
value: false,
|
||||
},
|
||||
]]);
|
||||
expect(testParse("[foo=null]")).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: BinOp.Equal,
|
||||
prop: [1],
|
||||
value: null,
|
||||
},
|
||||
]]);
|
||||
expect(testParse("[foo='str']")).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: BinOp.Equal,
|
||||
prop: [1],
|
||||
value: "str",
|
||||
},
|
||||
]]);
|
||||
expect(testParse('[foo="str"]')).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: BinOp.Equal,
|
||||
prop: [1],
|
||||
value: "str",
|
||||
},
|
||||
]]);
|
||||
expect(testParse("[foo=/str/]")).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: BinOp.Equal,
|
||||
prop: [1],
|
||||
value: /str/,
|
||||
},
|
||||
]]);
|
||||
expect(testParse("[foo=/str/g]")).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: BinOp.Equal,
|
||||
prop: [1],
|
||||
value: /str/g,
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
Deno.test("Parser - Attr nested", () => {
|
||||
expect(testParse("[foo.bar]")).toEqual([[
|
||||
{
|
||||
type: ATTR_EXISTS_NODE,
|
||||
prop: [1, 2],
|
||||
},
|
||||
]]);
|
||||
|
||||
expect(testParse("[foo.bar = 2]")).toEqual([[
|
||||
{
|
||||
type: ATTR_BIN_NODE,
|
||||
op: BinOp.Equal,
|
||||
prop: [1, 2],
|
||||
value: 2,
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
Deno.test("Parser - Pseudo no value", () => {
|
||||
expect(testParse(":first-child")).toEqual([[
|
||||
{
|
||||
type: PSEUDO_FIRST_CHILD,
|
||||
},
|
||||
]]);
|
||||
expect(testParse(":last-child")).toEqual([[
|
||||
{
|
||||
type: PSEUDO_LAST_CHILD,
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
Deno.test("Parser - Pseudo nth-child", () => {
|
||||
expect(testParse(":nth-child(2)")).toEqual([[
|
||||
{
|
||||
type: PSEUDO_NTH_CHILD,
|
||||
of: null,
|
||||
op: null,
|
||||
step: 0,
|
||||
stepOffset: 1,
|
||||
repeat: false,
|
||||
},
|
||||
]]);
|
||||
expect(testParse(":nth-child(2n)")).toEqual([[
|
||||
{
|
||||
type: PSEUDO_NTH_CHILD,
|
||||
of: null,
|
||||
op: null,
|
||||
step: 2,
|
||||
stepOffset: 0,
|
||||
repeat: true,
|
||||
},
|
||||
]]);
|
||||
expect(testParse(":nth-child(-2n)")).toEqual([[
|
||||
{
|
||||
type: PSEUDO_NTH_CHILD,
|
||||
of: null,
|
||||
op: null,
|
||||
step: -2,
|
||||
stepOffset: 0,
|
||||
repeat: true,
|
||||
},
|
||||
]]);
|
||||
expect(testParse(":nth-child(2n + 1)")).toEqual([[
|
||||
{
|
||||
type: PSEUDO_NTH_CHILD,
|
||||
of: null,
|
||||
op: "+",
|
||||
step: 2,
|
||||
stepOffset: 1,
|
||||
repeat: true,
|
||||
},
|
||||
]]);
|
||||
expect(testParse(":nth-child(2n + 1 of Foo[attr])"))
|
||||
.toEqual([[
|
||||
{
|
||||
type: PSEUDO_NTH_CHILD,
|
||||
of: [
|
||||
{ type: ELEM_NODE, elem: 1, wildcard: false },
|
||||
{ type: ATTR_EXISTS_NODE, prop: [4] },
|
||||
],
|
||||
op: "+",
|
||||
step: 2,
|
||||
stepOffset: 1,
|
||||
repeat: true,
|
||||
},
|
||||
]]);
|
||||
|
||||
// Invalid selectors
|
||||
expect(() => testParse(":nth-child(2n + 1 of Foo[attr], Bar)"))
|
||||
.toThrow();
|
||||
expect(() => testParse(":nth-child(2n - 1 foo)")).toThrow();
|
||||
});
|
||||
|
||||
Deno.test("Parser - Pseudo has/is/where", () => {
|
||||
expect(testParse(":has(Foo:has(Foo), Bar)")).toEqual([[
|
||||
{
|
||||
type: PSEUDO_HAS,
|
||||
selectors: [
|
||||
[
|
||||
{ type: ELEM_NODE, elem: 1, wildcard: false },
|
||||
{
|
||||
type: PSEUDO_HAS,
|
||||
selectors: [
|
||||
[{ type: ELEM_NODE, elem: 1, wildcard: false }],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{ type: ELEM_NODE, elem: 2, wildcard: false },
|
||||
],
|
||||
],
|
||||
},
|
||||
]]);
|
||||
expect(testParse(":where(Foo:where(Foo), Bar)")).toEqual([[
|
||||
{
|
||||
type: PSEUDO_HAS,
|
||||
selectors: [
|
||||
[
|
||||
{ type: ELEM_NODE, elem: 1, wildcard: false },
|
||||
{
|
||||
type: PSEUDO_HAS,
|
||||
selectors: [
|
||||
[{ type: ELEM_NODE, elem: 1, wildcard: false }],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{ type: ELEM_NODE, elem: 2, wildcard: false },
|
||||
],
|
||||
],
|
||||
},
|
||||
]]);
|
||||
expect(testParse(":is(Foo:is(Foo), Bar)")).toEqual([[
|
||||
{
|
||||
type: PSEUDO_HAS,
|
||||
selectors: [
|
||||
[
|
||||
{ type: ELEM_NODE, elem: 1, wildcard: false },
|
||||
{
|
||||
type: PSEUDO_HAS,
|
||||
selectors: [
|
||||
[{ type: ELEM_NODE, elem: 1, wildcard: false }],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{ type: ELEM_NODE, elem: 2, wildcard: false },
|
||||
],
|
||||
],
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
Deno.test("Parser - Pseudo not", () => {
|
||||
expect(testParse(":not(Foo:not(Foo), Bar)")).toEqual([[
|
||||
{
|
||||
type: PSEUDO_NOT,
|
||||
selectors: [
|
||||
[
|
||||
{ type: ELEM_NODE, elem: 1, wildcard: false },
|
||||
{
|
||||
type: PSEUDO_NOT,
|
||||
selectors: [
|
||||
[{ type: ELEM_NODE, elem: 1, wildcard: false }],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{ type: ELEM_NODE, elem: 2, wildcard: false },
|
||||
],
|
||||
],
|
||||
},
|
||||
]]);
|
||||
});
|
||||
|
||||
Deno.test("Parser - mixed", () => {
|
||||
expect(testParse("Foo[foo=true] Bar")).toEqual([[
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 1,
|
||||
wildcard: false,
|
||||
},
|
||||
{ type: ATTR_BIN_NODE, op: BinOp.Equal, prop: [1], value: true },
|
||||
{ type: RELATION_NODE, op: BinOp.Space },
|
||||
{
|
||||
type: ELEM_NODE,
|
||||
elem: 2,
|
||||
wildcard: false,
|
||||
},
|
||||
]]);
|
||||
});
|
|
@ -1,477 +0,0 @@
|
|||
import { splitSelectors } from "./40_lint_selector.js";
|
||||
import {
|
||||
compileSelector,
|
||||
MatchCtx,
|
||||
MatcherFn,
|
||||
parseSelector,
|
||||
} from "./40_lint_selector.js";
|
||||
import { expect } from "@std/expect";
|
||||
|
||||
/**
|
||||
* TS eslint selector Examples
|
||||
*
|
||||
* ```js
|
||||
* ForOfStatement[await=true]
|
||||
* VariableDeclaration[kind="await using"]
|
||||
* MethodDefinition[kind="constructor"] ThisExpression
|
||||
* PropertyDefinition > ArrowFunctionExpression.value
|
||||
* PropertyDefinition > *.key
|
||||
* ThisExpression, Super
|
||||
* VariableDeclarator,PropertyDefinition,:matches(FunctionDeclaration,FunctionExpression) > AssignmentPattern
|
||||
* ImportDeclaration[importKind = "type"]
|
||||
* ImportSpecifier[importKind = "type"]
|
||||
* ExportNamedDeclaration:not([source])
|
||||
* UnaryExpression[operator="delete"]
|
||||
* AssignmentExpression[operator = "+="], BinaryExpression[operator = "+"]
|
||||
* CallExpression > MemberExpression.callee > Identifier[name = "join"].property
|
||||
* CallExpression > MemberExpression.callee > Identifier[name = /^(toLocaleString|toString)$/].property
|
||||
* ImportDeclaration[importKind!="type"]
|
||||
* UnaryExpression[operator="void"]
|
||||
* UnaryExpression[operator="!"]
|
||||
* LogicalExpression[operator = "??"] > TSNonNullExpression.left
|
||||
* CallExpression[callee.name="require"]
|
||||
* :not(ArrowFunctionExpression) > TSTypeParameterDeclaration > TSTypeParameter[constraint]
|
||||
* ArrowFunctionExpression > TSTypeParameterDeclaration > TSTypeParameter[constraint]
|
||||
* PropertyDefinition[value != null]
|
||||
* :not(ObjectPattern) > Property
|
||||
* CallExpression > *.callee
|
||||
* TaggedTemplateExpression > *.tag
|
||||
* BinaryExpression[operator=/^[<>!=]?={0,2}$/]
|
||||
* :matches(ClassDeclaration, ClassExpression)
|
||||
* MethodDefinition[kind="constructor"]
|
||||
* MemberExpression[computed=true]
|
||||
* TSTypeLiteral[members.length = 1]
|
||||
* CallExpression[arguments.length=1] > MemberExpression.callee[property.name="test"][computed=false]
|
||||
* CallExpression > MemberExpression.callee
|
||||
* CallExpression[arguments.length=1] > MemberExpression
|
||||
* CallExpression > MemberExpression.callee[property.name="test"][computed=false]
|
||||
* :matches(MethodDefinition, TSMethodSignature)[kind=get]
|
||||
* ArrowFunctionExpression[async = true] > :not(BlockStatement, AwaitExpression)
|
||||
* ```
|
||||
*/
|
||||
|
||||
const AstNodes: Record<string, number> = {
|
||||
Foo: 1,
|
||||
Bar: 2,
|
||||
Baz: 3,
|
||||
Foobar: 4,
|
||||
};
|
||||
|
||||
const AstAttrs: Record<string, number> = {
|
||||
_empty: 0,
|
||||
key: 1,
|
||||
value: 2,
|
||||
attr: 3,
|
||||
attr2: 4,
|
||||
children: 5,
|
||||
msg: 6,
|
||||
};
|
||||
const toElem = (value: string) => AstNodes[value];
|
||||
const toAttr = (value: string) => AstAttrs[value];
|
||||
|
||||
export interface TestNode {
|
||||
type: keyof typeof AstNodes;
|
||||
children?: TestNode[];
|
||||
[key: string]: number | boolean | string | TestNode | TestNode[] | undefined;
|
||||
}
|
||||
|
||||
export interface FakeProp {
|
||||
propId: number;
|
||||
name: string;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
export interface FakeNode {
|
||||
type: number;
|
||||
name: string;
|
||||
parentId: number;
|
||||
props: FakeProp[];
|
||||
original: TestNode;
|
||||
}
|
||||
|
||||
function isFakeNode(x: unknown): x is FakeNode {
|
||||
return x !== null && typeof x === "object" && "type" in x &&
|
||||
typeof x.type === "number" &&
|
||||
"original" in x;
|
||||
}
|
||||
|
||||
class FakeContext implements MatchCtx {
|
||||
ids = new Map<number, FakeNode>();
|
||||
idByNode = new Map<FakeNode, number>();
|
||||
id = 0;
|
||||
|
||||
getAttrPathValue(id: number, props: number[]) {
|
||||
const node = this.ids.get(id);
|
||||
if (node === undefined) return undefined;
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
let tmp: any = node;
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
const prop = props[i];
|
||||
|
||||
if (isFakeNode(tmp)) {
|
||||
const found = tmp.props.find((node) => node.propId === prop);
|
||||
if (!found) return undefined;
|
||||
tmp = found.value;
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = AstAttrs[prop];
|
||||
if (!(name in tmp)) return undefined;
|
||||
tmp = tmp[name];
|
||||
}
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
hasAttrPath(id: number, propId: number[]): boolean {
|
||||
return this.getAttrPathValue(id, propId) !== undefined;
|
||||
}
|
||||
|
||||
getType(id: number): number {
|
||||
const node = this.ids.get(id);
|
||||
if (node === undefined) return -1;
|
||||
return node.type;
|
||||
}
|
||||
|
||||
getFirstChild(id: number): number {
|
||||
const node = this.ids.get(id);
|
||||
if (node === undefined) return -1;
|
||||
|
||||
let first = -1;
|
||||
// First check if there is an array prop
|
||||
for (const prop of node.props) {
|
||||
if (Array.isArray(prop.value)) {
|
||||
if (prop.value.length === 0) return -1;
|
||||
return prop.value[0];
|
||||
} else if (
|
||||
first === -1 && prop.value !== null && typeof prop.value === "object"
|
||||
) {
|
||||
// @ts-ignore loosely typed
|
||||
first = prop.value;
|
||||
}
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
getLastChild(id: number): number {
|
||||
const node = this.ids.get(id);
|
||||
if (node === undefined) return -1;
|
||||
|
||||
let last = -1;
|
||||
// First check if there is an array prop
|
||||
for (const prop of node.props) {
|
||||
if (Array.isArray(prop.value)) {
|
||||
if (prop.value.length === 0) return -1;
|
||||
return prop.value.at(-1);
|
||||
} else if (prop.value !== null && typeof prop.value !== "object") {
|
||||
// @ts-ignore loosely typed
|
||||
last = prop.value;
|
||||
}
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
getParent(id: number): number {
|
||||
const node = this.ids.get(id);
|
||||
if (node === undefined) return -1;
|
||||
return node.parentId;
|
||||
}
|
||||
|
||||
getSiblings(id: number): number[] {
|
||||
const parent = this.getParent(id);
|
||||
const node = this.ids.get(parent);
|
||||
if (node === undefined) return [];
|
||||
|
||||
for (const prop of node.props) {
|
||||
if (Array.isArray(prop.value)) {
|
||||
if (prop.value.includes(id)) {
|
||||
return prop.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function fakeSerializeAst(node: TestNode): FakeContext {
|
||||
const ctx = new FakeContext();
|
||||
serializeFakeNode(ctx, node, -1);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function serializeFakeNode(
|
||||
ctx: FakeContext,
|
||||
node: TestNode,
|
||||
parentId: number,
|
||||
): number {
|
||||
const id = ctx.id;
|
||||
ctx.id++;
|
||||
|
||||
const type = AstNodes[node.type];
|
||||
|
||||
const props: FakeProp[] = [];
|
||||
const fake: FakeNode = {
|
||||
type,
|
||||
name: node.type,
|
||||
parentId,
|
||||
props,
|
||||
get original() {
|
||||
return node;
|
||||
},
|
||||
};
|
||||
|
||||
ctx.ids.set(id, fake);
|
||||
ctx.idByNode.set(fake, id);
|
||||
|
||||
for (const [k, value] of Object.entries(node)) {
|
||||
if (k === "type") continue;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const propId = (AstAttrs as any)[k] as number;
|
||||
|
||||
const prop: FakeProp = {
|
||||
propId: propId,
|
||||
name: k,
|
||||
value: null,
|
||||
};
|
||||
props.push(prop);
|
||||
|
||||
if (value !== null && typeof value === "object") {
|
||||
if (Array.isArray(value)) {
|
||||
prop.value = value.map((v) => serializeFakeNode(ctx, v, id));
|
||||
} else {
|
||||
prop.value = serializeFakeNode(ctx, value, id);
|
||||
}
|
||||
} else {
|
||||
prop.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
function visit(
|
||||
ctx: FakeContext,
|
||||
selector: MatcherFn,
|
||||
id: number,
|
||||
): unknown {
|
||||
const node = ctx.ids.get(id)!;
|
||||
// console.log("visit", { node });
|
||||
const res = selector(ctx, id);
|
||||
if (res) {
|
||||
// console.log("<-- MATCHED");
|
||||
return node.original;
|
||||
}
|
||||
|
||||
for (let i = 0; i < node.props.length; i++) {
|
||||
const prop = node.props[i];
|
||||
const value = prop.value;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const res = visit(ctx, selector, value[i]);
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
} else if (isFakeNode(value)) {
|
||||
const id = ctx.idByNode.get(value)!;
|
||||
const res = visit(ctx, selector, id);
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function testSelector(
|
||||
ast: TestNode,
|
||||
selector: string,
|
||||
): unknown {
|
||||
const ctx = fakeSerializeAst(ast);
|
||||
const raw = parseSelector(selector, toElem, toAttr)[0];
|
||||
const sel = compileSelector(raw);
|
||||
|
||||
return visit(ctx, sel, 0);
|
||||
}
|
||||
|
||||
Deno.test("select descendant: A B", () => {
|
||||
const ast: TestNode = {
|
||||
type: "Foo",
|
||||
children: [{ type: "Bar" }, { type: "Baz" }],
|
||||
};
|
||||
|
||||
expect(testSelector(ast, "Foo")).toEqual(ast);
|
||||
expect(testSelector(ast, "Foo Bar")).toEqual(ast.children![0]);
|
||||
expect(testSelector(ast, "Foo Baz")).toEqual(ast.children![1]);
|
||||
|
||||
// Not matching
|
||||
expect(testSelector(ast, "Foo Foo")).toEqual(undefined);
|
||||
});
|
||||
|
||||
Deno.test("select child: A > B", () => {
|
||||
const ast: TestNode = {
|
||||
type: "Foo",
|
||||
foo: "fail",
|
||||
children: [{ type: "Bar", children: [{ type: "Foo" }] }, {
|
||||
type: "Foo",
|
||||
}],
|
||||
};
|
||||
expect(testSelector(ast, "Foo > Foo")).toEqual(ast.children![1]);
|
||||
expect(testSelector(ast, "Foo>Foo")).toEqual(ast.children![1]);
|
||||
expect(testSelector(ast, "* > Foo")).toEqual(ast.children![1]);
|
||||
expect(testSelector(ast, "*> Foo")).toEqual(ast.children![1]);
|
||||
expect(testSelector(ast, "* > *> Foo")).toEqual(ast.children![1]);
|
||||
});
|
||||
|
||||
Deno.test("select child: A > B #2", () => {
|
||||
const ast: TestNode = {
|
||||
type: "Foo",
|
||||
foo: "fail",
|
||||
children: [{ type: "Bar", children: [{ type: "Foo" }] }],
|
||||
};
|
||||
expect(testSelector(ast, "Foo > Foo")).toEqual(undefined);
|
||||
expect(testSelector(ast, "Foo>Foo")).toEqual(undefined);
|
||||
});
|
||||
|
||||
Deno.test("select child: A + B", () => {
|
||||
const ast: TestNode = {
|
||||
type: "Foo",
|
||||
children: [
|
||||
{ type: "Bar", msg: "FAIL" },
|
||||
{ type: "Bar", msg: "FAIL" },
|
||||
{ type: "Baz" },
|
||||
{ type: "Baz", msg: "FAIL" },
|
||||
{ type: "Foo", msg: "FAIL" },
|
||||
{ type: "Baz", msg: "FAIL" },
|
||||
],
|
||||
};
|
||||
expect(testSelector(ast, "Bar + Baz")).toEqual(ast.children![2]);
|
||||
expect(testSelector(ast, "Bar+Baz")).toEqual(ast.children![2]);
|
||||
});
|
||||
|
||||
Deno.test("select child: A ~ B", () => {
|
||||
const ast: TestNode = {
|
||||
type: "Foo",
|
||||
children: [
|
||||
{ type: "Bar", msg: "FAIL" },
|
||||
{ type: "Bar", msg: "FAIL" },
|
||||
{ type: "Foo", msg: "FAIL" },
|
||||
{ type: "Baz", msg: "ok #1" },
|
||||
],
|
||||
};
|
||||
expect(testSelector(ast, "Bar ~ Baz")).toEqual(ast.children![3]);
|
||||
expect(testSelector(ast, "Bar~Baz")).toEqual(ast.children![3]);
|
||||
});
|
||||
|
||||
Deno.test("select child: A[attr]", () => {
|
||||
const ast: TestNode = {
|
||||
type: "Foo",
|
||||
children: [
|
||||
{ type: "Foo", msg: "a" },
|
||||
{ type: "Bar", msg: "b" },
|
||||
{ type: "Baz" },
|
||||
],
|
||||
};
|
||||
expect(testSelector(ast, "[msg]")).toEqual(ast.children![0]);
|
||||
expect(testSelector(ast, "Foo[msg]")).toEqual(ast.children![0]);
|
||||
expect(testSelector(ast, "Foo[msg=a]")).toEqual(ast.children![0]);
|
||||
expect(testSelector(ast, "Foo[msg = a]")).toEqual(ast.children![0]);
|
||||
|
||||
expect(testSelector(ast, "Foo[msg='a']")).toEqual(ast.children![0]);
|
||||
expect(testSelector(ast, "Foo[msg = 'a']")).toEqual(ast.children![0]);
|
||||
expect(testSelector(ast, 'Foo[msg="a"]')).toEqual(ast.children![0]);
|
||||
expect(testSelector(ast, 'Foo[msg = "a"]')).toEqual(ast.children![0]);
|
||||
});
|
||||
|
||||
Deno.test("select child: A[attr <op> value]", () => {
|
||||
const ast: TestNode = {
|
||||
type: "Foo",
|
||||
children: [
|
||||
{ type: "Foo", msg: false },
|
||||
{ type: "Foo", msg: true },
|
||||
{ type: "Foo", msg: 1 },
|
||||
{ type: "Foo", msg: 2 },
|
||||
],
|
||||
};
|
||||
expect(testSelector(ast, "Foo[msg=true]")).toEqual(ast.children![1]);
|
||||
expect(testSelector(ast, "Foo[msg = true]")).toEqual(ast.children![1]);
|
||||
expect(testSelector(ast, "Foo[msg=false]")).toEqual(ast.children![0]);
|
||||
expect(testSelector(ast, "Foo[msg = false]")).toEqual(ast.children![0]);
|
||||
expect(testSelector(ast, "Foo[msg=1]")).toEqual(ast.children![2]);
|
||||
expect(testSelector(ast, "Foo[msg = 1]")).toEqual(ast.children![2]);
|
||||
|
||||
expect(testSelector(ast, "Foo[msg!=true]")).toEqual(ast.children![0]);
|
||||
expect(testSelector(ast, "Foo[msg != true]")).toEqual(ast.children![0]);
|
||||
expect(testSelector(ast, "Foo[msg!=false]")).toEqual(ast.children![1]);
|
||||
expect(testSelector(ast, "Foo[msg != false]")).toEqual(ast.children![1]);
|
||||
|
||||
expect(testSelector(ast, "Foo[msg<2]")).toEqual(ast.children![2]);
|
||||
expect(testSelector(ast, "Foo[msg < 2]")).toEqual(ast.children![2]);
|
||||
|
||||
expect(testSelector(ast, "Foo[msg<=1]")).toEqual(ast.children![2]);
|
||||
expect(testSelector(ast, "Foo[msg <= 1]")).toEqual(ast.children![2]);
|
||||
|
||||
expect(testSelector(ast, "Foo[msg>1]")).toEqual(ast.children![3]);
|
||||
expect(testSelector(ast, "Foo[msg > 1]")).toEqual(ast.children![3]);
|
||||
expect(testSelector(ast, "Foo[msg>=1]")).toEqual(ast.children![2]);
|
||||
expect(testSelector(ast, "Foo[msg >= 1]")).toEqual(ast.children![2]);
|
||||
});
|
||||
|
||||
Deno.test("select child: A:first-child", () => {
|
||||
const ast: TestNode = {
|
||||
type: "Foo",
|
||||
children: [
|
||||
{ type: "Foo", msg: "a" },
|
||||
{ type: "Foo", msg: "b" },
|
||||
{ type: "Foo", msg: "c" },
|
||||
{ type: "Foo", msg: "d" },
|
||||
],
|
||||
};
|
||||
expect(testSelector(ast, "Foo:first-child")).toEqual(ast.children![0]);
|
||||
expect(testSelector(ast, "Foo:last-child")).toEqual(ast.children!.at(-1));
|
||||
});
|
||||
|
||||
Deno.test("select child: A:nth-child", () => {
|
||||
const ast: TestNode = {
|
||||
type: "Bar",
|
||||
children: [
|
||||
{ type: "Foo", msg: "a" },
|
||||
{ type: "Foo", msg: "b" },
|
||||
{ type: "Foo", msg: "c" },
|
||||
{ type: "Foo", msg: "d" },
|
||||
],
|
||||
};
|
||||
expect(testSelector(ast, "Foo:nth-child(2)")).toEqual(ast.children![1]);
|
||||
expect(testSelector(ast, "Foo:nth-child(2n)")).toEqual(ast.children![1]);
|
||||
expect(testSelector(ast, "Foo:nth-child(2n + 3)")).toEqual(ast.children![2]);
|
||||
expect(testSelector(ast, "Foo:nth-child(2n - 1)")).toEqual(ast.children![0]);
|
||||
expect(testSelector(ast, ":nth-child(2n + 2 of Foo)")).toEqual(
|
||||
ast.children![1],
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("splitSelectors", () => {
|
||||
expect(splitSelectors("foo")).toEqual(["foo"]);
|
||||
expect(splitSelectors("foo, bar")).toEqual(["foo", "bar"]);
|
||||
expect(splitSelectors("foo:f(bar, baz)")).toEqual(["foo:f(bar, baz)"]);
|
||||
expect(splitSelectors("foo:f(bar, baz), foobar")).toEqual([
|
||||
"foo:f(bar, baz)",
|
||||
"foobar",
|
||||
]);
|
||||
});
|
||||
|
||||
Deno.test("mixed", () => {
|
||||
const ast: TestNode = {
|
||||
type: "Foo",
|
||||
attr: true,
|
||||
children: [
|
||||
{ type: "Foo", msg: "a" },
|
||||
{ type: "Foo", msg: "b" },
|
||||
{ type: "Foo", msg: "c" },
|
||||
{ type: "Foo", msg: "d" },
|
||||
],
|
||||
};
|
||||
expect(testSelector(ast, "Foo[attr=true] Foo")).toEqual(ast.children![0]);
|
||||
});
|
2
cli/js/40_lint_types.d.ts
vendored
2
cli/js/40_lint_types.d.ts
vendored
|
@ -116,7 +116,7 @@ export type TransformFn = (value: string) => number;
|
|||
export type VisitorFn = (node: Deno.AstNode) => void;
|
||||
|
||||
export interface CompiledVisitor {
|
||||
matcher: MatcherFn;
|
||||
matcher: (offset: number) => boolean;
|
||||
info: { enter: VisitorFn; exit: VisitorFn };
|
||||
}
|
||||
|
||||
|
|
|
@ -658,7 +658,6 @@ impl CliMainWorkerFactory {
|
|||
"40_bench.js",
|
||||
"40_jupyter.js",
|
||||
// TODO(bartlomieju): probably shouldn't include these files here?
|
||||
"40_lint_selector.js",
|
||||
"40_lint.js"
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue