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;
};