739 lines
31 KiB
JavaScript
739 lines
31 KiB
JavaScript
|
|
/* ============================================================== preferences */
|
|
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
|
function savePrefs() { store.set('prefs', S.prefs); }
|
|
|
|
function applyPrefs() {
|
|
var root = document.documentElement;
|
|
if (S.prefs.theme === 'auto') root.removeAttribute('data-theme'); else root.setAttribute('data-theme', S.prefs.theme);
|
|
root.style.setProperty('--fs', S.prefs.fs + 'px');
|
|
root.style.setProperty('--lh', Math.round(S.prefs.fs * 1.55) + 'px');
|
|
metricCache = {};
|
|
$('autorun').checked = !!S.prefs.autorun;
|
|
}
|
|
|
|
function applyLayout() {
|
|
var d = S.prefs.diagram, o = S.prefs.output;
|
|
$('p-diagram').hidden = !d;
|
|
$('p-output').hidden = !o;
|
|
$('split-h').hidden = !(d && o);
|
|
$('right').classList.toggle('one', !(d && o));
|
|
$('main').classList.toggle('no-right', !(d || o));
|
|
var root = document.documentElement;
|
|
root.style.setProperty('--leftw', (clamp(S.prefs.lw || 0.5, 0.2, 0.8) * 100) + '%');
|
|
root.style.setProperty('--toph', (clamp(S.prefs.th || 0.62, 0.15, 0.85) * 100) + '%');
|
|
setTimeout(function () { if (!V.userMoved) fit(); }, 0);
|
|
}
|
|
|
|
function initSplit(id, vertical) {
|
|
var sp = $(id);
|
|
sp.addEventListener('pointerdown', function (ev) {
|
|
if (ev.button) return;
|
|
sp.setPointerCapture(ev.pointerId);
|
|
sp.classList.add('drag');
|
|
});
|
|
sp.addEventListener('pointermove', function (ev) {
|
|
if (!sp.classList.contains('drag')) return;
|
|
if (vertical) {
|
|
var r = $('main').getBoundingClientRect();
|
|
S.prefs.lw = clamp((ev.clientX - r.left - 8) / (r.width - 16), 0.2, 0.8);
|
|
} else {
|
|
var r2 = $('right').getBoundingClientRect();
|
|
S.prefs.th = clamp((ev.clientY - r2.top) / r2.height, 0.15, 0.85);
|
|
}
|
|
applyLayout();
|
|
});
|
|
var end = function () { if (sp.classList.contains('drag')) { sp.classList.remove('drag'); savePrefs(); } };
|
|
sp.addEventListener('pointerup', end);
|
|
sp.addEventListener('pointercancel', end);
|
|
sp.addEventListener('keydown', function (ev) {
|
|
var step = ev.key === (vertical ? 'ArrowRight' : 'ArrowDown') ? 0.02 : ev.key === (vertical ? 'ArrowLeft' : 'ArrowUp') ? -0.02 : 0;
|
|
if (!step) return;
|
|
ev.preventDefault();
|
|
if (vertical) S.prefs.lw = clamp((S.prefs.lw || 0.5) + step, 0.2, 0.8); else S.prefs.th = clamp((S.prefs.th || 0.62) + step, 0.15, 0.85);
|
|
applyLayout();
|
|
savePrefs();
|
|
});
|
|
}
|
|
|
|
/* ================================================================== results */
|
|
function currentInput() { return getTab(S.lastInput); }
|
|
function currentResult() {
|
|
var it = currentInput();
|
|
return it ? (S.results[it.id] || null) : null;
|
|
}
|
|
function isStale() {
|
|
var it = currentInput(), r = currentResult();
|
|
if (!it || !r) return false;
|
|
return r.rev !== it.rev || r.grev !== S.grev || r.main !== S.lastGrammar || !!(S.gres && !S.gres.ok) || !!S.crash;
|
|
}
|
|
|
|
function parseToolLog(log) {
|
|
var out = [];
|
|
String(log || '').split(/\r?\n/).forEach(function (line) {
|
|
var m = /^(error|warning)\((\d+)\): (.*?):(\d+):(\d+): (.*)$/.exec(line);
|
|
if (m) { out.push({ sev: m[1], code: m[2], file: basename(m[3].replace(/\\/g, '/')), line: +m[4], col: +m[5], msg: m[6] }); return; }
|
|
var m2 = /^(error|warning)\((\d+)\): (.*)$/.exec(line);
|
|
if (m2) out.push({ sev: m2[1], code: m2[2], file: '', line: 0, col: 0, msg: m2[3] });
|
|
});
|
|
return out;
|
|
}
|
|
|
|
function setRange(kind, s, e) {
|
|
var it = currentInput();
|
|
if (!it) return;
|
|
var r = S.results[it.id];
|
|
if (s < 0 || !r || r.rev !== it.rev) { setEditorRange(it.ed, kind, null); return; }
|
|
var m = cpMap(it.ed.ta.value);
|
|
setEditorRange(it.ed, kind, [m(s), m(e + 1)]);
|
|
}
|
|
function togglePin(s, e) {
|
|
var it = currentInput();
|
|
if (!it || s < 0) return;
|
|
if (S.active !== it.id) activate(it.id, false);
|
|
var cur = it.ed.pin, m = cpMap(it.ed.ta.value), a = m(s), b = m(e + 1);
|
|
if (cur && cur[0] === a && cur[1] === b) { setEditorRange(it.ed, 'pin', null); return; }
|
|
setEditorRange(it.ed, 'pin', [a, b]);
|
|
revealUnit(it.ed, a);
|
|
}
|
|
|
|
function applyMarks() {
|
|
// input editor: syntax problems of the latest run
|
|
S.tabs.forEach(function (t) {
|
|
if (t.kind === 'input') {
|
|
var r = S.results[t.id], list = [];
|
|
if (r && r.rev === t.rev) {
|
|
var m = cpMap(t.ed.ta.value);
|
|
r.data.diagnostics.forEach(function (d) {
|
|
if (d.s >= 0) list.push({ s: m(d.s), e: m(d.e + 1), bit: d.sev === 'warning' ? 2 : 1 });
|
|
});
|
|
}
|
|
t.ed.diag = list;
|
|
paintEditor(t.ed);
|
|
} else {
|
|
var g = S.gres, marks = [];
|
|
if (g && g.snap && g.snap.revs[t.id] === t.rev) {
|
|
parseToolLog(g.log).forEach(function (p) {
|
|
if (p.file !== tabTitle(t) || p.line < 1) return;
|
|
var s = unitAt(t.ed, p.line, p.col), e = s, v = t.ed.ta.value;
|
|
while (e < v.length && isIdChar(v.charAt(e))) e++;
|
|
if (e === s) e = Math.min(v.length, s + 1);
|
|
marks.push({ s: s, e: e, bit: p.sev === 'warning' ? 2 : 1 });
|
|
});
|
|
}
|
|
t.ed.diag = marks;
|
|
paintEditor(t.ed);
|
|
}
|
|
});
|
|
}
|
|
|
|
function diagramNote() {
|
|
if (!getTab(S.lastGrammar)) return 'Open or create a grammar (File menu) to see a diagram.';
|
|
if (!currentInput()) return 'Open or create an input file (File menu). Its parse tree appears here.';
|
|
if (S.gres && !S.gres.ok) return 'The grammar has errors. See the Output panel.';
|
|
if (S.crash) return S.crash.message;
|
|
var r = currentResult();
|
|
if (r && r.data.noDiagram) return r.data.noDiagram;
|
|
if (S.running) return 'Running...';
|
|
return 'Press Run to parse this input.';
|
|
}
|
|
|
|
var shownData = null; // result data currently drawn in the heavy views
|
|
|
|
function refreshPanels() {
|
|
var it = currentInput(), gt = getTab(S.lastGrammar), r = currentResult();
|
|
$('dtitle').textContent = 'Diagram' + (it ? ' (' + tabTitle(it) + ')' : '');
|
|
$('otitle').textContent = 'Output' + (gt ? ' (' + stem(tabTitle(gt)) + ')' : '');
|
|
|
|
var chip = $('chip'), kind = null, text = '';
|
|
if (S.gres && !S.gres.ok) { kind = 'fail'; text = 'Grammar has errors'; }
|
|
else if (S.crash) { kind = 'fail'; text = S.crash.kind === 'timeout' ? 'Run timed out' : S.crash.kind === 'crashed' ? 'Run crashed' : 'Run failed'; }
|
|
else if (r) { kind = r.data.status.kind; text = r.data.status.text; }
|
|
chip.hidden = !kind;
|
|
if (kind) {
|
|
chip.className = 'chip ' + kind + (isStale() ? ' stale' : '');
|
|
chip.textContent = text;
|
|
chip.title = S.crash ? S.crash.message : (r && r.data.status.detail ? r.data.status.detail : '');
|
|
}
|
|
$('dgm').classList.toggle('stale', isStale());
|
|
|
|
var data = r ? r.data : null;
|
|
if (data !== shownData) { shownData = data; paintResultViews(data); }
|
|
else if (!data) paintResultViews(null);
|
|
|
|
paintProblems();
|
|
paintLog();
|
|
paintStartRule();
|
|
var busy = $('run');
|
|
busy.setAttribute('data-busy', S.running ? 'true' : 'false');
|
|
busy.textContent = S.running ? 'Running' : 'Run';
|
|
busy.disabled = !getTab(S.lastGrammar);
|
|
$('o-problems').setAttribute('aria-selected', S.prefs.out === 'problems' ? 'true' : 'false');
|
|
$('o-log').setAttribute('aria-selected', S.prefs.out === 'log' ? 'true' : 'false');
|
|
$('probview').hidden = S.prefs.out !== 'problems';
|
|
$('logview').hidden = S.prefs.out !== 'log';
|
|
}
|
|
|
|
function paintResultViews(data) {
|
|
paintDiagram(data);
|
|
var tree = $('treeout');
|
|
tree.textContent = data && data.tree ? (V.pretty ? data.treePretty : data.tree) : diagramNote();
|
|
paintTokens(data);
|
|
showView(S.prefs.view);
|
|
}
|
|
|
|
function showView(v) {
|
|
S.prefs.view = v;
|
|
['diagram', 'tree', 'tokens'].forEach(function (id) {
|
|
$('v-' + id).setAttribute('aria-selected', id === v ? 'true' : 'false');
|
|
});
|
|
$('dgm').hidden = v !== 'diagram';
|
|
$('treeview').hidden = v !== 'tree';
|
|
$('tokview').hidden = v !== 'tokens';
|
|
$('dtools').hidden = v !== 'diagram';
|
|
$('ttools').hidden = v !== 'tree';
|
|
$('b-pretty').setAttribute('aria-pressed', V.pretty ? 'true' : 'false');
|
|
$('b-flat').setAttribute('aria-pressed', V.pretty ? 'false' : 'true');
|
|
if (v === 'diagram' && V.needFit) fit();
|
|
}
|
|
|
|
function paintStartRule() {
|
|
var wrap = $('startwrap'), sel = $('startrule'), g = S.gres;
|
|
var show = !!(g && g.ok && g.hasParser && g.rules.length);
|
|
wrap.hidden = !show;
|
|
if (!show) return;
|
|
var sig = g.name + '|' + g.rules.join(',');
|
|
var chosen = S.start[g.name] || g.rules[0];
|
|
if (sel.getAttribute('data-sig') !== sig) {
|
|
clear(sel);
|
|
g.rules.forEach(function (n) { var o = el('option', '', n); o.value = n; sel.appendChild(o); });
|
|
sel.setAttribute('data-sig', sig);
|
|
}
|
|
sel.value = g.rules.indexOf(chosen) >= 0 ? chosen : g.rules[0];
|
|
}
|
|
|
|
/* -------------------------------------------------------------- problems */
|
|
function jumpToRange(tab, s, e) {
|
|
if (!tab) return;
|
|
activate(tab.id, true);
|
|
tab.ed.ta.setSelectionRange(s, e);
|
|
revealUnit(tab.ed, s);
|
|
}
|
|
|
|
function paintProblems() {
|
|
var list = $('problist');
|
|
clear(list);
|
|
var items = [], g = S.gres, r = currentResult(), it = currentInput();
|
|
if (g) {
|
|
var parsed = parseToolLog(g.log);
|
|
parsed.forEach(function (p) {
|
|
items.push({ src: 'grammar', sev: p.sev, loc: p.line ? p.file + ' ' + p.line + ':' + p.col : p.file, msg: p.msg, go: function () {
|
|
var t = null;
|
|
S.tabs.forEach(function (x) { if (x.kind === 'grammar' && tabTitle(x) === p.file) t = x; });
|
|
t = t || getTab(S.lastGrammar);
|
|
if (t && p.line) { var s = unitAt(t.ed, p.line, p.col); jumpToRange(t, s, s); }
|
|
} });
|
|
});
|
|
if (!g.ok && !parsed.some(function (p) { return p.sev === 'error'; })) {
|
|
items.push({ src: 'grammar', sev: 'error', loc: '', msg: g.error || 'The grammar could not be compiled.', go: null });
|
|
}
|
|
}
|
|
if (S.crash && !(g && !g.ok)) {
|
|
items.push({ src: 'runtime', sev: 'error', loc: S.crash.phase ? S.crash.phase : '', msg: S.crash.message, go: null });
|
|
}
|
|
if (r && it && !(g && !g.ok) && !S.crash) {
|
|
r.data.diagnostics.forEach(function (d) {
|
|
items.push({ src: d.src, sev: d.sev, loc: d.line ? d.line + ':' + d.col : '', msg: d.msg, go: function () {
|
|
if (r.rev !== it.rev) return;
|
|
var m = cpMap(it.ed.ta.value);
|
|
if (d.s >= 0) { jumpToRange(it, m(d.s), m(d.e + 1)); setEditorRange(it.ed, 'pin', [m(d.s), m(d.e + 1)]); }
|
|
else { var u = unitAt(it.ed, d.line, d.col); jumpToRange(it, u, u); }
|
|
} });
|
|
});
|
|
}
|
|
$('noprob').hidden = items.length > 0;
|
|
var errs = 0, warns = 0;
|
|
items.forEach(function (p) {
|
|
if (p.sev === 'error') errs++; else warns++;
|
|
var li = el('li', 'prob ' + p.sev);
|
|
li.tabIndex = 0;
|
|
li.appendChild(el('span', 'sev', p.sev));
|
|
li.appendChild(el('span', 'loc', p.loc));
|
|
li.appendChild(el('span', 'msg', p.msg));
|
|
li.appendChild(el('span', 'src', p.src));
|
|
if (p.go) {
|
|
li.onclick = function () {
|
|
Array.prototype.forEach.call(list.children, function (c) { c.removeAttribute('aria-current'); });
|
|
li.setAttribute('aria-current', 'true');
|
|
p.go();
|
|
};
|
|
li.onkeydown = function (ev) { if (ev.key === 'Enter') { ev.preventDefault(); li.click(); } };
|
|
}
|
|
list.appendChild(li);
|
|
});
|
|
var badge = $('badge');
|
|
badge.hidden = !items.length;
|
|
badge.className = 'badge' + (errs ? '' : ' warn');
|
|
badge.textContent = String(errs || warns);
|
|
}
|
|
|
|
function paintLog() {
|
|
var g = S.gres, text = '';
|
|
if (g) {
|
|
text = g.log && g.log.trim() ? g.log : (g.ok ? 'The ANTLR tool reported nothing.' : (g.error || ''));
|
|
if (g.ok) text += (text.slice(-1) === '\n' ? '' : '\n') + '\nGrammar files: ' + (g.entry || []).join(', ') + (g.cached ? ' (unchanged, reused)' : ' (compiled in ' + g.compileMs + ' ms)');
|
|
} else {
|
|
text = 'Nothing has run yet.';
|
|
}
|
|
$('logout').textContent = text;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- tokens */
|
|
function paintTokens(data) {
|
|
var wrap = $('tokview');
|
|
clear(wrap);
|
|
var toks = data ? data.tokens : [];
|
|
if (!toks.length) { wrap.appendChild(el('p', 'note', data ? 'No tokens.' : diagramNote())); return; }
|
|
var table = el('table'), head = el('tr');
|
|
['#', 'Text', 'Type', 'Channel', 'Line:col'].forEach(function (h) { head.appendChild(el('th', '', h)); });
|
|
var th = el('thead');
|
|
th.appendChild(head);
|
|
table.appendChild(th);
|
|
var body = el('tbody');
|
|
toks.slice(0, 5000).forEach(function (t) {
|
|
var tr = el('tr', t.ch && t.ch !== 'DEFAULT_TOKEN_CHANNEL' && t.ch !== '0' ? 'side' : '');
|
|
tr.appendChild(el('td', 'num', String(t.i)));
|
|
tr.appendChild(el('td', 'mono', t.t));
|
|
tr.appendChild(el('td', 'mono', t.ty));
|
|
tr.appendChild(el('td', 'mono', t.ch));
|
|
tr.appendChild(el('td', 'mono', t.l + ':' + t.c));
|
|
tr.onmouseenter = function () { setRange('hover', t.s, t.e); };
|
|
tr.onmouseleave = function () { setRange('hover', -1, -1); };
|
|
tr.onclick = function () { togglePin(t.s, t.e); };
|
|
body.appendChild(tr);
|
|
});
|
|
table.appendChild(body);
|
|
wrap.appendChild(table);
|
|
if (toks.length > 5000) wrap.appendChild(el('p', 'note', 'Showing the first 5000 of ' + toks.length + ' tokens.'));
|
|
}
|
|
|
|
/* ================================================================== diagram */
|
|
var V = { z: 1, x: 0, y: 0, w: 0, h: 0, key: '', owner: '', needFit: true, userMoved: false, pretty: true };
|
|
|
|
function applyView() { $('stage').style.transform = 'translate(' + V.x + 'px,' + V.y + 'px) scale(' + V.z + ')'; }
|
|
|
|
function fit() {
|
|
var c = $('dgm'), cw = c.clientWidth, ch = c.clientHeight;
|
|
if (!V.w || !cw || !ch) return;
|
|
var z = Math.max(0.05, Math.min(1.35, (cw - 40) / V.w, (ch - 40) / V.h));
|
|
V.z = z;
|
|
V.x = (cw - V.w * z) / 2;
|
|
V.y = Math.max(20, (ch - V.h * z) / 2);
|
|
V.needFit = false;
|
|
V.userMoved = false;
|
|
applyView();
|
|
}
|
|
|
|
function zoomAt(cx, cy, f) {
|
|
var z = clamp(V.z * f, 0.05, 8), k = z / V.z;
|
|
V.x = cx - (cx - V.x) * k;
|
|
V.y = cy - (cy - V.y) * k;
|
|
V.z = z;
|
|
V.userMoved = true;
|
|
applyView();
|
|
}
|
|
function zoomCentre(f) { var c = $('dgm'); zoomAt(c.clientWidth / 2, c.clientHeight / 2, f); }
|
|
|
|
function paintDiagram(data) {
|
|
var stage = $('stage'), note = $('dnote'), box = $('dgm');
|
|
var svg = data && data.svg ? data.svg : '';
|
|
var owner = S.lastInput || '';
|
|
if (owner !== V.owner) { V.owner = owner; V.needFit = true; V.userMoved = false; V.key = '\u0000'; }
|
|
if (svg && svg === V.key) return;
|
|
V.key = svg;
|
|
clear(stage);
|
|
if (!svg) {
|
|
note.textContent = diagramNote();
|
|
note.hidden = false;
|
|
box.classList.add('empty');
|
|
V.w = V.h = 0;
|
|
V.needFit = true;
|
|
return;
|
|
}
|
|
note.hidden = true;
|
|
box.classList.remove('empty');
|
|
stage.innerHTML = svg;
|
|
var vb = stage.querySelector('svg').getAttribute('viewBox').split(' ');
|
|
V.w = parseFloat(vb[2]);
|
|
V.h = parseFloat(vb[3]);
|
|
if (V.needFit || !V.userMoved) fit(); else applyView();
|
|
}
|
|
|
|
function svgMarkup(data) {
|
|
var NS = 'http://www.w3.org/2000/svg';
|
|
var c = $('stage').querySelector('svg').cloneNode(true);
|
|
var st = document.createElementNS(NS, 'style');
|
|
st.textContent = data.svgStyle;
|
|
var bg = document.createElementNS(NS, 'rect');
|
|
bg.setAttribute('class', 'al-bg');
|
|
bg.setAttribute('width', '100%');
|
|
bg.setAttribute('height', '100%');
|
|
c.insertBefore(bg, c.firstChild);
|
|
c.insertBefore(st, c.firstChild);
|
|
return '<?xml version="1.0" encoding="UTF-8"?>\n' + new XMLSerializer().serializeToString(c);
|
|
}
|
|
|
|
function download(name, blob) {
|
|
var a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = name;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
setTimeout(function () { URL.revokeObjectURL(a.href); }, 2000);
|
|
}
|
|
|
|
function exportName(ext) {
|
|
var it = currentInput();
|
|
return (it ? stem(tabTitle(it)) : 'tree') + '-tree.' + ext;
|
|
}
|
|
|
|
function savePng(data) {
|
|
var scale = Math.min(2, 8000 / Math.max(V.w, V.h));
|
|
var img = new Image();
|
|
img.onload = function () {
|
|
var c = document.createElement('canvas');
|
|
c.width = Math.max(1, Math.round(V.w * scale));
|
|
c.height = Math.max(1, Math.round(V.h * scale));
|
|
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
|
|
c.toBlob(function (b) { if (b) download(exportName('png'), b); }, 'image/png');
|
|
};
|
|
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgMarkup(data));
|
|
}
|
|
|
|
/* Zoom speed. A mouse-wheel notch reports a deltaY of about 100 (a trackpad many
|
|
* small ones, a pinch gesture arrives as wheel + ctrlKey); each event changes the
|
|
* zoom by exp(-delta * rate), and no single event may change it by more than maxStep. */
|
|
var ZOOM = { wheel: 0.0006, pinch: 0.005, maxStep: 0.08, button: 1.2 };
|
|
|
|
function stopGlide() { $('stage').style.transition = ''; }
|
|
|
|
// Pans (and, if it is very small, zooms) so that the node is in the middle.
|
|
function focusNode(g) {
|
|
var c = $('dgm'), stage = $('stage'), cw = c.clientWidth, ch = c.clientHeight;
|
|
if (!cw || !ch || !g.getBBox) return;
|
|
var bb = g.getBBox();
|
|
var z = V.z < 0.9 ? 1 : V.z;
|
|
Array.prototype.forEach.call(stage.querySelectorAll('.al-node.focus'), function (n) { n.classList.remove('focus'); });
|
|
g.classList.add('focus');
|
|
V.z = z;
|
|
V.x = cw / 2 - (bb.x + bb.width / 2) * z;
|
|
V.y = ch / 2 - (bb.y + bb.height / 2) * z;
|
|
V.userMoved = true;
|
|
var calm = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
stage.style.transition = calm ? '' : 'transform 220ms ease';
|
|
applyView();
|
|
if (!calm) setTimeout(stopGlide, 260);
|
|
}
|
|
|
|
// The node that best matches the characters [a, b] (code point indices): the
|
|
// smallest one that contains them; a token wins over a rule of the same size.
|
|
function nodeAround(a, b) {
|
|
var list = $('stage').querySelectorAll('.al-node[data-s]'), best = null, span = Infinity, leaf = false;
|
|
for (var i = 0; i < list.length; i++) {
|
|
var g = list[i], s = +g.getAttribute('data-s'), e = +g.getAttribute('data-e');
|
|
if (s > a || e < b) continue;
|
|
var isLeaf = g.classList.contains('al-term') || g.classList.contains('al-error');
|
|
if (e - s < span || (e - s === span && !leaf)) { best = g; span = e - s; leaf = isLeaf; }
|
|
}
|
|
return best;
|
|
}
|
|
|
|
// A click (or a selection) in the input text: show where it is in the tree.
|
|
function focusDiagramAt(tab) {
|
|
var it = currentInput(), r = it ? S.results[it.id] : null;
|
|
if (!it || it !== tab || !r || r.rev !== it.rev) return; // not the parsed input, or its tree is out of date
|
|
if ($('p-diagram').hidden || $('dgm').hidden || !V.w) return; // the diagram is not on screen
|
|
var ta = it.ed.ta, text = ta.value;
|
|
var a = unitToCp(text, ta.selectionStart), b = unitToCp(text, ta.selectionEnd);
|
|
var g = null;
|
|
if (b > a) g = nodeAround(a, b - 1);
|
|
else g = nodeAround(a, a) || (a > 0 ? nodeAround(a - 1, a - 1) : null); // at the end of a token or of the text: the one before
|
|
if (g) focusNode(g);
|
|
}
|
|
|
|
function initDiagram() {
|
|
var box = $('dgm'), stage = $('stage'), drag = null, moved = false;
|
|
box.addEventListener('wheel', function (ev) {
|
|
if (!V.w) return;
|
|
ev.preventDefault();
|
|
stopGlide();
|
|
var r = box.getBoundingClientRect(), unit = ev.deltaMode === 1 ? 16 : ev.deltaMode === 2 ? r.height : 1;
|
|
var step = clamp(-ev.deltaY * unit * (ev.ctrlKey ? ZOOM.pinch : ZOOM.wheel), -ZOOM.maxStep, ZOOM.maxStep);
|
|
zoomAt(ev.clientX - r.left, ev.clientY - r.top, Math.exp(step));
|
|
}, { passive: false });
|
|
// Dragging must pan, never select text; the diagram still has to get keyboard focus.
|
|
box.addEventListener('mousedown', function (ev) {
|
|
if (ev.button !== 0) return;
|
|
ev.preventDefault();
|
|
box.focus({ preventScroll: true });
|
|
});
|
|
box.addEventListener('selectstart', function (ev) { ev.preventDefault(); });
|
|
box.addEventListener('dragstart', function (ev) { ev.preventDefault(); });
|
|
box.addEventListener('pointerdown', function (ev) {
|
|
if (ev.button !== 0 || !V.w) return;
|
|
stopGlide();
|
|
drag = { x: ev.clientX, y: ev.clientY, vx: V.x, vy: V.y };
|
|
moved = false;
|
|
box.setPointerCapture(ev.pointerId);
|
|
box.classList.add('drag');
|
|
});
|
|
box.addEventListener('pointermove', function (ev) {
|
|
if (!drag) return;
|
|
var dx = ev.clientX - drag.x, dy = ev.clientY - drag.y;
|
|
if (Math.abs(dx) + Math.abs(dy) > 3) moved = true;
|
|
V.x = drag.vx + dx;
|
|
V.y = drag.vy + dy;
|
|
V.userMoved = true;
|
|
applyView();
|
|
});
|
|
var end = function () { drag = null; box.classList.remove('drag'); };
|
|
box.addEventListener('pointerup', end);
|
|
box.addEventListener('pointercancel', end);
|
|
box.addEventListener('dblclick', fit);
|
|
stage.addEventListener('click', function (ev) {
|
|
if (moved) return;
|
|
var g = ev.target.closest('.al-node');
|
|
if (g && g.hasAttribute('data-s')) togglePin(+g.getAttribute('data-s'), +g.getAttribute('data-e'));
|
|
});
|
|
stage.addEventListener('mouseover', function (ev) {
|
|
var g = ev.target.closest('.al-node');
|
|
if (g && g.hasAttribute('data-s')) setRange('hover', +g.getAttribute('data-s'), +g.getAttribute('data-e'));
|
|
});
|
|
stage.addEventListener('mouseout', function (ev) { if (ev.target.closest('.al-node')) setRange('hover', -1, -1); });
|
|
box.addEventListener('keydown', function (ev) {
|
|
var k = ev.key;
|
|
if (k === '+' || k === '=') zoomCentre(ZOOM.button);
|
|
else if (k === '-') zoomCentre(1 / ZOOM.button);
|
|
else if (k === '0') fit();
|
|
else if (k === 'ArrowLeft') { V.x += 60; V.userMoved = true; applyView(); }
|
|
else if (k === 'ArrowRight') { V.x -= 60; V.userMoved = true; applyView(); }
|
|
else if (k === 'ArrowUp') { V.y += 60; V.userMoved = true; applyView(); }
|
|
else if (k === 'ArrowDown') { V.y -= 60; V.userMoved = true; applyView(); }
|
|
else return;
|
|
ev.preventDefault();
|
|
});
|
|
$('zin').onclick = function () { zoomCentre(ZOOM.button); };
|
|
$('zout').onclick = function () { zoomCentre(1 / ZOOM.button); };
|
|
$('zfit').onclick = fit;
|
|
$('psvg').onclick = function () { var r = currentResult(); if (r && V.w) download(exportName('svg'), new Blob([svgMarkup(r.data)], { type: 'image/svg+xml' })); };
|
|
$('ppng').onclick = function () { var r = currentResult(); if (r && V.w) savePng(r.data); };
|
|
if (window.ResizeObserver) new ResizeObserver(function () { if (!V.userMoved && V.w) fit(); }).observe(box);
|
|
|
|
['diagram', 'tree', 'tokens'].forEach(function (id) {
|
|
$('v-' + id).onclick = function () { showView(id); savePrefs(); };
|
|
});
|
|
$('b-pretty').onclick = function () { V.pretty = true; shownData = null; refreshPanels(); };
|
|
$('b-flat').onclick = function () { V.pretty = false; shownData = null; refreshPanels(); };
|
|
$('b-copy').onclick = function () {
|
|
var text = $('treeout').textContent, b = $('b-copy');
|
|
var done = function () { var old = b.textContent; b.textContent = 'Copied'; setTimeout(function () { b.textContent = old; }, 1200); };
|
|
if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(text).then(done, function () {});
|
|
else {
|
|
var ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
try { document.execCommand('copy'); done(); } catch (e) { /* ignore */ }
|
|
ta.remove();
|
|
}
|
|
};
|
|
$('o-problems').onclick = function () { S.prefs.out = 'problems'; savePrefs(); refreshPanels(); };
|
|
$('o-log').onclick = function () { S.prefs.out = 'log'; savePrefs(); refreshPanels(); };
|
|
}
|
|
|
|
/* ====================================================================== run */
|
|
var runTimer = null;
|
|
function scheduleRun(ms) {
|
|
clearTimeout(runTimer);
|
|
if (!S.prefs.autorun) return;
|
|
runTimer = setTimeout(runNow, ms);
|
|
}
|
|
|
|
function runNow() {
|
|
clearTimeout(runTimer);
|
|
ensureLast();
|
|
var main = getTab(S.lastGrammar), inp = getTab(S.lastInput);
|
|
if (!main) { S.gres = null; refreshPanels(); return; }
|
|
if (S.running) { S.queued = true; return; }
|
|
S.running = true;
|
|
var snap = { revs: {}, inputRev: inp ? inp.rev : 0, grev: S.grev, inputId: inp ? inp.id : null };
|
|
var grammars = S.tabs.filter(function (t) { return t.kind === 'grammar'; });
|
|
grammars.forEach(function (t) { snap.revs[t.id] = t.rev; });
|
|
var name = stem(tabTitle(main));
|
|
var body = {
|
|
grammars: grammars.map(function (t) { return { name: tabTitle(t), path: t.path, text: t.ed.ta.value }; }),
|
|
main: tabTitle(main),
|
|
start: S.start[name] || ''
|
|
};
|
|
if (inp) body.input = { name: tabTitle(inp), text: inp.ed.ta.value };
|
|
refreshPanels();
|
|
|
|
api('POST', '/api/run', body).then(function (r) {
|
|
var g = r.grammar;
|
|
S.gres = { ok: !!g.ok, name: g.name, log: g.log || '', error: g.error || '', rules: g.rules || [], hasParser: !!g.hasParser,
|
|
entry: g.entry || [], cached: !!g.cached, compileMs: g.compileMs, snap: snap };
|
|
if (r.result && inp) S.results[inp.id] = { data: r.result, rev: snap.inputRev, grev: snap.grev, main: main.id };
|
|
var wasCrash = !!S.crash;
|
|
S.crash = r.crash || null;
|
|
if (S.crash && !wasCrash) toast(S.crash.kind === 'timeout' ? 'The run took too long and was stopped.' : 'The ANTLR runtime failed on this input. See Problems.', 'error');
|
|
if (r.runError) toast(r.runError, 'error');
|
|
if (!g.ok && S.prefs.out !== 'problems') { S.prefs.out = 'problems'; }
|
|
}).catch(function (e) {
|
|
S.gres = { ok: false, name: name, log: '', error: e.message, rules: [], hasParser: false, entry: [], cached: false, compileMs: 0, snap: snap };
|
|
toast('Run failed: ' + e.message, 'error');
|
|
}).then(function () {
|
|
S.running = false;
|
|
shownData = null;
|
|
applyMarks();
|
|
refreshPanels();
|
|
if (S.queued) { S.queued = false; runNow(); }
|
|
});
|
|
}
|
|
|
|
/* =================================================================== wiring */
|
|
function withEditor(fn) {
|
|
var t = getTab(S.active);
|
|
if (t) fn(t);
|
|
}
|
|
function changeFont(delta) {
|
|
S.prefs.fs = delta === 0 ? 13 : clamp(S.prefs.fs + delta, 10, 24);
|
|
applyPrefs();
|
|
savePrefs();
|
|
}
|
|
function setPref(key, value) {
|
|
S.prefs[key] = value;
|
|
applyPrefs();
|
|
applyLayout();
|
|
savePrefs();
|
|
}
|
|
|
|
function buildMenus() {
|
|
var hasTab = function () { return !!getTab(S.active); };
|
|
defineMenus([
|
|
{ label: 'File', items: function () {
|
|
var t = getTab(S.active);
|
|
return [
|
|
{ label: 'New grammar', action: newGrammar },
|
|
{ label: 'New input', action: newInput },
|
|
{ sep: true },
|
|
{ label: 'Open\u2026', key: 'Ctrl+O', action: openDialog },
|
|
{ sep: true },
|
|
{ label: 'Save', key: 'Ctrl+S', disabled: !t, action: function () { withEditor(saveTab); } },
|
|
{ label: 'Save as\u2026', key: 'Ctrl+Shift+S', disabled: !t, action: function () { withEditor(saveAs); } },
|
|
{ label: 'Save all', disabled: !S.tabs.some(function (x) { return x.dirty; }), action: saveAll },
|
|
{ sep: true },
|
|
{ label: 'Reload from disk', disabled: !(t && t.path), action: reloadActive },
|
|
{ label: 'Close tab', key: 'Alt+W', disabled: !t, action: function () { withEditor(function (x) { closeTab(x.id); }); } }
|
|
];
|
|
} },
|
|
{ label: 'Edit', items: function () {
|
|
return [
|
|
{ label: 'Undo', key: 'Ctrl+Z', disabled: !hasTab(), action: function () { withEditor(function (t) { t.ed.ta.focus(); document.execCommand('undo'); }); } },
|
|
{ label: 'Redo', key: 'Ctrl+Y', disabled: !hasTab(), action: function () { withEditor(function (t) { t.ed.ta.focus(); document.execCommand('redo'); }); } },
|
|
{ sep: true },
|
|
{ label: 'Find and replace', key: 'Ctrl+F', disabled: !hasTab(), action: function () { withEditor(function (t) { openFind(t.ed); }); } },
|
|
{ label: 'Toggle comment', key: 'Ctrl+/', disabled: !(getTab(S.active) && getTab(S.active).kind === 'grammar'), action: function () { withEditor(function (t) { t.ed.ta.focus(); toggleComment(t.ed); }); } }
|
|
];
|
|
} },
|
|
{ label: 'View', items: function () {
|
|
return [
|
|
{ label: 'Diagram panel', checked: S.prefs.diagram, action: function () { setPref('diagram', !S.prefs.diagram); } },
|
|
{ label: 'Output panel', checked: S.prefs.output, action: function () { setPref('output', !S.prefs.output); } },
|
|
{ sep: true },
|
|
{ label: 'Auto-run', checked: S.prefs.autorun, action: function () { setPref('autorun', !S.prefs.autorun); if (S.prefs.autorun) scheduleRun(100); } },
|
|
{ sep: true },
|
|
{ label: 'Theme: match system', checked: S.prefs.theme === 'auto', action: function () { setPref('theme', 'auto'); } },
|
|
{ label: 'Theme: light', checked: S.prefs.theme === 'light', action: function () { setPref('theme', 'light'); } },
|
|
{ label: 'Theme: dark', checked: S.prefs.theme === 'dark', action: function () { setPref('theme', 'dark'); } },
|
|
{ sep: true },
|
|
{ label: 'Larger text', action: function () { changeFont(1); } },
|
|
{ label: 'Smaller text', action: function () { changeFont(-1); } },
|
|
{ label: 'Reset text size', action: function () { changeFont(0); } }
|
|
];
|
|
} }
|
|
]);
|
|
}
|
|
|
|
function initShortcuts() {
|
|
document.addEventListener('keydown', function (ev) {
|
|
if ($('modal-root').firstChild) return;
|
|
var mod = ev.ctrlKey || ev.metaKey, k = ev.key;
|
|
if (mod && k === 'Enter') { ev.preventDefault(); runNow(); }
|
|
else if (mod && (k === 's' || k === 'S')) { ev.preventDefault(); withEditor(ev.shiftKey ? saveAs : saveTab); }
|
|
else if (mod && (k === 'o' || k === 'O')) { ev.preventDefault(); openDialog(); }
|
|
else if (ev.altKey && (k === 'w' || k === 'W')) { ev.preventDefault(); withEditor(function (t) { closeTab(t.id); }); }
|
|
else if (k === 'Escape') closeMenu(true);
|
|
});
|
|
}
|
|
|
|
function fatal(e) {
|
|
var m = $('main');
|
|
clear(m);
|
|
m.style.display = 'block';
|
|
var box = el('div', 'note');
|
|
box.appendChild(el('h2', '', 'Cannot reach antlrlab'));
|
|
box.appendChild(el('p', '', (e && e.message ? e.message + '. ' : '') + 'The server may have stopped, or this page was opened without its token. Restart antlrlab and open the address it prints.'));
|
|
m.appendChild(box);
|
|
}
|
|
|
|
function boot() {
|
|
var saved = store.get('prefs', null);
|
|
if (saved) Object.keys(saved).forEach(function (k) { S.prefs[k] = saved[k]; });
|
|
buildMenus();
|
|
applyPrefs();
|
|
applyLayout();
|
|
initDiagram();
|
|
initSplit('split-v', true);
|
|
initSplit('split-h', false);
|
|
initShortcuts();
|
|
$('run').onclick = runNow;
|
|
$('autorun').onchange = function () { setPref('autorun', this.checked); if (S.prefs.autorun) scheduleRun(100); };
|
|
$('startrule').onchange = function () {
|
|
var g = S.gres;
|
|
if (g) S.start[g.name] = this.value;
|
|
runNow();
|
|
};
|
|
$('x-diagram').onclick = function () { setPref('diagram', false); };
|
|
$('x-output').onclick = function () { setPref('output', false); };
|
|
$('w-open').onclick = openDialog;
|
|
$('w-grammar').onclick = newGrammar;
|
|
$('w-input').onclick = newInput;
|
|
showView(S.prefs.view || 'diagram');
|
|
refreshPanels();
|
|
|
|
api('GET', '/api/info').then(function (info) {
|
|
S.info = info;
|
|
$('rootname').textContent = info.rootName;
|
|
$('rootname').title = info.root;
|
|
if (!info.tool.found) toast(info.tool.error, 'error');
|
|
var session = store.get('session:' + info.root, null);
|
|
var fromCli = info.open.length > 0;
|
|
var list = fromCli ? info.open.map(function (p) { return { path: p }; }) : (session ? session.open : []);
|
|
var chain = Promise.resolve();
|
|
list.forEach(function (f) { chain = chain.then(function () { return openFile(f.path, { noFocus: true, kind: fromCli ? undefined : f.kind }); }); });
|
|
return chain.then(function () {
|
|
if (!S.tabs.length) { showEmpty(); return; }
|
|
var pick = null;
|
|
if (!fromCli && session && session.active) S.tabs.forEach(function (t) { if (t.path === session.active) pick = t; });
|
|
pick = pick || firstOfKind('input') || S.tabs[0];
|
|
activate(pick.id, true);
|
|
if (firstOfKind('grammar')) { S.lastGrammar = firstOfKind('grammar').id; }
|
|
scheduleRun(50);
|
|
});
|
|
}).catch(fatal);
|
|
}
|
|
|
|
boot();
|
|
})();
|