presentation/renderers/ontology.renderer.ts
presentation/renderers/ontology.renderer.ts is a file in Bane's Lab Site. 185 lines of code and 51 definitions.
import { ANCHOR_PREFIX, SPACE } from "#configuration/constants/document.constants";
import type { Block, GlossaryBlock, GlossaryEntry, ListBlock } from "#types/block.types";
import {
CATEGORIES_LABEL,
DIAGRAM_LABEL,
OF_LABEL,
SEARCH_LABEL,
SEARCH_PLACEHOLDER,
SHOWN_LABEL,
sectionPageLabel,
} from "#configuration/strings/ontology.strings";
import { RECORD_CLASS, SEARCH_ATTRIBUTE, WIDE_FIELD_LENGTH } from "#configuration/constants/ontology.constants";
import type { Section, Subsection, Tab } from "#types/document.types";
import { appendChildren, createElement } from "#core/factories/element.factory";
import { fillMarkup, plainText } from "#presentation/renderers/text.renderer";
import { renderDetails, renderRecordRow } from "#presentation/renderers/record.renderer";
import { ONTOLOGY_PAGE } from "#core/ids/page.ids";
import { createFilterBox } from "#presentation/components/filter.component";
import { isStaticRender } from "#core/predicates/document.predicate";
import { nodePath } from "#core/assets/link.assets";
import { recordPageOf } from "#domain/converters/ontology.converter";
import { renderBlock } from "#presentation/renderers/block.renderer";
import { subsectionId } from "#presentation/renderers/section.renderer";
const GROUP_CLASS = "explorer-group";
const FIELD_CLASS = "record-field";
const WIDE_FIELD_CLASS = "record-field-wide";
const isList = function isList(block: Block): block is ListBlock {
return block.kind === "list";
};
const isGlossary = function isGlossary(block: Block): block is GlossaryBlock {
return block.kind === "glossary";
};
const DIAGRAM_KINDS: ReadonlySet<Block["kind"]> = new Set<Block["kind"]>(["mermaid", "walk"]);
const isDiagram = function isDiagram(block: Block): boolean {
return DIAGRAM_KINDS.has(block.kind);
};
const entryRecord = function entryRecord(entry: GlossaryEntry, index: number, section: Section): Element {
const body = (): readonly Element[] => [
fillMarkup(createElement("p", { className: "record-text" }), entry.description),
];
const id = entry.id ?? subsectionId(section.id, index);
return renderRecordRow({ context: section.title, id, title: entry.term }, [], body, [entry.description]);
};
const fieldClass = function fieldClass(entry: GlossaryEntry): string {
return entry.wide === true || plainText(entry.description).length > WIDE_FIELD_LENGTH
? `${FIELD_CLASS} ${WIDE_FIELD_CLASS}`
: FIELD_CLASS;
};
export const renderFields = function renderFields(block: GlossaryBlock): Element {
const cards = block.entries.map((entry) =>
createElement("div", {
children: [
createElement("span", { className: "record-field-label", text: entry.term }),
fillMarkup(createElement("div", { className: "record-field-value" }), entry.description),
],
className: fieldClass(entry),
}),
);
return createElement("div", { children: cards, className: "record-fields" });
};
const renderBody = function renderBody(block: Block): Element {
return isGlossary(block) ? renderFields(block) : renderBlock(block);
};
const subsectionRecords = function subsectionRecords(sub: Subsection, section: Section, index: number): readonly Element[] {
const blocks = sub.blocks ?? [];
const [first] = blocks;
if (sub.id === undefined && blocks.length === 1 && first !== undefined && isGlossary(first)) {
const parent = { ...section, id: subsectionId(section.id, index) };
return first.entries.map((entry, at) => entryRecord(entry, at, parent));
}
const chips = first !== undefined && isList(first) ? first.items : [];
const rest = blocks.filter((block) => block !== first || !isList(block));
const searchable = blocks.flatMap((block) =>
isGlossary(block) ? block.entries.map((entry) => entry.term + SPACE + entry.description) : [],
);
const body = rest.length === 0 ? null : (): readonly Element[] => rest.map(renderBody);
const row = { context: section.title, id: subsectionId(section.id, index, sub.id), title: sub.title };
return [renderRecordRow(row, chips, body, searchable)];
};
const renderDiagrams = function renderDiagrams(blocks: readonly Block[]): readonly Element[] {
const diagrams = blocks.filter(isDiagram);
if (diagrams.length === 0) {
return [];
}
const panels = (): readonly Element[] =>
diagrams.map((block) => createElement("div", { children: [renderBlock(block)], className: "chapter-panel" }));
return [renderDetails(DIAGRAM_LABEL, panels)];
};
const recordsOf = function recordsOf(section: Section): readonly Element[] {
return section.subsections.flatMap((sub, index) => subsectionRecords(sub, section, index));
};
export const rowWeights = function rowWeights(section: Section): readonly number[] {
return section.subsections.flatMap((sub) => {
const [first] = sub.blocks ?? [];
const entries = sub.id === undefined && sub.blocks?.length === 1 && first !== undefined && isGlossary(first);
return entries ? first.entries.map((entry) => JSON.stringify(entry).length) : [JSON.stringify(sub).length];
});
};
export const rowCount = function rowCount(section: Section): number {
return rowWeights(section).length;
};
const sectionLink = function sectionLink(tab: Tab, section: Section): readonly Element[] {
const count = rowCount(section);
if (count === 0) {
return [];
}
const href = recordPageOf(section.id) ?? nodePath(ONTOLOGY_PAGE, tab.id, section.id);
const link = createElement("a", { attributes: { href }, text: sectionPageLabel(count, section.title) });
return [createElement("p", { children: [link], className: "explorer-section-link" })];
};
const renderGroup = function renderGroup(section: Section, records: readonly Element[], whole = true): Element {
const group = createElement("section", { attributes: { id: section.id }, className: GROUP_CLASS });
group.append(createElement("h2", { className: "explorer-group-title", text: section.title }));
if (section.intro !== undefined) {
group.append(fillMarkup(createElement("p", { className: "explorer-intro" }), section.intro));
}
if (whole) {
appendChildren(group, (section.blocks ?? []).filter((block) => !isDiagram(block)).map(renderBlock));
appendChildren(group, renderDiagrams(section.blocks ?? []));
}
group.append(createElement("div", { children: records, className: "explorer-records" }));
return group;
};
const renderNav = function renderNav(sections: readonly Section[]): Element {
const list = createElement("ul", { className: "explorer-nav-list" });
appendChildren(
list,
sections.map((section) =>
createElement("li", {
children: [
createElement("a", { attributes: { href: ANCHOR_PREFIX + section.id }, text: section.title }),
],
}),
),
);
return createElement("nav", {
children: [createElement("h2", { className: "explorer-nav-title", text: CATEGORIES_LABEL }), list],
className: "explorer-nav",
});
};
const applyQuery = function applyQuery(root: Element, count: Element, query: string): void {
const records = [...root.querySelectorAll<HTMLElement>(`.${RECORD_CLASS}`)];
let shown = 0;
for (const record of records) {
const visible = query.length === 0 || (record.getAttribute(SEARCH_ATTRIBUTE) ?? "").includes(query);
record.hidden = !visible;
shown += visible ? 1 : 0;
}
for (const group of root.querySelectorAll<HTMLElement>(`.${GROUP_CLASS}`)) {
const held = group.querySelector(`.${RECORD_CLASS}`) !== null;
group.hidden = held && group.querySelector(`.${RECORD_CLASS}:not([hidden])`) === null;
}
count.textContent = [String(shown), OF_LABEL, String(records.length), SHOWN_LABEL].join(SPACE);
};
export const renderExplorerSection = function renderExplorerSection(
tab: Tab,
id: string,
rows?: readonly [number, number],
): Element | null {
const section = tab.sections.find((candidate) => candidate.id === id);
if (section === undefined) {
return null;
}
const records = recordsOf(section);
return renderGroup(section, rows === undefined ? records : records.slice(...rows));
};
export const renderExplorer = function renderExplorer(tab: Tab): Element {
const linked = isStaticRender();
const children = tab.sections.map((section) =>
linked ? renderGroup(section, sectionLink(tab, section), rowCount(section) === 0) : renderGroup(section, recordsOf(section)),
);
const groups = createElement("div", { children, className: "explorer-groups" });
const body = createElement("div", { children: [renderNav(tab.sections), groups], className: "explorer-body" });
const title = createElement("h2", { className: "explorer-title", text: tab.label });
if (linked) {
return createElement("article", { children: [title, body], className: "explorer" });
}
const count = createElement("span", { className: "explorer-count" });
const search = createFilterBox(SEARCH_PLACEHOLDER, SEARCH_LABEL, (query) => {
applyQuery(groups, count, query);
});
const toolbar = createElement("div", { children: [search, count], className: "explorer-toolbar" });
applyQuery(groups, count, "");
return createElement("article", { children: [title, toolbar, body], className: "explorer" });
};