apfl/webpage/playground/web_playground.js
Laria Carolin Chabowski c5fbd92f75 Various cleanup tasks, mostly simplifying the REPL
Instead of passing a flag through the parser and tokenizer for telling
the input source if we need further input or not, we steal a trick from
Lua: In the REPL, we just continue to read lines and append them to the
input, until the input was loaded with no "unexpected EOF" error. After
all, when we didn't expect an EOF is exactly the scenario, when we need
more input.

Doing things this way simplifies a bunch of places and lets us remove
the ugly source_reader and iterative_runner concepts.

To allow the REPL to see the error that happened during loading required
some smaller refactorings, but those were honestly for the better
anyway.

I also decided to get rid of the token_source concept, the parser now
gets the tokenizer directly. This also made things a bit simpler, also
I want to soon-ish implement string interpolation, and for that the
parser needs to do more with the tokenizer than just reading the next
token.

One last thing: This also cleans up the web playground and makes the
playground and REPL share a bunch of code. Nice!
2025-11-30 22:23:40 +01:00

167 lines
4.6 KiB
JavaScript

window.apfl_playground = (() => {
let instances = new Map();
function playground_write(handle, error, s) {
let inst = instances.get(handle);
if (inst) {
inst.write(error, s);
}
}
window.playground_write = playground_write;
let repl_new_for_playground = undefined;
let repl_run = undefined;
let repl_destroy = undefined;
function Playground() {
let h = repl_new_for_playground();
if (!h) {
throw new Error("Could not init playground");
}
this.echoSource = true;
this._h = h;
this._onOutput = null;
this._onError = null;
this._lastResult = Playground.REPL_OK;
instances.set(h, this);
}
Playground.REPL_OK = 0;
Playground.REPL_MORE_INPUT = 1;
Playground.REPL_ERR = 2;
Playground.REPL_FATAL = 3;
Playground.prototype.destroy = function () {
repl_destroy(this._h);
instances.delete(this._h);
};
Playground.prototype.onOutput = function (f) {
this._onOutput = f;
};
Playground.prototype.onError = function (f) {
this._onError = f;
};
Playground.prototype.write = function (error, s) {
let writer = error ? this._onError : this._onOutput;
if (writer) {
writer(s);
}
}
Playground.prototype.getPrompt = function () {
return this._lastResult === Playground.REPL_MORE_INPUT ? "..." : ">";
};
Playground.prototype.runCode = function (source) {
source += "";
if (this.echoSource) {
this.write(false, `${this.getPrompt()} ${source}`);
}
return (this._lastResult = repl_run(this._h, source));
};
Playground.prototype.interactive = function (parent) {
const elem = (name, inner) => {
let out = document.createElement(name);
if (inner) {
inner(out);
}
return out;
};
let output;
let form;
let prompt;
let input;
parent.appendChild(elem("DIV", outer => {
outer.classList = "apfl_playground";
outer.appendChild(output = elem("PRE", output => {
output.classList = "apfl_playground_output";
}))
outer.appendChild(form = elem("FORM", form => {
form.appendChild(prompt = elem("SPAN", prompt => {
prompt.className = "apfl_playground_prompt";
prompt.innerText = this.getPrompt();
}));
form.appendChild(input = elem("INPUT", input => {
input.type = "text";
input.className = "apfl_playground_input";
input.placeholder = "code ...";
}));
}))
}));
form.onsubmit = (e) => {
e.preventDefault();
let code = input.value;
input.value = "";
if (!code) {
return;
}
this.runCode(code + "\n");
prompt.innerText = this.getPrompt();
};
const write = (error, s) => {
var span = elem("SPAN");
if (error) {
span.classList.add("error");
}
var lines = s.split("\n");
var scroll = false;
for (var i = 0 ; i < lines.length; i++) {
if (i > 0) {
span.appendChild(elem("BR"));
scroll = true;
}
var line = lines[i];
if (line != "") {
span.appendChild(document.createTextNode(lines[i]));
}
}
output.append(span);
if (scroll) {
output.scrollTop = output.scrollHeight - output.clientHeight;
}
};
this.onOutput(s => write(false, s));
this.onError(s => write(true, s));
};
let ok = false
let onModLoaded = [];
return async function () {
if (ok) {
return Playground;
}
let p = new Promise(resolve => {
onModLoaded.push(resolve);
});
WasmApflPlayground().then(instance => {
repl_new_for_playground = instance.cwrap("repl_new_for_playground", "number", []);
repl_run = instance.cwrap("repl_run", "number", ["number", "string"]);
repl_destroy = instance.cwrap("repl_destroy", "void", ["number"]);
ok = true;
onModLoaded.forEach(l => l(Playground));
});
return await p;
};
})();