UNPKG

@teachinglab/omd

Version:

omd

293 lines (247 loc) 10.8 kB
import { omdGroupNode } from "./omdGroupNode.js"; import { omdNode } from "./omdNode.js"; import { getNodeForAST } from "../core/omdUtilities.js"; /** * Represents a parenthesized expression node in the mathematical expression tree * Handles rendering of parentheses and their contained expression * @extends omdNode */ export class omdParenthesisNode extends omdNode { /** * Creates a parenthesis node from AST data * @param {Object} astNodeData - The AST node containing parenthesis information */ constructor(astNodeData) { super(astNodeData); this.type = "omdParenthesisNode"; const innerContent = astNodeData.content || (astNodeData.args && astNodeData.args[0]); if (!innerContent) { console.error("omdParenthesisNode requires inner content", astNodeData); return; } this.open = this.createParenthesis("("); this.expression = this.createExpression(innerContent); this.closed = this.createParenthesis(")"); // Populate the argumentNodeList for the mathematical child node this.argumentNodeList.expression = this.expression; } createParenthesis(parenthesis) { let child = new omdGroupNode(parenthesis); this.addChild(child); return child; } createExpression(ast) { let ExpressionType = getNodeForAST(ast); let child = new ExpressionType(ast); this.addChild(child); return child; } /** * Calculates the dimensions of the parenthesis node and its children * @override */ computeDimensions() { this.open.computeDimensions(); this.expression.computeDimensions(); this.closed.computeDimensions(); // Gerard: Calculate dimensions and padding let padding = 4 * this.getFontSize() / this.getRootFontSize(); // Gerard: Padding should scale with font size let sumWidth = this.open.width + this.expression.width + this.closed.width; let maxHeight = Math.max(this.expression.height, this.closed.height, this.open.height); this.setWidthAndHeight(sumWidth, maxHeight + padding); } /** * Updates the layout of the parenthesis node and its children * @override */ updateLayout() { let xCurrent = 0; // For proper mathematical typesetting, parentheses should be centered // relative to the baseline of the mathematical content, properly accounting // for power nodes and their baseline positioning // First, position the expression based on its alignment baseline this.expression.updateLayout(); const expressionBaseline = this.expression.getAlignmentBaseline(); const totalBaseline = this.getAlignmentBaseline(); const expressionY = totalBaseline - expressionBaseline; this.expression.setPosition(this.open.width, expressionY); // Position parentheses centered around the mathematical baseline // This ensures proper centering for power nodes where the baseline // represents the position of the base (e.g., 'x' in 'x²') const mathematicalBaseline = expressionY + expressionBaseline; const parenY = mathematicalBaseline - (this.open.height / 2); this.open.updateLayout(); this.open.setPosition(0, parenY); this.closed.updateLayout(); this.closed.setPosition(this.open.width + this.expression.width, parenY); } /** * For parenthesis nodes, the alignment baseline is the baseline of the inner expression. * @override * @returns {number} The y-coordinate for alignment. */ getAlignmentBaseline() { // The baseline is the y-position of the expression node plus its own baseline. const childY = (this.height - this.expression.height) / 2; return childY + this.expression.getAlignmentBaseline(); } clone() { // Create a new node. The constructor will add a backRect and temporary children. const tempAst = { type: 'ParenthesisNode', content: {type: 'ConstantNode', value: 1} }; const clone = new omdParenthesisNode(tempAst); // Keep the backRect, but get rid of the temporary children. const backRect = clone.backRect; clone.removeAllChildren(); clone.addChild(backRect); // Manually clone the real children to ensure the entire tree has correct provenance. clone.open = this.open.clone(); clone.addChild(clone.open); clone.expression = this.expression.clone(); clone.addChild(clone.expression); clone.closed = this.closed.clone(); clone.addChild(clone.closed); // Rebuild the argument list clone.argumentNodeList = { expression: clone.expression }; // Regenerate AST data from the cloned structure instead of copying it // This ensures the AST properly reflects the cloned nodes clone.astNodeData = clone.toMathJSNode(); // The crucial step: link the clone to its origin. clone.provenance.push(this.id); return clone; } /** * A parenthesis node is constant if its inner expression is constant. * @returns {boolean} */ isConstant() { return this.expression ? this.expression.isConstant() : false; } /** * The value of a parenthesis node is the value of its inner expression. * @returns {number} */ getValue() { if (!this.expression) { throw new Error("Parenthesis node has no expression from which to get a value."); } return this.expression.getValue(); } /** * Converts the omdParenthesisNode to a math.js AST node. * @returns {Object} A math.js-compatible AST node. */ toMathJSNode() { const astNode = { type: 'ParenthesisNode', content: this.expression.toMathJSNode(), id: this.id, provenance: this.provenance }; // Add a clone method to maintain compatibility with math.js's expectations. astNode.clone = function() { const clonedNode = { ...this }; if (this.content) { clonedNode.content = this.content.clone(); } return clonedNode; }; return astNode; } /** * Converts the parenthesis node to a string. * @returns {string} The string representation of the parenthesized expression. */ toString() { const innerExpr = this.expression.toString(); return `(${innerExpr})`; } /** * Evaluate the content within parentheses * @param {Object} variables - Variable name to value mapping * @returns {number} The evaluated result */ evaluate(variables = {}) { if (!this.expression) { throw new Error("Parenthesis node has no expression to evaluate"); } // Evaluate the inner expression if (this.expression.evaluate) { return this.expression.evaluate(variables); } else if (this.expression.isConstant()) { return this.expression.getValue(); } else { throw new Error("Cannot evaluate parenthesis content"); } } /** * Check if parentheses are necessary based on context * @returns {boolean} Whether parentheses affect evaluation order */ isNecessary() { // If there's no parent, parentheses at the top level are not necessary if (!this.parent) { return false; } // Check for single constants or variables - parentheses not needed if (this.expression.type === 'omdConstantNode' || this.expression.type === 'omdVariableNode') { return false; } // Check parent context const parent = this.parent; // If parent is a function, parentheses are part of function syntax if (parent.type === 'omdFunctionNode') { return true; } // If parent is a power node and this is the base or exponent if (parent.type === 'omdPowerNode') { // Check if we're the base if (parent.base === this) { // Base needs parentheses if it's a complex expression return this.expression.type === 'omdBinaryExpressionNode'; } // Check if we're the exponent if (parent.exponent === this) { // Exponent needs parentheses if it's not a simple value return !(this.expression.type === 'omdConstantNode' || this.expression.type === 'omdVariableNode'); } } // If parent is a binary expression, check operator precedence if (parent.type === 'omdBinaryExpressionNode') { // If the inner expression is also a binary expression, // parentheses might be needed based on precedence if (this.expression.type === 'omdBinaryExpressionNode') { return true; // Conservative approach - keep parentheses } } // Default: parentheses are not necessary return false; } /** * Create a parenthesis node from a string * @param {string} expressionString - Expression with parentheses * @returns {omdParenthesisNode} The created parenthesis node * @static */ static fromString(expressionString) { if (!window.math) { throw new Error("Math.js is required for parsing expressions"); } const trimmed = expressionString.trim(); // Ensure the string starts and ends with parentheses if (!trimmed.startsWith('(') || !trimmed.endsWith(')')) { throw new Error("Expression must be enclosed in parentheses"); } try { const ast = window.math.parse(trimmed); // Verify it's a parenthesis node if (ast.type !== 'ParenthesisNode') { throw new Error("Parsed expression is not a parenthesis node"); } return new omdParenthesisNode(ast); } catch (error) { throw new Error(`Failed to parse parenthesis expression: ${error.message}`); } } }