134 lines
4.3 KiB
TypeScript
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" },
|
|
],
|
|
};
|