summaryrefslogtreecommitdiffstats
path: root/site
diff options
context:
space:
mode:
authorBrian Cully <bjc@spork.org>2025-12-23 12:41:35 -0500
committerBrian Cully <bjc@spork.org>2025-12-23 12:41:35 -0500
commita0d6b9ce39bce2182c400b529a9698f12307ae2d (patch)
treef746a8552f303745b7b103b062bbef6fc874f0da /site
parente101e44b9bc88b3df45a71202d9f9f73773d84ad (diff)
downloadautomathon-a0d6b9ce39bce2182c400b529a9698f12307ae2d.tar.gz
automathon-a0d6b9ce39bce2182c400b529a9698f12307ae2d.zip
js: use web component for inspector
Diffstat (limited to 'site')
-rw-r--r--site/index.html8
-rw-r--r--site/inspector.mjs178
-rw-r--r--site/main.mjs172
3 files changed, 195 insertions, 163 deletions
diff --git a/site/index.html b/site/index.html
index d62f22e..4ddb3ea 100644
--- a/site/index.html
+++ b/site/index.html
@@ -53,10 +53,10 @@
fps: <span id='fps'>n/a</span>
</section> <!-- arena -->
- <section id='robview'>
- <h2>code</h2>
+ <x-inspector id='robview'>
+ <h2>inspector</h2>
<details id='editor' open>
- <summary>code</summary>
+ <summary>source code</summary>
<div id='code'>
<select id='src-select'>
<option>rob.fs</option>
@@ -91,7 +91,7 @@
<ol id='callstack'></ol>
</fieldset>
</details> <!-- scope -->
- </section> <!-- robview -->
+ </x-inspector>
</div>
<script src='./main.mjs' type='module'></script>
diff --git a/site/inspector.mjs b/site/inspector.mjs
new file mode 100644
index 0000000..76e635f
--- /dev/null
+++ b/site/inspector.mjs
@@ -0,0 +1,178 @@
+const SRC_SELECT_SELECTOR = '#src-select';
+const COMPILE_BUTTON_SELECTOR = '#compile';
+const WORDLIST_SELECTOR = '#wordlist';
+const STACK_SELECTOR = '#stack';
+const CALLSTACK_SELECTOR = '#callstack';
+const VARS_SELECTOR = '#vars';
+const SRC_SELECTOR = '#src';
+const IP_SELECTOR = '#wordlist .ip';
+
+function selectorForIP(word, offset) {
+ return `#wordlist x-bytecode[x-index='${word}'] x-op[x-index='${offset}']`;
+}
+
+function wordlistElts(wordlist) {
+ return wordlist.map((bc, i) => {
+ const bcElt = document.createElement('x-bytecode');
+ bcElt.setAttribute('x-index', i);
+ bc.forEach((op, i) => {
+ const opElt = document.createElement('x-op');
+ opElt.setAttribute('x-index', i);
+ opElt.textContent = op;
+ bcElt.appendChild(opElt);
+ })
+ return bcElt;
+ })
+}
+
+export default class extends HTMLElement {
+ static name = 'x-inspector';
+ static compileRequest = 'compile';
+ static register() {
+ console.debug('registering custom element', this.name, this);
+ self.customElements.define(this.name, this);
+ }
+
+ constructor() {
+ super();
+ this.highRange = new Range();
+ this.highlight = new Highlight(this.highRange);
+ CSS.highlights.set('exec', this.highlight);
+ console.debug('bjc class var?', this.constructor.name, this.__proto__.name, Object.getPrototypeOf(this))
+ }
+
+ connectedCallback() {
+ self.foo = this;
+ console.debug('connectedCallback()', this);
+ [this.srcSelect, this.compileButton, this.wordlist, this.stack, this.callstack, this.vars, this.src] = [
+ SRC_SELECT_SELECTOR,
+ COMPILE_BUTTON_SELECTOR,
+ WORDLIST_SELECTOR,
+ STACK_SELECTOR,
+ CALLSTACK_SELECTOR,
+ VARS_SELECTOR,
+ SRC_SELECTOR,
+ ].map(this.querySelector.bind(this));
+
+ this.compileButton.onclick = e => {
+ console.debug('compile clicked', e);
+
+ // always add a newline until i decide what to do with the parser.
+ const text = this.src.textContent + '\n';
+ const compileEvent = new CustomEvent(this.constructor.compileRequest, { detail: { text } });
+ this.dispatchEvent(compileEvent);
+ }
+
+ this.srcSelect.onchange = _ => this.loadForth(this.srcSelect.value);
+ this.loadForth(this.srcSelect.value);
+ }
+
+ attributeChangedCallback(name, old, v) {
+ console.debug('attributeChangedCallback', this, name, old, v);
+ }
+
+ loadForth(taintedPath) {
+ console.debug('loadForth', this, taintedPath);
+ // ascii only + ‘-’, ‘_’, ‘.’, and ‘/’, but no ‘../’
+ const path =
+ taintedPath
+ .replace(/[^-_A-Za-z./]/g, '')
+ .replace(/\.\.\//g, '');
+ fetch(`./samples/${path}`)
+ .then(resp => {
+ if (!resp.ok) {
+ throw `http status ${resp.status}`
+ }
+ return resp.text()
+ })
+ .then(text => {
+ this.src.textContent = text;
+ })
+ .catch(e => {
+ console.error(`couldn't fetch ‘${path}’`, e);
+ });
+ }
+
+ render(robo, fresh=false) {
+ console.debug('render', this, robo);
+ if (fresh) {
+ this.renderWordlist(robo);
+ }
+ this.renderTextHighlight(robo);
+ this.renderWordlistHighlight(robo);
+ this.renderVars(robo);
+ this.renderStack(robo);
+ this.renderCallstack(robo);
+ }
+
+ renderWordlist(vm) {
+ while (this.wordlist.lastChild) {
+ console.debug('removing child', this.wordlist.lastChild)
+ this.wordlist.removeChild(this.wordlist.lastChild);
+ }
+
+ const wordlist = wordlistElts(vm.wordlist);
+ wordlist.forEach(elt => this.wordlist.appendChild(elt));
+ }
+
+ renderTextHighlight(vm) {
+ const { word, offset } = vm.ip;
+ const anno = vm.annos[word][offset];
+ const src = document.querySelector(SRC_SELECTOR);
+
+ // this assumes the text node is the first child, maybe it isn't?
+ this.highRange.setStart(src.childNodes[0], anno.start);
+ this.highRange.setEnd(src.childNodes[0], anno.end);
+ }
+
+ renderWordlistHighlight(vm) {
+ const { word, offset } = vm.ip;
+ this.querySelectorAll(IP_SELECTOR).forEach(e => e.classList.remove('ip'));
+ const sel = selectorForIP(word, offset);
+ this.querySelectorAll(sel).forEach(e => {
+ e.classList.add('ip');
+ });
+ }
+
+ renderVars(vm) {
+ while (this.vars.lastChild) {
+ this.vars.removeChild(this.vars.lastChild);
+ }
+
+ ['out', 'heading', 'speed', 'doppler'].forEach(name => {
+ const dt = document.createElement('dt');
+ dt.textContent = name;
+ this.vars.appendChild(dt);
+
+ const dd = document.createElement('dd');
+ dd.textContent = vm.vars[name];
+ this.vars.appendChild(dd);
+ });
+ }
+
+ renderStack(vm) {
+ while (this.stack.lastChild) {
+ this.stack.removeChild(this.stack.lastChild);
+ }
+ vm.stack.reverse()
+ .forEach(datum => {
+ const elt = document.createElement('li');
+ elt.textContent = datum;
+ this.stack.appendChild(elt);
+ return elt;
+ });
+ }
+
+ renderCallstack(vm) {
+ while (this.callstack.lastChild) {
+ this.callstack.removeChild(this.callstack.lastChild);
+ }
+ vm.callstack.reverse()
+ .forEach(datum => {
+ const elt = document.createElement('li');
+ elt.textContent = `${datum.word}@${datum.offset}`;
+ this.callstack.appendChild(elt);
+ return elt;
+ });
+ }
+}
diff --git a/site/main.mjs b/site/main.mjs
index ffbda00..d99b3a3 100644
--- a/site/main.mjs
+++ b/site/main.mjs
@@ -1,3 +1,5 @@
+import Inspector from './inspector.mjs';
+
// simulation speed
let TICKS_PER_SECOND = 15;
let MS_PER_TICK = 1_000 / TICKS_PER_SECOND;
@@ -6,93 +8,9 @@ let MS_PER_TICK = 1_000 / TICKS_PER_SECOND;
const CANVAS_SELECTOR = '#arena canvas';
const TICK_BUTTON_SELECTOR = '#tick';
const TICK_RATE_SELECTOR = '#tick-rate-select';
-const BENCH_BUTTON_SELECTOR = '#bench';
const RUN_BUTTON_SELECTOR = '#run';
const FPS_SELECTOR = '#fps';
-// one per bot
-const SRC_SELECT_SELECTOR = '#src-select';
-const COMPILE_BUTTON_SELECTOR = '#compile';
-const WORDLIST_SELECTOR = '#wordlist';
-const STACK_SELECTOR = '#stack';
-const CALLSTACK_SELECTOR = '#callstack';
-const VARS_SELECTOR = '#vars';
-const SRC_SELECTOR = '#src';
-const IP_SELECTOR = '#wordlist .ip';
-
-function selectorForIP(word, offset) {
- return `#wordlist x-bytecode[x-index='${word}'] x-op[x-index='${offset}']`;
-}
-
-function wordlistElts(wordlist) {
- return wordlist.map((bc, i) => {
- const bcElt = document.createElement('x-bytecode');
- bcElt.setAttribute('x-index', i);
- bc.forEach((op, i) => {
- const opElt = document.createElement('x-op');
- opElt.setAttribute('x-index', i);
- opElt.textContent = op;
- bcElt.appendChild(opElt);
- })
- return bcElt;
- })
-}
-
-function initWordlist() {
- const sel = selectorForIP(0, 0);
- document.querySelectorAll(sel).forEach(e => {
- e.classList.add('ip')
- });
-}
-
-function renderStack(vmstack) {
- document.querySelectorAll(STACK_SELECTOR).forEach(e => {
- while (e.lastChild) {
- e.removeChild(e.lastChild);
- }
- vmstack.reverse()
- .forEach(datum => {
- const elt = document.createElement('li');
- elt.textContent = datum;
- e.appendChild(elt);
- return elt;
- });
- });
-}
-
-function renderCallStack(vmcallstack) {
- document.querySelectorAll(CALLSTACK_SELECTOR).forEach(e => {
- while (e.lastChild) {
- e.removeChild(e.lastChild);
- }
- vmcallstack.reverse()
- .forEach(datum => {
- const elt = document.createElement('li');
- elt.textContent = `${datum.word}@${datum.offset}`;
- e.appendChild(elt);
- return elt;
- });
- });
-}
-
-function renderVars(vmvars) {
- document.querySelectorAll(VARS_SELECTOR).forEach(e => {
- while (e.lastChild) {
- e.removeChild(e.lastChild);
- }
-
- ['out', 'heading', 'speed', 'doppler'].forEach(name => {
- const dt = document.createElement('dt');
- dt.textContent = name;
- e.appendChild(dt);
-
- const dd = document.createElement('dd');
- dd.textContent = vmvars[name];
- e.appendChild(dd);
- });
- });
-}
-
function clamp(radius, x, y, width, height) {
const xx = Math.min(Math.max(radius, x), width-radius);
const yy = Math.min(Math.max(radius, y), height-radius);
@@ -128,41 +46,6 @@ function renderArena(robos, delta=0) {
});
}
-const highRange = new Range();
-const highlight = new Highlight(highRange);
-CSS.highlights.set('exec', highlight);
-
-function renderTextHighlight(vm) {
- const ip = vm.ip;
- const anno = vm.annos[ip.word][ip.offset];
- const src = document.querySelector(SRC_SELECTOR);
-
- // this assumes the text node is the first child, maybe it isn't?
- highRange.setStart(src.childNodes[0], anno.start);
- highRange.setEnd(src.childNodes[0], anno.end);
-}
-
-function loadForth(taintedPath) {
- // ascii only + ‘-’, ‘_’, ‘.’, and ‘/’, but no ‘../’
- const path =
- taintedPath
- .replace(/[^-_A-Za-z./]/g, '')
- .replace(/\.\.\//g, '');
- fetch(`./samples/${path}`)
- .then(resp => {
- if (!resp.ok) {
- throw `http status ${resp.status}`
- }
- return resp.text()
- })
- .then(text => {
- document.querySelector(SRC_SELECTOR).textContent = text;
- })
- .catch(e => {
- console.error(`couldn't fetch ‘${path}’`, e);
- });
-}
-
async function loaded() {
const canvas = document.querySelector(CANVAS_SELECTOR);
const roboWorker = new Worker('robo.mjs', { type: 'module' });
@@ -181,30 +64,11 @@ async function loaded() {
switch (kind) {
case 'compile':
if (res) {
- let wordlistContainer = document.querySelector(WORDLIST_SELECTOR);
- const wordlist = wordlistElts(trans.wordlist);
- wordlist.forEach(elt => wordlistContainer.appendChild(elt));
- initWordlist();
- renderStack(trans.stack);
- renderCallStack(trans.callstack);
- renderVars(trans.vars);
- renderTextHighlight(trans);
-
renderArena([robo]);
+ document.querySelector('x-inspector').render(trans, true);
}
break;
case 'tick':
- const { word, offset } = trans.ip;
- document.querySelectorAll(IP_SELECTOR).forEach(e => e.classList.remove('ip'));
- const sel = selectorForIP(word, offset);
- document.querySelectorAll(sel).forEach(e => {
- e.classList.add('ip');
- });
- renderStack(trans.stack);
- renderCallStack(trans.callstack);
- renderVars(trans.vars);
- renderTextHighlight(trans);
-
robo.lastTick = document.timeline.currentTime;
const [x, y] = clamp(25, robo.x + robo.speedx, robo.y + robo.speedy, canvas.width, canvas.height);
@@ -220,6 +84,8 @@ async function loaded() {
].map(x => robo.speed * x);
robo.speedx = speedx;
robo.speedy = speedy;
+
+ document.querySelector('x-inspector').render(trans);
break;
default:
console.error('invalid message from robo worker', e.data);
@@ -229,26 +95,6 @@ async function loaded() {
console.error('error in roboWorker', e);
};
- document.querySelectorAll(SRC_SELECT_SELECTOR).forEach(sel => {
- sel.onchange = _ => loadForth(sel.value);
- loadForth(sel.value);
- });
-
- document.querySelector(COMPILE_BUTTON_SELECTOR).onclick = e => {
- console.debug('compile clicked', e);
-
- let wordlistContainer = document.querySelector(WORDLIST_SELECTOR);
- while (wordlistContainer.lastChild) {
- console.debug('removing child', wordlistContainer.lastChild)
- wordlistContainer.removeChild(wordlistContainer.lastChild);
- }
-
- // always add a newline until i decide what to do with the parser.
- const text = document.querySelector(SRC_SELECTOR).textContent + '\n';
- console.debug('compiling', text);
- roboWorker.postMessage({ kind: 'compile', text });
- };
-
document.querySelector(TICK_BUTTON_SELECTOR).onclick = e => {
console.debug('tick clicked', e);
roboWorker.postMessage({ kind: 'tick' });
@@ -307,6 +153,14 @@ async function loaded() {
document.querySelector(FPS_SELECTOR).textContent = 'n/a';
}
}
+
+ Inspector.register();
+ document.querySelectorAll(Inspector.name).forEach(elt => {
+ elt.addEventListener(Inspector.compileRequest, e => {
+ console.debug('compiling', e.detail.text);
+ roboWorker.postMessage({ kind: 'compile', text: e.detail.text });
+ });
+ });
}
document.addEventListener('DOMContentLoaded', loaded);