presentation/renderers/markdown.renderer.ts

presentation/renderers/markdown.renderer.ts is a file in Bane's Lab Site. 114 lines of code and 17 definitions.

import type { MarkdownNode, MarkdownParagraph } from "#types/markdown.types";
import { SOURCE_LOADING, SOURCE_MISSING, SOURCE_SERVED_LEAD } from "#configuration/strings/walk.strings";
import { appendChildren, createElement } from "#core/factories/element.factory";
import { enableSourceNavigation, markDocumentReferences } from "#presentation/components/source.component";
import { loadReferences, loadSource } from "#core/loaders/walk.loader";
import type { ElementTag } from "#types/element.types";
import { MARKDOWN_CLASS } from "#configuration/constants/markdown.constants";
import type { MarkdownBlock } from "#types/block.types";
import { SOURCE_LOADED_EVENT } from "#configuration/constants/anatomy.constants";
import { TEXT_LANGUAGE } from "#configuration/constants/code.constants";
import { createPlainCodeBlock } from "#presentation/components/code.component";
import { fillMarkup } from "#presentation/renderers/text.renderer";
import { inlineMarkup } from "#core/normalizers/markdown.normalizer";
import { isStaticRender } from "#core/predicates/document.predicate";
import { parseMarkdown } from "#core/converters/markdown.converter";
import { renderServedSource } from "#presentation/renderers/source.renderer";
import { sourceLocation } from "#core/assets/walk.assets";

const HEADING_TAGS: readonly ElementTag[] = ["h2", "h3", "h4", "h5", "h6"];

const heading = function heading(level: number, text: string): Element {
    const tag = HEADING_TAGS[level - 1] ?? "h6";
    return fillMarkup(
        createElement(tag, { className: `markdown-heading markdown-level-${String(Math.min(level, 4))}` }),
        inlineMarkup(text),
    );
};

const list = function list(ordered: boolean, items: readonly string[]): Element {
    const element = createElement(ordered ? "ol" : "ul", { className: "markdown-list" });
    appendChildren(
        element,
        items.map((item) => fillMarkup(createElement("li"), inlineMarkup(item))),
    );
    return element;
};

const table = function table(headers: readonly string[], rows: readonly (readonly string[])[]): Element {
    const head = createElement("tr");
    appendChildren(
        head,
        headers.map((cell) => fillMarkup(createElement("th"), inlineMarkup(cell))),
    );
    const body = createElement("tbody");
    appendChildren(
        body,
        rows.map((row) => {
            const line = createElement("tr");
            appendChildren(
                line,
                row.map((cell) => fillMarkup(createElement("td"), inlineMarkup(cell))),
            );
            return line;
        }),
    );
    return createElement("table", {
        children: [createElement("thead", { children: [head] }), body],
        className: "markdown-table",
    });
};

const paragraph = function paragraph(node: MarkdownParagraph): Element {
    return fillMarkup(createElement("p", { className: "markdown-text" }), inlineMarkup(node.text));
};

const renderNode = function renderNode(node: MarkdownNode): Element {
    switch (node.kind) {
        case "heading": {
            return heading(node.level, node.text);
        }
        case "code": {
            const language = node.language.length === 0 ? TEXT_LANGUAGE : node.language;
            return createPlainCodeBlock(node.code, language, language);
        }
        case "list": {
            return list(node.ordered, node.items);
        }
        case "quote": {
            return fillMarkup(createElement("blockquote", { className: "markdown-quote" }), inlineMarkup(node.text));
        }
        case "rule": {
            return createElement("hr", { className: "markdown-rule" });
        }
        case "table": {
            return table(node.headers, node.rows);
        }
        case "paragraph": {
            return paragraph(node);
        }
        default: {
            return paragraph(node);
        }
    }
};

export const renderMarkdown = function renderMarkdown(text: string): Element {
    const article = createElement("article", { className: MARKDOWN_CLASS });
    appendChildren(article, parseMarkdown(text).map(renderNode));
    return article;
};

export const renderDocument = function renderDocument(block: MarkdownBlock): Element {
    if (isStaticRender()) {
        return renderServedSource(SOURCE_SERVED_LEAD, sourceLocation(block.source), block.title);
    }
    const holder = createElement("div", {
        children: [createPlainCodeBlock(SOURCE_LOADING, block.title, TEXT_LANGUAGE)],
        className: "document-holder",
    });
    enableSourceNavigation(holder);
    void Promise.all([loadSource(block.source), loadReferences(block.references)]).then(([text, references]) => {
        if (text === null) {
            holder.replaceChildren(createPlainCodeBlock(SOURCE_MISSING, block.title, TEXT_LANGUAGE));
            return;
        }
        const rendered = renderMarkdown(text);
        markDocumentReferences(rendered, references);
        holder.replaceChildren(createElement("div", { className: "document-title", text: block.title }), rendered);
        holder.dispatchEvent(new Event(SOURCE_LOADED_EVENT, { bubbles: true }));
    });
    return holder;
};