Repository URL to install this package:
|
Version:
1.2.6 ▾
|
"use strict";
// conditionals
/* eslint complexity: "OFF" */
// inlined rollup
/* eslint import/max-dependencies: "OFF" */
// one file
/* eslint max-lines: "OFF" */
// debug conditionals
/* eslint max-depth: "OFF" */
const isEmpty = require("./is/empty");
const isTrue = require("./is/true");
const isIteratable = require("./is/iteratable");
const isUndefined = require("./is/undefined");
const isArray = require("./is/array");
const isMap = require("./is/map");
const isSet = require("./is/set");
const isObj = require("./is/obj");
const isPrimitive = require("./is/primitive");
const isNull = require("./is/null");
const ObjectKeys = require("./util/keys");
const reduce = require("./reduce");
const toarr = require("./to-arr");
const dotSet = require("./dot/set");
const emptyTarget = require("./dopemerge/emptyTarget");
const copy = require("./traversers/copy");
const eq = require("./traversers/_eq");
const addPoolingTo = require("./cache/pooler"); // const props = require('./util/props')
// const ENV_DEBUG = require('./env/debug')
// const ENV_DEBUG = true
// const TRUTH = true
const ENV_DEBUG = false;
/**
* {@link https://github.com/wmira/object-traverse/blob/master/index.js object-traverse}
* {@link https://www.npmjs.com/browse/keyword/traverse traverse-js}
* {@link https://www.npmjs.com/package/tree-walk tree-walk}
* {@link https://www.npmjs.com/package/1tree 1tree}
* {@link https://www.npmjs.com/package/pathway pathway}
* {@link https://www.npmjs.com/package/@mojule/tree tree}
* {@link http://web.archive.org/web/20160930054101/http://substack.net/tree_traversal tree-traversal-article}
* {@link https://medium.com/@alexanderv/tries-javascript-simple-implementation-e2a4e54e4330 js-trie-medium}
* --------------------
*
* if needed, clone
*
* first things to check are number/string/boolean/null/undefined
*
* then check non-iteratables
* symbol, promise,
*
* then check conversions
* - map, set
*
* then check empties
* - obj
* - fn
*
* -------
*
* numbers f-or first/last
* and as a sort of hash like
* 1 + 2 + 4 = ISLEAF & ISROOT ?
*
* Array
*
* Object Function Date Error Map Set
*
* String
* Number NaN Infinity
* Boolean
*
*
* null undefined
*
* Promise Symbol
*
* ----
*
* @emits before
* @emits pre
* @emits post
* @emits after
*/
/**
* @desc Traverse class, pooled
* @modifies this.node
* @modifies this.parent
* @modifies this.root
* @since 5.0.0
*
* @member Traverse
* @class
* @constructor
* @alias IterAteOr
* @extends pooler
*
* @param {Traversable} iteratee value to iterate, clone, copy, check for eq
* @param {Object | undefined} [config] wip config for things such as events or configs
*
* @see {@link tree-traversal-article}
* @see traverse
* @TODO make this a trie OR a linked-list
*
* @tests traverse
* @types traverse
*
* @example
*
* new Traverse([1])
* new Traverse([], {})
*
*/
function Traverse(iteratee, config) {
// always cleared when done anyway
if (isUndefined(this.parents)) this.parents = new Set();
this.node = iteratee;
this.parent = iteratee;
this.root = iteratee;
this.reset(); // to pass in the events (as commented below) without messing up scope?
// if (config.on) this.on = config.on
return this;
}
/**
* @desc reset the properties when finished pooling or instantiating
* @since 5.0.0
* @method
*
* @memberOf Traverse
* @modifies Traverse.path
* @modifies Traverse.key
* @modifies Traverse.isAlive
* @modifies Traverse.isCircular
* @modifies Traverse.isLeaf
* @modifies Traverse.isRoot
* @modifies Traverse.depth
* @return {void}
*
* @example
* traverse([]).reset()
*/
Traverse.prototype.reset = function () {
this.path = [];
this.key = undefined;
this.isAlive = true;
this.isCircular = false;
this.isLeaf = false;
this.isRoot = true; // iterates +1 so start at 0
this.depth = -1;
};
/**
* @desc find parent,
* is there a parent
* above the current depth
* with the same value,
* making it circular?
*
* @memberOf Traverse
* @since 5.0.0
* @private
* @method
*
* @param {number} depth current depth, to find parent >=
* @param {parent} value parent value to find
* @return {boolean} hasParent
*
* @example
*
* var obj = {eh: ['eh']}
* traverse(obj).addParent(0, obj)
*
*/
Traverse.prototype.hasParent = function (depth, value) {
// or array
return isObj(value) ? this.parents.has(value) : false;
};
/**
* @desc add parent, to prevent circular iterations
* @memberOf Traverse
* @since 5.0.0
* @private
* @method
*
* @param {number} depth current depth, to add parent to >=
* @param {parent} value parent value to add
* @return {void}
*
* @example
*
* var obj = {eh: ['eh']}
* traverse(obj).addParent(0, obj)
*
*/
Traverse.prototype.addParent = function (depth, value) {
// && this.hasParent(value) === false
if (isObj(value)) this.parents.add(value);
};
/**
* @desc remove all parents, reset the map
*
* @memberOf Traverse
* @since 5.0.0
* @private
* @method
*
* @return {void}
*
* @example
*
* var obj = {eh: ['eh']}
* traverse(obj).forEach((key, value, t) => {
* t.parents
* //=> Set([obj])
* t.clear()
* t.parents
* //=> Set[]
* })
*
*/
Traverse.prototype.clear = function () {
// if (!isUndefined(this.parents))
this.parents.clear();
};
/**
* @memberOf Traverse
* @since 5.0.0
* @private
* @method
*
* @param {number} depth current depth, to find parents >=
* @param {parent} value parent value to remove
* @return {void}
*
* @example
*
* var obj = {eh: ['eh']}
* traverse(obj).removeParent(0, obj)
*
*/
Traverse.prototype.removeParent = function (depth, value) {
this.parents.delete(value);
};
/**
* @desc this is the main usage of Traverse
* @memberOf Traverse
* @since 3.0.0
* @version 5.0.0
* @method
*
* @param {Function} cb callback for each iteration
* @return {*} mapped result or original value, depends how it is used
*
* @example
*
* traverse([1, 2, 3]).forEach((key, value) => console.log({[key]: value}))
* //=> {'0': 1}
* //=> {'1': 2}
* //=> {'2': 3}
*
*/
Traverse.prototype.forEach = function iterateForEach(cb) {
/* istanbul ignore next: dev */
if (ENV_DEBUG) {
console.log('\n forEach \n');
}
const result = this.iterate(cb); // TODO: HERE, WHEN THIS IS ADDED, CAN BREAK SOME TESTS? SCOPED PARENTS MAP?
Traverse.release(this);
return result;
};
/**
* @desc stop the iteration
* @modifies this.isAlive = false
* @memberOf Traverse
* @method
*
* @return {void}
*
* @example
*
* traverse({eh: true, arr: []}).forEach((key, val, t) => {
* if (isArray(val)) this.stop()
* })
*
*/
Traverse.prototype.stop = function stop() {
this.isAlive = false; // this.release()
};
/**
* @TODO skip 1 branch
* @version 5.0.0
* @since 3.0.0
* @memberOf Traverse
* @method
*
* @return {void}
*
* @example
*
* traverse([1, 2, 3, [4]]).forEach((key, val, t) => {
* if (isArray(val)) t.skip()
* })
*
*/
Traverse.prototype.skip = function skip() {
this.skipBranch = true;
};
/* prettier-ignore */
/**
* @desc checks whether a node is iteratable
* @modifies Traverse.isIteratable
* @modifies Traverse.isLeaf
* @modifies Traverse.isCircular
*
* @memberOf Traverse
* @protected
* @method
*
* @param {*} node value to check
* @return {void}
*
* @TODO move into the wrapper? if perf allows?
*
* @example
*
* .checkIteratable({eh: true})
* //=> this.isLeaf = false
* //=> this.isCircular = false
* //=> this.isIteratable = true
*
* .checkIteratable({} || [])
* //=> this.isLeaf = true
* //=> this.isCircular = false
* //=> this.isIteratable = false
*
* var circular = {}
* circular.circular = circular
* .checkIteratable(circular)
* //=> this.isLeaf = false
* //=> this.isCircular = true
* //=> this.isIteratable = true
*
*/
Traverse.prototype.checkIteratable = function check(node) {
this.isIteratable = isIteratable(node); // just put these as an array?
if (isTrue(this.isIteratable)) {
// native = leaf if not root
this.isLeaf = false;
const path = this.path.join('.');
if (this.hasParent(path, node)) {
/* istanbul ignore next: dev */
if (ENV_DEBUG) {
console.log('circular___________', {
node,
path: this.path
});
}
this.isCircular = true;
} else {
this.addParent(path, node);
this.isCircular = false;
}
/* istanbul ignore next: dev */
if (ENV_DEBUG) {// console.log('IS_CIRCULAR_JSON', isCircular(node), this.isCircular, node)
}
} else {
this.isLeaf = true;
this.isCircular = false;
}
};
/* prettier-ignore */
/**
* Remove the current element from the output.
* If the node is in an Array it will be spliced off.
* Otherwise it will be deleted from its parent.
*
* @memberOf Traverse
* @version 5.0.0
* @since 2.0.0
* @method
*
* @param {undefined | Object} [arg] optional obj to use, defaults to this.node
* @return {void}
*
* @example
*
* traverse([0]).forEach((key, val, it) => it.remove())
* //=> []
*
* traverse({eh: true}).forEach((key, val, it) => it.remove())
* //=> {}
*
* traverse({eh: true, str: 'stringy'}).forEach((key, val, it) => {
* if (!isString(val)) it.remove()
* })
* //=> {str: 'stringy'}
*
*/
Traverse.prototype.remove = function removes(arg) {
// ignore undefined & non-object/arrays
if (isUndefined(this.key)) return;
let obj = arg || this.node;
if (!isObj(obj)) return;
/* istanbul ignore next: dev */
if (ENV_DEBUG) {
console.log('remove');
console.log({
parent: this.parent,
key: this.key,
obj
});
}
this.removeParent(obj);
this.skip();
delete obj[this.key];
delete this.parent[this.key];
/* istanbul ignore next: dev */
if (ENV_DEBUG) {
console.log('traverse:remove:', this.key, {
obj,
iteratee: this.node
});
}
};
/**
* @desc update the value for the current key
* @version 5.0.0
* @since 2.0.0
* @memberOf Traverse
*
* @param {*} value this.node[this.key] = value
* @return {void}
*
* @example
*
* traverse({eh: true})
* .forEach((key, val, traverser) => {
* if (this.isRoot) return
* traverser.update(false)
* })
* //=> {eh: false}
*
*/
Traverse.prototype.update = function update(value) {
dotSet(this.root, this.path, value);
};
/**
* @desc mark the iteration as done, clear the map
* @NOTE this recycles the instance in the pooler to re-use allocated objects
* @memberOf Traverse
* @private
* @since 5.0.0
*
* @return {void}
*
* @see Traverse.iterate
*
* @example
*
* traverse([]).destructor()
*
*/
Traverse.prototype.destructor = function destructor() {
this.node = undefined;
this.parent = undefined;
this.reset();
this.clear();
};
/* prettier-ignore */
/**
* @TODO handler for Set & Map so they can be skipped or traversed, for example when cloning...
* @TODO add hook to add custom checking if isIteratable
* @TODO deal with .isRoot if needed
* @TODO examples with clone and stop
*
* @memberOf Traverse
* @protected
* @sig on(key: null | Primitive, val: any, instance: Traverse): any
*
* @param {Function} on callback fn for each iteration
* @return {*} this.node
*
* @example
*
* iterate([])
* //=> []
* //=> on(null, [])
*
* @example
*
* iterate([1])
* //=> [1]
* //=> on(null, [1])
* //=> on('1', 1)
*
* @example
*
* //primitive - same for any number, string, symbol, null, undefined
* iterate(Symbol('eh'))
* //=> Symbol('eh')
* //=> on(Symbol('eh'))
*
* @example
*
* var deeper = {eh: 'canada', arr: [{moose: true}, 0]}
* iterate(deeper)
* //=> deeper // returns
* //=> on(null, deeper, this) // root
*
* //=> on('eh', 'canada', this) // 1st branch
*
* //=> on('arr', [{moose: true}, 0], this)
* //=> on('arr.0', [{moose: true}], this)
* //=> on('arr.0.moose', true, this)
* //=> on('arr.1', [0], this)
*
*
*/
Traverse.prototype.iterate = function iterate(on) {
/* istanbul ignore next : dev */
if (ENV_DEBUG) {
// require('fliplog')
// .bold(this.path.join('.'))
// .data(parents.keys())
// .echo()
console.log('\n...iterate...\n');
}
if (this.isAlive === false) {
/* istanbul ignore next : dev */
if (ENV_DEBUG) {
console.log('DEAD');
}
return Traverse.release(this);
}
let node = this.node; // convert to iteratable
if (isMap(node)) {
node = reduce(node);
} else if (isSet(node)) {
node = toarr(node);
} // @TODO: maybe only right before sub-loop
this.addParent(this.depth, node);
const nodeIsArray = isArray(node);
const nodeIsObj = nodeIsArray || isObj(node); // ---
// @event
if (!isUndefined(this.onBefore)) {
// eslint-disable-next-line no-useless-call
this.onBefore(this);
}
/* istanbul ignore next : dev */
if (ENV_DEBUG) {
// const str = require('pretty-format')({nodeIsObj, nodeIsArray, node})
// require('fliplog').verbose(1).data({nodeIsObj, nodeIsArray, node}).echo()
// console.log(node, parents)
// console.log(str)
console.log({
nodeIsObj,
nodeIsArray,
node
});
}
/**
* call as root, helpful when we
* - iterate something with no keys
* - iterate a non-iteratable (symbol, error, native, promise, etc)
*/
if (isTrue(this.isRoot)) {
on.call(this, null, node, this);
this.isRoot = false;
}
const isObjOrArray = nodeIsArray || nodeIsObj; // if (isObjOrArray) {
// // @event
// if (!isUndefined(this.onBefore)) {
// // eslint-disable-next-line no-useless-call
// this.onBefore(this)
// }
// }
// --------------------
// IF OBJWITHOUTKEYS, IF ARRAY WITHOUT LENGTH...
if (isObjOrArray && isEmpty(node)) {
on.call(this, this.key, node, this);
this.node = node;
} // --------------------
else if (isObjOrArray) {
this.depth = this.path.length; // @TODO SAFETY WITH `props(node)` <- fixes Error
let keys = nodeIsArray ? node : ObjectKeys(node);
/* istanbul ignore next : dev */
if (ENV_DEBUG) {
console.log({
keys
}); // require('fliplog').verbose(1).data(this).echo()
} // @event
// if (!isUndefined(this.onBefore)) this.onBefore()
// @NOTE: safety here
// this.checkIteratable(node)
// const last = keys[keys.length - 1]
// @loop
for (let key = 0; key < keys.length; key++) {
// if (ENV_DEBUG)
// console.log('iterating:', {key})
// --- safety ---
if (this.isAlive === false) {
/* istanbul ignore next : dev */
if (ENV_DEBUG) {
console.log('DEAD');
}
return Traverse.release(this);
} // @NOTE: look above add prev add parent
// addParent(this.depth, node)
// ----- setup our data ----
// to make it deletable
if (node !== this.node) this.parent = node;
this.key = nodeIsArray ? key : keys[key]; // this.isLast = key === last
/* istanbul ignore next: dev */
if (ENV_DEBUG) {
console.log('alive', this.key);
} // @event
if (!isUndefined(this.onPre)) {
// eslint-disable-next-line no-useless-call
this.onPre(this);
}
const value = node[this.key];
this.checkIteratable(value); // addParent(value)
const pathBeforeNesting = this.path.slice(0); // @NOTE: can go forward-backwards if this is after the nested iterating
this.path.push(this.key);
this.depth = this.path.length; // ----- continue events, loop deeper when needed ----
// @NOTE since we check isAlive at the beginning of each loop
// could use .skip alongisde .stop
// @TODO @IMPORTANT @HACK @FIXME right here it should also handle the .stop
on.call(this, this.key, value, this);
if (isTrue(this.skipBranch)) {
this.skipBranch = false;
continue;
}
/* istanbul ignore next: dev */
if (ENV_DEBUG) {} // require('fliplog').data(parents).echo()
// require('fliplog').data(this).echo()
// handle data
if (isTrue(this.isCircular)) {
/* istanbul ignore next: dev */
if (ENV_DEBUG) {
console.log('(((circular)))', this.key);
} // on.call(this, this.key, value, this)
// this.path.pop()
this.path = pathBeforeNesting; // this.isCircular = false
// break
continue; // return
} // &&
if (isTrue(this.isIteratable)) {
/* istanbul ignore next: dev */
if (ENV_DEBUG) {
console.log('(((iteratable)))', this.key);
}
this.node = value;
this.iterate(on);
this.path = pathBeforeNesting;
}
/* istanbul ignore next: dev */
if (ENV_DEBUG) {
if (this.isIteratable === false) {
console.log('not iteratable', this.key);
}
console.log('----------------- post ----------', node);
} // @event
if (!isUndefined(this.onPost)) {
// eslint-disable-next-line no-useless-call
this.onPost(this);
} // cleanup, backup 1 level
this.path.pop();
this.removeParent(node);
} // this.path.pop()
this.depth = this.path.length;
} else {
// this.isLast = false
on.call(this, this.depth, node, this);
} // @NOTE: careful
// removeParent(node)
// @NOTE: just for .after ?
this.node = node; // @event
if (!isUndefined(this.onAfter)) {
// eslint-disable-next-line no-useless-call
this.onAfter(this);
}
this.path.pop();
return this.node;
}; // is smaller, but probably slower
// function onEvent(property) {
// return function(fn) {
// this[property] = function
// }
// }
// when it's some sort of itertable object, loop it further
// @TODO: need to handle these better without totally messing with bad scope
Traverse.prototype.pre = function (fn) {
this.onPre = fn;
};
Traverse.prototype.post = function (fn) {
this.onPost = fn;
};
Traverse.prototype.before = function (fn) {
this.onBefore = fn;
};
Traverse.prototype.after = function (fn) {
this.onAfter = fn;
}; // -----------------------
/**
* @TODO merge with dopemerge?
* @TODO needs tests converted back for this (observe tests do cover somewhat)
*
* @param {*} arg defaults to this.node
* @return {*} cloned
*
* @example
*
* var obj = {}
* var cloned = traverse().clone(obj)
* obj.eh = true
* eq(obj, cloned)
* //=> false
*
*/
Traverse.prototype.clone = clone;
/**
* @todo ugh, how to clone better with *recursive* objects?
* @param {any} src wip
* @return {any} wip
*/
Traverse.prototype.copy = copy;
/**
* @desc clone any value
* @version 5.0.0
* @since 4.0.0
* @memberOf Traverse
* @extends copy
* @extends Traverse
*
* @param {*} arg argument to clone
* @return {*} cloned value
*
* {@link http://underscorejs.org/#clone underscore-clone}
* @see {@link underscore-clone}
* @see dopemerge
*
* @example
*
* var obj = {eh: true}
* clone(obj) === obj //=> false
*
* var obj = {eh: true}
* var obj2 = clone(obj)
* obj.eh = false
* console.log(obj2.eh) //=> true
*
*/
function clone(arg) {
const obj = isUndefined(arg) ? this.node : arg;
if (isPrimitive(obj)) return obj;
let cloned = emptyTarget(obj);
let current = cloned;
traverse(obj).forEach((key, value, traverser) => {
// t.isRoot
if (isNull(key)) return;
let copied = copy(value);
if (traverser.isCircular && isArray(value)) copied = value.slice(0);
dotSet(current, traverser.path, copied);
});
return cloned;
} // @TODO could just have traverse = Traverse.getPooled ?
addPoolingTo(Traverse);
function traverse(value) {
return Traverse.getPooled(value);
}
traverse.eq = eq(traverse);
traverse.clone = clone;
traverse.copy = copy;
module.exports = traverse;