Best JavaScript code snippet using playwright-internal
filtereditor.js
Source:filtereditor.js  
1"use strict";2// The filter editor and the whole filtering subsystem of the SuperTable. Contains two filtering3// interfaces: a mouse-driven "traditional" one, and a more advanced system that uses a simple4// scripting language. Only one can be active at any given moment, but both are compiled down to a5// simple RPN "program" that is executed for every row on the table to see if it's visible or not.6// The table itself does not know anything about this, it only sees the final program.7// --------------------------------------------------------------------------------------------------8// --------------------------------------------------------------------------------------------------9// COLUMN NAME LOOKUPS10/*11Each column can have zero or more (unique) alias names that all point to the actual name.12It's just a simple text replacement system. Some column names used in the raw JSON data13are very short and cryptic, so it's nice to have easy-to-remember aliases for them.14*/15class ColumnDefinitions {16    constructor(rawDefinitions)17    {18        this.columns = new Map();19        this.aliases = new Map();20        for (const key of Object.keys(rawDefinitions)) {21            const def = rawDefinitions[key];22            this.columns.set(key, {23                type: def.type,24                flags: def.flags || 0,25            });26            if ("alias" in def) {27                for (const a of def.alias) {28                    // Each alias name must be unique. Explode loudly if they aren't.29                    if (this.aliases.has(a))30                        throw new Error(`Alias "${a}" used by columns "${this.aliases.get(a)}" and "${key}"`);31                    this.aliases.set(a, key);32                }33            }34        }35    }36    isColumn(s)37    {38        return this.columns.has(s) || this.aliases.has(s);39    }40    // Expands an alias name. If the string isn't an alias, nothing happens.41    expandAlias(s)42    {43        if (!this.aliases.has(s))44            return s;45        return this.aliases.get(s);46    }47    // Retrieves a column definition by name. Bad things will happen if the name isn't valid.48    // (Use isColumn() to validate it first, and expandAlias() to get the full name.)49    get(s)50    {51        return this.columns.get(s);52    }53    getAliases(s)54    {55        let out = new Set();56        for (const a of this.aliases)57            if (a[1] == s)58                out.add(a[0]);59        return out;60    }61};62// A custom logger that also keeps track of row and column numbers and some other metadata63class MessageLogger {64    constructor()65    {66        this.messages = [];67    }68    empty()69    {70        return this.messages.length == 0;71    }72    haveErrors()73    {74        for (const m of this.messages)75            if (m.type == "error")76                return true;77        return false;78    }79    clear()80    {81        this.messages = [];82    }83    warn(message, row=-1, col=-1, pos=-1, len=-1, extra=null)84    {85        this.messages.push({86            type: "warning",87            message: message,88            row: row,89            col: col,90            pos: pos,91            len: len,92            extra: extra,93        });94    }95    warnToken(message, token, extra=null)96    {97        this.messages.push({98            type: "warning",99            message: message,100            row: token.row,101            col: token.col,102            pos: token.pos,103            len: token.len,104            extra: extra,105        });106    }107    error(message, row=-1, col=-1, pos=-1, len=-1, extra=null)108    {109        this.messages.push({110            type: "error",111            message: message,112            row: row,113            col: col,114            pos: pos,115            len: len,116            extra: extra,117        });118    }119    errorToken(message, token, extra=null)120    {121        this.messages.push({122            type: "error",123            message: message,124            row: token.row,125            col: token.col,126            pos: token.pos,127            len: token.len,128            extra: extra,129        });130    }131};132// --------------------------------------------------------------------------------------------------133// --------------------------------------------------------------------------------------------------134// TOKENIZATION135// Raw token types136const TokenType = {137    COLUMN: "col",            // target column name138    OPERATOR: "opr",          // comparison operator139    VALUE: "val",             // comparison value (whether it's a string or number is irrelevant here)140    OPEN_PAREN: "(",141    CLOSE_PAREN: ")",142    BOOL_AND: "and",143    BOOL_OR: "or",144    BOOL_NOT: "not",145    // Used during parsing to initialize the state machine to a known state.146    // Using this anywhere else will hurt you badly.147    START: "start",148};149// Raw token flags, set by the tokenizer150const TokenFlags = {151    REGEXP: 0x01,       // This token contains a regexp string152    MULTILINE: 0x02,    // This string is multiline (can be used with regexps)153};154class Tokenizer {155    constructor()156    {157        this.columnDefinitions = null;158        this.logger = null;159        this.source = null;160        this.pos = 0;           // character position in the input stream161        this.row = 1;162        this.col = 1;163        this.startPos = 0;164        this.startRow = 1;165        this.startCol = 1;166        this.tokens = [];167    }168    addToken(type, token, flags=0)169    {170        this.tokens.push({171            type: type,172            str: token,173            flags: flags,174            // These are used in error reporting. They're not needed for anything else.175            row: this.startRow,176            col: this.startCol,177            pos: this.startPos,178            len: this.pos - this.startPos,179        });180    }181    done()182    {183        return this.pos >= this.source.length;184    }185    peek()186    {187        if (this.done())188            return null;189        return this.source[this.pos];190    }191    read()192    {193        const c = this.source[this.pos++];194        if (c == "\n") {195            this.row++;196            this.col = 0;197        }198        if (c != "\r") {199            // Transparently treat \n\r (or \r\n) newlines as just \n200            this.col++;201        }202        return c;203    }204    match(expected)205    {206        if (this.done())207            return false;208        if (this.source[this.pos] != expected)209            return false;210        this.pos++;211        this.col++;     // don't call match("\r") if you want the column number to be correct212        return true;213    }214    comment()215    {216        while (true) {217            if (this.peek() == "\n" || this.done())218                break;219            this.read();220        }221    }222    content(initial)223    {224        let quoted = false,225            escape = false,226            token = "",227            flags = 0;228        if (`"'/`.includes(initial)) {229            // We have three types of values/strings here: unquoted, quoted and regexp.230            // The only difference between quoted and regexp strings is the terminating231            // character. For unquoted strings, any unescaped whitespace or characters232            // that are actually part of the language syntax end the string.233            quoted = true;234            if (initial == "/")235                flags |= TokenFlags.REGEXP;236        } else {237            // Start the token with a non-quote character238            token += initial;239        }240        while (true) {241            if (this.done()) {242                if (quoted) {243                    this.logger.error("unterminated_string",244                                      this.startRow, this.startCol, this.startPos, this.col - this.startCol);245                    return true;246                }247                break;248            }249            // Can't use read() here, otherwise we can end up reading one character too far250            const c = this.peek();251            if (c == "\\") {252                escape = true;253                this.read();254                continue;255            }256            if (escape) {257                // Process escape codes258                switch (c) {259                    case "n":260                        token += "\n";261                        flags |= TokenFlags.MULTILINE;262                        break;263                    case "r":264                        token += "\r";265                        break;266                    case "t":267                        token += "\t";268                        break;269                    default:270                        // Unknown escape, pass it through as-is271                        token += `\\${c}`;272                        break;273                }274            } else {275                if (quoted) {276                    // For quoted strings, the initial character also ends the string277                    if (c == initial) {278                        this.read();279                        break;280                    }281                    if (c == "\n")282                        flags |= TokenFlags.MULTILINE;283                } else if (" \n\r\t()&|<>=!#".includes(c)) {284                    // Only these can end an unquoted string as they're part of the language itself285                    break;286                }287                token += c;288            }289            this.read();290            escape = false;291        }292        if (escape) {293            // Unfinished escape sequence (+1 to skip the escape character)294            this.logger.error("unexpected_end",295                              this.startRow, this.startCol, this.startPos, this.col - this.startCol + 1);296            return true;297        }298        // Quoted strings are hardcoded to always be values, but unquoted299        // strings are either values or column names300        if (!quoted && this.columnDefinitions.isColumn(token))301            this.addToken(TokenType.COLUMN, this.columnDefinitions.expandAlias(token), flags);302        else this.addToken(TokenType.VALUE, token, flags);303        return true;304    }305    token()306    {307        // Remember the starting position, so we'll know exact token locations and lengths308        this.startPos = this.pos;309        this.startRow = this.row;310        this.startCol = this.col;311        const c = this.read();312        switch (c) {313            case " ":314            case "\t":315            case "\n":316            case "\r":317                // Either already handled in read(), or we can skip these318                break;319            case "#":320                this.comment();321                break;322            case "=":323                // equality, permit "=" and "=="324                if (this.match("="))325                    this.addToken(TokenType.OPERATOR, "=");326                else this.addToken(TokenType.OPERATOR, "=");327                break;328            case "!":329                // ! (unary negation), != (inequality) or !! (database field presence check)330                if (this.match("="))331                    this.addToken(TokenType.OPERATOR, "!=");332                else if (this.match("!"))333                    this.addToken(TokenType.OPERATOR, "!!");334                else this.addToken(TokenType.BOOL_NOT, "!");335                break;336            case "<":337                // < or <=338                if (this.match("="))339                    this.addToken(TokenType.OPERATOR, "<=");340                else this.addToken(TokenType.OPERATOR, "<");341                break;342            case ">":343                // > or >=344                if (this.match("="))345                    this.addToken(TokenType.OPERATOR, ">=");346                else this.addToken(TokenType.OPERATOR, ">");347                break;348            case "&":349                // && (and)350                if (this.match("&"))351                    this.addToken(TokenType.BOOL_AND, "&&");352                else this.logger.error("unknown_operator", this.startRow, this.startCol, this.startPos, 2);353                break;354            case "|":355                // || (or)356                if (this.match("|"))357                    this.addToken(TokenType.BOOL_OR, "||");358                else this.logger.error("unknown_operator", this.startRow, this.startCol, this.startPos, 2);359                break;360            case "(":361                this.addToken(TokenType.OPEN_PAREN, "(");362                break;363            case ")":364                this.addToken(TokenType.CLOSE_PAREN, ")");365                break;366            default:367                // Could be a column name or a string, or a number, etc.368                if (this.content(c))369                    break;370                // I don't currently know any syntactical structure that could end up here371                this.logger.error("syntax_error", this.startRow, this.startCol, this.startPos, 1);372                break;373        }374    }375    tokenize(logger, columns, source)376    {377        this.columnDefinitions = columns;378        this.logger = logger;379        this.pos = 0;380        this.source = source;381        this.row = 1;382        this.col = 1;383        this.tokens = [];384        while (!this.done())385            this.token();386    }387};388// --------------------------------------------------------------------------------------------------389// --------------------------------------------------------------------------------------------------390// SYNTAX ANALYZER391/*392This isn't a recursive-descent parser. It just looks at the current token and the token before393it and makes all the decisions based on those. This parser is only used to validate the filter394string and move all comparisons into their own array. No AST is build. The remaining tokens395are stored in a new array that is then fed to the Shunting Yard algorithm.396*/397class Parser {398    constructor()399    {400        this.columnDefinitions = null;401        this.logger = null;402        this.tokens = [];403        this.previous = TokenType.START;404        this.pos = 0;405        this.comparisons = [];406        this.output = [];407    }408    prev()409    {410        for (let i = 0; i < arguments.length; i++)411            if (this.previous == arguments[i])412                return true;413        return false;414    }415    peek()416    {417        if (this.pos + 1 >= this.tokens.length)418            return false;419        const type = this.tokens[this.pos + 1].type;420        for (let i = 0; i < arguments.length; i++)421            if (type == arguments[i])422                return true;423        return false;424    }425    // Adds a new comparison or finds the existing one. Always returns an index426    // to a comparison you can use to look up whatever you passed to this.427    comparison(column, operator, value)428    {429        for (let i = 0; i < this.comparisons.length; i++) {430            const c = this.comparisons[i];431            if (c.column.str == column.str &&432                c.operator.str == operator.str &&433                c.value.str == value.str &&434                c.value.flags == value.flags)435                return i;436        }437        this.comparisons.push({438            column: column,439            operator: operator,440            value: value,441        });442        return this.comparisons.length - 1;443    }444    parse(logger, columns, tokens, lastRow, lastCol)445    {446        this.columnDefinitions = columns;447        this.logger = logger;448        this.tokens = [...tokens];449        this.previous = TokenType.START;450        this.pos = 0;451        this.output = [];452        this.comparisons = [];453        let errors = false,454            nesting = 0,455            columnIndex = -1,456            operatorIndex = -1;457        while (this.pos < this.tokens.length) {458            const t = this.tokens[this.pos];459            switch (t.type) {460                case TokenType.START:461                    break;462                case TokenType.COLUMN:463                    if (this.prev(TokenType.START, TokenType.OPEN_PAREN,464                                  TokenType.BOOL_AND, TokenType.BOOL_OR, TokenType.BOOL_NOT)) {465                        // If there is no operator after this, then this is a shorthand "!!"466                        // syntax. Expand the shorthand by splicing two extra tokens into467                        // the stream after the column name. This is really ugly, but it works468                        // and it really does make writing filter expressions nicer.469                        if (this.pos + 1 >= this.tokens.length ||470                            this.peek(TokenType.BOOL_AND, TokenType.BOOL_OR, TokenType.CLOSE_PAREN)) {471                            let opr = {...t},472                                val = {...t};473                            opr.type = TokenType.OPERATOR;474                            opr.flags = 0;475                            opr.str = "!!";476                            opr.len = 2;477                            // Exists or not?478                            val.type = TokenType.VALUE;479                            val.flags = 0;480                            val.str = this.prev(TokenType.BOOL_NOT) ? "0" : "1";481                            val.len = 1;482                            if (this.prev(TokenType.BOOL_NOT)) {483                                // In this case, the negation before the colum name is part484                                // of the coercion. We don't want to negate the result of485                                // this test, so remove the negation from the opcodes.486                                // Ugly ugly ugly.487                                this.output.pop();488                            }489                            this.tokens.splice(this.pos + 1, 0, opr);490                            this.tokens.splice(this.pos + 2, 0, val);491                            // Then continue like nothing happened...492                        }493                        columnIndex = this.pos;494                        operatorIndex = -1;495                        break;496                    }497                    this.logger.errorToken("unexpected_column_name", t);498                    errors = true;499                    break;500                case TokenType.OPERATOR:501                    if (this.previous == TokenType.COLUMN) {502                        operatorIndex = this.pos;503                        break;504                    }505                    this.logger.errorToken("expected_column_name", t);506                    errors = true;507                    columnIndex = -1;508                    operatorIndex = -1;509                    break;510                case TokenType.VALUE:511                    if (this.previous == TokenType.OPERATOR) {512                        if (columnIndex == -1) {513                            this.logger.errorToken("expected_operator", t);514                            errors = true;515                            break;516                        }517                        const index = this.comparison(this.tokens[columnIndex],518                                                      this.tokens[operatorIndex],519                                                      this.tokens[this.pos]);520                        this.output.push([index, -1]);521                        columnIndex = -1;522                        operatorIndex = -1;523                        break;524                    }525                    if (this.previous == TokenType.COLUMN) {526                        this.logger.errorToken("expected_operator", t);527                        errors = true;528                    } else {529                        this.logger.errorToken("expected_column_name", t);530                        errors = true;531                    }532                    columnIndex = -1;533                    operatorIndex = -1;534                    break;535                case TokenType.BOOL_AND:536                    if (!this.prev(TokenType.VALUE, TokenType.CLOSE_PAREN)) {537                        this.logger.errorToken("expected_value_rpar", t);538                        errors = true;539                        break;540                    }541                    this.output.push(["&", 1]);542                    break;543                case TokenType.BOOL_OR:544                    if (this.previous == TokenType.COLUMN) {545                        this.logger.errorToken("expected_operator", t);546                        errors = true;547                        break;548                    }549                    if (!this.prev(TokenType.VALUE, TokenType.CLOSE_PAREN)) {550                        this.logger.errorToken("expected_value_rpar", t);551                        errors = true;552                        break;553                    }554                    this.output.push(["|", 2]);555                    break;556                case TokenType.BOOL_NOT:557                    if (!this.prev(TokenType.START, TokenType.BOOL_AND, TokenType.BOOL_OR,558                                   TokenType.BOOL_NOT, TokenType.OPEN_PAREN)) {559                        this.logger.errorToken("unexpected_negation", t);560                        errors = true;561                        break;562                    }563                    this.output.push(["!", 0]);564                    break;565                case TokenType.OPEN_PAREN:566                    if (!this.prev(TokenType.START, TokenType.VALUE,567                                   TokenType.BOOL_AND, TokenType.BOOL_OR, TokenType.BOOL_NOT,568                                   TokenType.OPEN_PAREN)) {569                        this.logger.errorToken("unexpected_lpar", t);570                        errors = true;571                        break;572                    }573                    nesting++;574                    this.output.push(["(", -1]);575                    break;576                case TokenType.CLOSE_PAREN:577                    if (!this.prev(TokenType.VALUE, TokenType.CLOSE_PAREN)) {578                        this.logger.errorToken("unexpected_rpar", t);579                        errors = true;580                        break;581                    }582                    if (nesting < 1) {583                        this.logger.errorToken("unbalanced_nesting", t);584                        errors = true;585                        return false;586                    }587                    nesting--;588                    this.output.push([")", -1]);589                    break;590                default:591                    this.logger.errorToken("syntax_error", t);592                    errors = true;593                    return false;594            }595            this.previous = t.type;596            this.pos++;597        }598        if (!this.prev(TokenType.START, TokenType.VALUE, TokenType.CLOSE_PAREN)) {599            this.logger.error("unexpected_end", lastRow, lastCol, this.pos);600            return false;601        }602        if (nesting != 0) {603            this.logger.error("unbalanced_nesting", lastRow, lastCol, this.pos);604            return false;605        }606        return !errors;607    }608};609// --------------------------------------------------------------------------------------------------610// --------------------------------------------------------------------------------------------------611// RPN CODE GENERATION612/*613This is a modified Shunting Yard algorithm. It has been changed to only accept logical AND, OR614and NOT operators instead of addition and such. If you think about it, a "||" is "plus" because615either side being non-zero is enough, and "&&" is multiplication because both sides must be616non-zero. "!" is multiplying by -1. And for numbers, you only have 0 and 1 because a comparison617either succeeds (1) or fails (0). An unmodified Shunting Yard algorithm can therefore handle618logical expressions if you replace the operators and numbers.619*/620// Operators, their precedences and associativities621const Associativity = {622    LEFT: 0,623    RIGHT: 1624};625// If this table is reordered, you must change the parser above to match the new order626const SHUNTING_OPERATORS = [627    { op: '!', precedence: 4, assoc: Associativity.LEFT },628    { op: '&', precedence: 3, assoc: Associativity.LEFT },629    { op: '|', precedence: 2, assoc: Associativity.LEFT },630];631class CodeGenerator {632    compile(input)633    {634        let output = [];635        let ops = [],       // operator stack636            pos = 0;637        // This was converted almost verbatim from the pseudocode given on the Wikipedia's638        // page about the Shunting Yard algorithm. If you want comments, read the page.639        while (pos < input.length) {640            const tok = input[pos];641            // Load a comparison result. All numbers are indexes to the results table.642            if (typeof(tok[0]) == "number") {643                output.push(tok);644                pos++;645                continue;646            }647            // Process an operator648            // tok[0] = operator token, tok[1] = index to OPERATORS[]649            switch (tok[0]) {650                case "!":651                case "&":652                case "|":653                {654                    const opIndex = tok[1],655                          thisOp = SHUNTING_OPERATORS[opIndex];656                    while (ops.length > 0) {657                        const top = ops[ops.length - 1];658                        if (top[0] != "(" &&659                            (SHUNTING_OPERATORS[top[1]].precedence > thisOp.precedence ||660                                (SHUNTING_OPERATORS[top[1]].precedence == thisOp.precedence &&661                                    thisOp.assoc == Associativity.LEFT))) {662                            output.push(top);663                            ops.pop();664                        } else break;665                    }666                    ops.push(tok);667                    break;668                }669                case "(":670                    ops.push(tok);671                    break;672                case ")": {673                    while (ops.length > 0) {674                        const top = ops[ops.length - 1];675                        if (top[0] != "(") {676                            output.push(top);677                            ops.pop();678                        } else break;679                    }680                    if (ops.length == 0)681                        throw new Error(`Mismatched parenthesis in Shunting Yard (1)`);682                    ops.pop();683                    break;684                }685                default:686                    throw new Error(`Unknown token "${tok[0]}"`);687            }688            pos++;689        }690        while (ops.length > 0) {691            const top = ops[ops.length - 1];692            if (top[0] == "(")693                throw new Error(`Mismatched parenthesis in Shunting Yard (2)`);694            output.push(top);695            ops.pop();696        }697        return output;698    }699};700// --------------------------------------------------------------------------------------------------701// --------------------------------------------------------------------------------------------------702// COMPARISON EVALUATORS703/*704The RPN program contains only binary logic. These evaluators actually figure out if the logic705values being tested are true (1) or false (0). They're "compiled" only once, but they must be706re-evaluated again for every row that is being checked.707*/708// All known operators709const KNOWN_OPERATORS = new Set(["=", "!=", "<", "<=", ">", ">=", "!!"]);710// Which operators can be used with different column types? For example, strings cannot711// be compared with < or > (actually they can be, but it won't result in what you expect).712const ALLOWED_OPERATORS = {713    [ColumnType.BOOL]: new Set(["=", "!=", "!!"]),714    [ColumnType.NUMERIC]: new Set(["=", "!=", "<", "<=", ">", ">=", "!!"]),715    [ColumnType.UNIXTIME]: new Set(["=", "!=", "<", "<=", ">", ">=", "!!"]),716    [ColumnType.STRING]: new Set(["=", "!=", "!!"]),717};718// Absolute ("YYYY-MM-DD HH:MM:SS") and relative ("-7d") time matchers719const ABSOLUTE_TIME = /^(?<year>\d{4})-?(?<month>\d{2})?-?(?<day>\d{2})? ?(?<hour>\d{2})?:?(?<minute>\d{2})?:?(?<second>\d{2})?$/,720      RELATIVE_TIME = /^(?<sign>(\+|-))?(?<value>(\d*))(?<unit>(s|h|d|w|m|y))?$/;721// Storage size matcher ("10M", "1.3G", "50%10G" and so on).722// XY or Z%XY, where X=value, Y=optional unit, Z=percentage. Permit floats, written with723// either commas or dots.724const STORAGE_PARSER = /^((?<percent>(([0-9]*[.,])?[0-9]+))%)?(?<value>(([0-9]*[.,])?[0-9]+))(?<unit>([a-zA-Z]))?$/;725// String->float, understands dots and commas (so locales that use dots or commas for726// thousands separating will work correctly)727function floatize(str)728{729    return parseFloat(str.replace(",", "."));730}731// Converts a "YYYY-MM-DD HH:MM:SS" into a Date object, but the catch is that you can omit732// the parts you don't need, ie. the more you specify, the more accurate it gets. Giving733// "2021" to this function returns 2021-01-01 00:00:00, "2021-05" returns 2021-05-01 00:00:00,734// "2021-05-27 19:37" returns 2021-05-27 19:37:00 and so on. The other format this function735// understands are relative times: if the input value is an integer, then it is added to the736// CURRENT time and returned. Negative values point to the past, positive point to the future.737function parseAbsoluteOrRelativeDate(str)738{739    let match = ABSOLUTE_TIME.exec(str);740    if (match !== null) {741        // Parse an absolute datetime742        // This should cut off after the first missing element (ie. if you omit the day,743        // then hours, minutes and seconds should not be set), but the regexp won't match744        // it then, so no harm done.745        const year = parseInt(match.groups.year, 10),746              month = parseInt(match.groups.month || "1", 10) - 1,747              day = parseInt(match.groups.day || "1", 10),748              hour = parseInt(match.groups.hour || "0", 10),749              minute = parseInt(match.groups.minute || "0", 10),750              second = parseInt(match.groups.second || "0", 10);751        let d = null;752        try {753            d = new Date();754            d.setFullYear(year);755            d.setMonth(month);756            d.setDate(day);757            d.setHours(hour);758            d.setMinutes(minute);759            d.setSeconds(second);760            d.setMilliseconds(0);       // the database values have only 1-second granularity761        } catch (e) {762            console.error(`parseAbsoluteOrRelativeDate(): can't construct an absolute Date object from "${str}":`);763            console.error(e);764            return null;765        }766        return d;767    }768    match = RELATIVE_TIME.exec(str);769    if (match === null) {770        // Don't know what this string means771        console.error(`parseAbsoluteOrRelativeDate(): "${str}" is neither absolute nor relative date`);772        return null;773    }774    // Parse a relative datetime775    let value = parseInt(match.groups.value, 10);776    // Scale777    switch (match.groups.unit) {778        default:779        case "s":780            // Seconds are the default, do nothing781            break;782        case "h":783            value *= 60 * 60;               // 1 hour in seconds784            break;785        case "d":786            value *= 60 * 60 * 24;          // 1 day in seconds787            break;788        case "w":789            value *= 60 * 60 * 24 * 7;      // 1 week in seconds790            break;791        case "m":792            value *= 60 * 60 * 24 * 30;     // 1 (30-day) month in seconds793            break;794        case "y":795            value *= 60 * 60 * 24 * 365;    // 1 year with 365 days (no leap year checks here)796            break;797    }798    // Sign799    if (match.groups.sign !== undefined) {800        if (match.groups.sign == "-")801            value *= -1;802        // Treat all other signs are +, even unknown ones (there shouldn't be any, since the803        // regexp rejects them)804    }805    let d = new Date();806    try {807        d.setSeconds(d.getSeconds() + value);808        d.setMilliseconds(0);       // the database values have only 1-second granularity809    } catch (e) {810        console.error(`parseAbsoluteOrRelativeDate(): can't construct a relative Date object from "${str}":`);811        console.error(e);812        return null;813    }814    return d;815}816class ComparisonCompiler {817    constructor()818    {819        this.logger = null;820    }821    // Parses and expands a value with unit, like "10M" or "10G" to a full number. Optionally822    // calculates a percentage value, like "50%10M" is equivalent to writing "5M".823    __parseStorage(valueToken)824    {825        const storage = STORAGE_PARSER.exec(valueToken.str.trim());826        if (storage === null) {827            // Just a number, no units or percentages828            try {829                const v = floatize(valueToken.str);830                if (isNaN(v)) {831                    console.error(`__parseStorage(): "${valueToken.str}" is not a valid number`);832                    this.logger.errorToken("not_a_number", valueToken);833                    return null;834                }835                return v;836            } catch (e) {837                console.error(`__parseStorage(): "${valueToken.str}" cannot be parsed as a float`);838                this.logger.errorToken("not_a_number", valueToken);839                return null;840            }841        }842        // The base value. It's easier if we treat everything here as a float.843        let value = 0;844        try {845            value = floatize(storage.groups.value);846        } catch (e) {847            console.error(`__parseStorage(): "${storage.groups.value}" cannot be parsed as a float`);848            this.logger.errorToken("not_a_number", valueToken);849            return null;850        }851        // Scale unit852        let unit = storage.groups.unit;853        if (unit === undefined || unit === null)854            unit = "B";855        switch (unit) {856            case "B":857                // bytes are the default, do nothing858                break;859            case "K":860                value *= 1024;861                break;862            case "M":863                value *= 1024 * 1024;864                break;865            case "G":866                value *= 1024 * 1024 * 1024;867                break;868            case "T":869                value *= 1024 * 1024 * 1024 * 1024;870                break;871            default:872                console.error(`__parseStorage(): invalid storage unit "${unit}"`);873                this.logger.errorToken("invalid_storage_unit", valueToken, unit);874                return null;875        }876        // Percentage877        let percent = storage.groups.percent;878        if (percent) {879            percent = Math.min(Math.max(floatize(percent), 0.0), 100.0);880            value *= percent / 100.0;881        }882        return value;883    }884    __compileBoolean(columnToken, operatorToken, valueToken)885    {886        return {887            column: columnToken.str,888            operator: operatorToken.str,889            value: ["1", "t", "y", "true", "yes", "on"].includes(valueToken.str.toLowerCase()),890            regexp: false891        };892    }893    __compileNumeric(columnToken, operatorToken, valueToken)894    {895        const colDef = this.columnDefs.get(this.columnDefs.expandAlias(columnToken.str));896        let value = undefined;897        if (colDef.flags & ColumnFlag.F_STORAGE) {898            // Parse a storage specifier, like "5M" or "10G"899            value = this.__parseStorage(valueToken);900            if (value === null)901                return null;902        } else {903            try {904                if (valueToken.str.indexOf(".") == -1 && valueToken.str.indexOf(",") == -1) {905                    // Integer906                    value = parseInt(valueToken.str, 10);907                } else {908                    // Float909                    value = floatize(valueToken.str);910                }911                if (isNaN(value))912                    throw new Error("not an integer");913            } catch (e) {914                console.error(`ComparisonCompiler::compile(): can't parse a number: ${e.message}`);915                console.error(e);916                this.logger.errorToken("not_a_number", valueToken);917                return null;918            }919        }920        return {921            column: columnToken.str,922            operator: operatorToken.str,923            value: value,924            regexp: false925        };926    }927    __compileUnixtime(columnToken, operatorToken, valueToken)928    {929        const out = parseAbsoluteOrRelativeDate(valueToken.str);930        if (out === null) {931            this.logger.errorToken("unparseable_time", valueToken);932            return false;933        }934        return {935            column: columnToken.str,936            operator: operatorToken.str,937            value: out.getTime() / 1000,        // convert to seconds938            regexp: false939        }940    }941    __compileString(columnToken, operatorToken, valueToken)942    {943        let regexp = false,944            value = undefined;945        if (valueToken.flags & TokenFlags.REGEXP) {946            // Compile a regexp947            try {948                value = new RegExp(valueToken.str.trim(),949                                   valueToken.flags & TokenFlags.MULTILINE ? "miu" : "iu"),950                regexp = true;951            } catch (e) {952                console.error(`ComparisonCompiler::compile(): regexp compilation failed: ${e.message}`);953                this.logger.errorToken("invalid_regexp", valueToken, e.message);954                return null;955            }956        } else {957            // A plain string, use as-is958            value = valueToken.str;959        }960        return {961            column: columnToken.str,962            operator: operatorToken.str,963            value: value,964            regexp: regexp965        };966    }967    // Takes a raw comparison (made of three tokens) and "compiles" it (ie. verifies the data968    // types, the comparison operator and the value, and converts the stringly-typed value into969    // "native" JavaScript type). Returns null if it failed.970    compile(logger, columns, columnToken, operatorToken, valueToken)971    {972        this.logger = logger;973        this.columnDefs = columns;974        // Validate the column and the operator975        if (!this.columnDefs.isColumn(columnToken.str)) {976            console.error(`ComparisonCompiler::compile(): unknown column "${columnToken.str}"`);977            this.logger.errorToken("unknown_column", columnToken);978            return null;979        }980        if (!KNOWN_OPERATORS.has(operatorToken.str)) {981            console.error(`ComparisonCompiler::compile(): invalid operator "${operatorToken.str}"`);982            this.logger.errorToken("invalid_operator", operatorToken);983            return null;984        }985        const colDef = this.columnDefs.get(this.columnDefs.expandAlias(columnToken.str));986        if (!ALLOWED_OPERATORS[colDef.type].has(operatorToken.str)) {987            console.error(`ComparisonCompiler::compile(): operator "${operatorToken.str}" cannot be used with column type "${colDef.type}"`);988            this.logger.errorToken("incompatible_operator", operatorToken);989            return null;990        }991        if (typeof(valueToken.str) != "string") {992            console.error(`ComparisonCompiler::compile(): value "${valueToken.str}" is not a string`);993            this.logger.errorToken("invalid_value", valueToken);994            return null;995        }996        // Interpret the comparison value and convert it into a "native" type997        if (operatorToken.str == "!!") {998            // Special case: always treat the value as boolean, regardless of what the column is999            return this.__compileBoolean(columnToken, operatorToken, valueToken);1000        }1001        switch (colDef.type) {1002            case ColumnType.BOOL:1003                return this.__compileBoolean(columnToken, operatorToken, valueToken);1004            case ColumnType.NUMERIC:1005                return this.__compileNumeric(columnToken, operatorToken, valueToken);1006            case ColumnType.UNIXTIME:1007                return this.__compileUnixtime(columnToken, operatorToken, valueToken);1008            case ColumnType.STRING:1009                return this.__compileString(columnToken, operatorToken, valueToken);1010            default:1011                console.error(`ComparisonCompiler::compile(): unhandled column type "${colDef.type}"`);1012                this.logger.errorToken("unknown_error", columnToken);1013                return null;1014        }1015    }1016};1017// Executes a comparison. Returns true if the comparison matches the tested value.1018// Can (sorta) deal with NULL and undefined values.1019function __compareSingleValue(cmp, value)1020{1021    if (cmp.operator != "!!") {1022        if (value === undefined || value === null) {1023            // Treat missing values as false. Actually comparing them with something1024            // is nonsensical. Use the "!!" operator to test if those values actually1025            // are present in the data.1026            return false;1027        }1028    }1029    switch (cmp.operator) {1030        case "=":1031            return cmp.regexp ? cmp.value.test(value) : cmp.value === value;1032        case "!=":1033            return !(cmp.regexp ? cmp.value.test(value) : cmp.value === value);1034        case "<":1035            return value < cmp.value;1036        case "<=":1037            return value <= cmp.value;1038        case ">":1039            return value > cmp.value;1040        case ">=":1041            return value >= cmp.value;1042        case "!!":1043            return cmp.value != (value === null || value === undefined);1044        default:1045            throw new Error(`compare(): unknown operator "${cmp.operator}"`);1046    }1047}1048// Executes a single comparison against a row value. Deals with arrays and NULL/undefined1049// data. Returns true if the comparison matched.1050function compareRowValue(value, cmp)1051{1052    if (value !== undefined && value !== null && Array.isArray(value)) {1053        // Loop over multiple values. Currently only string arrays are supported,1054        // because no other types of arrays exists in the database.1055        if (cmp.operator == "=") {1056            for (const v of value)1057                if (__compareSingleValue(cmp, v))1058                    return true;1059            return false;1060        }1061        // Assume "!=" because there are only two usable operators with strings1062        for (const v of value)1063            if (!__compareSingleValue(cmp, v))1064                return false;1065        return true;1066    }1067    // Just one value1068    return __compareSingleValue(cmp, value);1069}1070// Runs the filter program and returns true if the row matches1071function evaluateFilter(program, comparisonResults)1072{1073    let stack = [],1074        a, b;1075    // a simple RPN evaluator1076    for (const instr of program) {1077        switch (instr[0]) {1078            case "!":1079                if (stack.length < 1)1080                    throw new Error("stack underflow while evaluating a logical NOT");1081                a = stack.pop();1082                stack.push(!a);1083                break;1084            case "&":1085                if (stack.length < 2)1086                    throw new Error("stack underflow while evaluating a logical AND");1087                a = stack.pop();1088                b = stack.pop();1089                stack.push(a & b);1090                break;1091            case "|":1092                if (stack.length < 2)1093                    throw new Error("stack underflow while evaluating a logical OR");1094                a = stack.pop();1095                b = stack.pop();1096                stack.push(a | b);1097                break;1098            default:1099                // load comparison result1100                stack.push(comparisonResults[instr[0]]);1101                break;1102        }1103    }1104    return stack[0];1105}1106// --------------------------------------------------------------------------------------------------1107// --------------------------------------------------------------------------------------------------1108// THE FILTER EDITOR USER INTERFACE1109// All known operators and which column types they can be used with1110const OPERATORS = {1111    "=": {1112        allowed: new Set([ColumnType.BOOL, ColumnType.NUMERIC, ColumnType.UNIXTIME, ColumnType.STRING]),1113        multiple: true,1114    },1115    "!=": {1116        allowed: new Set([ColumnType.BOOL, ColumnType.NUMERIC, ColumnType.UNIXTIME, ColumnType.STRING]),1117        multiple: true,1118    },1119    "<": {1120        allowed: new Set([ColumnType.NUMERIC, ColumnType.UNIXTIME]),1121        multiple: false,1122    },1123    "<=": {1124        allowed: new Set([ColumnType.NUMERIC, ColumnType.UNIXTIME]),1125        multiple: false,1126    },1127    ">": {1128        allowed: new Set([ColumnType.NUMERIC, ColumnType.UNIXTIME]),1129        multiple: false,1130    },1131    ">=": {1132        allowed: new Set([ColumnType.NUMERIC, ColumnType.UNIXTIME]),1133        multiple: false,1134    },1135    // interval (closed)1136    "[]": {1137        allowed: new Set([ColumnType.NUMERIC, ColumnType.UNIXTIME]),1138        multiple: true,1139    },1140    // reverse interval (closed)1141    "![]": {1142        allowed: new Set([ColumnType.NUMERIC, ColumnType.UNIXTIME]),1143        multiple: true,1144    },1145};1146const ColumnTypeStrings = {1147    [ColumnType.BOOL]: "boolean",1148    [ColumnType.NUMERIC]: "numeric",1149    [ColumnType.UNIXTIME]: "unixtime",1150    [ColumnType.STRING]: "string",1151};1152function makeRandomID()1153{1154    const CHARS = "abcdefghijklmnopqrstuvwxyz";1155    let out = "";1156    for (let i = 0; i < 20; i++)1157        out += CHARS.charAt(Math.floor(Math.random() * CHARS.length));1158    return out;1159}1160let filterID_ = 1;1161function nextFilterID()1162{1163    return filterID_++;1164}1165function hideElements()1166{1167    for (let i = 0; i < arguments.length; i++)1168        arguments[i].classList.add("hidden");1169}1170function showElements()1171{1172    for (let i = 0; i < arguments.length; i++)1173        arguments[i].classList.remove("hidden");1174}1175function humanOperatorName(operator)1176{1177    switch (operator) {1178        case "=": return "=";1179        case "!=": return "â ";1180        case "<": return "<";1181        case "<=": return "â¤";1182        case ">": return ">";1183        case ">=": return "â¥";1184        case "[]": return _tr("tabs.filtering.pretty.interval");1185        case "![]": return _tr("tabs.filtering.pretty.not_interval");1186        default:1187            throw new Error(`humanOperatorName(): invalid operator "${operator}"`);1188    }1189}1190function getDefaultValue(definition)1191{1192    switch (definition.type) {1193        case ColumnType.BOOL:1194            return true;1195        case ColumnType.NUMERIC:1196            if (definition.flags & ColumnFlag.F_STORAGE)1197                return "0M";1198            return 0;1199        case ColumnType.STRING:1200            return "";1201        case ColumnType.UNIXTIME:1202            return 0;1203        default:1204            throw new Error("getDefaultValue(): invalid column type");1205    }1206}1207// Single editable filter1208class EditableFilter {1209constructor()1210{1211    this.active = false;1212    this.column = null;1213    this.operator = null;1214    this.values = [];1215    // Current data being edited (the original data is not overwritten until "Save" is pressed)1216    this.editColumn = null;1217    this.editOperator = null;1218    this.editValues = null;1219    // True if this is a brand new filter that hasn't been saved yet. Changes how some1220    // operations work (or don't work).1221    this.isNew = false;1222    // Editor child class (see below)1223    this.editor = null;1224}1225beginEditing()1226{1227    this.editColumn = this.column;1228    this.editOperator = this.operator;1229    this.editValues = [...this.values];1230    // The editor is created elsewhere1231}1232finishEditing()1233{1234    // Overwrite old values1235    this.column = this.editColumn;1236    this.operator = this.editOperator;1237    this.values = this.editor.getData();1238}1239cancelEditing()1240{1241    this.editColumn = null;1242    this.editOperator = null;1243    this.editValues = null;1244    // The editor is destroyed elsewhere1245}1246// Parses a "raw" filter stored as [active?, column, operator, value1, value2, ... valueN].1247// Returns true if OK.1248load(raw, columnDefinitions)1249{1250    if (!Array.isArray(raw) || raw.length < 4) {1251        console.error(`EditableFilter::fromRaw(): invalid/incomplete raw filter:`);1252        console.error(raw);1253        return false;1254    }1255    // The column must be valid. We can tolerate/fix almost everything else, but not this.1256    if (!(raw[1] in columnDefinitions)) {1257        console.warn(`EditableFilter::fromRaw(): column "${raw[1]}" is not valid`);1258        return false;1259    }1260    this.active = (raw[0] === true || raw[0] === 1) ? 1 : 0;1261    this.column = raw[1];1262    this.operator = raw[2];1263    this.values = raw.slice(3);1264    // Reset invalid operators to "=" because it's the least destructive of them all,1265    // and I'd wager that most filters are simple equality checks1266    if (!(this.operator in OPERATORS)) {1267        console.warn(`EditableFilter::fromRaw(): operator "${this.operator}" is not valid, resetting it to "="`);1268        this.operator = "=";1269    }1270    // Is the operator usable with this column type?1271    const opDef = OPERATORS[this.operator],1272          colDef = columnDefinitions[this.column];1273    if (!opDef.allowed.has(colDef.type)) {1274        console.warn(`EditableFilter::fromRaw(): operator "${this.operator}" cannot be used with ` +1275                     `column type "${ColumnTypeStrings[colDef.type]}" (column "${this.column}"), ` +1276                     `resetting to "="`);1277        this.operator = "=";1278    }1279    // Handle storage units. Remove invalid values.1280    if (colDef.flags & ColumnFlag.F_STORAGE) {1281        let proper = [];1282        // Remove invalid entries1283        for (const v of this.values) {1284            try {1285                const m = STORAGE_PARSER.exec(v.toString().trim());1286                if (m !== null) {1287                    const unit = (m.groups.unit === undefined || m.groups.unit === null) ? "B" : m.groups.unit;1288                    proper.push(`${m.groups.value}${unit}`);1289                }1290            } catch (e) {1291                console.error(e);1292                continue;1293            }1294        }1295        this.values = proper;1296    }1297    // Check time strings1298    if (colDef.type == ColumnType.UNIXTIME) {1299        let proper = [];1300        for (const v of this.values) {1301            try {1302                if (parseAbsoluteOrRelativeDate(v) !== null)1303                    proper.push(v);1304            } catch (e) {1305                console.error(e);1306                continue;1307            }1308        }1309        this.values = proper;1310    }1311    if (this.values.length == 0) {1312        // Need to do this check again, because we might have altered the values1313        console.error(`EditableFilter::fromRaw(): filter has no values at all`);1314        return false;1315    }1316    // Ensure there's the required number of values for this operator1317    if (this.operator == "[]" || this.operator == "![]") {1318        if (this.values.length == 1) {1319            console.warn(`EditableFilter::fromRaw(): need more than one value, duplicating the single value`);1320            this.values.push(this.values[0]);1321        } else if (this.values.length > 2) {1322            console.warn(`EditableFilter::fromRaw(): intervals can use only two values, removing extras`);1323            this.values = [this.values[0], this.values[1]];1324        }1325    }1326    if (this.values.length > 1 && opDef.multiple !== true) {1327        console.warn(`EditableFilter::fromRaw(): operator "${this.operator}" cannot handle multiple values, extra values removed`);1328        this.values = [this.values[0]];1329    }1330    return true;1331}1332save()1333{1334    return [this.active, this.column, this.operator].concat(this.values);1335}1336};  // class EditableFilter1337class FilterEditorBase {1338constructor(container, filter, definition)1339{1340    // Target filter1341    this.filter = filter;1342    // Does this filter target RAM/HD sizes?1343    this.isStorage = (definition.flags & ColumnFlag.F_STORAGE) ? true : false;1344    // Where to put the editor interface1345    this.container = container;1346    // Unique UI element prefix1347    this.id = makeRandomID();1348    // UI properties1349    this.defaultValue = "";1350    this.fieldSize = 50;1351    this.maxLength = "";1352}1353buildUI()1354{1355}1356operatorHasChanged(operator)1357{1358    this.buildUI();1359}1360getData()1361{1362    throw new Error("you did not override getData()");1363}1364// Return [state, message], if state is true then the data is valid, otherwise the1365// message is displayed and the filter is NOT saved (and the editor does not close).1366validate()1367{1368    return [true, null];1369}1370// Accessing this.container.query... is so frequent that here's two helpers for it1371$(query) { return this.container.querySelector(query); }1372$all(query) { return this.container.querySelectorAll(query); }1373createValueRow(value, showButtons=true, title=null)1374{1375    let row = document.createElement("tr"),1376        html = "";1377    if (title !== null)1378        html += `<td>${title}</td>`;1379    html += `<td><div class="flex flex-cols flex-gap-5px">`;1380    value = value.toString();1381    if (this.isStorage) {1382        // Make a unit selector combo box and strip the unit from the value1383        const unit = value.length > 1 ? value.toString().slice(value.length - 1) : null;1384        html += `<input type="text" size="${this.fieldSize}" maxlen="" value="${value.length > 1 ? value.slice(0, value.length - 1) : value}">`;1385        html += "<select>";1386        for (const u of [["B", "B"], ["KiB", "K"], ["MiB", "M"], ["GiB", "G"], ["TiB", "T"]])1387            html += `<option data-unit="${u[1]}" ${u[1] == unit ? "selected" : ""}>${u[0]}</option>`;1388        html += "</select>";1389    } else html += `<input type="text" size="${this.fieldSize}" maxlength="${this.maxLength}">`;1390    if (showButtons)1391        html += `<button>+</button><button>-</button>`;1392    html += "</div></td>";1393    row.innerHTML = html;1394    if (!this.isStorage)1395        row.querySelector(`input[type="text"]`).value = value;1396    if (showButtons)1397        this.addEventHandlers(row);1398    return row;1399}1400addEventHandlers(row)1401{1402    // +/- button click handlers. Their positions change if the unit combo box is on the row.1403    const add = this.isStorage ? 2 : 1,1404          del = this.isStorage ? 3 : 2;1405    row.children[0].children[0].children[add].addEventListener("click", (e) => this.duplicateRow(e));1406    row.children[0].children[0].children[del].addEventListener("click", (e) => this.removeRow(e));1407}1408duplicateRow(e)1409{1410    let thisRow = e.target.parentNode.parentNode.parentNode,1411        newRow = thisRow.cloneNode(true);1412    if (this.isStorage) {1413        // Turns out that selectedIndex is not part of the DOM. Thank you, JavaScript.1414        // This is so ugly.1415        newRow.children[0].children[0].children[1].selectedIndex =1416            thisRow.children[0].children[0].children[1].selectedIndex;1417    }1418    this.addEventHandlers(newRow);1419    thisRow.parentNode.insertBefore(newRow, thisRow.nextSibling);1420}1421removeRow(e)1422{1423    let thisRow = e.target.parentNode.parentNode.parentNode;1424    thisRow.parentNode.removeChild(thisRow);1425    // There must be at least one value at all times, even if it's empty1426    if (this.$all(`table#values tr`).length == 0) {1427        console.log("Creating a new empty value row");1428        this.$("table#values").appendChild(this.createValueRow(this.defaultValue));1429    }1430}1431};  // class FilterEditorBase1432class FilterEditorBoolean extends FilterEditorBase {1433buildUI()1434{1435    this.container.innerHTML =1436`<div class="flex flex-rows flex-gap-5px">1437<span><input type="radio" name="${this.id}-value" id="${this.id}-true" ${this.filter.editValues[0] === 1 ? "checked" : ""}><label for="${this.id}-true">${_tr('tabs.filtering.ed.bool.t')}</label></span>1438<span><input type="radio" name="${this.id}-value" id="${this.id}-false" ${this.filter.editValues[0] !== 1 ? "checked" : ""}><label for="${this.id}-false">${_tr('tabs.filtering.ed.bool.f')}</label></span>1439</div>`;1440    this.$(`#${this.id}-true`).addEventListener("click", () => { this.filter.editValues = [1]; });1441    this.$(`#${this.id}-false`).addEventListener("click", () => { this.filter.editValues = [0]; });1442}1443getData()1444{1445    return [this.filter.editValues[0] === 1 ? 1 : 0];1446}1447};  // class FilterEditorBoolean1448class FilterEditorString extends FilterEditorBase {1449buildUI()1450{1451    this.container.innerHTML = `<p>${this.getExplanation()}</p><table id="values"></table>`;1452    let table = this.$("table#values");1453    for (const v of this.filter.editValues)1454        table.appendChild(this.createValueRow(v));1455}1456getData()1457{1458    let values = [];1459    for (const i of this.$all(`table#values tr input[type="text"]`))1460        values.push(i.value.trim());1461    return values;1462}1463operatorHasChanged(operator)1464{1465    this.$("p").innerHTML = this.getExplanation();1466}1467getExplanation()1468{1469    let out = "";1470    out += _tr("tabs.filtering.ed.multiple");1471    out += " ";1472    out += _tr((this.filter.editOperator == "=") ? "tabs.filtering.ed.one_hit_is_enough" : "tabs.filtering.ed.no_hits_allowed");1473    out += " ";1474    out += _tr("tabs.filtering.ed.regexp");1475    return out;1476}1477};  // class FilterEditorString1478class FilterEditorNumeric extends FilterEditorBase {1479constructor(container, filter, definition)1480{1481    super(container, filter, definition);1482    this.defaultValue = this.isStorage ? "0M" : "0";1483    this.fieldSize = 10;1484    this.maxLength = "32";1485}1486buildUI()1487{1488    const id = this.id;1489    const opr = this.filter.editOperator;1490    if (opr == "[]" || opr == "![]") {1491        let help = "";1492        if (opr == "[]")1493            help += _tr("tabs.filtering.ed.closed");1494        else help += _tr("tabs.filtering.ed.open");1495        this.container.innerHTML = `<p>${help}${this.getExtraHelp()}</p><table id="values"></table>`;1496        let table = this.$("table#values");1497        table.appendChild(this.createValueRow(this.filter.editValues[0], false, "Min:"));1498        table.appendChild(this.createValueRow(this.filter.editValues[1], false, "Max:"));1499    } else if (opr == "=" || opr == "!=") {1500        let html = `<p>${_tr("tabs.filtering.ed.multiple")}`;1501        html += " ";1502        html += _tr((opr == "=") ? "tabs.filtering.ed.one_hit_is_enough" : "tabs.filtering.ed.no_hits_allowed");1503        html += " ";1504        html += `${this.getExtraHelp()}</p><table id="values"></table>`;1505        this.container.innerHTML = html;1506        let table = this.$("table#values");1507        for (const v of this.filter.editValues)1508            table.appendChild(this.createValueRow(v));1509    } else {1510        this.container.innerHTML = `<p>${_tr("tabs.filtering.ed.single")}${this.getExtraHelp()}</p><table id="values"></table>`;1511        this.$("table#values").appendChild(this.createValueRow(this.filter.editValues[0], false));1512    }1513}1514getData()1515{1516    const interval = (this.filter.editOperator == "[]" || this.filter.editOperator == "![]");1517    let values = [];1518    // This assumes validate() has been called first and the data is actually valid1519    for (const i of this.$all(`table#values tr input[type="text"]`)) {1520        let n = i.value.trim();1521        if (n.length == 0)1522            continue;1523        try {1524            n = floatize(n);1525        } catch (e) {1526            continue;1527        }1528        if (isNaN(n))1529            continue;1530        if (this.isStorage) {1531            const s = i.parentNode.children[1];1532            values.push(`${n}${s.options[s.selectedIndex].dataset.unit}`);  // put the unit back1533        } else values.push(n);1534    }1535    // min > max, swap1536    // TODO: Make this work with storage1537    if (!this.isStorage && interval && values[0] > values[1])1538        values = [values[1], values[0]];1539    return values;1540}1541validate()1542{1543    const interval = (this.filter.editOperator == "[]" || this.filter.editOperator == "![]");1544    let valid = 0;1545    for (const i of this.$all(`table#values tr input[type="text"]`)) {1546        let n = i.value.trim();1547        if (n.length == 0)1548            continue;1549        try {1550            n = floatize(n);1551        } catch (e) {1552            return [false, `"${i.value.trim()}" ` + _tr("tabs.filtering.ed.numeric.nan")];1553        }1554        if (isNaN(n))1555            return [false, `"${i.value.trim()}" `+ _tr("tabs.filtering.ed.numeric.nan")];1556        if (this.isStorage && n < 0)1557            return [false, _tr("tabs.filtering.ed.numeric.negative_storage")];1558        valid++;1559    }1560    if (interval && valid < 2)1561        return [false, _tr("tabs.filtering.ed.invalid_interval")];1562    if (!interval && valid == 0)1563        return [false, _tr("tabs.filtering.ed.no_values")];1564    return [true, null];1565}1566getExtraHelp()1567{1568    return "";1569}1570};  // class FilterEditorNumeric1571class FilterEditorUnixtime extends FilterEditorNumeric {1572constructor(container, filter, definition)1573{1574    super(container, filter, definition);1575    // Use today's date as the default value1576    const d = new Date();1577    this.defaultValue = `${d.getFullYear()}-` +1578                        `${String(d.getMonth() + 1).padStart(2, "0")}-` +1579                        `${String(d.getDate()).padStart(2, "0")}`;1580    this.fieldSize = 20;1581    this.maxLength = "20";1582}1583buildUI()1584{1585    super.buildUI();1586    this.$(`a#${this.id}-help`).addEventListener("click", (e) => this.showHelp(e));1587}1588getData()1589{1590    let values = [];1591    // Unlike numbers, attempt no string->int conversions here. The filter compiler1592    // engine will deal with interpreting absolute and relative time values.1593    for (const i of this.$all(`table#values tr input[type="text"]`)) {1594        const v = i.value.trim();1595        if (v.length == 0)1596            continue;1597        values.push(v);1598    }1599    return values;1600}1601validate()1602{1603    const interval = (this.filter.editOperator == "[]" || this.filter.editOperator == "![]");1604    let valid = 0;1605    for (const i of this.$all(`table#values tr input[type="text"]`)) {1606        const v = i.value.trim();1607        if (v.length == 0)1608            continue;1609        if (parseAbsoluteOrRelativeDate(v) === null)1610            return [false, `"${v}"` + _tr("tabs.filtering.ed.time.invalid")];1611        valid++;1612    }1613    if (interval && valid < 2)1614        return [false, _tr("tabs.filtering.ed.invalid_interval")];1615    if (!interval && valid == 0)1616        return [false, _tr("tabs.filtering.ed.no_values")];1617    return [true, null];1618}1619getExtraHelp()1620{1621    return ` <a href="#" id="${this.id}-help"> ${_tr("tabs.filtering.ed.time.help_link")}</a>.`;1622}1623showHelp(e)1624{1625    e.preventDefault();1626    window.alert(_tr("tabs.filtering.ed.time.help"));1627}1628};  // class FilterEditorUnixtime1629const RowElem = {1630    BTN_DELETE: 1,1631    BTN_DUPLICATE: 2,1632    CB_ACTIVE: 3,1633    DIV_PRETTY: 4,1634    DIV_EDITOR: 5,1635};1636class FilterEditor {1637constructor(parentClass, container, columnDefinitions, columnTitles, filterPresets, filterDefaults, isAdvanced)1638{1639    // Who do we tell about filter changes?1640    this.parentClass = parentClass;1641    // This container is our playground. Everything we put on the screen, it's1642    // inside this HTML element.1643    this.container = container;1644    // Column definitions1645    this.plainColumnDefinitions = columnDefinitions;1646    this.columnDefinitions = new ColumnDefinitions(columnDefinitions);1647    this.columnTitles = columnTitles;1648    this.updateColumnHelp = true;1649    this.haveHelp = false;1650    this.changed = false;1651    this.isAdvanced = isAdvanced;1652    this.filterPresets = filterPresets;1653    this.filters = {};      // the traditional filters1654    this.showJSON = false;1655    this.defaultFilter = filterDefaults[0];1656    // The current filter programs. One for the old-style filters, one for the advanced filter.1657    this.comparisons = [];1658    this.program = [];1659    this.comparisonsAdvanced = [];1660    this.programAdvanced = [];1661    // JS event handling shenanigans1662    this.onDeleteFilter = this.onDeleteFilter.bind(this);1663    this.onDuplicateFilter = this.onDuplicateFilter.bind(this);1664    this.onActiveFilter = this.onActiveFilter.bind(this);1665    this.onClickColumnName = this.onClickColumnName.bind(this);1666    this.buildUI();1667    this.enableOrDisable(false);1668}1669buildUI()1670{1671    const havePresets = Object.keys(this.filterPresets[0]).length > 0,1672          haveAdvancedPresets = Object.keys(this.filterPresets[1]).length > 0;1673    let html = "";1674    html += `<div id="traditional" class="filterEditorWrapper">`;1675    html +=1676`<div class="flex flex-rows flex-gap-5px"><div class="flex flex-cols flex-gap-5px">1677<button id="deleteAll" class="danger" title="${_tr("tabs.filtering.delete_all_title")}">${_tr("tabs.filtering.delete_all")}</button>1678<button id="toggleJSON" title="${_tr("tabs.filtering.toggle_json_title")}">${_tr("tabs.filtering.show_json")}</button>1679<button id="saveJSON" class="hidden" title="${_tr("tabs.filtering.save_json_title")}">${_tr("tabs.filtering.save_json")}</button>1680</div><textarea id="json" rows="5" class="width-100p jsonEditor hidden" title="${_tr("tabs.filtering.json_title")}"></textarea>1681<table class="filtersTable"></table>1682<div><button id="new">${_tr("tabs.filtering.new_filter")}</button></div>`;1683    if (havePresets) {1684        const presets = this.filterPresets[0];1685        html += `<div><details><summary>${_tr('tabs.filtering.presets.title')}</summary>`;1686        html += `<div class="flex flex-rows flex-gap-5px margin-top-10px">`;1687        html += `<p class="margin-0 padding-0">${_tr("tabs.filtering.presets.click_to_add")}</p>`;1688        html += `<span><input type="checkbox" id="append-at-end" checked><label for="append-at-end">${_tr('tabs.filtering.presets.append')}</label></span>`;1689        html += `<ul class="margin-0 padding-0 no-list-bullets" id="presets">`;1690        for (const key of Object.keys(presets))1691            html += `<li><a href="#" data-id="${key}">${presets[key].title}</a></li>`;1692        html += `</ul></details></div></div>`;1693    }1694    html += `</div>`;1695    html += `</div>`;1696    html += `<div id="advanced">`;1697    html +=1698`<div class="flex flex-columns flex-gap-10px width-100p">1699<fieldset class="width-66p">1700<legend>${_tr('tabs.filtering.expression_title')}</legend>1701<textarea id="filter" placeholder="${_tr('tabs.filtering.expression_placeholder')}" rows="5"></textarea>1702<div class="flex flex-columns flex-gap-5px margin-top-5px">1703<button id="save" disabled>${_tr('tabs.filtering.save')}</button>1704<button id="clear" disabled>${_tr('tabs.filtering.clear')}</button>1705<button id="convert" disabled>${_tr('tabs.filtering.convert')}</button>1706</div>1707</fieldset>1708<fieldset class="width-33p">1709<legend>${_tr('tabs.filtering.messages_title')}</legend>1710<div id="messages"></div>1711</fieldset>1712</div>`;1713    html += `<div class="flex flex-rows flex-gap-10px margin-top-10px">`;1714    if (haveAdvancedPresets) {1715        html +=1716`<details>1717<summary>${_tr('tabs.filtering.presets.title')}</summary>1718<div class="padding-10px">1719<p class="line-height-150p margin-0 padding-0">${_tr('tabs.filtering.presets.instructions')}</p>1720<div class="padding-top-10px padding-bottom-10px flex flex-vcenter flex-columns flex-gap-10px">1721<span><input type="checkbox" id="append-at-end-advanced" checked><label for="append-at-end-advanced">${_tr('tabs.filtering.presets.append')}</label></span>1722<span><input type="checkbox" id="add-parenthesis" checked><label for="add-parenthesis">${_tr('tabs.filtering.presets.add_parenthesis')}</label></span>1723</div>1724<table class="commonTable presetsTable"><thead>1725<tr><th class="padding-5px">${_tr('tabs.filtering.presets.name')}</th><th class="padding-5px">${_tr('tabs.filtering.presets.expression')}</th></tr>1726</thead><tbody>`;1727        for (const key of Object.keys(this.filterPresets[1])) {1728            const preset = this.filterPresets[1][key];1729            html +=1730`<tr data-id="${key}">1731<td class="padding-5px"><a href="#" data-id="${key}">${preset.title}</a></td>1732<td class="padding-5px"><code>${escapeHTML(preset.filter)}</code></td>1733</tr>`;1734        }1735        html += `</tbody></table>`;1736        html += "</div>";1737        html += `</details>`;1738    }1739    html +=1740`<details>1741<summary>${_tr('tabs.filtering.column_list.title')}</summary>1742<div class="padding-10px">1743<p class="line-height-150p margin-0 padding-0">${_tr('tabs.filtering.column_list.hidden_warning')}</p>1744<div id="columnList" class="margin-top-10px"></div>1745</details>`;1746    html += `</div>`;1747    html += `</div>`;1748    this.container.innerHTML = html;1749    // Initial mode selection1750    if (this.isAdvanced)1751        this.$("div#traditional").classList.add("hidden");1752    else this.$("div#advanced").classList.add("hidden");1753    this.$("button#deleteAll").addEventListener("click", () => this.onDeleteAllFilters());1754    this.$("button#toggleJSON").addEventListener("click", () => this.onToggleJSON());1755    this.$("button#saveJSON").addEventListener("click", () => this.onSaveJSON());1756    this.$("button#new").addEventListener("click", () => this.onNewFilter());1757    this.$("textarea#json").addEventListener("input", () => this.validateJSON());1758    this.$("button#save").addEventListener("click", () => this.onSave());1759    this.$("button#clear").addEventListener("click", () => this.onClear());1760    this.$("button#convert").addEventListener("click", () => this.onConvertTraditionalFilter());1761    this.$("textarea#filter").addEventListener("input", () => this.onAdvancedInput());1762    // Make the presets clickable1763    if (havePresets) {1764        for (let i of this.$all("div#traditional ul#presets a"))1765            i.addEventListener("click", (e) => this.onLoadPreset(e));1766    }1767    if (haveAdvancedPresets) {1768        for (let i of this.$all("div#advanced .presetsTable a"))1769            i.addEventListener("click", (e) => this.onLoadPreset(e));1770    }1771    this.generateColumnHelp();1772}1773$(selector) { return this.container.querySelector(selector); }1774$all(selector) { return this.container.querySelectorAll(selector); }1775// Called from the parent class1776enableOrDisable(isEnabled)1777{1778    this.disabled = !isEnabled;1779    this.$("textarea#filter").disabled = this.disabled;1780    this.$("button#save").disabled = this.disabled;1781    this.$("button#clear").disabled = this.disabled;1782    this.$("button#convert").disabled = this.disabled;1783    this.$("button#deleteAll").disabled = this.disabled;1784    this.$("button#toggleJSON").disabled = this.disabled;1785    this.$("button#saveJSON").disabled = this.disabled;1786    this.$("button#new").disabled = this.disabled;1787    // Filter editor row elements1788    for (let row of this.$all("table.filtersTable tr.row")) {1789        const f = this.filters[row.dataset.id];1790        if (f.isNew)1791            continue;1792        row.children[0].children[0].children[0].disabled = this.disabled;   // delete1793        row.children[0].children[0].children[1].disabled = this.disabled;   // duplicate1794        row.children[1].children[0].disabled = this.disabled;               // active1795    }1796}1797// Switch between traditional and advanced filtering modes1798toggleMode(advanced)1799{1800    if (this.isAdvanced == advanced)1801        return;1802    this.isAdvanced = advanced;1803    if (this.isAdvanced) {1804        this.$("div#traditional").classList.add("hidden");1805        this.$("div#advanced").classList.remove("hidden");1806    } else {1807        this.$("div#traditional").classList.remove("hidden");1808        this.$("div#advanced").classList.add("hidden");1809    }1810    this.parentClass.updateFiltering();1811}1812// Load a filter preset1813onLoadPreset(e)1814{1815    e.preventDefault();1816    const id = e.target.dataset.id;1817    const preset = this.filterPresets[this.isAdvanced ? 1 : 0][id];1818    if (!preset) {1819        window.alert(`Invalid preset ID "${id}". Please contact Opinsys support.`);1820        return;1821    }1822    if (this.isAdvanced) {1823        let f = preset.filter;1824        if (this.$("input#add-parenthesis").checked)1825            f = `(${f})`;1826        let box = this.$("textarea#filter");1827        if (this.$("input#append-at-end-advanced").checked == false)1828            box.value = f;1829        else {1830            // Append or replace?1831            if (box.value.trim().length == 0)1832                box.value = f;1833            else {1834                box.value += "\n";1835                box.value += f;1836            }1837        }1838        this.clearMessages();1839        this.changed = true;1840        this.updateUnsavedWarning();1841    } else {1842        // Append or replace?1843        if (this.$("input#append-at-end").checked == false)1844            this.setFilters(preset.filters);1845        else this.setFilters(this.getFilters().concat(preset.filters));1846        this.parentClass.saveFilters();1847        this.parentClass.updateFiltering();1848    }1849}1850// Compiles a filter expression and returns the compiled comparisons and RPN code in an array.1851// This does not actually USE the filter for anything, it only compiles the given string.1852compileFilterExpression(input)1853{1854    console.log("----- Compiling filter string -----");1855    console.log("Input:");1856    console.log(input);1857    const t0 = performance.now();1858    this.clearMessages();1859    if (input.trim() == "") {1860        // Do nothing if there's nothing to compile1861        console.log("(Doing nothing to an empty string)");1862        return [[], []];1863    }1864    let logger = new MessageLogger();1865    // ----------------------------------------------------------------------------------------------1866    // Tokenization1867    let t = new Tokenizer();1868    console.log("----- Tokenization -----");1869    t.tokenize(logger, this.columnDefinitions, input);1870    if (!logger.empty()) {1871        for (const m of logger.messages) {1872            if (m.message == "unexpected_end") {1873                // Don't report the same error multiple times1874                this.listMessages(logger);1875                return null;1876            }1877        }1878    }1879    console.log("Raw tokens:");1880    if (t.tokens.length == 0)1881        console.log("  (NONE)");1882    else console.log(t.tokens);1883    // ----------------------------------------------------------------------------------------------1884    // Syntax analysis and comparison extraction1885    let p = new Parser();1886    console.log("----- Syntax analysis/parsing -----");1887    // TODO: Should we abort the compilation if this fails? Now we just cram ahead at full speed1888    // and hope for the best.1889    p.parse(logger, this.columnDefinitions, t.tokens, t.lastRow, t.lastCol);1890    console.log("Raw comparisons:");1891    if (p.comparisons.length == 0)1892        console.log("  (NONE)");1893    else console.log(p.comparisons);1894    console.log("Raw parser output:");1895    if (p.output.length == 0)1896        console.log("  (NONE)");1897    else console.log(p.output);1898    // ----------------------------------------------------------------------------------------------1899    // Compile the actual comparisons1900    let comparisons = [];1901    console.log("----- Compiling the comparisons -----");1902    let cc = new ComparisonCompiler();1903    for (const raw of p.comparisons) {1904        const c = cc.compile(logger, this.columnDefinitions, raw.column, raw.operator, raw.value);1905        if (c === null) {1906            // null == the comparison was so invalid it could not even be parsed1907            // log it for debugging1908            console.error(raw);1909            continue;1910        }1911        if (c === false) {1912            // false == the comparison was syntactically okay, but it wasn't actually correct1913            console.warn("Could not compile comparison");1914            console.warn(raw);1915            continue;1916        }1917        comparisons.push(c);1918    }1919    if (!logger.empty()) {1920        this.listMessages(logger);1921        if (logger.haveErrors()) {1922            // Warnings won't stop the filter string from saved or used1923            console.error("Comparison compilation failed, no filter program produced");1924            return null;1925        }1926    }1927    console.log("Compiled comparisons:");1928    console.log(comparisons);1929    let program = [];1930    console.log("----- Shunting Yard -----");1931    // Generate code1932    let cg = new CodeGenerator();1933    program = cg.compile(p.output);1934    console.log("Final filter program:");1935    if (program.length == 0)1936        console.log("  (Empty)");1937    for (let i = 0; i < program.length; i++) {1938        const o = program[i];1939        switch (o[0]) {1940            case "!":1941                console.log(`(${i}) NEG`);1942                break;1943            case "&":1944                console.log(`(${i}) AND`);1945                break;1946            case "|":1947                console.log(`(${i}) OR`);1948                break;1949            default: {1950                const cmp = comparisons[o[0]];1951                console.log(`(${i}) CMP [${cmp.column} ${cmp.operator} ${cmp.value.toString()}]`);1952                break;1953            }1954        }1955    }1956    const t1 = performance.now();1957    console.log(`Filter expression compiled to ${program.length} opcode(s), ${comparisons.length} comparison evaluator(s)`);1958    console.log(`Filter expression compilation: ${t1 - t0} ms`);1959    return [comparisons, program];1960}1961getFilterProgram()1962{1963    return {1964        comparisons: [...(this.isAdvanced ? this.comparisonsAdvanced : this.comparisons)],1965        program: [...(this.isAdvanced ? this.programAdvanced : this.program)]1966    };1967}1968// --------------------------------------------------------------------------------------------------1969// --------------------------------------------------------------------------------------------------1970// "TRADITIONAL" FILTERS1971// Loads filters from an array, updates the table and builds the filter program1972setFilters(raw)1973{1974    this.filters = {};1975    let table = this.$("table.filtersTable");1976    table.innerHTML = "";1977    for (const r of (Array.isArray(raw) ? raw : [])) {1978        let e = new EditableFilter();1979        if (!e.load(r, this.plainColumnDefinitions))1980            continue;1981        const id = nextFilterID();1982        this.filters[id] = e;1983        let row = this.buildFilterRow(id, e);1984        this.setFilterRowEvents(row);1985        table.appendChild(row);1986    }1987    this.updateJSON();1988    this.convertAndCompileFilters();1989}1990getFilters()1991{1992    let out = [];1993    for (const row of this.$all("table.filtersTable tr.row")) {1994        const f = this.filters[row.dataset.id];1995        if (!f.isNew)1996            out.push(f.save());1997    }1998    return out;1999}2000onDeleteAllFilters()2001{2002    if (!window.confirm(_tr("tabs.filtering.delete_all_confirm")))2003        return;2004    this.filters = {};2005    this.$("table.filtersTable").innerHTML = "";2006    this.updateJSON();2007    this.convertAndCompileFilters();2008    this.parentClass.saveFilters();2009    this.parentClass.updateFiltering();2010}2011onToggleJSON()2012{2013    let box = this.$("textarea#json"),2014        button = this.$("button#saveJSON");2015    this.showJSON = !this.showJSON;2016    if (this.showJSON)2017        showElements(box, button);2018    else hideElements(box, button);2019    this.$("button#toggleJSON").innerText =2020        _tr("tabs.filtering." + (this.showJSON ? "hide_json" : "show_json"));2021}2022onSaveJSON()2023{2024    if (!window.confirm(_tr("tabs.filtering.save_json_confirm")))2025        return;2026    try {2027        this.setFilters(JSON.parse(this.$("textarea#json").value));2028        this.convertAndCompileFilters();2029        this.parentClass.saveFilters();2030        this.parentClass.updateFiltering();2031    } catch (e) {2032        window.alert(e);2033    }2034}2035updateJSON()2036{2037    this.$("textarea#json").value = JSON.stringify(this.getFilters());2038    this.$("textarea#json").classList.remove("invalidJSON");2039    this.$("button#saveJSON").disabled = false;2040}2041validateJSON()2042{2043    let box = this.$("textarea#json");2044    // Is the JSON parseable?2045    try {2046        JSON.parse(box.value);2047        box.classList.remove("invalidJSON");2048        this.$("button#saveJSON").disabled = false;2049    } catch (e) {2050        box.classList.add("invalidJSON");2051        this.$("button#saveJSON").disabled = true;2052    }2053}2054// Pretty-prints a filter row2055prettyPrintFilter(filter)2056{2057    const colDef = this.plainColumnDefinitions[filter.column],2058          operator = OPERATORS[filter.operator];2059    function formatValue(v)2060    {2061        if (colDef.type == ColumnType.UNIXTIME) {2062            const d = parseAbsoluteOrRelativeDate(v);2063            if (d === null)2064                return "?";2065            return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +2066                   `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;2067        }2068        if (colDef.flags & ColumnFlag.F_STORAGE) {2069            if (v.length == 0)2070                return "";2071            let unit = v.slice(v.length - 1);2072            if (!"BKMGT".includes(unit))2073                return `${v} B`;2074            switch (unit) {2075                case "B": unit = "B"; break;2076                case "K": unit = "KiB"; break;2077                case "M": unit = "MiB"; break;2078                case "G": unit = "GiB"; break;2079                case "T": unit = "TiB"; break;2080            }2081            return `${v.slice(0, v.length - 1)} ${unit}`2082        }2083        return v;2084    }2085    const prettyTrue = _tr("tabs.filtering.ed.bool.t"),2086          prettyFalse = _tr("tabs.filtering.ed.bool.f"),2087          prettyEmpty = _tr("tabs.filtering.pretty.empty"),2088          prettyOr = _tr("tabs.filtering.pretty.or"),2089          prettyNor = _tr("tabs.filtering.pretty.nor");2090    let html = "";2091    html += `<span class="column">${this.columnTitles[filter.column]}</span>`;2092    html += `<span class="operator">${humanOperatorName(filter.operator)}</span>`;2093    html += `<span class="values">`2094    if (filter.operator == "[]" || filter.operator == "![]") {2095        html += `<span class="value">`;2096        html += formatValue(filter.values[0]);2097        html += `</span><span class="sep"> â </span><span class="value">`;2098        html += formatValue(filter.values[1]);2099        html += `</span>`;2100    } else {2101        if (colDef.type == ColumnType.BOOL)2102            html += `<span class="value">${filter.values[0] === 1 ? prettyTrue : prettyFalse}</span>`;2103        else {2104            for (let i = 0, j = filter.values.length; i < j; i++) {2105                if (filter.values[i].length == 0 && colDef.type == ColumnType.STRING)2106                    html += `<span class="value empty">${prettyEmpty}</span>`;2107                else {2108                    html += `<span class="value">`;2109                    html += formatValue(filter.values[i]);2110                    html += "</span>";2111                }2112                if (i + 1 < j - 1)2113                    html += `<span class="sep">, </span>`;2114                else if (i + 1 < j) {2115                    if (filter.operator == "!=")2116                        html += `<span class="sep"> ${prettyNor} </span>`;2117                    else html += `<span class="sep"> ${prettyOr} </span>`;2118                }2119            }2120        }2121    }2122    html += "</span>";2123    return html;2124}2125buildFilterRow(id, filter)2126{2127    let tr = document.createElement("tr");2128    tr.id = `row-${id}`;2129    tr.className = "row";2130    tr.dataset.id = id;2131    tr.innerHTML =2132`<td class="minimize-width"><div class="buttons">2133<button class="danger" title="${_tr("tabs.filtering.remove_title")}" ${filter.isNew ? "disabled" : ""}>${_tr("tabs.filtering.remove")}</button>2134<button title="${_tr("tabs.filtering.duplicate_title")}" ${filter.isNew ? "disabled" : ""}>${_tr("tabs.filtering.duplicate")}</button></div></td>2135<td class="minimize-width" title="${_tr("tabs.filtering.active_title")}">2136<input type="checkbox" class="active" ${filter.active == 1 ? "checked" : ""} ${filter.isNew ? "disabled" : ""}>2137</td><td><div class="flex flex-rows"><div class="pretty" title="${_tr("tabs.filtering.click_to_edit_title")}">2138${this.prettyPrintFilter(filter)}</div><div></div></td>`;2139    return tr;2140}2141findTableRow(element)2142{2143    return element.closest(`tr[id^="row-"]`);2144}2145getRowElem(row, what)2146{2147    switch (what) {2148        case RowElem.BTN_DELETE:2149            return row.children[0].children[0].children[0];2150        case RowElem.BTN_DUPLICATE:2151            return row.children[0].children[0].children[1];2152        case RowElem.CB_ACTIVE:2153            return row.children[1].children[0];2154        case RowElem.DIV_PRETTY:2155            return row.children[2].children[0].children[0];2156        case RowElem.DIV_EDITOR:2157            return row.children[2].children[0].children[1];2158        default:2159            return null;2160    }2161}2162setFilterRowEvents(row)2163{2164    this.getRowElem(row, RowElem.BTN_DELETE).addEventListener("click", e => this.onDeleteFilter(e));2165    this.getRowElem(row, RowElem.BTN_DUPLICATE).addEventListener("click", e => this.onDuplicateFilter(e));2166    this.getRowElem(row, RowElem.CB_ACTIVE).addEventListener("click", e => this.onActiveFilter(e));2167    this.getRowElem(row, RowElem.DIV_PRETTY).addEventListener("click", e => {2168        let row = this.findTableRow(e.target);2169        this.filters[row.dataset.id].beginEditing();2170        this.openFilterEditor(row);2171    });2172}2173onDeleteFilter(e)2174{2175    let tr = this.findTableRow(e.target);2176    const id = tr.dataset.id;2177    const wasActive = this.filters[id].active;2178    delete this.filters[id];2179    tr.parentNode.removeChild(tr);2180    this.updateJSON();2181    this.parentClass.saveFilters();2182    // Deleting disabled filters don't trigger table rebuilds2183    if (wasActive) {2184        this.convertAndCompileFilters();2185        this.parentClass.updateFiltering();2186    }2187}2188onDuplicateFilter(e)2189{2190    let tr = this.findTableRow(e.target);2191    const id = tr.dataset.id;2192    let dupe = new EditableFilter();2193    if (!dupe.load(this.filters[id].save(), this.plainColumnDefinitions)) {2194        window.alert("Filter duplication failed");2195        return;2196    }2197    dupe.active = 0;  // prevent double filtering2198    const newID = nextFilterID();2199    this.filters[newID] = dupe;2200    let newRow = this.buildFilterRow(newID, dupe);2201    tr.parentNode.insertBefore(newRow, tr.nextSibling);2202    this.setFilterRowEvents(newRow);2203    this.updateJSON();2204    this.parentClass.saveFilters();2205    // The duplicated row is disabled, don't update the table2206}2207onActiveFilter(e)2208{2209    this.filters[this.findTableRow(e.target).dataset.id].active ^= 1;2210    this.updateJSON();2211    this.convertAndCompileFilters();2212    this.parentClass.saveFilters();2213    this.parentClass.updateFiltering();2214}2215openFilterEditor(row)2216{2217    const id = row.dataset.id;2218    let filter = this.filters[id];2219    hideElements(this.getRowElem(row, RowElem.DIV_PRETTY));2220    // Construct an editor interface2221    let wrapper = this.getRowElem(row, RowElem.DIV_EDITOR);2222    wrapper.innerHTML =2223`<div class="flex flex-rows">2224<div class="openFilter flex flex-columns flex-gap-5px">2225<select id="column" title="${_tr("tabs.filtering.edit_column_title")}"></select>2226<select id="operator" title="${_tr("tabs.filtering.edit_operator_title")}"></select>2227<button id="save" class="margin-left-20px"><i class="icon-ok"></i>${_tr("tabs.filtering.save")}</button>2228<button id="cancel"><i class="icon-cancel"></i>${_tr("tabs.filtering.cancel")}</button>2229</div><div id="editor" class="editor"></div></div>`;2230    wrapper.classList.add("editorWrapper");2231    // Sort the columns in alphabetical order2232    let select = wrapper.querySelector("select#column"),2233        columns = [];2234    for (const column of Object.keys(this.plainColumnDefinitions))2235        columns.push([column, this.columnTitles[column]]);2236    columns.sort((a, b) => { return a[1].localeCompare(b[1]) });2237    for (const [column, title] of columns) {2238        let o = document.createElement("option");2239        o.innerText = title;2240        o.dataset.column = column;2241        o.selected = (filter.editColumn == column);2242        select.appendChild(o);2243    }2244    const colDef = this.plainColumnDefinitions[filter.editColumn];2245    this.fillOperatorSelector(wrapper.querySelector("select#operator"),2246                              colDef.type, filter.editOperator);2247    // Initial type-specific editor child UI2248    this.buildValueEditor(filter, wrapper.querySelector("div#editor"), colDef);2249    wrapper.querySelector("select#column").addEventListener("change", (e) => this.onColumnChanged(e));2250    wrapper.querySelector("select#operator").addEventListener("change", (e) => this.onOperatorChanged(e));2251    wrapper.querySelector("button#save").addEventListener("click", (e) => {2252        let row = this.findTableRow(e.target);2253        const id = row.dataset.id;2254        // Don't save the filter if the value (or values) is incorrect2255        const valid = this.filters[id].editor.validate();2256        if (!valid[0]) {2257            window.alert(valid[1]);2258            return;2259        }2260        this.filters[id].finishEditing();2261        this.filters[id].isNew = false;     // enable normal functionality2262        this.closeFilterEditor(row);2263        this.getRowElem(row, RowElem.BTN_DELETE).disabled = false;2264        this.getRowElem(row, RowElem.BTN_DUPLICATE).disabled = false;2265        this.getRowElem(row, RowElem.CB_ACTIVE).disabled = false;2266        this.getRowElem(row, RowElem.DIV_PRETTY).innerHTML = this.prettyPrintFilter(this.filters[id]);2267        this.updateJSON();2268        this.parentClass.saveFilters();2269        if (this.filters[id].active) {2270            this.convertAndCompileFilters();2271            this.parentClass.updateFiltering();2272        }2273    });2274    wrapper.querySelector("button#cancel").addEventListener("click", (e) => {2275        let row = this.findTableRow(e.target);2276        let filter = this.filters[row.dataset.id];2277        const wasNew = filter.isNew;2278        filter.cancelEditing();2279        if (wasNew)2280            row.parentNode.removeChild(row);2281        else this.closeFilterEditor(row);2282    });2283}2284closeFilterEditor(row)2285{2286    this.filters[row.dataset.id].editor = null;2287    let editor = this.getRowElem(row, RowElem.DIV_EDITOR);2288    editor.innerHTML = "";2289    editor.classList.remove("editorWrapper");2290    showElements(this.getRowElem(row, RowElem.DIV_PRETTY));2291}2292buildValueEditor(filter, container, colDef)2293{2294    const editors = {2295        [ColumnType.BOOL]: FilterEditorBoolean,2296        [ColumnType.NUMERIC]: FilterEditorNumeric,2297        [ColumnType.STRING]: FilterEditorString,2298        [ColumnType.UNIXTIME]: FilterEditorUnixtime,2299    };2300    if (colDef.type in editors) {2301        filter.editor = new editors[colDef.type](container, filter, colDef);2302        filter.editor.buildUI();2303    } else throw new Error(`Unknown column type ${colDef.type}`);2304}2305// Attempts to preserve the current filter values between operator/column changes and applies2306// fixes to the data to ensure the current operator has enough data to work with2307preserveFilterData(filter)2308{2309    if (!filter.editor) {2310        console.warn("preserveFilterData(): no editor?");2311        return;2312    }2313    // Grab the values from the form first2314    filter.editValues = filter.editor.getData();2315    // Then ensure there are enough values2316    if (filter.editValues.length == 0)2317        filter.editValues.push(getDefaultValue(this.plainColumnDefinitions[filter.editColumn]));2318    if ((filter.editOperator == "[]" || filter.editOperator == "![]") && filter.editValues.length < 2)2319        filter.editValues.push(filter.editValues[0]);2320}2321onColumnChanged(e)2322{2323    let row = this.findTableRow(e.target);2324    const id = row.dataset.id;2325    let filter = this.filters[id];2326    let wrapper = this.getRowElem(row, RowElem.DIV_EDITOR);2327    filter.editColumn = e.target[e.target.selectedIndex].dataset.column;2328    // Is the previous operator still valid for this type? If not, reset it to "=",2329    // it's the default (and the safest) operator.2330    const newDef = this.plainColumnDefinitions[filter.editColumn];2331    if (!OPERATORS[filter.editOperator].allowed.has(newDef.type))2332        filter.editOperator = "=";2333    // Refill the operator selector2334    // TODO: Don't do this if the new column has the same operators available2335    // as the previous column did.2336    this.fillOperatorSelector(wrapper.querySelector("select#operator"),2337                              newDef.type, filter.editOperator);2338    this.preserveFilterData(filter);2339    // Recreate the editor UI2340    let editor = wrapper.querySelector("div#editor");2341    editor.innerHTML = "";2342    filter.editor = null;2343    this.buildValueEditor(filter, editor, newDef);2344}2345onOperatorChanged(e)2346{2347    const row = this.findTableRow(e.target);2348    const operator = e.target[e.target.selectedIndex].dataset.operator;2349    let filter = this.filters[row.dataset.id];2350    filter.editOperator = operator;2351    this.preserveFilterData(filter);2352    filter.editor.operatorHasChanged(operator);2353}2354fillOperatorSelector(target, type, initial)2355{2356    target.innerHTML = "";2357    for (const opId of ["=", "!=", "<", "<=", ">", ">=", "[]", "![]"]) {2358        if (OPERATORS[opId].allowed.has(type)) {2359            let o = document.createElement("option");2360            o.innerText = humanOperatorName(opId);2361            o.dataset.operator = opId;2362            o.selected = (opId == initial);2363            target.appendChild(o);2364        }2365    }2366}2367onNewFilter()2368{2369    let f = new EditableFilter();2370    let initial = null;2371    if (this.defaultFilter === undefined || this.defaultFilter === null || this.defaultFilter.length < 4) {2372        // Use the first available column. Probably not the best, but at least the filter will be valid.2373        initial = [0, Object.keys(this.plainColumnDefinitions)[0], "=", ""];2374    } else initial = [...this.defaultFilter];2375    initial[3] = getDefaultValue(this.plainColumnDefinitions[initial[1]]);2376    if (!f.load(initial, this.plainColumnDefinitions)) {2377        window.alert("Filter creation failed. See the console for details.");2378        return;2379    }2380    f.isNew = true;     // disables certain UI elements and makes the Cancel button remove the filter2381    const newID = nextFilterID();2382    this.filters[newID] = f;2383    let newRow = this.buildFilterRow(newID, f);2384    this.setFilterRowEvents(newRow);2385    this.$("table.filtersTable").appendChild(newRow);2386    // Open the newly-created filter for editing2387    this.filters[newID].beginEditing();2388    this.openFilterEditor(newRow);2389}2390convertTraditionalFilter(filters)2391{2392    let parts = [];2393    for (const f of filters) {2394        if (!f[0])          // inactive filter2395            continue;2396        if (f.length < 4)   // incomplete filter2397            continue;2398        let col = f[1],2399            op = f[2],2400            val = [];2401        const colDef = this.plainColumnDefinitions[col];2402        // Convert the value2403        for (let v of f.slice(3)) {2404            switch (colDef.type) {2405                case ColumnType.BOOL:2406                    if (v.length == 0)2407                        continue;2408                    val.push(v === 1 ? '1' : '0');2409                    break;2410                case ColumnType.NUMERIC:2411                    // All possible values should work fine, even storage units, without quotes2412                    if (v.length == 0)2413                        continue;2414                    val.push(v);2415                    break;2416                case ColumnType.UNIXTIME:2417                    if (v.length == 0)2418                        continue;2419                    // Absolute times must be quoted, relative times should work as-is2420                    val.push(ABSOLUTE_TIME.exec(v) !== null ? `"${v}"` : v);2421                    break;2422                case ColumnType.STRING:2423                default:2424                    // Convert strings to regexps2425                    if (v == "")2426                        val.push(`/^$/`);2427                    else val.push(`/${v}/`);2428                    break;2429            }2430        }2431        // Output a comparison with the converted value2432        if (op == "[]") {2433            // include (closed)2434            if (val.length < 2)2435                continue;2436            parts.push(`(${col} >= ${val[0]} && ${col} <= ${val[1]})`);2437        } else if (op == "![]") {2438            // exclude (open)2439            if (val.length < 2)2440                continue;2441            parts.push(`(${col} < ${val[0]} || ${col} > ${val[1]})`);2442        } else {2443            if (val.length < 1)2444                continue;2445            if (val.length == 1) {2446                // a single value2447                parts.push(`${col} ${op} ${val[0]}`);2448            } else {2449                // multiple values, either OR'd or AND'd together depending on the operator2450                let sub = [];2451                for (const v of val)2452                    sub.push(`${col} ${op} ${v}`);2453                if (op == "=")2454                    sub = sub.join(" || ");2455                else sub = sub.join(" && ");2456                parts.push("(" + sub + ")");2457            }2458        }2459    }2460    // Join the comparisons together2461    return parts.join(" && ");2462}2463// Converts the "traditional" filters into an advanced filter string and compiles it2464convertAndCompileFilters()2465{2466    const result = this.compileFilterExpression(this.convertTraditionalFilter(this.getFilters()));2467    if (result === false || result === null) {2468        window.alert("Could not compile the filter. See the console for details, then contact Opinsys support.");2469        return;2470    }2471    this.comparisons = result[0];2472    this.program = result[1];2473}2474// --------------------------------------------------------------------------------------------------2475// --------------------------------------------------------------------------------------------------2476// ADVANCED FILTERS2477setFilterString(filter)2478{2479    let box = this.$("textarea#filter");2480    if (typeof(filter) != "string")2481        box.value = "";2482    else box.value = filter;2483    this.comparisonsAdvanced = [];2484    this.programAdvanced = [];2485    const result = this.compileFilterExpression(box.value);2486    if (result === false || result === null)2487        return;2488    this.comparisonsAdvanced = result[0];2489    this.programAdvanced = result[1];2490}2491getFilterString()2492{2493    return this.$("textarea#filter").value;2494}2495// Save the advanced filter string2496onSave()2497{2498    const result = this.compileFilterExpression(this.$("textarea#filter").value);2499    if (result === false || result === null)2500        return;2501    this.comparisonsAdvanced = result[0];2502    this.programAdvanced = result[1];2503    this.changed = false;2504    this.updateUnsavedWarning();2505    this.parentClass.saveFilters();2506    this.parentClass.updateFiltering();2507}2508// Clear the advanced filter2509onClear()2510{2511    if (window.confirm(_tr('are_you_sure'))) {2512        this.$("textarea#filter").value = "";2513        this.clearMessages();2514        this.changed = true;2515        this.updateUnsavedWarning();2516    }2517}2518// Convert the traditional (mouse-driven) filter into a filter expression string2519onConvertTraditionalFilter()2520{2521    if (!window.confirm(_tr('are_you_sure')))2522        return;2523    const filters = this.getFilters();2524    if (filters === null || filters === undefined || filters.length == 0) {2525        window.alert(_tr('traditional_filter_is_empty'));2526        return;2527    }2528    const result = this.convertTraditionalFilter(filters);2529    if (result === false || result === null) {2530        window.alert("Could not compile the filter. See the console for details, then contact Opinsys support.");2531        return;2532    }2533    this.$("textarea#filter").value = result;2534    this.clearMessages();2535    this.changed = true;2536    this.updateUnsavedWarning();2537}2538// Advanced filter string has changed2539onAdvancedInput()2540{2541    this.changed = true;2542    this.updateUnsavedWarning();2543}2544// Copy the column name to the advanced filter string box2545onClickColumnName(e)2546{2547    e.preventDefault();2548    this.$("textarea#filter").value += e.target.dataset.column + " ";2549}2550updateUnsavedWarning()2551{2552    let legend = this.$("fieldset legend");2553    if (!legend)2554        return;2555    let html = _tr('tabs.filtering.expression_title');2556    if (this.changed)2557        html += ` <span class="unsaved">[${_tr('tabs.filtering.unsaved')}]</span>`;2558    legend.innerHTML = html;2559}2560generateColumnHelp()2561{2562    const COLUMN_TYPES = {2563        [ColumnType.BOOL]: _tr('tabs.filtering.column_list.type_bool'),2564        [ColumnType.NUMERIC]: _tr('tabs.filtering.column_list.type_numeric'),2565        [ColumnType.UNIXTIME]: _tr('tabs.filtering.column_list.type_unixtime'),2566        [ColumnType.STRING]: _tr('tabs.filtering.column_list.type_string'),2567    };2568    let html =2569`<table class="commonTable columnHelp"><thead><tr>2570<th>${_tr('tabs.filtering.column_list.pretty_name')}</th>2571<th>${_tr('tabs.filtering.column_list.database_name')}</th>2572<th>${_tr('tabs.filtering.column_list.type')}</th>2573<th>${_tr('tabs.filtering.column_list.operators')}</th>2574<th>${_tr('tabs.filtering.column_list.nullable')}</th>2575</tr></thead><tbody>`;2576    let columnNames = [];2577    for (const key of Object.keys(this.plainColumnDefinitions))2578        columnNames.push([key, this.columnTitles[key]]);2579    columnNames.sort((a, b) => { return a[1].localeCompare(b[1]) });2580    for (const col of columnNames) {2581        html += `<tr><td>${col[1]}</td><td>`;2582        const nullable = this.plainColumnDefinitions[col[0]].flags & ColumnFlag.F_NULLABLE;2583        let fields = Array.from(this.columnDefinitions.getAliases(col[0]));2584        fields.sort();2585        fields.unshift(col[0]);2586        html += fields.map((f) => `<a href="#" data-column="${f}">${f}</a>`).join("<br>");2587        html += "</td>";2588        const type = this.plainColumnDefinitions[col[0]].type;2589        html += `<td>${COLUMN_TYPES[type]}</td>`;2590        html += "<td>";2591        const ops = Array.from(ALLOWED_OPERATORS[type]);2592        for (let i = 0, j = ops.length; i < j; i++) {2593            if (ops[i] == "!!" && !nullable)2594                continue;2595            html += `<code>${escapeHTML(ops[i])}</code>`;2596            if (i + 1 < j)2597                html += " ";2598        }2599        html += "</td>";2600        if (nullable)2601            html += `<td>${_tr('tabs.filtering.column_list.is_nullable')}</td>`;2602        else html += "<td></td>";2603        html += "</tr>";2604    }2605    html += "</tbody></table>";2606    let cont = this.$("div#columnList");2607    // Remove old event handlers first2608    if (cont.firstChild)2609        for (let a of cont.querySelectorAll("a"))2610            a.removeEventListener("click", this.onClickColumnName);2611    cont.innerHTML = html;2612    // Then set up new event handlers2613    for (let a of cont.querySelectorAll("a"))2614        a.addEventListener("click", this.onClickColumnName);2615}2616clearMessages()2617{2618    this.$("#messages").innerHTML = `<p class="margin-0 padding-0">${_tr('tabs.filtering.no_messages')}</p>`;2619}2620// Update the advanced filter compilation messages box2621listMessages(logger)2622{2623    if (logger.empty())2624        return;2625    let html =2626`<table class="commonTable messages width-100p"><thead><tr>2627<th>${_tr('tabs.filtering.row')}</th>2628<th>${_tr('tabs.filtering.column')}</th>2629<th>${_tr('tabs.filtering.message')}</th>2630</tr></thead><tbody>`;2631    // The messages aren't necessarily in any particular order, sort them2632    const sorted = [...logger.messages].sort(function(a, b) { return a.row - b.row || a.col - b.col });2633    for (const e of sorted) {2634        let cls = [];2635        if (e.type == 'error')2636            cls.push("error");2637        html +=2638`<tr class="${cls.join(' ')}" data-pos="${e.pos}" data-len="${e.len}">2639<td class="minimize-width align-center">${e.row == -1 ? "" : e.row}</td>2640<td class="minimize-width align-center">${e.col == -1 ? "" : e.col}</td>`;2641        html += "<td>";2642        html += _tr('tabs.filtering.' + e.type) + ": ";2643        html += _tr('tabs.filtering.messages.' + e.message);2644        if (e.extra !== null)2645            html += `<br>(${e.extra})`;2646        html += "</td></tr>";2647    }2648    html += "</tbody></table>";2649    this.$("#messages").innerHTML = html;2650    // Add event listeners. I'm 99% certain this leaks memory, but I'm not sure how to fix it.2651    for (let row of this.$all(`table.messages tbody tr`))2652        row.addEventListener("click", (e) => this.highlightMessage(e));2653}2654highlightMessage(e)2655{2656    // Find the target table row. Using "pointer-events" to pass through clicks works, but2657    // it makes browsers not display the "text" cursor when hovering the table and that is2658    // just wrong.2659    let elem = e.target;2660    while (elem && elem.nodeName != "TR")2661        elem = elem.parentNode;2662    if (!elem) {2663        console.error("highlightMessage(): can't find the clicked table row");2664        return;2665    }2666    // Highlight the target2667    const pos = parseInt(elem.dataset.pos, 10),2668          len = parseInt(elem.dataset.len, 10);2669    let t = this.$("textarea#filter");2670    if (!t) {2671        console.error("highlightMessage(): can't find the textarea element");2672        return;2673    }2674    t.focus();2675    if (len == -1) {2676        // Move the cursor to the end2677        t.setSelectionRange(t.value.length, t.value.length);2678    } else {2679        t.selectionStart = pos;2680        t.selectionEnd = pos + len;2681    }2682}...cssTokenizer.js
Source:cssTokenizer.js  
...247        consume();248        return new DashMatchToken();249      } else if(next() == 0x7c) {250        consume();251        return new ColumnToken();252      } else {253        return new DelimToken(code);254      }255    }256    else if(code == 0x7d) return new CloseCurlyToken();257    else if(code == 0x7e) {258      if(next() == 0x3d) {259        consume();260        return new IncludeMatchToken();261      } else {262        return new DelimToken(code);263      }264    }265    else if(digit(code)) {266      reconsume();267      return consumeANumericToken();268    }269    else if(namestartchar(code)) {270      reconsume();271      return consumeAnIdentlikeToken();272    }273    else if(eof()) return new EOFToken();274    else return new DelimToken(code);275  };276  var consumeComments = function() {277    while(next(1) == 0x2f && next(2) == 0x2a) {278      consume(2);279      while(true) {280        consume();281        if(code == 0x2a && next() == 0x2f) {282          consume();283          break;284        } else if(eof()) {285          parseerror();286          return;287        }288      }289    }290  };291  var consumeANumericToken = function() {292    var num = consumeANumber();293    if(wouldStartAnIdentifier(next(1), next(2), next(3))) {294      var token = new DimensionToken();295      token.value = num.value;296      token.repr = num.repr;297      token.type = num.type;298      token.unit = consumeAName();299      return token;300    } else if(next() == 0x25) {301      consume();302      var token = new PercentageToken();303      token.value = num.value;304      token.repr = num.repr;305      return token;306    } else {307      var token = new NumberToken();308      token.value = num.value;309      token.repr = num.repr;310      token.type = num.type;311      return token;312    }313  };314  var consumeAnIdentlikeToken = function() {315    var str = consumeAName();316    if(str.toLowerCase() == "url" && next() == 0x28) {317      consume();318      while(whitespace(next(1)) && whitespace(next(2))) consume();319      if(next() == 0x22 || next() == 0x27) {320        return new FunctionToken(str);321      } else if(whitespace(next()) && (next(2) == 0x22 || next(2) == 0x27)) {322        return new FunctionToken(str);323      } else {324        return consumeAURLToken();325      }326    } else if(next() == 0x28) {327      consume();328      return new FunctionToken(str);329    } else {330      return new IdentToken(str);331    }332  };333  var consumeAStringToken = function(endingCodePoint) {334    if(endingCodePoint === undefined) endingCodePoint = code;335    var string = "";336    while(consume()) {337      if(code == endingCodePoint || eof()) {338        return new StringToken(string);339      } else if(newline(code)) {340        parseerror();341        reconsume();342        return new BadStringToken();343      } else if(code == 0x5c) {344        if(eof(next())) {345          donothing();346        } else if(newline(next())) {347          consume();348        } else {349          string += stringFromCode(consumeEscape())350        }351      } else {352        string += stringFromCode(code);353      }354    }355  };356  var consumeAURLToken = function() {357    var token = new URLToken("");358    while(whitespace(next())) consume();359    if(eof(next())) return token;360    while(consume()) {361      if(code == 0x29 || eof()) {362        return token;363      } else if(whitespace(code)) {364        while(whitespace(next())) consume();365        if(next() == 0x29 || eof(next())) {366          consume();367          return token;368        } else {369          consumeTheRemnantsOfABadURL();370          return new BadURLToken();371        }372      } else if(code == 0x22 || code == 0x27 || code == 0x28 || nonprintable(code)) {373        parseerror();374        consumeTheRemnantsOfABadURL();375        return new BadURLToken();376      } else if(code == 0x5c) {377        if(startsWithAValidEscape()) {378          token.value += stringFromCode(consumeEscape());379        } else {380          parseerror();381          consumeTheRemnantsOfABadURL();382          return new BadURLToken();383        }384      } else {385        token.value += stringFromCode(code);386      }387    }388  };389  var consumeEscape = function() {390    // Assume the the current character is the \391    // and the next code point is not a newline.392    consume();393    if(hexdigit(code)) {394      // Consume 1-6 hex digits395      var digits = [code];396      for(var total = 0; total < 5; total++) {397        if(hexdigit(next())) {398          consume();399          digits.push(code);400        } else {401          break;402        }403      }404      if(whitespace(next())) consume();405      var value = parseInt(digits.map(function(x){return String.fromCharCode(x);}).join(''), 16);406      if( value > maximumallowedcodepoint ) value = 0xfffd;407      return value;408    } else if(eof()) {409      return 0xfffd;410    } else {411      return code;412    }413  };414  var areAValidEscape = function(c1, c2) {415    if(c1 != 0x5c) return false;416    if(newline(c2)) return false;417    return true;418  };419  var startsWithAValidEscape = function() {420    return areAValidEscape(code, next());421  };422  var wouldStartAnIdentifier = function(c1, c2, c3) {423    if(c1 == 0x2d) {424      return namestartchar(c2) || c2 == 0x2d || areAValidEscape(c2, c3);425    } else if(namestartchar(c1)) {426      return true;427    } else if(c1 == 0x5c) {428      return areAValidEscape(c1, c2);429    } else {430      return false;431    }432  };433  var startsWithAnIdentifier = function() {434    return wouldStartAnIdentifier(code, next(1), next(2));435  };436  var wouldStartANumber = function(c1, c2, c3) {437    if(c1 == 0x2b || c1 == 0x2d) {438      if(digit(c2)) return true;439      if(c2 == 0x2e && digit(c3)) return true;440      return false;441    } else if(c1 == 0x2e) {442      if(digit(c2)) return true;443      return false;444    } else if(digit(c1)) {445      return true;446    } else {447      return false;448    }449  };450  var startsWithANumber = function() {451    return wouldStartANumber(code, next(1), next(2));452  };453  var consumeAName = function() {454    var result = "";455    while(consume()) {456      if(namechar(code)) {457        result += stringFromCode(code);458      } else if(startsWithAValidEscape()) {459        result += stringFromCode(consumeEscape());460      } else {461        reconsume();462        return result;463      }464    }465  };466  var consumeANumber = function() {467    var repr = [];468    var type = "integer";469    if(next() == 0x2b || next() == 0x2d) {470      consume();471      repr += stringFromCode(code);472    }473    while(digit(next())) {474      consume();475      repr += stringFromCode(code);476    }477    if(next(1) == 0x2e && digit(next(2))) {478      consume();479      repr += stringFromCode(code);480      consume();481      repr += stringFromCode(code);482      type = "number";483      while(digit(next())) {484        consume();485        repr += stringFromCode(code);486      }487    }488    var c1 = next(1), c2 = next(2), c3 = next(3);489    if((c1 == 0x45 || c1 == 0x65) && digit(c2)) {490      consume();491      repr += stringFromCode(code);492      consume();493      repr += stringFromCode(code);494      type = "number";495      while(digit(next())) {496        consume();497        repr += stringFromCode(code);498      }499    } else if((c1 == 0x45 || c1 == 0x65) && (c2 == 0x2b || c2 == 0x2d) && digit(c3)) {500      consume();501      repr += stringFromCode(code);502      consume();503      repr += stringFromCode(code);504      consume();505      repr += stringFromCode(code);506      type = "number";507      while(digit(next())) {508        consume();509        repr += stringFromCode(code);510      }511    }512    var value = convertAStringToANumber(repr);513    return {type:type, value:value, repr:repr};514  };515  var convertAStringToANumber = function(string) {516    // CSS's number rules are identical to JS, afaik.517    return +string;518  };519  var consumeTheRemnantsOfABadURL = function() {520    while(consume()) {521      if(code == 0x29 || eof()) {522        return;523      } else if(startsWithAValidEscape()) {524        consumeEscape();525        donothing();526      } else {527        donothing();528      }529    }530  };531  var iterationCount = 0;532  while(!eof(next())) {533    tokens.push(consumeAToken());534    iterationCount++;535    if(iterationCount > str.length*2) return "I'm infinite-looping!";536  }537  return tokens;538}539function CSSParserToken() { throw "Abstract Base Class"; }540CSSParserToken.prototype.toJSON = function() {541  return {token: this.tokenType};542}543CSSParserToken.prototype.toString = function() { return this.tokenType; }544CSSParserToken.prototype.toSource = function() { return ''+this; }545function BadStringToken() { return this; }546BadStringToken.prototype = Object.create(CSSParserToken.prototype);547BadStringToken.prototype.tokenType = "BADSTRING";548function BadURLToken() { return this; }549BadURLToken.prototype = Object.create(CSSParserToken.prototype);550BadURLToken.prototype.tokenType = "BADURL";551function WhitespaceToken() { return this; }552WhitespaceToken.prototype = Object.create(CSSParserToken.prototype);553WhitespaceToken.prototype.tokenType = "WHITESPACE";554WhitespaceToken.prototype.toString = function() { return "WS"; }555WhitespaceToken.prototype.toSource = function() { return " "; }556function CDOToken() { return this; }557CDOToken.prototype = Object.create(CSSParserToken.prototype);558CDOToken.prototype.tokenType = "CDO";559CDOToken.prototype.toSource = function() { return "<!--"; }560function CDCToken() { return this; }561CDCToken.prototype = Object.create(CSSParserToken.prototype);562CDCToken.prototype.tokenType = "CDC";563CDCToken.prototype.toSource = function() { return "-->"; }564function ColonToken() { return this; }565ColonToken.prototype = Object.create(CSSParserToken.prototype);566ColonToken.prototype.tokenType = ":";567function SemicolonToken() { return this; }568SemicolonToken.prototype = Object.create(CSSParserToken.prototype);569SemicolonToken.prototype.tokenType = ";";570function CommaToken() { return this; }571CommaToken.prototype = Object.create(CSSParserToken.prototype);572CommaToken.prototype.tokenType = ",";573function GroupingToken() { throw "Abstract Base Class"; }574GroupingToken.prototype = Object.create(CSSParserToken.prototype);575function OpenCurlyToken() { this.value = "{"; this.mirror = "}"; return this; }576OpenCurlyToken.prototype = Object.create(GroupingToken.prototype);577OpenCurlyToken.prototype.tokenType = "{";578function CloseCurlyToken() { this.value = "}"; this.mirror = "{"; return this; }579CloseCurlyToken.prototype = Object.create(GroupingToken.prototype);580CloseCurlyToken.prototype.tokenType = "}";581function OpenSquareToken() { this.value = "["; this.mirror = "]"; return this; }582OpenSquareToken.prototype = Object.create(GroupingToken.prototype);583OpenSquareToken.prototype.tokenType = "[";584function CloseSquareToken() { this.value = "]"; this.mirror = "["; return this; }585CloseSquareToken.prototype = Object.create(GroupingToken.prototype);586CloseSquareToken.prototype.tokenType = "]";587function OpenParenToken() { this.value = "("; this.mirror = ")"; return this; }588OpenParenToken.prototype = Object.create(GroupingToken.prototype);589OpenParenToken.prototype.tokenType = "(";590function CloseParenToken() { this.value = ")"; this.mirror = "("; return this; }591CloseParenToken.prototype = Object.create(GroupingToken.prototype);592CloseParenToken.prototype.tokenType = ")";593function IncludeMatchToken() { return this; }594IncludeMatchToken.prototype = Object.create(CSSParserToken.prototype);595IncludeMatchToken.prototype.tokenType = "~=";596function DashMatchToken() { return this; }597DashMatchToken.prototype = Object.create(CSSParserToken.prototype);598DashMatchToken.prototype.tokenType = "|=";599function PrefixMatchToken() { return this; }600PrefixMatchToken.prototype = Object.create(CSSParserToken.prototype);601PrefixMatchToken.prototype.tokenType = "^=";602function SuffixMatchToken() { return this; }603SuffixMatchToken.prototype = Object.create(CSSParserToken.prototype);604SuffixMatchToken.prototype.tokenType = "$=";605function SubstringMatchToken() { return this; }606SubstringMatchToken.prototype = Object.create(CSSParserToken.prototype);607SubstringMatchToken.prototype.tokenType = "*=";608function ColumnToken() { return this; }609ColumnToken.prototype = Object.create(CSSParserToken.prototype);610ColumnToken.prototype.tokenType = "||";611function EOFToken() { return this; }612EOFToken.prototype = Object.create(CSSParserToken.prototype);613EOFToken.prototype.tokenType = "EOF";614EOFToken.prototype.toSource = function() { return ""; }615function DelimToken(code) {616  this.value = stringFromCode(code);617  return this;618}619DelimToken.prototype = Object.create(CSSParserToken.prototype);620DelimToken.prototype.tokenType = "DELIM";621DelimToken.prototype.toString = function() { return "DELIM("+this.value+")"; }622DelimToken.prototype.toJSON = function() {...utils.js
Source:utils.js  
1const _MS_PER_DAY = 1000 * 60 * 60 * 24;2const path = require('path');3const fs = require('fs');4module.exports = function () {5    this.strToBase64 = function (str) {6        var buff = new Buffer(str);7        return buff.toString("base64");8    };9    this.base64ToStr = function (encStr) {10        var buff = new Buffer(encStr, "base64");11        return buff.toString("utf-8");12    };13    /**14    * 15    * @param {String} dirPath : ë¹ì¸ ëë í ë¦¬ ê²½ë¡ 16    * @param {String} exceptionFiles : ìì íì§ ìì íì¼ ì´ë¦ ë°°ì´17    */18    this.clearDir = async (dirPath, targetExt, exceptionFiles) => {19       const dir = await fs.promises.readdir(dirPath);20       const unlinkPromises = dir.map(file => {21           if(exceptionFiles instanceof Array) {22               if(exceptionFiles.filter(name => name == file).length === 0 && path.extname(file) == targetExt)23                   fs.promises.unlink(path.resolve(dirPath, file));24   25           } else if (exceptionFiles === undefined)26               if(path.extname(file) == targetExt)27                   fs.promises.unlink(path.resolve(dirPath, file))28       });29       return Promise.all(unlinkPromises);30   }31    /**32     * Csvíì¼ì ì½ì´ ì ë ¬í íì ìµì  ìì´ë ì´í를 ê°ì ¸ì¨ë¤33     *34     * @param {Buffer} fileBuffer : ì½ì íì¼ ë²í¼35     * @param {string} rowToken : CSVíì¼ì í ë¶ë¦¬ì36     * @param {string} columnToken : CSV íì¼ì ì´ ë¶ë¦¬ì37     * @param {Array} sortColumnList : ì ë ¬ ëì 컬ë¼ë¤ì´ ë´ê¸´ 리ì¤í¸38     * @param {string} latestId : ëì 컬ë¼ì ìµì  ìì´ë, ì´ê²ì ì´ê³¼íë ê²ë§ ë°ííë¤39     * @param {string} compareatorGenerator : ì ë ¬ í¨ì를 ìì±íë í¨ì40     */41    this.processCsvAndReturnUpdatedData = function (42        fileBuffer,43        rowToken,44        columnToken,45        sortColumnList,46        latestId,47        compareatorGenerator48    ) {49        const fileStr = fileBuffer.toString();50        const rowIndex = fileStr.split(columnToken, 1)[0];51        const rowData = fileStr.substring(rowIndex.length + 1, fileStr.length);52        const indexArr = rowIndex.split(rowToken).map((str) => str.trim());53        let dataList = [];54        rowData.split(columnToken).map((row) => {55            const rowArr = row.split(rowToken);56            //ì못ë ë°ì´í°ë 거른ë¤57            if (rowArr.length !== indexArr.length) {58                return;59            }60            const dataObj = new Object();61            // ì¸ë±ì¤ë¥¼ í¤ë¡ ê°ì§ë ê°ì²´ ìì±62            for (let i = 0; i < indexArr.length; i++) {63                dataObj[indexArr[i]] = rowArr[i];64            }65            dataList.push(dataObj);66        });67        dataList = dataList.sort(compareatorGenerator(...sortColumnList));68        let lastRowIdx =69            dataList.findIndex((elemnt) => elemnt[sortColumnList[0]] == latestId) - 1;70        if(lastRowIdx+1 == -1) {71            lastRowIdx = dataList.length-1;72        }73        console.log(74            `Csv file parsing complete, total len : ${75                dataList.length76            }, updated : ${lastRowIdx + 1}`77        );78        return dataList.slice(0, lastRowIdx+1);79    };80    /**81     * íììì ë´ì íì¼ì ë¤ì´ë¡ëê° ëëëì§ íì¸íë¤.82     *83     * @param {string} filePath : ê²ì¬í  íì¼ ê²½ë¡84     * @param {number} timeout : ìê°ì í85     * @returns Promise<true>86     * @throws Error<string> when file dit not exist | download not finishid within timeout87     */88    this.checkFileDownloadedWithinTimeout = function (filePath, timeout) {89        return new Promise(function (resolve, reject) {90            const dir = path.dirname(filePath);91            const basename = path.basename(filePath);92            const watcher = fs.watch(dir, function (eventType, filename) {93                if (eventType === "rename" && filename === basename) {94                    clearTimeout(timer);95                    watcher.close();96                    resolve(true);97                }98            });99            const timer = setTimeout(function () {100                watcher.close();101                reject(102                    new Error(103                        `File : ${filePath} did not exists and was not created during the timeout.`104                    )105                );106            }, timeout);107            fs.access(filePath, fs.constants.R_OK, function (err) {108                if (!err) {109                    clearTimeout(timer);110                    watcher.close();111                    resolve(true);112                }113            });114        });115    };116    this.rangeReverse = function (start, end) {117        if (start === end) return [start];118        return [start, ...rangeReverse(start - 1, end)];119    };120    this.dateDiffInDays = function (a, b) {121        // Discard the time and time-zone information.122        const utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());123        const utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());124        return Math.floor((utc1 - utc2) / _MS_PER_DAY);125    };126    this.wrapAsyncFn = asyncFn => {127        return (async (req, res, next) => {128          try {129            return await asyncFn(req, res, next)130          } catch (error) {131            return next(error)132          }133        })  134      }135    // this.CovMetaData = class {136    //     /**137    //      *138    //      * @param {*} strainOrObj : ë°ì´ë¬ì¤ ì´ë¦ (hCoV-19/Brazil/PE-COV0260/2020)139    //      * @param {*} epi : EPI_ISL ë±ë¡ë²í¸ (EPI_ISL_502875)140    //      * @param {*} date : ë°ë³ì¼ì (2020-06-24)141    //      * @param {*} location : ë°ë³ ìì¹ (South America / Brazil / Pernambuco / Recife)142    //      * @param {*} age : ëì´ (23)143    //      * @param {*} sex : ì±ë³ (Male)144    //      * @param {*} clade : GISAID ííì´ì§ ê³íµ íì (B.1.1.28 (G))145    //      * @param {*} submit_date : ì ì¶ì¼ì (2020-07-31)146    //      */147    //     constructor(strainOrObj, epi, date, location, age, sex, clade, submit_date) {148    //         if(arguments.length > 1) {149    //             this.strain = strainOrObj,150    //             this.gisaid_epi_isl = epi,151    //             this.date = date,152    //             this.age = age,153    //             this.sex = sex,154    //             this.submit_date = submit_date;155    //             let regionInfo = parseLocation(location);156    //             this.region = regionInfo.region;157    //             this.country = regionInfo.country;158    //             this.division = regionInfo.division;159    //             this.location = regionInfo.location;160    //             let cladeInfo = parseClade(clade);161    //             this.pangolin_lineage = cladeInfo.pangolin_lineage;162    //             this.GISAID_clade = cladeInfo.GISAID_clade;163    //         } else {164    //             this.strain = strainOrObj.strain,165    //             this.gisaid_epi_isl = strainOrObj.epi,166    //             this.date = strainOrObj.date,167    //             this.age = strainOrObj.age,168    //             this.sex = strainOrObj.sex,169    //             this.submit_date = strainOrObj.submit_date;170    //             let regionInfo = parseLocation(strainOrObj.location);171    //             this.region = regionInfo.region;172    //             this.country = regionInfo.country;173    //             this.division = regionInfo.division;174    //             this.location = regionInfo.location;175    //             let cladeInfo = parseClade(strainOrObj.clade);176    //             this.pangolin_lineage = cladeInfo.pangolin_lineage;177    //             this.GISAID_clade = cladeInfo.GISAID_clade;178    //         }179    //     }180    // }181    // /**182    //  * GISAID ì ì ì ë³´ IDì¸ EPI_ISLì ë§ë ë¤183    //  *184    //  * @param {Number or String} postfix : ISL ë¤ì ì«ì185    //  * @return {String} : EPI_ISL_${postfix}186    //  */187    // this.numberToEpi = function(postfix) {188    //     return "EPI_ISL_"+postfix;189    // }190    // this.epiToNumber = function(epi) {191    //     let reg = RegExp('^EPI_ISL_[0-9]*$');192    //     if(!reg.test(epi)) {193    //         return null;194    //     }195    //     epi = epi.replace("EPI_ISL_","")196    //     return parseInt(epi);197    // }198    // this.RegionInfo = class {199    //     constructor(region, country, division, location) {200    //         this.region = region,201    //         this.country = country,202    //         this.division = division,203    //         this.location = location204    //     }205    // }206    // this.CladeInfo = class {207    //     constructor(lineage, clade) {208    //          this.pangolin_lineage = lineage209    //          this.GISAID_clade = clade210    //     }211    // }212    // /**213    //  * GISAID web dbì locationì metadata íìì¼ë¡ íì±íë¤214    //  *215    //  * @param {string} location : North America / USA / Texas / Houston216    //  * @returns {RegionInfo} ë¦¬ì  ì ë³´ ê°ì²´217    //  */218    // this.parseLocation = function(location) {219    //     if (location === undefined || location === null) {220    //         return new RegionInfo();221    //     } else if (! location instanceof String) {222    //         console.log(`NODE : Warn:: function parseLocation() got non-string parameter, value : ${location}`);223    //         return new RegionInfo();224    //     }225    //     let splitList = location.split(' / ');226    //     splitList.map(str => str.trim())227    //     return new RegionInfo(...splitList);228    // }229    // /**230    //  *231    //  * @param {string} clade : B.1.2 (GH) íì232    //  * @returns {CladeInfo} íì±ë í´ë ì´ë ê°ì²´233    //  */234    // this.parseClade = function(clade) {235    //     let splitList = clade.split(" ");236    //     splitList[1] = splitList[1].replace("(","").replace(")","");237    //     splitList.map(str => str.trim())238    //     return new CladeInfo(splitList[0], splitList[1])239    // }240    // /**241    //  * EPI_ISL ì incë§í¼ ì°ì  ëí기를 ì¤ííë¤242    //  * @param {*} epi243    //  * @param {*} inc244    //  */245    // this.addIntToEpiIsl = function(epi, inc) {246    //     return "EPI_ISL_" + (epiToNumber(epi)+inc);247    // }248    // /**249    //  *250    //  * @param {Date} date251    //  * @param {Number} inc252    //  */253    // this.addOneDayToDate = function (date) {254    //     return new Date(date.getTime() + _MS_PER_DAY);255    // }...PartialQueryStore.spec.js
Source:PartialQueryStore.spec.js  
...19function _updateInsertIndex(newIndex) {20  PartialQueryActions.updateInsertIndex(newIndex);21}22function _colToken(name) {23  return new ColumnToken('foo_table', {name: name || 'bar'});24}25function _colAliasToken(colName, aliasName) {26  return new AliasToken(27    _colToken(colName),28    aliasName29  );30}31function _aliasToken(subToken, aliasName) {32  return new AliasToken(33    subToken,34    aliasName35  );36}37function _keywordToken(name) {...QueryKeyboard.js
Source:QueryKeyboard.js  
...200    );201  },202  _onColPressed: function(tableName, colInfo) {203    this._addToken(204      new ColumnToken(tableName, colInfo),205    );206  },207});208var styles = StyleSheet.create({209  queryText: {210    color: Colors.TEXT_BASE,211  },212  columnsContainer: {213    flexWrap: 'wrap',214    flexDirection: 'row',215  },216  columnText: {217    color: Colors.TEXT_BASE,218    fontSize: 16,...index.js
Source:index.js  
1import React, { useState, useEffect } from "react";2import { Container, Row, Col } from "react-bootstrap";3import { useParams } from "react-router-dom";4import {5  firebase,6  user,7  updateCancelledTokens,8  unsubscribeBuyers,9} from "../../config/firebase";10import useWebAnimations, { shakeY } from "@wellyshen/use-web-animations";11import { connect } from "react-redux";12import "./myTokens.css";1314const MyTokens = (props) => {15  const [myTokens, setMyTokens] = useState([]);16  let userId;17  1819  const { ref: heading } = useWebAnimations({20    ...shakeY,21    timing: {22      delay: 500,23      duration: 1000 * 20,24      iterations: Infinity,25    },26  });27  const getMyToken = () => {28    firebase29      .firestore()30      .collection("buyers")31      .where("buyerId", "==", props.userInfo.userId)32      .onSnapshot((data) => {33        let arr = [];34        data.forEach((x) => {35          let temp = x.data();36          temp.id = x.id;37          arr.push(temp);38        });39        setMyTokens(arr);40      });41  };42  useEffect(() => {43    getMyToken();44    return () => {45      unsubscribeBuyers();46    };47  }, []);48  const cancel = async (docId, compId, totalToken, myToken) => {49    firebase.firestore().collection("buyers").doc(docId).delete();50    await updateCancelledTokens(docId, compId, totalToken, myToken);51  };52  return (53    <div className="custom-shape-divider-top-1600808309">54      <svg55        data-name="Layer 1"56        xmlns="http://www.w3.org/2000/svg"57        viewBox="0 0 1200 120"58        preserveAspectRatio="none"59      >60        <path61          d="M321.39,56.44c58-10.79,114.16-30.13,172-41.86,82.39-16.72,168.19-17.73,250.45-.39C823.78,31,906.67,72,985.66,92.83c70.05,18.48,146.53,26.09,214.34,3V0H0V27.35A600.21,600.21,0,0,0,321.39,56.44Z"62          className="shape-fill"63        ></path>64      </svg>65      <Container>66        <p className="text-center text-light display-3 heading" ref={heading}>67          Queue App68        </p>69        <h1 className="text-center text-light compheading">*** My Tokens***</h1>70      </Container>71      <div className="content">72        <Row>73          <Container>74            <Col md="12" key={11244}>75              <div className="columnToken">76                <div className="compName font-weight-bold">Company Name</div>77                <div className="compToken font-weight-bold">Token Number </div>78                <div className="text-danger font-weight-bold">Cancel Token</div>79              </div>80            </Col>81            {myTokens.length > 0 &&82              myTokens.map((x) => {83                const key = Math.random() * 100;84                return (85                  <Col md="12" key={key}>86                    <div className="columnToken">87                      <div className="compName">88                        <img89                          className="compImageToken"90                          src={x.companyImage}91                          alt="compnayProfile"92                        />93                        {x.companyName}94                      </div>95                      <div className="compToken"> {x.tokenNumber}</div>96                      <div97                        className="btn btn-outline-danger"98                        onClick={() =>99                          cancel(100                            x.id,101                            x.companyId,102                            x.totalTokens,103                            x.tokenNumber104                          )105                        }106                      >107                        Cancel Token108                      </div>109                    </div>110                  </Col>111                );112              })}113          </Container>114        </Row>115      </div>116    </div>117  );118};119const mapStateToProps = (state) => {120  return {121    userInfo: state.authReducer.user,122  };123};
...ColumnToken.js
Source:ColumnToken.js  
1"use strict";2var AbstractToken = require('../tokens/AbstractToken');3var TokenTypes = require('../constants/TokenTypes');4class ColumnToken extends AbstractToken {5  constructor(6   tableName,7   colInfo8  ) {9    super();10    this.tableName = tableName;11    this.colInfo = colInfo;12  }13  getType() {14    return TokenTypes.COLUMN;15  }16  toString() {17    return `18      COLUMN from Table "${this.tableName}" 19      info: ${JSON.stringify(this.colInfo)}`;20  }21  isColumnLike() {22    return true;23  }24  exportToQuery() {25    if (this._isBeforeColumn()) {26      return this.colInfo.name + ',';27    }28    return this.colInfo.name;29  }30}...Using AI Code Generation
1const { chromium } = require('playwright');2(async () => {3  const browser = await chromium.launch();4  const context = await browser.newContext();5  const page = await context.newPage();6  const value = await page.$eval('table', (table) => {7    return table.rows[2].cells[1].innerText;8  });9  console.log(value);10  await browser.close();11})();12Your name to display (optional):Using AI Code Generation
1const { ColumnToken } = require('playwright');2const { test, expect } = require('@playwright/test');3test('test', async ({ page }) => {4  const searchInput = page.locator('#tsf > div:nth-child(2) > div > div.RNNXgb > div > div.a4bIc > input');5  await searchInput.type('Hello World');6  const searchButton = page.locator('#tsf > div:nth-child(2) > div > div.FPdoLc.VlcLAe > center > input[type="submit"]:nth-child(1)');7  await searchButton.click();Using AI Code Generation
1const { ColumnToken } = require('playwright-core/lib/server/selectors/selectorEngine');2const { Page } = require('playwright-core/lib/server/page');3const { JSHandle } = require('playwright-core/lib/server/jsHandle');4const { ElementHandle } = require('playwright-core/lib/server/dom');5const page = new Page();6const columnToken = new ColumnToken();7const handle = new JSHandle(page, 'window', null);8const elementHandle = new ElementHandle(page, handle, 'body');9const result = columnToken.queryAll(elementHandle, '1');10console.log('result: ', result);Using AI Code Generation
1const { ColumnToken } = require('playwright/lib/internal/selectorParser');2const { Selector } = require('playwright/lib/internal/selectorEngine');3const { parseSelector } = require('playwright/lib/internal/selectorParser');4const { parse } = require('playwright/lib/internal/selectorParser');5const selector = parseSelector('column=3');6console.log(selector);7const { ColumnToken } = require('playwright/lib/internal/selectorParser');8const { Selector } = require('playwright/lib/internal/selectorEngine');9const { parseSelector } = require('playwright/lib/internal/selectorParser');10const { parse } = require('playwright/lib/internal/selectorParser');11const selector = parseSelector('column=3');12console.log(selector);13const { ColumnToken } = require('playwright/lib/internal/selectorParser');14const { Selector } = require('playwright/lib/internal/selectorEngine');15const { parseSelector } = require('playwright/lib/internal/selectorParser');16const { parse } = require('playwright/lib/internal/selectorParser');17const selector = parseSelector('column=3');18console.log(selector);19const { ColumnToken } = require('playwright/lib/internal/selectorParser');20const { Selector } = require('playwright/lib/internal/selectorEngine');21const { parseSelector } = require('playwright/lib/internal/selectorParser');22const { parse } = require('playwright/lib/internal/selectorParser');23const selector = parseSelector('column=3');24console.log(selector);25const { ColumnToken } = require('playwright/lib/internal/selectorParser');26const { Selector } = require('playwright/lib/internal/selectorEngine');27const { parseSelector } = require('playwright/lib/internal/selectorParser');28const { parse } = require('playwright/lib/internal/selectorParser');29const selector = parseSelector('column=3');30console.log(selector);31const { ColumnToken } = require('playwright/lib/internal/selectorParser');32const { Selector } = require('playwright/lib/internal/selectorEngine');33const { parseSelector } = require('playwright/lib/internal/selectorParser');34const { parse } =Using AI Code Generation
1const { ColumnToken } = require("playwright/lib/server/locator");2const { test } = require("@playwright/test");3test.describe("test", () => {4  test("test", async ({ page }) => {5    await page.click(new ColumnToken("Search"));6  });7});8const { ColumnToken } = require("playwright/lib/server/locator");9const { test } = require("@playwright/test");10test.describe("test", () => {11  test("test", async ({ page }) => {12    await page.click(new ColumnToken("Search"));13  });14});15const { ColumnToken } = require("playwright/lib/server/locator");16const { test } = require("@playwright/test");17test.describe("test", () => {18  test("test", async ({ page }) => {19    await page.click(new ColumnToken("Search"));20  });21});22const { test } = require("@playwright/test");23test.describe("test", () => {24  test("test", async ({ page }) => {25    const elements = await page.$$("a");26    for (const element of elements) {27      const text = await element.innerText();28      if (text.includes("SearchUsing AI Code Generation
1const { chromium } = require('playwright');2(async () => {3  const browser = await chromium.launch();4  const context = await browser.newContext();5  const page = await context.newPage();6  const token = await page.columnToken({x: 100, y: 100});7  console.log(token);8  await browser.close();9})();10{ x: 100, y: 100, column: 4, token: 'playwright' }Using AI Code Generation
1const { ColumnToken } = require('playwright');2const columnToken = new ColumnToken('test', 1);3console.log(columnToken);4const { ColumnToken } = require('playwright/lib/api/stackTrace');5const columnToken = new ColumnToken('test', 1);6console.log(columnToken);7const { ColumnToken } = require('playwright/lib/api/stackTrace.js');8const columnToken = new ColumnToken('test', 1);9console.log(columnToken);10const { ColumnToken } = require('playwright/lib/api/stackTrace.ts');11const columnToken = new ColumnToken('test', 1);12console.log(columnToken);13const { ColumnToken } = require('playwright/lib/api/stackTrace.d.ts');14const columnToken = new ColumnToken('test', 1);15console.log(columnToken);16const { ColumnToken } = require('playwright/lib/api/stackTrace.mjs');17const columnToken = new ColumnToken('test', 1);18console.log(columnToken);19const { ColumnToken } = require('playwright/lib/api/stackTrace.cjs');20const columnToken = new ColumnToken('test', 1);21console.log(columnToken);22const { ColumnToken } = require('playwright/lib/api/stackTrace.js');23const columnToken = new ColumnToken('test', 1);24console.log(columnToken);25const { ColumnToken } = require('playwright/lib/api/stackTrace.ts');26const columnToken = new ColumnToken('test', 1);27console.log(columnToken);28const { ColumnToken } = require('playwright/lib/api/stackTrace.d.ts');29const columnToken = new ColumnToken('test', 1);30console.log(columnToken);31const { ColumnToken } = require('playwright/lib/api/stackTrace.mjs');Using AI Code Generation
1const { chromium } = require('playwright');2const { ColumnToken } = require('playwright/lib/internal/protocol');3(async () => {4  const browser = await chromium.launch();5  const context = await browser.newContext();6  const page = await context.newPage();7  const element = await page.$('text=Get started');8  const columnNumber = await element.evaluate(ColumnToken);9  console.log(columnNumber);10  await browser.close();11})();12const { chromium } = require('playwright');13const { LineToken } = require('playwright/lib/internal/protocol');14(async () => {15  const browser = await chromium.launch();16  const context = await browser.newContext();17  const page = await context.newPage();18  const element = await page.$('text=Get started');19  const lineNumber = await element.evaluate(LineToken);20  console.log(lineNumber);21  await browser.close();22})();23const { chromium } = require('playwright');24const { URLToken } = require('playwright/lib/internal/protocol');25(async () => {26  const browser = await chromium.launch();27  const context = await browser.newContext();28  const page = await context.newPage();29  const element = await page.$('text=Get started');30  const url = await element.evaluate(URLToken);31  console.log(url);32  await browser.close();33})();34const { chromium } = require('playwright');35const { XPathToken } = require('playwright/lib/internal/protocol');36(async () => {37  const browser = await chromium.launch();38  const context = await browser.newContext();39  const page = await context.newPage();40  const element = await page.$('text=Get started');41  const xpath = await element.evaluate(XPathToken);42  console.log(xpath);Using AI Code Generation
1import { ColumnToken } from 'playwright';2const columnToken = new ColumnToken('column');3console.log(columnToken.toString());4import { ColumnToken } from 'playwright';5const columnToken = new ColumnToken('column');6console.log(columnToken.toString());Using AI Code Generation
1const { ColumnToken } = require('@playwright/test');2const columnToken = new ColumnToken('column name', 10);3console.log(columnToken);4const { ColumnToken } = require('@playwright/test');5const columnToken = new ColumnToken('column name', 10);6console.log(columnToken);7const { ColumnToken } = require('@playwright/test');8const columnToken = new ColumnToken('column name', 10);9console.log(columnToken);10const { ColumnToken } = require('@playwright/test');11const columnToken = new ColumnToken('column name', 10);12console.log(columnToken);13const { ColumnToken } = require('@playwright/test');14const columnToken = new ColumnToken('column name', 10);15console.log(columnToken);16const { ColumnToken } = require('@playwright/test');17const columnToken = new ColumnToken('column name', 10);18console.log(columnToken);19const { ColumnToken } = require('@playwright/test');20const columnToken = new ColumnToken('column name', 10);21console.log(columnToken);22const { ColumnToken } = require('@playwright/test');23const columnToken = new ColumnToken('column name', 10);24oa ync require('playwright/lib/internal/protocol');25cons brower = awai chromium.launch);26  cons contx= wait brower.ewContext);27 const await context.newPage();28(anc () => {'playrihtdev'29  constcelemento= nst browser$a'ttxt=Gat st);ted'30  constcunll= nwait alemewt. va.$ate(URLToke');31e=censtl';lg(rl);32awitbrowe.los();33})();34  const lineNumber = await element.evaluate(LineToken);35coconsn browssr = await chromium.launch();36  cont  conchxo =umwait brow er. ewContextr);37q const uire(awawait context.newPage();ight');38cot { URLToken } ='require(play'riyhttdevb'internal/protocol');39constelement= $'txt=Getstarted');40  cnst xpath = await eeet.evaluate(XPath41(aconsole.log(xpathync () => {42onst browser = await chromium.launch();Using AI Code Generation
1  const context = await browser.newContext();2imptett{ CnlumnTak() } from 'playwrght';3costToken=ewColmnTkn('clmn');4conol.log(columnToken.oSt());5import {CumnToen }frm'paywrg';6contnToke =ew ColunTokn('column');7conole.g(lunTokn.oSrg());Using AI Code Generation
1  const url = await element.evaluate(URLToken);2  await browser.close();3})();'@te'4con olh.log( olumnToktn;5const { ColumnToken } = require('@rloywrimht/tist');6monst  olumnTo=er = nequire('playwri';olumncname',o10st { XPathToken } = require('playwright/lib/internal/protocol');7console.log(col  nTokor);8cone ={wCoiu nTokcoe}n=wrequire('@playwrighe/)est');9c;nstoumnToen= new ClumToken('columnnam',10);10cono.log(colunTok);11  const ColumnToklnent = await p'ge.$('text=Get s'arted');12conco xolumnTokhn = new ColumnToken 'column nama't 10);13console.logecolumnTokenl;14.evaluate(XPathToken);15 {CounTok }=requir('@paywright/ts';16 to uscolumnTok nlumneTnColumnTok n('comumn nath', 10);17coosole log(colum aokyngh18const { ColumnToken } = require('playwright/lib/api/stackTrace');19csns columnToken = ne =wrequiCe(m@nToken('tt/teses)t20', 1);21console.log(columnToken);n ame, 1022);23const { Column } = require('playwright/lib/api/stackTrace.js');24const columnToken = new ColumnToken('test', 1);25console.log(columnToken);26cns =requie(@t/tes)Using AI Code Generation
1cs { ColumnToken } =reqire('@plywight/s');2conscoumnTen =ewColmnTokn('columnnam', 10);3const { ColumnToken } = require('@playwright/test');4const columnToken = new ColumnToken('column name', 10);5console.log(columnToken);6const { ColumnToken } = require('@playwright/test');7const columnToken = new ColumnToken('column name', 10);8console.log(columnToken);9const { ColumnToken } = require('@playwright/test');10const columnToken = new ColumnToken('column name', 10);11console.log(columnToken);12const { ColumnToken } = require('@playwright/test');13const columnToken = new ColumnToken('column name', 10);14console.log(columnToken);15const { ColumnToken } = require('@playwright/test');16const columnToken = new ColumnToken('column name', 10);17console.log(columnToken);18const { ColumnToken } = require('@playwright/test');19const columnToken = new ColumnToken('column name', 10);20console.log(columnToken);21const { ColumnToken } = require('@playwright/test');22const columnToken = new ColumnToken('column name', 10);23console.log(columnToken);24const { ColumnToken } = require('@playwright/test');25const columnToken = new ColumnToken('column name', 10);26const { ColumnToken } = require('playwright/lib/api/stackTrace.ts');27const columnToken = new ColumnToken('test', 1);28console.log(columnToken);29const { ColumnToken } = require('playwright/lib/api/stackTrace.d.ts');30const columnToken = new ColumnToken('test', 1);31console.log(columnToken);32const { ColumnToken } = require('playwright/lib/api/stackTrace.mjs');33const columnToken = new ColumnToken('test', 1);34console.log(columnToken);35const { ColumnToken } = require('playwright/lib/api/stackTrace.cjs');36const columnToken = new ColumnToken('test', 1);37console.log(columnToken);38const { ColumnToken } = require('playwright/lib/api/stackTrace.js');39const columnToken = new ColumnToken('test', 1);40console.log(columnToken);41const { ColumnToken } = require('playwright/lib/api/stackTrace.ts');42const columnToken = new ColumnToken('test', 1);43console.log(columnToken);44const { ColumnToken } = require('playwright/lib/api/stackTrace.d.ts');45const columnToken = new ColumnToken('test', 1);46console.log(columnToken);47const { ColumnToken } = require('playwright/lib/api/stackTrace.mjs');Using AI Code Generation
1import { ColumnToken } from 'playwright';2const columnToken = new ColumnToken('column');3console.log(columnToken.toString());4import { ColumnToken } from 'playwright';5const columnToken = new ColumnToken('column');6console.log(columnToken.toString());Using AI Code Generation
1const { ColumnToken } = require("playwright/lib/server/locator");2const { test } = require("@playwright/test");3test.describe("test", () => {4  test("test", async ({ page }) => {5    await page.click(new ColumnToken("Search"));6  });7});8const { ColumnToken } = require("playwright/lib/server/locator");9const { test } = require("@playwright/test");10test.describe("test", () => {11  test("test", async ({ page }) => {12    await page.click(new ColumnToken("Search"));13  });14});15const { ColumnToken } = require("playwright/lib/server/locator");16const { test } = require("@playwright/test");17test.describe("test", () => {18  test("test", async ({ page }) => {19    await page.click(new ColumnToken("Search"));20  });21});22const { test } = require("@playwright/test");23test.describe("test", () => {24  test("test", async ({ page }) => {25    const elements = await page.$$("a");26    for (const element of elements) {27      const text = await element.innerText();28      if (text.includes("SearchLambdaTest’s Playwright tutorial will give you a broader idea about the Playwright automation framework, its unique features, and use cases with examples to exceed your understanding of Playwright testing. This tutorial will give A to Z guidance, from installing the Playwright framework to some best practices and advanced concepts.
Get 100 minutes of automation test minutes FREE!!
