sitegen/framework/engine/marko-runtime.ts
2025-08-03 00:31:56 -04:00

139 lines
3.8 KiB
TypeScript

// 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<string, unknown>, 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);
render.setCurrent(null);
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<any, any>,
),
);
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<string>();
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<unknown>,
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";