simple-squiggle

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

import.js (12565B)


      1 import { isBigNumber, isComplex, isFraction, isMatrix, isUnit } from '../../utils/is.js';
      2 import { isFactory, stripOptionalNotation } from '../../utils/factory.js';
      3 import { hasOwnProperty, lazy } from '../../utils/object.js';
      4 import { contains } from '../../utils/array.js';
      5 import { ArgumentsError } from '../../error/ArgumentsError.js';
      6 export function importFactory(typed, load, math, importedFactories) {
      7   /**
      8    * Import functions from an object or a module.
      9    *
     10    * This function is only available on a mathjs instance created using `create`.
     11    *
     12    * Syntax:
     13    *
     14    *    math.import(functions)
     15    *    math.import(functions, options)
     16    *
     17    * Where:
     18    *
     19    * - `functions: Object`
     20    *   An object with functions or factories to be imported.
     21    * - `options: Object` An object with import options. Available options:
     22    *   - `override: boolean`
     23    *     If true, existing functions will be overwritten. False by default.
     24    *   - `silent: boolean`
     25    *     If true, the function will not throw errors on duplicates or invalid
     26    *     types. False by default.
     27    *   - `wrap: boolean`
     28    *     If true, the functions will be wrapped in a wrapper function
     29    *     which converts data types like Matrix to primitive data types like Array.
     30    *     The wrapper is needed when extending math.js with libraries which do not
     31    *     support these data type. False by default.
     32    *
     33    * Examples:
     34    *
     35    *    import { create, all } from 'mathjs'
     36    *    import * as numbers from 'numbers'
     37    *
     38    *    // create a mathjs instance
     39    *    const math = create(all)
     40    *
     41    *    // define new functions and variables
     42    *    math.import({
     43    *      myvalue: 42,
     44    *      hello: function (name) {
     45    *        return 'hello, ' + name + '!'
     46    *      }
     47    *    })
     48    *
     49    *    // use the imported function and variable
     50    *    math.myvalue * 2               // 84
     51    *    math.hello('user')             // 'hello, user!'
     52    *
     53    *    // import the npm module 'numbers'
     54    *    // (must be installed first with `npm install numbers`)
     55    *    math.import(numbers, {wrap: true})
     56    *
     57    *    math.fibonacci(7) // returns 13
     58    *
     59    * @param {Object | Array} functions  Object with functions to be imported.
     60    * @param {Object} [options]          Import options.
     61    */
     62   function mathImport(functions, options) {
     63     var num = arguments.length;
     64 
     65     if (num !== 1 && num !== 2) {
     66       throw new ArgumentsError('import', num, 1, 2);
     67     }
     68 
     69     if (!options) {
     70       options = {};
     71     }
     72 
     73     function flattenImports(flatValues, value, name) {
     74       if (Array.isArray(value)) {
     75         value.forEach(item => flattenImports(flatValues, item));
     76       } else if (typeof value === 'object') {
     77         for (var _name in value) {
     78           if (hasOwnProperty(value, _name)) {
     79             flattenImports(flatValues, value[_name], _name);
     80           }
     81         }
     82       } else if (isFactory(value) || name !== undefined) {
     83         var flatName = isFactory(value) ? isTransformFunctionFactory(value) ? value.fn + '.transform' // TODO: this is ugly
     84         : value.fn : name; // we allow importing the same function twice if it points to the same implementation
     85 
     86         if (hasOwnProperty(flatValues, flatName) && flatValues[flatName] !== value && !options.silent) {
     87           throw new Error('Cannot import "' + flatName + '" twice');
     88         }
     89 
     90         flatValues[flatName] = value;
     91       } else {
     92         if (!options.silent) {
     93           throw new TypeError('Factory, Object, or Array expected');
     94         }
     95       }
     96     }
     97 
     98     var flatValues = {};
     99     flattenImports(flatValues, functions);
    100 
    101     for (var name in flatValues) {
    102       if (hasOwnProperty(flatValues, name)) {
    103         // console.log('import', name)
    104         var value = flatValues[name];
    105 
    106         if (isFactory(value)) {
    107           // we ignore name here and enforce the name of the factory
    108           // maybe at some point we do want to allow overriding it
    109           // in that case we can implement an option overrideFactoryNames: true
    110           _importFactory(value, options);
    111         } else if (isSupportedType(value)) {
    112           _import(name, value, options);
    113         } else {
    114           if (!options.silent) {
    115             throw new TypeError('Factory, Object, or Array expected');
    116           }
    117         }
    118       }
    119     }
    120   }
    121   /**
    122    * Add a property to the math namespace
    123    * @param {string} name
    124    * @param {*} value
    125    * @param {Object} options  See import for a description of the options
    126    * @private
    127    */
    128 
    129 
    130   function _import(name, value, options) {
    131     // TODO: refactor this function, it's to complicated and contains duplicate code
    132     if (options.wrap && typeof value === 'function') {
    133       // create a wrapper around the function
    134       value = _wrap(value);
    135     } // turn a plain function with a typed-function signature into a typed-function
    136 
    137 
    138     if (hasTypedFunctionSignature(value)) {
    139       value = typed(name, {
    140         [value.signature]: value
    141       });
    142     }
    143 
    144     if (isTypedFunction(math[name]) && isTypedFunction(value)) {
    145       if (options.override) {
    146         // give the typed function the right name
    147         value = typed(name, value.signatures);
    148       } else {
    149         // merge the existing and typed function
    150         value = typed(math[name], value);
    151       }
    152 
    153       math[name] = value;
    154       delete importedFactories[name];
    155 
    156       _importTransform(name, value);
    157 
    158       math.emit('import', name, function resolver() {
    159         return value;
    160       });
    161       return;
    162     }
    163 
    164     if (math[name] === undefined || options.override) {
    165       math[name] = value;
    166       delete importedFactories[name];
    167 
    168       _importTransform(name, value);
    169 
    170       math.emit('import', name, function resolver() {
    171         return value;
    172       });
    173       return;
    174     }
    175 
    176     if (!options.silent) {
    177       throw new Error('Cannot import "' + name + '": already exists');
    178     }
    179   }
    180 
    181   function _importTransform(name, value) {
    182     if (value && typeof value.transform === 'function') {
    183       math.expression.transform[name] = value.transform;
    184 
    185       if (allowedInExpressions(name)) {
    186         math.expression.mathWithTransform[name] = value.transform;
    187       }
    188     } else {
    189       // remove existing transform
    190       delete math.expression.transform[name];
    191 
    192       if (allowedInExpressions(name)) {
    193         math.expression.mathWithTransform[name] = value;
    194       }
    195     }
    196   }
    197 
    198   function _deleteTransform(name) {
    199     delete math.expression.transform[name];
    200 
    201     if (allowedInExpressions(name)) {
    202       math.expression.mathWithTransform[name] = math[name];
    203     } else {
    204       delete math.expression.mathWithTransform[name];
    205     }
    206   }
    207   /**
    208    * Create a wrapper a round an function which converts the arguments
    209    * to their primitive values (like convert a Matrix to Array)
    210    * @param {Function} fn
    211    * @return {Function} Returns the wrapped function
    212    * @private
    213    */
    214 
    215 
    216   function _wrap(fn) {
    217     var wrapper = function wrapper() {
    218       var args = [];
    219 
    220       for (var i = 0, len = arguments.length; i < len; i++) {
    221         var arg = arguments[i];
    222         args[i] = arg && arg.valueOf();
    223       }
    224 
    225       return fn.apply(math, args);
    226     };
    227 
    228     if (fn.transform) {
    229       wrapper.transform = fn.transform;
    230     }
    231 
    232     return wrapper;
    233   }
    234   /**
    235    * Import an instance of a factory into math.js
    236    * @param {function(scope: object)} factory
    237    * @param {Object} options  See import for a description of the options
    238    * @param {string} [name=factory.name] Optional custom name
    239    * @private
    240    */
    241 
    242 
    243   function _importFactory(factory, options) {
    244     var name = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : factory.fn;
    245 
    246     if (contains(name, '.')) {
    247       throw new Error('Factory name should not contain a nested path. ' + 'Name: ' + JSON.stringify(name));
    248     }
    249 
    250     var namespace = isTransformFunctionFactory(factory) ? math.expression.transform : math;
    251     var existingTransform = (name in math.expression.transform);
    252     var existing = hasOwnProperty(namespace, name) ? namespace[name] : undefined;
    253 
    254     var resolver = function resolver() {
    255       // collect all dependencies, handle finding both functions and classes and other special cases
    256       var dependencies = {};
    257       factory.dependencies.map(stripOptionalNotation).forEach(dependency => {
    258         if (contains(dependency, '.')) {
    259           throw new Error('Factory dependency should not contain a nested path. ' + 'Name: ' + JSON.stringify(dependency));
    260         }
    261 
    262         if (dependency === 'math') {
    263           dependencies.math = math;
    264         } else if (dependency === 'mathWithTransform') {
    265           dependencies.mathWithTransform = math.expression.mathWithTransform;
    266         } else if (dependency === 'classes') {
    267           // special case for json reviver
    268           dependencies.classes = math;
    269         } else {
    270           dependencies[dependency] = math[dependency];
    271         }
    272       });
    273       var instance = /* #__PURE__ */factory(dependencies);
    274 
    275       if (instance && typeof instance.transform === 'function') {
    276         throw new Error('Transforms cannot be attached to factory functions. ' + 'Please create a separate function for it with exports.path="expression.transform"');
    277       }
    278 
    279       if (existing === undefined || options.override) {
    280         return instance;
    281       }
    282 
    283       if (isTypedFunction(existing) && isTypedFunction(instance)) {
    284         // merge the existing and new typed function
    285         return typed(existing, instance);
    286       }
    287 
    288       if (options.silent) {
    289         // keep existing, ignore imported function
    290         return existing;
    291       } else {
    292         throw new Error('Cannot import "' + name + '": already exists');
    293       }
    294     }; // TODO: add unit test with non-lazy factory
    295 
    296 
    297     if (!factory.meta || factory.meta.lazy !== false) {
    298       lazy(namespace, name, resolver); // FIXME: remove the `if (existing &&` condition again. Can we make sure subset is loaded before subset.transform? (Name collision, and no dependencies between the two)
    299 
    300       if (existing && existingTransform) {
    301         _deleteTransform(name);
    302       } else {
    303         if (isTransformFunctionFactory(factory) || factoryAllowedInExpressions(factory)) {
    304           lazy(math.expression.mathWithTransform, name, () => namespace[name]);
    305         }
    306       }
    307     } else {
    308       namespace[name] = resolver(); // FIXME: remove the `if (existing &&` condition again. Can we make sure subset is loaded before subset.transform? (Name collision, and no dependencies between the two)
    309 
    310       if (existing && existingTransform) {
    311         _deleteTransform(name);
    312       } else {
    313         if (isTransformFunctionFactory(factory) || factoryAllowedInExpressions(factory)) {
    314           lazy(math.expression.mathWithTransform, name, () => namespace[name]);
    315         }
    316       }
    317     } // TODO: improve factories, store a list with imports instead which can be re-played
    318 
    319 
    320     importedFactories[name] = factory;
    321     math.emit('import', name, resolver);
    322   }
    323   /**
    324    * Check whether given object is a type which can be imported
    325    * @param {Function | number | string | boolean | null | Unit | Complex} object
    326    * @return {boolean}
    327    * @private
    328    */
    329 
    330 
    331   function isSupportedType(object) {
    332     return typeof object === 'function' || typeof object === 'number' || typeof object === 'string' || typeof object === 'boolean' || object === null || isUnit(object) || isComplex(object) || isBigNumber(object) || isFraction(object) || isMatrix(object) || Array.isArray(object);
    333   }
    334   /**
    335    * Test whether a given thing is a typed-function
    336    * @param {*} fn
    337    * @return {boolean} Returns true when `fn` is a typed-function
    338    */
    339 
    340 
    341   function isTypedFunction(fn) {
    342     return typeof fn === 'function' && typeof fn.signatures === 'object';
    343   }
    344 
    345   function hasTypedFunctionSignature(fn) {
    346     return typeof fn === 'function' && typeof fn.signature === 'string';
    347   }
    348 
    349   function allowedInExpressions(name) {
    350     return !hasOwnProperty(unsafe, name);
    351   }
    352 
    353   function factoryAllowedInExpressions(factory) {
    354     return factory.fn.indexOf('.') === -1 && // FIXME: make checking on path redundant, check on meta data instead
    355     !hasOwnProperty(unsafe, factory.fn) && (!factory.meta || !factory.meta.isClass);
    356   }
    357 
    358   function isTransformFunctionFactory(factory) {
    359     return factory !== undefined && factory.meta !== undefined && factory.meta.isTransformFunction === true || false;
    360   } // namespaces and functions not available in the parser for safety reasons
    361 
    362 
    363   var unsafe = {
    364     expression: true,
    365     type: true,
    366     docs: true,
    367     error: true,
    368     json: true,
    369     chain: true // chain method not supported. Note that there is a unit chain too.
    370 
    371   };
    372   return mathImport;
    373 }