Typescript simple parser

interface ITokenConfig {
    name?: string;
    startsWith?: string;
    endsWith?: string;
    skip?: string;
    filter?: boolean;
    t?: string | string[];
    e?: string;
    b?: string;
    startScope?: string;
    endScope?: string;
}

interface ITokenMatcher extends ITokenConfig {
    name: string;
    scopeName: string;
    regex?: RegExp;
    blockRegex?: RegExp;
}

type IToken = {
    text: string;
    name: string;
    scope?: string;
    offset: number;
    line: number;
    column: number;
};

type ITokenArray = Array<ITokenArray | IToken> & {
    parent?: ITokenArray;
    scope?: string;
};

interface IGrammar {
    name?: string;
    scopes: Record<string, Record<string, ITokenConfig>>;
}

const getTokens = (grammar: IGrammar): ITokenMatcher[] => {
    const tokens: ITokenMatcher[] = [];

    for (const [scopeName, scope] of Object.entries(grammar.scopes)) {
        for (const [name, token] of Object.entries(scope)) {
            tokens.push({
                ...token,
                name,
                scopeName,
                regex: token.e
                    ? new RegExp(`^(?:${token.e})`)
                    : undefined,
                blockRegex: token.b
                    ? new RegExp(token.b, 'm')
                    : undefined
            });
        }
    }

    return tokens;
};

const findEnd = (
    input: string,
    start: number,
    token: ITokenMatcher
): number => {
    const sw = token.startsWith!;
    const ew = token.endsWith!;
    let end = input.indexOf(ew, start + sw.length);

    if (token.skip) {
        while (end >= 0) {
            let p = end - 1;

            while (p >= start + sw.length && input[p] === '\\') {
                p--;
            }

            if ((end - p) % 2 !== 0) break;

            end = input.indexOf(ew, end + ew.length);
        }
    }

    return end < 0 ? input.length : end + ew.length;
};

const matchToken = (
    input: string,
    pos: number,
    token: ITokenMatcher
): number => {
    if (token.t) {
        const values = Array.isArray(token.t) ? token.t : [token.t];
        let length = 0;

        for (const value of values) {
            if (input.startsWith(value, pos)) {
                length = Math.max(length, value.length);
            }
        }

        return length;
    }

    if (token.startsWith && token.endsWith) {
        if (!input.startsWith(token.startsWith, pos)) return 0;
        return findEnd(input, pos, token) - pos;
    }

    if (token.regex) {
        const match = input.slice(pos).match(token.regex);

        if (!match) return 0;
        if (token.blockRegex?.test(match[0])) return 0;

        return match[0].length;
    }

    return 0;
};

const newScope = (grammar: IGrammar) => {
    const allTokens = getTokens(grammar);
    let scopeTokens: ITokenMatcher[] = [];

    const root: ITokenArray = [];
    let current = root;

    const update = () => {
        scopeTokens = allTokens.filter(
            token => token.scopeName === current.scope
        );
    };

    const push = (scope: string) => {
        const child: ITokenArray = [];
        child.parent = current;
        child.scope = scope;
        current.push(child);
        current = child;
        update();
    };

    const pop = () => {
        if (current.parent) current = current.parent;
        update();
    };

    push('default');

    return {
        getTree: () => root,
        getScopeTokens: () => scopeTokens,

        addMatch: (
            token: ITokenMatcher | null,
            text: string,
            offset: number,
            line: number,
            column: number
        ) => {
            if (!token) {
                current.push({
                    text,
                    name: 'UNKNOWN',
                    offset,
                    line,
                    column
                });
                return;
            }

            if (!token.filter) {
                current.push({
                    text,
                    name: token.name,
                    offset,
                    line,
                    column
                });
            }

            if (token.startScope) {
                push(token.startScope);
            }

            if (token.endScope && current.scope === token.endScope) {
                pop();
            }
        }
    };
};

const lex = (input: string, grammar: IGrammar): ITokenArray => {
    const scope = newScope(grammar);

    let pos = 0;
    let line = 1;
    let column = 1;

    while (pos < input.length) {
        let best: ITokenMatcher | null = null;
        let bestLength = 0;

        for (const token of scope.getScopeTokens()) {
            const length = matchToken(input, pos, token);

            if (length > bestLength) {
                best = token;
                bestLength = length;
            }
        }

        if (!best || bestLength === 0) {
            const text = input[pos]!;

            scope.addMatch(
                null,
                text,
                pos,
                line,
                column
            );

            if (text === '\n') {
                line++;
                column = 1;
            } else {
                column++;
            }

            pos++;
            continue;
        }

        const text = input.slice(pos, pos + bestLength);

        scope.addMatch(
            best,
            text,
            pos,
            line,
            column
        );

        for (let i = 0; i < text.length; i++) {
            if (text[i] === '\n') {
                line++;
                column = 1;
            } else {
                column++;
            }
        }

        pos += bestLength;
    }

    return scope.getTree();
};

const grammar: IGrammar = {
    name: 'custom1',

    scopes: {
        default: {
            'comment-line': {
                startsWith: '//',
                endsWith: '\n',
                filter: true
            },

            'comment-block': {
                startsWith: '/*',
                endsWith: '*/',
                filter: true
            },

            'brace-start': {
                t: '(',
                startScope: 'default'
            },

            'brace-end': {
                t: ')',
                endScope: 'default'
            },

            assignment: {
                t: '='
            },

            'bool-operator': {
                t: ['==', '!=']
            },

            'incr-operator': {
                t: ['+=', '-=']
            },

            'math-operator': {
                t: ['+', '-', '*', '/']
            },

            number: {
                e: '[0-9]+\\.?[0-9]*'
            },

            separator: {
                t: ';'
            },

            string: {
                startsWith: '"',
                skip: '\\"',
                endsWith: '"'
            },

            function: {
                t: 'function',
                startScope: 'function'
            },

            name: {
                e: '[_a-zA-Z][_a-zA-Z0-9]*'
            },

            whitespace: {
                e: '\\s+',
                filter: true
            }
        },

        function: {
            'comment-line': {
                startsWith: '//',
                endsWith: '\n',
                filter: true
            },

            'comment-block': {
                startsWith: '/*',
                endsWith: '*/',
                filter: true
            },

            number: {
                e: '[0-9]+\\.?[0-9]*'
            },

            whitespace: {
                e: '\\s+',
                filter: true
            },

            end: {
                t: 'end',
                endScope: 'function'
            }
        }
    }
};

const expr = `
    var a = (1+(2+3));
    // single line comment
    "Hello \\"{0}\\" this is nice";
    /* multiline
       comment */
    work a function 0.5 4 3.22 end 5 6.2 8
`;

const tokens = lex(expr, grammar);

function printTokens(
    tokens: ITokenArray,
    depth = 0
): string[] {
    const result: string[] = [];

    for (const token of tokens) {
        if (Array.isArray(token)) {
            result.push(
                ...printTokens(token, depth + 1)
            );
        } else {
            result.push(
                `${'  '.repeat(depth)}${token.text.trim()} ` +
                `<-(${token.name}) ` +
                `[${token.line}:${token.column}]`
            );
        }
    }

    return result;
}

console.log(printTokens(tokens).join('\n'));

  var <-(name) [2:5]
  a <-(name) [2:9]
  = <-(assignment) [2:11]
  ( <-(brace-start) [2:13]
    1 <-(number) [2:14]
    + <-(math-operator) [2:15]
    ( <-(brace-start) [2:16]
      2 <-(number) [2:17]
      + <-(math-operator) [2:18]
      3 <-(number) [2:19]
      ) <-(brace-end) [2:20]
    ) <-(brace-end) [2:21]
  ; <-(separator) [2:22]
  "Hello \"{0}\" this is nice" <-(string) [4:5]
  ; <-(separator) [4:33]
  work <-(name) [7:5]
  a <-(name) [7:10]
  function <-(function) [7:12]
    0.5 <-(number) [7:21]
    4 <-(number) [7:25]
    3.22 <-(number) [7:27]
    end <-(end) [7:32]
  5 <-(number) [7:36]
  6.2 <-(number) [7:38]
  8 <-(number) [7:42]

25150cookie-checkTypescript simple parser