import { FALLBACK_LANGUAGE, SOURCE_LOADED_EVENT } from "#configuration/constants/anatomy.constants"; 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 type { ElementTag } from "#types/element.types"; import { MARKDOWN_CLASS } from "#configuration/constants/markdown.constants"; import type { MarkdownBlock } from "#types/block.types"; 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 { loadSource } from "#core/loaders/walk.loader"; import { parseMarkdown } from "#core/converters/markdown.converter"; import { renderServedSource } from "#presentation/renderers/source.renderer"; import { sourceLocation } from "#assets/walk.assets"; const HEADING_TAGS: readonly ElementTag[] = ["h1", "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" }), 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 ? FALLBACK_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, FALLBACK_LANGUAGE)], className: "document-holder", }); enableSourceNavigation(holder); void loadSource(block.source).then((text) => { if (text === null) { holder.replaceChildren(createPlainCodeBlock(SOURCE_MISSING, block.title, FALLBACK_LANGUAGE)); return; } const rendered = renderMarkdown(text); markDocumentReferences(rendered, block.path); holder.replaceChildren(createElement("div", { className: "document-title", text: block.title }), rendered); holder.dispatchEvent(new Event(SOURCE_LOADED_EVENT, { bubbles: true })); }); return holder; };