UNPKG

mathjs

Version:

Math.js is an extensive math library for JavaScript and Node.js. It features a flexible expression parser with support for symbolic computation, comes with a large set of built-in functions and constants, and offers an integrated solution to work with dif

326 lines (315 loc) 12.3 kB
import { isBigNumber, isComplex, isFraction, isMatrix, isUnit } from '../../utils/is.js'; import { isFactory, stripOptionalNotation } from '../../utils/factory.js'; import { hasOwnProperty, lazy } from '../../utils/object.js'; import { contains } from '../../utils/array.js'; import { ArgumentsError } from '../../error/ArgumentsError.js'; export function importFactory(typed, load, math, importedFactories) { /** * Import functions from an object or a module. * * This function is only available on a mathjs instance created using `create`. * * Syntax: * * math.import(functions) * math.import(functions, options) * * Where: * * - `functions: Object` * An object with functions or factories to be imported. * - `options: Object` An object with import options. Available options: * - `override: boolean` * If true, existing functions will be overwritten. False by default. * - `silent: boolean` * If true, the function will not throw errors on duplicates or invalid * types. False by default. * - `wrap: boolean` * If true, the functions will be wrapped in a wrapper function * which converts data types like Matrix to primitive data types like Array. * The wrapper is needed when extending math.js with libraries which do not * support these data type. False by default. * * Examples: * * import { create, all } from 'mathjs' * import * as numbers from 'numbers' * * // create a mathjs instance * const math = create(all) * * // define new functions and variables * math.import({ * myvalue: 42, * hello: function (name) { * return 'hello, ' + name + '!' * } * }) * * // use the imported function and variable * math.myvalue * 2 // 84 * math.hello('user') // 'hello, user!' * * // import the npm module 'numbers' * // (must be installed first with `npm install numbers`) * math.import(numbers, {wrap: true}) * * math.fibonacci(7) // returns 13 * * @param {Object | Array} functions Object with functions to be imported. * @param {Object} [options] Import options. */ function mathImport(functions, options) { var num = arguments.length; if (num !== 1 && num !== 2) { throw new ArgumentsError('import', num, 1, 2); } if (!options) { options = {}; } function flattenImports(flatValues, value, name) { if (Array.isArray(value)) { value.forEach(item => flattenImports(flatValues, item)); } else if (typeof value === 'object') { for (var _name in value) { if (hasOwnProperty(value, _name)) { flattenImports(flatValues, value[_name], _name); } } } else if (isFactory(value) || name !== undefined) { var flatName = isFactory(value) ? isTransformFunctionFactory(value) ? value.fn + '.transform' // TODO: this is ugly : value.fn : name; // we allow importing the same function twice if it points to the same implementation if (hasOwnProperty(flatValues, flatName) && flatValues[flatName] !== value && !options.silent) { throw new Error('Cannot import "' + flatName + '" twice'); } flatValues[flatName] = value; } else { if (!options.silent) { throw new TypeError('Factory, Object, or Array expected'); } } } var flatValues = {}; flattenImports(flatValues, functions); for (var name in flatValues) { if (hasOwnProperty(flatValues, name)) { // console.log('import', name) var value = flatValues[name]; if (isFactory(value)) { // we ignore name here and enforce the name of the factory // maybe at some point we do want to allow overriding it // in that case we can implement an option overrideFactoryNames: true _importFactory(value, options); } else if (isSupportedType(value)) { _import(name, value, options); } else { if (!options.silent) { throw new TypeError('Factory, Object, or Array expected'); } } } } } /** * Add a property to the math namespace * @param {string} name * @param {*} value * @param {Object} options See import for a description of the options * @private */ function _import(name, value, options) { // TODO: refactor this function, it's to complicated and contains duplicate code if (options.wrap && typeof value === 'function') { // create a wrapper around the function value = _wrap(value); } // turn a plain function with a typed-function signature into a typed-function if (hasTypedFunctionSignature(value)) { value = typed(name, { [value.signature]: value }); } if (typed.isTypedFunction(math[name]) && typed.isTypedFunction(value)) { if (options.override) { // give the typed function the right name value = typed(name, value.signatures); } else { // merge the existing and typed function value = typed(math[name], value); } math[name] = value; delete importedFactories[name]; _importTransform(name, value); math.emit('import', name, function resolver() { return value; }); return; } if (math[name] === undefined || options.override) { math[name] = value; delete importedFactories[name]; _importTransform(name, value); math.emit('import', name, function resolver() { return value; }); return; } if (!options.silent) { throw new Error('Cannot import "' + name + '": already exists'); } } function _importTransform(name, value) { if (value && typeof value.transform === 'function') { math.expression.transform[name] = value.transform; if (allowedInExpressions(name)) { math.expression.mathWithTransform[name] = value.transform; } } else { // remove existing transform delete math.expression.transform[name]; if (allowedInExpressions(name)) { math.expression.mathWithTransform[name] = value; } } } function _deleteTransform(name) { delete math.expression.transform[name]; if (allowedInExpressions(name)) { math.expression.mathWithTransform[name] = math[name]; } else { delete math.expression.mathWithTransform[name]; } } /** * Create a wrapper a round an function which converts the arguments * to their primitive values (like convert a Matrix to Array) * @param {Function} fn * @return {Function} Returns the wrapped function * @private */ function _wrap(fn) { var wrapper = function wrapper() { var args = []; for (var i = 0, len = arguments.length; i < len; i++) { var arg = arguments[i]; args[i] = arg && arg.valueOf(); } return fn.apply(math, args); }; if (fn.transform) { wrapper.transform = fn.transform; } return wrapper; } /** * Import an instance of a factory into math.js * @param {function(scope: object)} factory * @param {Object} options See import for a description of the options * @param {string} [name=factory.name] Optional custom name * @private */ function _importFactory(factory, options) { var name = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : factory.fn; if (contains(name, '.')) { throw new Error('Factory name should not contain a nested path. ' + 'Name: ' + JSON.stringify(name)); } var namespace = isTransformFunctionFactory(factory) ? math.expression.transform : math; var existingTransform = (name in math.expression.transform); var existing = hasOwnProperty(namespace, name) ? namespace[name] : undefined; var resolver = function resolver() { // collect all dependencies, handle finding both functions and classes and other special cases var dependencies = {}; factory.dependencies.map(stripOptionalNotation).forEach(dependency => { if (contains(dependency, '.')) { throw new Error('Factory dependency should not contain a nested path. ' + 'Name: ' + JSON.stringify(dependency)); } if (dependency === 'math') { dependencies.math = math; } else if (dependency === 'mathWithTransform') { dependencies.mathWithTransform = math.expression.mathWithTransform; } else if (dependency === 'classes') { // special case for json reviver dependencies.classes = math; } else { dependencies[dependency] = math[dependency]; } }); var instance = /* #__PURE__ */factory(dependencies); if (instance && typeof instance.transform === 'function') { throw new Error('Transforms cannot be attached to factory functions. ' + 'Please create a separate function for it with exports.path="expression.transform"'); } if (existing === undefined || options.override) { return instance; } if (typed.isTypedFunction(existing) && typed.isTypedFunction(instance)) { // merge the existing and new typed function return typed(existing, instance); } if (options.silent) { // keep existing, ignore imported function return existing; } else { throw new Error('Cannot import "' + name + '": already exists'); } }; // TODO: add unit test with non-lazy factory if (!factory.meta || factory.meta.lazy !== false) { 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) if (existing && existingTransform) { _deleteTransform(name); } else { if (isTransformFunctionFactory(factory) || factoryAllowedInExpressions(factory)) { lazy(math.expression.mathWithTransform, name, () => namespace[name]); } } } else { 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) if (existing && existingTransform) { _deleteTransform(name); } else { if (isTransformFunctionFactory(factory) || factoryAllowedInExpressions(factory)) { lazy(math.expression.mathWithTransform, name, () => namespace[name]); } } } // TODO: improve factories, store a list with imports instead which can be re-played importedFactories[name] = factory; math.emit('import', name, resolver); } /** * Check whether given object is a type which can be imported * @param {Function | number | string | boolean | null | Unit | Complex} object * @return {boolean} * @private */ function isSupportedType(object) { 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); } function hasTypedFunctionSignature(fn) { return typeof fn === 'function' && typeof fn.signature === 'string'; } function allowedInExpressions(name) { return !hasOwnProperty(unsafe, name); } function factoryAllowedInExpressions(factory) { return factory.fn.indexOf('.') === -1 && // FIXME: make checking on path redundant, check on meta data instead !hasOwnProperty(unsafe, factory.fn) && (!factory.meta || !factory.meta.isClass); } function isTransformFunctionFactory(factory) { return factory !== undefined && factory.meta !== undefined && factory.meta.isTransformFunction === true || false; } // namespaces and functions not available in the parser for safety reasons var unsafe = { expression: true, type: true, docs: true, error: true, json: true, chain: true // chain method not supported. Note that there is a unit chain too. }; return mathImport; }