···11+/**
22+ * Represents a node in the tree structure.
33+ * Can be either a string (a leaf node) or an array of TreeNodes (a branch with children).
44+ */
55+type TreeNode = string | TreeNode[]
66+77+/**
88+ * The strict tree input format. Must start with a string.
99+ * This type is exported for testing purposes and advanced usage.
1010+ */
1111+export type TreeInput = [string, ...Array<string | TreeNode[]>]
1212+1313+/**
1414+ * Flexible input type that accepts any array.
1515+ * Runtime validation ensures the first element is a string.
1616+ */
1717+type FlexibleTreeInput = readonly (string | unknown[])[]
1818+1919+const CHARS = {
2020+ BRANCH: '├─ ',
2121+ LAST_BRANCH: '└─ ',
2222+ PIPE: '│ ',
2323+ SPACE: ' ',
2424+} as const
2525+2626+/**
2727+ * @description Creates an ASCII tree representation from a nested array structure.
2828+ *
2929+ * The expected input format is a hierarchical structure where:
3030+ * - The first element must be a string (the root node)
3131+ * - String elements represent nodes at the current level
3232+ * - Array elements following a string represent the children of the previous node
3333+ * - Nested arrays create deeper levels in the tree
3434+ *
3535+ * Examples of supported formats:
3636+ * - `['root', ['child1', 'child2', 'child3']]` creates a root with three children
3737+ * - `['root', 'second', ['child1', 'child2']]` creates multiple root nodes with children
3838+ * - `['root', ['child1', ['grandchild1', 'grandchild2']]]` creates a root with nested children
3939+ * - `['root', ['childA', ['grandchildA'], 'childB']]` creates multiple branches
4040+ *
4141+ * The output uses ASCII characters to visualize the tree structure.
4242+ *
4343+ * @param list {FlexibleTreeInput} - An array representing the tree structure. First element must be a string.
4444+ * @returns {string} A string containing the ASCII tree representation
4545+ *
4646+ * @example
4747+ * treeify(['root', ['child1', 'child2', ['grandchild']]])
4848+ * // root
4949+ * // ├─ child1
5050+ * // └─ child2
5151+ * // └─ grandchild
5252+ */
5353+export function treeify(list: FlexibleTreeInput): string {
5454+ if (!Array.isArray(list) || list.length === 0) return ''
5555+ if (list[0] === undefined) return ''
5656+ if (typeof list[0] !== 'string')
5757+ throw new Error('First element must be a string')
5858+5959+ const result: string[] = []
6060+6161+ result.push(list[0]) // first string is the root
6262+6363+ let i = 1
6464+ while (i < list.length) {
6565+ const node = list[i]
6666+6767+ if (typeof node === 'string') {
6868+ // add strings here
6969+ result.push(node)
7070+ i++
7171+ } else if (Array.isArray(node)) {
7272+ // array is the children of the previous item
7373+ renderTreeNodes(node, '', result)
7474+ i++
7575+ } else {
7676+ // idk. skip it.
7777+ i++
7878+ }
7979+ }
8080+8181+ return result.join('\n')
8282+}
8383+8484+/**
8585+ * @description Renders tree nodes with appropriate ASCII indentation and branching
8686+ */
8787+function renderTreeNodes(
8888+ nodes: TreeNode[],
8989+ indent: string,
9090+ result: string[],
9191+): void {
9292+ if (!Array.isArray(nodes) || nodes.length === 0) return
9393+9494+ const parentNodeIndices = findParentNodeIndices(nodes)
9595+9696+ let i = 0
9797+ while (i < nodes.length) {
9898+ const node = nodes[i]
9999+ const isParentNode = parentNodeIndices.has(i)
100100+101101+ if (isParentNode) {
102102+ const parentIndex = i
103103+ const childrenIndex = i + 1
104104+ const stringNode = nodes[parentIndex] as string
105105+ const arrayNode = nodes[childrenIndex] as TreeNode[]
106106+107107+ const isLast = !hasNextStringNode(nodes, childrenIndex + 1)
108108+109109+ const prefix = isLast ? CHARS.LAST_BRANCH : CHARS.BRANCH
110110+ result.push(indent + prefix + stringNode)
111111+112112+ // children with increased indent
113113+ const childIndent = indent + (isLast ? CHARS.SPACE : CHARS.PIPE)
114114+ renderTreeNodes(arrayNode, childIndent, result)
115115+116116+ // skip both the parent node and its children array
117117+ i += 2
118118+ } else if (typeof node === 'string') {
119119+ // string is simple. add it.
120120+ const isLast = !hasNextStringNode(nodes, i + 1)
121121+ const prefix = isLast ? CHARS.LAST_BRANCH : CHARS.BRANCH
122122+ result.push(indent + prefix + node)
123123+ i++
124124+ } else if (Array.isArray(node)) {
125125+ // (>_>)
126126+ const isLast = i === nodes.length - 1
127127+ const childIndent = indent + (isLast ? CHARS.SPACE : CHARS.PIPE)
128128+ renderTreeNodes(node, childIndent, result)
129129+ i++
130130+ } else {
131131+ // (0_o)
132132+ i++
133133+ }
134134+ }
135135+}
136136+137137+/**
138138+ * @description Locate parent nodes in the array to handle nesting.
139139+ */
140140+function findParentNodeIndices(nodes: TreeNode[]): Set<number> {
141141+ const parentNodeIndices = new Set<number>()
142142+143143+ for (let i = 0; i < nodes.length; i++) {
144144+ if (
145145+ typeof nodes[i] === 'string' &&
146146+ i + 1 < nodes.length &&
147147+ Array.isArray(nodes[i + 1])
148148+ ) {
149149+ parentNodeIndices.add(i)
150150+ }
151151+ }
152152+153153+ return parentNodeIndices
154154+}
155155+156156+/**
157157+ * @description
158158+ * Determines if there's another string node after the given index.
159159+ * Used to decide if the current node is the last at its level.
160160+ */
161161+function hasNextStringNode(nodes: TreeNode[], startIndex: number): boolean {
162162+ for (let i = startIndex; i < nodes.length; i++) {
163163+ if (typeof nodes[i] === 'string') {
164164+ return true
165165+ }
166166+ }
167167+ return false
168168+}