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
JavaScript
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;
}