'use strict';
/* @module traversal */
var exists = require('101/exists');
var debug = require('debug');
var Recur = require('./recur.js');
var warning = debug('traversal:warning');
/**
* @class A tree traversal.
* @author Ryan Sandor Richards
*/
function TreeTraversal() {
this.visitors = {};
this.visitor = function() {};
this.preorderProperties = [];
this.postorderProperties = [];
}
/**
* Given a traversal and a node, this method find the appropriate
* visitor for the node.
* @private
* @param {TreeTraversal} traversal Traversal the contains the
* visitor.
* @param {Object} node Node for which to find the visitor.
* @return {TreeTraversal~visitorCallback} The visitor for the node
* or the default visitor if no, more specific, one could be found.
*/
function findVisitor(traversal, node) {
var visitor = traversal.visitor;
for (var propertyName in traversal.visitors) {
if (!node.hasOwnProperty(propertyName)) {
continue;
}
var value = node[propertyName];
var propertyHandler = traversal.visitors[propertyName][value];
if (exists(propertyHandler)) {
visitor = propertyHandler;
break;
}
}
return visitor;
}
/**
* Treats a given array as a set and adds a value only if
* the value doesn't exist in the array.
* @param {Array} set Set to modify
* @param {*} value Value to add to the set.
*/
function addToSet(set, value) {
if (~set.indexOf(value)) {
return;
}
set.push(value);
}
/**
* Adds all values to a set.
* @see {@link addToSet}
* @param {Array} set Set to modify.
* @param {Array} values Values to add to the set.
*/
function allToSet(set, values) {
var flat = [].concat.apply([], values);
flat.forEach(function(value) {
addToSet(set, value);
});
}
// Public methods
/**
* Defines a new visitor that only applies to nodes that have a given
* property set to the given value. If `node` is the node currently
* being visited in the traversal, then this will only apply if
* `node[property] === value`.
*
* @example
* // Add a visitor for all `node.type === 'number'`
* traversal().property('type', 'number', function(node, recur) {
* // Do something with the `node` and possibly `recur` on its
* // children.
* });
*
* @param {string} key Key the node must have.
* @param {string} value Value the node must have at the given key.
* @param {TreeTraversal~visitorCallback} visitor Visitor callback
* to apply when `node[key] === value`.
* @return {TreeTraversal} This tree traversal (for chaining).
*/
TreeTraversal.prototype.property = function(key, value, visitor) {
if (!exists(this.visitors[key])) {
this.visitors[key] = {};
}
this.visitors[key][value] = visitor;
return this;
};
/**
* Sets the default visitor for the traversal.
* @param {TreeTraversal~visitorCallback} visitor Default visitor to set.
* @return {TreeTraversal} This tree traversal (for chaining).
*/
TreeTraversal.prototype.visit = function(visitor) {
this.visitor = visitor;
return this;
};
/**
* Adds a node property name helper to the traversal. Note that
* property names that correspond to methods on the traversal
* will be ignored.
*
* @example
* // Create a couple property helpers for the traversal
* var myTraversal = traversal()
* .addPropertyHelper('name')
* .addPropertyHelper('coolness')
* // Use it to quickly handle special cases
* myTraversal
* .name('ryan', function() { console.log('Ryan found'); })
* .name('ryan', function() { console.log('Airiel found'); })
* .name('nallely', function() { console.log('Nallely found'); })
* .coolness('totally', function() { console.log('Totally cool'); })
*
* @param {string} Property name helper to add.
* @return {TreeTraversal} This tree traversal (for chaining).
* @see {@link traverse} for usage via the factory method.
*/
TreeTraversal.prototype.addPropertyHelper = function(propertyName) {
if (exists(this[propertyName])) {
warning('Cannot create helper "' + propertyName + '", method already exists.');
return;
}
this[propertyName] = function(value, visitor) {
return this.property(propertyName, value, visitor);
};
return this;
};
/**
* Adds node property names to the traversal that should
* be recursively traversed *after* the node has been visited.
* @param {...(string|string[])} propertyName Node property
* names that, if exist, should be automatically traversed.
* @return {TreeTraversal} This tree traversal (for chaining).
*/
TreeTraversal.prototype.preorder = function() {
var names = Array.prototype.slice.call(arguments);
allToSet(this.preorderProperties, names);
return this;
};
/**
* Adds node property names to the traversal that should
* be recursively traversed *before* the node has been visited.
* @param {...(string|string[])} propertyName Node property
* names that, if exist, should be automatically traversed.
* @return {TreeTraversal} This tree traversal (for chaining).
*/
TreeTraversal.prototype.postorder = function() {
var names = Array.prototype.slice.call(arguments);
allToSet(this.postorderProperties, names);
return this;
};
/**
* Perform the traversal on a given node.
* @param {Object} node Root node to traverse.
* @param {Number} [depth] Current depth of the traversal.
*/
TreeTraversal.prototype.walk = function(node, depth) {
if (!depth) {
depth = 0;
}
var visitor = findVisitor(this, node);
var recur = new Recur(this, node, depth);
// TODO Going to need a way to turn this off as a special case.
this.postorderProperties.forEach(function(name) {
if (exists(node[name])) {
recur(node[name], depth);
}
});
var result = visitor.call(this, node, recur, depth);
if (recur.performAutoTraversal) {
this.preorderProperties.forEach(function(name) {
if (exists(node[name])) {
if (Array.isArray(node[name])) {
recur.each(node[name], depth+1);
}
else {
recur(node[name], depth+1);
}
}
});
}
return result;
};
/**
* Alias for `walk`.
* @see {@link walk}
*/
TreeTraversal.prototype.traverse = TreeTraversal.prototype.walk;
/**
* Alias for `walk`.
* @see {@link walk}
*/
TreeTraversal.prototype.run = TreeTraversal.prototype.walk;
/**
* Factory method for creating new tree traversals.
* This is the only method exposed via the exports.
*
* @example
* // Basic usage
* traversal()
* // Handle nodes with `.name === 'wowza'`
* .addHandler('name', 'wowza', function(node, recur) {})
*
* // Handle nodes with `.name === 'gene'`
* .addHandler('name', 'gene', function(node, recur) {})
*
* // Handle nodes with `.value === 42`
* .addHandler('value', 42, function(node, recur) {})
*
* // Run the traversal on a root node
* .run(rootNode);
*
* @example
* // Add helper method to make thing faster
* traversal(['name', 'value'])
* .name('wowza', function(node, recur) {})
* .name('gene', function(node, recur) {})
* .value(42, function(node, recur) {})
* .run(rootNode);
*
* @param {Array} helper List of node properties for which
* to add helper methods to the tree traversal.
* @return {TreeTraversal} a new tree traversal.
*/
function traverse(helpers) {
var traversal = new TreeTraversal();
if (exists(helpers) && Array.isArray(helpers)) {
helpers.forEach(function(propertyName) {
traversal.addHelper(propertyName);
});
}
return traversal;
}
// Callback descriptions
/**
* Performs operations on a given node during a tree traversal.
* @callback TreeTraversal~visitorCallback
* @param {Object} node Node currently being visited during the traversal.
* @param {TreeTraversal~recur} recur Continues the traversal on a given node.
* @param {Number} depth Current depth of the traversal.
*/
// Export the factory method.
module.exports = traverse;