UNPKG

tree-morph

Version:
596 lines (507 loc) 13.1 kB
(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; })));