From 742e662573b9e3d22cbc413c2ed08f5630edae85 Mon Sep 17 00:00:00 2001 From: Matthew Canestraro Date: Wed, 15 Mar 2023 15:26:56 -0400 Subject: [PATCH 1/5] Add source mappings for all nodes on parse() --- src/expression/node/AccessorNode.js | 4 +- src/expression/node/ArrayNode.js | 4 +- src/expression/node/AssignmentNode.js | 4 +- src/expression/node/BlockNode.js | 4 +- src/expression/node/ConditionalNode.js | 4 +- src/expression/node/ConstantNode.js | 4 +- src/expression/node/FunctionAssignmentNode.js | 4 +- src/expression/node/FunctionNode.js | 4 +- src/expression/node/IndexNode.js | 4 +- src/expression/node/Node.js | 11 + src/expression/node/ObjectNode.js | 4 +- src/expression/node/OperatorNode.js | 4 +- src/expression/node/ParenthesisNode.js | 4 +- src/expression/node/RangeNode.js | 4 +- src/expression/node/RelationalNode.js | 4 +- src/expression/node/SymbolNode.js | 4 +- src/expression/parse.js | 215 +++++++++++---- src/function/algebra/resolve.js | 20 +- test/unit-tests/expression/parse.test.js | 246 +++++++++++++++++- .../function/algebra/resolve.test.js | 59 ++++- types/index.d.ts | 6 + 21 files changed, 532 insertions(+), 85 deletions(-) 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 */ From 173b20a00a2c592f1236801cbe8157162470ff32 Mon Sep 17 00:00:00 2001 From: Matthew Canestraro Date: Tue, 4 Apr 2023 15:36:21 -0400 Subject: [PATCH 2/5] Use constructors instead of setSources() --- AUTHORS | 1 + src/expression/node/AccessorNode.js | 12 +- src/expression/node/ArrayNode.js | 12 +- src/expression/node/AssignmentNode.js | 12 +- src/expression/node/BlockNode.js | 12 +- src/expression/node/ConditionalNode.js | 12 +- src/expression/node/ConstantNode.js | 12 +- src/expression/node/FunctionAssignmentNode.js | 12 +- src/expression/node/FunctionNode.js | 12 +- src/expression/node/IndexNode.js | 11 +- src/expression/node/Node.js | 20 ++-- src/expression/node/ObjectNode.js | 12 +- src/expression/node/OperatorNode.js | 12 +- src/expression/node/ParenthesisNode.js | 12 +- src/expression/node/RangeNode.js | 14 ++- src/expression/node/RelationalNode.js | 12 +- src/expression/node/SymbolNode.js | 12 +- src/expression/parse.js | 113 ++++++++---------- src/function/algebra/resolve.js | 15 +-- test/node-tests/doc.test.js | 9 +- .../expression/node/RangeNode.test.js | 2 +- 21 files changed, 180 insertions(+), 161 deletions(-) diff --git a/AUTHORS b/AUTHORS index 94bccd9c50..796e812026 100644 --- a/AUTHORS +++ b/AUTHORS @@ -221,6 +221,7 @@ Jaeu Jeong cyavictor88 <100557319+cyavictor88@users.noreply.github.com> David Contreras Jakub Riegel +Matthew Canestraro Angus Comrie TMTron diff --git a/src/expression/node/AccessorNode.js b/src/expression/node/AccessorNode.js index dd204bd253..b1a62f44c1 100644 --- a/src/expression/node/AccessorNode.js +++ b/src/expression/node/AccessorNode.js @@ -47,9 +47,10 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({ * @param {Node} object The object from which to retrieve * a property or subset. * @param {IndexNode} index IndexNode containing ranges + * @param {MetaOptions} object with additional options for building this node */ - constructor (object, index) { - super() + constructor (object, index, meta = {}) { + super(meta) if (!isNode(object)) { throw new TypeError('Node expected for parameter "object"') } @@ -133,11 +134,12 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({ /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {AccessorNode} */ - clone () { - const cloned = new AccessorNode(this.object, this.index) - cloned.sources = this.sources + clone (meta = {}) { + meta.sources = meta.sources || this.sources + const cloned = new AccessorNode(this.object, this.index, meta) return cloned } diff --git a/src/expression/node/ArrayNode.js b/src/expression/node/ArrayNode.js index 4535ad2e79..3220435fc4 100644 --- a/src/expression/node/ArrayNode.js +++ b/src/expression/node/ArrayNode.js @@ -14,9 +14,10 @@ export const createArrayNode = /* #__PURE__ */ factory(name, dependencies, ({ No * @extends {Node} * Holds an 1-dimensional array with items * @param {Node[]} [items] 1 dimensional array with items + * @param {MetaOptions} object with additional options for building this node */ - constructor (items) { - super() + constructor (items, meta = {}) { + super(meta) this.items = items || [] // validate input @@ -91,11 +92,12 @@ export const createArrayNode = /* #__PURE__ */ factory(name, dependencies, ({ No /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {ArrayNode} */ - clone () { - const cloned = new ArrayNode(this.items.slice(0)) - cloned.sources = this.sources + clone (meta = {}) { + meta.sources = meta.sources || this.sources + const cloned = new ArrayNode(this.items.slice(0), meta) return cloned } diff --git a/src/expression/node/AssignmentNode.js b/src/expression/node/AssignmentNode.js index 385b5e0ec4..2257bc7faf 100644 --- a/src/expression/node/AssignmentNode.js +++ b/src/expression/node/AssignmentNode.js @@ -65,9 +65,10 @@ export const createAssignmentNode = /* #__PURE__ */ factory(name, dependencies, * global scope. * @param {Node} value * The value to be assigned + * @param {MetaOptions} object with additional options for building this node */ - constructor (object, index, value) { - super() + constructor (object, index, value, meta = {}) { + super(meta) this.object = object this.index = value ? index : null this.value = value || index @@ -228,11 +229,12 @@ export const createAssignmentNode = /* #__PURE__ */ factory(name, dependencies, /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {AssignmentNode} */ - clone () { - const cloned = new AssignmentNode(this.object, this.index, this.value) - cloned.sources = this.sources + clone (meta = {}) { + meta.sources = meta.sources || this.sources + const cloned = new AssignmentNode(this.object, this.index, this.value, meta) return cloned } diff --git a/src/expression/node/BlockNode.js b/src/expression/node/BlockNode.js index fd165b56a7..dfc68748a2 100644 --- a/src/expression/node/BlockNode.js +++ b/src/expression/node/BlockNode.js @@ -19,9 +19,10 @@ export const createBlockNode = /* #__PURE__ */ factory(name, dependencies, ({ Re * Object with properties block, which is a Node, and visible, * which is a boolean. The property visible is optional and * is true by default + * @param {MetaOptions} object with additional options for building this node */ - constructor (blocks) { - super() + constructor (blocks, meta = {}) { + super(meta) // validate input, copy blocks if (!Array.isArray(blocks)) throw new Error('Array expected') this.blocks = blocks.map(function (block) { @@ -109,9 +110,11 @@ export const createBlockNode = /* #__PURE__ */ factory(name, dependencies, ({ Re /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {BlockNode} */ - clone () { + clone (meta = {}) { + meta.sources = meta.sources || this.sources const blocks = this.blocks.map(function (block) { return { node: block.node, @@ -119,8 +122,7 @@ export const createBlockNode = /* #__PURE__ */ factory(name, dependencies, ({ Re } }) - const cloned = new BlockNode(blocks) - cloned.sources = this.sources + const cloned = new BlockNode(blocks, meta) return cloned } diff --git a/src/expression/node/ConditionalNode.js b/src/expression/node/ConditionalNode.js index a7dc27ee6c..cdcd74d424 100644 --- a/src/expression/node/ConditionalNode.js +++ b/src/expression/node/ConditionalNode.js @@ -48,12 +48,13 @@ export const createConditionalNode = /* #__PURE__ */ factory(name, dependencies, * @param {Node} condition Condition, must result in a boolean * @param {Node} trueExpr Expression evaluated when condition is true * @param {Node} falseExpr Expression evaluated when condition is true + * @param {MetaOptions} object with additional options for building this node * * @constructor ConditionalNode * @extends {Node} */ - constructor (condition, trueExpr, falseExpr) { - super() + constructor (condition, trueExpr, falseExpr, meta = {}) { + super(meta) if (!isNode(condition)) { throw new TypeError('Parameter condition must be a Node') } if (!isNode(trueExpr)) { throw new TypeError('Parameter trueExpr must be a Node') } if (!isNode(falseExpr)) { throw new TypeError('Parameter falseExpr must be a Node') } @@ -118,11 +119,12 @@ export const createConditionalNode = /* #__PURE__ */ factory(name, dependencies, /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {ConditionalNode} */ - clone () { - const cloned = new ConditionalNode(this.condition, this.trueExpr, this.falseExpr) - cloned.sources = this.sources + clone (meta = {}) { + meta.sources = meta.sources || this.sources + const cloned = new ConditionalNode(this.condition, this.trueExpr, this.falseExpr, meta) return cloned } diff --git a/src/expression/node/ConstantNode.js b/src/expression/node/ConstantNode.js index 1ec8b8cf42..82f31b4b33 100644 --- a/src/expression/node/ConstantNode.js +++ b/src/expression/node/ConstantNode.js @@ -19,11 +19,12 @@ export const createConstantNode = /* #__PURE__ */ factory(name, dependencies, ({ * new ConstantNode('hello') * * @param {*} value Value can be any type (number, BigNumber, string, ...) + * @param {MetaOptions} object with additional options for building this node * @constructor ConstantNode * @extends {Node} */ - constructor (value) { - super() + constructor (value, meta = {}) { + super(meta) this.value = value } @@ -72,11 +73,12 @@ export const createConstantNode = /* #__PURE__ */ factory(name, dependencies, ({ /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {ConstantNode} */ - clone () { - const cloned = new ConstantNode(this.value) - cloned.sources = this.sources + clone (meta = {}) { + meta.sources = meta.sources || this.sources + const cloned = new ConstantNode(this.value, meta) return cloned } diff --git a/src/expression/node/FunctionAssignmentNode.js b/src/expression/node/FunctionAssignmentNode.js index ae9c9e90bd..9d12eb70b5 100644 --- a/src/expression/node/FunctionAssignmentNode.js +++ b/src/expression/node/FunctionAssignmentNode.js @@ -41,9 +41,10 @@ export const createFunctionAssignmentNode = /* #__PURE__ */ factory(name, depend * array with objects containing the name * and type of the parameter * @param {Node} expr The function expression + * @param {MetaOptions} object with additional options for building this node */ - constructor (name, params, expr) { - super() + constructor (name, params, expr, meta = {}) { + super(meta) // validate input if (typeof name !== 'string') { throw new TypeError('String expected for parameter "name"') } if (!Array.isArray(params)) { @@ -146,12 +147,13 @@ export const createFunctionAssignmentNode = /* #__PURE__ */ factory(name, depend /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {FunctionAssignmentNode} */ - clone () { + clone (meta = {}) { + meta.sources = meta.sources || this.sources const cloned = new FunctionAssignmentNode( - this.name, this.params.slice(0), this.expr) - cloned.sources = this.sources + this.name, this.params.slice(0), this.expr, meta) return cloned } diff --git a/src/expression/node/FunctionNode.js b/src/expression/node/FunctionNode.js index c350090bea..a07e931e7b 100644 --- a/src/expression/node/FunctionNode.js +++ b/src/expression/node/FunctionNode.js @@ -94,9 +94,10 @@ export const createFunctionNode = /* #__PURE__ */ factory(name, dependencies, ({ * Item resolving to a function on which to invoke * the arguments, typically a SymboNode or AccessorNode * @param {./Node[]} args + * @param {MetaOptions} object with additional options for building this node */ - constructor (fn, args) { - super() + constructor (fn, args = [], meta = {}) { + super(meta) if (typeof fn === 'string') { fn = new SymbolNode(fn) } @@ -307,11 +308,12 @@ export const createFunctionNode = /* #__PURE__ */ factory(name, dependencies, ({ /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {FunctionNode} */ - clone () { - const cloned = new FunctionNode(this.fn, this.args.slice(0)) - cloned.sources = this.sources + clone (meta = {}) { + meta.sources = meta.sources || this.sources + const cloned = new FunctionNode(this.fn, this.args.slice(0), meta) return cloned } diff --git a/src/expression/node/IndexNode.js b/src/expression/node/IndexNode.js index 9756d56cdc..7777c8e066 100644 --- a/src/expression/node/IndexNode.js +++ b/src/expression/node/IndexNode.js @@ -25,9 +25,10 @@ export const createIndexNode = /* #__PURE__ */ factory(name, dependencies, ({ No * Optional property describing whether this index was written using dot * notation like `a.b`, or using bracket notation like `a["b"]` * (which is the default). This property is used for string conversion. + * @param {MetaOptions} object with additional options for building this node */ - constructor (dimensions, dotNotation) { - super() + constructor (dimensions, dotNotation = false, meta = {}) { + super(meta) this.dimensions = dimensions this.dotNotation = dotNotation || false @@ -138,11 +139,11 @@ export const createIndexNode = /* #__PURE__ */ factory(name, dependencies, ({ No /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {IndexNode} */ - clone () { - const cloned = new IndexNode(this.dimensions.slice(0), this.dotNotation) - cloned.sources = this.sources + clone (meta = {}) { + const cloned = new IndexNode(this.dimensions.slice(0), this.dotNotation, meta) return cloned } diff --git a/src/expression/node/Node.js b/src/expression/node/Node.js index 5c318355c6..41b5d52e50 100644 --- a/src/expression/node/Node.js +++ b/src/expression/node/Node.js @@ -28,6 +28,17 @@ export const createNode = /* #__PURE__ */ factory(name, dependencies, ({ mathWit get type () { return 'Node' } get isNode () { return true } + /** + * @constructor Node + * A generic node, the parent of other AST nodes + * @param {MetaOptions} object with additional options for building this node + */ + constructor (meta = {}) { + if (meta.sources) { + this.sources = meta.sources + } + } + /** * Evaluate the node * @param {Object} [scope] Scope to read/write variables @@ -206,15 +217,6 @@ 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 09016eaa76..b6a202ad83 100644 --- a/src/expression/node/ObjectNode.js +++ b/src/expression/node/ObjectNode.js @@ -16,9 +16,10 @@ export const createObjectNode = /* #__PURE__ */ factory(name, dependencies, ({ N * @extends {Node} * Holds an object with keys/values * @param {Object.} [properties] object with key/value pairs + * @param {MetaOptions} object with additional options for building this node */ - constructor (properties) { - super() + constructor (properties, meta = {}) { + super(meta) this.properties = properties || {} // validate input @@ -112,17 +113,18 @@ export const createObjectNode = /* #__PURE__ */ factory(name, dependencies, ({ N /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {ObjectNode} */ - clone () { + clone (meta = {}) { + meta.sources = meta.sources || this.sources const properties = {} for (const key in this.properties) { if (hasOwnProperty(this.properties, key)) { properties[key] = this.properties[key] } } - const cloned = new ObjectNode(properties) - cloned.sources = this.sources + const cloned = new ObjectNode(properties, meta) return cloned } diff --git a/src/expression/node/OperatorNode.js b/src/expression/node/OperatorNode.js index 597130b73b..9f3af8ccfc 100644 --- a/src/expression/node/OperatorNode.js +++ b/src/expression/node/OperatorNode.js @@ -249,9 +249,10 @@ export const createOperatorNode = /* #__PURE__ */ factory(name, dependencies, ({ * @param {Node[]} args Operator arguments * @param {boolean} [implicit] Is this an implicit multiplication? * @param {boolean} [isPercentage] Is this an percentage Operation? + * @param {MetaOptions} object with additional options for building this node */ - constructor (op, fn, args, implicit, isPercentage) { - super() + constructor (op, fn, args, implicit = false, isPercentage = false, meta = {}) { + super(meta) // validate input if (typeof op !== 'string') { throw new TypeError('string expected for parameter "op"') @@ -353,12 +354,13 @@ export const createOperatorNode = /* #__PURE__ */ factory(name, dependencies, ({ /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {OperatorNode} */ - clone () { + clone (meta = {}) { + meta.sources = meta.sources || this.sources const cloned = new OperatorNode( - this.op, this.fn, this.args.slice(0), this.implicit, this.isPercentage) - cloned.sources = this.sources + this.op, this.fn, this.args.slice(0), this.implicit, this.isPercentage, meta) return cloned } diff --git a/src/expression/node/ParenthesisNode.js b/src/expression/node/ParenthesisNode.js index 3b99d0c751..fd2c7ce6be 100644 --- a/src/expression/node/ParenthesisNode.js +++ b/src/expression/node/ParenthesisNode.js @@ -13,10 +13,11 @@ export const createParenthesisNode = /* #__PURE__ */ factory(name, dependencies, * @extends {Node} * A parenthesis node describes manual parenthesis from the user input * @param {Node} content + * @param {MetaOptions} object with additional options for building this node * @extends {Node} */ - constructor (content) { - super() + constructor (content, meta = {}) { + super(meta) // validate input if (!isNode(content)) { throw new TypeError('Node expected for parameter "content"') @@ -76,11 +77,12 @@ export const createParenthesisNode = /* #__PURE__ */ factory(name, dependencies, /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {ParenthesisNode} */ - clone () { - const cloned = new ParenthesisNode(this.content) - cloned.sources = this.sources + clone (meta = {}) { + meta.sources = meta.sources || this.sources + const cloned = new ParenthesisNode(this.content, meta) return cloned } diff --git a/src/expression/node/RangeNode.js b/src/expression/node/RangeNode.js index 9fbccd7bc9..99e7c0ee4d 100644 --- a/src/expression/node/RangeNode.js +++ b/src/expression/node/RangeNode.js @@ -45,14 +45,15 @@ export const createRangeNode = /* #__PURE__ */ factory(name, dependencies, ({ No * @param {Node} start included lower-bound * @param {Node} end included upper-bound * @param {Node} [step] optional step + * @param {MetaOptions} object with additional options for building this node */ - constructor (start, end, step) { - super() + constructor (start, end, step = null, meta = {}) { + super(meta) // validate inputs if (!isNode(start)) throw new TypeError('Node expected') if (!isNode(end)) throw new TypeError('Node expected') if (step && !isNode(step)) throw new TypeError('Node expected') - if (arguments.length > 3) throw new Error('Too many arguments') + if (arguments.length > 4) throw new Error('Too many arguments') this.start = start // included lower-bound this.end = end // included upper-bound @@ -143,11 +144,12 @@ export const createRangeNode = /* #__PURE__ */ factory(name, dependencies, ({ No /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {RangeNode} */ - clone () { - const cloned = new RangeNode(this.start, this.end, this.step && this.step) - cloned.sources = this.sources + clone (meta = {}) { + meta.sources = meta.sources || this.sources + const cloned = new RangeNode(this.start, this.end, this.step && this.step, meta) return cloned } diff --git a/src/expression/node/RelationalNode.js b/src/expression/node/RelationalNode.js index be005bf23e..9d8fcc1c88 100644 --- a/src/expression/node/RelationalNode.js +++ b/src/expression/node/RelationalNode.js @@ -27,12 +27,13 @@ export const createRelationalNode = /* #__PURE__ */ factory(name, dependencies, * An array of conditional operators used to compare the parameters * @param {Node[]} params * The parameters that will be compared + * @param {MetaOptions} object with additional options for building this node * * @constructor RelationalNode * @extends {Node} */ - constructor (conditionals, params) { - super() + constructor (conditionals, params, meta = {}) { + super(meta) if (!Array.isArray(conditionals)) { throw new TypeError('Parameter conditionals must be an array') } if (!Array.isArray(params)) { throw new TypeError('Parameter params must be an array') } if (conditionals.length !== params.length - 1) { @@ -106,11 +107,12 @@ export const createRelationalNode = /* #__PURE__ */ factory(name, dependencies, /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {RelationalNode} */ - clone () { - const cloned = new RelationalNode(this.conditionals, this.params) - cloned.sources = this.sources + clone (meta = {}) { + meta.sources = meta.sources || this.sources + const cloned = new RelationalNode(this.conditionals, this.params, meta) return cloned } diff --git a/src/expression/node/SymbolNode.js b/src/expression/node/SymbolNode.js index dd9a949c28..ecb9852da7 100644 --- a/src/expression/node/SymbolNode.js +++ b/src/expression/node/SymbolNode.js @@ -26,10 +26,11 @@ export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ m * @extends {Node} * A symbol node can hold and resolve a symbol * @param {string} name + * @param {MetaOptions} object with additional options for building this node * @extends {Node} */ - constructor (name) { - super() + constructor (name, meta = {}) { + super(meta) // validate input if (typeof name !== 'string') { throw new TypeError('String expected for parameter "name"') @@ -111,11 +112,12 @@ export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ m /** * Create a clone of this node, a shallow copy + * @param {MetaOptions} object with additional options for cloning this node * @return {SymbolNode} */ - clone () { - const cloned = new SymbolNode(this.name) - cloned.sources = this.sources + clone (meta = {}) { + meta.sources = meta.sources || this.sources + const cloned = new SymbolNode(this.name, meta) return cloned } diff --git a/src/expression/parse.js b/src/expression/parse.js index b777760e30..4e0945da22 100644 --- a/src/expression/parse.js +++ b/src/expression/parse.js @@ -198,19 +198,6 @@ 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 @@ -664,10 +651,10 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ } if (blocks.length > 0) { - return withSources(new BlockNode(blocks), sources) + return new BlockNode(blocks, { sources }) } else { if (!node) { - node = withSources(new ConstantNode(undefined), [{ index: 0, text: '' }]) + node = new ConstantNode(undefined, { sources: [{ index: 0, text: '' }] }) if (state.comment) { node.comment = state.comment } @@ -696,15 +683,15 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (isSymbolNode(node)) { // parse a variable assignment like 'a = 2/3' name = node.name - const symbolNode = withSources(new SymbolNode(name), node.sources) + const symbolNode = new SymbolNode(name, { sources: node.sources }) getTokenSkipNewline(state) value = parseAssignment(state) - return withSources(new AssignmentNode(symbolNode, value), [source]) + return new AssignmentNode(symbolNode, value, null, { sources: [source] }) } else if (isAccessorNode(node)) { // parse a matrix subset assignment like 'A[1,2] = 4' getTokenSkipNewline(state) value = parseAssignment(state) - return withSources(new AssignmentNode(node.object, node.index, value), [source]) + return new AssignmentNode(node.object, node.index, value, { sources: [source] }) } else if (isFunctionNode(node) && isSymbolNode(node.fn)) { // parse function assignment like 'f(x) = x^2' valid = true @@ -722,7 +709,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (valid) { getTokenSkipNewline(state) value = parseAssignment(state) - return withSources(new FunctionAssignmentNode(name, args, value), [source]) + return new FunctionAssignmentNode(name, args, value, { sources: [source] }) } } @@ -767,7 +754,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ const falseExpr = parseAssignment(state) // Note: check for conditional operator again, right associativity - node = withSources(new ConditionalNode(condition, trueExpr, falseExpr), [condSource, colonSource]) + node = new ConditionalNode(condition, trueExpr, falseExpr, { sources: [condSource, colonSource] }) // restore the previous conditional level state.conditionalLevel = prev @@ -787,7 +774,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ while (state.token === 'or') { // eslint-disable-line no-unmodified-loop-condition const source = tokenSource(state) getTokenSkipNewline(state) - node = withSources(new OperatorNode('or', 'or', [node, parseLogicalXor(state)]), [source]) + node = new OperatorNode('or', 'or', [node, parseLogicalXor(state)], false, false, { sources: [source] }) } return node @@ -804,7 +791,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ while (state.token === 'xor') { // eslint-disable-line no-unmodified-loop-condition const source = tokenSource(state) getTokenSkipNewline(state) - node = withSources(new OperatorNode('xor', 'xor', [node, parseLogicalAnd(state)]), [source]) + node = new OperatorNode('xor', 'xor', [node, parseLogicalAnd(state)], false, false, { sources: [source] }) } return node @@ -821,7 +808,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ while (state.token === 'and') { // eslint-disable-line no-unmodified-loop-condition const source = tokenSource(state) getTokenSkipNewline(state) - node = withSources(new OperatorNode('and', 'and', [node, parseBitwiseOr(state)]), [source]) + node = new OperatorNode('and', 'and', [node, parseBitwiseOr(state)], false, false, { sources: [source] }) } return node @@ -838,7 +825,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ while (state.token === '|') { // eslint-disable-line no-unmodified-loop-condition const source = tokenSource(state) getTokenSkipNewline(state) - node = withSources(new OperatorNode('|', 'bitOr', [node, parseBitwiseXor(state)]), [source]) + node = new OperatorNode('|', 'bitOr', [node, parseBitwiseXor(state)], false, false, { sources: [source] }) } return node @@ -855,7 +842,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ while (state.token === '^|') { // eslint-disable-line no-unmodified-loop-condition const source = tokenSource(state) getTokenSkipNewline(state) - node = withSources(new OperatorNode('^|', 'bitXor', [node, parseBitwiseAnd(state)]), [source]) + node = new OperatorNode('^|', 'bitXor', [node, parseBitwiseAnd(state)], false, false, { sources: [source] }) } return node @@ -872,7 +859,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ while (state.token === '&') { // eslint-disable-line no-unmodified-loop-condition const source = tokenSource(state) getTokenSkipNewline(state) - node = withSources(new OperatorNode('&', 'bitAnd', [node, parseRelational(state)]), [source]) + node = new OperatorNode('&', 'bitAnd', [node, parseRelational(state)], false, false, { sources: [source] }) } return node @@ -907,9 +894,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (params.length === 1) { return params[0] } else if (params.length === 2) { - return withSources(new OperatorNode(conditionals[0].name, conditionals[0].fn, params), sources) + return new OperatorNode(conditionals[0].name, conditionals[0].fn, params, false, false, { sources }) } else { - return withSources(new RelationalNode(conditionals.map(c => c.fn), params), sources) + return new RelationalNode(conditionals.map(c => c.fn), params, { sources }) } } @@ -937,7 +924,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ getTokenSkipNewline(state) params = [node, parseConversion(state)] - node = withSources(new OperatorNode(name, fn, params), [source]) + node = new OperatorNode(name, fn, params, false, false, { sources: [source] }) } return node @@ -968,11 +955,11 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (name === 'in' && state.token === '') { // end of expression -> this is the unit 'in' ('inch') // no source mapping because this * operator is not explicitly in the source expression - node = new OperatorNode('*', 'multiply', [node, withSources(new SymbolNode('in'), [source])], true) + node = new OperatorNode('*', 'multiply', [node, new SymbolNode('in', { sources: [source] })], true) } else { // operator 'a to b' or 'a in b' params = [node, parseRange(state)] - node = withSources(new OperatorNode(name, fn, params), [source]) + node = new OperatorNode(name, fn, params, false, false, { sources: [source] }) } } @@ -992,7 +979,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ // implicit start=1 (one-based) const implicitSource = tokenSource(state) implicitSource.text = '' - node = withSources(new ConstantNode(1), [implicitSource]) + node = new ConstantNode(1, { sources: [implicitSource] }) } else { // explicit start node = parseAddSubtract(state) @@ -1012,7 +999,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ // implicit end const implicitSource = tokenSource(state) implicitSource.text = '' - params.push(withSources(new SymbolNode('end'), [implicitSource])) + params.push(new SymbolNode('end', { sources: [implicitSource] })) } else { // explicit end params.push(parseAddSubtract(state)) @@ -1021,10 +1008,10 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (params.length === 3) { // params = [start, step, end] - node = withSources(new RangeNode(params[0], params[2], params[1]), sources) // start, end, step + node = new RangeNode(params[0], params[2], params[1], { sources }) // start, end, step } else { // length === 2 // params = [start, end] - node = withSources(new RangeNode(params[0], params[1]), sources) // start, end + node = new RangeNode(params[0], params[1], null, { sources }) // start, end } } @@ -1058,7 +1045,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ } else { params = [node, rightNode] } - node = withSources(new OperatorNode(name, fn, params), [source]) + node = new OperatorNode(name, fn, params, false, false, { sources: [source] }) } return node @@ -1092,7 +1079,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ getTokenSkipNewline(state) last = parseImplicitMultiplication(state) - node = withSources(new OperatorNode(name, fn, [node, last]), [source]) + node = new OperatorNode(name, fn, [node, last], false, false, { sources: [source] }) } else { break } @@ -1132,7 +1119,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ source.text = '' } last = parseRule2(state) - node = withSources(new OperatorNode('*', 'multiply', [node, last], true /* implicit */), [source]) + node = new OperatorNode('*', 'multiply', [node, last], true /* implicit */, false, { sources: [source] }) } else { break } @@ -1176,7 +1163,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ Object.assign(state, tokenStates.pop()) tokenStates.pop() last = parsePercentage(state) - node = withSources(new OperatorNode('/', 'divide', [node, last]), [source]) + node = new OperatorNode('/', 'divide', [node, last], false, false, { sources: [source] }) } else { // Not a match, so rewind tokenStates.pop() @@ -1220,11 +1207,11 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (name === '%' && state.tokenType === TOKENTYPE.DELIMITER && state.token !== '(') { // If the expression contains only %, then treat that as /100 // 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 }]) + const constant = new ConstantNode(100, { sources: [source] }) + node = new OperatorNode('/', 'divide', [node, constant], false, true, { sources: [{ ...source }] }) } else { params = [node, parseUnary(state)] - node = withSources(new OperatorNode(name, fn, params), [source]) + node = new OperatorNode(name, fn, params, false, false, { sources: [source] }) } } @@ -1253,7 +1240,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ getTokenSkipNewline(state) params = [parseUnary(state)] - return withSources(new OperatorNode(name, fn, params), [source]) + return new OperatorNode(name, fn, params, false, false, { sources: [source] }) } return parsePow(state) @@ -1277,7 +1264,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ getTokenSkipNewline(state) params = [node, parseUnary(state)] // Go back to unary, we can have '2^-3' - node = withSources(new OperatorNode(name, fn, params), [source]) + node = new OperatorNode(name, fn, params, false, false, { sources: [source] }) } return node @@ -1306,7 +1293,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ getToken(state) params = [node] - node = withSources(new OperatorNode(name, fn, params), [source]) + node = new OperatorNode(name, fn, params, false, false, { sources: [source] }) node = parseAccessors(state, node) } @@ -1380,7 +1367,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ // create a new custom node // noinspection JSValidateTypes - return withSources(new CustomNode(params), sources) + return new CustomNode(params, { sources }) } return parseSymbol(state) @@ -1403,11 +1390,11 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ getToken(state) if (hasOwnProperty(CONSTANTS, name)) { // true, false, null, ... - node = withSources(new ConstantNode(CONSTANTS[name]), [source]) + node = new ConstantNode(CONSTANTS[name], { sources: [source] }) } else if (NUMERIC_CONSTANTS.indexOf(name) !== -1) { // NaN, Infinity - node = withSources(new ConstantNode(numeric(name, 'number')), [source]) + node = new ConstantNode(numeric(name, 'number'), { sources: [source] }) } else { - node = withSources(new SymbolNode(name), [source]) + node = new SymbolNode(name, { sources: [source] }) } // parse function parameters and matrix index @@ -1464,7 +1451,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ closeParams(state) getToken(state) - node = withSources(new FunctionNode(node, params), sources) + node = 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 @@ -1495,8 +1482,8 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ closeParams(state) getToken(state) - const indexNode = withSources(new IndexNode(params), indexSources) - node = withSources(new AccessorNode(node, indexNode), sources) + const indexNode = new IndexNode(params, false, { sources: indexSources }) + node = new AccessorNode(node, indexNode, { sources }) } else { // dot notation like variable.prop getToken(state) @@ -1505,12 +1492,12 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ throw createSyntaxError(state, 'Property name expected after dot') } const constantSource = tokenSource(state) - params.push(withSources(new ConstantNode(state.token), [constantSource])) + params.push(new ConstantNode(state.token, { sources: [constantSource] })) getToken(state) const dotNotation = true - const indexNode = withSources(new IndexNode(params, dotNotation), [{ ...constantSource }]) - node = withSources(new AccessorNode(node, indexNode, sources)) + const indexNode = new IndexNode(params, dotNotation, { sources: [{ ...constantSource }] }) + node = new AccessorNode(node, indexNode, { sources }) } } @@ -1534,7 +1521,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ getToken(state) // create constant - node = withSources(new ConstantNode(str), sources) + node = new ConstantNode(str, { sources }) // parse index parameters node = parseAccessors(state, node) @@ -1589,7 +1576,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ getToken(state) // create constant - node = withSources(new ConstantNode(str), sources) + node = new ConstantNode(str, { sources }) // parse index parameters node = parseAccessors(state, node) @@ -1676,7 +1663,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ } } - array = withSources(new ArrayNode(params), sources) + array = new ArrayNode(params, { sources }) } else { // 1 dimensional vector if (state.token !== ']') { @@ -1695,7 +1682,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ sources.push(tokenSource(state)) closeParams(state) getToken(state) - array = withSources(new ArrayNode([]), sources) + array = new ArrayNode([], { sources }) } return parseAccessors(state, array) @@ -1722,7 +1709,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ len++ } - return withSources(new ArrayNode(params), sources) + return new ArrayNode(params, { sources }) } /** @@ -1781,7 +1768,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ closeParams(state) getToken(state) - let node = withSources(new ObjectNode(properties), sources) + let node = new ObjectNode(properties, { sources }) // parse index parameters node = parseAccessors(state, node) @@ -1806,7 +1793,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ const source = tokenSource(state) getToken(state) - return withSources(new ConstantNode(numeric(numberStr, config.number)), [source]) + return new ConstantNode(numeric(numberStr, config.number), { sources: [source] }) } return parseParentheses(state) @@ -1838,7 +1825,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ closeParams(state) getToken(state) - node = withSources(new ParenthesisNode(node), sources) + node = 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 610291ef76..c826f67920 100644 --- a/src/function/algebra/resolve.js +++ b/src/function/algebra/resolve.js @@ -65,12 +65,10 @@ export const createResolve = /* #__PURE__ */ factory(name, dependencies, ({ nextWithin.add(node.name) return _resolve(value, scope, nextWithin) } else if (typeof value === 'number') { - const parsed = parse(String(value)) - parsed.sources = [] + const parsed = parse(String(value)).clone({ sources: [] }) return parsed } else if (value !== undefined) { - const parsed = new ConstantNode(value) - parsed.sources = [] + const parsed = new ConstantNode(value).clone({ sources: [] }) return parsed } else { return node @@ -79,19 +77,16 @@ export const createResolve = /* #__PURE__ */ factory(name, dependencies, ({ const args = node.args.map(function (arg) { return _resolve(arg, scope, within) }) - const newNode = new OperatorNode(node.op, node.fn, args, node.implicit) - newNode.sources = node.sources + const newNode = new OperatorNode(node.op, node.fn, args, node.implicit).clone({ sources: node.sources }) return newNode } else if (isParenthesisNode(node)) { - const parenNode = new ParenthesisNode(_resolve(node.content, scope, within)) - parenNode.sources = [] + const parenNode = new ParenthesisNode(_resolve(node.content, scope, within)).clone({ sources: [] }) return parenNode } else if (isFunctionNode(node)) { const args = node.args.map(function (arg) { return _resolve(arg, scope, within) }) - const fnNode = new FunctionNode(node.name, args) - fnNode.sources = [] + const fnNode = new FunctionNode(node.name, args).clone({ sources: [] }) return fnNode } diff --git a/test/node-tests/doc.test.js b/test/node-tests/doc.test.js index 4bae84eac8..aad5f40106 100644 --- a/test/node-tests/doc.test.js +++ b/test/node-tests/doc.test.js @@ -132,9 +132,14 @@ function checkExpectation (want, got) { if (typeof want === 'number' && typeof got === 'number' && want !== got) { console.log(` Note: return value ${got} not exactly as expected: ${want}`) return approx.equal(got, want, 1e-9) - } else { - assert.deepEqual(got, want) } + if (want instanceof math.Node && got instanceof math.Node) { + got.traverse((node) => { node.sources = [] }) + want.traverse((node) => { node.sources = [] }) + + return assert.deepEqual(got, want) + } + assert.deepEqual(got, want) } const OKundocumented = new Set([ diff --git a/test/unit-tests/expression/node/RangeNode.test.js b/test/unit-tests/expression/node/RangeNode.test.js index 00e07f20ec..c193c1e13c 100644 --- a/test/unit-tests/expression/node/RangeNode.test.js +++ b/test/unit-tests/expression/node/RangeNode.test.js @@ -39,7 +39,7 @@ describe('RangeNode', function () { assert.throws(function () { console.log(new RangeNode()) }, TypeError) assert.throws(function () { console.log(new RangeNode(start)) }, TypeError) assert.throws(function () { console.log(new RangeNode([])) }, TypeError) - assert.throws(function () { console.log(new RangeNode(start, end, start, end)) }, Error) + assert.throws(function () { console.log(new RangeNode(start, end, start, end, end)) }, Error) assert.throws(function () { console.log(new RangeNode(0, 10)) }, TypeError) }) From e3706b85f031547535f8d5b42c62d5f1dc137d7f Mon Sep 17 00:00:00 2001 From: Matthew Canestraro Date: Tue, 4 Apr 2023 15:37:25 -0400 Subject: [PATCH 3/5] Add type for MetaOptions --- types/index.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/types/index.d.ts b/types/index.d.ts index 679e204058..c20bfb7e5e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -33,6 +33,11 @@ declare namespace math { index: number text: string } + + // Additional options when building or cloning a node + interface MetaOptions { + sources: SourceMapping[] + } /** Available options for parse */ interface ParseOptions { From fea1cbe62bcab605e4d51368ea88e59121ea0534 Mon Sep 17 00:00:00 2001 From: Matthew Canestraro Date: Tue, 4 Apr 2023 16:15:55 -0400 Subject: [PATCH 4/5] Clean lint --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index c20bfb7e5e..7be93c9f6c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -33,7 +33,7 @@ declare namespace math { index: number text: string } - + // Additional options when building or cloning a node interface MetaOptions { sources: SourceMapping[] From c169f7d9e84fcda3873e783b62c3671afe727b12 Mon Sep 17 00:00:00 2001 From: Matthew Canestraro Date: Wed, 5 Apr 2023 12:42:43 -0400 Subject: [PATCH 5/5] Update docs --- docs/expressions/expression_trees.md | 83 +++++++++++++++++++++------- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/docs/expressions/expression_trees.md b/docs/expressions/expression_trees.md index 9dfc24a125..c7dc54f4e4 100644 --- a/docs/expressions/expression_trees.md +++ b/docs/expressions/expression_trees.md @@ -39,10 +39,11 @@ tree generated by `math.parse('sqrt(2 + x)')`. All nodes have the following methods: -- `clone() : Node` +- `clone(options: MetaOptions) : Node` Create a shallow clone of the node. - The node itself is cloned, its childs are not cloned. + The node itself is cloned, its childs are not cloned. + Information on available options can be found at [Type Definitions](expression_trees.md#type-definitions) - `cloneDeep() : Node` @@ -263,7 +264,10 @@ Each `Node` has the following properties: - `type: string` The type of the node, for example `'SymbolNode'` in case of a `SymbolNode`. - + +- `sources: SourceMapping[]` + An array of sources mapping this node back to its tokens in the parsed string. + The exact mapping will depend on the type of node and is listed in more detail for each node below. ## Nodes @@ -276,7 +280,7 @@ namespace `math`. Construction: ``` -new AccessorNode(object: Node, index: IndexNode) +new AccessorNode(object: Node, index: IndexNode, meta: MetaOptions) ``` Properties: @@ -284,6 +288,7 @@ Properties: - `object: Node` - `index: IndexNode` - `name: string` (read-only) The function or method name. Returns an empty string when undefined. +- `sources: SourceMapping[]` mappings to tokens defining this accessor. This will be `[` and `]` for array accessors, and `.` for dot notation accessors. Examples: @@ -301,12 +306,13 @@ const node2 = new math.AccessorNode(object, index) Construction: ``` -new ArrayNode(items: Node[]) +new ArrayNode(items: Node[], meta: MetaOptions) ``` Properties: - `items: Node[]` +- `sources: SourceMapping[]` mappings to the `[`, `]`, `,`, and `;` used to define this array in the parsed string Examples: @@ -325,8 +331,8 @@ const node2 = new math.ArrayNode([one, two, three]) Construction: ``` -new AssignmentNode(object: SymbolNode, value: Node) -new AssignmentNode(object: SymbolNode | AccessorNode, index: IndexNode, value: Node) +new AssignmentNode(object: SymbolNode, value: Node, meta: MetaOptions) +new AssignmentNode(object: SymbolNode | AccessorNode, index: IndexNode, value: Node, meta: MetaOptions) ``` Properties: @@ -335,6 +341,7 @@ Properties: - `index: IndexNode | null` - `value: Node` - `name: string` (read-only) The function or method name. Returns an empty string when undefined. +- `sources: SourceMapping[]` mapping to the `=` defining this assignment node in the parsed string Examples: @@ -358,12 +365,13 @@ a semicolon). Construction: ``` -block = new BlockNode(Array.<{node: Node} | {node: Node, visible: boolean}>) +block = new BlockNode(Array.<{node: Node} | {node: Node, visible: boolean}>, meta: MetaOptions) ``` Properties: - `blocks: Array.<{node: Node, visible: boolean}>` +- `sources: SourceMapping[]` mappings to each `;` token delimiting blocks in the parsed string Examples: @@ -395,7 +403,7 @@ const block2 = new BlockNode([ Construction: ``` -new ConditionalNode(condition: Node, trueExpr: Node, falseExpr: Node) +new ConditionalNode(condition: Node, trueExpr: Node, falseExpr: Node, meta: MetaOptions) ``` Properties: @@ -403,6 +411,7 @@ Properties: - `condition: Node` - `trueExpr: Node` - `falseExpr: Node` +- `sources: SourceMapping[]` mappings to the `?`, and `:` tokens defining this conditional in the parsed string Examples: @@ -422,12 +431,13 @@ const node2 = new math.ConditionalNode(condition, trueExpr, falseExpr) Construction: ``` -new ConstantNode(value: *) +new ConstantNode(value: *, meta: MetaOptions) ``` Properties: - `value: *` +- `sources: SourceMapping[]` mapping to the token representing the constant in the parsed string. Examples: @@ -444,7 +454,7 @@ const node3 = new math.ConstantNode('foo') Construction: ``` -new FunctionAssignmentNode(name: string, params: string[], expr: Node) +new FunctionAssignmentNode(name: string, params: string[], expr: Node, meta: MetaOptions) ``` Properties: @@ -452,6 +462,7 @@ Properties: - `name: string` - `params: string[]` - `expr: Node` +- `sources: SourceMapping[]` mapping to the `=` for this assignment in the parsed string Examples: @@ -470,13 +481,14 @@ const node2 = new math.FunctionAssignmentNode('f', ['x'], expr) Construction: ``` -new FunctionNode(fn: Node | string, args: Node[]) +new FunctionNode(fn: Node | string, args: Node[], meta: MetaOptions) ``` Properties: - `fn: Node | string` (read-only) The object or function name which to invoke. - `args: Node[]` +- `sources: SourceMapping[]` mappings to the `(` and `)` defining this function, as well as any `,` delimiting its parameters. Static functions: @@ -498,8 +510,8 @@ const node3 = new math.FunctionNode(new SymbolNode('sqrt'), [four]) Construction: ``` -new IndexNode(dimensions: Node[]) -new IndexNode(dimensions: Node[], dotNotation: boolean) +new IndexNode(dimensions: Node[], meta: MetaOptions) +new IndexNode(dimensions: Node[], dotNotation: boolean, meta: MetaOptions) ``` Each dimension can be a single value, a range, or a property. The values of @@ -514,6 +526,7 @@ Properties: - `dimensions: Node[]` - `dotNotation: boolean` +- `sources: SourceMapping[]` mappings to `,` delimiting items in an array index. If `dotNotation = true`, this will map to the constant following the `.` instead. Examples: @@ -535,12 +548,13 @@ const node2 = new math.AccessNode(A, index) Construction: ``` -new ObjectNode(properties: Object.) +new ObjectNode(properties: Object., meta: MetaOptions) ``` Properties: - `properties: Object.` +- `sources: SourceMapping[]` mappings to the `{`, `}`, `:`, and `,` tokens defining this object in the parsed string Examples: @@ -559,7 +573,7 @@ const node2 = new math.ObjectNode({a: a, b: b, c: c}) Construction: ``` -new OperatorNode(op: string, fn: string, args: Node[], implicit: boolean = false) +new OperatorNode(op: string, fn: string, args: Node[], implicit: boolean = false, meta: MetaOptions) ``` Additional methods: @@ -593,6 +607,7 @@ Properties: - `fn: string` - `args: Node[]` - `implicit: boolean` True in case of an implicit multiplication, false otherwise +- `sources: SourceMapping[]` mapping to the `+` or `-` defining this unary operator in the parsed string Examples: @@ -609,12 +624,13 @@ const node2 = new math.OperatorNode('+', 'add', [a, b]) Construction: ``` -new ParenthesisNode(content: Node) +new ParenthesisNode(content: Node, meta: MetaOptions) ``` Properties: - `content: Node` +- `sources: SourceMapping[]` mappings to the `(` and `)` for this node in the parsed string Examples: @@ -630,7 +646,7 @@ const node2 = new math.ParenthesisNode(a) Construction: ``` -new RangeNode(start: Node, end: Node [, step: Node]) +new RangeNode(start: Node, end: Node [, step: Node], meta: MetaOptions) ``` Properties: @@ -638,7 +654,8 @@ Properties: - `start: Node` - `end: Node` - `step: Node | null` - +- `sources: SourceMapping[]` mappings to the `:` defining this range node in the parsed string. There will be 1 or 2 mappings, depending on whether step size was defined for the range + Examples: ```js @@ -659,7 +676,7 @@ const node4 = new math.RangeNode(zero, ten, two) Construction: ``` -new RelationalNode(conditionals: string[], params: Node[]) +new RelationalNode(conditionals: string[], params: Node[], meta: MetaOptions) ``` `conditionals` is an array of strings, each of which may be 'smaller', 'larger', 'smallerEq', 'largerEq', 'equal', or 'unequal'. The `conditionals` array must contain exactly one fewer item than `params`. @@ -668,6 +685,7 @@ Properties: - `conditionals: string[]` - `params: Node[]` +- `sources: SourceMapping[]` mappings to the relational symbol `<`, `>`, `==`, `>=`, or `<=` defining this node in the parsed string. This may include multiple mappings if multiple relationals are chained: `10 < x < 20` A `RelationalNode` efficiently represents a chained conditional expression with two or more comparison operators, such as `10 < x <= 50`. The expression is equivalent to `10 < x and x <= 50`, except that `x` is evaluated only once, and evaluation stops (is "short-circuited") once any condition tests false. Operators that are subject to chaining are `<`, `>`, `<=`, `>=`, `==`, and `!=`. For backward compatibility, `math.parse` will return an `OperatorNode` if only a single conditional is present (such as `x > 2`). @@ -689,12 +707,13 @@ const node2 = math.parse('10 < x <= 50') Construction: ``` -new SymbolNode(name: string) +new SymbolNode(name: string, meta: MetaOptions) ``` Properties: - `name: string` +- `sources: SourceMapping[]` a single mapping to the symbol defining this node in the parsed string. The text will match whatever symbol is defined. Static functions: @@ -708,3 +727,25 @@ const node = math.parse('x') const x = new math.SymbolNode('x') ``` + +## Type Definitions + +A few node methods and properties have complex object structures as their parameters or return types. They are: + +### MetaOptions + +This object is passed as a final parameter in the constructor of any node, or as the parameter when calling `clone()`. + +Properties: + +- `sources: SourceMapping` sets the sources for the newly created or cloned node + +### SourceMapping + +Each node has an array of `SourceMapping` objects which map back to the node's corresponding tokens in the original source string + +Properties: + +- `text: string` the token representing this node in the parsed string +- `index; number` the index of the token in the parsed string +