UNPKG

@retronav/thicket

Version:

Components and utilities used by Thicket

149 lines (138 loc) 4.23 kB
import type { MarkdownHeader } from '@astrojs/markdown-remark'; export const SEPERATOR = '/'; /** * A representation of a sidebar node. * This is similar to a node in a tree. */ export class SidebarNode<T extends {} = unknown> { constructor( /** The display name of the node. */ public readonly name: string, /** The type of the node. */ public readonly type: 'directory' | 'node', /** * An array of headings in the node document, if any. * Only used in nodes of type "node". */ public readonly headings: MarkdownHeader[] = [], /** * The filesystem path of the node. Used for navigation. * Should start with a forward slash. */ public readonly path: string = '', /** Child nodes of the node, if any. */ public readonly children: SidebarNode[] = [], /** Custom mutable data, if any. */ public data?: T ) {} } /** * Convert a list of paths to a Tree (Array of SidebarNodes). * @param paths Array of paths to convert * @returns An array of SidebarNodes */ export function fsToTree(paths: { [file: string]: MarkdownHeader[]; }): SidebarNode<SidebarNode>[] { const root: SidebarNode<SidebarNode>[] = []; Object.entries(paths).forEach(([path, heading]) => { const sections = path.split(SEPERATOR); if (sections.length > 1) { let topNodeExists = false; let topNode = root.find((node) => node.name === sections[0]); if (topNode instanceof SidebarNode) topNodeExists = true; else topNode = new SidebarNode( sections[0], 'directory', [], SEPERATOR + sections[0] ); pathToBranch(sections.slice(1), topNode, heading); if (!topNodeExists) root.push(topNode); } else { root.push( new SidebarNode(path, 'node', heading, SEPERATOR + sections[0]) ); } }); return root; } /** * Get a child node in a parent node at a given path. * @param root The parent node * @param path The path to resolve * @returns The resolved node */ export function resolveNode( root: SidebarNode, path: string ): SidebarNode | undefined { function resolve(node: SidebarNode, name: string): SidebarNode | undefined { if (node instanceof SidebarNode) { // Cover for top nodes if (node.name === name) return node; for (const child of node.children) { if (child.name === name) return child; } } } function resolveInner( node: SidebarNode, sections: string[] ): SidebarNode | undefined { const parentNode = resolve(node, sections[0]); if (sections.length === 1) return parentNode; else if (parentNode && sections.length > 1) return resolveInner(parentNode, sections.slice(1)); } const sections = path.split(SEPERATOR); if (sections.length > 1) { return resolveInner(root, sections); } else if (sections.length === 1) { return resolve(root, sections.pop()); } } /** * Convert a path to a nested node structure and append to a parent node. * @param sections The sections in a path seperated by a seperator * @param topNode The top node to put the child nodes in * @param headings Headings for the node * @returns The topNode with its children. */ function pathToBranch( sections: string[], topNode: SidebarNode, headings: MarkdownHeader[] ): void { if (sections.length === 0) return; const parent = topNode.children.find((node) => node.name === sections[0]); if (!parent) { const isNode = sections.length === 1; const child = new SidebarNode( sections[0], isNode ? 'node' : 'directory', isNode ? headings : [], topNode.path + SEPERATOR + sections[0] ); topNode.children.push(child); pathToBranch(sections.slice(1), child, headings); } else { pathToBranch(sections.slice(1), parent, headings); } } /** * Traverse a node and its children. * @param root The node to traverse * @param callback Function which will be run for each node */ export function walkNode( root: SidebarNode, callback: (node: SidebarNode) => void ) { function walk(node: SidebarNode) { callback(node); } walk(root); root.children.forEach((node) => walkNode(node, callback)); }