presentation/renderers/glossary.renderer.ts

presentation/renderers/glossary.renderer.ts is a file in Bane's Lab Site. 111 lines of code and 24 definitions.

import type { Block, GlossaryBlock, GlossaryEntry } from "#types/block.types";
import { GLOSSARY_LETTER_PREFIX, GLOSSARY_LETTER_SEPARATOR } from "#configuration/constants/glossary.constants";
import type { Section, Tab } from "#types/document.types";
import { appendChildren, createElement } from "#core/factories/element.factory";
import { createJumpButton } from "#presentation/components/tab.component";
import { createLink } from "#presentation/components/link.component";
import { fillMarkup } from "#presentation/renderers/text.renderer";
import { renderBlock } from "#presentation/renderers/block.renderer";
import { renderChapterSection } from "#presentation/renderers/chapter.renderer";
import { sectionLabel } from "#presentation/renderers/section.renderer";

const isGlossary = function isGlossary(block: Block): block is GlossaryBlock {
    return block.kind === "glossary";
};

const letterOf = function letterOf(entry: GlossaryEntry): string {
    return entry.term.charAt(0).toUpperCase();
};

const letterId = function letterId(letter: string): string {
    return GLOSSARY_LETTER_PREFIX + letter;
};

export const sortedEntries = function sortedEntries(entries: readonly GlossaryEntry[]): readonly GlossaryEntry[] {
    return [...entries].sort((a, b) => a.term.localeCompare(b.term));
};

export const lettersOf = function lettersOf(entries: readonly GlossaryEntry[]): readonly string[] {
    return [...new Set(sortedEntries(entries).map(letterOf))];
};

const renderName = function renderName(entry: GlossaryEntry): Element {
    return entry.href === undefined
        ? createElement("span", { className: "glossary-name", text: entry.term })
        : createLink(entry.href, "glossary-name", [entry.term]);
};

const renderTerm = function renderTerm(entry: GlossaryEntry): Element {
    const heading = createElement("span", { className: "glossary-heading" });
    heading.append(renderName(entry));
    if (entry.code !== undefined) {
        heading.append(createElement("span", { className: "glossary-code", text: entry.code }));
    }
    const term = createElement("dt", {
        attributes: entry.id === undefined ? {} : { id: entry.id },
        children: [heading],
        className: "glossary-term",
    });
    if (entry.domains !== undefined) {
        term.append(
            createElement("span", {
                className: "glossary-domains",
                text: entry.domains.join(GLOSSARY_LETTER_SEPARATOR),
            }),
        );
    }
    return term;
};

const renderEntry = function renderEntry(entry: GlossaryEntry): readonly Element[] {
    return [
        renderTerm(entry),
        fillMarkup(createElement("dd", { className: "glossary-definition" }), entry.description),
    ];
};

const renderLetterGroup = function renderLetterGroup(letter: string, entries: readonly GlossaryEntry[]): Element {
    const list = createElement("dl", { className: "glossary-list" });
    appendChildren(list, entries.flatMap(renderEntry));
    return createElement("section", {
        attributes: { id: letterId(letter) },
        children: [createElement("h2", { className: "glossary-letter", text: letter }), list],
        className: "glossary-group",
    });
};

export const renderLetterJumps = function renderLetterJumps(tab: Tab): readonly Element[] {
    const entries = tab.sections
        .flatMap((section) => (section.blocks ?? []).filter(isGlossary))
        .flatMap((block) => block.entries);
    return lettersOf(entries).map((letter) => createJumpButton(letter, letter, letterId(letter)));
};

const renderEntries = function renderEntries(block: GlossaryBlock): readonly Element[] {
    const sorted = sortedEntries(block.entries);
    return lettersOf(sorted).map((letter) =>
        renderLetterGroup(
            letter,
            sorted.filter((entry) => letterOf(entry) === letter),
        ),
    );
};

const renderGlossaryBlock = function renderGlossaryBlock(block: Block): readonly Element[] {
    return isGlossary(block) ? renderEntries(block) : [renderBlock(block)];
};

const holdsGlossary = function holdsGlossary(section: Section): boolean {
    return (section.blocks ?? []).some(isGlossary);
};

const renderGlossarySection = function renderGlossarySection(section: Section, sectionIndex: number): Element {
    const element = createElement("section", { attributes: { id: section.id }, className: "glossary-section" });
    const title = createElement("h2", { className: "glossary-section-title", text: section.title });
    title.prepend(
        createElement("span", { className: "subsection-symbol chapter-label", text: sectionLabel(sectionIndex) }),
    );
    element.append(title);
    if (section.intro !== undefined) {
        element.append(fillMarkup(createElement("p", { className: "glossary-intro" }), section.intro));
    }
    appendChildren(element, (section.blocks ?? []).flatMap(renderGlossaryBlock));
    return element;
};

const renderSection = function renderSection(section: Section, sectionIndex: number): Element {
    return holdsGlossary(section)
        ? renderGlossarySection(section, sectionIndex)
        : renderChapterSection(section, sectionIndex);
};

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