treehouse/static/js/components/literate-programming.js

151 lines
4.3 KiB
JavaScript
Raw Normal View History

2024-02-16 22:01:19 +01:00
import { CodeJar } from "../vendor/codejar.js";
let literatePrograms = new Map();
function getLiterateProgram(name) {
if (literatePrograms.get(name) == null) {
literatePrograms.set(name, {
2024-02-17 14:56:17 +01:00
frames: [],
2024-02-16 22:01:19 +01:00
onChanged: [],
2024-02-17 14:56:17 +01:00
outputCount: 0,
nextOutputIndex() {
let index = this.outputCount;
++this.outputCount;
return index;
}
2024-02-16 22:01:19 +01:00
});
}
return literatePrograms.get(name);
}
2024-02-17 14:56:17 +01:00
function getLiterateProgramWorkerCommands(name) {
let commands = [];
2024-02-16 22:01:19 +01:00
let literateProgram = getLiterateProgram(name);
2024-02-17 14:56:17 +01:00
for (let frame of literateProgram.frames) {
if (frame.mode == "input") {
commands.push({ kind: "module", source: frame.textContent });
} else if (frame.mode == "output") {
commands.push({ kind: "output", expected: frame.textContent });
}
2024-02-16 22:01:19 +01:00
}
2024-02-17 14:56:17 +01:00
return commands;
2024-02-16 22:01:19 +01:00
}
2024-02-17 14:56:17 +01:00
class InputMode {
constructor(frame) {
this.frame = frame;
2024-02-16 22:01:19 +01:00
2024-02-17 14:56:17 +01:00
this.codeJar = CodeJar(frame, InputMode.highlight);
2024-02-16 22:01:19 +01:00
this.codeJar.onUpdate(() => {
2024-02-17 14:56:17 +01:00
for (let handler of frame.program.onChanged) {
handler(frame.programName);
2024-02-16 22:01:19 +01:00
}
})
2024-02-17 14:56:17 +01:00
frame.addEventListener("click", event => event.preventDefault());
2024-02-16 22:01:19 +01:00
}
2024-02-17 14:56:17 +01:00
static highlight(frame) {
2024-02-16 22:01:19 +01:00
// TODO: Syntax highlighting
}
}
2024-02-17 14:56:17 +01:00
class OutputMode {
constructor(frame) {
this.clearResultsOnNextOutput = false;
2024-02-16 22:01:19 +01:00
2024-02-17 14:56:17 +01:00
this.frame = frame;
2024-02-16 22:01:19 +01:00
2024-02-17 14:56:17 +01:00
this.frame.program.onChanged.push(_ => this.evaluate());
this.outputIndex = this.frame.program.nextOutputIndex();
2024-02-16 22:01:19 +01:00
this.evaluate();
}
2024-02-17 14:56:17 +01:00
evaluate() {
2024-02-16 22:01:19 +01:00
// This is a small bit of debouncing. If we cleared the output right away, the page would
2024-02-17 14:56:17 +01:00
// jitter around irritatingly.
2024-02-16 22:01:19 +01:00
this.clearResultsOnNextOutput = true;
if (this.worker != null) {
this.worker.terminate();
}
this.worker = new Worker(`${TREEHOUSE_SITE}/static/js/components/literate-programming/worker.js`, {
type: "module",
2024-02-17 14:56:17 +01:00
name: `evaluate LiterateOutput ${this.frame.programName}`
2024-02-16 22:01:19 +01:00
});
this.worker.addEventListener("message", event => {
let message = event.data;
if (message.kind == "evalComplete") {
this.worker.terminate();
2024-02-17 14:56:17 +01:00
} else if (message.kind == "output" && message.outputIndex == this.outputIndex) {
2024-02-16 22:01:19 +01:00
this.addOutput(message.output);
}
});
this.worker.postMessage({
action: "eval",
2024-02-17 14:56:17 +01:00
input: getLiterateProgramWorkerCommands(this.frame.programName),
2024-02-16 22:01:19 +01:00
});
2024-02-17 14:56:17 +01:00
}
2024-02-16 22:01:19 +01:00
addOutput(output) {
if (this.clearResultsOnNextOutput) {
this.clearResultsOnNextOutput = false;
this.clearResults();
}
// Don't show anything if the function didn't return a value.
if (output.kind == "result" && output.message[0] === undefined) return;
let line = document.createElement("code");
line.classList.add("output");
line.classList.add(output.kind);
2024-02-17 14:56:17 +01:00
// One day this will be more fancy. Today is not that day.
line.textContent = output.message
.map(x => {
if (typeof x === "object") return JSON.stringify(x);
else return x + "";
})
.join(" ");
2024-02-16 22:01:19 +01:00
if (output.kind == "result") {
let returnValueText = document.createElement("span");
returnValueText.classList.add("return-value");
returnValueText.textContent = "Return value: ";
line.insertBefore(returnValueText, line.firstChild);
}
2024-02-17 14:56:17 +01:00
this.frame.appendChild(line);
2024-02-16 22:01:19 +01:00
}
clearResults() {
2024-02-17 14:56:17 +01:00
this.frame.replaceChildren();
}
}
class LiterateProgram extends HTMLElement {
connectedCallback() {
this.programName = this.getAttribute("data-program");
this.program.frames.push(this);
this.mode = this.getAttribute("data-mode");
if (this.mode == "input") {
this.modeImpl = new InputMode(this);
} else if (this.mode == "output") {
this.modeImpl = new OutputMode(this);
}
}
get program() {
return getLiterateProgram(this.programName);
2024-02-16 22:01:19 +01:00
}
}
2024-02-17 14:56:17 +01:00
customElements.define("th-literate-program", LiterateProgram);