UNPKG

element-book

Version:

An [`element-vir`](https://npmjs.com/package/element-vir) drop-in element for building, testing, and demonstrating a collection of elements (or, in other words, a design system).

137 lines (136 loc) 5.12 kB
import { check } from '@augment-vir/assert'; import { BookEntryType } from '../book-entry/book-entry-type.js'; import { isBookEntry } from '../book-entry/book-entry.js'; import { listUrlBreadcrumbs, titleToUrlBreadcrumb } from '../book-entry/url-breadcrumbs.js'; import { bookEntryVerifiers } from '../book-entry/verify-book-entry.js'; import { isBookTreeNodeMarker } from './book-tree-node.js'; import { addTreeToCache, getTreeFromCache } from './tree-cache.js'; export function doesNodeHaveEntryType(node, entryType) { return (check.hasKey(node, 'entry') && check.isObject(node.entry) && node.entry.entryType === entryType); } export function isBookTreeNode(input, entryType) { return isAnyBookTreeNode(input) && input.entry.entryType === entryType; } export function isAnyBookTreeNode(input) { return !!(check.hasKeys(input, [ isBookTreeNodeMarker, 'entry', ]) && input[isBookTreeNodeMarker]); } export function createEmptyBookTreeRoot() { const rootNode = { [isBookTreeNodeMarker]: true, entry: { entryType: BookEntryType.Root, title: '', parent: undefined, errors: [], descriptionParagraphs: [], }, urlBreadcrumb: '', fullUrlBreadcrumbs: [], children: {}, manuallyAdded: true, }; return rootNode; } export function createBookTreeFromEntries({ entries, debug, }) { const cachedTree = getTreeFromCache(entries); if (cachedTree) { return cachedTree; } const tree = createEmptyBookTreeRoot(); entries.forEach((newEntry) => addEntryToTree({ tree, newEntry, debug, manuallyAdded: true })); const flattenedNodes = flattenTree(tree); const bookTree = { tree, flattenedNodes, }; addTreeToCache(entries, bookTree); if (debug) { console.info('element-book tree:', tree); } return bookTree; } function getOrAddImmediateParent(tree, entry, debug) { if (!entry.parent) { return tree; } const immediateParent = traverseToImmediateParent(entry, tree); if (immediateParent) { return immediateParent; } if (debug) { console.info(`parent of ${entry.title} not found in tree; adding it now.`); } addEntryToTree({ tree, newEntry: entry.parent, debug, manuallyAdded: false }); const immediateParentAfterAdding = traverseToImmediateParent(entry, tree); if (!immediateParentAfterAdding) { throw new Error(`Failed to find node despite having just added it: ${listUrlBreadcrumbs(entry, false).join(' > ')}`); } return immediateParentAfterAdding; } function addEntryToTree({ tree, newEntry, debug, manuallyAdded, }) { const errors = bookEntryVerifiers[newEntry.entryType](newEntry); newEntry.errors.push(...errors); const immediateParent = getOrAddImmediateParent(tree, newEntry, debug); const newEntryUrlBreadcrumb = titleToUrlBreadcrumb(newEntry.title); const existingChild = immediateParent.children[newEntryUrlBreadcrumb]; if (existingChild) { // ignores multiple entries that have been added by following parent chains if (manuallyAdded) { if (existingChild.manuallyAdded) { existingChild.entry.errors.push(new Error(`Cannot create duplicate '${newEntryUrlBreadcrumb}'${immediateParent.urlBreadcrumb ? ` in parent '${immediateParent.urlBreadcrumb}'.` : ''}`)); return; } existingChild.manuallyAdded = true; } return; } const newNode = { [isBookTreeNodeMarker]: true, children: {}, urlBreadcrumb: newEntryUrlBreadcrumb, fullUrlBreadcrumbs: [ ...immediateParent.fullUrlBreadcrumbs, newEntryUrlBreadcrumb, ], entry: newEntry, manuallyAdded, }; immediateParent.children[newEntryUrlBreadcrumb] = newNode; if (isBookEntry(newEntry, BookEntryType.Page) && Object.values(newEntry.elementExamples).length) { Object.values(newEntry.elementExamples).forEach((elementExample) => addEntryToTree({ tree, newEntry: elementExample, debug, manuallyAdded })); } } export function traverseToImmediateParent(entryOrNode, currentTree) { const breadcrumbs = isAnyBookTreeNode(entryOrNode) ? entryOrNode.fullUrlBreadcrumbs.slice(0, -1) : listUrlBreadcrumbs(entryOrNode, false); if (!breadcrumbs.length) { return undefined; } const immediateParentNode = breadcrumbs.reduce((currentAncestor, nextBreadcrumb) => { if (!currentAncestor) { return undefined; } return currentAncestor.children[nextBreadcrumb]; }, currentTree); return immediateParentNode; } export function flattenTree(node) { const hasErrors = !!node.entry.errors.length; const childNodes = hasErrors ? [] : Object.values(node.children).map((child) => flattenTree(child)); const entries = [ node, ...childNodes, ].flat(); return entries; }