presentation/renderers/chapter.renderer.ts

presentation/renderers/chapter.renderer.ts is a file in Bane's Lab Site. 130 lines of code and 27 definitions.

import {
    PAIRED_SECTION_CLASS,
    PANEL_COLUMN_CLASS,
    PANEL_COLUMN_LIMIT,
    PANEL_FOOTER_CLASS,
    PANEL_FOOTER_COLUMNS_PROPERTY,
    PANEL_KINDS,
} from "#configuration/constants/chapter.constants";
import type { Section, Subsection, Tab } from "#types/document.types";
import { appendChildren, createElement } from "#core/factories/element.factory";
import { renderChapterPanel, resolveCitations } from "#presentation/renderers/panel.renderer";
import { sectionLabel, subsectionId, subsectionLabel } from "#presentation/renderers/section.renderer";
import type { Block } from "#types/block.types";
import type { PanelReference } from "#types/panel.types";
import { chapterPageLabel } from "#configuration/strings/tab.strings";
import { declareStyle } from "#core/registries/style.registry";
import { fillMarkup } from "#presentation/renderers/text.renderer";
import { nodePath } from "#core/assets/link.assets";
import { renderBlock } from "#presentation/renderers/block.renderer";

const isPanel = function isPanel(block: Block): boolean {
    return PANEL_KINDS.has(block.kind);
};

const renderProseBlocks = function renderProseBlocks(blocks: readonly Block[]): Element[] {
    return blocks.filter((block) => !isPanel(block)).map(renderBlock);
};

const panelBlocksOf = function panelBlocksOf(section: Section): Block[] {
    return [...(section.blocks ?? []), ...section.subsections.flatMap((sub) => sub.blocks ?? [])].filter(isPanel);
};

const renderPanels = function renderPanels(section: Section, label: string): PanelReference[] {
    return panelBlocksOf(section).map((block, index) => renderChapterPanel(block, section.id, label, index));
};

const renderFooter = function renderFooter(panels: readonly Element[]): HTMLDivElement {
    const footer = createElement("div", { children: panels, className: PANEL_FOOTER_CLASS });
    declareStyle(footer, PANEL_FOOTER_COLUMNS_PROPERTY, String(Math.min(panels.length, PANEL_COLUMN_LIMIT)));
    return footer;
};

const labeled = function labeled(heading: Element, label: string): Element {
    heading.prepend(createElement("span", { className: "subsection-symbol chapter-label", text: label }));
    return heading;
};

const renderSubsection = function renderSubsection(sub: Subsection, id: string, label: string): Element[] {
    const heading = createElement("h3", { attributes: { id }, className: "chapter-subsection-title", text: sub.title });
    const held: Element[] = [labeled(heading, label)];
    if (sub.content !== undefined) {
        held.push(fillMarkup(createElement("p", { className: "chapter-text" }), sub.content));
    }
    return [...held, ...renderProseBlocks(sub.blocks ?? [])];
};

export const renderChapterSection = function renderChapterSection(section: Section, sectionIndex: number): Element {
    const prose = createElement("div", { className: "chapter-prose" });
    prose.append(
        labeled(
            createElement("h2", { className: "chapter-section-title", text: section.title }),
            sectionLabel(sectionIndex),
        ),
    );
    if (section.intro !== undefined) {
        prose.append(fillMarkup(createElement("p", { className: "chapter-text chapter-lead" }), section.intro));
    }
    appendChildren(prose, renderProseBlocks(section.blocks ?? []));
    appendChildren(
        prose,
        section.subsections.flatMap((sub, index) =>
            renderSubsection(sub, subsectionId(section.id, index, sub.id), subsectionLabel(sectionIndex, index)),
        ),
    );
    const panels = renderPanels(section, sectionLabel(sectionIndex));
    resolveCitations(prose, panels);
    const narrow = panels.filter((panel) => !panel.wide).map((panel) => panel.element);
    const wide = panels.filter((panel) => panel.wide).map((panel) => panel.element);
    const beside = narrow.slice(0, PANEL_COLUMN_LIMIT);
    const below = [...wide, ...narrow.slice(PANEL_COLUMN_LIMIT)];
    const children = [prose];
    if (beside.length > 0) {
        children.push(createElement("div", { children: beside, className: PANEL_COLUMN_CLASS }));
    }
    if (below.length > 0) {
        children.push(renderFooter(below));
    }
    return createElement("section", {
        attributes: { id: section.id },
        children,
        className: beside.length > 0 ? `chapter-section ${PAIRED_SECTION_CLASS}` : "chapter-section",
    });
};

export const renderChapter = function renderChapter(tab: Tab): Element {
    const article = createElement("article", { className: "chapter" });
    article.append(createElement("h2", { className: "chapter-title", text: tab.label }));
    appendChildren(article, tab.sections.map(renderChapterSection));
    return article;
};

const renderSectionEntry = function renderSectionEntry(page: string, tab: Tab, section: Section, index: number): Element {
    const prose = createElement("div", { className: "chapter-prose" });
    prose.append(
        labeled(createElement("h2", { className: "chapter-section-title", text: section.title }), sectionLabel(index)),
    );
    if (section.intro !== undefined) {
        prose.append(fillMarkup(createElement("p", { className: "chapter-text chapter-lead" }), section.intro));
    }
    const href = nodePath(page, tab.id, section.id);
    prose.append(
        createElement("p", {
            children: [createElement("a", { attributes: { href }, text: chapterPageLabel(section.title) })],
            className: "chapter-text chapter-section-link",
        }),
    );
    return createElement("section", { attributes: { id: section.id }, children: [prose], className: "chapter-section" });
};

export const renderChapterIndex = function renderChapterIndex(tab: Tab, page: string): Element {
    const article = createElement("article", { className: "chapter" });
    article.append(createElement("h2", { className: "chapter-title", text: tab.label }));
    appendChildren(
        article,
        tab.sections.map((section, index) => renderSectionEntry(page, tab, section, index)),
    );
    return article;
};

export const renderChapterSectionOf = function renderChapterSectionOf(
    tab: Tab,
    id: string,
    rows?: readonly [number, number],
): Element | null {
    const index = tab.sections.findIndex((candidate) => candidate.id === id);
    const section = tab.sections[index];
    if (section === undefined) {
        return null;
    }
    const shown = rows === undefined ? section : { ...section, subsections: section.subsections.slice(...rows) };
    return renderChapterSection(shown, index);
};