Skip to content

The most flexible console tree printer like the unix "tree" command that you can customize to suit your specific needs.

License

Notifications You must be signed in to change notification settings

h-sifat/flexible-tree-printer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

flexible-tree-printer

Module Type Npm Version GitHub Tag GitHub Issues

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.

Usages

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

Install

npm install flexible-tree-printer

Importing in HTML

<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;

Features

Almost every behavior of the printTree function is customizable. It allows

  1. custom indentation length
  2. setting max depth
  3. custom sort function
  4. printing slowly, step by step on your demand
  5. retrieving sub nodes on demand, so the tree object doesn't have be to ready before printing
  6. conditional rendering of nodes (may be a node is too deep and you don't want to descend anymore)
  7. changing connector characters (├, └, ─, , │)
  8. 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).

Interface

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.

connectors

The connectors object contains all the characters used for building the tree branches.

{
  tee: "├",
  elbow: "└",
  hLine: "─",
  space: " ",
  vLine: "│",
}

DEFAULTS

This object contains all the default properties for the printTree function's argument.

Options

Table of contents

  1. Introduction
  2. parentNode
  3. maxDepth
  4. connectors
  5. indentationLength
  6. numOfHLinesBeforeNode
  7. getSubNodes
  8. shouldDescend
  9. sortNodes
  10. forEach
  11. getNodePrefix
  12. printNode
  13. 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

parentNode

The object to print. Default: null.

Go to Table Of Contents

maxDepth

Specifies the max number of sub nodes from the root node to descend into. The default value is Infinity.

Go to Table Of Contents

connectors

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

indentationLength

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

numOfHLinesBeforeNode

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

getSubNodes

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

shouldDescend

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

sortNodes

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

forEach

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

getNodePrefix

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

printNode

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

printRootNode

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

Tutorial

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);
}

Development

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

Contributing

If you find a bug or want to improve this package then feel free to open an issue. Pull requests are also welcomed 💝.

About

The most flexible console tree printer like the unix "tree" command that you can customize to suit your specific needs.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published