-
Notifications
You must be signed in to change notification settings - Fork 3
/
index.js
281 lines (243 loc) · 10 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
// Copyright (c) 2014 Patrick Dubroy <[email protected]>
// 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);