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'; const HIGHLIGHT = new Highlight(); CSS.highlights.set('exec', HIGHLIGHT); 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); } #highRange = new Range(); #srcSelect; #compileButton; #wordlist; #stack; #callstack; #vars; #src; constructor() { super(); HIGHLIGHT.add(this.#highRange); } 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); } render(robo, fresh=false) { if (fresh) { this.#renderWordlist(robo); } this.#renderTextHighlight(robo); this.#renderWordlistHighlight(robo); this.#renderVars(robo); this.#renderStack(robo); this.#renderCallstack(robo); } #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); }); } #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]; // this assumes the text node is the first child, maybe it isn't? this.#highRange.setStart(this.#src.childNodes[0], anno.start); this.#highRange.setEnd(this.#src.childNodes[0], anno.end); } #renderWordlistHighlight(vm) { const { word, offset } = vm.ip; this.#wordlist.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; }); } }