[dingus] Functional core, imperative shell (kinda)
This commit is contained in:
parent
ba65c1929c
commit
aedb02dda4
1 changed files with 212 additions and 125 deletions
337
dingus/dingus.js
337
dingus/dingus.js
|
|
@ -2,155 +2,242 @@ const STATUS = document.getElementById("status-line");
|
||||||
const ERRORS = document.getElementById("error-root");
|
const ERRORS = document.getElementById("error-root");
|
||||||
const TREE = document.getElementById("tree-root");
|
const TREE = document.getElementById("tree-root");
|
||||||
|
|
||||||
function show_tree() {
|
const INITIAL_STATE = {
|
||||||
ERRORS.classList.add("hidden");
|
worker: null,
|
||||||
TREE.classList.remove("hidden");
|
status: "Initializing...",
|
||||||
}
|
tree: null,
|
||||||
|
errors: [],
|
||||||
function show_errors() {
|
grammar: {
|
||||||
ERRORS.classList.remove("hidden");
|
last: null,
|
||||||
TREE.classList.add("hidden");
|
pending: null,
|
||||||
}
|
next: null,
|
||||||
|
},
|
||||||
const DOC_CHAINS = {};
|
input: {
|
||||||
function chain_document_submit(kind, editor, on_success) {
|
last: null,
|
||||||
let pending = null;
|
pending: null,
|
||||||
let next = null;
|
next: null,
|
||||||
|
},
|
||||||
function do_submit() {
|
|
||||||
const document = editor.doc.getValue();
|
|
||||||
if (window.localStorage) {
|
|
||||||
window.localStorage.setItem(kind, document);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pending) {
|
|
||||||
console.log("Old still pending, parking it...");
|
|
||||||
next = document;
|
|
||||||
} else {
|
|
||||||
pending = document;
|
|
||||||
worker.postMessage({kind, data: document});
|
|
||||||
console.log("Document submitted");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function on_result(message) {
|
|
||||||
pending = null;
|
|
||||||
if (next) {
|
|
||||||
pending = next;
|
|
||||||
next = null;
|
|
||||||
|
|
||||||
worker.postMessage({kind, data: document});
|
|
||||||
console.log("Posted another document");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.status === "ok" && on_success) {
|
|
||||||
on_success(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.status === "error") {
|
|
||||||
render_errors(message.errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DOC_CHAINS[kind] = on_result;
|
|
||||||
|
|
||||||
let change_timer_id = null;
|
|
||||||
editor.doc.on("change", () => {
|
|
||||||
clearTimeout(change_timer_id);
|
|
||||||
change_timer_id = setTimeout(() => {
|
|
||||||
change_timer_id = null;
|
|
||||||
do_submit();
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.localStorage) {
|
|
||||||
const value = window.localStorage.getItem(kind);
|
|
||||||
if (value) {
|
|
||||||
editor.doc.setValue(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return do_submit;
|
|
||||||
}
|
|
||||||
|
|
||||||
const worker = new Worker('worker.js');
|
|
||||||
worker.onmessage = (e) => {
|
|
||||||
const message = e.data;
|
|
||||||
|
|
||||||
const chain = DOC_CHAINS[message.kind];
|
|
||||||
if (chain) {
|
|
||||||
chain(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
STATUS.innerText = message.message;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let grammar_editor = null;
|
|
||||||
let input_editor = null;
|
|
||||||
|
|
||||||
function render_errors(errors) {
|
/*
|
||||||
ERRORS.innerText = errors.join("\n")
|
* Render a tree `node` into the DOM.
|
||||||
}
|
*/
|
||||||
|
function render_tree_node(node, input_editor) {
|
||||||
|
const tree_div = document.createElement("div");
|
||||||
|
tree_div.classList.add("parsed-node");
|
||||||
|
|
||||||
function render_parse_results(message) {
|
const node_label = document.createElement("a");
|
||||||
function render_tree_node(parent, node) {
|
node_label.innerText = node.name;
|
||||||
const tree_div = document.createElement("div");
|
node_label.setAttribute("href", "#");
|
||||||
tree_div.classList.add("parsed-node");
|
node_label.onclick = () => {
|
||||||
|
const doc = input_editor.doc;
|
||||||
|
doc.setSelection(
|
||||||
|
doc.posFromIndex(node.start),
|
||||||
|
doc.posFromIndex(node.end),
|
||||||
|
{scroll: true},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
if (node.start == node.end) {
|
||||||
|
node_label.classList.add("parsed-error-node");
|
||||||
|
}
|
||||||
|
tree_div.appendChild(node_label);
|
||||||
|
|
||||||
const node_label = document.createElement("a");
|
|
||||||
node_label.innerText = node.name;
|
if (node.kind === "tree") {
|
||||||
node_label.setAttribute("href", "#");
|
tree_div.classList.add("parsed-tree");
|
||||||
node_label.onclick = () => {
|
for (const child of node.children) {
|
||||||
const doc = input_editor.doc;
|
tree_div.appendChild(render_tree_node(child, input_editor));
|
||||||
doc.setSelection(
|
|
||||||
doc.posFromIndex(node.start),
|
|
||||||
doc.posFromIndex(node.end),
|
|
||||||
{scroll: true},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
if (node.start == node.end) {
|
|
||||||
node_label.classList.add("parsed-error-node");
|
|
||||||
}
|
}
|
||||||
tree_div.appendChild(node_label);
|
} else {
|
||||||
|
tree_div.classList.add("parsed-token");
|
||||||
|
|
||||||
if (node.kind === "tree") {
|
|
||||||
tree_div.classList.add("parsed-tree");
|
|
||||||
for (const child of node.children) {
|
|
||||||
render_tree_node(tree_div, child);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tree_div.classList.add("parsed-token");
|
|
||||||
}
|
|
||||||
parent.appendChild(tree_div);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = document.getElementById("tree-root");
|
return tree_div;
|
||||||
root.innerHTML = "";
|
|
||||||
render_tree_node(root, message.tree);
|
|
||||||
render_errors(message.errors);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Render the state out into the DOM, the bindings to which we have already
|
||||||
|
* established. Fully side-effecting, no shadow DOM here.
|
||||||
|
*/
|
||||||
|
function render_state(state, input_editor) {
|
||||||
|
STATUS.innerText = state.status;
|
||||||
|
|
||||||
|
ERRORS.innerText = state.errors.join("\n");
|
||||||
|
|
||||||
|
TREE.innerHTML = "";
|
||||||
|
if (state.tree) {
|
||||||
|
TREE.appendChild(render_tree_node(state.tree, input_editor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Post a changed document out to the worker, if the worker can take it,
|
||||||
|
* otherwise just queue it for submission.
|
||||||
|
*/
|
||||||
|
function post_document(worker, kind, state, document) {
|
||||||
|
if (window.localStorage) {
|
||||||
|
window.localStorage.setItem(kind, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_state = {...state};
|
||||||
|
if (new_state.pending) {
|
||||||
|
new_state.next = document;
|
||||||
|
} else {
|
||||||
|
new_state.pending = document;
|
||||||
|
new_state.next = null;
|
||||||
|
worker.postMessage({kind, data: document});
|
||||||
|
}
|
||||||
|
return new_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Handle a document submission by rotating in the next document to be
|
||||||
|
* submitted. (Documents flow from next -> pending -> last.)
|
||||||
|
*/
|
||||||
|
function rotate_document(worker, kind, state) {
|
||||||
|
let new_state = {...state, last: state.pending, pending: null};
|
||||||
|
if (new_state.next) {
|
||||||
|
new_state.pending = new_state.next;
|
||||||
|
new_state.next = null;
|
||||||
|
worker.postMessage({kind, data: new_state.pending});
|
||||||
|
}
|
||||||
|
return new_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Update the state given the message and return a new state.
|
||||||
|
*
|
||||||
|
* This can be side-effecting, in that it might also post messages to the worker
|
||||||
|
* (to rotate documents and whatnot). (Maybe we should do something about that?)
|
||||||
|
*/
|
||||||
|
function update(state, message) {
|
||||||
|
let new_state = {...state};
|
||||||
|
if (message.message) {
|
||||||
|
new_state.status = message.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.kind === "grammar") {
|
||||||
|
if (message.status === "changed") {
|
||||||
|
new_state.grammar = post_document(
|
||||||
|
new_state.worker,
|
||||||
|
"grammar",
|
||||||
|
new_state.grammar,
|
||||||
|
message.data,
|
||||||
|
);
|
||||||
|
} else if (message.status === "ok" || message.status === "error") {
|
||||||
|
new_state.grammar = rotate_document(new_state.worker, "grammar", new_state.grammar);
|
||||||
|
|
||||||
|
if (message.status === "ok") {
|
||||||
|
// Re-submit the input, using whatever the latest document state was.
|
||||||
|
new_state.input = post_document(
|
||||||
|
new_state.worker,
|
||||||
|
"input",
|
||||||
|
new_state.input,
|
||||||
|
new_state.input.next || new_state.input.pending || new_state.input.last,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.status === "error") {
|
||||||
|
// Record the errors.
|
||||||
|
new_state.errors = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.kind === "input") {
|
||||||
|
if (message.status === "changed") {
|
||||||
|
new_state.input = post_document(
|
||||||
|
new_state.worker,
|
||||||
|
"input",
|
||||||
|
new_state.input,
|
||||||
|
message.data,
|
||||||
|
);
|
||||||
|
} else if (message.status === "ok" || message.status === "error") {
|
||||||
|
new_state.input = rotate_document(new_state.worker, "input", new_state.input);
|
||||||
|
|
||||||
|
if (message.status === "ok") {
|
||||||
|
new_state.tree = message.tree;
|
||||||
|
new_state.errors = message.errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.status === "error") {
|
||||||
|
new_state.errors = message.errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log(state, message, new_state);
|
||||||
|
return new_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Construct a new "message handler" by wrapping up the state into an update/
|
||||||
|
* render loop.
|
||||||
|
*/
|
||||||
|
function message_handler(worker, input_editor) {
|
||||||
|
let state = {...INITIAL_STATE, worker};
|
||||||
|
render_state(state, input_editor);
|
||||||
|
|
||||||
|
return (message) => {
|
||||||
|
state = update(state, message);
|
||||||
|
render_state(state, input_editor);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// And we can set up the codemirror editors to publish their changes.
|
||||||
function setup_editors() {
|
function setup_editors() {
|
||||||
grammar_editor = CodeMirror.fromTextArea(
|
function setup_editor(kind, editor, handler) {
|
||||||
|
let change_timer_id = null;
|
||||||
|
editor.doc.on("change", () => {
|
||||||
|
clearTimeout(change_timer_id);
|
||||||
|
change_timer_id = setTimeout(() => {
|
||||||
|
change_timer_id = null;
|
||||||
|
handler({
|
||||||
|
kind,
|
||||||
|
status: "changed",
|
||||||
|
data: editor.doc.getValue(),
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.localStorage) {
|
||||||
|
const value = window.localStorage.getItem(kind);
|
||||||
|
if (value) {
|
||||||
|
editor.doc.setValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the editors and worker and stuff but don't wire the handler
|
||||||
|
// yet...
|
||||||
|
const worker = new Worker('worker.js');
|
||||||
|
const grammar_editor = CodeMirror.fromTextArea(
|
||||||
document.getElementById("grammar"),
|
document.getElementById("grammar"),
|
||||||
{
|
{
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
mode: "python",
|
mode: "python",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const input_editor = CodeMirror.fromTextArea(
|
||||||
input_editor = CodeMirror.fromTextArea(
|
|
||||||
document.getElementById("input"),
|
document.getElementById("input"),
|
||||||
{
|
{
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const submit_input = chain_document_submit(
|
// ...now we can construct the handler with what it needs....
|
||||||
"input", input_editor, render_parse_results);
|
const handler = message_handler(worker, input_editor);
|
||||||
|
|
||||||
chain_document_submit(
|
// ...and finally hook the event sources to the handler.
|
||||||
"grammar", grammar_editor, submit_input);
|
worker.onmessage = (e) => {
|
||||||
|
const message = e.data;
|
||||||
|
handler(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
setup_editor("grammar", grammar_editor, handler);
|
||||||
|
setup_editor("input", input_editor, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_editors();
|
setup_editors();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue