PCv5/app/static/scripts/filter.js

144 lines
4.0 KiB
JavaScript

/* Tokens and patterns; keep arrays in order of token numbers */
const T = {
END:-2, ERR:-1, NAME:0, COMP:1, UNARY:2, AND:3, OR:4, LPAR:5, RPAR:6, STR:7
}
const patterns = [
/[a-z]+/, /=|!=|~=|!~=/, /!/, /&&/, /\|\|/, /\(/, /\)/, /"([^"]*)"/
]
function* lex(str) {
while(str = str.trim()) {
var t = T.ERR, best = undefined;
for(const i in patterns) {
const m = str.match(patterns[i]);
if(m === null || m.index != 0 || (best && m[0].length < best[0].length)) continue;
t = i;
best = m;
}
if(t == T.ERR) throw "LEXING ERROR";
yield [t, best[best.length-1]];
str = str.slice(best[0].length);
}
/* Finish with a continuous stream of T.END */
while(true) yield [T.END, undefined];
}
class Parser {
constructor(lex) {
this.lex = lex;
[this.la, this.value] = lex.next().value;
}
/* Expect a token of type t, returns the value */
expect(t) {
const v = this.value;
if(this.la != t) throw (`SYNTAX ERROR, EXPECTED ${t}, GOT ${this.la} ${this.value}`);
[this.la, this.value] = this.lex.next().value;
return v;
}
/* filter -> END | expr END */
filter() {
if(this.la == T.END) return true;
const e = this.expr();
this.expect(T.END);
return e;
}
/* expr -> term_and | term_and OR expr */
expr() {
const left = this.term_and();
if(this.la != T.OR) return left;
this.expect(T.OR);
return {
type: "Or",
left: left,
right: this.expr(),
};
}
/* term_and -> unary | unary AND term_and */
term_and() {
const left = this.unary();
if(this.la != T.AND) return left;
this.expect(T.AND);
return {
type: "And",
left: left,
right: this.term_and(),
};
}
/* unary -> UNARY* atom */
unary() {
if(this.la == T.UNARY) return {
type: "Unary",
op: this.expect(T.UNARY),
val: this.unary(),
};
return this.atom();
}
/* atom -> NAME COMP STR | LPAR expr RPAR */
atom() {
if(this.la == T.LPAR) {
this.expect(T.LPAR);
const e = this.expr();
this.expect(T.RPAR);
return e;
}
var e = {
type: "Atom",
field: this.expect(T.NAME),
op: this.expect(T.COMP),
};
const str = this.expect(T.STR);
/* Precompile regular expressions */
if(e.op == "~=" || e.op == "!~=")
e.regex = new RegExp(str, "i");
else
e.str = str;
return e;
}
}
function ev(e, row, fields) {
switch(e.type) {
case "Atom":
const val = row.children[fields[e.field]].textContent.trim();
if(e.op == "=") return val == e.str;
if(e.op == "!=") return val != e.str;
if(e.op == "~=") return e.regex.test(val);
if(e.op == "!~=") return !e.regex.test(val);
case "Unary":
if(e.op == "!") return !ev(e.val, row, fields);
case "And":
return ev(e.left, row, fields) && ev(e.right, row, fields);
case "Or":
return ev(e.left, row, fields) || ev(e.right, row, fields);
}
}
function filter_update(input) {
const t = document.querySelector(input.parentNode.dataset.target);
const th = t.querySelectorAll("tr:first-child > th");
/* Generate the names of fields from the header */
var fields = {};
for(var i = 0; i < th.length; i++) {
const name = th[i].dataset.filter;
if(name) fields[name] = i;
}
/* Parse the filter as an expression */
const parser = new Parser(lex(input.value));
const expr = parser.filter();
/* Evaluate the expression on each row of the table */
const rows = t.querySelectorAll("tr:not(:first-child)");
for(const row of rows) {
const ok = (expr === true) || ev(expr, row, fields);
row.style.display = ok ? "table-row" : "none";
}
}