/* 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()) { let 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; } let 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 */ let fields = {}; for(let 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"; } }