array-treeify
Version:
Simple text tree diagrams from arrays.
151 lines (150 loc) • 5.42 kB
JavaScript
const DEFAULT_CHARS = {
branch: '├─ ',
lastBranch: '└─ ',
pipe: '│ ',
space: ' ',
};
const SPACE = ' ';
const EMPTY_CHARS = {
branch: SPACE.repeat(DEFAULT_CHARS.branch.length),
lastBranch: SPACE.repeat(DEFAULT_CHARS.lastBranch.length),
pipe: SPACE.repeat(DEFAULT_CHARS.pipe.length),
space: SPACE.repeat(DEFAULT_CHARS.space.length),
};
/**
* @description Creates a text tree representation from a nested array structure using Unicode box-drawing characters.
*
* The expected input format is a hierarchical structure where:
* - The first element must be a string (the root node)
* - String elements represent nodes at the current level
* - Array elements following a string represent the children of the previous node
* - Nested arrays create deeper levels in the tree
*
* Examples of supported formats:
* - `['root', ['child1', 'child2', 'child3']]` creates a root with three children
* - `['root', 'second', ['child1', 'child2']]` creates multiple root nodes with children
* - `['root', ['child1', ['grandchild1', 'grandchild2']]]` creates a root with nested children
* - `['root', ['childA', ['grandchildA'], 'childB']]` creates multiple branches
*
* The output uses Unicode box-drawing characters to visualize the tree structure.
*
* @param list {FlexibleTreeInput} - An array representing the tree structure. First element must be a string.
* @param options {Object} - An object containing optional configuration:
* - `chars` {TreeChars} - Custom characters for the tree. Defaults to Unicode box-drawing characters.
* - `plain` {boolean} - Whether to use plain whitespace characters instead of Unicode box-drawing characters.
*
* @returns {string} A string containing the tree representation
*
* @example
* treeify(['root', ['child1', 'child2', ['grandchild']]])
* // root
* // ├─ child1
* // └─ child2
* // └─ grandchild
*/
export function treeify(list, options) {
if (!Array.isArray(list) || list.length === 0)
return '';
if (list[0] === undefined)
return '';
if (typeof list[0] !== 'string')
throw new Error('First element must be a string');
let chars = DEFAULT_CHARS;
if (options?.plain)
chars = EMPTY_CHARS;
if (options?.chars)
chars = options.chars;
const result = [];
result.push(list[0]); // first string is the root
let i = 1;
while (i < list.length) {
const node = list[i];
if (typeof node === 'string') {
// add strings here
result.push(node);
i++;
}
else if (Array.isArray(node)) {
// array is the children of the previous item
renderTreeNodes(node, '', result, chars);
i++;
}
else {
// idk. skip it.
i++;
}
}
return result.join('\n');
}
/**
* @description Renders tree nodes with appropriate ASCII indentation and branching
*/
function renderTreeNodes(nodes, indent, result, chars) {
if (!Array.isArray(nodes) || nodes.length === 0)
return;
const parentNodeIndices = findParentNodeIndices(nodes);
let i = 0;
while (i < nodes.length) {
const node = nodes[i];
const isParentNode = parentNodeIndices.has(i);
if (isParentNode) {
const parentIndex = i;
const childrenIndex = i + 1;
const stringNode = nodes[parentIndex];
const arrayNode = nodes[childrenIndex];
const isLast = !hasNextStringNode(nodes, childrenIndex + 1);
const prefix = isLast ? chars.lastBranch : chars.branch;
result.push(indent + prefix + stringNode);
// children with increased indent
const childIndent = indent + (isLast ? chars.space : chars.pipe);
renderTreeNodes(arrayNode, childIndent, result, chars);
// skip both the parent node and its children array
i += 2;
}
else if (typeof node === 'string') {
// string is simple. add it.
const isLast = !hasNextStringNode(nodes, i + 1);
const prefix = isLast ? chars.lastBranch : chars.branch;
result.push(indent + prefix + node);
i++;
}
else if (Array.isArray(node)) {
// (>_>)
const isLast = i === nodes.length - 1;
const childIndent = indent + (isLast ? chars.space : chars.pipe);
renderTreeNodes(node, childIndent, result, chars);
i++;
}
else {
// (0_o)
i++;
}
}
}
/**
* @description Locate parent nodes in the array to handle nesting.
*/
function findParentNodeIndices(nodes) {
const parentNodeIndices = new Set();
for (let i = 0; i < nodes.length; i++) {
if (typeof nodes[i] === 'string' &&
i + 1 < nodes.length &&
Array.isArray(nodes[i + 1])) {
parentNodeIndices.add(i);
}
}
return parentNodeIndices;
}
/**
* @description
* Determines if there's another string node after the given index.
* Used to decide if the current node is the last at its level.
*/
function hasNextStringNode(nodes, startIndex) {
for (let i = startIndex; i < nodes.length; i++) {
if (typeof nodes[i] === 'string') {
return true;
}
}
return false;
}