init
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
// A small assembly editor: line numbers, syntax highlighting (a coloured <pre>
|
||||
// underneath a transparent <textarea>), 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, '<').replace(/>/g, '>');
|
||||
const span = (cls, text) => `<span class="${cls}">${esc(text)}</span>`;
|
||||
|
||||
/** 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) => `<div class="ed-line${i + 1 === errorLine ? ' err' : ''}">${highlightLine(l) || ' '}</div>`)
|
||||
.join('');
|
||||
gutter.innerHTML = lines
|
||||
.map((_, i) => `<div class="ed-num${i + 1 === errorLine ? ' err' : ''}">${i + 1}</div>`)
|
||||
.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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user