presentation/renderers/anatomy.renderer.ts

presentation/renderers/anatomy.renderer.ts is a file in Bane's Lab Site. 169 lines of code and 35 definitions.

import type { FolderNode, TreeNode } from "#types/folder.types";
import {
    LINE_INFIX,
    NODE_ATTRIBUTE,
    SOURCE_LOADED_EVENT,
    TREE_LINK_SELECTOR,
} from "#configuration/constants/anatomy.constants";
import { REPOSITORY_LINK_LABEL, repositoryLinkName } from "#configuration/strings/folder.strings";
import { flatten, isFolder, markSelected, renderTree, treeNodesOf } from "#presentation/components/folder.component";
import { nodePath, tabLink } from "#core/assets/link.assets";
import { ANATOMY_PAGE } from "#core/ids/page.ids";
import { HREF_ATTRIBUTE } from "#configuration/constants/element.constants";
import { ICON_BASE_CLASS } from "#configuration/icons/element.icons";
import { NODE_REQUESTED } from "#core/ids/walk.ids";
import { REPOSITORY_ICON } from "#configuration/icons/anatomy.icons";
import type { Tab } from "#types/document.types";
import { createElement } from "#core/factories/element.factory";
import { createExternalLink } from "#presentation/components/link.component";
import { isStaticRender } from "#core/predicates/document.predicate";
import { missingRootLink } from "#configuration/strings/report.strings";
import { renderNode } from "#presentation/renderers/folder.renderer";
import { revealLine } from "#presentation/components/code.component";
import { subscribeEvent } from "#core/buses/base.bus";
import { targetOf } from "#core/converters/folder.converter";
import { treeLabelOf } from "#domain/converters/source.converter";

const CLICK_EVENT = "click";

const revealWhenLoaded = function revealWhenLoaded(view: Element, line: number): void {
    if (revealLine(view, line)) {
        return;
    }
    view.addEventListener(
        SOURCE_LOADED_EVENT,
        () => {
            revealLine(view, line);
        },
        { once: true },
    );
};

interface Explorer {
    readonly article: Element;
    readonly nodes: ReadonlyMap<string, TreeNode>;
    readonly repository: RepositoryLink | null;
    readonly root: FolderNode;
    readonly tab: string;
    readonly tree: Element;
    readonly view: Element;
}

interface RepositoryLink {
    readonly link: HTMLAnchorElement;
    readonly urls: Readonly<Record<string, string>>;
}

const repositoryKey = function repositoryKey(node: TreeNode): string {
    return isFolder(node) ? node.section.id : (node.sub.id ?? node.id);
};

const pointRepository = function pointRepository(repository: RepositoryLink | null, node: TreeNode): void {
    const url = repository?.urls[repositoryKey(node)];
    if (repository !== null && url !== undefined) {
        repository.link.setAttribute(HREF_ATTRIBUTE, url);
    }
};

const repositoryLink = function repositoryLink(tab: Tab, root: FolderNode): RepositoryLink | null {
    const urls = tab.repository;
    if (urls === undefined) {
        return null;
    }
    const first = urls[repositoryKey(root)];
    if (first === undefined) {
        throw new Error(missingRootLink(tab.id));
    }
    const link = createExternalLink(first, "explorer-repository", [
        createElement("i", { className: `${ICON_BASE_CLASS} ${REPOSITORY_ICON}` }),
        createElement("span", { text: REPOSITORY_LINK_LABEL }),
    ]);
    link.setAttribute("aria-label", repositoryLinkName(treeLabelOf(tab.id)));
    return { link, urls };
};

const enableSelection = function enableSelection(explorer: Explorer): void {
    const { article, nodes, repository, root, tab, tree, view } = explorer;
    const built = new Map<string, Element>();
    const select = function select(node: TreeNode, line: number | null): void {
        const element = built.get(node.id) ?? renderNode(root, node);
        built.set(node.id, element);
        view.replaceChildren(element);
        markSelected(tree, node.id);
        pointRepository(repository, node);
        if (line !== null) {
            revealWhenLoaded(view, line);
        }
    };
    const onClick = function onClick(event: Event): void {
        if (!article.isConnected) {
            document.removeEventListener(CLICK_EVENT, onClick, true);
            return;
        }
        const link =
            event.target instanceof Element ? event.target.closest<HTMLAnchorElement>(TREE_LINK_SELECTOR) : null;
        const href = link === null ? null : link.getAttribute(HREF_ATTRIBUTE);
        const target = href === null ? null : targetOf(href);
        const node = target === null ? undefined : nodes.get(target.id);
        if (node === undefined || href === null || target === null) {
            return;
        }
        event.preventDefault();
        event.stopPropagation();
        window.history.replaceState({}, "", href);
        select(node, target.line);
    };
    document.addEventListener(CLICK_EVENT, onClick, true);
    const release = subscribeEvent(NODE_REQUESTED, (event) => {
        if (!article.isConnected) {
            release();
            return;
        }
        const node = nodes.get(event.id);
        if (node === undefined) {
            return;
        }
        const suffix = event.line === null ? "" : LINE_INFIX + String(event.line);
        window.history.replaceState({}, "", tabLink(ANATOMY_PAGE, tab, event.id + suffix));
        select(node, event.line);
    });
    const requested = targetOf(window.location.hash);
    const node = requested === null ? undefined : nodes.get(requested.id);
    select(node ?? root, node === undefined || requested === null ? null : requested.line);
};

const NODES_BY_TAB = new WeakMap<Tab, { readonly nodes: ReadonlyMap<string, TreeNode>; readonly root: FolderNode }>();

export const renderFilesystemSection = function renderFilesystemSection(tab: Tab, id: string): Element | null {
    const held = NODES_BY_TAB.get(tab);
    const root = held?.root ?? treeNodesOf(tab.sections, tab.folders ?? {});
    if (root === null) {
        return null;
    }
    const nodes = held?.nodes ?? new Map(flatten(root).map((node) => [node.id, node] as const));
    NODES_BY_TAB.set(tab, { nodes, root });
    const node = nodes.get(id);
    return node === undefined ? null : renderNode(root, node);
};

export const renderFilesystem = function renderFilesystem(tab: Tab): Element {
    const root = treeNodesOf(tab.sections, tab.folders ?? {});
    const article = createElement("article", { className: "filesystem" });
    if (root === null) {
        return article;
    }
    const view = createElement("div", { className: "filesystem-view" });
    const repository = repositoryLink(tab, root);
    const title = createElement("h2", { className: "explorer-title", text: tab.label });
    article.append(
        repository === null
            ? title
            : createElement("div", { children: [title, repository.link], className: "explorer-title-row" }),
    );
    if (isStaticRender()) {
        view.append(renderNode(root, root));
        const paged = new Set(tab.pages ?? []);
        for (const link of view.querySelectorAll(`[${NODE_ATTRIBUTE}]`)) {
            const node = link.getAttribute(NODE_ATTRIBUTE) ?? "";
            if (paged.has(node)) {
                link.setAttribute(HREF_ATTRIBUTE, nodePath(ANATOMY_PAGE, tab.id, node));
            }
        }
        article.append(createElement("div", { children: [view], className: "filesystem-body" }));
        return article;
    }
    const tree = renderTree(root);
    article.append(createElement("div", { children: [tree, view], className: "filesystem-body" }));
    const nodes = new Map(flatten(root).map((node) => [node.id, node] as const));
    enableSelection({ article, nodes, repository, root, tab: tab.id, tree, view });
    return article;
};