UNPKG

sanity

Version:

Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches

111 lines (92 loc) 3.35 kB
import {type Path, pathToString} from 'sanity' interface BaseNode { path: Path } export interface Tree<Node extends BaseNode> { nodes?: Node[] children?: Record<string, Tree<Node>> } /** * Recursively calculates the max length of all the keys in the given validation * tree respecting extra length due to indentation depth. Used to calculate the * padding for the rest of the tree. */ export const maxKeyLength = (children: Record<string, Tree<BaseNode>> = {}, depth = 0): number => { return Object.entries(children) .map(([key, child]) => Math.max(key.length + depth * 2, maxKeyLength(child.children, depth + 1)), ) .reduce((max, next) => (next > max ? next : max), 0) } interface Options<Node extends BaseNode> { node?: Record<string, Tree<Node>> paddingLength: number indent?: string getNodes?: (node: Tree<Node>) => Node[] | undefined getMessage: (node: Node) => string } /** * Recursively formats a given tree into a printed user-friendly tree structure */ export const formatTree = <Node extends BaseNode>({ node = {}, paddingLength, indent = '', getNodes: getLeaves = ({nodes}) => nodes, getMessage, }: Options<Node>): string => { const entries = Object.entries(node) return entries .map(([key, child], index) => { const isLast = index === entries.length - 1 const nextIndent = `${indent}${isLast ? ' ' : '│ '}` const leaves = getLeaves(child) const nested = formatTree({ node: child.children, paddingLength, indent: nextIndent, getNodes: getLeaves, getMessage, }) if (!leaves?.length) { const current = `${indent}${isLast ? '└' : '├'}${key}` return [current, nested].filter(Boolean).join('\n') } const [first, ...rest] = leaves const firstPadding = '.'.repeat(paddingLength - indent.length - key.length) const elbow = isLast ? '└' : '├' const subsequentPadding = ' '.repeat(paddingLength - indent.length + 2) const firstMessage = `${indent}${elbow}${key} ${firstPadding} ${getMessage(first)}` const subsequentMessages = rest .map((marker) => `${nextIndent}${subsequentPadding} ${getMessage(marker)}`) .join('\n') const current = [firstMessage, subsequentMessages].filter(Boolean).join('\n') return [current, nested].filter(Boolean).join('\n') }) .join('\n') } /** * Converts a set of markers with paths into a tree of markers where the paths * are embedded in the tree */ export function convertToTree<const Node extends BaseNode>(nodes: Node[]): Tree<Node> { const root: Tree<Node> = {} // add the markers to the tree function addNode(node: Node, tree: Tree<Node> = root) { // if we've traversed the whole path if (!node.path.length) { if (!tree.nodes) tree.nodes = [] // ensure markers is defined // then add the marker to the front tree.nodes.push(node) return } const [current, ...rest] = node.path const key = pathToString([current]) // ensure the current node has children and the next node if (!tree.children) tree.children = {} if (!(key in tree.children)) tree.children[key] = {} addNode({...node, path: rest}, tree.children[key]) } for (const node of nodes) addNode(node) return root }