Add project files
This commit is contained in:
commit
ba5971a788
37 files changed
+11180
No files matched your search
+226
@@ -0,0 +1,226 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/* ================================================================== helpers */
|
||||
var $ = function (id) { return document.getElementById(id); };
|
||||
function el(tag, cls, text) {
|
||||
var e = document.createElement(tag);
|
||||
if (cls) e.className = cls;
|
||||
if (text !== undefined && text !== null) e.textContent = text;
|
||||
return e;
|
||||
}
|
||||
function clear(n) { while (n.firstChild) n.removeChild(n.firstChild); }
|
||||
function plural(n, w) { return n + ' ' + w + (n === 1 ? '' : 's'); }
|
||||
function basename(p) { var i = p.lastIndexOf('/'); return i < 0 ? p : p.slice(i + 1); }
|
||||
function dirname(p) { var i = p.lastIndexOf('/'); return i < 0 ? '' : p.slice(0, i); }
|
||||
function joinPath(d, n) { return d ? d + '/' + n : n; }
|
||||
function stem(n) { var i = n.lastIndexOf('.'); return i <= 0 ? n : n.slice(0, i); }
|
||||
function isGrammarFile(p) { return /\.g4$/i.test(p); }
|
||||
function human(n) { return n < 1024 ? n + ' B' : n < 1048576 ? (n / 1024).toFixed(1) + ' KB' : (n / 1048576).toFixed(1) + ' MB'; }
|
||||
|
||||
var store = {
|
||||
get: function (k, d) {
|
||||
try { var v = localStorage.getItem('antlrlab:' + k); return v === null ? d : JSON.parse(v); } catch (e) { return d; }
|
||||
},
|
||||
set: function (k, v) { try { localStorage.setItem('antlrlab:' + k, JSON.stringify(v)); } catch (e) { /* ignore */ } }
|
||||
};
|
||||
|
||||
function debounce(fn, ms) {
|
||||
var t = null;
|
||||
var d = function () { clearTimeout(t); var a = arguments; t = setTimeout(function () { fn.apply(null, a); }, ms); };
|
||||
d.cancel = function () { clearTimeout(t); };
|
||||
return d;
|
||||
}
|
||||
|
||||
/* ==================================================================== state */
|
||||
var S = {
|
||||
info: null,
|
||||
tabs: [],
|
||||
active: null, // id of the visible tab
|
||||
lastGrammar: null, // the grammar that Run uses
|
||||
lastInput: null, // the input that Run uses
|
||||
results: {}, // input tab id -> { data, rev, grev }
|
||||
gres: null, // last grammar outcome from the server
|
||||
crash: null, // set when the last run crashed, timed out or failed in the worker
|
||||
grev: 0, // bumps on every grammar edit
|
||||
start: {}, // grammar name -> chosen start rule
|
||||
prefs: { autorun: true, theme: 'auto', fs: 13, diagram: true, output: true, view: 'diagram', out: 'problems' },
|
||||
running: false,
|
||||
queued: false,
|
||||
untitled: { grammar: 0, input: 0 }
|
||||
};
|
||||
|
||||
/* ====================================================================== API */
|
||||
function api(method, path, body) {
|
||||
var opts = { method: method, credentials: 'same-origin', headers: { 'X-Requested-With': 'antlrlab' } };
|
||||
if (body !== undefined) {
|
||||
opts.body = JSON.stringify(body);
|
||||
opts.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
return fetch(path, opts).then(function (r) {
|
||||
return r.text().then(function (t) {
|
||||
var data = null;
|
||||
try { data = JSON.parse(t); } catch (e) { /* not JSON */ }
|
||||
if (!r.ok) {
|
||||
var err = new Error((data && data.error) || ('HTTP ' + r.status));
|
||||
err.status = r.status;
|
||||
err.data = data;
|
||||
throw err;
|
||||
}
|
||||
return data;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ==================================================================== toast */
|
||||
function toast(msg, kind) {
|
||||
var t = el('div', 'toast' + (kind ? ' ' + kind : ''), msg);
|
||||
$('toasts').appendChild(t);
|
||||
setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, kind === 'error' ? 6500 : 2600);
|
||||
}
|
||||
|
||||
/* ================================================================== dialogs */
|
||||
// modal({title, build(ctx) -> Node, buttons: [{label, value, primary}]}) -> Promise(value | null)
|
||||
function modal(opts) {
|
||||
return new Promise(function (resolve) {
|
||||
var root = $('modal-root');
|
||||
var prev = document.activeElement;
|
||||
clear(root);
|
||||
var back = el('div', 'modal-back');
|
||||
var box = el('div', 'modal');
|
||||
box.setAttribute('role', 'dialog');
|
||||
box.setAttribute('aria-modal', 'true');
|
||||
box.appendChild(el('h3', '', opts.title));
|
||||
var body = el('div', 'mbody');
|
||||
var ctx = { close: close, box: box, accept: null };
|
||||
if (opts.message) body.appendChild(el('p', '', opts.message));
|
||||
if (opts.build) body.appendChild(opts.build(ctx));
|
||||
box.appendChild(body);
|
||||
var foot = el('div', 'mfoot');
|
||||
var primary = null;
|
||||
(opts.buttons || []).forEach(function (b) {
|
||||
var btn = el('button', 'btn' + (b.primary ? ' primary' : ''), b.label);
|
||||
btn.type = 'button';
|
||||
btn.onclick = function () {
|
||||
if (b.check && !b.check()) return;
|
||||
close(b.value === undefined ? b.label : b.value);
|
||||
};
|
||||
if (b.primary) primary = btn;
|
||||
foot.appendChild(btn);
|
||||
});
|
||||
box.appendChild(foot);
|
||||
back.appendChild(box);
|
||||
root.appendChild(back);
|
||||
|
||||
function onKey(ev) {
|
||||
if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); close(null); }
|
||||
else if (ev.key === 'Enter' && primary && ev.target.tagName !== 'BUTTON' && !(ev.target.closest && ev.target.closest('.flist'))) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
primary.click();
|
||||
}
|
||||
}
|
||||
back.addEventListener('mousedown', function (ev) { if (ev.target === back) close(null); });
|
||||
document.addEventListener('keydown', onKey, true);
|
||||
function close(v) {
|
||||
document.removeEventListener('keydown', onKey, true);
|
||||
clear(root);
|
||||
if (prev && prev.focus) prev.focus();
|
||||
resolve(v);
|
||||
}
|
||||
var first = box.querySelector('[data-focus]') || primary || box.querySelector('button');
|
||||
if (first) first.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function ask(title, message, buttons) { return modal({ title: title, message: message, buttons: buttons }); }
|
||||
|
||||
/* ===================================================================== menus */
|
||||
var menus = []; // [{label, items: function() -> [...]}]
|
||||
var openMenu = -1;
|
||||
var dropdown = null;
|
||||
|
||||
function defineMenus(list) {
|
||||
menus = list;
|
||||
var bar = $('menubar');
|
||||
clear(bar);
|
||||
menus.forEach(function (m, i) {
|
||||
var b = el('button', 'menu-btn', m.label);
|
||||
b.type = 'button';
|
||||
b.setAttribute('role', 'menuitem');
|
||||
b.setAttribute('aria-haspopup', 'true');
|
||||
b.setAttribute('aria-expanded', 'false');
|
||||
b.onclick = function () { if (openMenu === i) closeMenu(); else showMenu(i, false); };
|
||||
b.onmouseenter = function () { if (openMenu >= 0 && openMenu !== i) showMenu(i, false); };
|
||||
b.onkeydown = function (ev) {
|
||||
if (ev.key === 'ArrowDown' || ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); showMenu(i, true); }
|
||||
else if (ev.key === 'ArrowRight') { ev.preventDefault(); bar.children[(i + 1) % menus.length].focus(); }
|
||||
else if (ev.key === 'ArrowLeft') { ev.preventDefault(); bar.children[(i + menus.length - 1) % menus.length].focus(); }
|
||||
};
|
||||
bar.appendChild(b);
|
||||
});
|
||||
var rn = el('span', '', '');
|
||||
rn.id = 'rootname';
|
||||
bar.appendChild(rn);
|
||||
}
|
||||
|
||||
function closeMenu(refocus) {
|
||||
if (dropdown) { dropdown.remove(); dropdown = null; }
|
||||
var bar = $('menubar');
|
||||
Array.prototype.forEach.call(bar.querySelectorAll('.menu-btn'), function (b) { b.setAttribute('aria-expanded', 'false'); });
|
||||
var was = openMenu;
|
||||
openMenu = -1;
|
||||
if (refocus && was >= 0) bar.children[was].focus();
|
||||
}
|
||||
|
||||
function showMenu(i, focusFirst) {
|
||||
closeMenu();
|
||||
openMenu = i;
|
||||
var btn = $('menubar').children[i];
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
var dd = el('div', 'dropdown');
|
||||
dd.setAttribute('role', 'menu');
|
||||
menus[i].items().forEach(function (it) {
|
||||
if (it.sep) { dd.appendChild(el('div', 'sep')); return; }
|
||||
var row = el('div', 'mi');
|
||||
row.setAttribute('role', 'menuitem');
|
||||
row.tabIndex = -1;
|
||||
if (it.disabled) row.setAttribute('aria-disabled', 'true');
|
||||
if (it.checked) { row.appendChild(el('span', 'ck', '\u2713')); row.setAttribute('aria-checked', 'true'); }
|
||||
row.appendChild(el('span', '', it.label));
|
||||
if (it.key) row.appendChild(el('span', 'k', it.key));
|
||||
var run = function () { closeMenu(); setTimeout(it.action, 0); };
|
||||
row.onclick = run;
|
||||
row.onkeydown = function (ev) {
|
||||
var items = Array.prototype.filter.call(dd.children, function (c) { return c.classList.contains('mi') && c.getAttribute('aria-disabled') !== 'true'; });
|
||||
var at = items.indexOf(row);
|
||||
if (ev.key === 'ArrowDown') { ev.preventDefault(); items[(at + 1) % items.length].focus(); }
|
||||
else if (ev.key === 'ArrowUp') { ev.preventDefault(); items[(at + items.length - 1) % items.length].focus(); }
|
||||
else if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); run(); }
|
||||
else if (ev.key === 'Escape') { ev.preventDefault(); closeMenu(true); }
|
||||
else if (ev.key === 'ArrowRight' || ev.key === 'ArrowLeft') {
|
||||
ev.preventDefault();
|
||||
var n = (i + (ev.key === 'ArrowRight' ? 1 : menus.length - 1)) % menus.length;
|
||||
showMenu(n, true);
|
||||
$('menubar').children[n].focus();
|
||||
var first = dropdown && dropdown.querySelector('.mi:not([aria-disabled=true])');
|
||||
if (first) first.focus();
|
||||
}
|
||||
};
|
||||
dd.appendChild(row);
|
||||
});
|
||||
document.body.appendChild(dd);
|
||||
var r = btn.getBoundingClientRect();
|
||||
dd.style.left = Math.max(0, Math.min(r.left, window.innerWidth - dd.offsetWidth - 4)) + 'px';
|
||||
dd.style.top = r.bottom + 'px';
|
||||
dropdown = dd;
|
||||
if (focusFirst) {
|
||||
var f = dd.querySelector('.mi:not([aria-disabled=true])');
|
||||
if (f) f.focus();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', function (ev) {
|
||||
if (dropdown && !dropdown.contains(ev.target) && !ev.target.closest('.menu-btn')) closeMenu();
|
||||
});
|
||||
window.addEventListener('blur', function () { closeMenu(); });
|
||||
@@ -0,0 +1,476 @@
|
||||
|
||||
/* ================================================================== editor
|
||||
* A plain <textarea> sits on top of a <pre> that paints the same text with
|
||||
* syntax colors and marks. Both have identical font metrics and size, so the
|
||||
* textarea provides the caret, selection, IME and native undo while the <pre>
|
||||
* provides the colors. One editor (and so one undo history) per tab. */
|
||||
|
||||
var CLS = ['', 't-com', 't-str', 't-set', 't-act', 't-ann', 't-kw', 't-tok', 't-def', 't-lab', 't-op', 't-tokdef'];
|
||||
var MKCLS = [[1, 'mk-err'], [2, 'mk-warn'], [4, 'mk-hl'], [8, 'mk-find'], [16, 'mk-cur']];
|
||||
var KW = { grammar: 1, lexer: 1, parser: 1, fragment: 1, options: 1, tokens: 1, channels: 1, 'import': 1, mode: 1, returns: 1,
|
||||
locals: 1, throws: 1, 'catch': 1, 'finally': 1, skip: 1, more: 1, pushMode: 1, popMode: 1, type: 1, channel: 1 };
|
||||
|
||||
function isIdStart(c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_'; }
|
||||
function isIdChar(c) { return isIdStart(c) || (c >= '0' && c <= '9'); }
|
||||
|
||||
// One class id per UTF-16 unit, for ANTLR grammar syntax.
|
||||
function g4Classes(t) {
|
||||
var n = t.length, cls = new Uint8Array(n), i = 0, prevKw = '';
|
||||
function paint(a, b, c) { for (var k = a; k < b; k++) cls[k] = c; }
|
||||
function quoted(from, close) {
|
||||
var j = from + 1;
|
||||
while (j < n && t.charAt(j) !== '\n') {
|
||||
if (t.charAt(j) === '\\') { j += 2; continue; }
|
||||
if (t.charAt(j) === close) { j++; break; }
|
||||
j++;
|
||||
}
|
||||
return Math.min(j, n);
|
||||
}
|
||||
while (i < n) {
|
||||
var c = t.charAt(i), j;
|
||||
if (c === '/' && t.charAt(i + 1) === '/') {
|
||||
j = t.indexOf('\n', i); if (j < 0) j = n;
|
||||
paint(i, j, 1); i = j;
|
||||
} else if (c === '/' && t.charAt(i + 1) === '*') {
|
||||
j = t.indexOf('*/', i + 2); j = j < 0 ? n : j + 2;
|
||||
paint(i, j, 1); i = j;
|
||||
} else if (c === "'") {
|
||||
j = quoted(i, "'"); paint(i, j, 2); i = j;
|
||||
} else if (c === '[') {
|
||||
j = quoted(i, ']'); paint(i, j, 3); i = j;
|
||||
} else if (c === '{') {
|
||||
var depth = 0; j = i;
|
||||
while (j < n) {
|
||||
var d = t.charAt(j);
|
||||
if (d === "'" || d === '"') { j = quoted(j, d); continue; }
|
||||
if (d === '{') depth++;
|
||||
else if (d === '}' && --depth === 0) { j++; break; }
|
||||
j++;
|
||||
}
|
||||
paint(i, j, 4); i = j;
|
||||
} else if (c === '@' || c === '#') {
|
||||
j = i + 1;
|
||||
while (j < n && (t.charAt(j) === ' ' || t.charAt(j) === '\t') && c === '#') j++;
|
||||
while (j < n && (isIdChar(t.charAt(j)) || t.charAt(j) === ':')) j++;
|
||||
paint(i, j, 5); i = j;
|
||||
} else if (isIdStart(c)) {
|
||||
j = i + 1;
|
||||
while (j < n && isIdChar(t.charAt(j))) j++;
|
||||
var word = t.slice(i, j);
|
||||
var k = j;
|
||||
while (k < n && (t.charAt(k) === ' ' || t.charAt(k) === '\t' || t.charAt(k) === '\n' || t.charAt(k) === '\r')) k++;
|
||||
var next = t.charAt(k), after = t.charAt(k + 1);
|
||||
var ls = t.lastIndexOf('\n', i - 1) + 1;
|
||||
var first = /^[ \t]*$/.test(t.slice(ls, i));
|
||||
var def = next === ':' && after !== ':' && (first || prevKw === 'fragment');
|
||||
var c0 = word.charAt(0);
|
||||
var up = c0 >= 'A' && c0 <= 'Z';
|
||||
var id;
|
||||
if (KW[word] === 1 && !def && next !== ':') id = 6;
|
||||
else if (def) id = up ? 11 : 8;
|
||||
else if ((next === '=' && after !== '=') || (next === '+' && after === '=')) id = 9;
|
||||
else id = up ? 7 : 0;
|
||||
paint(i, j, id);
|
||||
prevKw = word;
|
||||
i = j;
|
||||
} else {
|
||||
if ('|:;()*+?~.,<>=-'.indexOf(c) >= 0) cls[i] = 10;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return cls;
|
||||
}
|
||||
|
||||
// code point index -> UTF-16 index
|
||||
function cpMap(text) {
|
||||
if (!/[\ud800-\udbff]/.test(text)) return function (i) { return i; };
|
||||
var map = [];
|
||||
for (var i = 0; i < text.length;) {
|
||||
map.push(i);
|
||||
var c = text.charCodeAt(i);
|
||||
i += (c >= 0xd800 && c <= 0xdbff && i + 1 < text.length) ? 2 : 1;
|
||||
}
|
||||
map.push(text.length);
|
||||
return function (k) { return k < map.length ? map[k] : text.length; };
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------- line helpers */
|
||||
function lineStarts(ed) {
|
||||
if (!ed.ls) {
|
||||
var t = ed.ta.value, a = [0];
|
||||
for (var i = t.indexOf('\n'); i >= 0; i = t.indexOf('\n', i + 1)) a.push(i + 1);
|
||||
ed.ls = a;
|
||||
}
|
||||
return ed.ls;
|
||||
}
|
||||
function lineOf(ed, idx) {
|
||||
var a = lineStarts(ed), lo = 0, hi = a.length - 1;
|
||||
while (lo < hi) {
|
||||
var mid = (lo + hi + 1) >> 1;
|
||||
if (a[mid] <= idx) lo = mid; else hi = mid - 1;
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
// 1-based line + 0-based column -> unit index (column clamped to the line)
|
||||
function unitAt(ed, line, col) {
|
||||
var a = lineStarts(ed);
|
||||
if (line < 1) return 0;
|
||||
if (line > a.length) return ed.ta.value.length;
|
||||
var s = a[line - 1], e = line < a.length ? a[line] - 1 : ed.ta.value.length;
|
||||
return Math.min(s + col, e);
|
||||
}
|
||||
|
||||
var metricCache = {};
|
||||
function metrics(ed) {
|
||||
var key = S.prefs.fs;
|
||||
if (metricCache[key]) return metricCache[key];
|
||||
var probe = el('span', '', '0000000000000000000000000000000000000000');
|
||||
probe.style.position = 'absolute';
|
||||
probe.style.visibility = 'hidden';
|
||||
probe.style.whiteSpace = 'pre';
|
||||
probe.className = 'hl';
|
||||
probe.style.padding = '0';
|
||||
probe.style.minHeight = '0';
|
||||
ed.hl.parentNode.appendChild(probe);
|
||||
var w = probe.getBoundingClientRect().width / 40;
|
||||
probe.remove();
|
||||
var lh = parseFloat(getComputedStyle(ed.ta).lineHeight) || 20;
|
||||
metricCache[key] = { cw: w || 8, lh: lh };
|
||||
return metricCache[key];
|
||||
}
|
||||
|
||||
// Scrolls the editor so that the given unit index is visible.
|
||||
function revealUnit(ed, idx) {
|
||||
var m = metrics(ed), sc = ed.scroller;
|
||||
var line = lineOf(ed, idx);
|
||||
var ls = lineStarts(ed)[line];
|
||||
var col = 0, pre = ed.ta.value.slice(ls, idx);
|
||||
for (var i = 0; i < pre.length; i++) col = pre.charAt(i) === '\t' ? (Math.floor(col / 4) + 1) * 4 : col + 1;
|
||||
var y = 8 + line * m.lh;
|
||||
var gw = ed.gutter.offsetWidth;
|
||||
var x = gw + 10 + col * m.cw;
|
||||
if (y < sc.scrollTop + m.lh) sc.scrollTop = Math.max(0, y - m.lh * 2);
|
||||
else if (y + m.lh * 2 > sc.scrollTop + sc.clientHeight) sc.scrollTop = y + m.lh * 3 - sc.clientHeight;
|
||||
if (x < sc.scrollLeft + gw + 30) sc.scrollLeft = Math.max(0, x - gw - 60);
|
||||
else if (x + 40 > sc.scrollLeft + sc.clientWidth) sc.scrollLeft = x + 80 - sc.clientWidth;
|
||||
}
|
||||
|
||||
function caretInfo(ed) {
|
||||
var p = ed.ta.selectionEnd, line = lineOf(ed, p);
|
||||
return { line: line + 1, col: p - lineStarts(ed)[line] + 1 };
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- painting */
|
||||
function paintEditor(ed) {
|
||||
var t = ed.ta.value, n = t.length;
|
||||
var big = n > 800000;
|
||||
var cls = (ed.tab.kind === 'grammar' && !big) ? g4Classes(t) : null;
|
||||
var mk = new Uint8Array(n + 1);
|
||||
function mark(list, bit) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var r = list[i];
|
||||
for (var k = Math.max(0, r.s); k < Math.min(n, r.e); k++) mk[k] |= (r.bit || bit);
|
||||
}
|
||||
}
|
||||
if (!big) {
|
||||
mark(ed.diag, 1);
|
||||
if (ed.find) {
|
||||
for (var f = 0; f < ed.find.matches.length && f < 5000; f++) {
|
||||
var fm = ed.find.matches[f];
|
||||
for (var q = fm[0]; q < fm[1]; q++) mk[q] |= (f === ed.find.cur ? 16 : 8);
|
||||
}
|
||||
}
|
||||
var rng = ed.hover || ed.pin;
|
||||
if (rng) for (var h = Math.max(0, rng[0]); h < Math.min(n, rng[1]); h++) mk[h] |= 4;
|
||||
}
|
||||
|
||||
var frag = document.createDocumentFragment();
|
||||
var i = 0;
|
||||
while (i < n) {
|
||||
var key = (cls ? cls[i] : 0) | (mk[i] << 4), j = i + 1;
|
||||
while (j < n && ((cls ? cls[j] : 0) | (mk[j] << 4)) === key) j++;
|
||||
var txt = t.slice(i, j);
|
||||
if (key === 0) {
|
||||
frag.appendChild(document.createTextNode(txt));
|
||||
} else {
|
||||
var names = [];
|
||||
if (key & 15) names.push(CLS[key & 15]);
|
||||
for (var m = 0; m < MKCLS.length; m++) if ((key >> 4) & MKCLS[m][0]) names.push(MKCLS[m][1]);
|
||||
frag.appendChild(el('span', names.join(' '), txt));
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
frag.appendChild(document.createTextNode('\n')); // matches the textarea's trailing line
|
||||
clear(ed.hl);
|
||||
ed.hl.appendChild(frag);
|
||||
|
||||
var lines = lineStarts(ed).length;
|
||||
if (lines !== ed.lines) {
|
||||
var nums = [];
|
||||
for (var L = 1; L <= lines; L++) nums.push(L);
|
||||
ed.gutter.textContent = nums.join('\n') + '\n';
|
||||
ed.lines = lines;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- editing */
|
||||
function replaceRange(ed, a, b, text) {
|
||||
var ta = ed.ta;
|
||||
ta.focus();
|
||||
ta.setSelectionRange(a, b);
|
||||
var ok = false;
|
||||
// execCommand keeps the undo history but is very slow for big multi-line
|
||||
// strings in Chrome, so large edits take the direct route.
|
||||
if (text.length + (b - a) <= 20000) {
|
||||
try { ok = document.execCommand('insertText', false, text); } catch (e) { ok = false; }
|
||||
}
|
||||
if (!ok) {
|
||||
ta.setRangeText(text, a, b, 'end');
|
||||
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
function lineSpan(t, a, b) {
|
||||
if (b > a && t.charAt(b - 1) === '\n') b--;
|
||||
var s = t.lastIndexOf('\n', a - 1) + 1;
|
||||
var e = t.indexOf('\n', b);
|
||||
if (e < 0) e = t.length;
|
||||
return [s, e];
|
||||
}
|
||||
|
||||
function indentSelection(ed, outdent) {
|
||||
var ta = ed.ta, t = ta.value, a = ta.selectionStart, b = ta.selectionEnd;
|
||||
var unit = ed.tab.kind === 'grammar' ? ' ' : '\t';
|
||||
if (a === b && !outdent) {
|
||||
if (unit === '\t') { replaceRange(ed, a, b, '\t'); return; }
|
||||
var col = a - (t.lastIndexOf('\n', a - 1) + 1);
|
||||
replaceRange(ed, a, b, unit.slice(0, 4 - (col % 4)));
|
||||
return;
|
||||
}
|
||||
var sp = lineSpan(t, a, b);
|
||||
var lines = t.slice(sp[0], sp[1]).split('\n').map(function (l) {
|
||||
if (!outdent) return l.length ? unit + l : l;
|
||||
if (l.charAt(0) === '\t') return l.slice(1);
|
||||
var m = /^ {1,4}/.exec(l);
|
||||
return m ? l.slice(m[0].length) : l;
|
||||
});
|
||||
var out = lines.join('\n');
|
||||
replaceRange(ed, sp[0], sp[1], out);
|
||||
ed.ta.setSelectionRange(sp[0], sp[0] + out.length);
|
||||
}
|
||||
|
||||
function toggleComment(ed) {
|
||||
if (ed.tab.kind !== 'grammar') return;
|
||||
var ta = ed.ta, t = ta.value, sp = lineSpan(t, ta.selectionStart, ta.selectionEnd);
|
||||
var lines = t.slice(sp[0], sp[1]).split('\n');
|
||||
var live = lines.filter(function (l) { return l.trim().length; });
|
||||
var all = live.length && live.every(function (l) { return /^\s*\/\//.test(l); });
|
||||
var indent = Math.min.apply(null, live.map(function (l) { return /^\s*/.exec(l)[0].length; }).concat([1e9]));
|
||||
var out = lines.map(function (l) {
|
||||
if (!l.trim().length) return l;
|
||||
return all ? l.replace(/^(\s*)\/\/ ?/, '$1') : l.slice(0, indent) + '// ' + l.slice(indent);
|
||||
}).join('\n');
|
||||
replaceRange(ed, sp[0], sp[1], out);
|
||||
ta.setSelectionRange(sp[0], sp[0] + out.length);
|
||||
}
|
||||
|
||||
function editorKeys(ed, ev) {
|
||||
var ta = ed.ta, k = ev.key, mod = ev.ctrlKey || ev.metaKey;
|
||||
if (k === 'Tab' && !mod && !ev.altKey) { ev.preventDefault(); indentSelection(ed, ev.shiftKey); return; }
|
||||
if (k === 'Enter' && !mod && !ev.altKey && !ev.shiftKey) {
|
||||
ev.preventDefault();
|
||||
var a = ta.selectionStart, t = ta.value, ls = t.lastIndexOf('\n', a - 1) + 1;
|
||||
var lead = /^[ \t]*/.exec(t.slice(ls, a))[0];
|
||||
var before = t.slice(ls, a).replace(/\s+$/, '');
|
||||
var extra = (ed.tab.kind === 'grammar' && /[:({]$/.test(before)) ? ' ' : '';
|
||||
replaceRange(ed, a, ta.selectionEnd, '\n' + lead + extra);
|
||||
return;
|
||||
}
|
||||
if (mod && k === '/') { ev.preventDefault(); toggleComment(ed); return; }
|
||||
if (mod && (k === 'f' || k === 'F' || k === 'h' || k === 'H')) { ev.preventDefault(); openFind(ed); return; }
|
||||
if (k === 'Escape' && ed.findBar && !ed.findBar.hidden) { ev.preventDefault(); closeFind(ed); }
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------- find */
|
||||
function buildFind(ed) {
|
||||
var bar = el('div', 'find');
|
||||
bar.setAttribute('role', 'search');
|
||||
var r1 = el('div', 'row'), r2 = el('div', 'row');
|
||||
var q = el('input'); q.type = 'text'; q.placeholder = 'Find'; q.setAttribute('aria-label', 'Find');
|
||||
var cnt = el('span', 'cnt');
|
||||
var bPrev = el('button', 'btn sm', '\u2191'); bPrev.title = 'Previous (Shift+Enter)';
|
||||
var bNext = el('button', 'btn sm', '\u2193'); bNext.title = 'Next (Enter)';
|
||||
var bCase = el('button', 'btn sm', 'Aa'); bCase.title = 'Match case'; bCase.setAttribute('aria-pressed', 'false');
|
||||
var bX = el('button', 'btn sm', '\u00d7'); bX.title = 'Close (Esc)';
|
||||
var rep = el('input'); rep.type = 'text'; rep.placeholder = 'Replace'; rep.setAttribute('aria-label', 'Replace with');
|
||||
var bRep = el('button', 'btn sm', 'Replace');
|
||||
var bAll = el('button', 'btn sm', 'All');
|
||||
[bPrev, bNext, bCase, bX, bRep, bAll].forEach(function (b) { b.type = 'button'; });
|
||||
[q, bPrev, bNext, bCase, cnt, bX].forEach(function (n) { r1.appendChild(n); });
|
||||
[rep, bRep, bAll].forEach(function (n) { r2.appendChild(n); });
|
||||
bar.appendChild(r1); bar.appendChild(r2);
|
||||
ed.root.appendChild(bar);
|
||||
ed.findBar = bar;
|
||||
ed.fq = q; ed.fr = rep; ed.fcnt = cnt; ed.fcase = false;
|
||||
|
||||
q.oninput = function () { runFind(ed, true); };
|
||||
q.onkeydown = function (ev) {
|
||||
if (ev.key === 'Enter') { ev.preventDefault(); stepFind(ed, ev.shiftKey ? -1 : 1); }
|
||||
else if (ev.key === 'Escape') { ev.preventDefault(); closeFind(ed); }
|
||||
};
|
||||
rep.onkeydown = function (ev) {
|
||||
if (ev.key === 'Enter') { ev.preventDefault(); replaceOne(ed); }
|
||||
else if (ev.key === 'Escape') { ev.preventDefault(); closeFind(ed); }
|
||||
};
|
||||
bPrev.onclick = function () { stepFind(ed, -1); };
|
||||
bNext.onclick = function () { stepFind(ed, 1); };
|
||||
bCase.onclick = function () {
|
||||
ed.fcase = !ed.fcase;
|
||||
bCase.setAttribute('aria-pressed', ed.fcase ? 'true' : 'false');
|
||||
runFind(ed, true);
|
||||
};
|
||||
bX.onclick = function () { closeFind(ed); };
|
||||
bRep.onclick = function () { replaceOne(ed); };
|
||||
bAll.onclick = function () { replaceAll(ed); };
|
||||
}
|
||||
|
||||
function openFind(ed) {
|
||||
if (!ed.findBar) buildFind(ed);
|
||||
ed.findBar.hidden = false;
|
||||
var sel = ed.ta.value.slice(ed.ta.selectionStart, ed.ta.selectionEnd);
|
||||
if (sel && sel.indexOf('\n') < 0) ed.fq.value = sel;
|
||||
ed.fq.focus();
|
||||
ed.fq.select();
|
||||
runFind(ed, false);
|
||||
}
|
||||
|
||||
function closeFind(ed) {
|
||||
if (!ed.findBar) return;
|
||||
ed.findBar.hidden = true;
|
||||
ed.find = null;
|
||||
paintEditor(ed);
|
||||
ed.ta.focus();
|
||||
}
|
||||
|
||||
function findMatches(ed) {
|
||||
var q = ed.fq.value, out = [];
|
||||
if (!q) return out;
|
||||
var hay = ed.fcase ? ed.ta.value : ed.ta.value.toLowerCase();
|
||||
var needle = ed.fcase ? q : q.toLowerCase();
|
||||
for (var i = hay.indexOf(needle); i >= 0 && out.length < 5000; i = hay.indexOf(needle, i + Math.max(1, needle.length))) {
|
||||
out.push([i, i + needle.length]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function runFind(ed, jump) {
|
||||
var ms = findMatches(ed), cur = -1;
|
||||
if (ms.length) {
|
||||
var from = ed.ta.selectionStart;
|
||||
for (var i = 0; i < ms.length; i++) { if (ms[i][0] >= from) { cur = i; break; } }
|
||||
if (cur < 0) cur = 0;
|
||||
}
|
||||
ed.find = ms.length || ed.fq.value ? { matches: ms, cur: cur } : null;
|
||||
ed.fcnt.textContent = ed.fq.value ? (ms.length ? (cur + 1) + ' of ' + ms.length : 'No results') : '';
|
||||
paintEditor(ed);
|
||||
if (jump && cur >= 0) selectMatch(ed, ms[cur]);
|
||||
}
|
||||
|
||||
function selectMatch(ed, m) {
|
||||
ed.ta.setSelectionRange(m[0], m[1]);
|
||||
revealUnit(ed, m[0]);
|
||||
}
|
||||
|
||||
function stepFind(ed, dir) {
|
||||
if (!ed.find || !ed.find.matches.length) { runFind(ed, true); return; }
|
||||
var n = ed.find.matches.length;
|
||||
ed.find.cur = (ed.find.cur + dir + n) % n;
|
||||
ed.fcnt.textContent = (ed.find.cur + 1) + ' of ' + n;
|
||||
paintEditor(ed);
|
||||
selectMatch(ed, ed.find.matches[ed.find.cur]);
|
||||
}
|
||||
|
||||
function replaceOne(ed) {
|
||||
if (!ed.find || ed.find.cur < 0) { runFind(ed, true); return; }
|
||||
var m = ed.find.matches[ed.find.cur];
|
||||
replaceRange(ed, m[0], m[1], ed.fr.value);
|
||||
ed.fq.focus();
|
||||
runFind(ed, true);
|
||||
}
|
||||
|
||||
function replaceAll(ed) {
|
||||
var ms = findMatches(ed);
|
||||
if (!ms.length) return;
|
||||
var t = ed.ta.value, out = '', last = 0;
|
||||
ms.forEach(function (m) { out += t.slice(last, m[0]) + ed.fr.value; last = m[1]; });
|
||||
out += t.slice(last);
|
||||
replaceRange(ed, 0, t.length, out);
|
||||
ed.fq.focus();
|
||||
runFind(ed, false);
|
||||
toast('Replaced ' + plural(ms.length, 'match'));
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ editor */
|
||||
function makeEditor(tab) {
|
||||
var root = el('div', 'ed');
|
||||
root.hidden = true;
|
||||
var banner = el('div', 'banner');
|
||||
banner.hidden = true;
|
||||
var scroller = el('div', 'scroller');
|
||||
var rows = el('div', 'rows');
|
||||
var gutter = el('pre', 'gutter');
|
||||
gutter.setAttribute('aria-hidden', 'true');
|
||||
var content = el('div', 'content');
|
||||
var hl = el('pre', 'hl');
|
||||
hl.setAttribute('aria-hidden', 'true');
|
||||
var ta = el('textarea', 'ta');
|
||||
ta.spellcheck = false;
|
||||
ta.setAttribute('wrap', 'off');
|
||||
ta.setAttribute('autocomplete', 'off');
|
||||
ta.setAttribute('autocorrect', 'off');
|
||||
ta.setAttribute('autocapitalize', 'off');
|
||||
ta.setAttribute('aria-label', 'Editor');
|
||||
content.appendChild(hl);
|
||||
content.appendChild(ta);
|
||||
rows.appendChild(gutter);
|
||||
rows.appendChild(content);
|
||||
scroller.appendChild(rows);
|
||||
root.appendChild(banner);
|
||||
root.appendChild(scroller);
|
||||
|
||||
var ed = { tab: tab, root: root, banner: banner, scroller: scroller, gutter: gutter, hl: hl, ta: ta,
|
||||
diag: [], hover: null, pin: null, find: null, ls: null, lines: 0, findBar: null, onInput: null, onCaret: null };
|
||||
|
||||
ta.addEventListener('input', function () {
|
||||
ed.ls = null;
|
||||
ed.diag = [];
|
||||
ed.pin = null;
|
||||
if (ed.find) runFind(ed, false); else paintEditor(ed);
|
||||
revealUnit(ed, ta.selectionEnd);
|
||||
if (ed.onInput) ed.onInput(ed);
|
||||
if (ed.onCaret) ed.onCaret(ed);
|
||||
});
|
||||
ta.addEventListener('keydown', function (ev) { editorKeys(ed, ev); });
|
||||
['keyup', 'mouseup', 'focus'].forEach(function (name) {
|
||||
ta.addEventListener(name, function (ev) {
|
||||
if (name === 'keyup' && /^(Arrow|Page|Home|End)/.test(ev.key)) revealUnit(ed, ta.selectionEnd);
|
||||
if (ed.onCaret) ed.onCaret(ed);
|
||||
});
|
||||
});
|
||||
return ed;
|
||||
}
|
||||
|
||||
function setEditorText(ed, text) {
|
||||
ed.ta.value = text;
|
||||
ed.ls = null;
|
||||
ed.diag = [];
|
||||
ed.pin = null;
|
||||
ed.find = null;
|
||||
if (ed.findBar) { ed.findBar.hidden = true; }
|
||||
paintEditor(ed);
|
||||
}
|
||||
|
||||
function setEditorRange(ed, kind, range) { // kind: 'hover' | 'pin'; range [s,e) in units or null
|
||||
ed[kind] = range;
|
||||
paintEditor(ed);
|
||||
}
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
|
||||
/* ==================================================================== tabs */
|
||||
var nextTabId = 1;
|
||||
var NEW_GRAMMAR = 'grammar MyGrammar;\n\nprog : expr EOF ;\n\nexpr : term (\'+\' term)* ;\n\nterm : INT | ID | \'(\' expr \')\' ;\n\nINT : [0-9]+ ;\nID : [a-zA-Z_] [a-zA-Z_0-9]* ;\nWS : [ \\t\\r\\n]+ -> skip ;\n';
|
||||
|
||||
function grammarName(text) {
|
||||
var m = /^\s*(?:lexer\s+|parser\s+)?grammar\s+([A-Za-z_]\w*)\s*;/m.exec(text.slice(0, 20000));
|
||||
return m ? m[1] : '';
|
||||
}
|
||||
function getTab(id) {
|
||||
for (var i = 0; i < S.tabs.length; i++) if (S.tabs[i].id === id) return S.tabs[i];
|
||||
return null;
|
||||
}
|
||||
function tabTitle(t) {
|
||||
if (t.path) return basename(t.path);
|
||||
if (t.kind === 'grammar') { var n = grammarName(t.ed.ta.value); return (n || 'Untitled') + '.g4'; }
|
||||
return 'Input ' + t.num;
|
||||
}
|
||||
function firstOfKind(kind) {
|
||||
for (var i = 0; i < S.tabs.length; i++) if (S.tabs[i].kind === kind) return S.tabs[i];
|
||||
return null;
|
||||
}
|
||||
function ensureLast() {
|
||||
var g = getTab(S.lastGrammar), i = getTab(S.lastInput);
|
||||
if (!g || g.kind !== 'grammar') { g = firstOfKind('grammar'); S.lastGrammar = g ? g.id : null; }
|
||||
if (!i || i.kind !== 'input') { i = firstOfKind('input'); S.lastInput = i ? i.id : null; }
|
||||
}
|
||||
|
||||
function addTab(o) {
|
||||
var t = { id: 't' + (nextTabId++), kind: o.kind, path: o.path || '', num: 0, saved: o.text, etag: o.etag || '',
|
||||
eol: o.eol || 'lf', bom: !!o.bom, dirty: false, rev: 0, banner: '', diskEtag: '', shown: '' };
|
||||
if (!t.path) t.num = ++S.untitled[t.kind];
|
||||
t.ed = makeEditor(t);
|
||||
t.ed.onInput = function () { onEdit(t); };
|
||||
t.ed.onCaret = updatePos;
|
||||
setEditorText(t.ed, o.text);
|
||||
$('editors').appendChild(t.ed.root);
|
||||
S.tabs.push(t);
|
||||
return t;
|
||||
}
|
||||
|
||||
function onEdit(t) {
|
||||
t.rev++;
|
||||
if (t.kind === 'grammar') S.grev++;
|
||||
t.dirty = t.ed.ta.value !== t.saved;
|
||||
var sig = (t.dirty ? '*' : '') + tabTitle(t);
|
||||
if (sig !== t.shown) renderTabs();
|
||||
refreshPanels();
|
||||
if (t.kind === 'grammar' || t.id === S.lastInput) scheduleRun(t.kind === 'grammar' ? 800 : 500);
|
||||
}
|
||||
|
||||
function activate(id, focus) {
|
||||
var t = getTab(id);
|
||||
if (!t) return;
|
||||
S.active = id;
|
||||
if (t.kind === 'grammar') S.lastGrammar = id; else S.lastInput = id;
|
||||
ensureLast();
|
||||
S.tabs.forEach(function (x) { x.ed.root.hidden = x.id !== id; });
|
||||
$('welcome').hidden = true;
|
||||
renderTabs();
|
||||
updateBanner(t);
|
||||
updatePos();
|
||||
refreshPanels();
|
||||
if (focus !== false) t.ed.ta.focus();
|
||||
rememberSession();
|
||||
if (S.prefs.autorun) {
|
||||
var it = getTab(S.lastInput), r = it ? S.results[it.id] : null;
|
||||
if (S.lastGrammar && it && (!r || r.rev !== it.rev || r.grev !== S.grev || r.main !== S.lastGrammar)) scheduleRun(120);
|
||||
}
|
||||
}
|
||||
|
||||
function showEmpty() {
|
||||
S.active = null;
|
||||
$('welcome').hidden = false;
|
||||
renderTabs();
|
||||
updatePos();
|
||||
refreshPanels();
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
var bar = $('tabs');
|
||||
clear(bar);
|
||||
var activeEl = null;
|
||||
S.tabs.forEach(function (t) {
|
||||
var title = tabTitle(t);
|
||||
t.shown = (t.dirty ? '*' : '') + title;
|
||||
var on = t.id === S.active;
|
||||
var d = el('div', 'tab');
|
||||
d.setAttribute('role', 'tab');
|
||||
d.setAttribute('aria-selected', on ? 'true' : 'false');
|
||||
d.tabIndex = on ? 0 : -1;
|
||||
d.title = t.path || 'Not saved to disk yet';
|
||||
if (t.dirty) { var dot = el('span', 'dot'); dot.title = 'Unsaved changes'; d.appendChild(dot); }
|
||||
d.appendChild(el('span', '', title));
|
||||
var kind = el('button', 'kind', t.kind === 'grammar' ? 'G' : 'I');
|
||||
kind.type = 'button';
|
||||
kind.title = (t.kind === 'grammar' ? 'Grammar' : 'Input') + ' - click to treat this file as ' + (t.kind === 'grammar' ? 'an input' : 'a grammar');
|
||||
kind.onclick = function (ev) { ev.stopPropagation(); toggleKind(t); };
|
||||
d.appendChild(kind);
|
||||
var x = el('button', 'tx', '\u00d7');
|
||||
x.type = 'button';
|
||||
x.setAttribute('aria-label', 'Close ' + title);
|
||||
x.onclick = function (ev) { ev.stopPropagation(); closeTab(t.id); };
|
||||
d.appendChild(x);
|
||||
d.onclick = function () { activate(t.id); };
|
||||
d.onauxclick = function (ev) { if (ev.button === 1) { ev.preventDefault(); closeTab(t.id); } };
|
||||
d.onkeydown = function (ev) {
|
||||
var i = S.tabs.indexOf(t), n = S.tabs.length;
|
||||
if (ev.key === 'ArrowRight') { ev.preventDefault(); activate(S.tabs[(i + 1) % n].id, false); $('tabs').querySelector('[aria-selected=true]').focus(); }
|
||||
else if (ev.key === 'ArrowLeft') { ev.preventDefault(); activate(S.tabs[(i + n - 1) % n].id, false); $('tabs').querySelector('[aria-selected=true]').focus(); }
|
||||
else if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); activate(t.id); }
|
||||
};
|
||||
bar.appendChild(d);
|
||||
if (on) activeEl = d;
|
||||
});
|
||||
if (activeEl && activeEl.scrollIntoView) activeEl.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
}
|
||||
|
||||
function toggleKind(t) {
|
||||
t.kind = t.kind === 'grammar' ? 'input' : 'grammar';
|
||||
delete S.results[t.id];
|
||||
S.grev++;
|
||||
ensureLast();
|
||||
paintEditor(t.ed);
|
||||
if (t.id === S.active) activate(t.id, false); else renderTabs();
|
||||
refreshPanels();
|
||||
scheduleRun(200);
|
||||
}
|
||||
|
||||
function closeTab(id, force) {
|
||||
var t = getTab(id);
|
||||
if (!t) return Promise.resolve(true);
|
||||
function proceed() {
|
||||
var idx = S.tabs.indexOf(t);
|
||||
S.tabs.splice(idx, 1);
|
||||
t.ed.root.remove();
|
||||
delete S.results[id];
|
||||
if (t.kind === 'grammar') S.grev++;
|
||||
if (S.lastGrammar === id) S.lastGrammar = null;
|
||||
if (S.lastInput === id) S.lastInput = null;
|
||||
ensureLast();
|
||||
if (S.active === id) {
|
||||
var next = S.tabs[Math.min(idx, S.tabs.length - 1)];
|
||||
S.active = null;
|
||||
if (next) activate(next.id); else showEmpty();
|
||||
} else {
|
||||
renderTabs();
|
||||
refreshPanels();
|
||||
}
|
||||
rememberSession();
|
||||
scheduleRun(300);
|
||||
}
|
||||
if (!t.dirty || force) { proceed(); return Promise.resolve(true); }
|
||||
return ask('Unsaved changes', '"' + tabTitle(t) + '" has unsaved changes.',
|
||||
[{ label: 'Save', value: 'save', primary: true }, { label: "Don't save", value: 'discard' }, { label: 'Cancel', value: null }])
|
||||
.then(function (v) {
|
||||
if (v === 'discard') { proceed(); return true; }
|
||||
if (v === 'save') return saveTab(t).then(function (ok) { if (ok) proceed(); return ok; });
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function updatePos() {
|
||||
var t = getTab(S.active);
|
||||
var p = $('pos');
|
||||
if (!t) { p.textContent = ''; return; }
|
||||
var c = caretInfo(t.ed);
|
||||
p.textContent = 'Ln ' + c.line + ', Col ' + c.col;
|
||||
}
|
||||
|
||||
function rememberSession() {
|
||||
if (!S.info) return;
|
||||
var active = getTab(S.active);
|
||||
store.set('session:' + S.info.root, {
|
||||
open: S.tabs.filter(function (t) { return t.path; }).map(function (t) { return { path: t.path, kind: t.kind }; }),
|
||||
active: active ? active.path : ''
|
||||
});
|
||||
}
|
||||
|
||||
/* ========================================================== files on disk */
|
||||
function openFile(path, o) {
|
||||
var ex = null;
|
||||
S.tabs.forEach(function (t) { if (t.path === path) ex = t; });
|
||||
if (ex) { activate(ex.id); return Promise.resolve(ex); }
|
||||
return api('GET', '/api/file?path=' + encodeURIComponent(path)).then(function (f) {
|
||||
var t = addTab({ kind: (o && o.kind) || (isGrammarFile(f.path) ? 'grammar' : 'input'), path: f.path, text: f.text,
|
||||
etag: f.etag, eol: f.eol, bom: f.bom });
|
||||
S.lastDir = dirname(f.path);
|
||||
if (f.lossy) toast(basename(f.path) + ' contains bytes that are not valid UTF-8; they were replaced.', 'error');
|
||||
activate(t.id, !(o && o.noFocus));
|
||||
return t;
|
||||
}).catch(function (e) {
|
||||
toast('Cannot open ' + path + ': ' + e.message, 'error');
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function saveError(e) {
|
||||
toast('Save failed: ' + e.message, 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
function doSave(t, path, extra) {
|
||||
var text = t.ed.ta.value;
|
||||
var body = { path: path, text: text, eol: t.eol, bom: t.bom };
|
||||
Object.keys(extra || {}).forEach(function (k) { body[k] = extra[k]; });
|
||||
return api('POST', '/api/save', body).then(function (r) {
|
||||
t.path = r.path;
|
||||
t.saved = text;
|
||||
t.etag = r.etag;
|
||||
t.dirty = t.ed.ta.value !== text;
|
||||
t.banner = '';
|
||||
S.lastDir = dirname(r.path);
|
||||
renderTabs();
|
||||
updateBanner(t);
|
||||
rememberSession();
|
||||
toast('Saved ' + basename(r.path));
|
||||
var gn = t.kind === 'grammar' ? grammarName(text) : '';
|
||||
if (gn && gn !== stem(basename(r.path))) {
|
||||
toast('ANTLR expects the file to be called ' + gn + '.g4 (the grammar is named ' + gn + ').', 'error');
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function saveTab(t) {
|
||||
if (!t.path) return saveAs(t);
|
||||
return doSave(t, t.path, { baseEtag: t.etag }).catch(function (e) {
|
||||
if (e.status === 409 && e.data && e.data.changed) {
|
||||
return ask('File changed on disk', basename(t.path) + ' was modified outside this editor after you opened it. Saving now overwrites those changes.',
|
||||
[{ label: 'Overwrite', value: 'o', primary: true }, { label: 'Cancel', value: null }]).then(function (v) {
|
||||
if (v !== 'o') return false;
|
||||
return doSave(t, t.path, { force: true }).catch(saveError);
|
||||
});
|
||||
}
|
||||
return saveError(e);
|
||||
});
|
||||
}
|
||||
|
||||
function saveAs(t) {
|
||||
var suggest = t.path ? basename(t.path) : (t.kind === 'grammar' ? tabTitle(t) : 'input' + t.num + '.txt');
|
||||
return fileDialog({ title: 'Save as', mode: 'save', dir: t.path ? dirname(t.path) : S.lastDir, name: suggest, okLabel: 'Save' }).then(function (path) {
|
||||
if (!path) return false;
|
||||
return doSave(t, path, { create: true }).catch(function (e) {
|
||||
if (e.status === 409 && e.data && e.data.exists) {
|
||||
return ask('Replace file?', path + ' already exists. Replace it?', [{ label: 'Replace', value: 'r', primary: true }, { label: 'Cancel', value: null }])
|
||||
.then(function (v) { return v === 'r' ? doSave(t, path, {}).catch(saveError) : false; });
|
||||
}
|
||||
return saveError(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function saveAll() {
|
||||
var chain = Promise.resolve(true);
|
||||
S.tabs.filter(function (t) { return t.dirty; }).forEach(function (t) { chain = chain.then(function () { return saveTab(t); }); });
|
||||
return chain;
|
||||
}
|
||||
|
||||
function reloadTab(t, quiet) {
|
||||
if (!t.path) return Promise.resolve(false);
|
||||
return api('GET', '/api/file?path=' + encodeURIComponent(t.path)).then(function (f) {
|
||||
var caret = Math.min(t.ed.ta.selectionStart, f.text.length);
|
||||
setEditorText(t.ed, f.text);
|
||||
t.ed.ta.setSelectionRange(caret, caret);
|
||||
t.saved = f.text;
|
||||
t.etag = f.etag;
|
||||
t.eol = f.eol;
|
||||
t.bom = f.bom;
|
||||
t.dirty = false;
|
||||
t.banner = '';
|
||||
t.rev++;
|
||||
if (t.kind === 'grammar') S.grev++;
|
||||
renderTabs();
|
||||
updateBanner(t);
|
||||
refreshPanels();
|
||||
toast((quiet ? 'Reloaded ' + basename(t.path) + ' (changed on disk)' : 'Reloaded ' + basename(t.path)));
|
||||
if (t.kind === 'grammar' || t.id === S.lastInput) scheduleRun(300);
|
||||
return true;
|
||||
}).catch(function (e) {
|
||||
toast('Cannot reload ' + t.path + ': ' + e.message, 'error');
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function reloadActive() {
|
||||
var t = getTab(S.active);
|
||||
if (!t || !t.path) return;
|
||||
if (!t.dirty) { reloadTab(t, false); return; }
|
||||
ask('Reload from disk', 'Discard your unsaved changes to ' + tabTitle(t) + ' and reload it?', [{ label: 'Reload', value: 'r', primary: true }, { label: 'Cancel', value: null }])
|
||||
.then(function (v) { if (v === 'r') reloadTab(t, false); });
|
||||
}
|
||||
|
||||
function newGrammar() {
|
||||
var t = addTab({ kind: 'grammar', text: NEW_GRAMMAR });
|
||||
activate(t.id);
|
||||
scheduleRun(300);
|
||||
}
|
||||
function newInput() {
|
||||
var t = addTab({ kind: 'input', text: '' });
|
||||
activate(t.id);
|
||||
}
|
||||
|
||||
function updateBanner(t) {
|
||||
var b = t.ed.banner;
|
||||
clear(b);
|
||||
if (!t.banner) { b.hidden = true; return; }
|
||||
b.hidden = false;
|
||||
function btn(label, fn) { var x = el('button', 'btn sm', label); x.type = 'button'; x.onclick = fn; return x; }
|
||||
if (t.banner === 'deleted') {
|
||||
b.appendChild(el('span', '', 'This file was deleted on disk.'));
|
||||
b.appendChild(btn('Keep editing', function () { t.banner = ''; t.etag = ''; updateBanner(t); }));
|
||||
} else {
|
||||
b.appendChild(el('span', '', 'This file changed on disk.'));
|
||||
b.appendChild(btn('Reload', function () { reloadTab(t, false); }));
|
||||
b.appendChild(btn('Keep my version', function () { t.etag = t.diskEtag; t.banner = ''; updateBanner(t); }));
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------ external change polling */
|
||||
var polling = false;
|
||||
function pollDisk() {
|
||||
if (polling || document.hidden) return;
|
||||
var paths = S.tabs.filter(function (t) { return t.path; }).map(function (t) { return t.path; });
|
||||
if (!paths.length) return;
|
||||
polling = true;
|
||||
api('POST', '/api/stat', { paths: paths }).then(function (r) {
|
||||
S.tabs.forEach(function (t) {
|
||||
if (!t.path || !(t.path in r.etags)) return;
|
||||
var tag = r.etags[t.path];
|
||||
if (tag === t.etag) return;
|
||||
if (tag === null) {
|
||||
if (t.banner !== 'deleted' && t.etag) { t.banner = 'deleted'; updateBanner(t); }
|
||||
return;
|
||||
}
|
||||
if (!t.dirty) { reloadTab(t, true); return; }
|
||||
if (t.diskEtag !== tag || t.banner !== 'changed') { t.diskEtag = tag; t.banner = 'changed'; updateBanner(t); }
|
||||
});
|
||||
}).catch(function () { /* server gone or busy: try again later */ }).then(function () { polling = false; });
|
||||
}
|
||||
setInterval(pollDisk, 2000);
|
||||
window.addEventListener('focus', pollDisk);
|
||||
window.addEventListener('beforeunload', function (ev) {
|
||||
if (S.tabs.some(function (t) { return t.dirty; })) { ev.preventDefault(); ev.returnValue = ''; }
|
||||
});
|
||||
|
||||
/* ======================================================== open / save dialog */
|
||||
function fileDialog(o) {
|
||||
var cur = o.dir !== undefined && o.dir !== null ? o.dir : (S.lastDir || '');
|
||||
var chosen = '';
|
||||
var entries = [];
|
||||
var crumbs = el('div', 'crumbs');
|
||||
var list = el('ul', 'flist');
|
||||
list.setAttribute('role', 'listbox');
|
||||
var filter = null, nameIn = null, ctxRef = null;
|
||||
|
||||
function load(dir) {
|
||||
api('GET', '/api/dir?path=' + encodeURIComponent(dir)).then(function (d) {
|
||||
cur = d.path;
|
||||
entries = d.entries;
|
||||
chosen = '';
|
||||
drawCrumbs();
|
||||
drawList();
|
||||
}).catch(function (e) {
|
||||
toast(e.message, 'error');
|
||||
if (dir) load('');
|
||||
});
|
||||
}
|
||||
function crumb(label, path) {
|
||||
var b = el('button', '', label);
|
||||
b.type = 'button';
|
||||
b.onclick = function () { load(path); };
|
||||
return b;
|
||||
}
|
||||
function drawCrumbs() {
|
||||
clear(crumbs);
|
||||
crumbs.appendChild(crumb(S.info.rootName || 'workspace', ''));
|
||||
var acc = '';
|
||||
(cur ? cur.split('/') : []).forEach(function (p) {
|
||||
acc = joinPath(acc, p);
|
||||
crumbs.appendChild(el('span', 'muted', '/'));
|
||||
crumbs.appendChild(crumb(p, acc));
|
||||
});
|
||||
}
|
||||
function drawList() {
|
||||
clear(list);
|
||||
var q = filter ? filter.value.toLowerCase() : '';
|
||||
var shown = entries.filter(function (e) { return !q || e.name.toLowerCase().indexOf(q) >= 0; });
|
||||
if (!shown.length) list.appendChild(el('li', 'muted', entries.length ? 'No matches' : 'Empty folder'));
|
||||
shown.forEach(function (e) {
|
||||
var li = el('li');
|
||||
li.setAttribute('role', 'option');
|
||||
li.tabIndex = 0;
|
||||
li.appendChild(el('span', '', (e.dir ? '\u25b8 ' : '') + e.name));
|
||||
if (!e.dir) li.appendChild(el('span', 'sz', human(e.size)));
|
||||
var path = joinPath(cur, e.name);
|
||||
function select() {
|
||||
Array.prototype.forEach.call(list.children, function (c) { c.removeAttribute('aria-selected'); });
|
||||
li.setAttribute('aria-selected', 'true');
|
||||
chosen = path;
|
||||
if (nameIn) nameIn.value = e.name;
|
||||
}
|
||||
li.onclick = function () { if (e.dir) load(path); else select(); };
|
||||
li.ondblclick = function () { if (!e.dir) { select(); ctxRef.close('ok'); } };
|
||||
li.onkeydown = function (ev) {
|
||||
if (ev.key === 'Enter') { ev.preventDefault(); ev.stopPropagation(); if (e.dir) load(path); else { select(); ctxRef.close('ok'); } }
|
||||
else if (ev.key === ' ') { ev.preventDefault(); if (e.dir) load(path); else select(); }
|
||||
else if (ev.key === 'ArrowDown' && li.nextSibling) { ev.preventDefault(); li.nextSibling.focus(); }
|
||||
else if (ev.key === 'ArrowUp' && li.previousSibling) { ev.preventDefault(); li.previousSibling.focus(); }
|
||||
};
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
return modal({
|
||||
title: o.title,
|
||||
build: function (ctx) {
|
||||
ctxRef = ctx;
|
||||
var wrap = el('div');
|
||||
wrap.appendChild(crumbs);
|
||||
if (o.mode === 'open') {
|
||||
filter = el('input');
|
||||
filter.type = 'text';
|
||||
filter.placeholder = 'Filter';
|
||||
filter.setAttribute('aria-label', 'Filter files');
|
||||
filter.setAttribute('data-focus', '1');
|
||||
filter.oninput = drawList;
|
||||
filter.onkeydown = function (ev) { if (ev.key === 'ArrowDown' && list.firstChild) { ev.preventDefault(); list.firstChild.focus(); } };
|
||||
wrap.appendChild(filter);
|
||||
}
|
||||
wrap.appendChild(list);
|
||||
if (o.mode === 'save') {
|
||||
var lab = el('p', 'muted', 'File name');
|
||||
lab.style.margin = '10px 0 4px';
|
||||
wrap.appendChild(lab);
|
||||
nameIn = el('input');
|
||||
nameIn.type = 'text';
|
||||
nameIn.value = o.name || '';
|
||||
nameIn.setAttribute('aria-label', 'File name');
|
||||
nameIn.setAttribute('data-focus', '1');
|
||||
wrap.appendChild(nameIn);
|
||||
setTimeout(function () { nameIn.focus(); nameIn.select(); }, 0);
|
||||
}
|
||||
load(cur);
|
||||
return wrap;
|
||||
},
|
||||
buttons: [
|
||||
{ label: 'Cancel', value: null },
|
||||
{ label: o.okLabel || 'Open', value: 'ok', primary: true, check: function () {
|
||||
if (o.mode === 'open' && !chosen) { toast('Select a file first'); return false; }
|
||||
if (o.mode === 'save') {
|
||||
var n = nameIn.value.trim();
|
||||
if (!n || /[\/\\]/.test(n)) { toast('Enter a file name without slashes'); return false; }
|
||||
}
|
||||
return true;
|
||||
} }
|
||||
]
|
||||
}).then(function (v) {
|
||||
if (v !== 'ok') return null;
|
||||
S.lastDir = cur;
|
||||
return o.mode === 'open' ? chosen : joinPath(cur, nameIn.value.trim());
|
||||
});
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
var t = getTab(S.active);
|
||||
fileDialog({ title: 'Open file', mode: 'open', dir: t && t.path ? dirname(t.path) : S.lastDir, okLabel: 'Open' }).then(function (p) {
|
||||
if (p) openFile(p);
|
||||
});
|
||||
}
|
||||
+676
@@ -0,0 +1,676 @@
|
||||
|
||||
/* ============================================================== 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));
|
||||
}
|
||||
|
||||
function initDiagram() {
|
||||
var box = $('dgm'), stage = $('stage'), drag = null, moved = false;
|
||||
box.addEventListener('wheel', function (ev) {
|
||||
if (!V.w) return;
|
||||
ev.preventDefault();
|
||||
var r = box.getBoundingClientRect(), unit = ev.deltaMode === 1 ? 16 : 1;
|
||||
zoomAt(ev.clientX - r.left, ev.clientY - r.top, Math.exp(-ev.deltaY * unit * (ev.ctrlKey ? 0.01 : 0.0015)));
|
||||
}, { passive: false });
|
||||
box.addEventListener('pointerdown', function (ev) {
|
||||
if (ev.button !== 0 || !V.w) return;
|
||||
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(1.25);
|
||||
else if (k === '-') zoomCentre(0.8);
|
||||
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(1.25); };
|
||||
$('zout').onclick = function () { zoomCentre(0.8); };
|
||||
$('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();
|
||||
})();
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
:root{--bg:#e6ecf4;--pane:#ffffff;--edit:#ffffff;--ink:#1b2a41;--muted:#5b6a80;--line:#cbd5e3;
|
||||
--title:#cfe2f5;--title-ink:#0e2a45;--tab:#eef2f8;--hov:#e3ebf6;--hl:rgba(36,86,211,.2);--hl2:#dbe7fb;
|
||||
--accent:#2456d3;--onaccent:#ffffff;--ok:#1a7f45;--err:#c62f2a;--warn:#a86a06;--onstatus:#ffffff;
|
||||
--okbg:#e5f4ea;--errbg:#fbe7e5;--warnbg:#fbf0d9;--findbg:#fff0a8;--curbg:#ffd25e;--dot:#cdd5e1;
|
||||
--c-com:#6b7c93;--c-str:#1f7a3d;--c-set:#a35a00;--c-act:#6b4fb3;--c-ann:#a02a74;--c-kw:#1c4fc4;--c-tok:#0b7a66;--c-lab:#4a5d78;--fs:13px;--lh:20px}
|
||||
@media (prefers-color-scheme:dark){:root:not([data-theme=light]){--bg:#08111f;--pane:#0f1c30;--edit:#0b1626;--ink:#e4ecf8;--muted:#8a9db8;--line:#233752;
|
||||
--title:#17476b;--title-ink:#eef6ff;--tab:#13233a;--hov:#1a2e4a;--hl:rgba(110,168,255,.28);--hl2:#1d3a5e;
|
||||
--accent:#6ea8ff;--onaccent:#07162b;--ok:#4cc38a;--err:#ff7b72;--warn:#e3b341;--onstatus:#08111f;
|
||||
--okbg:#0f2a27;--errbg:#2e1a24;--warnbg:#2b2617;--findbg:#4d4318;--curbg:#7a5f12;--dot:#1b2d48;
|
||||
--c-com:#7e90aa;--c-str:#9ad0a0;--c-set:#e6b673;--c-act:#b39ddb;--c-ann:#f0a8d0;--c-kw:#7fb2ff;--c-tok:#7dd3c0;--c-lab:#9db4d6;}}
|
||||
:root[data-theme=dark]{--bg:#08111f;--pane:#0f1c30;--edit:#0b1626;--ink:#e4ecf8;--muted:#8a9db8;--line:#233752;
|
||||
--title:#17476b;--title-ink:#eef6ff;--tab:#13233a;--hov:#1a2e4a;--hl:rgba(110,168,255,.28);--hl2:#1d3a5e;
|
||||
--accent:#6ea8ff;--onaccent:#07162b;--ok:#4cc38a;--err:#ff7b72;--warn:#e3b341;--onstatus:#08111f;
|
||||
--okbg:#0f2a27;--errbg:#2e1a24;--warnbg:#2b2617;--findbg:#4d4318;--curbg:#7a5f12;--dot:#1b2d48;
|
||||
--c-com:#7e90aa;--c-str:#9ad0a0;--c-set:#e6b673;--c-act:#b39ddb;--c-ann:#f0a8d0;--c-kw:#7fb2ff;--c-tok:#7dd3c0;--c-lab:#9db4d6;}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
[hidden]{display:none!important}
|
||||
html,body{height:100%;margin:0}
|
||||
body{display:flex;flex-direction:column;background:var(--bg);color:var(--ink);overflow:hidden;
|
||||
font:13px/1.4 system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',sans-serif}
|
||||
button,input,select,textarea{font:inherit;color:inherit}
|
||||
pre,code,.mono,.ta,.hl,.gutter,.out,td.mono,#pos{font-family:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,'DejaVu Sans Mono','Liberation Mono',monospace}
|
||||
:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
|
||||
.spacer{flex:1}
|
||||
.muted{color:var(--muted)}
|
||||
|
||||
/* menu bar */
|
||||
#menubar{display:flex;align-items:center;gap:2px;height:32px;padding:0 8px;background:var(--pane);border-bottom:1px solid var(--line);flex:none}
|
||||
.menu-btn{padding:4px 10px;background:none;border:0;border-radius:3px;cursor:pointer}
|
||||
.menu-btn:hover,.menu-btn[aria-expanded=true]{background:var(--hov)}
|
||||
#rootname{margin-left:auto;color:var(--muted);font-size:12px;padding-right:6px}
|
||||
.dropdown{position:fixed;z-index:60;min-width:230px;padding:4px 0;background:var(--pane);border:1px solid var(--line)}
|
||||
.mi{display:flex;align-items:center;gap:14px;padding:5px 14px 5px 26px;cursor:pointer;position:relative;white-space:nowrap}
|
||||
.mi:hover,.mi:focus{background:var(--hl2);outline:0}
|
||||
.mi .k{margin-left:auto;padding-left:24px;color:var(--muted);font-size:12px}
|
||||
.mi .ck{position:absolute;left:8px}
|
||||
.mi[aria-disabled=true]{opacity:.45;pointer-events:none}
|
||||
.sep{height:1px;margin:4px 0;background:var(--line)}
|
||||
|
||||
/* layout */
|
||||
main{flex:1;min-height:0;display:grid;grid-template-columns:var(--leftw,50%) 8px minmax(0,1fr);padding:8px}
|
||||
main.no-right{grid-template-columns:minmax(0,1fr)}
|
||||
main.no-right #split-v,main.no-right #right{display:none}
|
||||
#right{display:grid;grid-template-rows:var(--toph,62%) 8px minmax(0,1fr);min-height:0;min-width:0}
|
||||
#right.one{grid-template-rows:minmax(0,1fr)}
|
||||
.split{background:none;position:relative}
|
||||
.split::after{content:"";position:absolute;background:var(--line);opacity:0;transition:opacity .1s}
|
||||
.split-v::after{left:3px;right:3px;top:0;bottom:0}
|
||||
.split-h::after{top:3px;bottom:3px;left:0;right:0}
|
||||
.split:hover::after,.split:focus-visible::after,.split.drag::after{opacity:1;background:var(--accent)}
|
||||
.split-v{cursor:col-resize}
|
||||
.split-h{cursor:row-resize}
|
||||
.pane{display:flex;flex-direction:column;min-height:0;min-width:0;background:var(--pane);border:1px solid var(--line)}
|
||||
.ptitle{display:flex;align-items:center;gap:10px;flex:none;padding:5px 6px 5px 12px;background:var(--title);color:var(--title-ink);font-weight:600}
|
||||
.x{background:none;border:0;border-radius:3px;padding:0 6px;font-size:17px;line-height:1.1;cursor:pointer;opacity:.75}
|
||||
.x:hover{opacity:1;background:rgba(127,127,127,.25)}
|
||||
.psub{display:flex;align-items:center;gap:8px;flex:none;min-height:36px;padding:4px 8px;border-bottom:1px solid var(--line)}
|
||||
.seg{display:flex;border:1px solid var(--line);border-radius:4px;overflow:hidden}
|
||||
.seg button{border:0;background:none;padding:3px 11px;color:var(--muted);cursor:pointer;white-space:nowrap}
|
||||
.seg button:hover{color:var(--ink)}
|
||||
.seg button[aria-selected=true]{background:var(--hl2);color:var(--ink);font-weight:600}
|
||||
.tools{display:flex;gap:5px}
|
||||
.btn{padding:4px 12px;background:var(--pane);border:1px solid var(--line);border-radius:4px;cursor:pointer;white-space:nowrap}
|
||||
.btn:hover{border-color:var(--accent)}
|
||||
.btn.sm{padding:2px 9px}
|
||||
.btn[aria-pressed=true]{background:var(--hl2);border-color:var(--accent)}
|
||||
.btn.primary{background:var(--accent);color:var(--onaccent);border-color:var(--accent);font-weight:600}
|
||||
.view{flex:1;min-height:0;position:relative;overflow:hidden}
|
||||
.scroll{overflow:auto;padding:10px 14px}
|
||||
.flush{padding:0}
|
||||
.note{margin:0;padding:18px;color:var(--muted)}
|
||||
.out{margin:0;font-size:12.5px;line-height:1.55;white-space:pre-wrap;word-break:break-word}
|
||||
.chip{padding:0 9px;border-radius:9px;font-size:12px;font-weight:600;background:var(--ok);color:var(--onstatus)}
|
||||
.chip.warn{background:var(--warn)}
|
||||
.chip.error,.chip.fail{background:var(--err)}
|
||||
.chip.stale{opacity:.55}
|
||||
.badge{margin-left:6px;padding:0 6px;border-radius:9px;background:var(--err);color:var(--onstatus);font-size:11px;font-weight:600}
|
||||
.badge.warn{background:var(--warn)}
|
||||
|
||||
/* tabs and toolbar */
|
||||
#tabs{display:flex;flex:none;overflow-x:auto;background:var(--tab);border-bottom:1px solid var(--line)}
|
||||
.tab{display:flex;align-items:center;gap:8px;padding:6px 6px 6px 12px;background:var(--tab);color:var(--muted);border-right:1px solid var(--line);cursor:pointer;white-space:nowrap;user-select:none}
|
||||
.tab:hover{color:var(--ink)}
|
||||
.tab[aria-selected=true]{background:var(--title);color:var(--title-ink);font-weight:600}
|
||||
.tab .kind{font:600 11px ui-monospace,Menlo,Consolas,monospace;padding:0 4px;border:1px solid currentColor;border-radius:2px;background:none;color:inherit;opacity:.8;cursor:pointer}
|
||||
.tab .kind:hover{opacity:1}
|
||||
.tab .dot{width:8px;height:8px;border-radius:50%;background:var(--warn)}
|
||||
.tab .tx{background:none;border:0;border-radius:3px;padding:0 5px;font-size:15px;line-height:1.1;cursor:pointer;color:inherit;opacity:.7}
|
||||
.tab .tx:hover{opacity:1;background:rgba(127,127,127,.25)}
|
||||
#toolbar{display:flex;align-items:center;gap:14px;flex:none;min-height:36px;padding:4px 8px;border-bottom:1px solid var(--line)}
|
||||
.tb-item{display:flex;align-items:center;gap:6px;color:var(--muted)}
|
||||
.tb-item select{background:var(--pane);border:1px solid var(--line);border-radius:4px;padding:2px 4px;max-width:180px}
|
||||
#pos{font-size:12px}
|
||||
.runbtn{padding:4px 18px;border:0;border-radius:4px;background:var(--accent);color:var(--onaccent);font-weight:600;cursor:pointer}
|
||||
.runbtn[data-busy=true]{opacity:.6}
|
||||
.runbtn:disabled{opacity:.4;cursor:default}
|
||||
|
||||
/* editor */
|
||||
#editors{flex:1;min-height:0;position:relative;background:var(--edit)}
|
||||
.ed{position:absolute;inset:0;display:flex;flex-direction:column}
|
||||
.banner{display:flex;align-items:center;gap:10px;flex:none;padding:6px 12px;background:var(--warnbg);border-bottom:1px solid var(--line)}
|
||||
.scroller{flex:1;min-height:0;overflow:auto;position:relative}
|
||||
.rows{display:grid;grid-template-columns:auto 1fr;width:max-content;min-width:100%;min-height:100%}
|
||||
.gutter,.hl,.ta{font-size:var(--fs);line-height:var(--lh);tab-size:4;font-variant-ligatures:none;letter-spacing:normal}
|
||||
.gutter{position:sticky;left:0;z-index:1;margin:0;padding:8px 10px 8px 14px;text-align:right;color:var(--muted);background:var(--edit);border-right:1px solid var(--line);user-select:none}
|
||||
.content{position:relative;min-width:0}
|
||||
.hl{margin:0;padding:8px 28px 8px 10px;min-height:100%;white-space:pre;color:var(--ink);pointer-events:none}
|
||||
.ta{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:8px 28px 8px 10px;border:0;resize:none;overflow:hidden;white-space:pre;background:transparent;color:transparent;caret-color:var(--ink);outline:0}
|
||||
.ta::selection{background:rgba(110,168,255,.38);color:transparent}
|
||||
.t-com{color:var(--c-com);font-style:italic}
|
||||
.t-str{color:var(--c-str)}
|
||||
.t-set{color:var(--c-set)}
|
||||
.t-act{color:var(--c-act)}
|
||||
.t-ann{color:var(--c-ann)}
|
||||
.t-kw{color:var(--c-kw);font-weight:600}
|
||||
.t-tok{color:var(--c-tok)}
|
||||
.t-def{font-weight:700}
|
||||
.t-tokdef{color:var(--c-tok);font-weight:700}
|
||||
.t-lab{color:var(--c-lab);font-style:italic}
|
||||
.t-op{color:var(--muted)}
|
||||
.mk-err{background:var(--errbg);text-decoration:underline wavy var(--err);text-underline-offset:3px}
|
||||
.mk-warn{text-decoration:underline wavy var(--warn);text-underline-offset:3px}
|
||||
.mk-hl{background:var(--hl);box-shadow:0 0 0 1px var(--accent);border-radius:2px}
|
||||
.mk-find{background:var(--findbg)}
|
||||
.mk-cur{background:var(--curbg)}
|
||||
.find{position:absolute;top:6px;right:22px;z-index:4;display:grid;gap:6px;padding:8px;background:var(--pane);border:1px solid var(--line)}
|
||||
.find .row{display:flex;gap:5px;align-items:center}
|
||||
.find input{width:190px;padding:3px 6px;background:var(--edit);border:1px solid var(--line);border-radius:3px}
|
||||
.find .cnt{min-width:58px;color:var(--muted);font-size:12px}
|
||||
#welcome{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;color:var(--muted);text-align:center;padding:20px}
|
||||
#welcome h2{margin:0;color:var(--ink);font-size:18px}
|
||||
#welcome p{margin:0 0 10px}
|
||||
.wbtns{display:flex;gap:8px}
|
||||
|
||||
/* diagram */
|
||||
#dgm{cursor:grab;touch-action:none;background-color:var(--bg);background-image:radial-gradient(var(--dot) 1px,transparent 1px);background-size:22px 22px}
|
||||
#dgm.drag{cursor:grabbing}
|
||||
#dgm.empty{cursor:default}
|
||||
#dgm.stale #stage{opacity:.45}
|
||||
#stage{position:absolute;left:0;top:0;transform-origin:0 0}
|
||||
#stage svg{display:block;overflow:visible}
|
||||
#stage .al-node{cursor:pointer}
|
||||
#dnote{position:absolute;left:0;right:0;top:0}
|
||||
|
||||
/* tokens, problems */
|
||||
table{border-collapse:collapse;width:100%;font-size:12.5px}
|
||||
th{position:sticky;top:-10px;background:var(--pane);text-align:left;font-weight:600;color:var(--muted);padding:5px 10px;border-bottom:1px solid var(--line)}
|
||||
td{padding:3px 10px;border-bottom:1px solid var(--line);vertical-align:top}
|
||||
tbody tr:hover{background:var(--hl)}
|
||||
tr.side td{opacity:.55}
|
||||
td.num{text-align:right;color:var(--muted)}
|
||||
.probs{list-style:none;margin:0;padding:0}
|
||||
.prob{display:grid;grid-template-columns:60px 130px minmax(0,1fr) auto;gap:10px;padding:7px 14px;border-bottom:1px solid var(--line);cursor:pointer}
|
||||
.prob:hover,.prob[aria-current=true]{background:var(--hl)}
|
||||
.prob .sev{font-weight:600;color:var(--err)}
|
||||
.prob.warning .sev{color:var(--warn)}
|
||||
.prob .loc{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--muted);overflow-wrap:anywhere}
|
||||
.prob .src{color:var(--muted);font-size:12px}
|
||||
|
||||
/* dialogs, toasts */
|
||||
.modal-back{position:fixed;inset:0;z-index:80;display:flex;align-items:flex-start;justify-content:center;padding-top:12vh;background:rgba(5,10,20,.5)}
|
||||
.modal{width:min(560px,92vw);max-height:76vh;display:flex;flex-direction:column;background:var(--pane);border:1px solid var(--line)}
|
||||
.modal h3{margin:0;padding:10px 14px;font-size:14px;background:var(--title);color:var(--title-ink)}
|
||||
.mbody{padding:12px 14px;overflow:auto;min-height:0}
|
||||
.mbody p{margin:0 0 6px}
|
||||
.mfoot{display:flex;justify-content:flex-end;gap:8px;padding:10px 14px;border-top:1px solid var(--line)}
|
||||
.crumbs{display:flex;flex-wrap:wrap;gap:2px;margin-bottom:8px;font-size:12.5px}
|
||||
.crumbs button{background:none;border:0;padding:1px 5px;border-radius:3px;cursor:pointer;color:var(--accent)}
|
||||
.crumbs button:hover{background:var(--hov)}
|
||||
.flist{list-style:none;margin:8px 0 0;padding:0;height:260px;overflow:auto;border:1px solid var(--line);background:var(--edit)}
|
||||
.flist li{display:flex;gap:10px;padding:4px 10px;cursor:pointer}
|
||||
.flist li:hover{background:var(--hov)}
|
||||
.flist li[aria-selected=true]{background:var(--hl2)}
|
||||
.flist .sz{margin-left:auto;color:var(--muted);font-size:12px}
|
||||
.mbody input[type=text]{width:100%;padding:5px 8px;background:var(--edit);border:1px solid var(--line);border-radius:4px}
|
||||
#toasts{position:fixed;left:14px;bottom:14px;z-index:90;display:flex;flex-direction:column;gap:6px}
|
||||
.toast{padding:7px 12px;background:var(--title);color:var(--title-ink);border:1px solid var(--line);max-width:420px}
|
||||
.toast.error{background:var(--err);color:var(--onstatus)}
|
||||
|
||||
@media (max-width:900px){
|
||||
body{overflow:auto}
|
||||
main{display:block;overflow:visible}
|
||||
.split{display:none}
|
||||
#left{height:70vh}
|
||||
#right{display:block;margin-top:8px}
|
||||
#right>.panel{height:70vh;margin-bottom:8px}
|
||||
}
|
||||
@media (prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto!important}}
|
||||
@@ -0,0 +1,85 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>antlrlab</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%2317476b'/%3E%3Cpath d='M8 3.5v3.5M4 12.5V9.5h8v3M8 7v2.5' stroke='%23fff' fill='none' stroke-width='1.4'/%3E%3C/svg%3E">
|
||||
<link rel="stylesheet" href="/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<header id="menubar" role="menubar" aria-label="Main menu"></header>
|
||||
<main id="main">
|
||||
<section id="left" class="pane" aria-label="Editor">
|
||||
<div id="tabs" role="tablist" aria-label="Open files"></div>
|
||||
<div id="toolbar">
|
||||
<label class="tb-item" id="startwrap" hidden>Start rule
|
||||
<select id="startrule" aria-label="Start rule"></select>
|
||||
</label>
|
||||
<label class="tb-item"><input type="checkbox" id="autorun"> Auto-run</label>
|
||||
<span class="spacer"></span>
|
||||
<span id="pos" class="muted"></span>
|
||||
<button id="run" class="runbtn" type="button" title="Run (Ctrl+Enter)">Run</button>
|
||||
</div>
|
||||
<div id="editors">
|
||||
<div id="welcome" hidden>
|
||||
<h2>No file open</h2>
|
||||
<p>Open a grammar and an input file from disk, or start with new ones.</p>
|
||||
<div class="wbtns">
|
||||
<button type="button" class="btn" id="w-open">Open…</button>
|
||||
<button type="button" class="btn" id="w-grammar">New grammar</button>
|
||||
<button type="button" class="btn" id="w-input">New input</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div id="split-v" class="split split-v" role="separator" aria-orientation="vertical" aria-label="Resize editor and results" tabindex="0"></div>
|
||||
<section id="right">
|
||||
<section id="p-diagram" class="pane panel" aria-label="Diagram">
|
||||
<div class="ptitle"><span id="dtitle">Diagram</span><span id="chip" class="chip" hidden></span><span class="spacer"></span><button class="x" id="x-diagram" type="button" aria-label="Close diagram panel">×</button></div>
|
||||
<div class="psub">
|
||||
<div class="seg" role="tablist" aria-label="Result view">
|
||||
<button type="button" role="tab" id="v-diagram">Diagram</button>
|
||||
<button type="button" role="tab" id="v-tree">Tree</button>
|
||||
<button type="button" role="tab" id="v-tokens">Tokens</button>
|
||||
</div>
|
||||
<span class="spacer"></span>
|
||||
<span id="dtools" class="tools">
|
||||
<button class="btn sm" id="zin" type="button" aria-label="Zoom in">+</button>
|
||||
<button class="btn sm" id="zout" type="button" aria-label="Zoom out">−</button>
|
||||
<button class="btn sm" id="zfit" type="button">Fit</button>
|
||||
<button class="btn sm" id="psvg" type="button">SVG</button>
|
||||
<button class="btn sm" id="ppng" type="button">PNG</button>
|
||||
</span>
|
||||
<span id="ttools" class="tools" hidden>
|
||||
<button class="btn sm" id="b-pretty" type="button">Indented</button>
|
||||
<button class="btn sm" id="b-flat" type="button">One line</button>
|
||||
<button class="btn sm" id="b-copy" type="button">Copy</button>
|
||||
</span>
|
||||
</div>
|
||||
<div id="dgm" class="view" tabindex="0" aria-label="Parse tree diagram. Drag to pan, scroll to zoom.">
|
||||
<div id="stage"></div>
|
||||
<p id="dnote" class="note" hidden></p>
|
||||
</div>
|
||||
<div id="treeview" class="view scroll" hidden><pre id="treeout" class="out"></pre></div>
|
||||
<div id="tokview" class="view scroll" hidden></div>
|
||||
</section>
|
||||
<div id="split-h" class="split split-h" role="separator" aria-orientation="horizontal" aria-label="Resize diagram and output" tabindex="0"></div>
|
||||
<section id="p-output" class="pane panel" aria-label="Output">
|
||||
<div class="ptitle"><span id="otitle">Output</span><span class="spacer"></span><button class="x" id="x-output" type="button" aria-label="Close output panel">×</button></div>
|
||||
<div class="psub">
|
||||
<div class="seg" role="tablist" aria-label="Output view">
|
||||
<button type="button" role="tab" id="o-problems">Problems<span class="badge" id="badge" hidden></span></button>
|
||||
<button type="button" role="tab" id="o-log">ANTLR tool</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="probview" class="view scroll flush"><p id="noprob" class="note">No problems.</p><ul id="problist" class="probs"></ul></div>
|
||||
<div id="logview" class="view scroll" hidden><pre id="logout" class="out"></pre></div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
<div id="toasts" aria-live="polite"></div>
|
||||
<div id="modal-root"></div>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user