// 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: renderFn } = marko.createTemplate(templateId, renderer); function wrap(props: Record, n: number) { // Marko Custom Tags const cloverAsyncMarker = { isAsync: false }; const r = render.current; // Support using Marko outside of Clover SSR if (!r) return renderer(props, n); const markoResult = renderFn.call(renderer, { ...props, $global: { clover: r, cloverAsyncMarker }, }); if (cloverAsyncMarker.isAsync) { return markoResult.then(render.raw); } const rr = markoResult.toString(); return render.raw(rr); } 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; } const r = render.current ?? (marko.$global().clover as render.State); if (!r) throw new Error("No Clover Render Active"); const subRender = render.init(r.async !== -1, r.addon); const resolved = render.resolveNode( subRender, render.element( tag as render.Component, inputOrArgs as Record, ), ); 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(render.stringifyNode(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(render.stringifyNode(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: '` + render.inspect(input) + "'. " + `To emit a literal '${input}', use \${String(value)}`, ); } return marko.escapeXML(input); } interface Async { isAsync: boolean; } import * as render from "#engine/render"; import type { ServerRenderer } from "marko/html/template"; import { type Accessor } from "marko/common/types"; import * as marko from "#marko/html";