diff --git a/src/expression/node/AccessorNode.js b/src/expression/node/AccessorNode.js index b3c289986c..dd204bd253 100644 --- a/src/expression/node/AccessorNode.js +++ b/src/expression/node/AccessorNode.js @@ -136,7 +136,9 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({ * @return {AccessorNode} */ clone () { - return new AccessorNode(this.object, this.index) + const cloned = new AccessorNode(this.object, this.index) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/node/ArrayNode.js b/src/expression/node/ArrayNode.js index 800e536029..4535ad2e79 100644 --- a/src/expression/node/ArrayNode.js +++ b/src/expression/node/ArrayNode.js @@ -94,7 +94,9 @@ export const createArrayNode = /* #__PURE__ */ factory(name, dependencies, ({ No * @return {ArrayNode} */ clone () { - return new ArrayNode(this.items.slice(0)) + const cloned = new ArrayNode(this.items.slice(0)) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/node/AssignmentNode.js b/src/expression/node/AssignmentNode.js index 87805f110f..385b5e0ec4 100644 --- a/src/expression/node/AssignmentNode.js +++ b/src/expression/node/AssignmentNode.js @@ -231,7 +231,9 @@ export const createAssignmentNode = /* #__PURE__ */ factory(name, dependencies, * @return {AssignmentNode} */ clone () { - return new AssignmentNode(this.object, this.index, this.value) + const cloned = new AssignmentNode(this.object, this.index, this.value) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/node/BlockNode.js b/src/expression/node/BlockNode.js index 75b041d916..fd165b56a7 100644 --- a/src/expression/node/BlockNode.js +++ b/src/expression/node/BlockNode.js @@ -119,7 +119,9 @@ export const createBlockNode = /* #__PURE__ */ factory(name, dependencies, ({ Re } }) - return new BlockNode(blocks) + const cloned = new BlockNode(blocks) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/node/ConditionalNode.js b/src/expression/node/ConditionalNode.js index 121f4df6b4..a7dc27ee6c 100644 --- a/src/expression/node/ConditionalNode.js +++ b/src/expression/node/ConditionalNode.js @@ -121,7 +121,9 @@ export const createConditionalNode = /* #__PURE__ */ factory(name, dependencies, * @return {ConditionalNode} */ clone () { - return new ConditionalNode(this.condition, this.trueExpr, this.falseExpr) + const cloned = new ConditionalNode(this.condition, this.trueExpr, this.falseExpr) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/node/ConstantNode.js b/src/expression/node/ConstantNode.js index a2d44441d5..1ec8b8cf42 100644 --- a/src/expression/node/ConstantNode.js +++ b/src/expression/node/ConstantNode.js @@ -75,7 +75,9 @@ export const createConstantNode = /* #__PURE__ */ factory(name, dependencies, ({ * @return {ConstantNode} */ clone () { - return new ConstantNode(this.value) + const cloned = new ConstantNode(this.value) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/node/FunctionAssignmentNode.js b/src/expression/node/FunctionAssignmentNode.js index 459f8e06bc..ae9c9e90bd 100644 --- a/src/expression/node/FunctionAssignmentNode.js +++ b/src/expression/node/FunctionAssignmentNode.js @@ -149,8 +149,10 @@ export const createFunctionAssignmentNode = /* #__PURE__ */ factory(name, depend * @return {FunctionAssignmentNode} */ clone () { - return new FunctionAssignmentNode( + const cloned = new FunctionAssignmentNode( this.name, this.params.slice(0), this.expr) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/node/FunctionNode.js b/src/expression/node/FunctionNode.js index c873bff1c7..c350090bea 100644 --- a/src/expression/node/FunctionNode.js +++ b/src/expression/node/FunctionNode.js @@ -310,7 +310,9 @@ export const createFunctionNode = /* #__PURE__ */ factory(name, dependencies, ({ * @return {FunctionNode} */ clone () { - return new FunctionNode(this.fn, this.args.slice(0)) + const cloned = new FunctionNode(this.fn, this.args.slice(0)) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/node/IndexNode.js b/src/expression/node/IndexNode.js index bf4186bbc1..9756d56cdc 100644 --- a/src/expression/node/IndexNode.js +++ b/src/expression/node/IndexNode.js @@ -141,7 +141,9 @@ export const createIndexNode = /* #__PURE__ */ factory(name, dependencies, ({ No * @return {IndexNode} */ clone () { - return new IndexNode(this.dimensions.slice(0), this.dotNotation) + const cloned = new IndexNode(this.dimensions.slice(0), this.dotNotation) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/node/Node.js b/src/expression/node/Node.js index 3ac251313d..5c318355c6 100644 --- a/src/expression/node/Node.js +++ b/src/expression/node/Node.js @@ -23,6 +23,8 @@ export const createNode = /* #__PURE__ */ factory(name, dependencies, ({ mathWit } class Node { + sources = [] + get type () { return 'Node' } get isNode () { return true } @@ -204,6 +206,15 @@ export const createNode = /* #__PURE__ */ factory(name, dependencies, ({ mathWit throw new Error('Cannot clone a Node interface') } + /** + * Set the source indices mapping this node back to its + * location in the original source string + * @param {SourceMapping[]} sources - the data mapping this node back to its source string + */ + setSources (sources) { + this.sources = sources + } + /** * Create a deep clone of this node * @return {Node} diff --git a/src/expression/node/ObjectNode.js b/src/expression/node/ObjectNode.js index 99c8b5b0dc..09016eaa76 100644 --- a/src/expression/node/ObjectNode.js +++ b/src/expression/node/ObjectNode.js @@ -121,7 +121,9 @@ export const createObjectNode = /* #__PURE__ */ factory(name, dependencies, ({ N properties[key] = this.properties[key] } } - return new ObjectNode(properties) + const cloned = new ObjectNode(properties) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/node/OperatorNode.js b/src/expression/node/OperatorNode.js index 21eeeb744c..597130b73b 100644 --- a/src/expression/node/OperatorNode.js +++ b/src/expression/node/OperatorNode.js @@ -356,8 +356,10 @@ export const createOperatorNode = /* #__PURE__ */ factory(name, dependencies, ({ * @return {OperatorNode} */ clone () { - return new OperatorNode( + const cloned = new OperatorNode( this.op, this.fn, this.args.slice(0), this.implicit, this.isPercentage) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/node/ParenthesisNode.js b/src/expression/node/ParenthesisNode.js index 3a6848d3d4..3b99d0c751 100644 --- a/src/expression/node/ParenthesisNode.js +++ b/src/expression/node/ParenthesisNode.js @@ -79,7 +79,9 @@ export const createParenthesisNode = /* #__PURE__ */ factory(name, dependencies, * @return {ParenthesisNode} */ clone () { - return new ParenthesisNode(this.content) + const cloned = new ParenthesisNode(this.content) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/node/RangeNode.js b/src/expression/node/RangeNode.js index dd814d058a..9fbccd7bc9 100644 --- a/src/expression/node/RangeNode.js +++ b/src/expression/node/RangeNode.js @@ -146,7 +146,9 @@ export const createRangeNode = /* #__PURE__ */ factory(name, dependencies, ({ No * @return {RangeNode} */ clone () { - return new RangeNode(this.start, this.end, this.step && this.step) + const cloned = new RangeNode(this.start, this.end, this.step && this.step) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/node/RelationalNode.js b/src/expression/node/RelationalNode.js index 66dffea1c0..be005bf23e 100644 --- a/src/expression/node/RelationalNode.js +++ b/src/expression/node/RelationalNode.js @@ -109,7 +109,9 @@ export const createRelationalNode = /* #__PURE__ */ factory(name, dependencies, * @return {RelationalNode} */ clone () { - return new RelationalNode(this.conditionals, this.params) + const cloned = new RelationalNode(this.conditionals, this.params) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/node/SymbolNode.js b/src/expression/node/SymbolNode.js index da92197191..dd9a949c28 100644 --- a/src/expression/node/SymbolNode.js +++ b/src/expression/node/SymbolNode.js @@ -114,7 +114,9 @@ export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ m * @return {SymbolNode} */ clone () { - return new SymbolNode(this.name) + const cloned = new SymbolNode(this.name) + cloned.sources = this.sources + return cloned } /** diff --git a/src/expression/parse.js b/src/expression/parse.js index 54ce64bfd1..b777760e30 100644 --- a/src/expression/parse.js +++ b/src/expression/parse.js @@ -198,6 +198,29 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ } } + /** + * Convenience method which returns a given node with sources added. + * Useful for keeping `new Node()` one-liners as one-liners + * @param {Node} the node map + * @param {SourceMapping[]} the sources to add + * @return {Node} the mapped node + * @private + */ + function withSources (node, sources) { + node.setSources(sources) + return node + } + + /** + * Returns a mapping of the current token in state to its place in the source expression + * @param {Object} state + * @return {SourceMapping} the source mapping + * @private + */ + function tokenSource (state) { + return { index: state.index - state.token.length, text: state.token } + } + /** * View upto `length` characters of the expression starting at the current character. * @@ -609,6 +632,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ function parseBlock (state) { let node const blocks = [] + const sources = [] let visible if (state.token !== '' && state.token !== '\n' && state.token !== ';') { @@ -620,6 +644,8 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ // TODO: simplify this loop while (state.token === '\n' || state.token === ';') { // eslint-disable-line no-unmodified-loop-condition + sources.push(tokenSource(state)) + if (blocks.length === 0 && node) { visible = (state.token !== ';') blocks.push({ node, visible }) @@ -638,10 +664,10 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ } if (blocks.length > 0) { - return new BlockNode(blocks) + return withSources(new BlockNode(blocks), sources) } else { if (!node) { - node = new ConstantNode(undefined) + node = withSources(new ConstantNode(undefined), [{ index: 0, text: '' }]) if (state.comment) { node.comment = state.comment } @@ -664,18 +690,21 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ const node = parseConditional(state) + const source = tokenSource(state) + if (state.token === '=') { if (isSymbolNode(node)) { // parse a variable assignment like 'a = 2/3' name = node.name + const symbolNode = withSources(new SymbolNode(name), node.sources) getTokenSkipNewline(state) value = parseAssignment(state) - return new AssignmentNode(new SymbolNode(name), value) + return withSources(new AssignmentNode(symbolNode, value), [source]) } else if (isAccessorNode(node)) { // parse a matrix subset assignment like 'A[1,2] = 4' getTokenSkipNewline(state) value = parseAssignment(state) - return new AssignmentNode(node.object, node.index, value) + return withSources(new AssignmentNode(node.object, node.index, value), [source]) } else if (isFunctionNode(node) && isSymbolNode(node.fn)) { // parse function assignment like 'f(x) = x^2' valid = true @@ -693,7 +722,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (valid) { getTokenSkipNewline(state) value = parseAssignment(state) - return new FunctionAssignmentNode(name, args, value) + return withSources(new FunctionAssignmentNode(name, args, value), [source]) } } @@ -716,7 +745,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ function parseConditional (state) { let node = parseLogicalOr(state) + const condSources = [] while (state.token === '?') { // eslint-disable-line no-unmodified-loop-condition + condSources.push(tokenSource(state)) // set a conditional level, the range operator will be ignored as long // as conditionalLevel === state.nestingLevel. const prev = state.conditionalLevel @@ -728,12 +759,15 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.token !== ':') throw createSyntaxError(state, 'False part of conditional expression expected') + const condSource = condSources.pop() + const colonSource = tokenSource(state) + state.conditionalLevel = null getTokenSkipNewline(state) const falseExpr = parseAssignment(state) // Note: check for conditional operator again, right associativity - node = new ConditionalNode(condition, trueExpr, falseExpr) + node = withSources(new ConditionalNode(condition, trueExpr, falseExpr), [condSource, colonSource]) // restore the previous conditional level state.conditionalLevel = prev @@ -751,8 +785,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ let node = parseLogicalXor(state) while (state.token === 'or') { // eslint-disable-line no-unmodified-loop-condition + const source = tokenSource(state) getTokenSkipNewline(state) - node = new OperatorNode('or', 'or', [node, parseLogicalXor(state)]) + node = withSources(new OperatorNode('or', 'or', [node, parseLogicalXor(state)]), [source]) } return node @@ -767,8 +802,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ let node = parseLogicalAnd(state) while (state.token === 'xor') { // eslint-disable-line no-unmodified-loop-condition + const source = tokenSource(state) getTokenSkipNewline(state) - node = new OperatorNode('xor', 'xor', [node, parseLogicalAnd(state)]) + node = withSources(new OperatorNode('xor', 'xor', [node, parseLogicalAnd(state)]), [source]) } return node @@ -783,8 +819,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ let node = parseBitwiseOr(state) while (state.token === 'and') { // eslint-disable-line no-unmodified-loop-condition + const source = tokenSource(state) getTokenSkipNewline(state) - node = new OperatorNode('and', 'and', [node, parseBitwiseOr(state)]) + node = withSources(new OperatorNode('and', 'and', [node, parseBitwiseOr(state)]), [source]) } return node @@ -799,8 +836,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ let node = parseBitwiseXor(state) while (state.token === '|') { // eslint-disable-line no-unmodified-loop-condition + const source = tokenSource(state) getTokenSkipNewline(state) - node = new OperatorNode('|', 'bitOr', [node, parseBitwiseXor(state)]) + node = withSources(new OperatorNode('|', 'bitOr', [node, parseBitwiseXor(state)]), [source]) } return node @@ -815,8 +853,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ let node = parseBitwiseAnd(state) while (state.token === '^|') { // eslint-disable-line no-unmodified-loop-condition + const source = tokenSource(state) getTokenSkipNewline(state) - node = new OperatorNode('^|', 'bitXor', [node, parseBitwiseAnd(state)]) + node = withSources(new OperatorNode('^|', 'bitXor', [node, parseBitwiseAnd(state)]), [source]) } return node @@ -831,8 +870,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ let node = parseRelational(state) while (state.token === '&') { // eslint-disable-line no-unmodified-loop-condition + const source = tokenSource(state) getTokenSkipNewline(state) - node = new OperatorNode('&', 'bitAnd', [node, parseRelational(state)]) + node = withSources(new OperatorNode('&', 'bitAnd', [node, parseRelational(state)]), [source]) } return node @@ -855,7 +895,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ '>=': 'largerEq' } + const sources = [] while (hasOwnProperty(operators, state.token)) { // eslint-disable-line no-unmodified-loop-condition + sources.push(tokenSource(state)) const cond = { name: state.token, fn: operators[state.token] } conditionals.push(cond) getTokenSkipNewline(state) @@ -865,9 +907,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (params.length === 1) { return params[0] } else if (params.length === 2) { - return new OperatorNode(conditionals[0].name, conditionals[0].fn, params) + return withSources(new OperatorNode(conditionals[0].name, conditionals[0].fn, params), sources) } else { - return new RelationalNode(conditionals.map(c => c.fn), params) + return withSources(new RelationalNode(conditionals.map(c => c.fn), params), sources) } } @@ -891,9 +933,11 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ name = state.token fn = operators[name] + const source = tokenSource(state) + getTokenSkipNewline(state) params = [node, parseConversion(state)] - node = new OperatorNode(name, fn, params) + node = withSources(new OperatorNode(name, fn, params), [source]) } return node @@ -916,17 +960,19 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ while (hasOwnProperty(operators, state.token)) { name = state.token + const source = tokenSource(state) fn = operators[name] getTokenSkipNewline(state) if (name === 'in' && state.token === '') { // end of expression -> this is the unit 'in' ('inch') - node = new OperatorNode('*', 'multiply', [node, new SymbolNode('in')], true) + // no source mapping because this * operator is not explicitly in the source expression + node = new OperatorNode('*', 'multiply', [node, withSources(new SymbolNode('in'), [source])], true) } else { // operator 'a to b' or 'a in b' params = [node, parseRange(state)] - node = new OperatorNode(name, fn, params) + node = withSources(new OperatorNode(name, fn, params), [source]) } } @@ -944,7 +990,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.token === ':') { // implicit start=1 (one-based) - node = new ConstantNode(1) + const implicitSource = tokenSource(state) + implicitSource.text = '' + node = withSources(new ConstantNode(1), [implicitSource]) } else { // explicit start node = parseAddSubtract(state) @@ -954,13 +1002,17 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ // we ignore the range operator when a conditional operator is being processed on the same level params.push(node) + const sources = [] // parse step and end while (state.token === ':' && params.length < 3) { // eslint-disable-line no-unmodified-loop-condition + sources.push(tokenSource(state)) getTokenSkipNewline(state) if (state.token === ')' || state.token === ']' || state.token === ',' || state.token === '') { // implicit end - params.push(new SymbolNode('end')) + const implicitSource = tokenSource(state) + implicitSource.text = '' + params.push(withSources(new SymbolNode('end'), [implicitSource])) } else { // explicit end params.push(parseAddSubtract(state)) @@ -969,10 +1021,10 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (params.length === 3) { // params = [start, step, end] - node = new RangeNode(params[0], params[2], params[1]) // start, end, step + node = withSources(new RangeNode(params[0], params[2], params[1]), sources) // start, end, step } else { // length === 2 // params = [start, end] - node = new RangeNode(params[0], params[1]) // start, end + node = withSources(new RangeNode(params[0], params[1]), sources) // start, end } } @@ -996,15 +1048,17 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ while (hasOwnProperty(operators, state.token)) { name = state.token fn = operators[name] + const source = tokenSource(state) getTokenSkipNewline(state) const rightNode = parseMultiplyDivide(state) if (rightNode.isPercentage) { + // no mapping as this * operator is not in source expression params = [node, new OperatorNode('*', 'multiply', [node, rightNode])] } else { params = [node, rightNode] } - node = new OperatorNode(name, fn, params) + node = withSources(new OperatorNode(name, fn, params), [source]) } return node @@ -1033,11 +1087,12 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ // explicit operators name = state.token fn = operators[name] + const source = tokenSource(state) getTokenSkipNewline(state) last = parseImplicitMultiplication(state) - node = new OperatorNode(name, fn, [node, last]) + node = withSources(new OperatorNode(name, fn, [node, last]), [source]) } else { break } @@ -1069,8 +1124,15 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ // 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)' + const source = tokenSource(state) + + // mapping is an empty string at the index where * would be + // in the case of "in", the word itself represents the * + if (source.text !== 'in') { + source.text = '' + } last = parseRule2(state) - node = new OperatorNode('*', 'multiply', [node, last], true /* implicit */) + node = withSources(new OperatorNode('*', 'multiply', [node, last], true /* implicit */), [source]) } else { break } @@ -1096,6 +1158,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ while (true) { // Match the "number /" part of the pattern "number / number symbol" if (state.token === '/' && rule2Node(last)) { + const source = tokenSource(state) // Look ahead to see if the next token is a number tokenStates.push(Object.assign({}, state)) getTokenSkipNewline(state) @@ -1113,7 +1176,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ Object.assign(state, tokenStates.pop()) tokenStates.pop() last = parsePercentage(state) - node = new OperatorNode('/', 'divide', [node, last]) + node = withSources(new OperatorNode('/', 'divide', [node, last]), [source]) } else { // Not a match, so rewind tokenStates.pop() @@ -1150,15 +1213,18 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ while (hasOwnProperty(operators, state.token)) { name = state.token fn = operators[name] + const source = tokenSource(state) getTokenSkipNewline(state) if (name === '%' && state.tokenType === TOKENTYPE.DELIMITER && state.token !== '(') { // If the expression contains only %, then treat that as /100 - node = new OperatorNode('/', 'divide', [node, new ConstantNode(100)], false, true) + // source mapping for both / and 100 points back to % + const constant = withSources(new ConstantNode(100), [source]) + node = withSources(new OperatorNode('/', 'divide', [node, constant], false, true), [{ ...source }]) } else { params = [node, parseUnary(state)] - node = new OperatorNode(name, fn, params) + node = withSources(new OperatorNode(name, fn, params), [source]) } } @@ -1182,11 +1248,12 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (hasOwnProperty(operators, state.token)) { fn = operators[state.token] name = state.token + const source = tokenSource(state) getTokenSkipNewline(state) params = [parseUnary(state)] - return new OperatorNode(name, fn, params) + return withSources(new OperatorNode(name, fn, params), [source]) } return parsePow(state) @@ -1206,10 +1273,11 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.token === '^' || state.token === '.^') { name = state.token fn = (name === '^') ? 'pow' : 'dotPow' + const source = tokenSource(state) getTokenSkipNewline(state) params = [node, parseUnary(state)] // Go back to unary, we can have '2^-3' - node = new OperatorNode(name, fn, params) + node = withSources(new OperatorNode(name, fn, params), [source]) } return node @@ -1233,11 +1301,12 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ while (hasOwnProperty(operators, state.token)) { name = state.token fn = operators[name] + const source = tokenSource(state) getToken(state) params = [node] - node = new OperatorNode(name, fn, params) + node = withSources(new OperatorNode(name, fn, params), [source]) node = parseAccessors(state, node) } @@ -1278,11 +1347,14 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.tokenType === TOKENTYPE.SYMBOL && hasOwnProperty(state.extraNodes, state.token)) { const CustomNode = state.extraNodes[state.token] + const sources = [tokenSource(state)] + getToken(state) // parse parameters if (state.token === '(') { params = [] + sources.push(tokenSource(state)) openParams(state) getToken(state) @@ -1292,6 +1364,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ // parse a list with parameters while (state.token === ',') { // eslint-disable-line no-unmodified-loop-condition + sources.push(tokenSource(state)) getToken(state) params.push(parseAssignment(state)) } @@ -1300,13 +1373,14 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.token !== ')') { throw createSyntaxError(state, 'Parenthesis ) expected') } + sources.push(tokenSource(state)) closeParams(state) getToken(state) } // create a new custom node // noinspection JSValidateTypes - return new CustomNode(params) + return withSources(new CustomNode(params), sources) } return parseSymbol(state) @@ -1324,14 +1398,16 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ (state.tokenType === TOKENTYPE.DELIMITER && state.token in NAMED_DELIMITERS)) { name = state.token + const source = tokenSource(state) + getToken(state) if (hasOwnProperty(CONSTANTS, name)) { // true, false, null, ... - node = new ConstantNode(CONSTANTS[name]) + node = withSources(new ConstantNode(CONSTANTS[name]), [source]) } else if (NUMERIC_CONSTANTS.indexOf(name) !== -1) { // NaN, Infinity - node = new ConstantNode(numeric(name, 'number')) + node = withSources(new ConstantNode(numeric(name, 'number')), [source]) } else { - node = new SymbolNode(name) + node = withSources(new SymbolNode(name), [source]) } // parse function parameters and matrix index @@ -1359,6 +1435,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ function parseAccessors (state, node, types) { let params + const sources = [tokenSource(state)] while ((state.token === '(' || state.token === '[' || state.token === '.') && (!types || types.indexOf(state.token) !== -1)) { // eslint-disable-line no-unmodified-loop-condition params = [] @@ -1374,6 +1451,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ // parse a list with parameters while (state.token === ',') { // eslint-disable-line no-unmodified-loop-condition + sources.push(tokenSource(state)) getToken(state) params.push(parseAssignment(state)) } @@ -1382,10 +1460,11 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.token !== ')') { throw createSyntaxError(state, 'Parenthesis ) expected') } + sources.push(tokenSource(state)) closeParams(state) getToken(state) - node = new FunctionNode(node, params) + node = withSources(new FunctionNode(node, params), sources) } 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 @@ -1396,12 +1475,14 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ // index notation like variable[2, 3] openParams(state) getToken(state) + const indexSources = [] if (state.token !== ']') { params.push(parseAssignment(state)) // parse a list with parameters while (state.token === ',') { // eslint-disable-line no-unmodified-loop-condition + indexSources.push(tokenSource(state)) getToken(state) params.push(parseAssignment(state)) } @@ -1410,10 +1491,12 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.token !== ']') { throw createSyntaxError(state, 'Parenthesis ] expected') } + sources.push(tokenSource(state)) closeParams(state) getToken(state) - node = new AccessorNode(node, new IndexNode(params)) + const indexNode = withSources(new IndexNode(params), indexSources) + node = withSources(new AccessorNode(node, indexNode), sources) } else { // dot notation like variable.prop getToken(state) @@ -1421,11 +1504,13 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.tokenType !== TOKENTYPE.SYMBOL) { throw createSyntaxError(state, 'Property name expected after dot') } - params.push(new ConstantNode(state.token)) + const constantSource = tokenSource(state) + params.push(withSources(new ConstantNode(state.token), [constantSource])) getToken(state) const dotNotation = true - node = new AccessorNode(node, new IndexNode(params, dotNotation)) + const indexNode = withSources(new IndexNode(params, dotNotation), [{ ...constantSource }]) + node = withSources(new AccessorNode(node, indexNode, sources)) } } @@ -1441,10 +1526,15 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ let node, str if (state.token === '"') { + const sources = [tokenSource(state)] + str = parseDoubleQuotesStringToken(state) + sources.push(tokenSource(state)) + getToken(state) + // create constant - node = new ConstantNode(str) + node = withSources(new ConstantNode(str), sources) // parse index parameters node = parseAccessors(state, node) @@ -1478,7 +1568,6 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.token !== '"') { throw createSyntaxError(state, 'End of string " expected') } - getToken(state) return JSON.parse('"' + str + '"') // unescape escaped characters } @@ -1492,10 +1581,15 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ let node, str if (state.token === '\'') { + const sources = [tokenSource(state)] + str = parseSingleQuotesStringToken(state) + sources.push(tokenSource(state)) + getToken(state) + // create constant - node = new ConstantNode(str) + node = withSources(new ConstantNode(str), sources) // parse index parameters node = parseAccessors(state, node) @@ -1529,7 +1623,6 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.token !== '\'') { throw createSyntaxError(state, 'End of string \' expected') } - getToken(state) return JSON.parse('"' + str + '"') // unescape escaped characters } @@ -1543,6 +1636,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ let array, params, rows, cols if (state.token === '[') { + const sources = [tokenSource(state)] // matrix [...] openParams(state) getToken(state) @@ -1558,6 +1652,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ // the rows of the matrix are separated by dot-comma's while (state.token === ';') { // eslint-disable-line no-unmodified-loop-condition + sources.push(tokenSource(state)) getToken(state) params[rows] = parseRow(state) @@ -1567,6 +1662,8 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.token !== ']') { throw createSyntaxError(state, 'End of matrix ] expected') } + + sources.push(tokenSource(state)) closeParams(state) getToken(state) @@ -1579,12 +1676,15 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ } } - array = new ArrayNode(params) + array = withSources(new ArrayNode(params), sources) } else { // 1 dimensional vector if (state.token !== ']') { throw createSyntaxError(state, 'End of matrix ] expected') } + + // merge the [] sources with the ,,, sources from parseRow + row.sources = [...sources, ...row.sources, tokenSource(state)] closeParams(state) getToken(state) @@ -1592,9 +1692,10 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ } } else { // this is an empty matrix "[ ]" + sources.push(tokenSource(state)) closeParams(state) getToken(state) - array = new ArrayNode([]) + array = withSources(new ArrayNode([]), sources) } return parseAccessors(state, array) @@ -1611,7 +1712,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ const params = [parseAssignment(state)] let len = 1 + const sources = [] while (state.token === ',') { // eslint-disable-line no-unmodified-loop-condition + sources.push(tokenSource(state)) getToken(state) // parse expression @@ -1619,7 +1722,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ len++ } - return new ArrayNode(params) + return withSources(new ArrayNode(params), sources) } /** @@ -1629,19 +1732,26 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ */ function parseObject (state) { if (state.token === '{') { + const sources = [tokenSource(state)] openParams(state) let key const properties = {} do { + if (state.token === ',') { + sources.push(tokenSource(state)) + } + getToken(state) if (state.token !== '}') { // parse key if (state.token === '"') { key = parseDoubleQuotesStringToken(state) + getToken(state) } else if (state.token === '\'') { key = parseSingleQuotesStringToken(state) + getToken(state) } else if (state.tokenType === TOKENTYPE.SYMBOL || (state.tokenType === TOKENTYPE.DELIMITER && state.token in NAMED_DELIMITERS)) { key = state.token getToken(state) @@ -1653,6 +1763,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.token !== ':') { throw createSyntaxError(state, 'Colon : expected after object key') } + sources.push(tokenSource(state)) getToken(state) // parse key @@ -1664,10 +1775,13 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.token !== '}') { throw createSyntaxError(state, 'Comma , or bracket } expected after object value') } + + sources.push(tokenSource(state)) + closeParams(state) getToken(state) - let node = new ObjectNode(properties) + let node = withSources(new ObjectNode(properties), sources) // parse index parameters node = parseAccessors(state, node) @@ -1689,9 +1803,10 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.tokenType === TOKENTYPE.NUMBER) { // this is a number numberStr = state.token + const source = tokenSource(state) getToken(state) - return new ConstantNode(numeric(numberStr, config.number)) + return withSources(new ConstantNode(numeric(numberStr, config.number)), [source]) } return parseParentheses(state) @@ -1707,6 +1822,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ // check if it is a parenthesized expression if (state.token === '(') { + const sources = [tokenSource(state)] // parentheses (...) openParams(state) getToken(state) @@ -1716,10 +1832,13 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.token !== ')') { throw createSyntaxError(state, 'Parenthesis ) expected') } + + sources.push(tokenSource(state)) + closeParams(state) getToken(state) - node = new ParenthesisNode(node) + node = withSources(new ParenthesisNode(node), sources) node = parseAccessors(state, node) return node } diff --git a/src/function/algebra/resolve.js b/src/function/algebra/resolve.js index 5e8f80f155..610291ef76 100644 --- a/src/function/algebra/resolve.js +++ b/src/function/algebra/resolve.js @@ -65,9 +65,13 @@ export const createResolve = /* #__PURE__ */ factory(name, dependencies, ({ nextWithin.add(node.name) return _resolve(value, scope, nextWithin) } else if (typeof value === 'number') { - return parse(String(value)) + const parsed = parse(String(value)) + parsed.sources = [] + return parsed } else if (value !== undefined) { - return new ConstantNode(value) + const parsed = new ConstantNode(value) + parsed.sources = [] + return parsed } else { return node } @@ -75,14 +79,20 @@ export const createResolve = /* #__PURE__ */ factory(name, dependencies, ({ const args = node.args.map(function (arg) { return _resolve(arg, scope, within) }) - return new OperatorNode(node.op, node.fn, args, node.implicit) + const newNode = new OperatorNode(node.op, node.fn, args, node.implicit) + newNode.sources = node.sources + return newNode } else if (isParenthesisNode(node)) { - return new ParenthesisNode(_resolve(node.content, scope, within)) + const parenNode = new ParenthesisNode(_resolve(node.content, scope, within)) + parenNode.sources = [] + return parenNode } else if (isFunctionNode(node)) { const args = node.args.map(function (arg) { return _resolve(arg, scope, within) }) - return new FunctionNode(node.name, args) + const fnNode = new FunctionNode(node.name, args) + fnNode.sources = [] + return fnNode } // Otherwise just recursively resolve any children (might also work diff --git a/test/unit-tests/expression/parse.test.js b/test/unit-tests/expression/parse.test.js index c2bf5000c2..cb989dc0c8 100644 --- a/test/unit-tests/expression/parse.test.js +++ b/test/unit-tests/expression/parse.test.js @@ -29,6 +29,16 @@ function parseAndStringifyWithParens (expr) { return parse(expr).toString({ parenthesis: 'all' }) } +/** + * Helper to delete sources from a node and return it. We use this because + * "identical" nodes will not be equal if parsed from different strings, + * which breaks deepStrictEqual assertions + */ +function emptySources (node) { + node.sources = [] + return node +} + describe('parse', function () { it('should parse a single expression', function () { approx.equal(parse('2 + 6 / 3').compile().evaluate(), 4) @@ -951,18 +961,18 @@ describe('parse', function () { }) it('should parse constants', function () { - assert.strictEqual(parse('true').type, 'ConstantNode') - assert.deepStrictEqual(parse('true'), new ConstantNode(true)) - assert.deepStrictEqual(parse('false'), new ConstantNode(false)) - assert.deepStrictEqual(parse('null'), new ConstantNode(null)) - assert.deepStrictEqual(parse('undefined'), new ConstantNode(undefined)) + assert.strictEqual(emptySources(parse('true')).type, 'ConstantNode') + assert.deepStrictEqual(emptySources(parse('true')), new ConstantNode(true)) + assert.deepStrictEqual(emptySources(parse('false')), new ConstantNode(false)) + assert.deepStrictEqual(emptySources(parse('null')), new ConstantNode(null)) + assert.deepStrictEqual(emptySources(parse('undefined')), new ConstantNode(undefined)) }) it('should parse numeric constants', function () { const nanConstantNode = parse('NaN') assert.deepStrictEqual(nanConstantNode.type, 'ConstantNode') assert.ok(isNaN(nanConstantNode.value)) - assert.deepStrictEqual(parse('Infinity'), new ConstantNode(Infinity)) + assert.deepStrictEqual(emptySources(parse('Infinity')), new ConstantNode(Infinity)) }) it('should evaluate constants', function () { @@ -2231,4 +2241,228 @@ describe('parse', function () { assert.strictEqual(mathClone.evaluate('2'), 2) }) + + describe('sources', function () { + it('adds sources for constants', function () { + const parsed = math.parse('4') + assert.deepStrictEqual(parsed.sources, [{ index: 0, text: '4' }]) + }) + + it('adds sources for symbols', function () { + const parsed = math.parse('foo') + assert.deepStrictEqual(parsed.sources, [{ index: 0, text: 'foo' }]) + }) + + it('adds sources for operators', function () { + const parsed = math.parse('1 + 2') + assert.deepStrictEqual(parsed.sources, [{ index: 2, text: '+' }]) + assert.deepStrictEqual(parsed.args[0].sources, [{ index: 0, text: '1' }]) + assert.deepStrictEqual(parsed.args[1].sources, [{ index: 4, text: '2' }]) + }) + + it('adds sources for blocks', function () { + const parsed = math.parse('1 + 1; 2 + 2\n3 + 3') + + // should have a source for each block delimiter + const expected = [ + { index: 5, text: ';' }, + { index: 12, text: '\n' } + ] + + assert.deepStrictEqual(parsed.sources, expected) + }) + + it('adds sources for 1D matrices', function () { + const parsed = math.parse('[1, 2, 3]') + + // should have a source for brackets and item delimiters + const expected = [ + { index: 0, text: '[' }, + { index: 2, text: ',' }, + { index: 5, text: ',' }, + { index: 8, text: ']' } + ] + + assert.deepStrictEqual(parsed.sources, expected) + }) + + it('adds sources for 2D matrices', function () { + const parsed = math.parse('[1, 2; 3, 4]') + + // outer matrix has sources for brackets and row delimeters + const expected = [ + { index: 0, text: '[' }, + { index: 5, text: ';' }, + { index: 11, text: ']' } + ] + + assert.deepStrictEqual(parsed.sources, expected) + + // inner matrices only have sources for item delimeters + assert.deepStrictEqual(parsed.items[0].sources, [{ index: 2, text: ',' }]) + assert.deepStrictEqual(parsed.items[1].sources, [{ index: 8, text: ',' }]) + }) + + it('adds sources for empty matrices', function () { + const parsed = math.parse('[]') + + // should have a source for brackets and item delimiters + const expected = [ + { index: 0, text: '[' }, + { index: 1, text: ']' } + ] + + assert.deepStrictEqual(parsed.sources, expected) + }) + + it('adds sources for ranges', function () { + const parsed = math.parse('1:2:3') + + assert.deepStrictEqual(parsed.start.sources, [{ index: 0, text: '1' }]) + assert.deepStrictEqual(parsed.step.sources, [{ index: 2, text: '2' }]) + assert.deepStrictEqual(parsed.end.sources, [{ index: 4, text: '3' }]) + + const delimiters = [ + { index: 1, text: ':' }, + { index: 3, text: ':' } + ] + + assert.deepStrictEqual(parsed.sources, delimiters) + + // implicit start and end sources point to where the value would be + + const implicitStart = math.parse(':1') + assert.deepStrictEqual(implicitStart.start.sources, [{ index: 0, text: '' }]) + + const implicitEnd = math.parse('1:') + assert.deepStrictEqual(implicitEnd.end.sources, [{ index: 2, text: '' }]) + }) + + it('adds sources for parentheses', function () { + // should properly match outer and inner parentheses + const outerParen = math.parse('( 1 + (2 + 3))') + const outerSources = [ + { index: 0, text: '(' }, + { index: 13, text: ')' } + ] + assert.deepStrictEqual(outerParen.sources, outerSources) + + const innerParen = outerParen.content.args[1] + const innerSources = [ + { index: 6, text: '(' }, + { index: 12, text: ')' } + ] + assert.deepStrictEqual(innerParen.sources, innerSources) + }) + + it('adds sources for the conditional operator', function () { + // should properly match outer and inner conditional delimeters + const outerCond = math.parse('true ? (false ? 1 : 2) : 3') + const outerSources = [ + { index: 5, text: '?' }, + { index: 23, text: ':' } + ] + assert.deepStrictEqual(outerCond.sources, outerSources) + + const innerCond = outerCond.trueExpr.content + const innerSources = [ + { index: 14, text: '?' }, + { index: 18, text: ':' } + ] + assert.deepStrictEqual(innerCond.sources, innerSources) + }) + + it('adds sources for assignments', function () { + const parsed = math.parse('val = 42') + assert.deepStrictEqual(parsed.sources, [{ index: 4, text: '=' }]) + }) + + it('adds sources for percents', function () { + const parsed = math.parse('13%') + assert.deepStrictEqual(parsed.sources, [{ index: 2, text: '%' }]) + }) + + it('adds sources for implicit multiplication', function () { + const parsed = math.parse('2a') + + // index is where the multiplication symbol would be + assert.deepStrictEqual(parsed.sources, [{ index: 1, text: '' }]) + }) + + it('adds sources for conversions', function () { + const parsedTo = math.parse('1 foot to in') + assert.deepStrictEqual(parsedTo.sources, [{ index: 7, text: 'to' }]) + + const parsedIn = math.parse('in in 1 foot') + assert.deepStrictEqual(parsedIn.sources, [{ index: 3, text: 'in' }]) + }) + + it('adds sources for unary operators', function () { + const unaryPlus = math.parse('+1') + const unaryMinus = math.parse('-1') + assert.deepStrictEqual(unaryPlus.sources, [{ index: 0, text: '+' }]) + assert.deepStrictEqual(unaryMinus.sources, [{ index: 0, text: '-' }]) + }) + + it('adds sources for power operators', function () { + const parsed = math.parse('2^4') + assert.deepStrictEqual(parsed.sources, [{ index: 1, text: '^' }]) + }) + + it('adds sources for constants', function () { + const parsedTrue = math.parse('true') + assert.deepStrictEqual(parsedTrue.sources, [{ index: 0, text: 'true' }]) + + const parsedNull = math.parse('null') + assert.deepStrictEqual(parsedNull.sources, [{ index: 0, text: 'null' }]) + + const parsedInfinity = math.parse('Infinity') + assert.deepStrictEqual(parsedInfinity.sources, [{ index: 0, text: 'Infinity' }]) + + const parsedNaN = math.parse('NaN') + assert.deepStrictEqual(parsedNaN.sources, [{ index: 0, text: 'NaN' }]) + }) + + it('adds sources for function calls', function () { + const parsed = math.parse('foo(1, 2)') + + // should have sources for parens and each param delimeter + const sources = [ + { index: 3, text: '(' }, + { index: 5, text: ',' }, + { index: 8, text: ')' } + ] + assert.deepStrictEqual(parsed.sources, sources) + }) + + it('adds sources for string literals', function () { + const singleQuote = math.parse("'hello'") + const singleSources = [ + { index: 0, text: "'" }, + { index: 6, text: "'" } + ] + assert.deepStrictEqual(singleQuote.sources, singleSources) + + const doubleQuote = math.parse('"hello"') + const doubleSources = [ + { index: 0, text: '"' }, + { index: 6, text: '"' } + ] + assert.deepStrictEqual(doubleQuote.sources, doubleSources) + }) + + it('adds sources for objects', function () { + const parsed = math.parse('{ foo: 13, bar: 25 }') + + // sources include brackets, key-value delimiters, and entry delimeters + const expected = [ + { index: 0, text: '{' }, + { index: 5, text: ':' }, + { index: 9, text: ',' }, + { index: 14, text: ':' }, + { index: 19, text: '}' } + ] + assert.deepStrictEqual(parsed.sources, expected) + }) + }) }) diff --git a/test/unit-tests/function/algebra/resolve.test.js b/test/unit-tests/function/algebra/resolve.test.js index c0e73b7504..57c9a3a760 100644 --- a/test/unit-tests/function/algebra/resolve.test.js +++ b/test/unit-tests/function/algebra/resolve.test.js @@ -5,6 +5,28 @@ import math from '../../../../src/defaultInstance.js' import { simplifyAndCompare } from './simplify.test.js' +function emptySources (...args) { + args.forEach((item) => { + if (item.traverse != null) { + emptySourcesFromTree(item) + } else if (item.forEach != null) { + emptySourcesFromArray(item) + } + }) + + return args +} + +function emptySourcesFromArray (array) { + array.forEach((item) => emptySources(item)) +} + +function emptySourcesFromTree (tree) { + tree.traverse((node) => { + node.sources = [] + }) +} + describe('resolve', function () { it('should substitute scoped constants', function () { const sumxy = math.parse('x+y') @@ -45,21 +67,25 @@ describe('resolve', function () { it('should operate directly on strings', function () { const collapsingScope = { x: math.parse('y'), y: math.parse('z') } - assert.deepStrictEqual(math.resolve('x+y', { x: 1 }), math.parse('1 + y')) - assert.deepStrictEqual( + assert.deepStrictEqual(...emptySources(math.resolve('x+y', { x: 1 }), math.parse('1 + y'))) + assert.deepStrictEqual(...emptySources( math.resolve('x + y', collapsingScope), - math.parse('z + z')) - assert.deepStrictEqual( + math.parse('z + z') + + )) + assert.deepStrictEqual(...emptySources( math.resolve('[x, y, 1, w]', collapsingScope), - math.parse('[z, z, 1, w]')) + math.parse('[z, z, 1, w]') + )) }) it('should substitute scoped constants from Map like scopes', function () { assert.strictEqual( math.resolve(math.parse('x+y'), new Map([['x', 1]])).toString(), '1 + y' ) // direct - assert.deepStrictEqual( + assert.deepStrictEqual(...emptySources( math.resolve('x+y', new Map([['x', 1]])), math.parse('1 + y')) + ) simplifyAndCompare('x+y', 'x+y', new Map()) // operator simplifyAndCompare('x+y', 'y+1', new Map([['x', 1]])) simplifyAndCompare('x+y', 'y+1', new Map([['x', math.parse('1')]])) @@ -70,16 +96,16 @@ describe('resolve', function () { const scope = { x: 1, y: 2 } const expressions = [parse('x+z'), 'y+z', 'y-x'] let results = [parse('x+z'), parse('y+z'), parse('y-x')] - assert.deepStrictEqual(math.resolve(expressions), results) + assert.deepStrictEqual(...emptySources(math.resolve(expressions), results)) results = [parse('1+z'), parse('2+z'), parse('2-1')] - assert.deepStrictEqual(math.resolve(expressions, scope), results) - assert.deepStrictEqual( + assert.deepStrictEqual(...emptySources(math.resolve(expressions, scope), results)) + assert.deepStrictEqual(...emptySources( math.resolve(math.matrix(expressions), scope), math.matrix(results) - ) + )) const nested = ['z/y', ['x+x', 'gcd(x,y)'], '3+x'] results = [parse('z/2'), [parse('1+1'), parse('gcd(1,2)')], parse('3+1')] - assert.deepStrictEqual(math.resolve(nested, scope), results) + assert.deepStrictEqual(...emptySources(math.resolve(nested, scope), results)) }) it('should throw a readable error if one item is wrong type', function () { @@ -102,4 +128,15 @@ describe('resolve', function () { }), /ReferenceError.*\{x, y, z\}/) }) + + it('should set blank sources for resolved values', function () { + const resolved = math.resolve('1 + x', { x: 5 }) + + // standard nodes should still have sources + assert.deepStrictEqual(resolved.sources, [{ index: 2, text: '+' }]) + assert.deepStrictEqual(resolved.args[0].sources, [{ index: 0, text: '1' }]) + + // resolved variable should have no sources + assert.deepStrictEqual(resolved.args[1].sources, []) + }) }) diff --git a/types/index.d.ts b/types/index.d.ts index 515a843746..2347ddd2b9 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -28,6 +28,12 @@ declare namespace math { [key: string]: FactoryFunction | FactoryFunctionMap } + // Maps a parsed node back to its place in the original source string + interface SourceMapping { + index: number + text: string + } + /** Available options for parse */ interface ParseOptions { /** a set of custom nodes */