// A small assembly editor: line numbers, syntax highlighting (a coloured
// underneath a transparent ), Tab-indents and error-line marking. import { h } from './dom.js'; const MNEMONICS = new Set( ('nop yield halt ldi lui mov add sub mul div mod and or xor shl shr sar addi jmp beq bne blt bge ' + 'call ret push pop ldb ldh ldw stb sth stw in out li').split(' '), ); const TOKEN = new RegExp( [ '(\\.[A-Za-z_]\\w*)', // 1 directive '([A-Za-z_]\\w*)(?=:)', // 2 label definition '\\b(r(?:1[0-5]|\\d)|sp)\\b', // 3 register '(-?\\b(?:0x[0-9a-fA-F]+|0b[01]+|\\d+)\\b)', // 4 number '([A-Za-z_]\\w*)', // 5 identifier '([\\[\\]+,:-])', // 6 punctuation ].join('|'), 'gi', ); const esc = (s) => s.replace(/&/g, '&').replace(//g, '>'); const span = (cls, text) => `${esc(text)}`; /** Highlight one line of assembly, returning HTML. Exported for testing. */ export function highlightLine(line) { const c = line.search(/[;#]/); const code = c < 0 ? line : line.slice(0, c); const comment = c < 0 ? '' : line.slice(c); let out = ''; let pos = 0; TOKEN.lastIndex = 0; for (let m; (m = TOKEN.exec(code)); ) { out += esc(code.slice(pos, m.index)); pos = m.index + m[0].length; if (m[1]) out += span('hl-dir', m[0]); else if (m[2]) out += span('hl-lbl', m[0]); else if (m[3]) out += span('hl-reg', m[0]); else if (m[4]) out += span('hl-num', m[0]); else if (m[5]) out += span(MNEMONICS.has(m[5].toLowerCase()) ? 'hl-mn' : 'hl-sym', m[0]); else out += span('hl-pun', m[0]); } out += esc(code.slice(pos)); if (comment) out += span('hl-cmt', comment); return out; } export function createEditor({ value = '', onInput = () => {}, onCursor = () => {} } = {}) { const gutter = h('div', { class: 'ed-gutter', 'aria-hidden': 'true' }); const hl = h('div', { class: 'ed-hl', 'aria-hidden': 'true' }); const input = h('textarea', { class: 'ed-input', spellcheck: 'false', autocapitalize: 'off', autocomplete: 'off', autocorrect: 'off', wrap: 'off', 'aria-label': 'Assembly source code', }); const el = h('div', { class: 'editor' }, gutter, h('div', { class: 'ed-body' }, hl, input)); let errorLine = null; let escaped = false; // Escape pressed: let the next Tab leave the editor function render() { const lines = input.value.split('\n'); hl.innerHTML = lines .map((l, i) => `${highlightLine(l) || ' '}`) .join(''); gutter.innerHTML = lines .map((_, i) => `${i + 1}`) .join(''); syncScroll(); } function syncScroll() { hl.scrollTop = gutter.scrollTop = input.scrollTop; hl.scrollLeft = input.scrollLeft; } function cursor() { const upTo = input.value.slice(0, input.selectionStart); const line = upTo.split('\n').length; const col = upTo.length - upTo.lastIndexOf('\n'); onCursor({ line, col }); } input.addEventListener('input', () => { errorLine = null; render(); onInput(input.value); }); input.addEventListener('scroll', syncScroll); for (const ev of ['keyup', 'click', 'focus']) input.addEventListener(ev, cursor); input.addEventListener('blur', () => { escaped = false; }); input.addEventListener('keydown', (e) => { if (e.key === 'Escape') { escaped = true; return; } if (e.key === 'Tab' && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey && !escaped) { e.preventDefault(); input.setRangeText(' ', input.selectionStart, input.selectionEnd, 'end'); input.dispatchEvent(new Event('input')); } }); input.value = value; render(); return { el, get value() { return input.value; }, set value(v) { input.value = v; errorLine = null; render(); onInput(v); }, insertAtTop(text) { input.value = text + input.value; errorLine = null; render(); onInput(input.value); }, setError(line) { errorLine = line; render(); if (line) { const lh = parseFloat(getComputedStyle(input).lineHeight) || 22; input.scrollTop = Math.max(0, (line - 3) * lh); syncScroll(); } }, focus() { input.focus(); }, render, // re-measure after the element is attached }; }