mirror of
https://github.com/josdejong/mathjs.git
synced 2026-01-25 15:07:57 +00:00
1662 lines
40 KiB
JavaScript
1662 lines
40 KiB
JavaScript
'use strict'
|
|
|
|
const ArgumentsError = require('../error/ArgumentsError')
|
|
const deepMap = require('../utils/collection/deepMap')
|
|
|
|
function factory (type, config, load, typed) {
|
|
const numeric = load(require('../type/numeric'))
|
|
|
|
const AccessorNode = load(require('./node/AccessorNode'))
|
|
const ArrayNode = load(require('./node/ArrayNode'))
|
|
const AssignmentNode = load(require('./node/AssignmentNode'))
|
|
const BlockNode = load(require('./node/BlockNode'))
|
|
const ConditionalNode = load(require('./node/ConditionalNode'))
|
|
const ConstantNode = load(require('./node/ConstantNode'))
|
|
const FunctionAssignmentNode = load(require('./node/FunctionAssignmentNode'))
|
|
const IndexNode = load(require('./node/IndexNode'))
|
|
const ObjectNode = load(require('./node/ObjectNode'))
|
|
const OperatorNode = load(require('./node/OperatorNode'))
|
|
const ParenthesisNode = load(require('./node/ParenthesisNode'))
|
|
const FunctionNode = load(require('./node/FunctionNode'))
|
|
const RangeNode = load(require('./node/RangeNode'))
|
|
const SymbolNode = load(require('./node/SymbolNode'))
|
|
|
|
/**
|
|
* Parse an expression. Returns a node tree, which can be evaluated by
|
|
* invoking node.eval().
|
|
*
|
|
* Syntax:
|
|
*
|
|
* parse(expr)
|
|
* parse(expr, options)
|
|
* parse([expr1, expr2, expr3, ...])
|
|
* parse([expr1, expr2, expr3, ...], options)
|
|
*
|
|
* Example:
|
|
*
|
|
* const node = parse('sqrt(3^2 + 4^2)')
|
|
* node.compile(math).eval() // 5
|
|
*
|
|
* let scope = {a:3, b:4}
|
|
* const node = parse('a * b') // 12
|
|
* const code = node.compile(math)
|
|
* code.eval(scope) // 12
|
|
* scope.a = 5
|
|
* code.eval(scope) // 20
|
|
*
|
|
* const nodes = math.parse(['a = 3', 'b = 4', 'a * b'])
|
|
* nodes[2].compile(math).eval() // 12
|
|
*
|
|
* @param {string | string[] | Matrix} expr
|
|
* @param {{nodes: Object<string, Node>}} [options] Available options:
|
|
* - `nodes` a set of custom nodes
|
|
* @return {Node | Node[]} node
|
|
* @throws {Error}
|
|
*/
|
|
function parse (expr, options) {
|
|
if (arguments.length !== 1 && arguments.length !== 2) {
|
|
throw new ArgumentsError('parse', arguments.length, 1, 2)
|
|
}
|
|
|
|
// pass extra nodes
|
|
extraNodes = (options && options.nodes) ? options.nodes : {}
|
|
|
|
if (typeof expr === 'string') {
|
|
// parse a single expression
|
|
expression = expr
|
|
return parseStart()
|
|
} else if (Array.isArray(expr) || expr instanceof type.Matrix) {
|
|
// parse an array or matrix with expressions
|
|
return deepMap(expr, function (elem) {
|
|
if (typeof elem !== 'string') throw new TypeError('String expected')
|
|
|
|
expression = elem
|
|
return parseStart()
|
|
})
|
|
} else {
|
|
// oops
|
|
throw new TypeError('String or matrix expected')
|
|
}
|
|
}
|
|
|
|
// token types enumeration
|
|
const TOKENTYPE = {
|
|
NULL: 0,
|
|
DELIMITER: 1,
|
|
NUMBER: 2,
|
|
SYMBOL: 3,
|
|
UNKNOWN: 4
|
|
}
|
|
|
|
// map with all delimiters
|
|
const DELIMITERS = {
|
|
',': true,
|
|
'(': true,
|
|
')': true,
|
|
'[': true,
|
|
']': true,
|
|
'{': true,
|
|
'}': true,
|
|
'\"': true,
|
|
';': true,
|
|
|
|
'+': true,
|
|
'-': true,
|
|
'*': true,
|
|
'.*': true,
|
|
'/': true,
|
|
'./': true,
|
|
'%': true,
|
|
'^': true,
|
|
'.^': true,
|
|
'~': true,
|
|
'!': true,
|
|
'&': true,
|
|
'|': true,
|
|
'^|': true,
|
|
'\'': true,
|
|
'=': true,
|
|
':': true,
|
|
'?': true,
|
|
|
|
'==': true,
|
|
'!=': true,
|
|
'<': true,
|
|
'>': true,
|
|
'<=': true,
|
|
'>=': true,
|
|
|
|
'<<': true,
|
|
'>>': true,
|
|
'>>>': true
|
|
}
|
|
|
|
// map with all named delimiters
|
|
const NAMED_DELIMITERS = {
|
|
'mod': true,
|
|
'to': true,
|
|
'in': true,
|
|
'and': true,
|
|
'xor': true,
|
|
'or': true,
|
|
'not': true
|
|
}
|
|
|
|
const CONSTANTS = {
|
|
'true': true,
|
|
'false': false,
|
|
'null': null,
|
|
'undefined': undefined
|
|
}
|
|
|
|
const NUMERIC_CONSTANTS = [
|
|
'NaN',
|
|
'Infinity'
|
|
]
|
|
|
|
let extraNodes = {} // current extra nodes
|
|
let expression = '' // current expression
|
|
let comment = '' // last parsed comment
|
|
let index = 0 // current index in expr
|
|
let c = '' // current token character in expr
|
|
let token = '' // current token
|
|
let tokenType = TOKENTYPE.NULL // type of the token
|
|
let nestingLevel = 0 // level of nesting inside parameters, used to ignore newline characters
|
|
let conditionalLevel = null // when a conditional is being parsed, the level of the conditional is stored here
|
|
const tokenStates = [] // holds saved token states
|
|
|
|
/**
|
|
* Get the first character from the expression.
|
|
* The character is stored into the char c. If the end of the expression is
|
|
* reached, the function puts an empty string in c.
|
|
* @private
|
|
*/
|
|
function first () {
|
|
index = 0
|
|
c = expression.charAt(0)
|
|
nestingLevel = 0
|
|
conditionalLevel = null
|
|
}
|
|
|
|
/**
|
|
* Get the next character from the expression.
|
|
* The character is stored into the char c. If the end of the expression is
|
|
* reached, the function puts an empty string in c.
|
|
* @private
|
|
*/
|
|
function next () {
|
|
index++
|
|
c = expression.charAt(index)
|
|
}
|
|
|
|
/**
|
|
* Preview the previous character from the expression.
|
|
* @return {string} cNext
|
|
* @private
|
|
*/
|
|
function prevPreview () {
|
|
return expression.charAt(index - 1)
|
|
}
|
|
|
|
/**
|
|
* Preview the next character from the expression.
|
|
* @return {string} cNext
|
|
* @private
|
|
*/
|
|
function nextPreview () {
|
|
return expression.charAt(index + 1)
|
|
}
|
|
|
|
/**
|
|
* Preview the second next character from the expression.
|
|
* @return {string} cNext
|
|
* @private
|
|
*/
|
|
function nextNextPreview () {
|
|
return expression.charAt(index + 2)
|
|
}
|
|
|
|
/**
|
|
* Save the current token state so we can rewind later if necessary.
|
|
* @private
|
|
*/
|
|
function pushTokenState () {
|
|
tokenStates.push({
|
|
tokenType: tokenType,
|
|
token: token,
|
|
comment: comment,
|
|
index: index,
|
|
c: c
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Rewind the parser by one token by restoring the last saved token state
|
|
* @private
|
|
*/
|
|
function popTokenState () {
|
|
const restoredState = tokenStates.pop()
|
|
tokenType = restoredState.tokenType
|
|
token = restoredState.token
|
|
comment = restoredState.comment
|
|
index = restoredState.index
|
|
c = restoredState.c
|
|
}
|
|
|
|
/**
|
|
* Discard the most recent token state without restoring it
|
|
* @private
|
|
*/
|
|
function discardTokenState () {
|
|
tokenStates.pop()
|
|
}
|
|
|
|
/**
|
|
* Get next token in the current string expr.
|
|
* The token and token type are available as token and tokenType
|
|
* @private
|
|
*/
|
|
function getToken () {
|
|
tokenType = TOKENTYPE.NULL
|
|
token = ''
|
|
comment = ''
|
|
|
|
// skip over whitespaces
|
|
// space, tab, and newline when inside parameters
|
|
while (parse.isWhitespace(c, nestingLevel)) {
|
|
next()
|
|
}
|
|
|
|
// skip comment
|
|
if (c === '#') {
|
|
while (c !== '\n' && c !== '') {
|
|
comment += c
|
|
next()
|
|
}
|
|
}
|
|
|
|
// check for end of expression
|
|
if (c === '') {
|
|
// token is still empty
|
|
tokenType = TOKENTYPE.DELIMITER
|
|
return
|
|
}
|
|
|
|
// check for new line character
|
|
if (c === '\n' && !nestingLevel) {
|
|
tokenType = TOKENTYPE.DELIMITER
|
|
token = c
|
|
next()
|
|
return
|
|
}
|
|
|
|
// check for delimiters consisting of 3 characters
|
|
let c2 = c + nextPreview()
|
|
const c3 = c2 + nextNextPreview()
|
|
if (c3.length === 3 && DELIMITERS[c3]) {
|
|
tokenType = TOKENTYPE.DELIMITER
|
|
token = c3
|
|
next()
|
|
next()
|
|
next()
|
|
return
|
|
}
|
|
|
|
// check for delimiters consisting of 2 characters
|
|
if (c2.length === 2 && DELIMITERS[c2]) {
|
|
tokenType = TOKENTYPE.DELIMITER
|
|
token = c2
|
|
next()
|
|
next()
|
|
return
|
|
}
|
|
|
|
// check for delimiters consisting of 1 character
|
|
if (DELIMITERS[c]) {
|
|
tokenType = TOKENTYPE.DELIMITER
|
|
token = c
|
|
next()
|
|
return
|
|
}
|
|
|
|
// check for a number
|
|
if (parse.isDigitDot(c)) {
|
|
tokenType = TOKENTYPE.NUMBER
|
|
|
|
// get number, can have a single dot
|
|
if (c === '.') {
|
|
token += c
|
|
next()
|
|
|
|
if (!parse.isDigit(c)) {
|
|
// this is no number, it is just a dot (can be dot notation)
|
|
tokenType = TOKENTYPE.DELIMITER
|
|
}
|
|
} else {
|
|
while (parse.isDigit(c)) {
|
|
token += c
|
|
next()
|
|
}
|
|
if (parse.isDecimalMark(c, nextPreview())) {
|
|
token += c
|
|
next()
|
|
}
|
|
}
|
|
while (parse.isDigit(c)) {
|
|
token += c
|
|
next()
|
|
}
|
|
|
|
// check for exponential notation like "2.3e-4", "1.23e50" or "2e+4"
|
|
c2 = nextPreview()
|
|
if (c === 'E' || c === 'e') {
|
|
if (parse.isDigit(c2) || c2 === '-' || c2 === '+') {
|
|
token += c
|
|
next()
|
|
|
|
if (c === '+' || c === '-') {
|
|
token += c
|
|
next()
|
|
}
|
|
|
|
// Scientific notation MUST be followed by an exponent
|
|
if (!parse.isDigit(c)) {
|
|
throw createSyntaxError('Digit expected, got "' + c + '"')
|
|
}
|
|
|
|
while (parse.isDigit(c)) {
|
|
token += c
|
|
next()
|
|
}
|
|
|
|
if (parse.isDecimalMark(c, nextPreview())) {
|
|
throw createSyntaxError('Digit expected, got "' + c + '"')
|
|
}
|
|
} else if (c2 === '.') {
|
|
next()
|
|
throw createSyntaxError('Digit expected, got "' + c + '"')
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// check for variables, functions, named operators
|
|
if (parse.isAlpha(c, prevPreview(), nextPreview())) {
|
|
while (parse.isAlpha(c, prevPreview(), nextPreview()) || parse.isDigit(c)) {
|
|
token += c
|
|
next()
|
|
}
|
|
|
|
if (NAMED_DELIMITERS.hasOwnProperty(token)) {
|
|
tokenType = TOKENTYPE.DELIMITER
|
|
} else {
|
|
tokenType = TOKENTYPE.SYMBOL
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// something unknown is found, wrong characters -> a syntax error
|
|
tokenType = TOKENTYPE.UNKNOWN
|
|
while (c !== '') {
|
|
token += c
|
|
next()
|
|
}
|
|
throw createSyntaxError('Syntax error in part "' + token + '"')
|
|
}
|
|
|
|
/**
|
|
* Get next token and skip newline tokens
|
|
*/
|
|
function getTokenSkipNewline () {
|
|
do {
|
|
getToken()
|
|
}
|
|
while (token === '\n') // eslint-disable-line no-unmodified-loop-condition
|
|
}
|
|
|
|
/**
|
|
* Open parameters.
|
|
* New line characters will be ignored until closeParams() is called
|
|
*/
|
|
function openParams () {
|
|
nestingLevel++
|
|
}
|
|
|
|
/**
|
|
* Close parameters.
|
|
* New line characters will no longer be ignored
|
|
*/
|
|
function closeParams () {
|
|
nestingLevel--
|
|
}
|
|
|
|
/**
|
|
* Checks whether the current character `c` is a valid alpha character:
|
|
*
|
|
* - A latin letter (upper or lower case) Ascii: a-z, A-Z
|
|
* - An underscore Ascii: _
|
|
* - A dollar sign Ascii: $
|
|
* - A latin letter with accents Unicode: \u00C0 - \u02AF
|
|
* - A greek letter Unicode: \u0370 - \u03FF
|
|
* - A mathematical alphanumeric symbol Unicode: \u{1D400} - \u{1D7FF} excluding invalid code points
|
|
*
|
|
* The previous and next characters are needed to determine whether
|
|
* this character is part of a unicode surrogate pair.
|
|
*
|
|
* @param {string} c Current character in the expression
|
|
* @param {string} cPrev Previous character
|
|
* @param {string} cNext Next character
|
|
* @return {boolean}
|
|
*/
|
|
parse.isAlpha = function isAlpha (c, cPrev, cNext) {
|
|
return parse.isValidLatinOrGreek(c) ||
|
|
parse.isValidMathSymbol(c, cNext) ||
|
|
parse.isValidMathSymbol(cPrev, c)
|
|
}
|
|
|
|
/**
|
|
* Test whether a character is a valid latin, greek, or letter-like character
|
|
* @param {string} c
|
|
* @return {boolean}
|
|
*/
|
|
parse.isValidLatinOrGreek = function isValidLatinOrGreek (c) {
|
|
return /^[a-zA-Z_$\u00C0-\u02AF\u0370-\u03FF\u2100-\u214F]$/.test(c)
|
|
}
|
|
|
|
/**
|
|
* Test whether two given 16 bit characters form a surrogate pair of a
|
|
* unicode math symbol.
|
|
*
|
|
* http://unicode-table.com/en/
|
|
* http://www.wikiwand.com/en/Mathematical_operators_and_symbols_in_Unicode
|
|
*
|
|
* Note: In ES6 will be unicode aware:
|
|
* http://stackoverflow.com/questions/280712/javascript-unicode-regexes
|
|
* https://mathiasbynens.be/notes/es6-unicode-regex
|
|
*
|
|
* @param {string} high
|
|
* @param {string} low
|
|
* @return {boolean}
|
|
*/
|
|
parse.isValidMathSymbol = function isValidMathSymbol (high, low) {
|
|
return /^[\uD835]$/.test(high) &&
|
|
/^[\uDC00-\uDFFF]$/.test(low) &&
|
|
/^[^\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDFCC\uDFCD]$/.test(low)
|
|
}
|
|
|
|
/**
|
|
* Check whether given character c is a white space character: space, tab, or enter
|
|
* @param {string} c
|
|
* @param {number} nestingLevel
|
|
* @return {boolean}
|
|
*/
|
|
parse.isWhitespace = function isWhitespace (c, nestingLevel) {
|
|
// TODO: also take '\r' carriage return as newline? Or does that give problems on mac?
|
|
return c === ' ' || c === '\t' || (c === '\n' && nestingLevel > 0)
|
|
}
|
|
|
|
/**
|
|
* Test whether the character c is a decimal mark (dot).
|
|
* This is the case when it's not the start of a delimiter '.*', './', or '.^'
|
|
* @param {string} c
|
|
* @param {string} cNext
|
|
* @return {boolean}
|
|
*/
|
|
parse.isDecimalMark = function isDecimalMark (c, cNext) {
|
|
return c === '.' && cNext !== '/' && cNext !== '*' && cNext !== '^'
|
|
}
|
|
|
|
/**
|
|
* checks if the given char c is a digit or dot
|
|
* @param {string} c a string with one character
|
|
* @return {boolean}
|
|
*/
|
|
parse.isDigitDot = function isDigitDot (c) {
|
|
return ((c >= '0' && c <= '9') || c === '.')
|
|
}
|
|
|
|
/**
|
|
* checks if the given char c is a digit
|
|
* @param {string} c a string with one character
|
|
* @return {boolean}
|
|
*/
|
|
parse.isDigit = function isDigit (c) {
|
|
return (c >= '0' && c <= '9')
|
|
}
|
|
|
|
/**
|
|
* Start of the parse levels below, in order of precedence
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseStart () {
|
|
// get the first character in expression
|
|
first()
|
|
|
|
getToken()
|
|
|
|
const node = parseBlock()
|
|
|
|
// check for garbage at the end of the expression
|
|
// an expression ends with a empty character '' and tokenType DELIMITER
|
|
if (token !== '') {
|
|
if (tokenType === TOKENTYPE.DELIMITER) {
|
|
// user entered a not existing operator like "//"
|
|
|
|
// TODO: give hints for aliases, for example with "<>" give as hint " did you mean !== ?"
|
|
throw createError('Unexpected operator ' + token)
|
|
} else {
|
|
throw createSyntaxError('Unexpected part "' + token + '"')
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Parse a block with expressions. Expressions can be separated by a newline
|
|
* character '\n', or by a semicolon ';'. In case of a semicolon, no output
|
|
* of the preceding line is returned.
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseBlock () {
|
|
let node
|
|
const blocks = []
|
|
let visible
|
|
|
|
if (token !== '' && token !== '\n' && token !== ';') {
|
|
node = parseAssignment()
|
|
node.comment = comment
|
|
}
|
|
|
|
// TODO: simplify this loop
|
|
while (token === '\n' || token === ';') { // eslint-disable-line no-unmodified-loop-condition
|
|
if (blocks.length === 0 && node) {
|
|
visible = (token !== ';')
|
|
blocks.push({
|
|
node: node,
|
|
visible: visible
|
|
})
|
|
}
|
|
|
|
getToken()
|
|
if (token !== '\n' && token !== ';' && token !== '') {
|
|
node = parseAssignment()
|
|
node.comment = comment
|
|
|
|
visible = (token !== ';')
|
|
blocks.push({
|
|
node: node,
|
|
visible: visible
|
|
})
|
|
}
|
|
}
|
|
|
|
if (blocks.length > 0) {
|
|
return new BlockNode(blocks)
|
|
} else {
|
|
if (!node) {
|
|
node = new ConstantNode(undefined)
|
|
node.comment = comment
|
|
}
|
|
|
|
return node
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assignment of a function or variable,
|
|
* - can be a variable like 'a=2.3'
|
|
* - or a updating an existing variable like 'matrix(2,3:5)=[6,7,8]'
|
|
* - defining a function like 'f(x) = x^2'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseAssignment () {
|
|
let name, args, value, valid
|
|
|
|
const node = parseConditional()
|
|
|
|
if (token === '=') {
|
|
if (type.isSymbolNode(node)) {
|
|
// parse a variable assignment like 'a = 2/3'
|
|
name = node.name
|
|
getTokenSkipNewline()
|
|
value = parseAssignment()
|
|
return new AssignmentNode(new SymbolNode(name), value)
|
|
} else if (type.isAccessorNode(node)) {
|
|
// parse a matrix subset assignment like 'A[1,2] = 4'
|
|
getTokenSkipNewline()
|
|
value = parseAssignment()
|
|
return new AssignmentNode(node.object, node.index, value)
|
|
} else if (type.isFunctionNode(node) && type.isSymbolNode(node.fn)) {
|
|
// parse function assignment like 'f(x) = x^2'
|
|
valid = true
|
|
args = []
|
|
|
|
name = node.name
|
|
node.args.forEach(function (arg, index) {
|
|
if (type.isSymbolNode(arg)) {
|
|
args[index] = arg.name
|
|
} else {
|
|
valid = false
|
|
}
|
|
})
|
|
|
|
if (valid) {
|
|
getTokenSkipNewline()
|
|
value = parseAssignment()
|
|
return new FunctionAssignmentNode(name, args, value)
|
|
}
|
|
}
|
|
|
|
throw createSyntaxError('Invalid left hand side of assignment operator =')
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* conditional operation
|
|
*
|
|
* condition ? truePart : falsePart
|
|
*
|
|
* Note: conditional operator is right-associative
|
|
*
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseConditional () {
|
|
let node = parseLogicalOr()
|
|
|
|
while (token === '?') { // eslint-disable-line no-unmodified-loop-condition
|
|
// set a conditional level, the range operator will be ignored as long
|
|
// as conditionalLevel === nestingLevel.
|
|
const prev = conditionalLevel
|
|
conditionalLevel = nestingLevel
|
|
getTokenSkipNewline()
|
|
|
|
const condition = node
|
|
const trueExpr = parseAssignment()
|
|
|
|
if (token !== ':') throw createSyntaxError('False part of conditional expression expected')
|
|
|
|
conditionalLevel = null
|
|
getTokenSkipNewline()
|
|
|
|
const falseExpr = parseAssignment() // Note: check for conditional operator again, right associativity
|
|
|
|
node = new ConditionalNode(condition, trueExpr, falseExpr)
|
|
|
|
// restore the previous conditional level
|
|
conditionalLevel = prev
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* logical or, 'x or y'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseLogicalOr () {
|
|
let node = parseLogicalXor()
|
|
|
|
while (token === 'or') { // eslint-disable-line no-unmodified-loop-condition
|
|
getTokenSkipNewline()
|
|
node = new OperatorNode('or', 'or', [node, parseLogicalXor()])
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* logical exclusive or, 'x xor y'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseLogicalXor () {
|
|
let node = parseLogicalAnd()
|
|
|
|
while (token === 'xor') { // eslint-disable-line no-unmodified-loop-condition
|
|
getTokenSkipNewline()
|
|
node = new OperatorNode('xor', 'xor', [node, parseLogicalAnd()])
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* logical and, 'x and y'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseLogicalAnd () {
|
|
let node = parseBitwiseOr()
|
|
|
|
while (token === 'and') { // eslint-disable-line no-unmodified-loop-condition
|
|
getTokenSkipNewline()
|
|
node = new OperatorNode('and', 'and', [node, parseBitwiseOr()])
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* bitwise or, 'x | y'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseBitwiseOr () {
|
|
let node = parseBitwiseXor()
|
|
|
|
while (token === '|') { // eslint-disable-line no-unmodified-loop-condition
|
|
getTokenSkipNewline()
|
|
node = new OperatorNode('|', 'bitOr', [node, parseBitwiseXor()])
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* bitwise exclusive or (xor), 'x ^| y'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseBitwiseXor () {
|
|
let node = parseBitwiseAnd()
|
|
|
|
while (token === '^|') { // eslint-disable-line no-unmodified-loop-condition
|
|
getTokenSkipNewline()
|
|
node = new OperatorNode('^|', 'bitXor', [node, parseBitwiseAnd()])
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* bitwise and, 'x & y'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseBitwiseAnd () {
|
|
let node = parseRelational()
|
|
|
|
while (token === '&') { // eslint-disable-line no-unmodified-loop-condition
|
|
getTokenSkipNewline()
|
|
node = new OperatorNode('&', 'bitAnd', [node, parseRelational()])
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* relational operators
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseRelational () {
|
|
let node, operators, name, fn, params
|
|
|
|
node = parseShift()
|
|
|
|
operators = {
|
|
'==': 'equal',
|
|
'!=': 'unequal',
|
|
'<': 'smaller',
|
|
'>': 'larger',
|
|
'<=': 'smallerEq',
|
|
'>=': 'largerEq'
|
|
}
|
|
while (operators.hasOwnProperty(token)) {
|
|
name = token
|
|
fn = operators[name]
|
|
|
|
getTokenSkipNewline()
|
|
params = [node, parseShift()]
|
|
node = new OperatorNode(name, fn, params)
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Bitwise left shift, bitwise right arithmetic shift, bitwise right logical shift
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseShift () {
|
|
let node, operators, name, fn, params
|
|
|
|
node = parseConversion()
|
|
|
|
operators = {
|
|
'<<': 'leftShift',
|
|
'>>': 'rightArithShift',
|
|
'>>>': 'rightLogShift'
|
|
}
|
|
|
|
while (operators.hasOwnProperty(token)) {
|
|
name = token
|
|
fn = operators[name]
|
|
|
|
getTokenSkipNewline()
|
|
params = [node, parseConversion()]
|
|
node = new OperatorNode(name, fn, params)
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* conversion operators 'to' and 'in'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseConversion () {
|
|
let node, operators, name, fn, params
|
|
|
|
node = parseRange()
|
|
|
|
operators = {
|
|
'to': 'to',
|
|
'in': 'to' // alias of 'to'
|
|
}
|
|
|
|
while (operators.hasOwnProperty(token)) {
|
|
name = token
|
|
fn = operators[name]
|
|
|
|
getTokenSkipNewline()
|
|
|
|
if (name === 'in' && token === '') {
|
|
// end of expression -> this is the unit 'in' ('inch')
|
|
node = new OperatorNode('*', 'multiply', [node, new SymbolNode('in')], true)
|
|
} else {
|
|
// operator 'a to b' or 'a in b'
|
|
params = [node, parseRange()]
|
|
node = new OperatorNode(name, fn, params)
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* parse range, "start:end", "start:step:end", ":", "start:", ":end", etc
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseRange () {
|
|
let node
|
|
const params = []
|
|
|
|
if (token === ':') {
|
|
// implicit start=1 (one-based)
|
|
node = new ConstantNode(1)
|
|
} else {
|
|
// explicit start
|
|
node = parseAddSubtract()
|
|
}
|
|
|
|
if (token === ':' && (conditionalLevel !== nestingLevel)) {
|
|
// we ignore the range operator when a conditional operator is being processed on the same level
|
|
params.push(node)
|
|
|
|
// parse step and end
|
|
while (token === ':' && params.length < 3) { // eslint-disable-line no-unmodified-loop-condition
|
|
getTokenSkipNewline()
|
|
|
|
if (token === ')' || token === ']' || token === ',' || token === '') {
|
|
// implicit end
|
|
params.push(new SymbolNode('end'))
|
|
} else {
|
|
// explicit end
|
|
params.push(parseAddSubtract())
|
|
}
|
|
}
|
|
|
|
if (params.length === 3) {
|
|
// params = [start, step, end]
|
|
node = new RangeNode(params[0], params[2], params[1]) // start, end, step
|
|
} else { // length === 2
|
|
// params = [start, end]
|
|
node = new RangeNode(params[0], params[1]) // start, end
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* add or subtract
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseAddSubtract () {
|
|
let node, operators, name, fn, params
|
|
|
|
node = parseMultiplyDivide()
|
|
|
|
operators = {
|
|
'+': 'add',
|
|
'-': 'subtract'
|
|
}
|
|
while (operators.hasOwnProperty(token)) {
|
|
name = token
|
|
fn = operators[name]
|
|
|
|
getTokenSkipNewline()
|
|
params = [node, parseMultiplyDivide()]
|
|
node = new OperatorNode(name, fn, params)
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* multiply, divide, modulus
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseMultiplyDivide () {
|
|
let node, last, operators, name, fn
|
|
|
|
node = parseImplicitMultiplication()
|
|
last = node
|
|
|
|
operators = {
|
|
'*': 'multiply',
|
|
'.*': 'dotMultiply',
|
|
'/': 'divide',
|
|
'./': 'dotDivide',
|
|
'%': 'mod',
|
|
'mod': 'mod'
|
|
}
|
|
|
|
while (true) {
|
|
if (operators.hasOwnProperty(token)) {
|
|
// explicit operators
|
|
name = token
|
|
fn = operators[name]
|
|
|
|
getTokenSkipNewline()
|
|
|
|
last = parseImplicitMultiplication()
|
|
node = new OperatorNode(name, fn, [node, last])
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* implicit multiplication
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseImplicitMultiplication () {
|
|
let node, last
|
|
|
|
node = parseRule2()
|
|
last = node
|
|
|
|
while (true) {
|
|
if ((tokenType === TOKENTYPE.SYMBOL) ||
|
|
(token === 'in' && type.isConstantNode(node)) ||
|
|
(tokenType === TOKENTYPE.NUMBER &&
|
|
!type.isConstantNode(last) &&
|
|
(!type.isOperatorNode(last) || last.op === '!')) ||
|
|
(token === '(')) {
|
|
// parse implicit multiplication
|
|
//
|
|
// symbol: implicit multiplication like '2a', '(2+3)a', 'a b'
|
|
// number: implicit multiplication like '(2+3)2'
|
|
// parenthesis: implicit multiplication like '2(3+4)', '(3+4)(1+2)'
|
|
last = parseRule2()
|
|
node = new OperatorNode('*', 'multiply', [node, last], true /* implicit */)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Infamous "rule 2" as described in https://github.com/josdejong/mathjs/issues/792#issuecomment-361065370
|
|
* Explicit division gets higher precedence than implicit multiplication
|
|
* when the division matches this pattern: [number] / [number] [symbol]
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseRule2 () {
|
|
let node, last
|
|
|
|
node = parseUnary()
|
|
last = node
|
|
|
|
while (true) {
|
|
// Match the "number /" part of the pattern "number / number symbol"
|
|
if (token === '/' && type.isConstantNode(last)) {
|
|
// Look ahead to see if the next token is a number
|
|
pushTokenState()
|
|
getTokenSkipNewline()
|
|
|
|
// Match the "number / number" part of the pattern
|
|
if (tokenType === TOKENTYPE.NUMBER) {
|
|
// Look ahead again
|
|
pushTokenState()
|
|
getTokenSkipNewline()
|
|
|
|
// Match the "symbol" part of the pattern, or a left parenthesis
|
|
if (tokenType === TOKENTYPE.SYMBOL || token === '(') {
|
|
// We've matched the pattern "number / number symbol".
|
|
// Rewind once and build the "number / number" node; the symbol will be consumed later
|
|
popTokenState()
|
|
discardTokenState()
|
|
last = parseUnary()
|
|
node = new OperatorNode('/', 'divide', [node, last])
|
|
} else {
|
|
// Not a match, so rewind
|
|
popTokenState()
|
|
popTokenState()
|
|
break
|
|
}
|
|
} else {
|
|
// Not a match, so rewind
|
|
popTokenState()
|
|
break
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Unary plus and minus, and logical and bitwise not
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseUnary () {
|
|
let name, params, fn
|
|
const operators = {
|
|
'-': 'unaryMinus',
|
|
'+': 'unaryPlus',
|
|
'~': 'bitNot',
|
|
'not': 'not'
|
|
}
|
|
|
|
if (operators.hasOwnProperty(token)) {
|
|
fn = operators[token]
|
|
name = token
|
|
|
|
getTokenSkipNewline()
|
|
params = [parseUnary()]
|
|
|
|
return new OperatorNode(name, fn, params)
|
|
}
|
|
|
|
return parsePow()
|
|
}
|
|
|
|
/**
|
|
* power
|
|
* Note: power operator is right associative
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parsePow () {
|
|
let node, name, fn, params
|
|
|
|
node = parseLeftHandOperators()
|
|
|
|
if (token === '^' || token === '.^') {
|
|
name = token
|
|
fn = (name === '^') ? 'pow' : 'dotPow'
|
|
|
|
getTokenSkipNewline()
|
|
params = [node, parseUnary()] // Go back to unary, we can have '2^-3'
|
|
node = new OperatorNode(name, fn, params)
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Left hand operators: factorial x!, ctranspose x'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseLeftHandOperators () {
|
|
let node, operators, name, fn, params
|
|
|
|
node = parseCustomNodes()
|
|
|
|
operators = {
|
|
'!': 'factorial',
|
|
'\'': 'ctranspose'
|
|
}
|
|
|
|
while (operators.hasOwnProperty(token)) {
|
|
name = token
|
|
fn = operators[name]
|
|
|
|
getToken()
|
|
params = [node]
|
|
|
|
node = new OperatorNode(name, fn, params)
|
|
node = parseAccessors(node)
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Parse a custom node handler. A node handler can be used to process
|
|
* nodes in a custom way, for example for handling a plot.
|
|
*
|
|
* A handler must be passed as second argument of the parse function.
|
|
* - must extend math.expression.node.Node
|
|
* - must contain a function _compile(defs: Object) : string
|
|
* - must contain a function find(filter: Object) : Node[]
|
|
* - must contain a function toString() : string
|
|
* - the constructor is called with a single argument containing all parameters
|
|
*
|
|
* For example:
|
|
*
|
|
* nodes = {
|
|
* 'plot': PlotHandler
|
|
* }
|
|
*
|
|
* The constructor of the handler is called as:
|
|
*
|
|
* node = new PlotHandler(params)
|
|
*
|
|
* The handler will be invoked when evaluating an expression like:
|
|
*
|
|
* node = math.parse('plot(sin(x), x)', nodes)
|
|
*
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseCustomNodes () {
|
|
let params = []
|
|
|
|
if (tokenType === TOKENTYPE.SYMBOL && extraNodes.hasOwnProperty(token)) {
|
|
const CustomNode = extraNodes[token]
|
|
|
|
getToken()
|
|
|
|
// parse parameters
|
|
if (token === '(') {
|
|
params = []
|
|
|
|
openParams()
|
|
getToken()
|
|
|
|
if (token !== ')') {
|
|
params.push(parseAssignment())
|
|
|
|
// parse a list with parameters
|
|
while (token === ',') { // eslint-disable-line no-unmodified-loop-condition
|
|
getToken()
|
|
params.push(parseAssignment())
|
|
}
|
|
}
|
|
|
|
if (token !== ')') {
|
|
throw createSyntaxError('Parenthesis ) expected')
|
|
}
|
|
closeParams()
|
|
getToken()
|
|
}
|
|
|
|
// create a new custom node
|
|
// noinspection JSValidateTypes
|
|
return new CustomNode(params)
|
|
}
|
|
|
|
return parseSymbol()
|
|
}
|
|
|
|
/**
|
|
* parse symbols: functions, variables, constants, units
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseSymbol () {
|
|
let node, name
|
|
|
|
if (tokenType === TOKENTYPE.SYMBOL ||
|
|
(tokenType === TOKENTYPE.DELIMITER && token in NAMED_DELIMITERS)) {
|
|
name = token
|
|
|
|
getToken()
|
|
|
|
if (CONSTANTS.hasOwnProperty(name)) { // true, false, null, ...
|
|
node = new ConstantNode(CONSTANTS[name])
|
|
} else if (NUMERIC_CONSTANTS.indexOf(name) !== -1) { // NaN, Infinity
|
|
node = new ConstantNode(numeric(name))
|
|
} else {
|
|
node = new SymbolNode(name)
|
|
}
|
|
|
|
// parse function parameters and matrix index
|
|
node = parseAccessors(node)
|
|
return node
|
|
}
|
|
|
|
return parseString()
|
|
}
|
|
|
|
/**
|
|
* parse accessors:
|
|
* - function invocation in round brackets (...), for example sqrt(2)
|
|
* - index enclosed in square brackets [...], for example A[2,3]
|
|
* - dot notation for properties, like foo.bar
|
|
* @param {Node} node Node on which to apply the parameters. If there
|
|
* are no parameters in the expression, the node
|
|
* itself is returned
|
|
* @param {string[]} [types] Filter the types of notations
|
|
* can be ['(', '[', '.']
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseAccessors (node, types) {
|
|
let params
|
|
|
|
while ((token === '(' || token === '[' || token === '.') &&
|
|
(!types || types.indexOf(token) !== -1)) { // eslint-disable-line no-unmodified-loop-condition
|
|
params = []
|
|
|
|
if (token === '(') {
|
|
if (type.isSymbolNode(node) || type.isAccessorNode(node)) {
|
|
// function invocation like fn(2, 3) or obj.fn(2, 3)
|
|
openParams()
|
|
getToken()
|
|
|
|
if (token !== ')') {
|
|
params.push(parseAssignment())
|
|
|
|
// parse a list with parameters
|
|
while (token === ',') { // eslint-disable-line no-unmodified-loop-condition
|
|
getToken()
|
|
params.push(parseAssignment())
|
|
}
|
|
}
|
|
|
|
if (token !== ')') {
|
|
throw createSyntaxError('Parenthesis ) expected')
|
|
}
|
|
closeParams()
|
|
getToken()
|
|
|
|
node = new FunctionNode(node, params)
|
|
} else {
|
|
// implicit multiplication like (2+3)(4+5) or sqrt(2)(1+2)
|
|
// don't parse it here but let it be handled by parseImplicitMultiplication
|
|
// with correct precedence
|
|
return node
|
|
}
|
|
} else if (token === '[') {
|
|
// index notation like variable[2, 3]
|
|
openParams()
|
|
getToken()
|
|
|
|
if (token !== ']') {
|
|
params.push(parseAssignment())
|
|
|
|
// parse a list with parameters
|
|
while (token === ',') { // eslint-disable-line no-unmodified-loop-condition
|
|
getToken()
|
|
params.push(parseAssignment())
|
|
}
|
|
}
|
|
|
|
if (token !== ']') {
|
|
throw createSyntaxError('Parenthesis ] expected')
|
|
}
|
|
closeParams()
|
|
getToken()
|
|
|
|
node = new AccessorNode(node, new IndexNode(params))
|
|
} else {
|
|
// dot notation like variable.prop
|
|
getToken()
|
|
|
|
if (tokenType !== TOKENTYPE.SYMBOL) {
|
|
throw createSyntaxError('Property name expected after dot')
|
|
}
|
|
params.push(new ConstantNode(token))
|
|
getToken()
|
|
|
|
const dotNotation = true
|
|
node = new AccessorNode(node, new IndexNode(params, dotNotation))
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* parse a string.
|
|
* A string is enclosed by double quotes
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseString () {
|
|
let node, str
|
|
|
|
if (token === '"') {
|
|
str = parseStringToken()
|
|
|
|
// create constant
|
|
node = new ConstantNode(str)
|
|
|
|
// parse index parameters
|
|
node = parseAccessors(node)
|
|
|
|
return node
|
|
}
|
|
|
|
return parseMatrix()
|
|
}
|
|
|
|
/**
|
|
* Parse a string surrounded by double quotes "..."
|
|
* @return {string}
|
|
*/
|
|
function parseStringToken () {
|
|
let str = ''
|
|
|
|
while (c !== '' && c !== '\"') {
|
|
if (c === '\\') {
|
|
// escape character, immediately process the next
|
|
// character to prevent stopping at a next '\"'
|
|
str += c
|
|
next()
|
|
}
|
|
|
|
str += c
|
|
next()
|
|
}
|
|
|
|
getToken()
|
|
if (token !== '"') {
|
|
throw createSyntaxError('End of string " expected')
|
|
}
|
|
getToken()
|
|
|
|
return JSON.parse('"' + str + '"') // unescape escaped characters
|
|
}
|
|
|
|
/**
|
|
* parse the matrix
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseMatrix () {
|
|
let array, params, rows, cols
|
|
|
|
if (token === '[') {
|
|
// matrix [...]
|
|
openParams()
|
|
getToken()
|
|
|
|
if (token !== ']') {
|
|
// this is a non-empty matrix
|
|
const row = parseRow()
|
|
|
|
if (token === ';') {
|
|
// 2 dimensional array
|
|
rows = 1
|
|
params = [row]
|
|
|
|
// the rows of the matrix are separated by dot-comma's
|
|
while (token === ';') { // eslint-disable-line no-unmodified-loop-condition
|
|
getToken()
|
|
|
|
params[rows] = parseRow()
|
|
rows++
|
|
}
|
|
|
|
if (token !== ']') {
|
|
throw createSyntaxError('End of matrix ] expected')
|
|
}
|
|
closeParams()
|
|
getToken()
|
|
|
|
// check if the number of columns matches in all rows
|
|
cols = params[0].items.length
|
|
for (let r = 1; r < rows; r++) {
|
|
if (params[r].items.length !== cols) {
|
|
throw createError('Column dimensions mismatch ' +
|
|
'(' + params[r].items.length + ' !== ' + cols + ')')
|
|
}
|
|
}
|
|
|
|
array = new ArrayNode(params)
|
|
} else {
|
|
// 1 dimensional vector
|
|
if (token !== ']') {
|
|
throw createSyntaxError('End of matrix ] expected')
|
|
}
|
|
closeParams()
|
|
getToken()
|
|
|
|
array = row
|
|
}
|
|
} else {
|
|
// this is an empty matrix "[ ]"
|
|
closeParams()
|
|
getToken()
|
|
array = new ArrayNode([])
|
|
}
|
|
|
|
return parseAccessors(array)
|
|
}
|
|
|
|
return parseObject()
|
|
}
|
|
|
|
/**
|
|
* Parse a single comma-separated row from a matrix, like 'a, b, c'
|
|
* @return {ArrayNode} node
|
|
*/
|
|
function parseRow () {
|
|
const params = [parseAssignment()]
|
|
let len = 1
|
|
|
|
while (token === ',') { // eslint-disable-line no-unmodified-loop-condition
|
|
getToken()
|
|
|
|
// parse expression
|
|
params[len] = parseAssignment()
|
|
len++
|
|
}
|
|
|
|
return new ArrayNode(params)
|
|
}
|
|
|
|
/**
|
|
* parse an object, enclosed in angle brackets{...}, for example {value: 2}
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseObject () {
|
|
if (token === '{') {
|
|
let key
|
|
|
|
const properties = {}
|
|
do {
|
|
getToken()
|
|
|
|
if (token !== '}') {
|
|
// parse key
|
|
if (token === '"') {
|
|
key = parseStringToken()
|
|
} else if (tokenType === TOKENTYPE.SYMBOL) {
|
|
key = token
|
|
getToken()
|
|
} else {
|
|
throw createSyntaxError('Symbol or string expected as object key')
|
|
}
|
|
|
|
// parse key/value separator
|
|
if (token !== ':') {
|
|
throw createSyntaxError('Colon : expected after object key')
|
|
}
|
|
getToken()
|
|
|
|
// parse key
|
|
properties[key] = parseAssignment()
|
|
}
|
|
}
|
|
while (token === ',') // eslint-disable-line no-unmodified-loop-condition
|
|
|
|
if (token !== '}') {
|
|
throw createSyntaxError('Comma , or bracket } expected after object value')
|
|
}
|
|
getToken()
|
|
|
|
let node = new ObjectNode(properties)
|
|
|
|
// parse index parameters
|
|
node = parseAccessors(node)
|
|
|
|
return node
|
|
}
|
|
|
|
return parseNumber()
|
|
}
|
|
|
|
/**
|
|
* parse a number
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseNumber () {
|
|
let numberStr
|
|
|
|
if (tokenType === TOKENTYPE.NUMBER) {
|
|
// this is a number
|
|
numberStr = token
|
|
getToken()
|
|
|
|
return new ConstantNode(numeric(numberStr, config.number))
|
|
}
|
|
|
|
return parseParentheses()
|
|
}
|
|
|
|
/**
|
|
* parentheses
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseParentheses () {
|
|
let node
|
|
|
|
// check if it is a parenthesized expression
|
|
if (token === '(') {
|
|
// parentheses (...)
|
|
openParams()
|
|
getToken()
|
|
|
|
node = parseAssignment() // start again
|
|
|
|
if (token !== ')') {
|
|
throw createSyntaxError('Parenthesis ) expected')
|
|
}
|
|
closeParams()
|
|
getToken()
|
|
|
|
node = new ParenthesisNode(node)
|
|
node = parseAccessors(node)
|
|
return node
|
|
}
|
|
|
|
return parseEnd()
|
|
}
|
|
|
|
/**
|
|
* Evaluated when the expression is not yet ended but expected to end
|
|
* @return {Node} res
|
|
* @private
|
|
*/
|
|
function parseEnd () {
|
|
if (token === '') {
|
|
// syntax error or unexpected end of expression
|
|
throw createSyntaxError('Unexpected end of expression')
|
|
} else if (token === "'") {
|
|
throw createSyntaxError('Value expected. Note: strings must be enclosed by double quotes')
|
|
} else {
|
|
throw createSyntaxError('Value expected')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shortcut for getting the current row value (one based)
|
|
* Returns the line of the currently handled expression
|
|
* @private
|
|
*/
|
|
/* TODO: implement keeping track on the row number
|
|
function row () {
|
|
return null
|
|
}
|
|
*/
|
|
|
|
/**
|
|
* Shortcut for getting the current col value (one based)
|
|
* Returns the column (position) where the last token starts
|
|
* @private
|
|
*/
|
|
function col () {
|
|
return index - token.length + 1
|
|
}
|
|
|
|
/**
|
|
* Create an error
|
|
* @param {string} message
|
|
* @return {SyntaxError} instantiated error
|
|
* @private
|
|
*/
|
|
function createSyntaxError (message) {
|
|
const c = col()
|
|
const error = new SyntaxError(message + ' (char ' + c + ')')
|
|
error['char'] = c
|
|
|
|
return error
|
|
}
|
|
|
|
/**
|
|
* Create an error
|
|
* @param {string} message
|
|
* @return {Error} instantiated error
|
|
* @private
|
|
*/
|
|
function createError (message) {
|
|
const c = col()
|
|
const error = new SyntaxError(message + ' (char ' + c + ')')
|
|
error['char'] = c
|
|
|
|
return error
|
|
}
|
|
|
|
return parse
|
|
}
|
|
|
|
exports.name = 'parse'
|
|
exports.path = 'expression'
|
|
exports.factory = factory
|