Compare commits

...

2 Commits

Author SHA1 Message Date
Neale Pickett 0696e7c61c handle prefers-reduced-motion 2024-04-08 17:22:35 -06:00
Neale Pickett cc74318e15 Workspaces: graceful failover
If the workspace fails to load for any reason, or if the language handler fails to load, we now fall back to displaying no editor, just like a normal code block.
2024-04-08 17:08:24 -06:00
7 changed files with 95 additions and 73 deletions

View File

@ -11,7 +11,13 @@ Some puzzles can have embedded code.
Your theme may turn this into a full in-browser development environment! Your theme may turn this into a full in-browser development environment!
## Python ##
```python ```python
print(open("boop.txt").read()) print(open("boop.txt").read())
setanswer(0x58 + 58) setanswer(0x58 + 58)
``` ```
## JavaScript ##
```javascript
console.log("moo")
```

View File

@ -55,6 +55,11 @@ canvas.wallpaper {
opacity: 0.2; opacity: 0.2;
image-rendering: pixelated; image-rendering: pixelated;
} }
@media (prefers-reduced-motion) {
canvas.wallpaper {
display: none;
}
}
main { main {
max-width: 40em; max-width: 40em;
margin: 1em auto; margin: 1em auto;

View File

@ -48,6 +48,10 @@
font-size: 9pt; font-size: 9pt;
flex-grow: 2; flex-grow: 2;
} }
.controls .language {
font-size: 9pt;
font-style: italic;
}
.stdout, .stdout,
.stderr, .stderr,

View File

@ -40,6 +40,7 @@
<button class="run">Run</button> <button class="run">Run</button>
<button class="font" title="Switch in and out of monospace font">Font</button> <button class="font" title="Switch in and out of monospace font">Font</button>
<span class="status">Execution time: 0.03s</span> <span class="status">Execution time: 0.03s</span>
<span class="language"></span>
<button class="revert" title="Reset code to original">Revert</button> <button class="revert" title="Reset code to original">Revert</button>
</div> </div>
<div class="output"> <div class="output">

View File

@ -3,7 +3,8 @@
*/ */
import * as moth from "./moth.mjs" import * as moth from "./moth.mjs"
import * as common from "./common.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(".") const server = new moth.Server(".")
@ -194,29 +195,14 @@ async function loadPuzzle(category, points) {
document.getElementById("files").appendChild(li) document.getElementById("files").appendChild(li)
} }
workspacePromise.then(workspace => {
let codeBlocks = document.querySelectorAll("code[class^=language-]") let codeBlocks = document.querySelectorAll("code[class^=language-]")
for (let i = 0; i < codeBlocks.length; i++) { for (let i = 0; i < codeBlocks.length; i++) {
console.info(`Loading workspace ${i}...`)
let codeBlock = codeBlocks[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]
}
}
let id = category + "#" + points + "#" + i let id = category + "#" + points + "#" + i
let element = document.createElement("div") new workspace.Workspace(codeBlock, id, attachmentUrls)
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...") console.info("Filling debug information...")
for (let e of document.querySelectorAll(".debug")) { for (let e of document.querySelectorAll(".debug")) {
@ -235,7 +221,7 @@ async function loadPuzzle(category, points) {
return puzzle 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() { async function CorrectAnswer() {
setInterval(window.close, 3 * common.Second) setInterval(window.close, 3 * common.Second)

View File

@ -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" const HOME = "/home/web_user"

View File

@ -1,5 +1,6 @@
import {Toast} from "../common.mjs" import {Toast} from "../common.mjs"
import "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js" 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 = {} var workers = {}
@ -11,7 +12,6 @@ function loadWorker(language) {
worker = new Worker(url, { worker = new Worker(url, {
type: "module", type: "module",
}) })
console.info("Loading worker", url, worker)
workers[language] = worker workers[language] = worker
} }
return worker return worker
@ -20,16 +20,29 @@ function loadWorker(language) {
export class Workspace { 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 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 * @param attachmentUrls {URL[]} List of attachment URLs
*/ */
constructor(element, id, code, language, attachmentUrls) { constructor(codeBlock, id, attachmentUrls) {
this.element = element // Show a progress bar
this.originalCode = code let loadingElement = document.createElement("progress")
this.language = language 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.attachmentUrls = attachmentUrls
this.storageKey = "code:" + id this.storageKey = "code:" + id
@ -50,28 +63,50 @@ export class Workspace {
this.runButton = this.element.querySelector("button.run") this.runButton = this.element.querySelector("button.run")
this.revertButton = this.element.querySelector("button.revert") this.revertButton = this.element.querySelector("button.revert")
this.fontButton = this.element.querySelector("button.font") this.fontButton = this.element.querySelector("button.font")
this.element.querySelector(".language").textContent = this.language
this.runButton.disabled = true this.runButton.disabled = true
// Load in the editor // Load in the editor
this.editor.classList.add("language-" + language) this.editor.classList.add("language-" + this.language)
import("https://cdn.jsdelivr.net/npm/codejar@4.2.0").then((module) => this.editorReady(module)) 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 // 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.runButton.addEventListener("click", () => this.run())
this.revertButton.addEventListener("click", () => this.revert()) this.revertButton.addEventListener("click", () => this.revert())
this.fontButton.addEventListener("click", () => this.font()) this.fontButton.addEventListener("click", () => this.font())
} }
async initLanguage(language) { initLanguage(language) {
let start = performance.now() let start = performance.now()
this.status.textContent = "Initializing..." this.status.textContent = "Initializing..."
this.status.appendChild(document.createElement("progress")) this.status.appendChild(document.createElement("progress"))
this.worker = loadWorker(language)
await this.workerReady()
let workerUrl = new URL(language + ".mjs", import.meta.url)
this.worker = new Worker(workerUrl, {type: "module"})
// 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 runtime = performance.now() - start
let duration = new Date(runtime).toISOString().slice(11, -1) let duration = new Date(runtime).toISOString().slice(11, -1)
this.status.textContent = "Loaded in " + duration this.status.textContent = "Loaded in " + duration
@ -79,12 +114,14 @@ export class Workspace {
for (let a of this.attachmentUrls) { for (let a of this.attachmentUrls) {
let filename = a.pathname.split("/").pop() let filename = a.pathname.split("/").pop()
this.workerWget(a) this.workerMessage({type: "wget", url: a.href || a})
.then(ret => { .then(ret => {
this.stdinfo.appendChild(this.document.createElement("div")).textContent = "Downloaded " + filename this.stdinfo.appendChild(this.document.createElement("div")).textContent = "Downloaded " + filename
}) })
} }
resolve()
})
})
} }
workerMessage(message) { workerMessage(message) {
@ -141,23 +178,6 @@ export class Workspace {
} }
} }
/**
* 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) { setAnswer(answer) {
let evt = new CustomEvent("setAnswer", {detail: {value: answer}, bubbles: true, cancelable: true}) let evt = new CustomEvent("setAnswer", {detail: {value: answer}, bubbles: true, cancelable: true})
this.element.dispatchEvent(evt) this.element.dispatchEvent(evt)
@ -208,7 +228,7 @@ export class Workspace {
} }
} }
font(force) { font() {
this.element.classList.toggle("fixed", force) this.element.classList.toggle("fixed")
} }
} }