rspace-online/modules/rspace/mod.ts

134 lines
4.3 KiB
TypeScript

/**
* Canvas module — the collaborative infinite canvas.
*
* This is the original rSpace canvas restructured as an rSpace module.
* Routes are relative to the mount point (/:space/canvas in unified mode,
* / in standalone mode).
*/
import { Hono } from "hono";
import { resolve } from "node:path";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
const DIST_DIR = resolve(import.meta.dir, "../../dist");
const routes = new Hono();
/**
* Extract body content and scripts from the full canvas.html page.
* Strips the shell chrome (header, tab-bar, welcome overlay) that renderShell provides,
* and returns just the canvas-specific DOM + inline styles + module scripts.
*/
function extractCanvasContent(html: string): { body: string; styles: string; scripts: string } {
// Extract inline <style> blocks from <head> (canvas CSS)
const headMatch = html.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
const headContent = headMatch?.[1] || "";
const styleBlocks: string[] = [];
headContent.replace(/<style[^>]*>([\s\S]*?)<\/style>/gi, (match) => {
styleBlocks.push(match);
return "";
});
// Extract module script src from <head>
const scriptSrcs: string[] = [];
headContent.replace(/<script[^>]*\bsrc="([^"]+)"[^>]*>/gi, (_m, src: string) => {
if (!src.includes("shell.js")) scriptSrcs.push(src);
return "";
});
// Extract <body> content
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
let bodyContent = bodyMatch?.[1] || "";
// Strip shell chrome that renderShell already provides:
// header, tab-bar (contains only a <rstack-tab-bar>), and welcome overlay.
bodyContent = bodyContent
.replace(/<header[\s\S]*?<\/header>/i, "")
.replace(/<div class="rstack-tab-row"[^>]*>\s*<rstack-tab-bar[^>]*><\/rstack-tab-bar>\s*<\/div>/i, "")
.replace(/<!--\s*Welcome overlay[\s\S]*?<\/div>\s*<\/div>\s*<\/div>/i, "");
const styles = styleBlocks.join("\n");
const scripts = scriptSrcs.map(s => `<script type="module" crossorigin src="${s}"></script>`).join("\n");
return { body: bodyContent.trim(), styles, scripts };
}
// Cache the extracted canvas content (parsed once at startup)
let canvasContentCache: { body: string; styles: string; scripts: string } | null = null;
async function getCanvasContent(): Promise<{ body: string; styles: string; scripts: string }> {
if (canvasContentCache) return canvasContentCache;
// Prefer canvas-module.html (pre-extracted body partial)
const moduleFile = Bun.file(resolve(DIST_DIR, "canvas-module.html"));
if (await moduleFile.exists()) {
canvasContentCache = {
body: await moduleFile.text(),
styles: "",
scripts: `<script type="module" src="/canvas-module.js"></script>`,
};
return canvasContentCache;
}
// Fall back to extracting from full canvas.html
const fullFile = Bun.file(resolve(DIST_DIR, "canvas.html"));
if (await fullFile.exists()) {
canvasContentCache = extractCanvasContent(await fullFile.text());
return canvasContentCache;
}
return {
body: `<div style="padding:2rem;text-align:center;color:#64748b;">Canvas loading...</div>`,
styles: "",
scripts: "",
};
}
// GET / — serve the canvas page wrapped in shell
routes.get("/", async (c) => {
const spaceSlug = c.req.param("space") || c.req.query("space") || "demo";
const canvas = await getCanvasContent();
const html = renderShell({
title: `${spaceSlug} — Canvas | rSpace`,
moduleId: "rspace",
spaceSlug,
body: canvas.body,
modules: getModuleInfoList(),
theme: "dark",
styles: canvas.styles,
scripts: canvas.scripts,
});
return c.html(html);
});
export const canvasModule: RSpaceModule = {
id: "rspace",
name: "rSpace",
icon: "🎨",
description: "Real-time collaborative canvas",
routes,
feeds: [
{
id: "shapes",
name: "Canvas Shapes",
kind: "data",
description: "All shapes on this canvas layer — notes, embeds, arrows, etc.",
filterable: true,
},
{
id: "connections",
name: "Shape Connections",
kind: "data",
description: "Arrow connections between shapes — the canvas graph",
},
],
acceptsFeeds: ["economic", "trust", "data", "attention", "governance", "resource"],
outputPaths: [
{ path: "canvases", name: "Canvases", icon: "🎨", description: "Collaborative infinite canvases" },
],
};