├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .nycrc ├── .reserved ├── .travis.yml ├── documentation.yml ├── index.d.ts ├── index.js ├── lib ├── bfs.js ├── dfs-post.js ├── dfs-pre.js └── internal │ ├── bfs-cursor.js │ ├── context.js │ ├── dfs-cursor.js │ ├── flags.js │ ├── queue.js │ ├── stack.js │ └── util.js ├── license ├── package.json ├── perf ├── bench.js ├── helpers │ ├── queue.js │ └── tree.js ├── profile.js ├── scripts │ └── run.js └── trace.js ├── readme.md ├── rollup.config.js ├── test ├── .eslintrc ├── bfs.js ├── dfs-post.js ├── dfs-pre.js ├── fixtures │ ├── custom-children.json │ ├── deep.json │ └── simple.json └── helpers │ └── fixtures.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env" 4 | ], 5 | "env": { 6 | "test": { 7 | "plugins": [ 8 | "istanbul" 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "ngryman" 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # project 2 | .nyc_output 3 | .todo 4 | code.asm 5 | hydrogen.cfg 6 | coverage/ 7 | dist/ 8 | 9 | # node 10 | node_modules/ 11 | npm-debug.log 12 | 13 | # OS 14 | .DS_Store 15 | Desktop.ini 16 | ._* 17 | Thumbs.db 18 | .Spotlight-V100 19 | .Trashes 20 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMap": false, 3 | "instrument": false 4 | } 5 | -------------------------------------------------------------------------------- /.reserved: -------------------------------------------------------------------------------- 1 | { 2 | "vars": [], 3 | "props": [ 4 | "exports", 5 | "amd", 6 | "prototype", 7 | "assign", 8 | "push", 9 | "slice", 10 | "length", 11 | "break", 12 | "skip", 13 | "remove", 14 | "replace", 15 | "parent", 16 | "depth", 17 | "level", 18 | "index", 19 | "order", 20 | "getChildren" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: yarn 4 | before_install: yarn global add greenkeeper-lockfile@1 5 | before_script: greenkeeper-lockfile-update 6 | after_script: greenkeeper-lockfile-upload 7 | after_success: yarn coverage 8 | branches: 9 | only: 10 | - master 11 | - /^greenkeeper/.*$/ 12 | notifications: 13 | email: false 14 | node_js: 15 | - 'stable' 16 | - 'lts/*' 17 | -------------------------------------------------------------------------------- /documentation.yml: -------------------------------------------------------------------------------- 1 | toc: 2 | - crawl 3 | - Iteratee 4 | - Options 5 | - Context 6 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Because this module exports a function `crawl` we have to define used types 2 | // in a namespace with the same name. 3 | // Found this workaround in DefinitelyTyped/DefinitelyTyped#12869. 4 | 5 | declare namespace crawl { 6 | /** 7 | * Walk options. 8 | */ 9 | export type Options = { 10 | /** 11 | * Return node's children. 12 | * 13 | * By default, node's `children` property is used. 14 | */ 15 | getChildren?(node: TreeNode): TreeNode[], 16 | /** 17 | * Order of the walk: either in DFS pre or post order, or BFS. 18 | */ 19 | order?: 'pre' | 'post' | 'bfs' 20 | } 21 | 22 | /** 23 | * A traversal context. 24 | * 25 | * Four operations are available. Note that depending on the traversal order, some operations have no effects. 26 | */ 27 | export type Context = { 28 | /** 29 | * Skip current node, children won't be visited. 30 | */ 31 | skip(): void, 32 | /** 33 | * Stop traversal now. 34 | */ 35 | break(): void, 36 | /** 37 | * Notifies that the current node has been removed, children won't be visited. 38 | * 39 | * Because `tree-crawl` has no idea about the intrinsic structure of your tree, you have to remove the node yourself. 40 | * `Context#remove` only notifies the traversal code that the structure of the tree has changed. 41 | */ 42 | remove(): void, 43 | /** 44 | * Notifies that the current node has been replaced, the new node's children will be visited instead. 45 | * 46 | * Because `tree-crawl` has no idea about the intrinsic structure of your tree, you have to replace the node yourself. 47 | * `Context#replace` notifies the traversal code that the structure of the tree has changed. 48 | * 49 | * @param node Replacement node. 50 | */ 51 | replace(node: TreeNode): void, 52 | /** 53 | * Get the parent of the current node. `null` for the root node. 54 | */ 55 | parent: TreeNode | null, 56 | /** 57 | * Get the depth of the current node. The depth is the number of ancestors the current node has. 58 | */ 59 | depth: number, 60 | /** 61 | * Get the level of current node. The level is the number of ancestors+1 the current node has. 62 | */ 63 | level: number, 64 | /** 65 | * Get the index of the current node. 66 | */ 67 | index: number 68 | } 69 | } 70 | 71 | /** 72 | * Walk a tree recursively. 73 | * 74 | * @param root Root node of the tree to be walked. 75 | * @param iteratee Function invoked on each node. 76 | * @param options Options customizing the walk. By default `getChildren` will return the `children` property of a node. 77 | */ 78 | declare function crawl( 79 | root: TreeNode, 80 | iteratee: (node: TreeNode, context: crawl.Context) => void, 81 | options?: crawl.Options 82 | ): void 83 | 84 | export = crawl 85 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import dfsPre from './lib/dfs-pre' 2 | import dfsPost from './lib/dfs-post' 3 | import bfs from './lib/bfs' 4 | 5 | /** 6 | * Walk options. 7 | * 8 | * @typedef {Object} Options 9 | * @property {Function} [getChildren] Return a node's children. 10 | * @property {'pre'|'post'|'bfs'} [order=pre] Order of the walk either in DFS pre or post order, or 11 | * BFS. 12 | * 13 | * @example Traverse a DOM tree. 14 | * crawl(document.body, doSomeStuff, { getChildren: node => node.childNodes }) 15 | * 16 | * @example BFS traversal 17 | * crawl(root, doSomeStuff, { order: 'bfs' }) 18 | */ 19 | 20 | /** 21 | * Called on each node of the tree. 22 | * @callback Iteratee 23 | * @param {Object} node Node being visited. 24 | * @param {Context} context Traversal context 25 | * @see [Traversal context](#traversal-context). 26 | */ 27 | 28 | const defaultGetChildren = (node) => node.children 29 | 30 | /** 31 | * Walk a tree recursively. 32 | * 33 | * By default `getChildren` will return the `children` property of a node. 34 | * 35 | * @param {Object} root Root node of the tree to be walked. 36 | * @param {Iteratee} iteratee Function invoked on each node. 37 | * @param {Options} [options] Options customizing the walk. 38 | */ 39 | export default function crawl(root, iteratee, options) { 40 | if (null == root) return 41 | 42 | options = options || {} 43 | 44 | // default options 45 | const order = options.order || 'pre' 46 | const getChildren = options.getChildren || defaultGetChildren 47 | 48 | // walk the tree! 49 | if ('pre' === order) { 50 | dfsPre(root, iteratee, getChildren) 51 | } 52 | else if ('post' === order) { 53 | dfsPost(root, iteratee, getChildren) 54 | } 55 | else if ('bfs' === order) { 56 | bfs(root, iteratee, getChildren) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/bfs.js: -------------------------------------------------------------------------------- 1 | import BfsCursor from './internal/bfs-cursor' 2 | import Context from './internal/context' 3 | import Flags from './internal/flags' 4 | import Queue from './internal/queue' 5 | import { 6 | isNotEmpty 7 | } from './internal/util' 8 | 9 | export default function bfs(root, iteratee, getChildren) { 10 | const flags = Flags() 11 | const cursor = BfsCursor() 12 | const context = Context(flags, cursor) 13 | const queue = Queue(root) 14 | 15 | while (!queue.isEmpty()) { 16 | let node = queue.dequeue() 17 | 18 | flags.reset() 19 | 20 | iteratee(node, context) 21 | 22 | if (flags.break) break 23 | 24 | if (!flags.remove) { 25 | cursor.moveNext() 26 | 27 | if (flags.replace) { 28 | node = flags.replace 29 | } 30 | 31 | if (!flags.skip) { 32 | const children = getChildren(node) 33 | if (isNotEmpty(children)) { 34 | queue.enqueueMultiple(children) 35 | cursor.store(node, children.length) 36 | } 37 | } 38 | } 39 | 40 | cursor.moveForward() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/dfs-post.js: -------------------------------------------------------------------------------- 1 | import Context from './internal/context' 2 | import DfsCursor from './internal/dfs-cursor' 3 | import Flags from './internal/flags' 4 | import Stack from './internal/stack' 5 | import { 6 | isNotEmpty 7 | } from './internal/util' 8 | 9 | export default function dfsPost(root, iteratee, getChildren) { 10 | const flags = Flags() 11 | const cursor = DfsCursor() 12 | const context = Context(flags, cursor) 13 | const stack = Stack(root) 14 | // perf: avoid bounds check deopt when calling Queue#peek later, 15 | // instead we put an initial value 16 | const ancestors = Stack(null) 17 | 18 | while (!stack.isEmpty()) { 19 | const node = stack.peek() 20 | const parent = ancestors.peek() 21 | const children = getChildren(node) 22 | 23 | flags.reset() 24 | 25 | if (node === parent || !isNotEmpty(children)) { 26 | if (node === parent) { 27 | ancestors.pop() 28 | cursor.moveUp() 29 | } 30 | 31 | stack.pop() 32 | 33 | iteratee(node, context) 34 | 35 | if (flags.break) break 36 | if (flags.remove) continue 37 | 38 | cursor.moveNext() 39 | } 40 | else { 41 | ancestors.push(node) 42 | cursor.moveDown(node) 43 | stack.pushArrayReverse(children) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/dfs-pre.js: -------------------------------------------------------------------------------- 1 | import Context from './internal/context' 2 | import DfsCursor from './internal/dfs-cursor' 3 | import Flags from './internal/flags' 4 | import Stack from './internal/stack' 5 | import { 6 | isNotEmpty 7 | } from './internal/util' 8 | 9 | export default function dfsPre(root, iteratee, getChildren) { 10 | const flags = Flags() 11 | const cursor = DfsCursor() 12 | const context = Context(flags, cursor) 13 | const stack = Stack(root) 14 | 15 | // perf: use same hidden class than root node in order to 16 | // keep the stack monomorphic 17 | const dummy = Object.assign({}, root) 18 | 19 | while (!stack.isEmpty()) { 20 | let node = stack.pop() 21 | 22 | if (node === dummy) { 23 | cursor.moveUp() 24 | continue 25 | } 26 | 27 | flags.reset() 28 | 29 | iteratee(node, context) 30 | 31 | if (flags.break) break 32 | if (flags.remove) continue 33 | 34 | cursor.moveNext() 35 | 36 | if (!flags.skip) { 37 | if (flags.replace) { 38 | node = flags.replace 39 | } 40 | 41 | const children = getChildren(node) 42 | if (isNotEmpty(children)) { 43 | stack.push(dummy) 44 | stack.pushArrayReverse(children) 45 | cursor.moveDown(node) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/internal/bfs-cursor.js: -------------------------------------------------------------------------------- 1 | import Queue from './queue' 2 | 3 | function BfsCursor() { 4 | this.depth = 0 5 | this.index = -1 6 | this.queue = Queue({ node: null, arity: 1 }) 7 | this.levelNodes = 1 8 | this.nextLevelNodes = 0 9 | } 10 | 11 | BfsCursor.prototype = { 12 | store(node, arity) { 13 | this.queue.enqueue({ node, arity }) 14 | this.nextLevelNodes += arity 15 | }, 16 | 17 | moveNext() { 18 | this.index++ 19 | }, 20 | 21 | moveForward() { 22 | this.queue.peek().arity-- 23 | this.levelNodes-- 24 | if (0 === this.queue.peek().arity) { 25 | this.index = 0 26 | this.queue.dequeue() 27 | } 28 | if (0 === this.levelNodes) { 29 | this.depth++ 30 | this.levelNodes = this.nextLevelNodes 31 | this.nextLevelNodes = 0 32 | } 33 | }, 34 | 35 | get parent() { 36 | return this.queue.peek().node 37 | } 38 | } 39 | 40 | export default function CursorFactory() { 41 | return new BfsCursor() 42 | } 43 | -------------------------------------------------------------------------------- /lib/internal/context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A traversal context. 3 | * 4 | * Four operations are available. Note that depending on the traversal order, some operations have 5 | * no effects. 6 | * 7 | * @param {Flags} flags 8 | * @param {Cursor} cursor 9 | */ 10 | function Context(flags, cursor) { 11 | this.flags = flags 12 | this.cursor = cursor 13 | } 14 | 15 | Context.prototype = { 16 | /** 17 | * Skip current node, children won't be visited. 18 | * 19 | * @example 20 | * crawl(root, (node, context) => { 21 | * if ('foo' === node.type) { 22 | * context.skip() 23 | * } 24 | * }) 25 | */ 26 | skip() { 27 | this.flags.skip = true 28 | }, 29 | 30 | /** 31 | * Stop traversal now. 32 | * 33 | * @example 34 | * crawl(root, (node, context) => { 35 | * if ('foo' === node.type) { 36 | * context.break() 37 | * } 38 | * }) 39 | */ 40 | break() { 41 | this.flags.break = true 42 | }, 43 | 44 | /** 45 | * Notifies that the current node has been removed, children won't be visited. 46 | * 47 | * Because `tree-crawl` has no idea about the intrinsic structure of your tree, you have to 48 | * remove the node yourself. `Context#remove` only notifies the traversal code that the structure 49 | * of the tree has changed. 50 | * 51 | * @example 52 | * crawl(root, (node, context) => { 53 | * if ('foo' === node.type) { 54 | * context.parent.children.splice(context.index, 1) 55 | * context.remove() 56 | * } 57 | * }) 58 | */ 59 | remove() { 60 | this.flags.remove = true 61 | }, 62 | 63 | /** 64 | * Notifies that the current node has been replaced, the new node's children will be visited 65 | * instead. 66 | * 67 | * Because `tree-crawl` has no idea about the intrinsic structure of your tree, you have to 68 | * replace the node yourself. `Context#replace` notifies the traversal code that the structure of 69 | * the tree has changed. 70 | * 71 | * @param {Object} node Replacement node. 72 | * 73 | * @example 74 | * crawl(root, (node, context) => { 75 | * if ('foo' === node.type) { 76 | * const node = { 77 | * type: 'new node', 78 | * children: [ 79 | * { type: 'new leaf' } 80 | * ] 81 | * } 82 | * context.parent.children[context.index] = node 83 | * context.replace(node) 84 | * } 85 | * }) 86 | */ 87 | replace(node) { 88 | this.flags.replace = node 89 | }, 90 | 91 | /** 92 | * Get the parent of the current node. 93 | * 94 | * @return {Object} Parent node. 95 | */ 96 | get parent() { 97 | return this.cursor.parent 98 | }, 99 | 100 | /** 101 | * Get the **depth** of the current node. The depth is the number of ancestors the current node 102 | * has. 103 | * 104 | * @return {Number} Depth. 105 | */ 106 | get depth() { 107 | return this.cursor.depth 108 | }, 109 | 110 | /** 111 | * Get the **level** of current node. The level is the number of ancestors+1 the current node has. 112 | * 113 | * @return {Number} Level. 114 | */ 115 | get level() { 116 | return (this.cursor.depth + 1) 117 | }, 118 | 119 | /** 120 | * Get the index of the current node. 121 | * 122 | * @return {Number} Node's index. 123 | */ 124 | get index() { 125 | return this.cursor.index 126 | } 127 | } 128 | 129 | export default function ContextFactory(flags, cursor) { 130 | return new Context(flags, cursor) 131 | } 132 | -------------------------------------------------------------------------------- /lib/internal/dfs-cursor.js: -------------------------------------------------------------------------------- 1 | import Stack from './stack' 2 | 3 | function DfsCursor() { 4 | this.depth = 0 5 | this.stack = Stack({ node: null, index: -1 }) 6 | } 7 | 8 | DfsCursor.prototype = { 9 | moveDown(node) { 10 | this.depth++ 11 | this.stack.push({ node, index: 0 }) 12 | }, 13 | 14 | moveUp() { 15 | this.depth-- 16 | this.stack.pop() 17 | }, 18 | 19 | moveNext() { 20 | this.stack.peek().index++ 21 | }, 22 | 23 | get parent() { 24 | return this.stack.peek().node 25 | }, 26 | 27 | get index() { 28 | return this.stack.peek().index 29 | } 30 | } 31 | 32 | export default function CursorFactory() { 33 | return new DfsCursor() 34 | } 35 | -------------------------------------------------------------------------------- /lib/internal/flags.js: -------------------------------------------------------------------------------- 1 | function Flags() { 2 | // perf: explicit hidden class, do not call reset 3 | this.break = false 4 | this.skip = false 5 | this.remove = false 6 | this.replace = null 7 | } 8 | 9 | Flags.prototype = { 10 | reset() { 11 | this.break = false 12 | this.skip = false 13 | this.remove = false 14 | this.replace = null 15 | } 16 | } 17 | 18 | export default function FlagsFactory() { 19 | return new Flags() 20 | } 21 | -------------------------------------------------------------------------------- /lib/internal/queue.js: -------------------------------------------------------------------------------- 1 | const THRESHOLD = 32768 2 | 3 | function Queue(initial) { 4 | this.xs = [initial] 5 | this.top = 0 6 | this.maxLength = 0 7 | } 8 | 9 | Queue.prototype = { 10 | enqueue(x) { 11 | this.xs.push(x) 12 | }, 13 | 14 | enqueueMultiple(xs) { 15 | for (let i = 0, len = xs.length; i < len; i++) { 16 | this.enqueue(xs[i]) 17 | } 18 | }, 19 | 20 | dequeue() { 21 | const x = this.peek() 22 | this.top++ 23 | /* istanbul ignore next */ 24 | if (this.top === THRESHOLD) { 25 | this.xs = this.xs.slice(this.top) 26 | this.top = 0 27 | } 28 | return x 29 | }, 30 | 31 | peek() { 32 | return this.xs[this.top] 33 | }, 34 | 35 | isEmpty() { 36 | return (this.top === this.xs.length) 37 | } 38 | } 39 | 40 | export default function QueueFactory(initial) { 41 | return new Queue(initial) 42 | } 43 | -------------------------------------------------------------------------------- /lib/internal/stack.js: -------------------------------------------------------------------------------- 1 | function Stack(initial) { 2 | this.xs = [initial] 3 | this.top = 0 4 | } 5 | 6 | Stack.prototype = { 7 | push(x) { 8 | this.top++ 9 | if (this.top < this.xs.length) { 10 | this.xs[this.top] = x 11 | } 12 | else { 13 | this.xs.push(x) 14 | } 15 | }, 16 | 17 | pushArrayReverse(xs) { 18 | for (let i = xs.length - 1; i >= 0; i--) { 19 | this.push(xs[i]) 20 | } 21 | }, 22 | 23 | pop() { 24 | const x = this.peek() 25 | this.top-- 26 | return x 27 | }, 28 | 29 | peek() { 30 | return this.xs[this.top] 31 | }, 32 | 33 | isEmpty() { 34 | return (-1 === this.top) 35 | } 36 | } 37 | 38 | export default function QueueFactory(initial) { 39 | return new Stack(initial) 40 | } 41 | -------------------------------------------------------------------------------- /lib/internal/util.js: -------------------------------------------------------------------------------- 1 | export function isNotEmpty(xs) { 2 | return (xs && 0 !== xs.length) 3 | } 4 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Nicolas Gryman (ngryman.sh) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the Software), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tree-crawl", 3 | "version": "1.2.2", 4 | "description": "Agnostic tree traversal library.", 5 | "author": "Nicolas Gryman (http://ngryman.sh/)", 6 | "license": "MIT", 7 | "repository": "ngryman/tree-crawl", 8 | "main": "dist/tree-crawl.js", 9 | "browser": "dist/tree-crawl.js", 10 | "module": "dist/tree-crawl.esm.js", 11 | "jsnext:main": "index.js", 12 | "types": "index.d.ts", 13 | "engines": { 14 | "node": ">=5" 15 | }, 16 | "files": [ 17 | "index.js", 18 | "index.d.ts", 19 | "lib/", 20 | "dist/" 21 | ], 22 | "scripts": { 23 | "lint": "eslint *.js {lib,test}/**/*.js", 24 | "pretest": "npm run lint", 25 | "test": "cross-env NODE_ENV=test nyc ava", 26 | "debug": "cross-env NODE_ENV=debug inspect -r babel-register -r ./test/helpers/fixtures.js --debug-exception node_modules/ava/profile.js", 27 | "docs": "documentation readme index.js lib/internal/context.js -c documentation.yml -s API", 28 | "start": "meta dev", 29 | "coverage": "nyc report --reporter=text-lcov | codecov", 30 | "see-coverage": "nyc report --reporter=html && open coverage/index.html", 31 | "check-coverage": "nyc check-coverage --lines 100 --functions 100 --branches 100", 32 | "bundle": "cross-env NODE_ENV=build rollup -c", 33 | "minify": "uglifyjs dist/tree-crawl.js -c -m --reserved-file .reserved --mangle-props -o dist/tree-crawl.min.js", 34 | "build": "npm run bundle && npm run minify", 35 | "prepublish": "npm run build", 36 | "bench": "npm run bundle && node ./perf/bench.js", 37 | "profile": "npm run bundle && devtool ./perf/profile.js", 38 | "trace": "npm run bundle && node-irhydra ./perf/trace.js" 39 | }, 40 | "keywords": [ 41 | "tree", 42 | "n-ary tree", 43 | "k-ary tree", 44 | "n-way tree", 45 | "multiway tree", 46 | "rose tree", 47 | "generic", 48 | "agnostic", 49 | "traverse", 50 | "traversal", 51 | "walk", 52 | "visit", 53 | "visitor", 54 | "recursive", 55 | "breadth first", 56 | "preorder", 57 | "postorder" 58 | ], 59 | "ava": { 60 | "require": [ 61 | "babel-register", 62 | "./test/helpers/fixtures.js" 63 | ] 64 | }, 65 | "devDependencies": { 66 | "ava": "^0.22.0", 67 | "babel-plugin-external-helpers": "^6.22.0", 68 | "babel-plugin-istanbul": "^4.1.1", 69 | "babel-preset-env": "^1.3.1", 70 | "benchmark": "^2.1.3", 71 | "clone": "^2.0.0", 72 | "codecov": "^3.7.1", 73 | "cross-env": "^7.0.3", 74 | "documentation": "^5.3.3", 75 | "eslint": "^4.9.0", 76 | "eslint-config-ngryman": "^1.7.1", 77 | "nyc": "^11.2.1", 78 | "rollup": "^0.54.1", 79 | "rollup-plugin-babel": "^3.0.2", 80 | "rollup-plugin-cleanup": "^1.0.0", 81 | "rollup-plugin-commonjs": "^8.0.2", 82 | "rollup-plugin-node-resolve": "^3.0.0", 83 | "uglify-js": "^3.0.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /perf/bench.js: -------------------------------------------------------------------------------- 1 | const Benchmark = require('benchmark') 2 | const crawl = require('../dist/tree-crawl') 3 | const tree = require('./helpers/tree') 4 | 5 | const root = tree(2, 16) 6 | 7 | const orders = ['pre', 'post', 'bfs'] 8 | const order = process.argv[2] 9 | 10 | const filterOrder = o => !order || o === order 11 | 12 | let nodesCount = 0 13 | const iteratee = node => nodesCount++ 14 | const getChildren = node => node.c 15 | 16 | const suite = Benchmark.Suite() 17 | 18 | orders.filter(filterOrder).forEach(order => { 19 | suite.add(`${order}`, () => { 20 | crawl(root, iteratee, { order, getChildren }) 21 | }) 22 | }) 23 | 24 | suite.on('cycle', function(event) { 25 | console.log(String(event.target)) 26 | }).on('complete', function() { 27 | console.log() 28 | }) 29 | .run() 30 | -------------------------------------------------------------------------------- /perf/helpers/queue.js: -------------------------------------------------------------------------------- 1 | const THRESHOLD = 32768 2 | 3 | function Queue(initial) { 4 | this.xs = [initial] 5 | this.top = 0 6 | this.maxLength = 0 7 | } 8 | 9 | Queue.prototype = { 10 | enqueue(x) { 11 | this.xs.push(x) 12 | }, 13 | 14 | enqueueMultiple(xs) { 15 | for (let i = 0, len = xs.length; i < len; i++) { 16 | this.enqueue(xs[i]) 17 | } 18 | }, 19 | 20 | dequeue() { 21 | const x = this.peek() 22 | this.top++ 23 | /* istanbul ignore next */ 24 | if (this.top === THRESHOLD) { 25 | this.xs = this.xs.slice(this.top) 26 | this.top = 0 27 | } 28 | return x 29 | }, 30 | 31 | peek() { 32 | return this.xs[this.top] 33 | }, 34 | 35 | isEmpty() { 36 | return (this.top === this.xs.length) 37 | } 38 | } 39 | 40 | module.exports = function QueueFactory(initial) { 41 | return new Queue(initial) 42 | } 43 | -------------------------------------------------------------------------------- /perf/helpers/tree.js: -------------------------------------------------------------------------------- 1 | const Queue = require('./queue') 2 | 3 | let nextValue = 0 4 | 5 | const createNode = () => 6 | ({ v: nextValue++, c: [] }) 7 | 8 | function tree(arity, levels) { 9 | const root = createNode() 10 | const queue = Queue(root) 11 | 12 | let level = 1 13 | let nodesLeft = arity 14 | 15 | while (level < levels) { 16 | const node = queue.dequeue() 17 | for (let i = 0; i < arity; i++) { 18 | const child = createNode() 19 | node.c.push(child) 20 | queue.enqueue(child) 21 | } 22 | 23 | nodesLeft -= arity 24 | if (0 === nodesLeft) { 25 | level++ 26 | nodesLeft = Math.pow(arity, level) 27 | } 28 | } 29 | 30 | return root 31 | } 32 | 33 | module.exports = tree 34 | -------------------------------------------------------------------------------- /perf/profile.js: -------------------------------------------------------------------------------- 1 | const crawl = require('../dist/tree-crawl') 2 | const tree = require('./helpers/tree') 3 | 4 | const root = tree(2, 16) 5 | 6 | const orders = ['pre', 'post', 'bfs'] 7 | const order = process.argv[2] 8 | 9 | const filterOrder = o => !order || o === order 10 | 11 | let nodesCount = 0 12 | const iteratee = node => nodesCount++ 13 | const getChildren = node => node.c 14 | 15 | orders.filter(filterOrder).forEach(order => { 16 | console.profile(order) 17 | for (let i = 0; i < 5; i++) { 18 | crawl(root, iteratee, { order, getChildren }) 19 | } 20 | console.profileEnd(order) 21 | }) 22 | -------------------------------------------------------------------------------- /perf/scripts/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const execSync = require('child_process').execSync 4 | const path = require('path') 5 | 6 | const type = process.argv[2] 7 | const order = process.argv[3] || 'all' 8 | const filename = path.join(__dirname, '..', type) 9 | 10 | let command = 'node' 11 | let output = '' 12 | 13 | if ('profile' === type) { 14 | command = 'devtool' 15 | } 16 | else if ('trace' === type) { 17 | command = 'node-irhydra' 18 | output = '🔗 http://mrale.ph/irhydra/2' 19 | } 20 | 21 | execSync(`${command} ${filename} ${order}`, { stdio: 'inherit' }) 22 | console.log(`\n${output}\n`) 23 | -------------------------------------------------------------------------------- /perf/trace.js: -------------------------------------------------------------------------------- 1 | const crawl = require('../dist/tree-crawl') 2 | const tree = require('./helpers/tree') 3 | 4 | const root = tree(2, 16) 5 | 6 | const orders = ['pre', 'post', 'bfs'] 7 | const order = process.argv[2] 8 | 9 | const filterOrder = o => !order || o === order 10 | 11 | let nodesCount = 0 12 | const iteratee = node => nodesCount++ 13 | const getChildren = node => node.c 14 | 15 | orders.filter(filterOrder).forEach(order => { 16 | // pre-morphic 17 | crawl(root, iteratee, { order, getChildren }) 18 | // monomorphic 19 | crawl(root, iteratee, { order, getChildren }) 20 | // optimized 21 | crawl(root, iteratee, { order, getChildren }) 22 | }) 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # tree-crawl [![travis][travis-image]][travis-url] [![codecov][codecov-image]][codecov-url] [![greenkeeper][greenkeeper-image]][greenkeeper-url] [![size][size-image]][size-url] 2 | 3 | > Agnostic tree traversal library. 4 | 5 | [travis-image]: https://img.shields.io/travis/ngryman/tree-crawl.svg?style=flat 6 | [travis-url]: https://travis-ci.org/ngryman/tree-crawl 7 | [codecov-image]: https://img.shields.io/codecov/c/github/ngryman/tree-crawl.svg 8 | [codecov-url]: https://codecov.io/github/ngryman/tree-crawl 9 | [greenkeeper-image]: https://badges.greenkeeper.io/ngryman/tree-crawl.svg 10 | [greenkeeper-url]: https://greenkeeper.io/ 11 | [size-image]: http://img.badgesize.io/https://unpkg.com/tree-crawl@1.0.0/dist/tree-crawl.min.js?compression=gzip 12 | [size-url]: https://unpkg.com/tree-crawl@1.0.0/dist/tree-crawl.min.js 13 | 14 | - **Agnostic**: Supports any kind of tree. You provide a way to access a node's children, that's it. 15 | - **Fast**: Crafted to be optimizer-friendly. See [performance](#performance) for more details. 16 | - **Mutation friendly**: Does not 💥 when you mutate the tree. 17 | - **Multiple orders**: Supports DFS pre and post order and BFS traversals. 18 | 19 | ## Quickstart 20 | 21 | ### Installation 22 | 23 | You can install `tree-crawl` with `yarn`: 24 | 25 | ```sh 26 | $ yarn add tree-crawl 27 | ``` 28 | 29 | Alternatively using `npm`: 30 | 31 | ```sh 32 | $ npm install --save tree-crawl 33 | ``` 34 | 35 | ### Usage 36 | 37 | ```js 38 | import crawl from 'tree-crawl' 39 | 40 | // traverse the tree in pre-order 41 | crawl(tree, console.log) 42 | crawl(tree, console.log, { order: 'pre' }) 43 | 44 | // traverse the tree in post-order 45 | crawl(tree, console.log, { order: 'post' }) 46 | 47 | // traverse the tree using `childNodes` as the children key 48 | crawl(tree, console.log, { getChildren: node => node.childNodes }) 49 | 50 | // skip a node and its children 51 | crawl(tree, (node, context) => { 52 | if ('foo' === node.type) { 53 | context.skip() 54 | } 55 | }) 56 | 57 | // stop the walk 58 | crawl(tree, (node, context) => { 59 | if ('foo' === node.type) { 60 | context.break() 61 | } 62 | }) 63 | 64 | // remove a node 65 | crawl(tree, (node, context) => { 66 | if ('foo' === node.type) { 67 | context.parent.children.splice(context.index, 1) 68 | context.remove() 69 | } 70 | }) 71 | 72 | // replace a node 73 | crawl(tree, (node, context) => { 74 | if ('foo' === node.type) { 75 | const node = { 76 | type: 'new node', 77 | children: [ 78 | { type: 'new leaf' } 79 | ] 80 | } 81 | context.parent.children[context.index] = node 82 | context.replace(node) 83 | } 84 | }) 85 | ``` 86 | 87 | ## FAQ 88 | 89 | ### How can I get the path of the current node ([#37](https://github.com/ngryman/tree-crawl/issues/37))? 90 | 91 | **tl;dr It's easy for DFS, less easy for BFS** 92 | 93 | If you are using DFS you can use the following utility function: 94 | ```javascript 95 | const getPath = context => 96 | context.cursor.stack.xs.reduce((path, item) => { 97 | if (item.node) { 98 | path.push(item.node) 99 | } 100 | return path 101 | }, []) 102 | ``` 103 | If you are really concerned about performance, you could read items from the stack directly. Each item has a `node` and `index` property that you can use. The first item in the stack can be discarded and will have a `node` set to `null`. Be aware that you should not mutate the stack, or it will break the traversal. 104 | 105 | If you are using BFS, things gets more complex. A *simple hacky* way to do so is to traverse the tree using DFS first. You can ad a `path` property to your nodes using the method above. And then do your regular BFS traversal using that `path` property. 106 | 107 | ## API 108 | 109 | 110 | 111 | ### Iteratee 112 | 113 | - **See: [Traversal context](#traversal-context).** 114 | 115 | Called on each node of the tree. 116 | 117 | Type: [Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function) 118 | 119 | **Parameters** 120 | 121 | - `node` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Node being visited. 122 | - `context` **[Context](#context)** Traversal context 123 | 124 | ### Options 125 | 126 | Walk options. 127 | 128 | Type: [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) 129 | 130 | **Parameters** 131 | 132 | - `node` 133 | 134 | **Properties** 135 | 136 | - `getChildren` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)?** Return a node's children. 137 | - `order` **(`"pre"` \| `"post"` \| `"bfs"`)?** Order of the walk either in DFS pre or post order, or 138 | BFS. 139 | 140 | **Examples** 141 | 142 | _Traverse a DOM tree._ 143 | 144 | ```javascript 145 | crawl(document.body, doSomeStuff, { getChildren: node => node.childNodes }) 146 | ``` 147 | 148 | _BFS traversal_ 149 | 150 | ```javascript 151 | crawl(root, doSomeStuff, { order: 'bfs' }) 152 | ``` 153 | 154 | ### crawl 155 | 156 | Walk a tree recursively. 157 | 158 | By default `getChildren` will return the `children` property of a node. 159 | 160 | **Parameters** 161 | 162 | - `root` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Root node of the tree to be walked. 163 | - `iteratee` **[Iteratee](#iteratee)** Function invoked on each node. 164 | - `options` **[Options](#options)?** Options customizing the walk. 165 | 166 | ### Context 167 | 168 | A traversal context. 169 | 170 | Four operations are available. Note that depending on the traversal order, some operations have 171 | no effects. 172 | 173 | **Parameters** 174 | 175 | - `flags` **Flags** 176 | - `cursor` **Cursor** 177 | 178 | #### skip 179 | 180 | Skip current node, children won't be visited. 181 | 182 | **Examples** 183 | 184 | ```javascript 185 | crawl(root, (node, context) => { 186 | if ('foo' === node.type) { 187 | context.skip() 188 | } 189 | }) 190 | ``` 191 | 192 | #### break 193 | 194 | Stop traversal now. 195 | 196 | **Examples** 197 | 198 | ```javascript 199 | crawl(root, (node, context) => { 200 | if ('foo' === node.type) { 201 | context.break() 202 | } 203 | }) 204 | ``` 205 | 206 | #### remove 207 | 208 | Notifies that the current node has been removed, children won't be visited. 209 | 210 | Because `tree-crawl` has no idea about the intrinsic structure of your tree, you have to 211 | remove the node yourself. `Context#remove` only notifies the traversal code that the structure 212 | of the tree has changed. 213 | 214 | **Examples** 215 | 216 | ```javascript 217 | crawl(root, (node, context) => { 218 | if ('foo' === node.type) { 219 | context.parent.children.splice(context.index, 1) 220 | context.remove() 221 | } 222 | }) 223 | ``` 224 | 225 | #### replace 226 | 227 | Notifies that the current node has been replaced, the new node's children will be visited 228 | instead. 229 | 230 | Because `tree-crawl` has no idea about the intrinsic structure of your tree, you have to 231 | replace the node yourself. `Context#replace` notifies the traversal code that the structure of 232 | the tree has changed. 233 | 234 | **Parameters** 235 | 236 | - `node` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Replacement node. 237 | 238 | **Examples** 239 | 240 | ```javascript 241 | crawl(root, (node, context) => { 242 | if ('foo' === node.type) { 243 | const node = { 244 | type: 'new node', 245 | children: [ 246 | { type: 'new leaf' } 247 | ] 248 | } 249 | context.parent.children[context.index] = node 250 | context.replace(node) 251 | } 252 | }) 253 | ``` 254 | 255 | #### parent 256 | 257 | Get the parent of the current node. 258 | 259 | Returns **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Parent node. 260 | 261 | #### depth 262 | 263 | Get the **depth** of the current node. The depth is the number of ancestors the current node 264 | has. 265 | 266 | Returns **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** Depth. 267 | 268 | #### level 269 | 270 | Get the **level** of current node. The level is the number of ancestors+1 the current node has. 271 | 272 | Returns **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** Level. 273 | 274 | #### index 275 | 276 | Get the index of the current node. 277 | 278 | Returns **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** Node's index. 279 | 280 | ## Performance 281 | 282 | `tree-crawl` is built to be super fast and traverse potentially huge trees. It's possible because it implements its own stack and queue for traversal algorithms and makes sure the code is optimizable by the VM. 283 | 284 | If you do need real good performance please consider reading this [checklist] first. 285 | 286 | Your main objective is to keep the traversal code optimized and avoid de-optimizations and bailouts. To do so, your nodes should have the same [hidden class] and your code stay [monomorphic]. 287 | 288 | [checklist]: http://mrale.ph/blog/2011/12/18/v8-optimization-checklist.html 289 | 290 | [hidden class]: http://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.html 291 | 292 | [monomorphic]: http://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html 293 | 294 | ## Related 295 | 296 | - [arbre](https://github.com/arbrejs/arbre) Agnostic tree library. 297 | - [tree-mutate](https://github.com/ngryman/tree-mutate) Agnostic tree mutation library. 298 | - [tree-morph](https://github.com/ngryman/tree-morph) Agnostic tree morphing library. 299 | 300 | ## License 301 | 302 | MIT © [Nicolas Gryman](http://ngryman.sh) 303 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import babel from 'rollup-plugin-babel' 4 | import cleanup from 'rollup-plugin-cleanup' 5 | 6 | export default { 7 | name: 'crawl', 8 | input: 'index.js', 9 | output: [{ 10 | format: 'umd', 11 | file: 'dist/tree-crawl.js' 12 | }, { 13 | format: 'es', 14 | file: 'dist/tree-crawl.esm.js' 15 | }], 16 | plugins: [ 17 | resolve({ 18 | jsnext: true, 19 | main: true 20 | }), 21 | commonjs(), 22 | // XXX: https://github.com/rollup/rollup-plugin-babel/issues/120 23 | babel({ 24 | babelrc: false, 25 | presets: [ 26 | ['env', { 27 | targets: { browsers: '> 1%' }, 28 | modules: false 29 | }] 30 | ], 31 | plugins: [ 32 | 'external-helpers' 33 | ] 34 | }), 35 | cleanup() 36 | ].filter(Boolean) 37 | } 38 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "fixtures": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/bfs.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import bfs from '../lib/bfs' 3 | 4 | test.beforeEach(t => { 5 | t.context.traverse = (treeName, iteratee, getChildren = node => node.children) => { 6 | const root = t.context.root = fixtures(treeName) 7 | bfs(root, iteratee, getChildren) 8 | } 9 | }) 10 | 11 | test('traverse a tree', t => { 12 | const values = [] 13 | t.context.traverse('deep', node => values.push(node.value)) 14 | t.deepEqual(values, [1, 2, 5, 3, 4, 6]) 15 | }) 16 | 17 | test('accept a custom getChildren function', t => { 18 | const values = [] 19 | t.context.traverse('custom-children', node => values.push(node.value), node => node.childNodes) 20 | t.deepEqual(values, [1, 2, 3]) 21 | }) 22 | 23 | test('provide current node parent', t => { 24 | t.context.traverse('deep', (node, context) => { 25 | if (3 === node.value) { 26 | t.is(context.parent, t.context.root.children[0]) 27 | } 28 | else if (6 === node.value) { 29 | t.is(context.parent, t.context.root.children[1]) 30 | } 31 | }) 32 | }) 33 | 34 | test('provide current node index', t => { 35 | const indices = [] 36 | t.context.traverse('deep', (node, context) => indices.push(context.index)) 37 | t.deepEqual(indices, [-1, 0, 1, 0, 1, 0]) 38 | }) 39 | 40 | test('provide current node depth', t => { 41 | t.context.traverse('deep', (node, context) => { 42 | if (1 === node.value) { 43 | t.is(context.depth, 0) 44 | } 45 | else if (2 === node.value || 5 === node.value) { 46 | t.is(context.depth, 1) 47 | } 48 | else { 49 | t.is(context.depth, 2) 50 | } 51 | }) 52 | }) 53 | 54 | test('provide current node level', t => { 55 | t.context.traverse('deep', (node, context) => { 56 | if (1 === node.value) { 57 | t.is(context.level, 1) 58 | } 59 | else if (2 === node.value || 5 === node.value) { 60 | t.is(context.level, 2) 61 | } 62 | else { 63 | t.is(context.level, 3) 64 | } 65 | }) 66 | }) 67 | 68 | test('break walk', t => { 69 | const values = [] 70 | t.context.traverse('deep', (node, context) => { 71 | values.push(node.value) 72 | if (2 === node.value) { 73 | context.break() 74 | } 75 | }) 76 | t.deepEqual(values, [1, 2]) 77 | }) 78 | 79 | test('skip current node children', t => { 80 | const values = [] 81 | t.context.traverse('deep', (node, context) => { 82 | values.push(node.value) 83 | if (2 === node.value) { 84 | context.skip() 85 | } 86 | }) 87 | t.deepEqual(values, [1, 2, 5, 6]) 88 | }) 89 | 90 | test('remove current node', t => { 91 | const values = [] 92 | t.context.traverse('deep', (node, context) => { 93 | values.push(node.value) 94 | if (2 === node.value) { 95 | t.context.root.children.splice(0, 1) 96 | context.remove() 97 | } 98 | }) 99 | t.deepEqual(values, [1, 2, 5, 6]) 100 | }) 101 | 102 | test('provide up-to-date index when a node is removed', t => { 103 | const indices = [] 104 | t.context.traverse('deep', (node, context) => { 105 | indices.push(context.index) 106 | if (2 === node.value) { 107 | t.context.root.children.splice(0, 1) 108 | context.remove() 109 | } 110 | }) 111 | t.deepEqual(indices, [-1, 0, 0, 0]) 112 | }) 113 | 114 | test('replace current node', t => { 115 | const values = [] 116 | const newNode = { 117 | value: 2, 118 | children: [ 119 | { value: 1337, children: [] } 120 | ] 121 | } 122 | t.context.traverse('deep', (node, context) => { 123 | values.push(node.value) 124 | if (2 === node.value) { 125 | context.replace(newNode) 126 | } 127 | }) 128 | t.deepEqual(values, [1, 2, 5, 1337, 6]) 129 | }) 130 | -------------------------------------------------------------------------------- /test/dfs-post.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import dfsPost from '../lib/dfs-post' 3 | 4 | test.beforeEach(t => { 5 | t.context.traverse = (treeName, iteratee, getChildren = node => node.children) => { 6 | const root = t.context.root = fixtures(treeName) 7 | dfsPost(root, iteratee, getChildren) 8 | } 9 | }) 10 | 11 | test('traverse a tree', t => { 12 | const values = [] 13 | t.context.traverse('deep', node => values.push(node.value)) 14 | t.deepEqual(values, [3, 4, 2, 6, 5, 1]) 15 | }) 16 | 17 | test('accept a custom getChildren function', t => { 18 | const values = [] 19 | t.context.traverse('custom-children', node => values.push(node.value), node => node.childNodes) 20 | t.deepEqual(values, [2, 3, 1]) 21 | }) 22 | 23 | test('provide current node parent', t => { 24 | t.context.traverse('deep', (node, context) => { 25 | if (3 === node.value) { 26 | t.is(context.parent, t.context.root.children[0]) 27 | } 28 | else if (6 === node.value) { 29 | t.is(context.parent, t.context.root.children[1]) 30 | } 31 | }) 32 | }) 33 | 34 | test('provide current node index', t => { 35 | const indices = [] 36 | t.context.traverse('deep', (node, context) => indices.push(context.index)) 37 | t.deepEqual(indices, [0, 1, 0, 0, 1, -1]) 38 | }) 39 | 40 | test('provide current node depth', t => { 41 | t.context.traverse('deep', (node, context) => { 42 | if (1 === node.value) { 43 | t.is(context.depth, 0) 44 | } 45 | else if (2 === node.value || 5 === node.value) { 46 | t.is(context.depth, 1) 47 | } 48 | else { 49 | t.is(context.depth, 2) 50 | } 51 | }) 52 | }) 53 | 54 | test('provide current node level', t => { 55 | t.context.traverse('deep', (node, context) => { 56 | if (1 === node.value) { 57 | t.is(context.level, 1) 58 | } 59 | else if (2 === node.value || 5 === node.value) { 60 | t.is(context.level, 2) 61 | } 62 | else { 63 | t.is(context.level, 3) 64 | } 65 | }) 66 | }) 67 | 68 | test('break walk', t => { 69 | const values = [] 70 | t.context.traverse('deep', (node, context) => { 71 | values.push(node.value) 72 | if (2 === node.value) { 73 | context.break() 74 | } 75 | }) 76 | t.deepEqual(values, [3, 4, 2]) 77 | }) 78 | 79 | test('ignore skipping current node', t => { 80 | const values = [] 81 | t.context.traverse('deep', (node, context) => { 82 | values.push(node.value) 83 | if (2 === node.value) { 84 | context.skip() 85 | } 86 | }) 87 | t.deepEqual(values, [3, 4, 2, 6, 5, 1]) 88 | }) 89 | 90 | test('ignore removing current node', t => { 91 | const values = [] 92 | t.context.traverse('deep', (node, context) => { 93 | values.push(node.value) 94 | if (2 === node.value) { 95 | t.context.root.children.splice(0, 1) 96 | context.remove() 97 | } 98 | }) 99 | t.deepEqual(values, [3, 4, 2, 6, 5, 1]) 100 | }) 101 | 102 | test('provide up-to-date index when a node is removed', t => { 103 | const indices = [] 104 | t.context.traverse('deep', (node, context) => { 105 | indices.push(context.index) 106 | if (2 === node.value) { 107 | t.context.root.children.splice(0, 1) 108 | context.remove() 109 | } 110 | }) 111 | t.deepEqual(indices, [0, 1, 0, 0, 0, -1]) 112 | }) 113 | 114 | test('ignore replacing current node', t => { 115 | const values = [] 116 | const newNode = { 117 | value: 2, 118 | children: [ 119 | { value: 1337, children: [] } 120 | ] 121 | } 122 | t.context.traverse('deep', (node, context) => { 123 | values.push(node.value) 124 | if (2 === node.value) { 125 | context.replace(newNode) 126 | } 127 | }) 128 | t.deepEqual(values, [3, 4, 2, 6, 5, 1]) 129 | }) 130 | -------------------------------------------------------------------------------- /test/dfs-pre.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import dfsPre from '../lib/dfs-pre' 3 | 4 | test.beforeEach(t => { 5 | t.context.traverse = (treeName, iteratee, getChildren = node => node.children) => { 6 | const root = t.context.root = fixtures(treeName) 7 | dfsPre(root, iteratee, getChildren) 8 | } 9 | }) 10 | 11 | test('traverse a tree', t => { 12 | const values = [] 13 | t.context.traverse('deep', node => values.push(node.value)) 14 | t.deepEqual(values, [1, 2, 3, 4, 5, 6]) 15 | }) 16 | 17 | test('accept a custom getChildren function', t => { 18 | const values = [] 19 | t.context.traverse('custom-children', node => values.push(node.value), node => node.childNodes) 20 | t.deepEqual(values, [1, 2, 3]) 21 | }) 22 | 23 | test.todo('traverse a tree without cursor') 24 | 25 | test('provide current node parent', t => { 26 | t.context.traverse('deep', (node, context) => { 27 | if (3 === node.value) { 28 | t.is(context.parent, t.context.root.children[0]) 29 | } 30 | else if (6 === node.value) { 31 | t.is(context.parent, t.context.root.children[1]) 32 | } 33 | }) 34 | }) 35 | 36 | test('provide current node index', t => { 37 | const indices = [] 38 | t.context.traverse('deep', (node, context) => indices.push(context.index)) 39 | t.deepEqual(indices, [-1, 0, 0, 1, 1, 0]) 40 | }) 41 | 42 | test('provide current node depth', t => { 43 | t.context.traverse('deep', (node, context) => { 44 | if (1 === node.value) { 45 | t.is(context.depth, 0) 46 | } 47 | else if (2 === node.value || 5 === node.value) { 48 | t.is(context.depth, 1) 49 | } 50 | else { 51 | t.is(context.depth, 2) 52 | } 53 | }) 54 | }) 55 | 56 | test('provide current node level', t => { 57 | t.context.traverse('deep', (node, context) => { 58 | if (1 === node.value) { 59 | t.is(context.level, 1) 60 | } 61 | else if (2 === node.value || 5 === node.value) { 62 | t.is(context.level, 2) 63 | } 64 | else { 65 | t.is(context.level, 3) 66 | } 67 | }) 68 | }) 69 | 70 | test('break walk', t => { 71 | const values = [] 72 | t.context.traverse('deep', (node, context) => { 73 | values.push(node.value) 74 | if (2 === node.value) { 75 | context.break() 76 | } 77 | }) 78 | t.deepEqual(values, [1, 2]) 79 | }) 80 | 81 | test('skip current node children', t => { 82 | const values = [] 83 | t.context.traverse('deep', (node, context) => { 84 | values.push(node.value) 85 | if (2 === node.value) { 86 | context.skip() 87 | } 88 | }) 89 | t.deepEqual(values, [1, 2, 5, 6]) 90 | }) 91 | 92 | test('remove current node', t => { 93 | const values = [] 94 | t.context.traverse('deep', (node, context) => { 95 | values.push(node.value) 96 | if (2 === node.value) { 97 | t.context.root.children.splice(0, 1) 98 | context.remove() 99 | } 100 | }) 101 | t.deepEqual(values, [1, 2, 5, 6]) 102 | }) 103 | 104 | test('provide up-to-date index when a node is removed', t => { 105 | const indices = [] 106 | t.context.traverse('deep', (node, context) => { 107 | indices.push(context.index) 108 | if (2 === node.value) { 109 | t.context.root.children.splice(0, 1) 110 | context.remove() 111 | } 112 | }) 113 | t.deepEqual(indices, [-1, 0, 0, 0]) 114 | }) 115 | 116 | test('replace current node', t => { 117 | const values = [] 118 | const newNode = { 119 | value: 2, 120 | children: [ 121 | { value: 1337, children: [] } 122 | ] 123 | } 124 | t.context.traverse('deep', (node, context) => { 125 | values.push(node.value) 126 | if (2 === node.value) { 127 | context.replace(newNode) 128 | } 129 | }) 130 | t.deepEqual(values, [1, 2, 1337, 5, 6]) 131 | }) 132 | -------------------------------------------------------------------------------- /test/fixtures/custom-children.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": 1, 3 | "childNodes": [ 4 | { "value": 2, "childNodes": [] }, 5 | { "value": 3, "childNodes": [] } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/deep.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": 1, 3 | "children": [ 4 | { 5 | "value": 2, 6 | "children": [ 7 | { "value": 3, "children": [] }, 8 | { "value": 4, "children": [] } 9 | ] 10 | }, 11 | { 12 | "value": 5, 13 | "children": [ 14 | { "value": 6, "children": [] } 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": 1, 3 | "children": [ 4 | { "value": 2, "children": [] }, 5 | { "value": 3, "children": [] } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /test/helpers/fixtures.js: -------------------------------------------------------------------------------- 1 | import clone from 'clone' 2 | import path from 'path' 3 | 4 | global.fixtures = (name) => { 5 | return clone(require(path.resolve('test', 'fixtures', `${name}.json`))) 6 | } 7 | --------------------------------------------------------------------------------