diff options
| author | Brian Cully <bjc@spork.org> | 2025-12-23 12:41:35 -0500 |
|---|---|---|
| committer | Brian Cully <bjc@spork.org> | 2025-12-23 12:41:35 -0500 |
| commit | a0d6b9ce39bce2182c400b529a9698f12307ae2d (patch) | |
| tree | f746a8552f303745b7b103b062bbef6fc874f0da /site/inspector.mjs | |
| parent | e101e44b9bc88b3df45a71202d9f9f73773d84ad (diff) | |
| download | automathon-a0d6b9ce39bce2182c400b529a9698f12307ae2d.tar.gz automathon-a0d6b9ce39bce2182c400b529a9698f12307ae2d.zip | |
js: use web component for inspector
Diffstat (limited to 'site/inspector.mjs')
| -rw-r--r-- | site/inspector.mjs | 178 |
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; + }); + } +} |
