tree-morph
Version:
Agnostic tree morphing library.
596 lines (507 loc) • 13.1 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.treeMorph = factory());
}(this, (function () { 'use strict';
/**
* A traversal context.
*
* Four operations are available. Note that depending on the traversal order, some operations have
* no effects.
*
* @param {Flags} flags
* @param {Cursor} cursor
*/
function Context(flags, cursor) {
this.flags = flags;
this.cursor = cursor;
}
Context.prototype = {
/**
* Skip current node, children won't be visited.
*
* @example
* crawl(root, (node, context) => {
* if ('foo' === node.type) {
* context.skip()
* }
* })
*/
skip() {
this.flags.skip = true;
},
/**
* Stop traversal now.
*
* @example
* crawl(root, (node, context) => {
* if ('foo' === node.type) {
* context.break()
* }
* })
*/
break() {
this.flags.break = true;
},
/**
* Notifies that the current node has been removed, children won't be visited.
*
* Because `tree-crawl` has no idea about the intrinsic structure of your tree, you have to
* remove the node yourself. `Context#remove` only notifies the traversal code that the structure
* of the tree has changed.
*
* @example
* crawl(root, (node, context) => {
* if ('foo' === node.type) {
* context.parent.children.splice(context.index, 1)
* context.remove()
* }
* })
*/
remove() {
this.flags.remove = true;
},
/**
* Notifies that the current node has been replaced, the new node's children will be visited
* instead.
*
* Because `tree-crawl` has no idea about the intrinsic structure of your tree, you have to
* replace the node yourself. `Context#replace` notifies the traversal code that the structure of
* the tree has changed.
*
* @param {Object} node Replacement node.
*
* @example
* crawl(root, (node, context) => {
* if ('foo' === node.type) {
* const node = {
* type: 'new node',
* children: [
* { type: 'new leaf' }
* ]
* }
* context.parent.children[context.index] = node
* context.replace(node)
* }
* })
*/
replace(node) {
this.flags.replace = node;
},
/**
* Get the parent of the current node.
*
* @return {Object} Parent node.
*/
get parent() {
return this.cursor.parent
},
/**
* Get the **depth** of the current node. The depth is the number of ancestors the current node
* has.
*
* @return {Number} Depth.
*/
get depth() {
return this.cursor.depth
},
/**
* Get the **level** of current node. The level is the number of ancestors+1 the current node has.
*
* @return {Number} Level.
*/
get level() {
return (this.cursor.depth + 1)
},
/**
* Get the index of the current node.
*
* @return {Number} Node's index.
*/
get index() {
return this.cursor.index
}
};
function ContextFactory(flags, cursor) {
return new Context(flags, cursor)
}
function Stack(initial) {
this.xs = [initial];
this.top = 0;
}
Stack.prototype = {
push(x) {
this.top++;
if (this.top < this.xs.length) {
this.xs[this.top] = x;
}
else {
this.xs.push(x);
}
},
pushArrayReverse(xs) {
for (let i = xs.length - 1; i >= 0; i--) {
this.push(xs[i]);
}
},
pop() {
const x = this.peek();
this.top--;
return x
},
peek() {
return this.xs[this.top]
},
isEmpty() {
return (-1 === this.top)
}
};
function QueueFactory(initial) {
return new Stack(initial)
}
function DfsCursor() {
this.depth = 0;
this.stack = QueueFactory({ node: null, index: -1 });
}
DfsCursor.prototype = {
moveDown(node) {
this.depth++;
this.stack.push({ node, index: 0 });
},
moveUp() {
this.depth--;
this.stack.pop();
},
moveNext() {
this.stack.peek().index++;
},
get parent() {
return this.stack.peek().node
},
get index() {
return this.stack.peek().index
}
};
function CursorFactory() {
return new DfsCursor()
}
function Flags() {
// perf: explicit hidden class, do not call reset
this.break = false;
this.skip = false;
this.remove = false;
this.replace = null;
}
Flags.prototype = {
reset() {
this.break = false;
this.skip = false;
this.remove = false;
this.replace = null;
}
};
function FlagsFactory() {
return new Flags()
}
function isNotEmpty(xs) {
return (xs && 0 !== xs.length)
}
function dfsPre(root, iteratee, getChildren) {
const flags = FlagsFactory();
const cursor = CursorFactory();
const context = ContextFactory(flags, cursor);
const stack = QueueFactory(root);
// perf: use same hidden class than root node in order to
// keep the stack monomorphic
const dummy = Object.assign({}, root);
while (!stack.isEmpty()) {
let node = stack.pop();
if (node === dummy) {
cursor.moveUp();
continue
}
flags.reset();
iteratee(node, context);
if (flags.break) break
if (flags.remove) continue
cursor.moveNext();
if (!flags.skip) {
if (flags.replace) {
node = flags.replace;
}
const children = getChildren(node);
if (isNotEmpty(children)) {
stack.push(dummy);
stack.pushArrayReverse(children);
cursor.moveDown(node);
}
}
}
}
function dfsPost(root, iteratee, getChildren) {
const flags = FlagsFactory();
const cursor = CursorFactory();
const context = ContextFactory(flags, cursor);
const stack = QueueFactory(root);
// perf: avoid bounds check deopt when calling Queue#peek later,
// instead we put an initial value
const ancestors = QueueFactory(null);
while (!stack.isEmpty()) {
const node = stack.peek();
const parent = ancestors.peek();
const children = getChildren(node);
flags.reset();
if (node === parent || !isNotEmpty(children)) {
if (node === parent) {
ancestors.pop();
cursor.moveUp();
}
stack.pop();
iteratee(node, context);
if (flags.break) break
if (flags.remove) continue
cursor.moveNext();
}
else {
ancestors.push(node);
cursor.moveDown(node);
stack.pushArrayReverse(children);
}
}
}
const THRESHOLD = 32768;
function Queue(initial) {
this.xs = [initial];
this.top = 0;
this.maxLength = 0;
}
Queue.prototype = {
enqueue(x) {
this.xs.push(x);
},
enqueueMultiple(xs) {
for (let i = 0, len = xs.length; i < len; i++) {
this.enqueue(xs[i]);
}
},
dequeue() {
const x = this.peek();
this.top++;
/* istanbul ignore next */
if (this.top === THRESHOLD) {
this.xs = this.xs.slice(this.top);
this.top = 0;
}
return x
},
peek() {
return this.xs[this.top]
},
isEmpty() {
return (this.top === this.xs.length)
}
};
function QueueFactory$1(initial) {
return new Queue(initial)
}
function BfsCursor() {
this.depth = 0;
this.index = -1;
this.queue = QueueFactory$1({ node: null, arity: 1 });
this.levelNodes = 1;
this.nextLevelNodes = 0;
}
BfsCursor.prototype = {
store(node, arity) {
this.queue.enqueue({ node, arity });
this.nextLevelNodes += arity;
},
moveNext() {
this.index++;
},
moveForward() {
this.queue.peek().arity--;
this.levelNodes--;
if (0 === this.queue.peek().arity) {
this.index = 0;
this.queue.dequeue();
}
if (0 === this.levelNodes) {
this.depth++;
this.levelNodes = this.nextLevelNodes;
this.nextLevelNodes = 0;
}
},
get parent() {
return this.queue.peek().node
}
};
function CursorFactory$1() {
return new BfsCursor()
}
function bfs(root, iteratee, getChildren) {
const flags = FlagsFactory();
const cursor = CursorFactory$1();
const context = ContextFactory(flags, cursor);
const queue = QueueFactory$1(root);
while (!queue.isEmpty()) {
let node = queue.dequeue();
flags.reset();
iteratee(node, context);
if (flags.break) break
if (!flags.remove) {
cursor.moveNext();
if (flags.replace) {
node = flags.replace;
}
if (!flags.skip) {
const children = getChildren(node);
if (isNotEmpty(children)) {
queue.enqueueMultiple(children);
cursor.store(node, children.length);
}
}
}
cursor.moveForward();
}
}
/**
* Walk options.
*
* @typedef {Object} Options
* @property {Function} [getChildren] Return a node's children.
* @property {'pre'|'post'|'bfs'} [order=pre] Order of the walk either in DFS pre or post order, or
* BFS.
*
* @example <caption>Traverse a DOM tree.</caption>
* crawl(document.body, doSomeStuff, { getChildren: node => node.childNodes })
*
* @example <caption>BFS traversal</caption>
* crawl(root, doSomeStuff, { order: 'bfs' })
*/
/**
* Called on each node of the tree.
* @callback Iteratee
* @param {Object} node Node being visited.
* @param {Context} context Traversal context
* @see [Traversal context](#traversal-context).
*/
const defaultGetChildren = (node) => node.children;
/**
* Walk a tree recursively.
*
* By default `getChildren` will return the `children` property of a node.
*
* @param {Object} root Root node of the tree to be walked.
* @param {Iteratee} iteratee Function invoked on each node.
* @param {Options} [options] Options customizing the walk.
*/
function crawl(root, iteratee, options) {
if (null == root) return
options = options || {};
// default options
const order = options.order || 'pre';
const getChildren = options.getChildren || defaultGetChildren;
// walk the tree!
if ('pre' === order) {
dfsPre(root, iteratee, getChildren);
}
else if ('post' === order) {
dfsPost(root, iteratee, getChildren);
}
else if ('bfs' === order) {
bfs(root, iteratee, getChildren);
}
}
/**
* Mutate node data.
*
* It treats the node atomically and create a deep clone of it.
* Structural properties should be left untouched and modified in the layout
* mutator instead.
* If `null` is returned, the node is marked as removed and processed by the
* layout mutator.
*
* @callback DataMutator
* @param {Object} node Node to be mutated.
* @param {Context} context Walk context.
* @return {Object|undefined|null} The node itself, nothing or `null`.
*/
/**
* Mutate node layout.
*
* It treats the node as a black box that has a position in the tree. It
* modifies its structural properties and may alter ancestors, siblings or
* descendants nodes.
*
* @callback LayoutMutator
* @param {Object|null} node Node to be mutated.
* @param {Object} parentNode Parent of the node to be mutated.
*/
/**
* Walk over an **immutable** tree and invoke **mutators** on each node.
*
* Mutators implements mutations at 2 different levels:
* - data level: mutate node data
* - layout level: mutate node layout
*
* @param {Object} root Root node of the tree.
* @param {DataMutator} dataMutator Mutate node data.
* @param {LayoutMutator} layoutMutator Mutate node layout.
* @return {Object} The mutated tree.
*/
function morph(root, dataMutator, layoutMutator) {
// both mutators are mandatory
if ('function' !== typeof dataMutator) {
throw new TypeError('dataMutator is not a function');
}
if ('function' !== typeof layoutMutator) {
throw new TypeError('layoutMutator is not a function');
}
var newRoot = null,
newPath = [];
crawl(root, function (node, context) {
// mutate node data
var newNode = dataMutator(node, context);
// get the current path item representing the potential parent of the
// current node
var parent = newPath[newPath.length - 1];
// special case for the first iteration as it's the root we are handling
if (undefined === parent) {
// if new node is not null, set it as the new root
if (null != newNode) {
newRoot = newNode;
}
// otherwize break as the whole tree has been discarded
else {
context.break();
return;
}
}
// standard case for other nodes deeper in the hierarchy
else {
// if new node is not null we apply a layout mutation
if (null != newNode) {
layoutMutator(newNode, parent.node);
}
// otherwize it is discarded
else {
context.skip();
}
// decrement parent TTL, if it reaches zero all the children have been
// added and we go up in the hierarchy
parent.ttl--;
if (0 === parent.ttl) {
newPath.pop();
}
}
// when all conditions are met, push new node in the path storing how many
// children may be added as its TTL (Time To Live)
if (newNode && node.children && 0 !== node.children.length && !context.flags.skip) {
newPath.push({ node: newNode, ttl: node.children.length });
}
});
return newRoot;
}
return morph;
})));