The most flexible console tree printer like the unix tree
command that you
can customize to suit your specific needs. This library uses the UMD module
system so it supports all JavaScript environments.
const { printTree } = require("flexible-tree-printer");
// or with es6 module system
import { printTree } from "flexible-tree-printer";
const categories = {
study: {
academic: { Math: null, English: null },
programming: {
DSA: null,
"Number Theory": {},
Backend: { "Node.Js": {}, Sqlite: {} },
},
},
work: { personal_projects: null, job: {} },
};
printTree({ parentNode: categories });
Running the above snippet produces the following result:
.
├── study
│ ├── academic
│ │ ├── Math
│ │ └── English
│ └── programming
│ ├── DSA
│ ├── Number Theory
│ └── Backend
│ ├── Node.Js
│ └── Sqlite
└── work
├── personal_projects
└── job
npm install flexible-tree-printer
<script src="https://unpkg.com/flexible-tree-printer"></script>
<!-- It will be available globally by the name "flexibleTreePrinter" -->
Note: If you're using dynamic import with the unpkg link then the import
statement wont return the actual module. You'll have to access it through the
global name flexibleTreePrinter
.
Incorrect:
const { printTree } = await import("https://unpkg.com/flexible-tree-printer");
// printTree: undefined
Corrects:
await import("https://unpkg.com/flexible-tree-printer");
const { printTree } = flexibleTreePrinter;
Almost every behavior of the printTree
function is customizable. It allows
- custom indentation length
- setting max depth
- custom sort function
- printing slowly, step by step on your demand
- retrieving sub nodes on demand, so the tree object doesn't have be to ready before printing
- conditional rendering of nodes (may be a node is too deep and you don't want to descend anymore)
- changing connector characters (
├, └, ─, , │
) - changing how every line gets printed. In fact, it doesn't even know where it's printing! The console? a file? or something else? It's up to you to decide.
It has no external dependency thus it's very lite-weight (only 5.00KiB
at time
of writing this).
The printTree
function has the following interface. It takes an object as
its only argument.
interface PrintTree_Argument<Type = any> {
maxDepth?: number;
parentNode?: Type;
printNode?: PrintNode;
connectors?: Connectors;
forEach?: ForEach<Type>;
printRootNode?: () => void;
indentationLength?: number;
getNodePrefix?: GetNodePrefix;
numOfHLinesBeforeNode?: number;
getSubNodes?: GetSubNodes<Type>;
sortNodes?: (arg: ShouldDescend_Argument) => void;
shouldDescend?: (arg: ShouldDescend_Argument) => boolean;
}
Don't fret after seeing the long list of properties. All are optional and a
default for every one of them is already provided for you. If you don't
provide any argument or just an empty object as the argument it'll just print a
dot "."
(the root node).
The library also exposes two other objects besides the printTree
function in
case you find them useful. They are the connectors
and the DEFAULT_ARGUMENT
.
The connectors
object contains all the characters used for building the tree
branches.
{
tee: "├",
elbow: "└",
hLine: "─",
space: " ",
vLine: "│",
}
This object contains all the default properties for the printTree
function's
argument.
Introduction
parentNode
maxDepth
connectors
indentationLength
numOfHLinesBeforeNode
getSubNodes
shouldDescend
sortNodes
forEach
getNodePrefix
printNode
printRootNode
Before we start, we need to know that printing a single line increases the
levelY
by one, and descending into a sub node increases the levelX
by
the indentationLength
(the default is 4 characters e.g., "├── "
or "└── "
).
The maxDepth
property refers to the max allowed levelX
thus the max
number of sub nodes to descend into from the root node.
.-----> level X ----------------->
|
Y--+-[1 ][2 ][3 ]----------
1 | .
2 | ├── study
3 | │ ├── academic
4 | │ │ ├── Math
5 | │ │ └── English
6 | │ └── programming
7 | └── work
8 | ├── personal_projects
9 | └── job
|
+-[1 ][2 ][3 ]----------
Go to Table Of Contents
The object to print. Default: null
.
Go to Table Of Contents
Specifies the max number of sub nodes from the root node to descend into. The
default value is Infinity
.
Go to Table Of Contents
An object containing characters to build the tree structure with.
Example
const connectors = {
tee: "+",
elbow: "*",
hLine: "-",
space: ".",
vLine: "|",
};
printTree({
connectors,
maxDepth: 3,
parentNode: categories,
});
The above snippet will produce:
.
+--.study
|...+--.academic
|...*--.programming
*--.work
....+--.personal_projects
....*--.job
Tip: If you just want to change only one or two character then borrow the others form the exported one.
const { connectors } = require("flexible-tree-printer");
const myConnectors = {
...connectors,
elbow: "+",
};
Go to Table Of Contents
Specifies the number of characters to use before every node. The default is 4 characters.
Example:
printTree({ maxDepth: 1, indentationLength: 20, parentNode: categories });
will produce:
.
├────────────────── study
└────────────────── work
Go to Table Of Contents
Specifies the number of connectors.hLine
before a node. It must be less than
the indentationLength
otherwise a error is thrown. The default is
indentationLength - 2
.
Example:
const { connectors, printTree } = require("flexible-tree-printer");
const myConnectors = {
...connectors,
hLine: "~",
space: ".",
};
// default numOfHLinesBeforeNode
printTree({
maxDepth: 1,
indentationLength: 20,
parentNode: categories,
connectors: myConnectors,
});
// .
// ├~~~~~~~~~~~~~~~~~~.study
// └~~~~~~~~~~~~~~~~~~.work
// numOfHLinesBeforeNode: 10
printTree({
maxDepth: 1,
indentationLength: 20,
parentNode: categories,
connectors: myConnectors,
numOfHLinesBeforeNode: 10,
});
// .
// ├~~~~~~~~~~.........study
// └~~~~~~~~~~.........work
Go to Table Of Contents
A function that returns all the sub nodes of the given parent. It has the
following interface. It returns a Node<Type>[]
array. We can use this function
to retrieve sub nodes on demand, for example reading a directory.
interface Node<Type> {
value: Type;
name: string;
}
interface GetSubNode_Argument<Type> {
levelX: number;
levelY: number;
parentNode: Type;
path: Readonly<string[]>;
}
type GetSubNodes<Type> = (arg: GetSubNode_Argument<Type>) => Node<Type>[];
Here the path
array contains names of all the ancestors of the parentNode
node from the root.
Example (the default getSubNodes
implementation):
function getSubNodes(arg) {
const { parentNode } = arg;
// @TODO for the reader
if(/* parentNode is not a plain object*/) return []
return Object.entries(parentNode)
.map(([name, value]) => ({ name, value }));
}
// -------- example ------------
const parentNode = {
study: null,
work: { job: null, personal_projects: {} },
};
getSubNodes({parentNode});
/* should return the following array:
* [
* { name: 'study', value: null },
* { name: 'work', value: { job: null, personal_projects: {} } }
* ]
* */
Go to Table Of Contents
With this method we can decide whether we want to descend into a node or not. For example, if the number of sub nodes is more than 2000 then we may not want to print all those nodes.
The interface of this function is:
type ShouldDescend = (
arg: GetSubNode_Argument & { subNodes: Node<any>[] }
) => boolean;
It takes the same argument as getSubNodes
with an extra
property subNodes
; the array generated by the getSubNodes
function.
Example:
function shouldDescend({ subNodes }) {
return subNodes.length < 2000;
}
Go to Table Of Contents
A custom sort function used to sort the subNodes
array generated by the
getSubNodes
function. It takes same argument as the
shouldDescend
function.
Interface:
type ShouldDescend = (arg: ShouldDescend_Argument) => void;
Example: Sorting nodes based on the length of their name.
function sortNodes({ subNodes }) {
subNodes.sort((nodeA, nodeB) => nodeA.name.length - nodeB.name.length);
}
Go to Table Of Contents
With this function we can customize the way we iterate over all the sub nodes.
Interface:
export type ForEachCallback<Type> = (
item: Type,
index: number,
array: Type[]
) => void;
export type ForEach<Type> = (
array: Type[],
callback: ForEachCallback<Type>
) => void;
Example: Printing nodes slowly
function forEach(array, callback) {
let index = 0;
const intervalId = setInterval(() => {
if (index < array.length) callback(array[index], index, array);
else clearInterval(intervalId);
index++;
}, 1000);
}
Go to Table Of Contents
This function is the main component of the printTree
function and you
probably don't want to change the default. It is still replaceable in case the
need arises. It generates the prefix before every node and returns a character
array.
So, if a line is: "│ │ └── English"
then the "English" node's prefix is:
"│ │ └── "
.
Interface:
type GetNodePrefix_Argument = {
levelX: number;
connectors: object;
isLastNode: boolean;
indentationLength: number;
numOfHLinesBeforeNode?: number;
xLevelsOfLastNodeAncestors: number[];
};
type GetNodePrefix = (arg: GetNodePrefix_Argument) => string[];
Here xLevelsOfLastNodeAncestors
property is a number array where it contains
all the X
levels of ancestors which are the last node of their parent.
Example
.
└── study
. └── academic
. . ├── Math
. . └── English
-------------------
1 2 3 level X ->
For the nodes "English" and "Math", their ancestors "study" (level 2) and the
"root" (level 1) nodes are the last child of their parent. So for these nodes,
we should not fill these levels with the "│"
(connectors.vLine
) character.
Otherwise the tree would look like:
.
└── study
│ └── academic
│ │ ├── Math
│ │ └── English
-------------------
1 2 3 level X ->
Go to Table Of Contents
The printNode
function is responsible for printing every line of the tree. We
can use this function to change the way we want to print a line.
Interface:
type PrintNode_Argument = {
levelX: number;
levelY: number;
path: string[];
node: Node<any>;
parentNode: any;
connectors: object;
isLastNode: boolean;
nodePrefix: string[];
indentationLength: number;
numOfHLinesBeforeNode?: number;
xLevelsOfLastNodeAncestors: number[];
};
type PrintNode = (arg: PrintNode_Argument) => void;
The Default Implementation:
function printNode({ nodePrefix, node }) {
const line = nodePrefix.join("") + node.name;
console.log(line);
}
Go to Table Of Contents
Allows us to change how the root node should be printed. The default implementation is:
const printRootNode = () => console.log(".");
Example:
printTree({ printRootNode: () => console.log("[*]") });
// [*]
// it just prints the root node
Let's make a simple program like the unix tree
command with this library.
const fs = require("fs");
const path = require("path");
const { printTree } = require("flexible-tree-printer");
const maxDepth = 3;
const startDirectory = "<your-start-directory-here>";
const baseDirectory = path.resolve(startDirectory);
printTree({ getSubNodes, maxDepth });
function getSubNodes({ path: pathArray }) {
const currentPath = path.join(baseDirectory, ...pathArray);
try {
const entries = fs.readdirSync(currentPath);
return entries.map((entry) => ({ name: entry, value: entry }));
} catch (ex) {
// if the path is not a dir so return an empty array
// denoting that this node has no sub nodes.
if (ex.code === "ENOTDIR") return [];
// quit the program
exit(ex.message);
}
}
function exit(message, code = 1) {
console.error(message);
process.exit(code);
}
To run tests:
npm test
npm test:watch # run tests in watch mode
npm test:coverage # run test coverage
To build the package run:
npm run build
If you find a bug or want to improve this package then feel free to open an issue. Pull requests are also welcomed 💝.