// This file is used to integrate Marko into the Clover Engine and Sitegen // To use, replace the "marko/html" import with this file. export * from "#marko/html"; interface BodyContentObject { [x: PropertyKey]: unknown; content: ServerRenderer; } export const createTemplate = ( templateId: string, renderer: ServerRenderer, ) => { const { render } = marko.createTemplate(templateId, renderer); function wrap(props: Record, n: number) { // Marko Custom Tags const cloverAsyncMarker = { isAsync: false }; let r: engine.Render | undefined = undefined; try { r = engine.getCurrentRender(); } catch {} // Support using Marko outside of Clover SSR if (r) { engine.setCurrentRender(null); const markoResult = render.call(renderer, { ...props, $global: { clover: r, cloverAsyncMarker }, }); if (cloverAsyncMarker.isAsync) { return markoResult.then(engine.html); } const rr = markoResult.toString(); return engine.html(rr); } else { return renderer(props, n); } } wrap.render = render; wrap.unwrapped = renderer; return wrap; }; export const dynamicTag = ( scopeId: number, accessor: Accessor, tag: unknown | string | ServerRenderer | BodyContentObject, inputOrArgs: unknown, content?: (() => void) | 0, inputIsArgs?: 1, serializeReason?: 1 | 0, ) => { if (typeof tag === "function") { clover: { const unwrapped = (tag as any).unwrapped; if (unwrapped) { tag = unwrapped; break clover; } let r: engine.Render; try { r = engine.getCurrentRender(); if (!r) throw 0; } catch { r = marko.$global().clover as engine.Render; } if (!r) throw new Error("No Clover Render Active"); const subRender = engine.initRender(r.async !== -1, r.addon); const resolved = engine.resolveNode(subRender, [ engine.kElement, tag, inputOrArgs, ]); if (subRender.async > 0) { const marker = marko.$global().cloverAsyncMarker as Async; marker.isAsync = true; // Wait for async work to finish const { resolve, reject, promise } = Promise.withResolvers(); subRender.asyncDone = () => { const rejections = subRender.rejections; if (!rejections) return resolve(engine.renderNode(resolved)); (r.rejections ??= []).push(...rejections); return reject(new Error("Render had errors")); }; marko.fork( scopeId, accessor, promise, (string: string) => marko.write(string), 0, ); } else { marko.write(engine.renderNode(resolved)); } return; } } return marko.dynamicTag( scopeId, accessor, tag, inputOrArgs, content, inputIsArgs, serializeReason, ); }; export function fork( scopeId: number, accessor: Accessor, promise: Promise, callback: (data: unknown) => void, serializeMarker?: 0 | 1, ) { const marker = marko.$global().cloverAsyncMarker as Async; marker.isAsync = true; marko.fork(scopeId, accessor, promise, callback, serializeMarker); } export function escapeXML(input: unknown) { // The rationale of this check is that the default toString method // creating `[object Object]` is universally useless to any end user. if ( input == null || (typeof input === "object" && input && // only block this if it's the default `toString` input.toString === Object.prototype.toString) ) { throw new Error( `Unexpected value in template placeholder: '` + engine.inspect(input) + "'. " + `To emit a literal '${input}', use \${String(value)}`, ); } return marko.escapeXML(input); } interface Async { isAsync: boolean; } import * as engine from "./ssr.ts"; import type { ServerRenderer } from "marko/html/template"; import { type Accessor } from "marko/common/types"; import * as marko from "#marko/html";