diff --git a/example-puzzles/example/7/puzzle.md b/example-puzzles/example/7/puzzle.md index c083655..c9e87d5 100644 --- a/example-puzzles/example/7/puzzle.md +++ b/example-puzzles/example/7/puzzle.md @@ -11,7 +11,13 @@ Some puzzles can have embedded code. Your theme may turn this into a full in-browser development environment! +## Python ## ```python print(open("boop.txt").read()) setanswer(0x58 + 58) ``` + +## JavaScript ## +```javascript +console.log("moo") +``` diff --git a/theme/puzzle.css b/theme/puzzle.css index 0b004f0..7c44117 100644 --- a/theme/puzzle.css +++ b/theme/puzzle.css @@ -48,6 +48,10 @@ font-size: 9pt; flex-grow: 2; } +.controls .language { + font-size: 9pt; + font-style: italic; +} .stdout, .stderr, diff --git a/theme/puzzle.html b/theme/puzzle.html index 908f335..67d799d 100644 --- a/theme/puzzle.html +++ b/theme/puzzle.html @@ -40,6 +40,7 @@ Execution time: 0.03s +
diff --git a/theme/puzzle.mjs b/theme/puzzle.mjs index 1738c24..f60ef02 100644 --- a/theme/puzzle.mjs +++ b/theme/puzzle.mjs @@ -3,7 +3,8 @@ */ import * as moth from "./moth.mjs" import * as common from "./common.mjs" -import * as workspace from "./workspace/workspace.mjs" + +const workspacePromise = import("./workspace/workspace.mjs") const server = new moth.Server(".") @@ -173,7 +174,7 @@ async function loadPuzzle(category, points) { document.querySelector("#answer").pattern = puzzle.AnswerPattern } puzzleElement().innerHTML = puzzle.Body - + console.info("Adding attached scripts...") for (let script of (puzzle.Scripts || [])) { let st = document.createElement("script") @@ -193,30 +194,15 @@ async function loadPuzzle(category, points) { li.appendChild(a) document.getElementById("files").appendChild(li) } - - let codeBlocks = document.querySelectorAll("code[class^=language-]") - for (let i = 0; i < codeBlocks.length; i++) { - console.info(`Loading workspace ${i}...`) - let codeBlock = codeBlocks[i] - let language = "unknown" - let sourceCode = codeBlock.textContent - for (let c of codeBlock.classList) { - let parts = c.split("-") - if ((parts.length == 2) && parts[0].startsWith("lang")) { - language = parts[1] - } + + workspacePromise.then(workspace => { + let codeBlocks = document.querySelectorAll("code[class^=language-]") + for (let i = 0; i < codeBlocks.length; i++) { + let codeBlock = codeBlocks[i] + let id = category + "#" + points + "#" + i + new workspace.Workspace(codeBlock, id, attachmentUrls) } - - let id = category + "#" + points + "#" + i - let element = document.createElement("div") - let template = document.querySelector("template#workspace") - element.classList.add("workspace") - element.appendChild(template.content.cloneNode(true)) - element.workspace = new workspace.Workspace(element, id, sourceCode, language, attachmentUrls) - - // Now swap it in for the pre - codeBlock.parentElement.replaceWith(element) - } + }) console.info("Filling debug information...") for (let e of document.querySelectorAll(".debug")) { @@ -235,7 +221,7 @@ async function loadPuzzle(category, points) { return puzzle } -const confettiPromise = import("https://cdn.jsdelivr.net/npm/canvas-conefetti@1.9.2/+esm") +const confettiPromise = import("https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.2/+esm") async function CorrectAnswer() { setInterval(window.close, 3 * common.Second) diff --git a/theme/workspace/python.mjs b/theme/workspace/python.mjs index 50936d2..b675e74 100644 --- a/theme/workspace/python.mjs +++ b/theme/workspace/python.mjs @@ -1,4 +1,4 @@ -import * as pyodide from "https://cdn.jsdelivr.net/npm/pyodide@0.25.1/pyodide.mjs" // v0.16.1 known good +const pyodidePromise = import("https://cdn.jsdelivr.net/npm/pyodide@0.25.1/pyodide.mjs") const HOME = "/home/web_user" diff --git a/theme/workspace/workspace.mjs b/theme/workspace/workspace.mjs index b3ce2f7..00babe8 100644 --- a/theme/workspace/workspace.mjs +++ b/theme/workspace/workspace.mjs @@ -1,5 +1,6 @@ import {Toast} from "../common.mjs" import "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js" +import * as CodeJar from "https://cdn.jsdelivr.net/npm/codejar@4.2.0" var workers = {} @@ -11,7 +12,6 @@ function loadWorker(language) { worker = new Worker(url, { type: "module", }) - console.info("Loading worker", url, worker) workers[language] = worker } return worker @@ -20,16 +20,29 @@ function loadWorker(language) { export class Workspace { /** * - * @param element {HTMLElement} Element to populate with the workspace + * @param codeBlock {HTMLElement} The element containing the source code * @param id {string} A unique identifier of this workspace - * @param code {string} The "pristine" source code for this workspace - * @param language {string} The language for this workspace * @param attachmentUrls {URL[]} List of attachment URLs */ - constructor(element, id, code, language, attachmentUrls) { - this.element = element - this.originalCode = code - this.language = language + constructor(codeBlock, id, attachmentUrls) { + // Show a progress bar + let loadingElement = document.createElement("progress") + codeBlock.insertAdjacentElement("afterend", loadingElement) + + this.language = "unknown" + for (let c of codeBlock.classList) { + let parts = c.split("-") + if ((parts.length == 2) && parts[0].startsWith("lang")) { + this.language = parts[1] + } + } + + this.element = document.createElement("div") + this.element.classList.add("workspace") + let template = document.querySelector("template#workspace") + this.element.appendChild(template.content.cloneNode(true)) + + this.originalCode = codeBlock.textContent this.attachmentUrls = attachmentUrls this.storageKey = "code:" + id @@ -50,41 +63,66 @@ export class Workspace { this.runButton = this.element.querySelector("button.run") this.revertButton = this.element.querySelector("button.revert") this.fontButton = this.element.querySelector("button.font") + this.element.querySelector(".language").textContent = this.language this.runButton.disabled = true // Load in the editor - this.editor.classList.add("language-" + language) - import("https://cdn.jsdelivr.net/npm/codejar@4.2.0").then((module) => this.editorReady(module)) + this.editor.classList.add("language-" + this.language) + this.jar = CodeJar.CodeJar(this.editor, (editor) => this.highlight(editor), {window: this.window}) + this.jar.updateCode(this.code) + switch (this.language) { + case "python": + this.jar.updateOptions({ + tab: " ", + indentOn: /:$/, + }) + break + } // Load the interpreter - this.initLanguage(language) - + this.initLanguage(this.language) + .then(() => { + codeBlock.parentElement.replaceWith(this.element) + }) + .catch(err => console.warn(`Unable to load ${this.language} interpreter`)) + .finally(() => { + loadingElement.remove() + }) this.runButton.addEventListener("click", () => this.run()) this.revertButton.addEventListener("click", () => this.revert()) this.fontButton.addEventListener("click", () => this.font()) + } - async initLanguage(language) { + initLanguage(language) { let start = performance.now() this.status.textContent = "Initializing..." this.status.appendChild(document.createElement("progress")) - this.worker = loadWorker(language) - await this.workerReady() - let runtime = performance.now() - start - let duration = new Date(runtime).toISOString().slice(11, -1) - this.status.textContent = "Loaded in " + duration - this.runButton.disabled = false + let workerUrl = new URL(language + ".mjs", import.meta.url) + this.worker = new Worker(workerUrl, {type: "module"}) - for (let a of this.attachmentUrls) { - let filename = a.pathname.split("/").pop() - this.workerWget(a) - .then(ret => { - this.stdinfo.appendChild(this.document.createElement("div")).textContent = "Downloaded " + filename + // XXX: There has got to be a cleaner way to do this + return new Promise((resolve, reject) => { + this.worker.addEventListener("error", err => reject(err)) + this.workerMessage({type: "nop"}) + .then(() => { + let runtime = performance.now() - start + let duration = new Date(runtime).toISOString().slice(11, -1) + this.status.textContent = "Loaded in " + duration + this.runButton.disabled = false + + for (let a of this.attachmentUrls) { + let filename = a.pathname.split("/").pop() + this.workerMessage({type: "wget", url: a.href || a}) + .then(ret => { + this.stdinfo.appendChild(this.document.createElement("div")).textContent = "Downloaded " + filename + }) + } + resolve() }) - - } + }) } workerMessage(message) { @@ -140,23 +178,6 @@ export class Workspace { this.linenos.textContent = ltxt } } - - /** - * Called when the editor has imported - * - */ - editorReady(module) { - this.jar = module.CodeJar(this.editor, (editor) => this.highlight(editor), {window: this.window}) - this.jar.updateCode(this.code) - switch (this.language) { - case "python": - this.jar.updateOptions({ - tab: " ", - indentOn: /:$/, - }) - break - } - } setAnswer(answer) { let evt = new CustomEvent("setAnswer", {detail: {value: answer}, bubbles: true, cancelable: true})