import type { Component, Context, Element, ElementCreator, ElementRenderer, ElementRendererCreator, Location, Page, Props,} from "../domain.ts";import { HTMLEmptyElements } from "../domain.ts";import { path } from "../deps.ts";
export const h: ElementCreator = (type, props, ...children) => { const element: Element = { type, props, children }; if (typeof type !== "string") { element.wantsPages = type.wantsPages; element.needsCss = type.needsCss; } return element;};
export const Fragment = "Fragment";
const renderProps = (props?: unknown): string => { if (!props || typeof props !== "object") return ""; return Object.entries(props).reduce( (all, [attr, value]) => value === false ? all : value === true ? `${all} ${attr}` : `${all} ${attr}="${value}"`, "", );};
const _shouldHaveChildPages = ({ contentPath }: Location) => contentPath.split(path.sep).pop() === "index.md";
const _getChildPagesGlobs = ({ contentPath }: Location): string[] => { const contentDir = path.dirname(contentPath); return [`${contentDir}/!(index).md`, `${contentDir}/*/index.md`];};
export const createRenderer: ElementRendererCreator = (options, getPages) => (contentPage) => { const renderContext = { needsCss: [] as string[], }; const { log } = options; const render: ElementRenderer = async (element) => { let html = "";
element = await element;
if (Array.isArray(element)) { for (const c of element) { html += await render(c); } return html; }
if (element === null) { return ""; }
switch (typeof element) { case "undefined": return ""; case "string": return element; case "number": return element.toString(); case "boolean": return element ? element.toString() : ""; }
if ( typeof element.type === "string" && element.type === Fragment && element.children ) { for (const child of element.children) { html += await render(child); } return html; }
if (typeof element.type === "string") { const { type, props, children } = element;
html += `<${type}${renderProps(props)}>`;
if (HTMLEmptyElements.includes(type)) { children && children.length && options.log?.error( `HTML element ${type} on page ${ contentPage?.location.inputPath } should not have children!`, ); return html; }
if (children) { for (const child of children) { html += await render(child); } }
html += `</${type}>`;
return html; }
const props = element.props as Props || {}; props.children = element.children; if (element.needsCss) { renderContext.needsCss = [ ...renderContext.needsCss, path.join(options.layoutDir, element.needsCss), ]; } const context: Context<unknown, unknown, unknown> = { page: contentPage as Page, needsCss: renderContext.needsCss, get childPages() { if (getPages && _shouldHaveChildPages(this.page.location)) { return getPages(_getChildPagesGlobs(this.page.location)); } return undefined; }, }; if (element.wantsPages) { log?.warning( "DEPRECATED: `Page.wantedPages` Use `Page.children` instead", ); context.wantedPages = getPages && await getPages(element.wantsPages) .then((wantedPages) => wantedPages.filter((page) => page.location.inputPath !== context.page.location.inputPath ) ); }
try { const component = element.type as Component< unknown, unknown, unknown, unknown >; element = await component(props, context); return render(element); } catch (e) { options.log?.error( `Error rendering page ${contentPage?.location.inputPath}`, ); options.log?.error( `Content page: ${JSON.stringify(contentPage, undefined, 2)}`, ); throw e; } }; return render; };