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 }