You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
322 lines
8.9 KiB
322 lines
8.9 KiB
module.exports = Traverse; |
|
function Traverse (obj) { |
|
if (!(this instanceof Traverse)) return new Traverse(obj); |
|
this.value = obj; |
|
} |
|
|
|
Traverse.prototype.get = function (ps) { |
|
var node = this.value; |
|
for (var i = 0; i < ps.length; i ++) { |
|
var key = ps[i]; |
|
if (!Object.hasOwnProperty.call(node, key)) { |
|
node = undefined; |
|
break; |
|
} |
|
node = node[key]; |
|
} |
|
return node; |
|
}; |
|
|
|
Traverse.prototype.set = function (ps, value) { |
|
var node = this.value; |
|
for (var i = 0; i < ps.length - 1; i ++) { |
|
var key = ps[i]; |
|
if (!Object.hasOwnProperty.call(node, key)) node[key] = {}; |
|
node = node[key]; |
|
} |
|
node[ps[i]] = value; |
|
return value; |
|
}; |
|
|
|
Traverse.prototype.map = function (cb) { |
|
return walk(this.value, cb, true); |
|
}; |
|
|
|
Traverse.prototype.forEach = function (cb) { |
|
this.value = walk(this.value, cb, false); |
|
return this.value; |
|
}; |
|
|
|
Traverse.prototype.reduce = function (cb, init) { |
|
var skip = arguments.length === 1; |
|
var acc = skip ? this.value : init; |
|
this.forEach(function (x) { |
|
if (!this.isRoot || !skip) { |
|
acc = cb.call(this, acc, x); |
|
} |
|
}); |
|
return acc; |
|
}; |
|
|
|
Traverse.prototype.deepEqual = function (obj) { |
|
if (arguments.length !== 1) { |
|
throw new Error( |
|
'deepEqual requires exactly one object to compare against' |
|
); |
|
} |
|
|
|
var equal = true; |
|
var node = obj; |
|
|
|
this.forEach(function (y) { |
|
var notEqual = (function () { |
|
equal = false; |
|
//this.stop(); |
|
return undefined; |
|
}).bind(this); |
|
|
|
//if (node === undefined || node === null) return notEqual(); |
|
|
|
if (!this.isRoot) { |
|
/* |
|
if (!Object.hasOwnProperty.call(node, this.key)) { |
|
return notEqual(); |
|
} |
|
*/ |
|
if (typeof node !== 'object') return notEqual(); |
|
node = node[this.key]; |
|
} |
|
|
|
var x = node; |
|
|
|
this.post(function () { |
|
node = x; |
|
}); |
|
|
|
var toS = function (o) { |
|
return Object.prototype.toString.call(o); |
|
}; |
|
|
|
if (this.circular) { |
|
if (Traverse(obj).get(this.circular.path) !== x) notEqual(); |
|
} |
|
else if (typeof x !== typeof y) { |
|
notEqual(); |
|
} |
|
else if (x === null || y === null || x === undefined || y === undefined) { |
|
if (x !== y) notEqual(); |
|
} |
|
else if (x.__proto__ !== y.__proto__) { |
|
notEqual(); |
|
} |
|
else if (x === y) { |
|
// nop |
|
} |
|
else if (typeof x === 'function') { |
|
if (x instanceof RegExp) { |
|
// both regexps on account of the __proto__ check |
|
if (x.toString() != y.toString()) notEqual(); |
|
} |
|
else if (x !== y) notEqual(); |
|
} |
|
else if (typeof x === 'object') { |
|
if (toS(y) === '[object Arguments]' |
|
|| toS(x) === '[object Arguments]') { |
|
if (toS(x) !== toS(y)) { |
|
notEqual(); |
|
} |
|
} |
|
else if (x instanceof Date || y instanceof Date) { |
|
if (!(x instanceof Date) || !(y instanceof Date) |
|
|| x.getTime() !== y.getTime()) { |
|
notEqual(); |
|
} |
|
} |
|
else { |
|
var kx = Object.keys(x); |
|
var ky = Object.keys(y); |
|
if (kx.length !== ky.length) return notEqual(); |
|
for (var i = 0; i < kx.length; i++) { |
|
var k = kx[i]; |
|
if (!Object.hasOwnProperty.call(y, k)) { |
|
notEqual(); |
|
} |
|
} |
|
} |
|
} |
|
}); |
|
|
|
return equal; |
|
}; |
|
|
|
Traverse.prototype.paths = function () { |
|
var acc = []; |
|
this.forEach(function (x) { |
|
acc.push(this.path); |
|
}); |
|
return acc; |
|
}; |
|
|
|
Traverse.prototype.nodes = function () { |
|
var acc = []; |
|
this.forEach(function (x) { |
|
acc.push(this.node); |
|
}); |
|
return acc; |
|
}; |
|
|
|
Traverse.prototype.clone = function () { |
|
var parents = [], nodes = []; |
|
|
|
return (function clone (src) { |
|
for (var i = 0; i < parents.length; i++) { |
|
if (parents[i] === src) { |
|
return nodes[i]; |
|
} |
|
} |
|
|
|
if (typeof src === 'object' && src !== null) { |
|
var dst = copy(src); |
|
|
|
parents.push(src); |
|
nodes.push(dst); |
|
|
|
Object.keys(src).forEach(function (key) { |
|
dst[key] = clone(src[key]); |
|
}); |
|
|
|
parents.pop(); |
|
nodes.pop(); |
|
return dst; |
|
} |
|
else { |
|
return src; |
|
} |
|
})(this.value); |
|
}; |
|
|
|
function walk (root, cb, immutable) { |
|
var path = []; |
|
var parents = []; |
|
var alive = true; |
|
|
|
return (function walker (node_) { |
|
var node = immutable ? copy(node_) : node_; |
|
var modifiers = {}; |
|
|
|
var state = { |
|
node : node, |
|
node_ : node_, |
|
path : [].concat(path), |
|
parent : parents.slice(-1)[0], |
|
key : path.slice(-1)[0], |
|
isRoot : path.length === 0, |
|
level : path.length, |
|
circular : null, |
|
update : function (x) { |
|
if (!state.isRoot) { |
|
state.parent.node[state.key] = x; |
|
} |
|
state.node = x; |
|
}, |
|
'delete' : function () { |
|
delete state.parent.node[state.key]; |
|
}, |
|
remove : function () { |
|
if (Array.isArray(state.parent.node)) { |
|
state.parent.node.splice(state.key, 1); |
|
} |
|
else { |
|
delete state.parent.node[state.key]; |
|
} |
|
}, |
|
before : function (f) { modifiers.before = f }, |
|
after : function (f) { modifiers.after = f }, |
|
pre : function (f) { modifiers.pre = f }, |
|
post : function (f) { modifiers.post = f }, |
|
stop : function () { alive = false } |
|
}; |
|
|
|
if (!alive) return state; |
|
|
|
if (typeof node === 'object' && node !== null) { |
|
state.isLeaf = Object.keys(node).length == 0; |
|
|
|
for (var i = 0; i < parents.length; i++) { |
|
if (parents[i].node_ === node_) { |
|
state.circular = parents[i]; |
|
break; |
|
} |
|
} |
|
} |
|
else { |
|
state.isLeaf = true; |
|
} |
|
|
|
state.notLeaf = !state.isLeaf; |
|
state.notRoot = !state.isRoot; |
|
|
|
// use return values to update if defined |
|
var ret = cb.call(state, state.node); |
|
if (ret !== undefined && state.update) state.update(ret); |
|
if (modifiers.before) modifiers.before.call(state, state.node); |
|
|
|
if (typeof state.node == 'object' |
|
&& state.node !== null && !state.circular) { |
|
parents.push(state); |
|
|
|
var keys = Object.keys(state.node); |
|
keys.forEach(function (key, i) { |
|
path.push(key); |
|
|
|
if (modifiers.pre) modifiers.pre.call(state, state.node[key], key); |
|
|
|
var child = walker(state.node[key]); |
|
if (immutable && Object.hasOwnProperty.call(state.node, key)) { |
|
state.node[key] = child.node; |
|
} |
|
|
|
child.isLast = i == keys.length - 1; |
|
child.isFirst = i == 0; |
|
|
|
if (modifiers.post) modifiers.post.call(state, child); |
|
|
|
path.pop(); |
|
}); |
|
parents.pop(); |
|
} |
|
|
|
if (modifiers.after) modifiers.after.call(state, state.node); |
|
|
|
return state; |
|
})(root).node; |
|
} |
|
|
|
Object.keys(Traverse.prototype).forEach(function (key) { |
|
Traverse[key] = function (obj) { |
|
var args = [].slice.call(arguments, 1); |
|
var t = Traverse(obj); |
|
return t[key].apply(t, args); |
|
}; |
|
}); |
|
|
|
function copy (src) { |
|
if (typeof src === 'object' && src !== null) { |
|
var dst; |
|
|
|
if (Array.isArray(src)) { |
|
dst = []; |
|
} |
|
else if (src instanceof Date) { |
|
dst = new Date(src); |
|
} |
|
else if (src instanceof Boolean) { |
|
dst = new Boolean(src); |
|
} |
|
else if (src instanceof Number) { |
|
dst = new Number(src); |
|
} |
|
else if (src instanceof String) { |
|
dst = new String(src); |
|
} |
|
else { |
|
dst = Object.create(Object.getPrototypeOf(src)); |
|
} |
|
|
|
Object.keys(src).forEach(function (key) { |
|
dst[key] = src[key]; |
|
}); |
|
return dst; |
|
} |
|
else return src; |
|
}
|
|
|