UNPKG

@ibyar/expressions

Version:

Aurora expression, an template expression and evaluation, An 100% spec compliant ES2022 JavaScript toolchain,

1,356 lines 65.8 kB
import { Token, TokenExpression } from './token.js'; import { Identifier, Literal } from '../api/definition/values.js'; import { PrivateIdentifier, Super } from '../api/class/class.js'; import { DebuggerStatement } from '../api/computing/debugger.js'; import { isStrict, LanguageMode } from './language.js'; const FORBIDDEN_CODE_POINT = ['200b', '200c', '200d', 'feff']; export const IdentifierRegex = /^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*/; export const IdentifierStartRegex = /[_$a-zA-Z\xA0-\uFFFF]/; export const IdentifierPartRegex = /[_$a-zA-Z0-9\xA0-\uFFFF]/; ; export class PreTemplateLiteral { strings; expressions; constructor(strings, expressions) { this.strings = strings; this.expressions = expressions; } } ; export class TemplateStringLiteral { string; range; constructor(string, range) { this.string = string; this.range = range; } } export class TokenStream { mode; static getTokenStream(source, mode) { if (Array.isArray(source)) { return new TokenStreamer(source, mode); } else if (typeof source === 'string') { return new TokenStreamImpl(source, mode); } throw new Error(`Can't build token stream for ${source}`); } pos = 0; savedPosition = 0; current; savedCurrent; last; constructor(mode = LanguageMode.Strict) { this.mode = mode; } setLanguageMode(mode) { this.mode = mode; } getLanguageMode() { return this.mode; } save() { this.savedPosition = this.pos; this.savedCurrent = this.current; } restore() { this.pos = this.savedPosition; this.current = this.savedCurrent; this.last = undefined; } reset() { this.pos = 0; this.savedPosition = 0; this.current = this.savedCurrent = undefined; } lastToken() { return this.last; } currentToken() { return this.current; } seekTo(expect) { let token; while (true) { token = this.next(); if (token.token === expect) { return true; } else if (token.token === Token.EOS) { return false; } } } getPos() { return this.pos; } setPos(pos) { this.pos = pos; this.current = undefined; } getSavedPos() { return this.savedPosition; } getStreamer(expect = Token.EOS) { const tokens = []; this.readTill(expect, tokens); return new TokenStreamer(tokens); } readTill(expect = Token.EOS, tokens) { if (Token.isClosePair(expect)) { this.readTokensPair(Token.openOf(expect), expect, tokens); } else { this.readTokens(expect, tokens); } } readTokens(expect, tokens) { let token; while (true) { token = this.next(); if (token.token === expect || token.token === Token.EOS) { break; } tokens.push(token); } } readTokensConsiderPair(tokens, ...expect) { let token, open; while (true) { if (Token.isOpenPair(open = this.peek().token)) { this.readTokensPair(open, Token.closeOf(open), tokens); continue; } token = this.next(); if (expect.includes(token.token) || token.token === Token.EOS) { break; } tokens.push(token); } } readTokensPair(open, close, tokens) { // check pair let count = 0; let token; while (true) { token = this.next(); if (token.token === open) { count++; } else if (token.token === close) { if (count > 0) { count--; } if (count === 0) { tokens.push(token); break; } } else if (token.token === Token.EOS) { break; } tokens.push(token); } } toTokens() { const tokens = []; let token; while (true) { token = this.next(); if (token.token === Token.EOS) { break; } tokens.push(token); } return tokens; } [Symbol.iterator]() { this.pos = 0; const self = this; return { [Symbol.iterator]: function () { return this; }, next: () => { const value = self.next(); const done = value.isType(Token.EOS); return { value, done }; } }; } scanRegExpPattern() { return false; } ; peek() { this.save(); const exp = this.next(); this.restore(); return exp; } peekAhead() { this.save(); this.next(); const exp = this.next(); this.restore(); return exp; } peekPosition() { this.save(); this.next(); const pos = this.pos; this.restore(); return pos; } peekAheadPosition() { this.save(); this.next(); this.next(); const pos = this.pos; this.restore(); return pos; } } export class TokenStreamer extends TokenStream { tokens; constructor(tokens, mode) { super(mode); this.tokens = tokens; } next() { if (this.pos === this.tokens.length) { return new TokenExpression(Token.EOS, [-1, -1]); } this.last = this.current; return this.current = this.tokens[this.pos++]; } createError(message) { return 'parse error [' + this.pos + ']: ' + message; } hasLineTerminatorBeforeNext() { return false; } hasLineTerminatorAfterNext() { return false; } scanTemplateContinuation() { return this.next(); } } export class TokenStreamImpl extends TokenStream { expression; static REGEXP_FLAGS = ['g', 'i', 'm', 's', 'u', 'y']; static UnicodePattern = /^[0-9a-f]{4}$/i; static DecimalPattern = /^[0-9a-f]{2}$/i; static AsciiPattern = /^[0-7]{1,3}$/i; static OctalPattern = /^[0-7]+$/i; constructor(expression, mode) { super(mode); this.expression = expression; } newToken(type, range, value) { return new TokenExpression(type, range, value); } next() { if (this.pos >= this.expression.length) { return this.current = this.newToken(Token.EOS, [this.expression.length, this.expression.length]); } this.last = this.current; if (this.isWhitespace() || this.isComment()) { return this.next(); } else if (this.isRadixInteger() || this.isNumber() // || this.isRegExp() || this.isString() || this.isTemplateLiteral() || this.isCurlY() || this.isParentheses() || this.isBracket() || this.isComma() || this.isSemicolon() || this.isOperator() || this.isStatement() || this.isProperty()) { return this.current; } else if (this.pos >= this.expression.length) { return this.current = this.newToken(Token.EOS, [this.expression.length, this.expression.length]); } else { throw new Error(this.createError('Unknown character "' + this.expression.charAt(this.pos) + '"')); } } hasLineTerminatorBeforeNext() { const limit = this.peekPosition(); const str = this.expression.substring(this.pos, limit); return /(?:\r?\n)/g.test(str); } hasLineTerminatorAfterNext() { const start = this.peekPosition(); const end = this.peekAheadPosition(); const str = this.expression.substring(start, end); return /(?:\r?\n)/g.test(str); } isString() { const quote = this.expression.charAt(this.pos); if (quote === '\'' || quote === '"') { const start = this.pos; let literal = []; let c0 = ''; const advance = () => { c0 = this.expression.charAt(++this.pos); }; const advanceUntil = (check) => { while (true) { c0 = this.expression.charAt(++this.pos); if (this.pos >= this.expression.length) { return; } const result = check(c0); if (result) { break; } } }; const addLiteralChar = (char) => literal.push(char); while (true) { advanceUntil(char => { if (char.charCodeAt(0) > 127) { if (this.isStringLiteralLineTerminator(char)) { return true; } addLiteralChar(char); return false; } if (this.mayTerminateString(char)) return true; addLiteralChar(c0); return false; }); while (c0 === '\\') { advance(); const char = this.scanEscape(false); if (char == '' || !char) { return false; } addLiteralChar(char); advance(); } if (c0 === quote) { advance(); const end = this.pos; const string = literal.join(''); const rawString = this.expression.substring(start, end); const range = [start, end]; const stringNode = new Literal(string, rawString, undefined, undefined, range); this.current = this.newToken(Token.STRING, range, stringNode); return true; } if (c0 == '' || this.pos >= this.expression.length) { // Unterminated string literal break; } addLiteralChar(c0); } } return false; } isTemplateLiteral() { const quote = this.expression.charAt(this.pos); if (quote === '`') { this.pos++; this.current = this.scanTemplateSpan(); return true; } return false; } scanTemplateSpan() { // When scanning a TemplateSpan, we are looking for the following construct: // TEMPLATE_SPAN :: // ` LiteralChars* ${ // | } LiteralChars* ${ // // TEMPLATE_TAIL :: // ` LiteralChars* ` // | } LiteralChar* ` // // A TEMPLATE_SPAN should always be followed by an Expression, while a // TEMPLATE_TAIL terminates a TemplateLiteral and does not need to be // followed by an Expression. // These scoped helpers save and restore the original error state, so that we // can specially treat invalid escape sequences in templates (which are // handled by the parser). const start = this.pos; let result = Token.TEMPLATE_SPAN; let literal = [], c0 = this.expression.charAt(this.pos); const advance = () => { this.pos++; c0 = this.expression.charAt(this.pos); }; const addLiteralChar = (char) => literal.push(char); while (true) { let c = c0; if (c == '`') { advance(); // Consume '`' result = Token.TEMPLATE_TAIL; break; } else if (c == '$' && this.expression.charAt(this.pos + 1) == '{') { advance(); // Consume '$' advance(); // Consume '{' break; } else if (c == '\\') { advance(); // Consume '\\' if (this.isLineTerminator(c0)) { // The TV of LineContinuation :: \ LineTerminatorSequence is the empty // code unit sequence. let lastChar = c0; advance(); if (lastChar == '\r') { // Also skip \n. if (c0 == '\n') advance(); lastChar = '\n'; } addLiteralChar(lastChar); } else { const char = this.scanEscape(true); if (char === false) { throw new SyntaxError(this.createError('Unterminated Template Escape char')); } addLiteralChar(char); advance(); } } else if (c == '' || this.pos >= this.expression.length) { // Unterminated template literal break; } else { advance(); // Consume c. // The TRV of LineTerminatorSequence :: <CR> is the CV 0x000A. // The TRV of LineTerminatorSequence :: <CR><LF> is the sequence // consisting of the CV 0x000A. if (c == '\r') { if (c0 == '\n') advance(); // Consume '\n' c = '\n'; } addLiteralChar(c); } } const range = [start - 1, this.pos]; return this.newToken(result, range, new TemplateStringLiteral(literal.join(''), range)); } scanTemplateContinuation() { if (this.current.isNotType(Token.RBRACE)) { throw new SyntaxError(this.createError('Unterminated Template Expr')); } return this.scanTemplateSpan(); } scanEscape(scanTemplateSpan) { let c0 = this.expression.charAt(this.pos); let c = c0; // Skip escaped newlines. if (!scanTemplateSpan && this.isLineTerminator(c)) { // Allow escaped CR+LF newlines in multiline string literals. if (c == '\r' && this.expression.charAt(this.pos + 1) == '\n') this.pos++; return '\n'; } switch (c) { case 'b': c = '\b'; break; case 'f': c = '\f'; break; case 'n': c = '\n'; break; case 'r': c = '\r'; break; case 't': c = '\t'; break; case 'u': { this.pos++; c = this.scanUnicodeEscape(); if (c === false) return false; break; } case 'v': c = '\v'; break; case 'x': { this.pos++; c = this.scanHexNumber(2); if (c === false) return false; break; } case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': this.pos++; c = this.scanOctalEscape(c, 2); break; case '8': case '9': // '\8' and '\9' are disallowed in strict mode. // Re-use the octal error state to propagate the error. throw new SyntaxError(this.createError(`'\\8' and '\\9' are disallowed in strict mode.`)); } // Other escaped characters are interpreted as their non-escaped version. return c; } scanUnicodeEscape() { // Accept both \uxxxx and \u{xxxxxx}. In the latter case, the number of // hex digits between { } is arbitrary. \ and u have already been read. let c0 = this.expression.charAt(this.pos); if (c0 == '{') { this.pos++; const cp = this.scanUnlimitedLengthHexNumber(0x10ffff); c0 = this.expression.charAt(++this.pos); if (c0 != '}') { return false; } return cp; } return this.scanHexNumber(4); } scanHexNumber(length) { if (length > 4) { // prevent overflow throw new RangeError(this.createError('ScanHexNumber: length should be less than or equal to 4')); } let x = 0; let scanned = this.pos; let c0 = this.expression.charAt(scanned); for (let i = 0; i < length; i++) { let d = this.hexValue(c0); if (d < 0) { return false; } x = x * 16 + d; c0 = this.expression.charAt(++scanned); } this.pos = scanned - 1; return String.fromCodePoint(x); } scanUnlimitedLengthHexNumber(max_value) { let x = 0; let scanned = this.pos; let c0 = this.expression.charAt(scanned); let d = this.hexValue(c0); if (d < 0) { throw new RangeError(this.createError('Invalid code')); } while (d >= 0) { x = x * 16 + d; if (x > max_value) { throw new RangeError(this.createError('Undefined Unicode Code Point')); } c0 = this.expression.charAt(++scanned); d = this.hexValue(c0); } this.pos = scanned - 1; return String.fromCodePoint(x); } hexValue(c) { let x = c.charCodeAt(0) - '0'.charCodeAt(0); if (x <= 9) return x; x = (x | 0x20) - ('a'.charCodeAt(0) - '0'.charCodeAt(0)); // detect 0x11..0x16 and 0x31..0x36. if (x <= 5) return x + 10; return -1; } scanOctalEscape(c, length) { if (!('0' <= c && c <= '7')) { throw new SyntaxError(this.createError('Invalid octal escape sequence')); } const zero = '0'.charCodeAt(0); let x = c.charCodeAt(0) - zero; let scanned = 0; let i = 0; let char = ''; for (; i < length; i++) { char = this.expression.charAt(this.pos + i); let d = char.charCodeAt(0) - zero; if (d < 0 || d > 7) break; const nx = x * 8 + d; if (nx >= 256) break; x = nx; scanned++; } this.pos += scanned - 1; return String.fromCharCode(x); } isParentheses() { const char = this.expression.charAt(this.pos); if (char === '(') { this.current = this.newToken(Token.LPAREN, [this.pos, ++this.pos]); return true; } else if (char === ')') { this.current = this.newToken(Token.RPAREN, [this.pos, ++this.pos]); return true; } return false; } isBracket() { const char = this.expression.charAt(this.pos); if (char === '[') { this.current = this.newToken(Token.LBRACK, [this.pos, ++this.pos]); return true; } else if (char === ']') { this.current = this.newToken(Token.RBRACK, [this.pos, ++this.pos]); return true; } return false; } isCurlY() { const char = this.expression.charAt(this.pos); if (char === '{') { this.current = this.newToken(Token.LBRACE, [this.pos, ++this.pos]); return true; } else if (char === '}') { this.current = this.newToken(Token.RBRACE, [this.pos, ++this.pos]); return true; } return false; } isComma() { const char = this.expression.charAt(this.pos); if (char === ',') { this.current = this.newToken(Token.COMMA, [this.pos, ++this.pos]); return true; } return false; } isSemicolon() { const char = this.expression.charAt(this.pos); if (char === ';') { this.current = this.newToken(Token.SEMICOLON, [this.pos, ++this.pos]); return true; } return false; } isProperty() { let startPos = this.pos; let i = startPos; let isPrivate = false; let escape = false; let escapeFlag = false; // check is private property let char = this.expression.charAt(i); if (char === '#') { isPrivate = true; startPos++; i++; } char = this.expression.charAt(i); if (!IdentifierStartRegex.test(char)) { if (char === '\\' && this.expression.charAt(i + 1) === 'u') { i += 2; escapeFlag = escape = true; // need to valid the unicode char } else { return false; } } else { i++; } for (; i < this.expression.length; i++) { char = this.expression.charAt(i); if (!IdentifierPartRegex.test(char)) { if (char === '\\' && this.expression.charAt(i + 1) === 'u') { i++; escapeFlag = escape = true; continue; } if ('{' === char && escapeFlag) { const end = this.expression.indexOf('}', i + 1); if (end === -1) { return false; } escapeFlag = false; i = end; continue; } break; } } let identifierName = this.expression.substring(startPos, i); if (escape) { identifierName = this.unescape(identifierName); if (!IdentifierRegex.test(identifierName)) { return false; } this.pos = i; } else { this.pos += identifierName.length + (isPrivate ? 1 : 0); } let node; if (isPrivate) { const range = [startPos - 1, this.pos]; node = new PrivateIdentifier(identifierName, range); this.current = this.newToken(Token.PRIVATE_NAME, range, node); } else { const range = [startPos, this.pos]; switch (identifierName) { case 'this': this.current = this.newToken(Token.THIS, range); break; case 'null': this.current = this.newToken(Token.NULL_LITERAL, range, new Literal(null, undefined, undefined, undefined, range)); break; case 'undefined': this.current = this.newToken(Token.UNDEFINED_LITERAL, range, new Literal(undefined, undefined, undefined, undefined, range)); break; case 'true': this.current = this.newToken(Token.TRUE_LITERAL, range, new Literal(true, undefined, undefined, undefined, range)); break; case 'false': this.current = this.newToken(Token.FALSE_LITERAL, range, new Literal(false, undefined, undefined, undefined, range)); break; case 'globalThis': this.current = this.newToken(Token.IDENTIFIER, range, new Identifier('globalThis', range)); break; case 'Symbol': this.current = this.newToken(Token.IDENTIFIER, range, new Identifier('Symbol', range)); break; case 'of': this.current = this.newToken(Token.IDENTIFIER, range, new Identifier('of', range)); break; case 'as': this.current = this.newToken(Token.IDENTIFIER, range, new Identifier('as', range)); break; case 'default': this.current = this.newToken(Token.DEFAULT, range, new Identifier('default', range)); break; case 'yield': this.current = this.newToken(Token.YIELD, range, new Identifier('yield', range)); break; case 'constructor': this.current = this.newToken(Token.IDENTIFIER, range, new Identifier('constructor', range)); break; case 'arguments': this.current = this.newToken(Token.IDENTIFIER, range, new Identifier('arguments', range)); break; case 'name': this.current = this.newToken(Token.IDENTIFIER, range, new Identifier('name', range)); break; case 'eval': this.current = this.newToken(Token.IDENTIFIER, range, new Identifier('eval', range)); break; case 'debugger': this.current = this.newToken(Token.DEBUGGER, range, new DebuggerStatement(range)); break; case 'class': this.current = this.newToken(Token.CLASS, range); break; case 'await': this.current = this.newToken(Token.AWAIT, range, new Identifier('await', range)); break; case 'async': this.current = this.newToken(Token.ASYNC, range, new Identifier('async', range)); break; default: node = new Identifier(identifierName, range); this.current = this.newToken(Token.IDENTIFIER, range, node); break; } } return true; } isWhitespace() { let result = false; while (FORBIDDEN_CODE_POINT.includes(this.expression.charCodeAt(this.pos).toString(16))) { this.pos++; } let char = this.expression.charAt(this.pos); while (/\s/.test(char)) { result = true; this.pos++; if (this.pos >= this.expression.length) { break; } char = this.expression.charAt(this.pos); } return result; } /** * [JavaScript 5 Comment Types](https://developer.adobe.com/experience-manager/reference-materials/6-5/javadoc/org/mozilla/javascript/ast/Comment.html) * @returns boolean */ isComment() { const char = this.expression.charAt(this.pos); const nextChar = this.expression.charAt(this.pos + 1); let closeTag; let endPos; switch (true) { // // line comments case (char === '/' && nextChar === '/'): closeTag = '\n'; endPos = this.expression.indexOf(closeTag, this.pos); break; // /* block comments *\/ and /** jsdoc comments *\/ case (char === '/' && nextChar === '*'): closeTag = '*/'; endPos = this.expression.indexOf(closeTag, this.pos); if (endPos === -1) { throw new SyntaxError(this.createError('Invalid or unexpected token')); } break; // <!-- html-open line comments case (char === '<' && nextChar === '!'): if ('<!--' === this.expression.substring(this.pos, this.pos + 4)) { if (isStrict(this.mode)) { throw new SyntaxError('Html Comment not allowed In Module'); } const nextLinePos = this.expression.indexOf('\n', this.pos); const closePos = this.expression.indexOf('-->', this.pos); if (nextLinePos < closePos) { closeTag = '\n'; endPos = nextLinePos; } else if (closePos < nextLinePos) { closeTag = '-->'; endPos = closePos; } else { closeTag = ''; endPos = -1; } break; } // ^\\s*--> html-close line comments case (char === '-' && nextChar === '-'): if ('-->' === this.expression.substring(this.pos, this.pos + 3)) { if (isStrict(this.mode)) { throw new SyntaxError('Html Comment not allowed In Module'); } // check if '-->' is the first non-whitespace on the line let indexOfLastLine = this.expression.lastIndexOf('\n', this.pos + 1); if (indexOfLastLine == -1) { indexOfLastLine = 0; } let lastLine = this.expression.substring(indexOfLastLine, this.pos + 3); if (!/^\s*-->$/g.test(lastLine)) { // test with close of block comments let lastBlockCommentPos = lastLine.lastIndexOf('*/'); if (lastBlockCommentPos == -1) { return false; } lastLine = lastLine.substring(lastBlockCommentPos + 2); if (!/^\s*-->$/g.test(lastLine)) { return false; } } endPos = this.expression.indexOf('\n', this.pos); closeTag = '\n'; break; } default: return false; } this.pos = endPos == -1 ? this.expression.length : endPos + closeTag.length; return true; } isLineTerminator(c) { return /[\n\r\u2028\u2029]/.test(c); } isStringLiteralLineTerminator(c) { return /[\n\r]/.test(c); } mayTerminateString(c) { return (c == '\'' || c == '"' || c == '\n' || c == '\r' || c == '\\'); } scanRegExpPattern() { const peek = this.peek(); if (!(peek.isType(Token.DIV) || peek.isType(Token.DIV_ASSIGN))) { return false; } const start = this.pos; this.next(); let pattern = peek.isType(Token.DIV_ASSIGN) ? '=' : ''; let currentPos = this.pos; let inCharacterClass = false; let c = this.expression.charAt(currentPos); while (c !== '/' || inCharacterClass) { if (c == '' || this.isLineTerminator(c)) { return false; } if (c === '\\') { // Escape sequence. pattern += c; c = this.expression.charAt(++currentPos); if (c == '' || this.isLineTerminator(c)) { return false; } pattern += c; c = this.expression.charAt(++currentPos); } else { if (c == '[') inCharacterClass = true; if (c == ']') inCharacterClass = false; pattern += c; c = this.expression.charAt(++currentPos); } } currentPos++; // consume '/' let flags = ''; const remainFlags = TokenStreamImpl.REGEXP_FLAGS.slice(); while (true) { const code = this.expression.charCodeAt(currentPos); const nextChar = this.expression.charAt(currentPos); if (/[\.\s]/.test(nextChar) || Number.isNaN(code) /* || FORBIDDEN_CODE_POINT.includes(code.toString(16))*/) { break; } const nextCharIndex = remainFlags.indexOf(nextChar); if (nextCharIndex === -1) { if (flags.includes(nextChar)) { throw new Error(this.createError('Invalid regular expression flags')); } break; } else { flags += nextChar; remainFlags.splice(nextCharIndex, 1); currentPos++; } } const range = [start, currentPos]; const regexNode = new Literal(new RegExp(pattern, flags), `/${pattern}/${flags}`, { pattern, flags }, undefined, range); this.current = this.newToken(Token.REGEXP_LITERAL, range, regexNode); this.pos = currentPos; return true; } unescape(v) { let index = v.indexOf('\\'); if (index < 0) { return v; } let buffer = v.substring(0, index); while (index >= 0) { const c = v.charAt(++index); switch (c) { case '\b': case '\f': case '\n': case '\r': case '\t': case '\v': index++; buffer += '\\' + c; break; case 'b': index++; buffer += '\b'; break; case 'f': index++; buffer += '\f'; break; case 'n': index++; buffer += '\n'; break; case 'r': index++; buffer += '\r'; break; case 't': index++; buffer += '\t'; break; case 'v': index++; buffer += '\v'; break; case 'u': { index++; // interpret the following 4 characters as the hex of the unicode code point if ('{' === v.charAt(index)) { index++; const start = index; const end = v.indexOf('}', start + 1); const codePoint = v.substring(start, end); buffer += String.fromCodePoint(parseInt(codePoint, 16)); index = end + 1; } else { const codePoint = v.substring(index, index + 4); if (!TokenStreamImpl.UnicodePattern.test(codePoint)) { throw new Error(this.createError('Illegal escape sequence: \\u ' + codePoint)); } buffer += String.fromCodePoint(parseInt(codePoint, 16)); index += 4; } break; } case 'x': { index++; // interpret the following 2 characters as the hex of the decimal code point let codePoint = v.substring(index, index + 2); if (!TokenStreamImpl.DecimalPattern.test(codePoint)) { throw new Error(this.createError('Illegal escape sequence: \\x ' + codePoint)); } buffer += String.fromCharCode(parseInt(codePoint, 16)); index += 2; break; } default: if (!Number.isNaN(parseInt(c, 8))) { let codePoint = c; for (const i of [1, 2]) { const tempChar = v.charAt(index + i); if (!Number.isNaN(parseInt(tempChar, 8))) { codePoint += tempChar; } else { break; } } if (!TokenStreamImpl.AsciiPattern.test(codePoint)) { throw new Error(this.createError('Illegal escape sequence: ASCII 8 base, ' + codePoint)); } buffer += String.fromCharCode(parseInt(codePoint, 8)); index += codePoint.length; break; } index++; buffer += c; break; // throw new Error(this.createError('Illegal escape sequence: "\\' + c + '"')); } let backslash = v.indexOf('\\', index); if (backslash == -1) { buffer += v.substring(index, v.length); break; } else { buffer += v.substring(index, backslash); index = backslash; } } return buffer; } isRadixInteger() { let pos = this.pos; if (pos >= this.expression.length - 2 || this.expression.charAt(pos) !== '0') { return false; } ++pos; let radix; let validDigit; if (this.expression.charAt(pos) === 'x') { radix = 16; validDigit = /^[0-9a-f]$/i; ++pos; } else if (this.expression.charAt(pos) === 'b') { radix = 2; validDigit = /^[01]$/i; ++pos; } else { return false; } let valid = false; let startPos = pos; while (pos < this.expression.length) { const c = this.expression.charAt(pos); if (validDigit.test(c)) { pos++; valid = true; } else { break; } } if (valid) { const string = this.expression.substring(startPos, pos); const rawString = this.expression.substring(this.pos, pos); const range = [this.pos, pos]; const numNode = new Literal(parseInt(string, radix), rawString, undefined, undefined, range); this.current = this.newToken(Token.NUMBER, range, numNode); this.pos = pos; } return valid; } isNumber() { const startPos = this.pos; let valid = false; let pos = this.pos; let resetPos = pos; let foundDot = false; let foundDigits = false; let isOctal = false; let c; while (pos < this.expression.length) { c = this.expression.charAt(pos); if ((c >= '0' && c <= '9') || (!foundDot && c === '.')) { if (c === '.') { foundDot = true; } else { foundDigits = true; } pos++; valid = foundDigits; } else if ('o' == c.toLowerCase()) { if (pos == startPos + 1) { pos++; isOctal = true; } else { break; } } else { break; } } if (valid) { resetPos = pos; } if (c === 'e' || c === 'E') { pos++; let acceptSign = true; let validExponent = false; while (pos < this.expression.length) { c = this.expression.charAt(pos); if (acceptSign && (c === '+' || c === '-')) { acceptSign = false; } else if (c >= '0' && c <= '9') { validExponent = true; acceptSign = false; } else { break; } pos++; } if (!validExponent) { pos = resetPos; } } if (valid) { const rawString = this.expression.substring(startPos, pos); if (this.expression.charAt(pos) === 'n') { pos++; const range = [startPos, this.pos]; const bigintNode = new Literal(BigInt(rawString), rawString + 'n', undefined, rawString, range); this.current = this.newToken(Token.BIGINT, range, bigintNode); } else { let numNode; const range = [startPos, this.pos]; if (isOctal) { const octal = rawString.substring(2); if (TokenStreamImpl.OctalPattern.test(octal)) { numNode = new Literal(parseInt(octal, 8), rawString, undefined, undefined, range); } else { return false; } } else { numNode = new Literal(parseFloat(rawString), rawString, undefined, undefined, range); } this.current = this.newToken(Token.NUMBER, range, numNode); } this.pos = pos; } else { this.pos = resetPos; } return valid; } isOperator() { const char = this.expression.charAt(this.pos); switch (char) { case '+': if (this.expression.charAt(this.pos + 1) === '+') { this.current = this.newToken(Token.INC, [this.pos, (this.pos += 2)]); } else if (this.expression.charAt(this.pos + 1) === '=') { this.current = this.newToken(Token.ADD_ASSIGN, [this.pos, (this.pos += 2)]); } else { this.current = this.newToken(Token.ADD, [this.pos, (this.pos += 1)]); } return true; case '-': if (this.expression.charAt(this.pos + 1) === '-') { this.current = this.newToken(Token.DEC, [this.pos, (this.pos += 2)]); } else if (this.expression.charAt(this.pos + 1) === '=') { this.current = this.newToken(Token.SUB_ASSIGN, [this.pos, (this.pos += 2)]); } else { this.current = this.newToken(Token.SUB, [this.pos, (this.pos += 1)]); } return true; case '=': if (this.expression.charAt(this.pos + 1) === '=') { if (this.expression.charAt(this.pos + 2) === '=') { this.current = this.newToken(Token.EQ_STRICT, [this.pos, (this.pos += 3)]); } else { this.current = this.newToken(Token.EQ, [this.pos, (this.pos += 2)]); } } else if (this.expression.charAt(this.pos + 1) === '>') { this.current = this.newToken(Token.ARROW, [this.pos, (this.pos += 2)]); } else { this.current = this.newToken(Token.ASSIGN, [this.pos, (this.pos += 1)]); } return true; case '*': if (this.expression.charAt(this.pos + 1) === '*') { if (this.expression.charAt(this.pos + 2) === '=') { this.current = this.newToken(Token.EXP_ASSIGN, [this.pos, (this.pos += 3)]); } else { this.current = this.newToken(Token.EXP, [this.pos, (this.pos += 2)]); } } else if (this.expression.charAt(this.pos + 1) === '=') { this.current = this.newToken(Token.MUL_ASSIGN, [this.pos, (this.pos += 2)]); } else { this.current = this.newToken(Token.MUL, [this.pos, (this.pos += 1)]); } return true; case '/': if (this.expression.charAt(this.pos + 1) === '=') { this.current = this.newToken(Token.DIV_ASSIGN, [this.pos, (this.pos += 2)]); } else { this.current = this.newToken(Token.DIV, [this.pos, (this.pos += 1)]); } return true; case '%': if (this.expression.charAt(this.pos + 1) === '%') { if (this.expression.charAt(this.pos + 1) === '=') { this.current = this.newToken(Token.MODULO_ASSIGN, [this.pos, (this.pos += 3)]); } else { this.current = this.newToken(Token.MODULO, [this.pos, (this.pos += 2)]); } } else if (this.expression.charAt(this.pos + 1) === '=') { this.current = this.newToken(Token.MOD_ASSIGN, [this.pos, (this.pos += 2)]); } else { this.current = this.newToken(Token.MOD, [this.pos, (this.pos += 1)]); } return true; case '^': if (this.expression.charAt(this.pos + 1) === '=') { this.current = this.newToken(Token.BIT_XOR_ASSIGN, [this.pos, (this.pos += 2)]); } else { this.current = this.newToken(Token.BIT_XOR, [this.pos, (this.pos += 1)]); } return true; case '>': if (this.expression.charAt(this.pos + 1) === '>') { if (this.expression.charAt(this.pos + 2) === '>') { if (this.expression.charAt(this.pos + 3) === '=') { this.current = this.newToken(Token.SHR_ASSIGN, [this.pos, (this.pos += 4)]); } else { this.current = this.newToken(Token.SHR, [this.pos, (this.pos += 3)]); } } else if (this.expression.charAt(this.pos + 2) === '=') { this.current = this.newToken(Token.SAR_ASSIGN, [this.pos, (this.pos += 3)]); } else { this.current = this.newToken(Token.SAR, [this.pos, (this.pos += 2)]); } } else if (this.expression.charAt(this.pos + 1) === '=') { this.current = this.newToken(Token.GTE, [this.pos, (this.pos += 2)]); } else if (this.expression.charAt(this.pos + 1) === '?') { if (this.expression.charAt(this.pos + 2) === '=') { this.current = this.newToken(Token.LARGER_ASSIGN, [this.pos, (this.pos += 3)]); } else { this.current = this.newToken(Token.LARGER, [this.pos, (this.pos += 2)]); } } else { this.current = this.newToken(Token.GT, [this.pos, (this.pos += 2)]); } return true; case '<': if (this.expression.charAt(this.pos + 1) === '<') { if (this.expression.charAt(this.pos + 2) === '=') { this.current = this.newToken(Token.SHL_ASSIGN, [this.pos, (this.pos += 3)]); } else { this.current = this.newToken(Token.SHL, [this.pos, (this.pos += 2)]); } } else if (this.expression.charAt(this.pos + 1) === '=') { if (this.expression.charAt(this.pos + 2) === '>') { this.current = this.newToken(Token.SPACESHIP, [this.pos, (this.pos += 3)]); } else { this.current = this.newToken(Token.LTE, [this.pos, (this.pos += 2)]); } } else if (this.expression.charAt(this.pos + 1) === '?') { if (this.expression.charAt(this.pos + 2) === '=') { this.current = this.newToken(Token.SMALLER_ASSIGN, [this.pos, (this.pos += 3)]); } else { this.current = this.newToken(Token.SMALLER, [this.pos, (this.pos += 2)]); } } else { this.current = this.newToken(Token.LT, [this.pos, (this.pos += 1)]); } return true; case '|': if (this.expression.charAt(this.pos + 1) === '|') { if (this.expression.charAt(this.pos + 2) === '=') { this.current = this.newToken(Token.OR_ASSIGN, [this.pos, (this.pos += 3)]); } else { this.current = this.newToken(Token.OR, [this.pos, (this.pos += 2)]); } } else if (this.expression.charAt(this.pos + 1) === '=') { this.current = this.newToken(Token.BIT_OR_ASSIGN, [this.pos, (this.pos += 2)]); } else if (this.expr