import type { Ref } from "./types"; export interface TreeNode { /** One `/`-separated part of the name. */ label: string; /** The name up to and including this part; unique within one tree. */ path: string; children: TreeNode[]; /** Set when a ref ends here. A node can be both a ref and a folder (`a` and `a/b`). */ ref?: Ref; } /** * Splits short names by `/` into a tree. Folders come before refs, each sorted by name, * or, with `keepOrder`, in the order of `items` (a folder takes the place of its first item). */ export function buildTree(items: { short: string; ref: Ref }[], keepOrder = false): TreeNode[] { const root: TreeNode = { label: "", path: "", children: [] }; for (const { short, ref } of items) { let node = root; for (const part of short.split("/")) { const path = node.path ? `${node.path}/${part}` : part; let child = node.children.find((c) => c.label === part); if (!child) { child = { label: part, path, children: [] }; node.children.push(child); } node = child; } node.ref = ref; } const sort = (nodes: TreeNode[]) => { nodes.sort( (a, b) => Number(b.children.length > 0) - Number(a.children.length > 0) || a.label.localeCompare(b.label), ); nodes.forEach((n) => sort(n.children)); }; const foldersFirst = (nodes: TreeNode[]) => { // Array.prototype.sort is stable, so items keep their order within each group. nodes.sort((a, b) => Number(b.children.length > 0) - Number(a.children.length > 0)); nodes.forEach((n) => foldersFirst(n.children)); }; (keepOrder ? foldersFirst : sort)(root.children); return root.children; } /** Every ref in the subtree. */ export function refsUnder(node: TreeNode): Ref[] { return [...(node.ref ? [node.ref] : []), ...node.children.flatMap(refsUnder)]; }