simple-squiggle

A restricted subset of Squiggle
Log | Files | Refs | README

FunctionNode.js (17094B)


      1 import { isAccessorNode, isFunctionAssignmentNode, isIndexNode, isNode, isSymbolNode } from '../../utils/is.js';
      2 import { escape, format } from '../../utils/string.js';
      3 import { hasOwnProperty } from '../../utils/object.js';
      4 import { getSafeProperty, validateSafeMethod } from '../../utils/customs.js';
      5 import { createSubScope } from '../../utils/scope.js';
      6 import { factory } from '../../utils/factory.js';
      7 import { defaultTemplate, latexFunctions } from '../../utils/latex.js';
      8 var name = 'FunctionNode';
      9 var dependencies = ['math', 'Node', 'SymbolNode'];
     10 export var createFunctionNode = /* #__PURE__ */factory(name, dependencies, _ref => {
     11   var {
     12     math,
     13     Node,
     14     SymbolNode
     15   } = _ref;
     16 
     17   /**
     18    * @constructor FunctionNode
     19    * @extends {./Node}
     20    * invoke a list with arguments on a node
     21    * @param {./Node | string} fn Node resolving with a function on which to invoke
     22    *                             the arguments, typically a SymboNode or AccessorNode
     23    * @param {./Node[]} args
     24    */
     25   function FunctionNode(fn, args) {
     26     if (!(this instanceof FunctionNode)) {
     27       throw new SyntaxError('Constructor must be called with the new operator');
     28     }
     29 
     30     if (typeof fn === 'string') {
     31       fn = new SymbolNode(fn);
     32     } // validate input
     33 
     34 
     35     if (!isNode(fn)) throw new TypeError('Node expected as parameter "fn"');
     36 
     37     if (!Array.isArray(args) || !args.every(isNode)) {
     38       throw new TypeError('Array containing Nodes expected for parameter "args"');
     39     }
     40 
     41     this.fn = fn;
     42     this.args = args || []; // readonly property name
     43 
     44     Object.defineProperty(this, 'name', {
     45       get: function () {
     46         return this.fn.name || '';
     47       }.bind(this),
     48       set: function set() {
     49         throw new Error('Cannot assign a new name, name is read-only');
     50       }
     51     });
     52   }
     53 
     54   FunctionNode.prototype = new Node();
     55   FunctionNode.prototype.type = 'FunctionNode';
     56   FunctionNode.prototype.isFunctionNode = true;
     57   /* format to fixed length */
     58 
     59   var strin = entity => format(entity, {
     60     truncate: 78
     61   });
     62   /**
     63    * Compile a node into a JavaScript function.
     64    * This basically pre-calculates as much as possible and only leaves open
     65    * calculations which depend on a dynamic scope with variables.
     66    * @param {Object} math     Math.js namespace with functions and constants.
     67    * @param {Object} argNames An object with argument names as key and `true`
     68    *                          as value. Used in the SymbolNode to optimize
     69    *                          for arguments from user assigned functions
     70    *                          (see FunctionAssignmentNode) or special symbols
     71    *                          like `end` (see IndexNode).
     72    * @return {function} Returns a function which can be called like:
     73    *                        evalNode(scope: Object, args: Object, context: *)
     74    */
     75 
     76 
     77   FunctionNode.prototype._compile = function (math, argNames) {
     78     if (!(this instanceof FunctionNode)) {
     79       throw new TypeError('No valid FunctionNode');
     80     } // compile arguments
     81 
     82 
     83     var evalArgs = this.args.map(arg => arg._compile(math, argNames));
     84 
     85     if (isSymbolNode(this.fn)) {
     86       var _name = this.fn.name;
     87 
     88       if (!argNames[_name]) {
     89         // we can statically determine whether the function has an rawArgs property
     90         var fn = _name in math ? getSafeProperty(math, _name) : undefined;
     91         var isRaw = typeof fn === 'function' && fn.rawArgs === true;
     92 
     93         var resolveFn = scope => {
     94           var value;
     95 
     96           if (scope.has(_name)) {
     97             value = scope.get(_name);
     98           } else if (_name in math) {
     99             value = getSafeProperty(math, _name);
    100           } else {
    101             return FunctionNode.onUndefinedFunction(_name);
    102           }
    103 
    104           if (typeof value === 'function') {
    105             return value;
    106           }
    107 
    108           throw new TypeError("'".concat(_name, "' is not a function; its value is:\n  ").concat(strin(value)));
    109         };
    110 
    111         if (isRaw) {
    112           // pass unevaluated parameters (nodes) to the function
    113           // "raw" evaluation
    114           var rawArgs = this.args;
    115           return function evalFunctionNode(scope, args, context) {
    116             var fn = resolveFn(scope);
    117             return fn(rawArgs, math, createSubScope(scope, args), scope);
    118           };
    119         } else {
    120           // "regular" evaluation
    121           switch (evalArgs.length) {
    122             case 0:
    123               return function evalFunctionNode(scope, args, context) {
    124                 var fn = resolveFn(scope);
    125                 return fn();
    126               };
    127 
    128             case 1:
    129               return function evalFunctionNode(scope, args, context) {
    130                 var fn = resolveFn(scope);
    131                 var evalArg0 = evalArgs[0];
    132                 return fn(evalArg0(scope, args, context));
    133               };
    134 
    135             case 2:
    136               return function evalFunctionNode(scope, args, context) {
    137                 var fn = resolveFn(scope);
    138                 var evalArg0 = evalArgs[0];
    139                 var evalArg1 = evalArgs[1];
    140                 return fn(evalArg0(scope, args, context), evalArg1(scope, args, context));
    141               };
    142 
    143             default:
    144               return function evalFunctionNode(scope, args, context) {
    145                 var fn = resolveFn(scope);
    146                 var values = evalArgs.map(evalArg => evalArg(scope, args, context));
    147                 return fn(...values);
    148               };
    149           }
    150         }
    151       } else {
    152         // the function symbol is an argName
    153         var _rawArgs = this.args;
    154         return function evalFunctionNode(scope, args, context) {
    155           var fn = args[_name];
    156 
    157           if (typeof fn !== 'function') {
    158             throw new TypeError("Argument '".concat(_name, "' was not a function; received: ").concat(strin(fn)));
    159           }
    160 
    161           if (fn.rawArgs) {
    162             return fn(_rawArgs, math, createSubScope(scope, args), scope); // "raw" evaluation
    163           } else {
    164             var values = evalArgs.map(evalArg => evalArg(scope, args, context));
    165             return fn.apply(fn, values);
    166           }
    167         };
    168       }
    169     } else if (isAccessorNode(this.fn) && isIndexNode(this.fn.index) && this.fn.index.isObjectProperty()) {
    170       // execute the function with the right context: the object of the AccessorNode
    171       var evalObject = this.fn.object._compile(math, argNames);
    172 
    173       var prop = this.fn.index.getObjectProperty();
    174       var _rawArgs2 = this.args;
    175       return function evalFunctionNode(scope, args, context) {
    176         var object = evalObject(scope, args, context);
    177         validateSafeMethod(object, prop);
    178         var isRaw = object[prop] && object[prop].rawArgs;
    179 
    180         if (isRaw) {
    181           return object[prop](_rawArgs2, math, createSubScope(scope, args), scope); // "raw" evaluation
    182         } else {
    183           // "regular" evaluation
    184           var values = evalArgs.map(evalArg => evalArg(scope, args, context));
    185           return object[prop].apply(object, values);
    186         }
    187       };
    188     } else {
    189       // node.fn.isAccessorNode && !node.fn.index.isObjectProperty()
    190       // we have to dynamically determine whether the function has a rawArgs property
    191       var fnExpr = this.fn.toString();
    192 
    193       var evalFn = this.fn._compile(math, argNames);
    194 
    195       var _rawArgs3 = this.args;
    196       return function evalFunctionNode(scope, args, context) {
    197         var fn = evalFn(scope, args, context);
    198 
    199         if (typeof fn !== 'function') {
    200           throw new TypeError("Expression '".concat(fnExpr, "' did not evaluate to a function; value is:") + "\n  ".concat(strin(fn)));
    201         }
    202 
    203         if (fn.rawArgs) {
    204           return fn(_rawArgs3, math, createSubScope(scope, args), scope); // "raw" evaluation
    205         } else {
    206           // "regular" evaluation
    207           var values = evalArgs.map(evalArg => evalArg(scope, args, context));
    208           return fn.apply(fn, values);
    209         }
    210       };
    211     }
    212   };
    213   /**
    214    * Execute a callback for each of the child nodes of this node
    215    * @param {function(child: Node, path: string, parent: Node)} callback
    216    */
    217 
    218 
    219   FunctionNode.prototype.forEach = function (callback) {
    220     callback(this.fn, 'fn', this);
    221 
    222     for (var i = 0; i < this.args.length; i++) {
    223       callback(this.args[i], 'args[' + i + ']', this);
    224     }
    225   };
    226   /**
    227    * Create a new FunctionNode having it's childs be the results of calling
    228    * the provided callback function for each of the childs of the original node.
    229    * @param {function(child: Node, path: string, parent: Node): Node} callback
    230    * @returns {FunctionNode} Returns a transformed copy of the node
    231    */
    232 
    233 
    234   FunctionNode.prototype.map = function (callback) {
    235     var fn = this._ifNode(callback(this.fn, 'fn', this));
    236 
    237     var args = [];
    238 
    239     for (var i = 0; i < this.args.length; i++) {
    240       args[i] = this._ifNode(callback(this.args[i], 'args[' + i + ']', this));
    241     }
    242 
    243     return new FunctionNode(fn, args);
    244   };
    245   /**
    246    * Create a clone of this node, a shallow copy
    247    * @return {FunctionNode}
    248    */
    249 
    250 
    251   FunctionNode.prototype.clone = function () {
    252     return new FunctionNode(this.fn, this.args.slice(0));
    253   };
    254   /**
    255    * Throws an error 'Undefined function {name}'
    256    * @param {string} name
    257    */
    258 
    259 
    260   FunctionNode.onUndefinedFunction = function (name) {
    261     throw new Error('Undefined function ' + name);
    262   }; // backup Node's toString function
    263   // @private
    264 
    265 
    266   var nodeToString = FunctionNode.prototype.toString;
    267   /**
    268    * Get string representation. (wrapper function)
    269    * This overrides parts of Node's toString function.
    270    * If callback is an object containing callbacks, it
    271    * calls the correct callback for the current node,
    272    * otherwise it falls back to calling Node's toString
    273    * function.
    274    *
    275    * @param {Object} options
    276    * @return {string} str
    277    * @override
    278    */
    279 
    280   FunctionNode.prototype.toString = function (options) {
    281     var customString;
    282     var name = this.fn.toString(options);
    283 
    284     if (options && typeof options.handler === 'object' && hasOwnProperty(options.handler, name)) {
    285       // callback is a map of callback functions
    286       customString = options.handler[name](this, options);
    287     }
    288 
    289     if (typeof customString !== 'undefined') {
    290       return customString;
    291     } // fall back to Node's toString
    292 
    293 
    294     return nodeToString.call(this, options);
    295   };
    296   /**
    297    * Get string representation
    298    * @param {Object} options
    299    * @return {string} str
    300    */
    301 
    302 
    303   FunctionNode.prototype._toString = function (options) {
    304     var args = this.args.map(function (arg) {
    305       return arg.toString(options);
    306     });
    307     var fn = isFunctionAssignmentNode(this.fn) ? '(' + this.fn.toString(options) + ')' : this.fn.toString(options); // format the arguments like "add(2, 4.2)"
    308 
    309     return fn + '(' + args.join(', ') + ')';
    310   };
    311   /**
    312    * Get a JSON representation of the node
    313    * @returns {Object}
    314    */
    315 
    316 
    317   FunctionNode.prototype.toJSON = function () {
    318     return {
    319       mathjs: 'FunctionNode',
    320       fn: this.fn,
    321       args: this.args
    322     };
    323   };
    324   /**
    325    * Instantiate an AssignmentNode from its JSON representation
    326    * @param {Object} json  An object structured like
    327    *                       `{"mathjs": "FunctionNode", fn: ..., args: ...}`,
    328    *                       where mathjs is optional
    329    * @returns {FunctionNode}
    330    */
    331 
    332 
    333   FunctionNode.fromJSON = function (json) {
    334     return new FunctionNode(json.fn, json.args);
    335   };
    336   /**
    337    * Get HTML representation
    338    * @param {Object} options
    339    * @return {string} str
    340    */
    341 
    342 
    343   FunctionNode.prototype.toHTML = function (options) {
    344     var args = this.args.map(function (arg) {
    345       return arg.toHTML(options);
    346     }); // format the arguments like "add(2, 4.2)"
    347 
    348     return '<span class="math-function">' + escape(this.fn) + '</span><span class="math-paranthesis math-round-parenthesis">(</span>' + args.join('<span class="math-separator">,</span>') + '<span class="math-paranthesis math-round-parenthesis">)</span>';
    349   };
    350   /*
    351    * Expand a LaTeX template
    352    *
    353    * @param {string} template
    354    * @param {Node} node
    355    * @param {Object} options
    356    * @private
    357    **/
    358 
    359 
    360   function expandTemplate(template, node, options) {
    361     var latex = ''; // Match everything of the form ${identifier} or ${identifier[2]} or $$
    362     // while submatching identifier and 2 (in the second case)
    363 
    364     var regex = /\$(?:\{([a-z_][a-z_0-9]*)(?:\[([0-9]+)\])?\}|\$)/gi;
    365     var inputPos = 0; // position in the input string
    366 
    367     var match;
    368 
    369     while ((match = regex.exec(template)) !== null) {
    370       // go through all matches
    371       // add everything in front of the match to the LaTeX string
    372       latex += template.substring(inputPos, match.index);
    373       inputPos = match.index;
    374 
    375       if (match[0] === '$$') {
    376         // escaped dollar sign
    377         latex += '$';
    378         inputPos++;
    379       } else {
    380         // template parameter
    381         inputPos += match[0].length;
    382         var property = node[match[1]];
    383 
    384         if (!property) {
    385           throw new ReferenceError('Template: Property ' + match[1] + ' does not exist.');
    386         }
    387 
    388         if (match[2] === undefined) {
    389           // no square brackets
    390           switch (typeof property) {
    391             case 'string':
    392               latex += property;
    393               break;
    394 
    395             case 'object':
    396               if (isNode(property)) {
    397                 latex += property.toTex(options);
    398               } else if (Array.isArray(property)) {
    399                 // make array of Nodes into comma separated list
    400                 latex += property.map(function (arg, index) {
    401                   if (isNode(arg)) {
    402                     return arg.toTex(options);
    403                   }
    404 
    405                   throw new TypeError('Template: ' + match[1] + '[' + index + '] is not a Node.');
    406                 }).join(',');
    407               } else {
    408                 throw new TypeError('Template: ' + match[1] + ' has to be a Node, String or array of Nodes');
    409               }
    410 
    411               break;
    412 
    413             default:
    414               throw new TypeError('Template: ' + match[1] + ' has to be a Node, String or array of Nodes');
    415           }
    416         } else {
    417           // with square brackets
    418           if (isNode(property[match[2]] && property[match[2]])) {
    419             latex += property[match[2]].toTex(options);
    420           } else {
    421             throw new TypeError('Template: ' + match[1] + '[' + match[2] + '] is not a Node.');
    422           }
    423         }
    424       }
    425     }
    426 
    427     latex += template.slice(inputPos); // append rest of the template
    428 
    429     return latex;
    430   } // backup Node's toTex function
    431   // @private
    432 
    433 
    434   var nodeToTex = FunctionNode.prototype.toTex;
    435   /**
    436    * Get LaTeX representation. (wrapper function)
    437    * This overrides parts of Node's toTex function.
    438    * If callback is an object containing callbacks, it
    439    * calls the correct callback for the current node,
    440    * otherwise it falls back to calling Node's toTex
    441    * function.
    442    *
    443    * @param {Object} options
    444    * @return {string}
    445    */
    446 
    447   FunctionNode.prototype.toTex = function (options) {
    448     var customTex;
    449 
    450     if (options && typeof options.handler === 'object' && hasOwnProperty(options.handler, this.name)) {
    451       // callback is a map of callback functions
    452       customTex = options.handler[this.name](this, options);
    453     }
    454 
    455     if (typeof customTex !== 'undefined') {
    456       return customTex;
    457     } // fall back to Node's toTex
    458 
    459 
    460     return nodeToTex.call(this, options);
    461   };
    462   /**
    463    * Get LaTeX representation
    464    * @param {Object} options
    465    * @return {string} str
    466    */
    467 
    468 
    469   FunctionNode.prototype._toTex = function (options) {
    470     var args = this.args.map(function (arg) {
    471       // get LaTeX of the arguments
    472       return arg.toTex(options);
    473     });
    474     var latexConverter;
    475 
    476     if (latexFunctions[this.name]) {
    477       latexConverter = latexFunctions[this.name];
    478     } // toTex property on the function itself
    479 
    480 
    481     if (math[this.name] && (typeof math[this.name].toTex === 'function' || typeof math[this.name].toTex === 'object' || typeof math[this.name].toTex === 'string')) {
    482       // .toTex is a callback function
    483       latexConverter = math[this.name].toTex;
    484     }
    485 
    486     var customToTex;
    487 
    488     switch (typeof latexConverter) {
    489       case 'function':
    490         // a callback function
    491         customToTex = latexConverter(this, options);
    492         break;
    493 
    494       case 'string':
    495         // a template string
    496         customToTex = expandTemplate(latexConverter, this, options);
    497         break;
    498 
    499       case 'object':
    500         // an object with different "converters" for different numbers of arguments
    501         switch (typeof latexConverter[args.length]) {
    502           case 'function':
    503             customToTex = latexConverter[args.length](this, options);
    504             break;
    505 
    506           case 'string':
    507             customToTex = expandTemplate(latexConverter[args.length], this, options);
    508             break;
    509         }
    510 
    511     }
    512 
    513     if (typeof customToTex !== 'undefined') {
    514       return customToTex;
    515     }
    516 
    517     return expandTemplate(defaultTemplate, this, options);
    518   };
    519   /**
    520    * Get identifier.
    521    * @return {string}
    522    */
    523 
    524 
    525   FunctionNode.prototype.getIdentifier = function () {
    526     return this.type + ':' + this.name;
    527   };
    528 
    529   return FunctionNode;
    530 }, {
    531   isClass: true,
    532   isNode: true
    533 });