Files
AntlrLab/web/10-core.js
T
alejo ba5971a788
build / linux-clang (push) Canceled after 0s
build / linux-gcc (push) Canceled after 0s
build / macos-clang (push) Canceled after 0s
build / windows-mingw (push) Canceled after 0s
build / windows-msvc (push) Canceled after 0s
Add project files
2026-10-07 00:25:24 -03:00

227 lines
9.2 KiB
JavaScript

(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(); });