Import the exercise builder
The drag-and-drop workspace for building BI122G exercise sheets, served by the Workshop at /projects/exercise-builder. Plain HTML, CSS and ES modules; no build step. It runs entirely in the browser — work in progress lives in localStorage and finished sheets leave as downloads, so nothing is written on the server. This directory is canonical. It began as a deployment target for deploy_builder.sh syncing from the BI122G course folder, and that arrangement has ended; see the README. Includes the sheet's light/dark switch: the stylesheet already carried both themes and followed the reader's system setting, so this adds the control that overrides it — a corner toggle built by sheet.js, applied before first paint by a head script the exporter writes, and hidden in print. The builder's toolbar previews the same attribute on the canvas. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ed5kU3HfZq9dfj7EYj5J2v
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
/* =====================================================================
|
||||
Assembling and reading whole sheet files.
|
||||
|
||||
The preview iframe and the exported file are built from the same
|
||||
renderer, so what you arrange on the canvas is what lands on disk.
|
||||
The only difference is that the preview carries editing chrome and
|
||||
omits the click-to-copy script, because on the canvas a click selects.
|
||||
===================================================================== */
|
||||
|
||||
import { renderBody } from './render.js';
|
||||
|
||||
let cache = null;
|
||||
|
||||
export async function assets() {
|
||||
if (!cache) {
|
||||
const [css, js, logo] = await Promise.all([
|
||||
fetch('./sheet.css').then((r) => r.text()),
|
||||
fetch('./sheet.js').then((r) => r.text()),
|
||||
fetch('./logo.txt').then((r) => r.text()).catch(() => ''),
|
||||
]);
|
||||
cache = { css, js, logo: logo.trim() };
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
const esc = (s) => String(s).replace(/[&<>]/g,
|
||||
(c) => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
||||
|
||||
function title(doc) {
|
||||
const code = (doc.meta.courseCode || '').trim() || 'Exercise';
|
||||
return `${code} · ${doc.meta.exercise} · ${doc.meta.title}`;
|
||||
}
|
||||
|
||||
/** A complete, self-contained sheet — what Export writes. */
|
||||
export function wholeFile(doc, { css, js }) {
|
||||
return [
|
||||
'<!DOCTYPE html>',
|
||||
'<html lang="en">',
|
||||
'<head>',
|
||||
'<meta charset="utf-8">',
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
||||
`<title>${esc(title(doc))}</title>`,
|
||||
'',
|
||||
// A reader who overrode their system setting has to have it applied
|
||||
// before the first paint, or every load of a dark sheet starts with a
|
||||
// white flash. That rules out sheet.js, which runs at the end of the
|
||||
// body — so the read happens here and the switch itself is still built
|
||||
// down there. The key is shared with sheet.js; keep the two in step.
|
||||
'<script>',
|
||||
'try{var t=localStorage.getItem("bi122g-sheet-theme");',
|
||||
'if(t==="dark"||t==="light")document.documentElement.setAttribute("data-theme",t)}catch(e){}',
|
||||
'<\/script>',
|
||||
'',
|
||||
'<style>',
|
||||
css.trim(),
|
||||
'</style>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
renderBody(doc),
|
||||
'',
|
||||
'<script>',
|
||||
js.trim(),
|
||||
'<\/script>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/** An empty shell for the canvas; the body is filled on every update. */
|
||||
export function previewShell({ css }, chromeCSS) {
|
||||
return [
|
||||
'<!DOCTYPE html>',
|
||||
'<html lang="en">',
|
||||
'<head><meta charset="utf-8">',
|
||||
'<style>', css.trim(), chromeCSS, '</style>',
|
||||
'</head><body></body></html>',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function download(name, text, type = 'text/html;charset=utf-8') {
|
||||
const url = URL.createObjectURL(new Blob([text], { type }));
|
||||
const a = Object.assign(document.createElement('a'), { href: url, download: name });
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 2000);
|
||||
}
|
||||
Reference in New Issue
Block a user