2025-07-07 20:58:02 -07:00
|
|
|
// 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,
|
|
|
|
) => {
|
2025-08-02 19:22:07 -07:00
|
|
|
const { render: renderFn } = marko.createTemplate(templateId, renderer);
|
2025-07-07 20:58:02 -07:00
|
|
|
function wrap(props: Record<string, unknown>, n: number) {
|
|
|
|
// Marko Custom Tags
|
|
|
|
const cloverAsyncMarker = { isAsync: false };
|
2025-08-02 19:22:07 -07:00
|
|
|
const r = render.current;
|
2025-07-07 20:58:02 -07:00
|
|
|
// Support using Marko outside of Clover SSR
|
2025-08-02 19:22:07 -07:00
|
|
|
if (!r) return renderer(props, n);
|
|
|
|
const markoResult = renderFn.call(renderer, {
|
|
|
|
...props,
|
|
|
|
$global: { clover: r, cloverAsyncMarker },
|
|
|
|
});
|
|
|
|
if (cloverAsyncMarker.isAsync) {
|
|
|
|
return markoResult.then(render.raw);
|
2025-07-07 20:58:02 -07:00
|
|
|
}
|
2025-08-02 19:22:07 -07:00
|
|
|
const rr = markoResult.toString();
|
|
|
|
return render.raw(rr);
|
2025-07-07 20:58:02 -07:00
|
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
2025-08-02 19:22:07 -07:00
|
|
|
const r = render.current ?? (marko.$global().clover as render.State);
|
2025-07-07 20:58:02 -07:00
|
|
|
if (!r) throw new Error("No Clover Render Active");
|
2025-08-02 19:22:07 -07:00
|
|
|
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>,
|
|
|
|
),
|
|
|
|
);
|
2025-07-07 20:58:02 -07:00
|
|
|
|
|
|
|
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;
|
2025-08-02 19:22:07 -07:00
|
|
|
if (!rejections) return resolve(render.stringifyNode(resolved));
|
2025-07-07 20:58:02 -07:00
|
|
|
(r.rejections ??= []).push(...rejections);
|
|
|
|
return reject(new Error("Render had errors"));
|
|
|
|
};
|
|
|
|
marko.fork(
|
|
|
|
scopeId,
|
|
|
|
accessor,
|
|
|
|
promise,
|
|
|
|
(string: string) => marko.write(string),
|
|
|
|
0,
|
|
|
|
);
|
|
|
|
} else {
|
2025-08-02 19:22:07 -07:00
|
|
|
marko.write(render.stringifyNode(resolved));
|
2025-07-07 20:58:02 -07:00
|
|
|
}
|
|
|
|
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 ||
|
2025-08-02 19:22:07 -07:00
|
|
|
(typeof input === "object" &&
|
|
|
|
input &&
|
2025-07-07 20:58:02 -07:00
|
|
|
// only block this if it's the default `toString`
|
|
|
|
input.toString === Object.prototype.toString)
|
|
|
|
) {
|
|
|
|
throw new Error(
|
|
|
|
`Unexpected value in template placeholder: '` +
|
2025-08-02 19:22:07 -07:00
|
|
|
render.inspect(input) +
|
|
|
|
"'. " +
|
2025-07-07 20:58:02 -07:00
|
|
|
`To emit a literal '${input}', use \${String(value)}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return marko.escapeXML(input);
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Async {
|
|
|
|
isAsync: boolean;
|
|
|
|
}
|
|
|
|
|
2025-08-02 19:22:07 -07:00
|
|
|
import * as render from "#engine/render";
|
2025-07-07 20:58:02 -07:00
|
|
|
import type { ServerRenderer } from "marko/html/template";
|
|
|
|
import { type Accessor } from "marko/common/types";
|
|
|
|
import * as marko from "#marko/html";
|