mirror of
https://github.com/josdejong/mathjs.git
synced 2026-01-25 15:07:57 +00:00
438 lines
14 KiB
JavaScript
438 lines
14 KiB
JavaScript
'use strict'
|
|
|
|
import { isBigNumber, isComplex, isFraction, isMatrix, isUnit } from '../../utils/is'
|
|
import { isFactory, stripOptionalNotation } from '../../utils/factory'
|
|
import { get, isLegacyFactory, lazy, set, traverse } from '../../utils/object'
|
|
import { initial, last } from '../../utils/array'
|
|
import { ArgumentsError } from '../../error/ArgumentsError'
|
|
import { warnOnce } from '../../utils/log'
|
|
|
|
export function importFactory (typed, load, math, factories) {
|
|
/**
|
|
* Import functions from an object or a module
|
|
*
|
|
* Syntax:
|
|
*
|
|
* math.import(object)
|
|
* math.import(object, options)
|
|
*
|
|
* Where:
|
|
*
|
|
* - `object: Object`
|
|
* An object with functions 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:
|
|
*
|
|
* // 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(require('numbers'), {wrap: true})
|
|
*
|
|
* math.fibonacci(7) // returns 13
|
|
*
|
|
* @param {Object | Array} object Object with functions to be imported.
|
|
* @param {Object} [options] Import options.
|
|
*/
|
|
function mathImport (object, options) {
|
|
const num = arguments.length
|
|
if (num !== 1 && num !== 2) {
|
|
throw new ArgumentsError('import', num, 1, 2)
|
|
}
|
|
|
|
if (!options) {
|
|
options = {}
|
|
}
|
|
|
|
// TODO: allow a typed-function with name too
|
|
if (isFactory(object)) {
|
|
_importFactory(object, options)
|
|
} else if (isLegacyFactory(object)) {
|
|
_importLegacyFactory(object, options)
|
|
} else if (Array.isArray(object)) {
|
|
object.forEach((entry) => mathImport(entry, options))
|
|
} else if (typeof object === 'object') {
|
|
// a map with functions
|
|
for (const name in object) {
|
|
if (object.hasOwnProperty(name)) {
|
|
const value = object[name]
|
|
if (isFactory(value)) {
|
|
_importFactory(value, options, name)
|
|
} else if (isSupportedType(value)) {
|
|
_import(name, value, options)
|
|
} else if (isLegacyFactory(object)) {
|
|
_importLegacyFactory(object, options)
|
|
} else {
|
|
mathImport(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 (isTypedFunction(math[name]) && 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 factories[name]
|
|
|
|
_importTransform(name, value)
|
|
math.emit('import', name, function resolver () {
|
|
return value
|
|
})
|
|
return
|
|
}
|
|
|
|
if (math[name] === undefined || options.override) {
|
|
math[name] = value
|
|
delete factories[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) {
|
|
const wrapper = function wrapper () {
|
|
const args = []
|
|
for (let i = 0, len = arguments.length; i < len; i++) {
|
|
const 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 {{factory: Function, name: string, path: string, math: boolean}} factory
|
|
* @param {Object} options See import for a description of the options
|
|
* @private
|
|
*/
|
|
// TODO: _importLegacyFactory is deprecated since v6.0.0, clean up some day
|
|
function _importLegacyFactory (factory, options) {
|
|
warnOnce('Factories of type { name, factory } are deprecated since v6. ' +
|
|
'Please refactor your factory functions.')
|
|
|
|
if (typeof factory.name === 'string') {
|
|
const name = factory.name
|
|
const existingTransform = name in math.expression.transform
|
|
const namespace = factory.path ? traverse(math, factory.path) : math
|
|
const existing = namespace.hasOwnProperty(name) ? namespace[name] : undefined
|
|
|
|
const resolver = function () {
|
|
let instance = load(factory)
|
|
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 (isTypedFunction(existing) && isTypedFunction(instance)) {
|
|
if (options.override) {
|
|
// replace the existing typed function (nothing to do)
|
|
} else {
|
|
// merge the existing and new typed function
|
|
instance = typed(existing, instance)
|
|
}
|
|
|
|
return instance
|
|
}
|
|
|
|
if (existing === undefined || options.override) {
|
|
return instance
|
|
}
|
|
|
|
if (options.silent) {
|
|
return existing
|
|
} else {
|
|
throw new Error('Cannot import "' + name + '": already exists')
|
|
}
|
|
}
|
|
|
|
if (factory.lazy !== false) {
|
|
lazy(namespace, name, resolver)
|
|
|
|
if (existingTransform) {
|
|
_deleteTransform(name)
|
|
} else {
|
|
if (factory.path === 'expression.transform' || legacyFactoryAllowedInExpressions(factory)) {
|
|
lazy(math.expression.mathWithTransform, name, resolver)
|
|
}
|
|
}
|
|
} else {
|
|
namespace[name] = resolver()
|
|
|
|
if (existingTransform) {
|
|
_deleteTransform(name)
|
|
} else {
|
|
if (factory.path === 'expression.transform' || legacyFactoryAllowedInExpressions(factory)) {
|
|
math.expression.mathWithTransform[name] = resolver()
|
|
}
|
|
}
|
|
}
|
|
|
|
const key = factory.path ? (factory.path + '.' + factory.name) : factory.name
|
|
factories[key] = factory
|
|
|
|
math.emit('import', name, resolver, factory.path)
|
|
} else {
|
|
// unnamed factory.
|
|
// no lazy loading
|
|
load(factory)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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} [fullName=factory.name] Optional custom name
|
|
* @private
|
|
*/
|
|
function _importFactory (factory, options, fullName = factory.fn) {
|
|
// FIXME: remove support for path in factory name
|
|
const nameContainsPath = fullName.indexOf('.') !== -1
|
|
const path = nameContainsPath
|
|
? initial(fullName.split('.'))
|
|
: (factory.meta && factory.meta.isNode === true)
|
|
? ['expression', 'node'] // path where we want to put node classes
|
|
: (factory.meta && factory.meta.isClass === true)
|
|
? ['type'] // path where we want to put classes
|
|
: (fullName === 'reviver') // special case, JSON util function
|
|
? ['json']
|
|
: undefined
|
|
const name = nameContainsPath ? last(fullName.split('.')) : fullName
|
|
const namespace = path !== undefined ? traverse(math, path) : math
|
|
const existingTransform = name in math.expression.transform
|
|
const existing = namespace.hasOwnProperty(name) ? namespace[name] : undefined
|
|
|
|
const resolver = function () {
|
|
// collect all dependencies, handle finding both functions and classes and other special cases
|
|
const dependencies = {}
|
|
factory.dependencies
|
|
.map(stripOptionalNotation)
|
|
.forEach(dependency => {
|
|
if (dependency === 'math') {
|
|
dependencies.math = math
|
|
} else if (dependency === 'classes') { // special case for json reviver
|
|
dependencies.classes = Object.assign({}, math.type, math.expression.node)
|
|
} else {
|
|
// dependencies[name] = math.type[name] || math.expression.node[name] || math[name] // FIXME: replace with non nested solution
|
|
const value = get(math, dependency) || get(math.type, dependency) || get(math.expression.node, dependency)
|
|
set(dependencies, dependency, value)
|
|
}
|
|
})
|
|
|
|
let 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 (isTypedFunction(existing) && isTypedFunction(instance)) {
|
|
if (options.override) {
|
|
// replace the existing typed function (nothing to do)
|
|
} else {
|
|
// merge the existing and new typed function
|
|
instance = typed(existing, instance)
|
|
}
|
|
|
|
return instance
|
|
}
|
|
|
|
if (existing === undefined || options.override) {
|
|
return instance
|
|
}
|
|
|
|
if (options.silent) {
|
|
return existing
|
|
} else {
|
|
throw new Error('Cannot import "' + fullName + '": already exists')
|
|
}
|
|
}
|
|
|
|
if (factory.lazy !== false) {
|
|
lazy(namespace, name, resolver)
|
|
|
|
if (existingTransform) {
|
|
_deleteTransform(name)
|
|
} else {
|
|
if (((path && path.join('.') === 'expression.transform')) || factoryAllowedInExpressions(factory)) {
|
|
lazy(math.expression.mathWithTransform, name, () => namespace[name])
|
|
}
|
|
}
|
|
} else {
|
|
namespace[name] = resolver()
|
|
|
|
if (existingTransform) {
|
|
_deleteTransform(name)
|
|
} else {
|
|
if (((path && path.join('.') === 'expression.transform')) || factoryAllowedInExpressions(factory)) {
|
|
math.expression.mathWithTransform[name] = namespace[name]
|
|
}
|
|
}
|
|
}
|
|
|
|
factories[fullName] = factory
|
|
|
|
math.emit('import', name, resolver, path)
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
}
|
|
|
|
/**
|
|
* Test whether a given thing is a typed-function
|
|
* @param {*} fn
|
|
* @return {boolean} Returns true when `fn` is a typed-function
|
|
*/
|
|
function isTypedFunction (fn) {
|
|
return typeof fn === 'function' && typeof fn.signatures === 'object'
|
|
}
|
|
|
|
function hasTypedFunctionSignature (fn) {
|
|
return typeof fn === 'function' && typeof fn.signature === 'string'
|
|
}
|
|
|
|
function allowedInExpressions (name) {
|
|
return !unsafe.hasOwnProperty(name)
|
|
}
|
|
|
|
function legacyFactoryAllowedInExpressions (factory) {
|
|
return factory.path === undefined && !unsafe.hasOwnProperty(factory.name)
|
|
}
|
|
|
|
function factoryAllowedInExpressions (factory) {
|
|
return factory.fn.indexOf('.') === -1 && // FIXME: make checking on path redundant, check on meta data instead
|
|
!unsafe.hasOwnProperty(factory.fn) &&
|
|
(!factory.meta || !factory.meta.isClass)
|
|
}
|
|
|
|
// namespaces and functions not available in the parser for safety reasons
|
|
const 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.
|
|
'reviver': true
|
|
}
|
|
|
|
return mathImport
|
|
}
|