112 lines
3.8 KiB
JavaScript
112 lines
3.8 KiB
JavaScript
// lib/page.js
|
|
// The Builder SPA shell page (ARCHITECTURE.md 8.2).
|
|
//
|
|
// renderEditorShell(req, res) builds a COMPLETE, dependency-free HTML document
|
|
// (no React in the served shell) that:
|
|
// (a) loads the compiled Phase-1 editor from the plugin's static public dir,
|
|
// /plugins/public/theme-builder/builderApp.js (plugins.js:1246-1276),
|
|
// (b) bootstraps window.__TB__ = { apiBase, cssRoute, csrfToken } -- the CSRF
|
|
// token is read from req exactly the way core does it (req.csrfToken(),
|
|
// e.g. server/wrapper.js:259, server/app.js:74 where it defaults to ""),
|
|
// (c) exposes a single mount point <div id="theme-builder-root"> the editor
|
|
// attaches to.
|
|
//
|
|
// The function RETURNS the complete document string; when a res is supplied
|
|
// (the apiHandlers.getEditor path) it also res.send()s that document. We send a
|
|
// standalone document rather than res.sendWrap()'ing a body fragment so the SPA
|
|
// owns the whole page and stays free of the core React chrome -- per the task's
|
|
// "COMPLETE HTML document / dependency-free shell" contract.
|
|
|
|
const { API_BASE, CSS_ROUTE, URL_PREFIX, PLUGIN_NAME } = require("./constants");
|
|
|
|
// The plugin's committed editor bundle, served by core's static plugin route.
|
|
const BUNDLE_URL = `/plugins/public/${PLUGIN_NAME}/builderApp.js`;
|
|
|
|
|
|
// Mirror core's CSRF read: req.csrfToken is a function (csurf adds it; in some
|
|
// modes app.js stubs it to return ""). Guard so a missing token never throws.
|
|
function readCsrfToken(req) {
|
|
if (req && typeof req.csrfToken === "function") {
|
|
try {
|
|
return req.csrfToken() || "";
|
|
} catch (e) {
|
|
return "";
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
|
|
// Minimal HTML-escaper for the <title> and any attribute text. The JSON blob is
|
|
// emitted via JSON.stringify with "<" escaped so it cannot break out of the
|
|
// <script> element.
|
|
function escapeHtml(s) {
|
|
return String(s)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
|
|
// JSON for inline <script>: prevent "</script>" / "<!--" from terminating the
|
|
// element, and keep it ASCII-only.
|
|
function safeJson(value) {
|
|
return JSON.stringify(value)
|
|
.replace(/</g, "\\u003c")
|
|
.replace(/>/g, "\\u003e")
|
|
.replace(/&/g, "\\u0026")
|
|
.replace(/[\u2028]/g, "\\u2028")
|
|
.replace(/[\u2029]/g, "\\u2029");
|
|
}
|
|
|
|
|
|
// Build the complete HTML document string. openThemeId (?id=) lets a deep-link
|
|
// open straight into a theme; the editor reads it from the boot blob.
|
|
function buildShellHtml(req) {
|
|
const csrfToken = readCsrfToken(req);
|
|
const openThemeId = (req && req.query && req.query.id) ? String(req.query.id) : null;
|
|
const boot = {
|
|
apiBase: API_BASE,
|
|
cssRoute: CSS_ROUTE,
|
|
base: URL_PREFIX,
|
|
csrfToken,
|
|
openThemeId,
|
|
};
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="robots" content="noindex,nofollow">
|
|
<title>${escapeHtml("Theme Builder")}</title>
|
|
<style>
|
|
html, body { margin: 0; padding: 0; height: 100%; }
|
|
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
|
#theme-builder-root { height: 100%; }
|
|
#theme-builder-root .tb-loading { padding: 2rem; color: #555; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="theme-builder-root"><div class="tb-loading">Loading Theme Builder...</div></div>
|
|
<script>window.__TB__ = ${safeJson(boot)};</script>
|
|
<script src="${BUNDLE_URL}" defer></script>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
|
|
// Public entry point. Returns the document string; if res is provided, sends it.
|
|
function renderEditorShell(req, res) {
|
|
const html = buildShellHtml(req);
|
|
if (res && typeof res.send === "function") {
|
|
res.set("Content-Type", "text/html; charset=utf-8");
|
|
res.send(html);
|
|
}
|
|
return html;
|
|
}
|
|
|
|
|
|
module.exports = { renderEditorShell, buildShellHtml, BUNDLE_URL };
|