Skip to main content
Module

x/lume/core/renderer.ts

πŸ”₯ Static site generator for Deno πŸ¦•
Very Popular
Go to Latest
File
import { concurrent } from "./utils.ts";import { Exception } from "./errors.ts";import { Page } from "./filesystem.ts";
import type { Content, Data, Formats, IncludesLoader, PagePreparer, Processors,} from "../core.ts";
export interface Options { includesLoader: IncludesLoader; pagePreparer: PagePreparer; prettyUrls: boolean | "no-html-extension"; preprocessors: Processors; formats: Formats;}
/** * The renderer is responsible for rendering the site pages * in the right order and using the right template engine. */export default class Renderer { /** To load the includes files (layouts) */ includesLoader: IncludesLoader;
/** To convert the urls to pretty /example.html => /example/ */ prettyUrls: boolean | "no-html-extension";
/** All preprocessors */ preprocessors: Processors;
/** To prepare the autogenerated pages */ pagePreparer: PagePreparer;
/** Available file formats */ formats: Formats;
/** The registered helpers */ helpers = new Map<string, [Helper, HelperOptions]>();
constructor(options: Options) { this.includesLoader = options.includesLoader; this.prettyUrls = options.prettyUrls; this.preprocessors = options.preprocessors; this.pagePreparer = options.pagePreparer; this.formats = options.formats; }
/** Register a new helper used by the template engines */ addHelper(name: string, fn: Helper, options: HelperOptions) { this.helpers.set(name, [fn, options]);
for (const format of this.formats.entries.values()) { format.engines?.forEach((engine) => engine.addHelper(name, fn, options)); }
return this; }
/** Render the provided pages */ async renderPages(from: Page[], to: Page[], onDemand: Page[]): Promise<void> { for (const group of this.#groupPages(from)) { const pages: Page[] = []; const generators: Page[] = [];
// Split regular pages and generators for (const page of group) { if (isGenerator(page.data.content)) { generators.push(page); continue; }
if (page.data.ondemand) { onDemand.push(page); continue; } pages.push(page); }
// Preprocess the pages and add them to site.pages await this.preprocessors.run(pages); to.push(...pages);
const generatedPages: Page[] = []; for (const page of generators) { const data = { ...page.data }; const { content } = data; delete data.content;
const generator = await this.render<Generator<Data, Data>>( content, data, page.src.path + page.src.ext, );
let index = 0; const basePath: string | false = typeof page.data.url === "string" ? page.data.url : false;
for await (const data of generator) { if (!data.content) { data.content = null; } const newPage = page.duplicate(index++, data); newPage.data = this.pagePreparer.getData(newPage, page.data); newPage.data.url = basePath ? this.pagePreparer.getUrl(newPage, basePath) : false; newPage.data.date = this.pagePreparer.getDate(newPage); generatedPages.push(newPage); } }
// Preprocess the generators and add them to site.pages await this.preprocessors.run(generatedPages); to.push(...generatedPages);
// Render the pages content const renderedPages: [Page, Content][] = []; await concurrent( pages.concat(generatedPages), async (page) => { try { // If the page is an asset, just return the content without rendering if (this.formats.get(page.src.ext || "")?.asset) { page.content = page.data.content as Content; return; } const content = await this.#renderPage(page); renderedPages.push([page, content]); } catch (cause) { throw new Exception("Error rendering this page", { cause, page }); } }, );
// Render the pages layouts await concurrent( renderedPages, async ([page, content]) => { try { page.content = await this.#renderLayout(page, content); } catch (cause) { throw new Exception("Error rendering the layout of this page", { cause, page, }); } }, ); } }
/** Render the provided pages */ async renderPageOnDemand(page: Page): Promise<void> { if (isGenerator(page.data.content)) { throw new Exception("Cannot render a multiple page on demand.", { page, }); }
await this.preprocessors.run([page]);
if (this.formats.get(page.src.ext || "")?.asset) { page.content = page.data.content as Content; } else { const content = await this.#renderPage(page); page.content = await this.#renderLayout(page, content); } }
/** Render a template */ async render<T>( content: unknown, data: Data, filename: string, ): Promise<T> { const engines = this.#getEngine(filename, data);
if (engines) { for (const engine of engines) { content = await engine.render(content, data, filename); } }
return content as T; }
/** Group the pages by renderOrder */ #groupPages(pages: Page[]): Page[][] { const renderOrder: Record<number | string, Page[]> = {};
for (const page of pages) { const order = page.data.renderOrder || 0; renderOrder[order] = renderOrder[order] || []; renderOrder[order].push(page); }
return Object.keys(renderOrder).sort().map((order) => renderOrder[order]); }
/** Render a page */ async #renderPage(page: Page): Promise<Content> { const data = { ...page.data }; const { content } = data; delete data.content;
return await this.render<Content>( content, data, page.src.path + page.src.ext, ); }
/** Render the page layout */ async #renderLayout(page: Page, content: Content): Promise<Content> { let data = { ...page.data }; let path = page.src.path + page.src.ext; let layout = data.layout;
// Render the layouts recursively while (layout) { const format = this.formats.search(layout);
if (!format || !format.pageLoader) { throw new Exception( "There's no handler for this layout format", { layout }, ); }
const result = await this.includesLoader.load(layout, format, path);
if (!result) { throw new Exception( "Couldn't load this layout", { layout }, ); }
delete data.layout; delete data.templateEngine;
const [layoutPath, layoutData] = result;
data = { ...layoutData, ...data, content, };
content = await this.render<Content>( layoutData.content, data, layoutPath, ); layout = layoutData.layout; path = layoutPath; }
return content; }
/** Get the engines assigned to an extension or configured in the data */ #getEngine(path: string, data: Data): Engine[] | undefined { let { templateEngine } = data;
if (templateEngine) { templateEngine = Array.isArray(templateEngine) ? templateEngine : templateEngine.split(",");
return templateEngine.reduce((engines, name) => { const format = this.formats.get(`.${name.trim()}`);
if (format?.engines) { return engines.concat(format.engines); }
throw new Exception( "Invalid value for templateEngine", { path, templateEngine }, ); }, [] as Engine[]); }
return this.formats.search(path)?.engines; }}
/** * Check if the content of a page is a generator. * Used to generate multiple pages */function isGenerator(content: unknown) { if (typeof content !== "function") { return false; }
const name = content.constructor.name; return (name === "GeneratorFunction" || name === "AsyncGeneratorFunction");}
/** An interface used by all template engines */export interface Engine<T = string | { toString(): string }> { /** Delete a cached template */ deleteCache(file: string): void;
/** Render a template (used to render pages) */ render( content: unknown, data?: Data, filename?: string, ): T | Promise<T>;
/** Render a template synchronous (used to render components) */ renderSync( content: unknown, data?: Data, filename?: string, ): T;
/** Add a helper to the template engine */ addHelper( name: string, fn: Helper, options: HelperOptions, ): void;}
/** A generic helper to be used in template engines */// deno-lint-ignore no-explicit-anyexport type Helper = (...args: any[]) => any;
/** The options for a template helper */export interface HelperOptions { /** The type of the helper (tag, filter, etc) */ type: string;
/** Whether the helper returns an instance or not */ async?: boolean;
/** Whether the helper has a body or not (used for tag types) */ body?: boolean;}