this repo has no description
at main 200 lines 5.9 kB view raw
1/** 2 * The strict tree input format. Must start with a string. 3 * This type is exported for testing purposes and advanced usage. 4 */ 5export type TreeInput = Array<string | TreeInput> 6 7/** 8 * Represents a node in the tree structure. 9 * Can be either a string (a leaf node) or an array of TreeNodes (a branch with children). 10 */ 11type TreeNode = string | TreeNode[] 12 13/** 14 * Flexible input type that accepts any array. 15 * Runtime validation ensures the first element is a string. 16 */ 17type FlexibleTreeInput = readonly (string | unknown[])[] 18 19/** 20 * ASCII characters used to render the tree. 21 */ 22export type TreeChars = { 23 branch: string 24 lastBranch: string 25 pipe: string 26 space: string 27} 28 29const DEFAULT_CHARS: TreeChars = { 30 branch: '├─ ', 31 lastBranch: '└─ ', 32 pipe: '│ ', 33 space: ' ', 34} as const 35const SPACE = ' ' as const 36const EMPTY_CHARS: TreeChars = { 37 branch: SPACE.repeat(DEFAULT_CHARS.branch.length), 38 lastBranch: SPACE.repeat(DEFAULT_CHARS.lastBranch.length), 39 pipe: SPACE.repeat(DEFAULT_CHARS.pipe.length), 40 space: SPACE.repeat(DEFAULT_CHARS.space.length), 41} as const 42 43/** 44 * @description Creates a text tree representation from a nested array structure using Unicode box-drawing characters. 45 * 46 * The expected input format is a hierarchical structure where: 47 * - The first element must be a string (the root node) 48 * - String elements represent nodes at the current level 49 * - Array elements following a string represent the children of the previous node 50 * - Nested arrays create deeper levels in the tree 51 * 52 * Examples of supported formats: 53 * - `['root', ['child1', 'child2', 'child3']]` creates a root with three children 54 * - `['root', 'second', ['child1', 'child2']]` creates multiple root nodes with children 55 * - `['root', ['child1', ['grandchild1', 'grandchild2']]]` creates a root with nested children 56 * - `['root', ['childA', ['grandchildA'], 'childB']]` creates multiple branches 57 * 58 * The output uses Unicode box-drawing characters to visualize the tree structure. 59 * 60 * @param list {FlexibleTreeInput} - An array representing the tree structure. First element must be a string. 61 * @param options {Object} - An object containing optional configuration: 62 * - `chars` {TreeChars} - Custom characters for the tree. Defaults to Unicode box-drawing characters. 63 * - `plain` {boolean} - Whether to use plain whitespace characters instead of Unicode box-drawing characters. 64 * 65 * @returns {string} A string containing the tree representation 66 * 67 * @example 68 * treeify(['root', ['child1', 'child2', ['grandchild']]]) 69 * // root 70 * // ├─ child1 71 * // └─ child2 72 * // └─ grandchild 73 */ 74export function treeify( 75 list: FlexibleTreeInput, 76 options?: { 77 chars?: TreeChars 78 plain?: boolean 79 }, 80): string { 81 if (!Array.isArray(list) || list.length === 0) return '' 82 if (list[0] === undefined) return '' 83 if (typeof list[0] !== 'string') 84 throw new Error('First element must be a string') 85 86 let chars = DEFAULT_CHARS 87 if (options?.plain) chars = EMPTY_CHARS 88 if (options?.chars) chars = options.chars 89 90 const result: string[] = [] 91 92 result.push(list[0]) // first string is the root 93 94 let i = 1 95 while (i < list.length) { 96 const node = list[i] 97 98 if (typeof node === 'string') { 99 // add strings here 100 result.push(node) 101 i++ 102 } else if (Array.isArray(node)) { 103 // array is the children of the previous item 104 renderTreeNodes(node, '', result, chars) 105 i++ 106 } else { 107 // idk. skip it. 108 i++ 109 } 110 } 111 112 return result.join('\n') 113} 114 115/** 116 * @description Renders tree nodes with appropriate ASCII indentation and branching 117 */ 118function renderTreeNodes( 119 nodes: TreeNode[], 120 indent: string, 121 result: string[], 122 chars: TreeChars, 123): void { 124 if (!Array.isArray(nodes) || nodes.length === 0) return 125 126 const parentNodeIndices = findParentNodeIndices(nodes) 127 128 let i = 0 129 while (i < nodes.length) { 130 const node = nodes[i] 131 const isParentNode = parentNodeIndices.has(i) 132 133 if (isParentNode) { 134 const parentIndex = i 135 const childrenIndex = i + 1 136 const stringNode = nodes[parentIndex] as string 137 const arrayNode = nodes[childrenIndex] as TreeNode[] 138 139 const isLast = !hasNextStringNode(nodes, childrenIndex + 1) 140 141 const prefix = isLast ? chars.lastBranch : chars.branch 142 result.push(indent + prefix + stringNode) 143 144 // children with increased indent 145 const childIndent = indent + (isLast ? chars.space : chars.pipe) 146 renderTreeNodes(arrayNode, childIndent, result, chars) 147 148 // skip both the parent node and its children array 149 i += 2 150 } else if (typeof node === 'string') { 151 // string is simple. add it. 152 const isLast = !hasNextStringNode(nodes, i + 1) 153 const prefix = isLast ? chars.lastBranch : chars.branch 154 result.push(indent + prefix + node) 155 i++ 156 } else if (Array.isArray(node)) { 157 // (>_>) 158 const isLast = i === nodes.length - 1 159 const childIndent = indent + (isLast ? chars.space : chars.pipe) 160 renderTreeNodes(node, childIndent, result, chars) 161 i++ 162 } else { 163 // (0_o) 164 i++ 165 } 166 } 167} 168 169/** 170 * @description Locate parent nodes in the array to handle nesting. 171 */ 172function findParentNodeIndices(nodes: TreeNode[]): Set<number> { 173 const parentNodeIndices = new Set<number>() 174 175 for (let i = 0; i < nodes.length; i++) { 176 if ( 177 typeof nodes[i] === 'string' && 178 i + 1 < nodes.length && 179 Array.isArray(nodes[i + 1]) 180 ) { 181 parentNodeIndices.add(i) 182 } 183 } 184 185 return parentNodeIndices 186} 187 188/** 189 * @description 190 * Determines if there's another string node after the given index. 191 * Used to decide if the current node is the last at its level. 192 */ 193function hasNextStringNode(nodes: TreeNode[], startIndex: number): boolean { 194 for (let i = startIndex; i < nodes.length; i++) { 195 if (typeof nodes[i] === 'string') { 196 return true 197 } 198 } 199 return false 200}