UNPKG

@danielkalen/simplybind

Version:

Magically simple, framework-less one-way/two-way data binding for frontend/backend in ~5kb.

426 lines (354 loc) 8.74 kB
export class Token { constructor(index, text) { this.index = index; this.text = text; } withOp(op) { this.opKey = op; return this; } withGetterSetter(key) { this.key = key; return this; } withValue(value) { this.value = value; return this; } toString() { return `Token(${this.text})`; } } export class Lexer { lex(text) { let scanner = new Scanner(text); let tokens = []; let token = scanner.scanToken(); while (token) { tokens.push(token); token = scanner.scanToken(); } return tokens; } } export class Scanner { constructor(input) { this.input = input; this.length = input.length; this.peek = 0; this.index = -1; this.advance(); } scanToken() { // Skip whitespace. while (this.peek <= $SPACE) { if (++this.index >= this.length) { this.peek = $EOF; return null; } this.peek = this.input.charCodeAt(this.index); } // Handle identifiers and numbers. if (isIdentifierStart(this.peek)) { return this.scanIdentifier(); } if (isDigit(this.peek)) { return this.scanNumber(this.index); } let start = this.index; switch (this.peek) { case $PERIOD: this.advance(); return isDigit(this.peek) ? this.scanNumber(start) : new Token(start, '.'); case $LPAREN: case $RPAREN: case $LBRACE: case $RBRACE: case $LBRACKET: case $RBRACKET: case $COMMA: case $COLON: case $SEMICOLON: return this.scanCharacter(start, String.fromCharCode(this.peek)); case $SQ: case $DQ: return this.scanString(); case $PLUS: case $MINUS: case $STAR: case $SLASH: case $PERCENT: case $CARET: case $QUESTION: return this.scanOperator(start, String.fromCharCode(this.peek)); case $LT: case $GT: case $BANG: case $EQ: return this.scanComplexOperator(start, $EQ, String.fromCharCode(this.peek), '='); case $AMPERSAND: return this.scanComplexOperator(start, $AMPERSAND, '&', '&'); case $BAR: return this.scanComplexOperator(start, $BAR, '|', '|'); case $NBSP: while (isWhitespace(this.peek)) { this.advance(); } return this.scanToken(); // no default } let character = String.fromCharCode(this.peek); this.error(`Unexpected character [${character}]`); return null; } scanCharacter(start, text) { assert(this.peek === text.charCodeAt(0)); this.advance(); return new Token(start, text); } scanOperator(start, text) { assert(this.peek === text.charCodeAt(0)); assert(OPERATORS.indexOf(text) !== -1); this.advance(); return new Token(start, text).withOp(text); } scanComplexOperator(start, code, one, two) { assert(this.peek === one.charCodeAt(0)); this.advance(); let text = one; if (this.peek === code) { this.advance(); text += two; } if (this.peek === code) { this.advance(); text += two; } assert(OPERATORS.indexOf(text) !== -1); return new Token(start, text).withOp(text); } scanIdentifier() { assert(isIdentifierStart(this.peek)); let start = this.index; this.advance(); while (isIdentifierPart(this.peek)) { this.advance(); } let text = this.input.substring(start, this.index); let result = new Token(start, text); // TODO(kasperl): Deal with null, undefined, true, and false in // a cleaner and faster way. if (OPERATORS.indexOf(text) !== -1) { result.withOp(text); } else { result.withGetterSetter(text); } return result; } scanNumber(start) { assert(isDigit(this.peek)); let simple = (this.index === start); this.advance(); // Skip initial digit. while (true) { // eslint-disable-line no-constant-condition if (!isDigit(this.peek)) { if (this.peek === $PERIOD) { simple = false; } else if (isExponentStart(this.peek)) { this.advance(); if (isExponentSign(this.peek)) { this.advance(); } if (!isDigit(this.peek)) { this.error('Invalid exponent', -1); } simple = false; } else { break; } } this.advance(); } let text = this.input.substring(start, this.index); let value = simple ? parseInt(text, 10) : parseFloat(text); return new Token(start, text).withValue(value); } scanString() { assert(this.peek === $SQ || this.peek === $DQ); let start = this.index; let quote = this.peek; this.advance(); // Skip initial quote. let buffer; let marker = this.index; while (this.peek !== quote) { if (this.peek === $BACKSLASH) { if (!buffer) { buffer = []; } buffer.push(this.input.substring(marker, this.index)); this.advance(); let unescaped; if (this.peek === $u) { // TODO(kasperl): Check bounds? Make sure we have test // coverage for this. let hex = this.input.substring(this.index + 1, this.index + 5); if (!/[A-Z0-9]{4}/.test(hex)) { this.error(`Invalid unicode escape [\\u${hex}]`); } unescaped = parseInt(hex, 16); for (let i = 0; i < 5; ++i) { this.advance(); } } else { unescaped = unescape(this.peek); this.advance(); } buffer.push(String.fromCharCode(unescaped)); marker = this.index; } else if (this.peek === $EOF) { this.error('Unterminated quote'); } else { this.advance(); } } let last = this.input.substring(marker, this.index); this.advance(); // Skip terminating quote. let text = this.input.substring(start, this.index); // Compute the unescaped string value. let unescaped = last; if (buffer !== null && buffer !== undefined) { buffer.push(last); unescaped = buffer.join(''); } return new Token(start, text).withValue(unescaped); } advance() { if (++this.index >= this.length) { this.peek = $EOF; } else { this.peek = this.input.charCodeAt(this.index); } } error(message, offset = 0) { // TODO(kasperl): Try to get rid of the offset. It is only used to match // the error expectations in the lexer tests for numbers with exponents. let position = this.index + offset; throw new Error(`Lexer Error: ${message} at column ${position} in expression [${this.input}]`); } } const OPERATORS = [ 'undefined', 'null', 'true', 'false', '+', '-', '*', '/', '%', '^', '=', '==', '===', '!=', '!==', '<', '>', '<=', '>=', '&&', '||', '&', '|', '!', '?' ]; const $EOF = 0; const $TAB = 9; const $LF = 10; const $VTAB = 11; const $FF = 12; const $CR = 13; const $SPACE = 32; const $BANG = 33; const $DQ = 34; const $$ = 36; const $PERCENT = 37; const $AMPERSAND = 38; const $SQ = 39; const $LPAREN = 40; const $RPAREN = 41; const $STAR = 42; const $PLUS = 43; const $COMMA = 44; const $MINUS = 45; const $PERIOD = 46; const $SLASH = 47; const $COLON = 58; const $SEMICOLON = 59; const $LT = 60; const $EQ = 61; const $GT = 62; const $QUESTION = 63; const $0 = 48; const $9 = 57; const $A = 65; const $E = 69; const $Z = 90; const $LBRACKET = 91; const $BACKSLASH = 92; const $RBRACKET = 93; const $CARET = 94; const $_ = 95; const $a = 97; const $e = 101; const $f = 102; const $n = 110; const $r = 114; const $t = 116; const $u = 117; const $v = 118; const $z = 122; const $LBRACE = 123; const $BAR = 124; const $RBRACE = 125; const $NBSP = 160; function isWhitespace(code) { return (code >= $TAB && code <= $SPACE) || (code === $NBSP); } function isIdentifierStart(code) { return ($a <= code && code <= $z) || ($A <= code && code <= $Z) || (code === $_) || (code === $$); } function isIdentifierPart(code) { return ($a <= code && code <= $z) || ($A <= code && code <= $Z) || ($0 <= code && code <= $9) || (code === $_) || (code === $$); } function isDigit(code) { return ($0 <= code && code <= $9); } function isExponentStart(code) { return (code === $e || code === $E); } function isExponentSign(code) { return (code === $MINUS || code === $PLUS); } function unescape(code) { switch (code) { case $n: return $LF; case $f: return $FF; case $r: return $CR; case $t: return $TAB; case $v: return $VTAB; default: return code; } } function assert(condition, message) { if (!condition) { throw message || 'Assertion failed'; } }