summaryrefslogtreecommitdiffstats
path: root/site/inspector.mjs
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/inspector.mjs
parente101e44b9bc88b3df45a71202d9f9f73773d84ad (diff)
downloadautomathon-a0d6b9ce39bce2182c400b529a9698f12307ae2d.tar.gz
automathon-a0d6b9ce39bce2182c400b529a9698f12307ae2d.zip
js: use web component for inspector
Diffstat (limited to 'site/inspector.mjs')
-rw-r--r--site/inspector.mjs178
1 files changed, 178 insertions, 0 deletions
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;
+ });
+ }
+}