UNPKG

tree-walk

Version:

Helpers for traversing, inspecting, and transforming arbitrary trees.

282 lines (243 loc) 10.2 kB
// Copyright (c) 2014 Patrick Dubroy <pdubroy@gmail.com> // This software is distributed under the terms of the MIT License. 'use strict'; var extend = require('util-extend'), WeakMap = require('./third_party/WeakMap'); // eslint-disable-line no-undef,no-native-reassign // An internal object that can be returned from a visitor function to // prevent a top-down walk from walking subtrees of a node. var stopRecursion = {}; // An internal object that can be returned from a visitor function to // cause the walk to immediately stop. var stopWalk = {}; var hasOwnProp = Object.prototype.hasOwnProperty; // Helpers // ------- function isElement(obj) { return !!(obj && obj.nodeType === 1); } function isObject(obj) { var type = typeof obj; return type === 'function' || type === 'object' && !!obj; } function isString(obj) { return Object.prototype.toString.call(obj) === '[object String]'; } function each(obj, predicate) { for (var k in obj) { if (obj.hasOwnProperty(k)) { if (predicate(obj[k], k, obj)) return false; } } return true; } // Returns a copy of `obj` containing only the properties given by `keys`. function pick(obj, keys) { var result = {}; for (var i = 0, length = keys.length; i < length; i++) { var key = keys[i]; if (key in obj) result[key] = obj[key]; } return result; } // Makes a shallow copy of `arr`, and adds `obj` to the end of the copy. function copyAndPush(arr, obj) { var result = arr.slice(); result.push(obj); return result; } // Implements the default traversal strategy: if `obj` is a DOM node, walk // its DOM children; otherwise, walk all the objects it references. function defaultTraversal(obj) { return isElement(obj) ? obj.children : obj; } // Walk the tree recursively beginning with `root`, calling `beforeFunc` // before visiting an objects descendents, and `afterFunc` afterwards. // If `collectResults` is true, the last argument to `afterFunc` will be a // collection of the results of walking the node's subtrees. function walkImpl(root, traversalStrategy, beforeFunc, afterFunc, context, collectResults) { return (function _walk(stack, value, key, parent) { if (isObject(value) && stack.indexOf(value) >= 0) throw new TypeError('A cycle was detected at ' + value); if (beforeFunc) { var result = beforeFunc.call(context, value, key, parent); if (result === stopWalk) return stopWalk; if (result === stopRecursion) return; // eslint-disable-line consistent-return } var subResults; var target = traversalStrategy(value); if (isObject(target) && Object.keys(target).length > 0) { // Collect results from subtrees in the same shape as the target. if (collectResults) subResults = Array.isArray(target) ? [] : {}; var ok = each(target, function(obj, key) { var result = _walk(copyAndPush(stack, value), obj, key, value); if (result === stopWalk) return false; if (subResults) subResults[key] = result; }); if (!ok) return stopWalk; } if (afterFunc) return afterFunc.call(context, value, key, parent, subResults); })([], root); } // Internal helper providing the implementation for `pluck` and `pluckRec`. function pluck(obj, propertyName, recursive) { var results = []; this.preorder(obj, function(value, key) { if (!recursive && key === propertyName) return stopRecursion; if (hasOwnProp.call(value, propertyName)) results[results.length] = value[propertyName]; }); return results; } function defineEnumerableProperty(obj, propName, getterFn) { Object.defineProperty(obj, propName, { enumerable: true, get: getterFn }); } // Returns an object containing the walk functions. If `traversalStrategy` // is specified, it is a function determining how objects should be // traversed. Given an object, it returns the object to be recursively // walked. The default strategy is equivalent to `_.identity` for regular // objects, and for DOM nodes it returns the node's DOM children. function Walker(traversalStrategy) { if (!(this instanceof Walker)) return new Walker(traversalStrategy); // There are two different strategy shorthands: if a single string is // specified, treat the value of that property as the traversal target. // If an array is specified, the traversal target is the node itself, but // only the properties contained in the array will be traversed. if (isString(traversalStrategy)) { var prop = traversalStrategy; traversalStrategy = function(node) { if (isObject(node) && prop in node) return node[prop]; }; } else if (Array.isArray(traversalStrategy)) { var props = traversalStrategy; traversalStrategy = function(node) { if (isObject(node)) return pick(node, props); }; } this._traversalStrategy = traversalStrategy || defaultTraversal; } extend(Walker.prototype, { STOP_RECURSION: stopRecursion, // Performs a preorder traversal of `obj` and returns the first value // which passes a truth test. find: function(obj, visitor, context) { var result; this.preorder(obj, function(value, key, parent) { if (visitor.call(context, value, key, parent)) { result = value; return stopWalk; } }, context); return result; }, // Recursively traverses `obj` and returns all the elements that pass a // truth test. `strategy` is the traversal function to use, e.g. `preorder` // or `postorder`. filter: function(obj, strategy, visitor, context) { var results = []; if (obj === null) return results; strategy(obj, function(value, key, parent) { if (visitor.call(context, value, key, parent)) results.push(value); }, null, this._traversalStrategy); return results; }, // Recursively traverses `obj` and returns all the elements for which a // truth test fails. reject: function(obj, strategy, visitor, context) { return this.filter(obj, strategy, function(value, key, parent) { return !visitor.call(context, value, key, parent); }); }, // Produces a new array of values by recursively traversing `obj` and // mapping each value through the transformation function `visitor`. // `strategy` is the traversal function to use, e.g. `preorder` or // `postorder`. map: function(obj, strategy, visitor, context) { var results = []; strategy(obj, function(value, key, parent) { results[results.length] = visitor.call(context, value, key, parent); }, null, this._traversalStrategy); return results; }, // Return the value of properties named `propertyName` reachable from the // tree rooted at `obj`. Results are not recursively searched; use // `pluckRec` for that. pluck: function(obj, propertyName) { return pluck.call(this, obj, propertyName, false); }, // Version of `pluck` which recursively searches results for nested objects // with a property named `propertyName`. pluckRec: function(obj, propertyName) { return pluck.call(this, obj, propertyName, true); }, // Recursively traverses `obj` in a depth-first fashion, invoking the // `visitor` function for each object only after traversing its children. // `traversalStrategy` is intended for internal callers, and is not part // of the public API. postorder: function(obj, visitor, context, traversalStrategy) { traversalStrategy = traversalStrategy || this._traversalStrategy; walkImpl(obj, traversalStrategy, null, visitor, context); }, // Recursively traverses `obj` in a depth-first fashion, invoking the // `visitor` function for each object before traversing its children. // `traversalStrategy` is intended for internal callers, and is not part // of the public API. preorder: function(obj, visitor, context, traversalStrategy) { traversalStrategy = traversalStrategy || this._traversalStrategy; walkImpl(obj, traversalStrategy, visitor, null, context); }, // Builds up a single value by doing a post-order traversal of `obj` and // calling the `visitor` function on each object in the tree. For leaf // objects, the `memo` argument to `visitor` is the value of the `leafMemo` // argument to `reduce`. For non-leaf objects, `memo` is a collection of // the results of calling `reduce` on the object's children. reduce: function(obj, visitor, leafMemo, context) { var reducer = function(value, key, parent, subResults) { return visitor(subResults || leafMemo, value, key, parent); }; return walkImpl(obj, this._traversalStrategy, null, reducer, context, true); }, // An 'attribute' is a value that is calculated by invoking a visitor // function on a node. The first argument of the visitor is a collection // of the attribute values for the node's children. These are calculated // lazily -- in this way the visitor can decide in what order to visit the // subtrees. createAttribute: function(visitor, defaultValue, context) { var self = this; var memo = new WeakMap(); function _visit(stack, value, key, parent) { if (isObject(value) && stack.indexOf(value) >= 0) throw new TypeError('A cycle was detected at ' + value); if (memo.has(value)) return memo.get(value); var subResults; var target = self._traversalStrategy(value); if (isObject(target) && Object.keys(target).length > 0) { subResults = {}; each(target, function(child, k) { defineEnumerableProperty(subResults, k, function() { return _visit(copyAndPush(stack, value), child, k, value); }); }); } var result = visitor.call(context, subResults, value, key, parent); memo.set(value, result); return result; } return function(obj) { return _visit([], obj); }; } }); var WalkerProto = Walker.prototype; // Set up a few convenient aliases. WalkerProto.each = WalkerProto.preorder; WalkerProto.collect = WalkerProto.map; WalkerProto.detect = WalkerProto.find; WalkerProto.select = WalkerProto.filter; // Export the walker constructor, but make it behave like an instance. Walker._traversalStrategy = defaultTraversal; module.exports = extend(Walker, WalkerProto);