refactor: decompose render pipeline into decorate() + source value
Replace the one branchy bootstrapTemplateWithHTMXetc (six positional
args, five jobs) with small pure functions: headChrome, blissAttrs,
decoratePage, decorateFragment, and a decorate() entry point. The
route/clone/copy provenance triple becomes a single `source` value
({structureId, routeId, copyUrl}) threaded through render — the same
data the inspector roadmap needs.
Collapse the four copy-pasted render sites (res.render, ws.render,
template preview, scaffold preview) onto renderTemplate + decorate,
and dedupe the per-structure route() closure into makeRoute().
Emitted HTML (head chrome + data-bliss-* attributes) is unchanged;
verified full-page and htmx-fragment output.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ae51fc763c
commit
14f9ec3160
1 changed files with 106 additions and 111 deletions
211
index.js
211
index.js
|
|
@ -165,23 +165,15 @@ function bootstrapWebsocketHandler(route) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ws.render = (template, context) => {
|
ws.render = (template, context = {}) => {
|
||||||
context = context || {};
|
|
||||||
const eta = model.getTemplater(route.structure_id);
|
|
||||||
const structure = model.getStructure(route.structure_id);
|
const structure = model.getStructure(route.structure_id);
|
||||||
context.route = function (url) {
|
context.route = makeRoute(structure);
|
||||||
return withPrefix(structure.route_prefix, url);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.send(
|
ws.send(
|
||||||
bootstrapTemplateWithHTMXetc(
|
decorate(renderTemplate(route.structure_id, template, context), {
|
||||||
eta.render(template, context),
|
headInjection: structure.head_injection,
|
||||||
`/workshop/${route.structure_id}/route/${route.id}`,
|
source: sourceFor(route, req),
|
||||||
`/workshop/${route.structure_id}/clone_modal/`,
|
fragment: true,
|
||||||
route.verb == "GET" ? embedHTML(req.originalUrl) : null,
|
}),
|
||||||
structure.head_injection,
|
|
||||||
htmxRequest = true,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -297,30 +289,10 @@ app.all("/logout", async (req, res) => {
|
||||||
// | _ || || | | || _ | _____| || _ || || |
|
// | _ || || | | || _ | _____| || _ || || |
|
||||||
// |__| |__||_______||___| |_||___| |_||_______||__| |__||_______||___|
|
// |__| |__||_______||___| |_||___| |_||_______||__| |__||_______||___|
|
||||||
//
|
//
|
||||||
function bootstrapTemplateWithHTMXetc(
|
// The chrome injected into the <head> of every rendered page: the client-side
|
||||||
htmlString,
|
// stack (htmx/hyperscript/tailwind) plus the in-page editor overlay.
|
||||||
blissRoute,
|
function headChrome(headInjection) {
|
||||||
blissClone,
|
return `
|
||||||
blissCopy,
|
|
||||||
headInjection,
|
|
||||||
htmxRequest = false,
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
htmlString.toLowerCase().startsWith("<html>") ||
|
|
||||||
htmlString.toLowerCase().startsWith("<!doctype") ||
|
|
||||||
!htmxRequest
|
|
||||||
) {
|
|
||||||
const $ = cheerio.load(htmlString);
|
|
||||||
let head = $("head");
|
|
||||||
|
|
||||||
if (head.length === 0) {
|
|
||||||
$("html").prepend("<head></head>");
|
|
||||||
head = $("head");
|
|
||||||
}
|
|
||||||
|
|
||||||
head.attr("id", "head");
|
|
||||||
|
|
||||||
head.append(`
|
|
||||||
<script src="/js/hyperscript.js"></script>
|
<script src="/js/hyperscript.js"></script>
|
||||||
<script src="/js/tailwind.js"></script>
|
<script src="/js/tailwind.js"></script>
|
||||||
<script src="/js/htmx.js"></script>
|
<script src="/js/htmx.js"></script>
|
||||||
|
|
@ -335,48 +307,96 @@ function bootstrapTemplateWithHTMXetc(
|
||||||
</script>
|
</script>
|
||||||
<style> body { margin: 0; }</style>
|
<style> body { margin: 0; }</style>
|
||||||
${headInjection || ""}
|
${headInjection || ""}
|
||||||
`);
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
if (blissRoute) {
|
// A `source` is the provenance of a rendered fragment: which structure/route
|
||||||
$("body").attr("data-bliss-route", blissRoute);
|
// produced it, and (for GET routes) the snippet that re-embeds it. It becomes
|
||||||
$("body").attr("data-bliss-clone", blissClone);
|
// the data-bliss-* attributes the inspector reads.
|
||||||
if (blissCopy) $("body").attr("data-bliss-copy", blissCopy);
|
function blissAttrs(source) {
|
||||||
|
if (!source) return null;
|
||||||
|
const attrs = {
|
||||||
|
"data-bliss-route": `/workshop/${source.structureId}/route/${source.routeId}`,
|
||||||
|
"data-bliss-clone": `/workshop/${source.structureId}/clone_modal/`,
|
||||||
|
};
|
||||||
|
if (source.copyUrl) attrs["data-bliss-copy"] = source.copyUrl;
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full HTML document: inject the head chrome and stamp provenance on <body>.
|
||||||
|
function decoratePage(html, { headInjection, source } = {}) {
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
let head = $("head");
|
||||||
|
if (head.length === 0) {
|
||||||
|
$("html").prepend("<head></head>");
|
||||||
|
head = $("head");
|
||||||
}
|
}
|
||||||
|
head.attr("id", "head");
|
||||||
|
head.append(headChrome(headInjection));
|
||||||
|
|
||||||
htmlString = $.html();
|
const attrs = blissAttrs(source);
|
||||||
} else if (htmxRequest && blissRoute) {
|
if (attrs) {
|
||||||
const $ = cheerio.load(htmlString, null, false);
|
for (const [k, v] of Object.entries(attrs)) $("body").attr(k, v);
|
||||||
|
}
|
||||||
|
return $.html();
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTMX partial: stamp provenance on the swapped-in elements (an oob swap loses
|
||||||
|
// its own attrs, so we descend into its children) and append an out-of-band
|
||||||
|
// <head> so head injection still reaches the page.
|
||||||
|
function decorateFragment(html, { headInjection, source } = {}) {
|
||||||
|
const $ = cheerio.load(html, null, false);
|
||||||
|
const attrs = blissAttrs(source);
|
||||||
|
if (attrs) {
|
||||||
const targets = [];
|
const targets = [];
|
||||||
|
for (const child of $.root().children()) {
|
||||||
// when we return an oob thing from htmx, we'll lose all the attrs when it swaps
|
|
||||||
// so this adds those attrs to the children of the swap if possible
|
|
||||||
for (let child of $.root().children()) {
|
|
||||||
if (child.attribs && child.attribs["hx-swap-oob"]) {
|
if (child.attribs && child.attribs["hx-swap-oob"]) {
|
||||||
for (let childchild of child.children) {
|
for (const grandchild of child.children) {
|
||||||
if (childchild.attribs) {
|
if (grandchild.attribs) targets.push(grandchild);
|
||||||
targets.push(childchild);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
targets.push(child);
|
targets.push(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const target of targets) Object.assign(target.attribs, attrs);
|
||||||
// Directly update attributes without wrapping in Cheerio
|
|
||||||
for (let target of targets) {
|
|
||||||
target.attribs["data-bliss-route"] = blissRoute;
|
|
||||||
target.attribs["data-bliss-clone"] = blissClone;
|
|
||||||
if (blissCopy) target.attribs["data-bliss-copy"] = blissCopy;
|
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
$.html() +
|
||||||
|
`<head id="head" hx-oob-swap="beforeend">${headInjection || ""}</head>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
htmlString = $.html();
|
// Single entry point for turning rendered HTML into a response body. A full
|
||||||
|
// document (or any non-htmx request) becomes a decorated page; an htmx partial
|
||||||
if (htmxRequest) {
|
// carrying provenance becomes a decorated fragment; anything else passes through.
|
||||||
htmlString += `<head id="head" hx-oob-swap="beforeend">${headInjection}</head>`;
|
function decorate(html, { headInjection, source, fragment } = {}) {
|
||||||
}
|
const lower = html.toLowerCase();
|
||||||
|
const isFullDoc =
|
||||||
|
lower.startsWith("<html>") || lower.startsWith("<!doctype");
|
||||||
|
if (isFullDoc || !fragment) {
|
||||||
|
return decoratePage(html, { headInjection, source });
|
||||||
}
|
}
|
||||||
|
if (source) return decorateFragment(html, { headInjection, source });
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
return htmlString;
|
// One prefix-bound route() helper per structure, shared by every render site.
|
||||||
|
function makeRoute(structure) {
|
||||||
|
return (url) => withPrefix(structure.route_prefix, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTemplate(structureId, templateName, context) {
|
||||||
|
return model.getTemplater(structureId).render(templateName, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provenance for a rendered response: GET routes are re-embeddable, so they
|
||||||
|
// carry the copy-embed snippet; other verbs don't.
|
||||||
|
function sourceFor(route, req) {
|
||||||
|
return {
|
||||||
|
structureId: route.structure_id,
|
||||||
|
routeId: route.id,
|
||||||
|
copyUrl: route.verb === "GET" ? embedHTML(req.originalUrl) : null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
app.post("/workshop", (req, res) => {
|
app.post("/workshop", (req, res) => {
|
||||||
|
|
@ -391,7 +411,7 @@ app.get("/workshop", (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderWorkshop(res, template, context) {
|
function renderWorkshop(res, template, context) {
|
||||||
res.send(bootstrapTemplateWithHTMXetc(eta.render(template, context)));
|
res.send(decorate(eta.render(template, context)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function smartRedirect(req, res, redirectUrl) {
|
function smartRedirect(req, res, redirectUrl) {
|
||||||
|
|
@ -599,24 +619,16 @@ app.get("/workshop/:structure_id/template/:template_id", (req, res) => {
|
||||||
app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => {
|
app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => {
|
||||||
const template = model.getTemplate(req.params.template_id);
|
const template = model.getTemplate(req.params.template_id);
|
||||||
const struct = model.getStructure(req.params.structure_id);
|
const struct = model.getStructure(req.params.structure_id);
|
||||||
const eta = model.getTemplater(req.params.structure_id);
|
|
||||||
|
|
||||||
const context = vm.createContext({ it: null });
|
const context = vm.createContext({ it: null });
|
||||||
vm.runInContext(template.test_object, context);
|
vm.runInContext(template.test_object, context);
|
||||||
|
|
||||||
context.it.route = function (url) {
|
context.it.route = makeRoute(struct);
|
||||||
return withPrefix(struct.route_prefix, url);
|
|
||||||
};
|
|
||||||
|
|
||||||
return res.send(
|
return res.send(
|
||||||
bootstrapTemplateWithHTMXetc(
|
decorate(renderTemplate(req.params.structure_id, template.name, context.it), {
|
||||||
eta.render(template.name, context.it),
|
headInjection: struct.head_injection,
|
||||||
null,
|
}),
|
||||||
null,
|
|
||||||
null,
|
|
||||||
struct.head_injection,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -647,21 +659,12 @@ app.get("/workshop/:structure_id/route/:route_id/preview", (req, res) => {
|
||||||
const route = model.getRoute(req.params.route_id);
|
const route = model.getRoute(req.params.route_id);
|
||||||
const struct = model.getStructure(req.params.structure_id);
|
const struct = model.getStructure(req.params.structure_id);
|
||||||
|
|
||||||
const it = {
|
const it = { route: makeRoute(struct) };
|
||||||
route: function (url) {
|
|
||||||
return withPrefix(struct.route_prefix, url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.send(
|
return res.send(
|
||||||
bootstrapTemplateWithHTMXetc(
|
decorate(eta.renderString(route.scaffold_page_content, it), {
|
||||||
eta.renderString(route.scaffold_page_content, it),
|
headInjection: struct.head_injection,
|
||||||
null,
|
}),
|
||||||
null,
|
|
||||||
null,
|
|
||||||
struct.head_injection,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -806,7 +809,6 @@ app.all("*", async (req, res) => {
|
||||||
.get(routeMatch.id);
|
.get(routeMatch.id);
|
||||||
|
|
||||||
const structure = model.getStructure(route.structure_id);
|
const structure = model.getStructure(route.structure_id);
|
||||||
const __urlPrefix = structure.route_prefix;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// todo: only add req res to contexst when running the handler()
|
// todo: only add req res to contexst when running the handler()
|
||||||
|
|
@ -817,21 +819,14 @@ app.all("*", async (req, res) => {
|
||||||
eta,
|
eta,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.render = (template, context) => {
|
res.render = (template, context = {}) => {
|
||||||
context = context || {};
|
context.route = makeRoute(structure);
|
||||||
context.route = function (url) {
|
|
||||||
return withPrefix(__urlPrefix, url);
|
|
||||||
};
|
|
||||||
const eta = model.getTemplater(route.structure_id);
|
|
||||||
res.send(
|
res.send(
|
||||||
bootstrapTemplateWithHTMXetc(
|
decorate(renderTemplate(route.structure_id, template, context), {
|
||||||
eta.render(template, context),
|
headInjection: structure.head_injection,
|
||||||
`/workshop/${route.structure_id}/route/${route.id}`,
|
source: sourceFor(route, req),
|
||||||
`/workshop/${route.structure_id}/clone_modal/`,
|
fragment: req.headers["hx-request"],
|
||||||
verb == "GET" ? embedHTML(req.originalUrl) : null,
|
}),
|
||||||
structure.head_injection,
|
|
||||||
req.headers["hx-request"],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const handlerScript = new vm.Script(route.handler, { filename: `handler_${route.id}.js` });
|
const handlerScript = new vm.Script(route.handler, { filename: `handler_${route.id}.js` });
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue