add templater

This commit is contained in:
HRG @ SCExC 2024-06-02 10:20:01 -04:00
parent 4d6c343422
commit c33a49e817
8 changed files with 248 additions and 27 deletions

2
.gitignore vendored
View file

@ -1 +1,3 @@
node_modules node_modules
.DS_Store
dbs

103
index.js
View file

@ -3,6 +3,7 @@ const vm = require("node:vm");
const path = require("path"); const path = require("path");
const express = require("express"); const express = require("express");
const session = require("express-session"); const session = require("express-session");
const fileUpload = require("express-fileupload");
const SQLiteStore = require("better-sqlite3-session-store")(session); const SQLiteStore = require("better-sqlite3-session-store")(session);
const betterSqlite3 = require("better-sqlite3"); const betterSqlite3 = require("better-sqlite3");
const { Eta } = require("eta"); const { Eta } = require("eta");
@ -20,6 +21,7 @@ const routes = { GET: [], POST: [], PUT: [], DELETE: [] };
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static("public")); app.use(express.static("public"));
app.use(fileUpload());
const db = betterSqlite3("./prime.db"); const db = betterSqlite3("./prime.db");
@ -209,12 +211,42 @@ function updateDb(db, appDb) {
db.prepare(sql).run(...values); db.prepare(sql).run(...values);
} }
function updateTemplate(db, template) {
const fields = ["content", "name", "test_object"];
const values = fields.map((field) => template[field]);
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
const sql = `UPDATE templates SET ${placeholders} WHERE id = ?`;
values.push(template.id);
db.prepare(sql).run(...values);
}
function getTemplates(db, structureId) { function getTemplates(db, structureId) {
return db return db
.prepare("SELECT * from templates where structure_id = ?") .prepare("SELECT * from templates where structure_id = ?")
.all(structureId); .all(structureId);
} }
function getTemplate(db, templateId) {
return db.prepare("SELECT * from templates where id = ?").get(templateId);
}
function getTemplateContentByName(db, structId, name) {
return db
.prepare(
"SELECT content from templates where structure_id = ? AND name = ?"
)
.get(structId, name);
}
function createTemplate(db, structureId, name, content, testObjectString) {
return db
.prepare(
"INSERT INTO templates (structure_id, name, content, test_object) VALUES (?, ?, ?, ?)"
)
.run(structureId, name, content, testObjectString).lastInsertRowid;
}
function getDbsForStructure(db, structureId) { function getDbsForStructure(db, structureId) {
return db return db
.prepare( .prepare(
@ -280,8 +312,6 @@ function createDb(db, structId, name) {
} }
const { LRUCache } = require("lru-cache"); const { LRUCache } = require("lru-cache");
const { templateSettings } = require("dot");
// we'll figure out max size later
const templateCache = new LRUCache({ max: 100 }); const templateCache = new LRUCache({ max: 100 });
function getTemplater(structId) { function getTemplater(structId) {
@ -293,7 +323,7 @@ function getTemplater(structId) {
return path; return path;
}; };
etaInstance.readFile = function (templateAlias) { etaInstance.readFile = function (templateAlias) {
return getTemplateString(db, templateAlias, structId); return getTemplateContentByName(db, structId, templateAlias).content;
}; };
templateCache.set(structId, etaInstance); templateCache.set(structId, etaInstance);
} }
@ -315,9 +345,8 @@ function getDbInstance(dbId) {
return dbInstance; return dbInstance;
} }
function bootstrapTemplateWithHTMXetc(htmlString, blissRoute) { function bootstrapTemplateWithHTMXetc(htmlString, blissRoute, wrapHTML) {
console.log(htmlString); if (htmlString.startsWith("<html>") || wrapHTML) {
if (htmlString.startsWith("<html>")) {
const $ = cheerio.load(htmlString); const $ = cheerio.load(htmlString);
let head = $("head"); let head = $("head");
@ -487,6 +516,63 @@ app.post("/workshop/:structure_id/db/:db_id/repl", (req, res) => {
} }
}); });
app.post("/workshop/:structure_id/template", (req, res) => {
let name = req.body.name;
const template = createTemplate(
db,
req.params.structure_id,
name,
"<div>henlo <%= it.name %></div>",
"it = { name: 'templates!' };"
);
return smartRedirect(
req,
res,
`/workshop/${req.params.structure_id}/template/${template}`
);
});
app.put("/workshop/:structure_id/template/:template_id", (req, res) => {
let id = req.params.template_id;
let content = req.body.content;
let test_object = req.body.test_object;
updateTemplate(db, { ...getTemplate(db, id), content, test_object });
return res.send("good");
});
app.get("/workshop/:structure_id/template/:template_id", (req, res) => {
const template = getTemplate(db, req.params.template_id);
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/template", {
template: template,
...sidebarStuff(db, req.params.structure_id),
})
)
);
});
app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => {
const template = getTemplate(db, req.params.template_id);
const eta = getTemplater(req.params.structure_id);
const context = vm.createContext({ it: null });
vm.runInContext(template.test_object, context);
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render(template.name, context.it),
null,
true
)
);
});
app.get("/workshop/:structure_id/route/:id", (req, res) => { app.get("/workshop/:structure_id/route/:id", (req, res) => {
const route = getRoute(db, req.params.id); const route = getRoute(db, req.params.id);
return res.send( return res.send(
@ -581,7 +667,6 @@ app.all("*", (req, res) => {
res.rawSend = res.send; res.rawSend = res.send;
res.send = (...args) => { res.send = (...args) => {
console.log(args[0], args[1], "bingo")
res.rawSend( res.rawSend(
bootstrapTemplateWithHTMXetc( bootstrapTemplateWithHTMXetc(
args[0], args[0],
@ -589,6 +674,10 @@ app.all("*", (req, res) => {
) )
); );
}; };
res.render = (template, context) => {
const eta = getTemplater(route.structure_id);
res.send(bootstrapTemplateWithHTMXetc(eta.render(template, context)));
};
return vm.runInContext( return vm.runInContext(
(route.handler += `\n\nhandler(req, res)`), (route.handler += `\n\nhandler(req, res)`),
context context

View file

@ -32,13 +32,13 @@ CREATE TABLE templates (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
content TEXT, content TEXT,
user_id INTEGER,
structure_id INTEGER, structure_id INTEGER,
test_object TEXT DEFAULT "it = {}",
engine TEXT NOT NULL DEFAULT "eta", engine TEXT NOT NULL DEFAULT "eta",
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id),
FOREIGN KEY(structure_id) REFERENCES structures(id) FOREIGN KEY(structure_id) REFERENCES structures(id)
UNIQUE(name, structure_id)
); );
CREATE TABLE routes ( CREATE TABLE routes (

52
package-lock.json generated
View file

@ -18,6 +18,7 @@
"ejs": "^3.1.9", "ejs": "^3.1.9",
"eta": "^3.2.0", "eta": "^3.2.0",
"express": "^4.18.2", "express": "^4.18.2",
"express-fileupload": "^1.5.0",
"express-session": "^1.18.0", "express-session": "^1.18.0",
"jsdom": "^24.1.0", "jsdom": "^24.1.0",
"lru-cache": "^10.2.0", "lru-cache": "^10.2.0",
@ -290,6 +291,17 @@
"ieee754": "^1.1.13" "ieee754": "^1.1.13"
} }
}, },
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -829,6 +841,17 @@
"node": ">= 0.10.0" "node": ">= 0.10.0"
} }
}, },
"node_modules/express-fileupload": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.5.0.tgz",
"integrity": "sha512-jSW3w9evqM37VWkEPkL2Ck5wUo2a8qa03MH+Ou/0ZSTpNlQFBvSLjU12k2nYcHhaMPv4JVvv6+Ac1OuLgUZb7w==",
"dependencies": {
"busboy": "^1.6.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/express-session": { "node_modules/express-session": {
"version": "1.18.0", "version": "1.18.0",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
@ -2189,6 +2212,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": { "node_modules/string_decoder": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@ -2711,6 +2742,14 @@
"ieee754": "^1.1.13" "ieee754": "^1.1.13"
} }
}, },
"busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"requires": {
"streamsearch": "^1.1.0"
}
},
"bytes": { "bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -3136,6 +3175,14 @@
} }
} }
}, },
"express-fileupload": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.5.0.tgz",
"integrity": "sha512-jSW3w9evqM37VWkEPkL2Ck5wUo2a8qa03MH+Ou/0ZSTpNlQFBvSLjU12k2nYcHhaMPv4JVvv6+Ac1OuLgUZb7w==",
"requires": {
"busboy": "^1.6.0"
}
},
"express-session": { "express-session": {
"version": "1.18.0", "version": "1.18.0",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
@ -4075,6 +4122,11 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
}, },
"streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="
},
"string_decoder": { "string_decoder": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",

View file

@ -18,6 +18,7 @@
"ejs": "^3.1.9", "ejs": "^3.1.9",
"eta": "^3.2.0", "eta": "^3.2.0",
"express": "^4.18.2", "express": "^4.18.2",
"express-fileupload": "^1.5.0",
"express-session": "^1.18.0", "express-session": "^1.18.0",
"jsdom": "^24.1.0", "jsdom": "^24.1.0",
"lru-cache": "^10.2.0", "lru-cache": "^10.2.0",

View file

@ -1,16 +1,22 @@
<div class="window" _="on closeModal remove me"> <div class="fixed inset-0 flex items-center justify-center z-10" _="on closeModal remove me">
<div class="title-bar"> <div class="window max-w-[768px]">
<div class="title-bar-text">Command Prompt</div> <div class="title-bar">
<div class="title-bar-controls"> <div class="title-bar-text">Create Template</div>
<button aria-label="Minimize"></button> <div class="title-bar-controls">
<button aria-label="Maximize"></button> <button aria-label="Close" _="on click trigger closeModal"></button>
<button aria-label="Close" _="on click trigger closeModal"></button> </div>
</div> </div>
</div> <div class="window-body">
<div class="window-body"> <form hx-post="/workshop/<%= it.structure.id %>/template" hx-target="#response">
<pre>Microsoft&#10094;R&#10095; Windows DOS <div class="field-row-stacked" style="width: 200px">
&#10094;C&#10095; Copyright Microsoft Corp 1990-2001. <label>Name</label>
<br>C:&#92;WINDOWS&#92;SYSTEM32> You can build a command line easily with a window and pre tag <input name="name" id="name" type="text" />
</pre> </div>
</div> <section class="field-row" style="justify-content: flex-end">
</div> <button type="submit">OK</button>
<button type="button" _="on click trigger closeModal">Cancel</button>
</section>
</form>
<div id="response"></div>
</div>
</div>

View file

@ -24,7 +24,9 @@
<ul> <ul>
<% it.templates.map(template => { %> <% it.templates.map(template => { %>
<li> <li>
<%= template.alias %> <a href="/workshop/<%= it.structure.id %>/template/<%= template.id %>">
<%= template.name %>
</a>
</li> </li>
<% }) %> <% }) %>
<li hx-get="/workshop/<%= it.structure.id %>/new_template_modal" hx-target="#modal-zone" hx-swap="beforeend" class="cursor-pointer"> <li hx-get="/workshop/<%= it.structure.id %>/new_template_modal" hx-target="#modal-zone" hx-swap="beforeend" class="cursor-pointer">
@ -41,8 +43,8 @@
<ul> <ul>
<% it.dbs.map(db => { %> <% it.dbs.map(db => { %>
<li class="<% if (it.db?.id == db.id) { %>font-bold text-green-800<% } %>"> <li class="<% if (it.db?.id == db.id) { %>font-bold text-green-800<% } %>">
<a href="/workshop/<%= it.structure.id %>/db/<%= db.id %>"> <a href="/workshop/<%= it.structure.id %>/db/<%= db.id %>">
<%= db.alias %> <%= db.alias %>
</a> </a>
</li> </li>
<% }) %> <% }) %>

View file

@ -0,0 +1,69 @@
<html>
<head>
<link rel="stylesheet" href="/css/xp.css">
<script src="/js/ace/ace.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
<div class="flex flex-col w-full h-full window">
<div class="title-bar">
<div class="title-bar-text">Workshop - <%~ include('/workshop/structure_name', {structure: it.structure}) %>
</div>
</div>
<div class="flex flex-row flex-grow window-body">
<div class="w-1/5 h-full">
<%~ include('/workshop/sidebar', it) %>
</div>
<div id="main-editor" class="flex flex-grow">
<div id="left-pane" class="w-1/2 flex-grow h-full flex flex-col">
Template
<div id="template-editor" class="w-full flex-grow"><%= it.template.content %></div>
Test Object
<div id="object-editor" class="w-full flex-grow"><%= it.template.test_object %></div>
<button id="save-btn" type="submit">Save</button>
</div>
<div id="preview-area" class="w-1/2 h-full flex flex-col">
<form _="on submit halt the event then set #preview's contentWindow.location.href to #url's value">
<input id="url" class="w-full">
</form>
<iframe id="preview"
src="/workshop/<%= it.template.structure_id %>/template/<%= it.template.id %>/preview"
class="w-full flex-grow"
_="on load set #url's value to my contentWindow.location.href">
</iframe>
</div>
</div>
</div>
</div>
<script>
const templateEditor = ace.edit("template-editor");
templateEditor.setTheme("ace/theme/monokai");
templateEditor.session.setMode("ace/mode/ejs");
const objectEditor = ace.edit("object-editor");
objectEditor.setTheme("ace/theme/monokai");
objectEditor.session.setMode("ace/mode/javascript");
document.getElementById('save-btn').onclick = function() {
fetch('/workshop/<%= it.structure.id %>/template/<%= it.template.id %>', {
method: 'PUT',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
content: templateEditor.getValue(),
test_object: objectEditor.getValue(),
}).toString()
}).then(response => {
if (response.ok) {
document.getElementById('preview').contentWindow.location.reload()
}
});
};
</script>
</body>
</html>