const Koa = require("koa"); const koaStatic = require("koa-static"); const cors = require("@koa/cors"); const fetch = require("node-fetch"); const cookie = require("koa-cookie"); const app = new Koa(); const Router = require("koa-router"); const bodyParser = require("koa-bodyparser"); const router = new Router(); const fs = require("fs"); const rimraf = require("rimraf"); const parser = require("@observablehq/parser"); const CompilerModule = require("@alex.garcia/unofficial-observablehq-compiler"); const BASE = __dirname; //To edit set this on your browser and refresh //document.cookie = "secret=editor" const gen_viewer_markup = (url, data, isEditor) => `<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="data:," /> <link rel="stylesheet" href="/index.css" /> </head> <body class="${ isEditor ? "" : "IridiumViewOnly" }" data-hint-view-only="IridiumViewOnly"> <div id="iridium-root-wrapper"> <div id="iridium-root"></div> </div> <script src="/index.js"></script> <script> // Override with custom load, save, new, delete, list promises. Iridium.load = () => { return new Promise((yes, no) => { yes(${data}); }); }; Iridium.save = (name,data) => { return new Promise((yes, no) => { fetch("/save", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ content: data, path: "${url}" }), }).then(d=>{ yes(true); }) }); }; </script> <style> .IridiumTitle:after { content: "${url}"; } </style> </body> </html> `; app.use(bodyParser()); router.post("/proxy", async (ctx) => { ctx.body = await fetch(ctx.request.body.url, ctx.request.body.options) .then((d) => { return d.text(); }) .catch((d) => {}); }); router.get("/.list", async (ctx) => { ctx.body = fs.readdirSync(`${BASE}/.iridium-notebooks/`); }); router.post("/save", async (ctx) => { const cookies = ctx.cookie; const editor = cookies && cookies.secret == "editor"; if (editor) { const body = ctx.request.body; const notebook_url = body.path; const notebook_file_path = `${BASE}/.iridium-notebooks/${notebook_url}.ojs`; const compiled_file_path = `${BASE}/.iridium-compiled/${notebook_url}.js`; const ojs = cell_array_to_ojs(body.content); const js = source_to_esm(ojs); fs.writeFileSync(notebook_file_path, ojs); fs.writeFileSync(compiled_file_path, js); ctx.body = { status: `{"STATUS":"OK"}`, }; } else { ctx.status = 403; } }); const valid_url_only = (str) => { return ( "/" + str .replace(/^\//, "") .replace(/\/.*$/, "") .replace(/[^a-zA-Z0-9]/g, "-") ); }; app.use(async (ctx, next) => { try { //try static routes first await next(); const status = ctx.status || 404; if (status === 404) { ctx.throw(404); } } catch (err) { ctx.url = valid_url_only(ctx.url); const cookies = ctx.cookie; ctx.type = "html"; const editor = cookies && cookies.secret == "editor"; const notebook_url = ctx.url.replace(/^\//, ""); const notebook_file_path = `${BASE}/.iridium-notebooks/${notebook_url}.ojs`; let notebook_content = "md`# 404!`"; if (fs.existsSync(notebook_file_path)) { notebook_content = fs.readFileSync(notebook_file_path, { encoding: "utf8", }); } ctx.body = gen_viewer_markup( notebook_url, JSON.stringify(notebook_content_to_cells(notebook_content), null, 4), editor ); } }); const notebook_content_to_cells = (source) => { var parsed = parser.parseModule(source); var ends = parsed.cells.map((d) => d.end); var contents = ends .map((d, i) => { return source.substring(i ? ends[i - 1] : 0, d); }) .map((d, i) => { return { id: i, sourceCode: d .replace(/^\n+/g, "") .replace(/^\/\*PIN\*\//g, "") .replace(/^\n+/g, "") .replace(/;$/, ""), pin: d.startsWith("/*PIN*/") || d.startsWith("\n/*PIN*/"), }; }); return contents; }; const cell_array_to_ojs = (cells) => { return cells .map((d) => { return `${d.pin ? "/*PIN*/" : ""} ${d.sourceCode};`; }) .join("\n"); }; const source_to_esm = (source) => { const compiler = new CompilerModule.Compiler({ resolveImportPath: (p) => { return p; }, }); return compiler.module(source); }; app.use(cookie.default()); app.use(cors()); app.use(router.routes()); app.use(koaStatic(`${BASE}/editor`)); //static server for editor app.use(koaStatic(`${BASE}/files`)); //static server for files app.use(koaStatic(`${BASE}/.iridium-compiled`)); //static server for compiled js for imports app.listen(80); //http