291 lines
10 KiB
JavaScript
291 lines
10 KiB
JavaScript
// The global map of forest node index => NodeView.
|
|
views = [];
|
|
// NodeView is a visible forest node.
|
|
// It has an entry in the navigation tree, and a span in the code itself.
|
|
// Each NodeView is associated with a forest node, but not all nodes have views:
|
|
// - nodes not reachable though current ambiguity selection
|
|
// - trivial "wrapping" sequence nodes are abbreviated away
|
|
class NodeView {
|
|
// Builds a node representing forest[index], or its target if it is a wrapper.
|
|
// Registers the node in the global map.
|
|
static make(index, parent, abbrev) {
|
|
var node = forest[index];
|
|
if (node.kind == 'sequence' && node.children.length == 1 &&
|
|
forest[node.children[0]].kind != 'ambiguous') {
|
|
abbrev ||= [];
|
|
abbrev.push(index);
|
|
return NodeView.make(node.children[0], parent, abbrev);
|
|
}
|
|
return views[index] = new NodeView(index, parent, node, abbrev);
|
|
}
|
|
|
|
constructor(index, parent, node, abbrev) {
|
|
this.abbrev = abbrev || [];
|
|
this.parent = parent;
|
|
this.children =
|
|
(node.kind == 'ambiguous' ? [ node.selected ] : node.children || [])
|
|
.map((c) => NodeView.make(c, this));
|
|
this.index = index;
|
|
this.node = node;
|
|
views[index] = this;
|
|
|
|
this.span = this.buildSpan();
|
|
this.tree = this.buildTree();
|
|
}
|
|
|
|
// Replaces the token sequence in #code with a <span class=node>.
|
|
buildSpan() {
|
|
var elt = document.createElement('span');
|
|
elt.dataset['index'] = this.index;
|
|
elt.classList.add("node");
|
|
elt.classList.add("selectable-node");
|
|
elt.classList.add(this.node.kind);
|
|
|
|
var begin = null, end = null;
|
|
if (this.children.length != 0) {
|
|
begin = this.children[0].span;
|
|
end = this.children[this.children.length - 1].span.nextSibling;
|
|
} else if (this.node.kind == 'terminal') {
|
|
begin = document.getElementById(this.node.token);
|
|
end = begin.nextSibling;
|
|
} else if (this.node.kind == 'opaque') {
|
|
begin = document.getElementById(this.node.firstToken);
|
|
end = (this.node.lastToken == null)
|
|
? begin
|
|
: document.getElementById(this.node.lastToken).nextSibling;
|
|
}
|
|
var parent = begin.parentNode;
|
|
splice(begin, end, elt);
|
|
parent.insertBefore(elt, end);
|
|
return elt;
|
|
}
|
|
|
|
// Returns a (detached) <li class=tree-node> suitable for use in #tree.
|
|
buildTree() {
|
|
var elt = document.createElement('li');
|
|
elt.dataset['index'] = this.index;
|
|
elt.classList.add('tree-node');
|
|
elt.classList.add('selectable-node');
|
|
elt.classList.add(this.node.kind);
|
|
var header = document.createElement('header');
|
|
elt.appendChild(header);
|
|
|
|
if (this.abbrev.length > 0) {
|
|
var abbrev = document.createElement('span');
|
|
abbrev.classList.add('abbrev');
|
|
abbrev.innerText = forest[this.abbrev[0]].symbol;
|
|
header.appendChild(abbrev);
|
|
}
|
|
var name = document.createElement('span');
|
|
name.classList.add('name');
|
|
name.innerText = this.node.symbol;
|
|
header.appendChild(name);
|
|
|
|
if (this.children.length != 0) {
|
|
var sublist = document.createElement('ul');
|
|
this.children.forEach((c) => sublist.appendChild(c.tree));
|
|
elt.appendChild(sublist);
|
|
}
|
|
return elt;
|
|
}
|
|
|
|
// Make this view visible on the screen by scrolling if needed.
|
|
scrollVisible() {
|
|
scrollIntoViewV(document.getElementById('tree'), this.tree.firstChild);
|
|
scrollIntoViewV(document.getElementById('code'), this.span);
|
|
}
|
|
|
|
// Fill #info with details of this node.
|
|
renderInfo() {
|
|
document.getElementById('info').classList = this.node.kind;
|
|
document.getElementById('i_symbol').innerText = this.node.symbol;
|
|
document.getElementById('i_kind').innerText = this.node.kind;
|
|
|
|
// For sequence nodes, add LHS := RHS rule.
|
|
// If this node abbreviates trivial sequences, we want those rules too.
|
|
var rules = document.getElementById('i_rules');
|
|
rules.textContent = '';
|
|
function addRule(i) {
|
|
var ruleText = forest[i].rule;
|
|
if (ruleText == null)
|
|
return;
|
|
var rule = document.createElement('div');
|
|
rule.classList.add('rule');
|
|
rule.innerText = ruleText;
|
|
rules.insertBefore(rule, rules.firstChild);
|
|
}
|
|
this.abbrev.forEach(addRule);
|
|
addRule(this.index);
|
|
|
|
// For ambiguous nodes, show a selectable list of alternatives.
|
|
var alternatives = document.getElementById('i_alternatives');
|
|
alternatives.textContent = '';
|
|
var that = this;
|
|
function addAlternative(i) {
|
|
var altNode = forest[i];
|
|
var text = altNode.rule || altNode.kind;
|
|
var alt = document.createElement('div');
|
|
alt.classList.add('alternative');
|
|
alt.innerText = text;
|
|
alt.dataset['index'] = i;
|
|
alt.dataset['parent'] = that.index;
|
|
if (i == that.node.selected)
|
|
alt.classList.add('selected');
|
|
alternatives.appendChild(alt);
|
|
}
|
|
if (this.node.kind == 'ambiguous')
|
|
this.node.children.forEach(addAlternative);
|
|
|
|
// Show the stack of ancestor nodes.
|
|
// The part of each rule that leads to the current node is bolded.
|
|
var ancestors = document.getElementById('i_ancestors');
|
|
ancestors.textContent = '';
|
|
var child = this;
|
|
for (var view = this.parent; view != null;
|
|
child = view, view = view.parent) {
|
|
var indexInParent = view.children.indexOf(child);
|
|
|
|
var ctx = document.createElement('div');
|
|
ctx.classList.add('ancestors');
|
|
ctx.classList.add('selectable-node');
|
|
ctx.classList.add(view.node.kind);
|
|
if (view.node.rule) {
|
|
// Rule syntax is LHS := RHS1 [annotation] RHS2.
|
|
// We walk through the chunks and bold the one at parentInIndex.
|
|
var chunkCount = 0;
|
|
ctx.innerHTML = view.node.rule.replaceAll(/[^ ]+/g, function(match) {
|
|
if (!(match.startsWith('[') && match.endsWith(']')) /*annotations*/
|
|
&& chunkCount++ == indexInParent + 2 /*skip LHS :=*/)
|
|
return '<b>' + match + '</b>';
|
|
return match;
|
|
});
|
|
} else /*ambiguous*/ {
|
|
ctx.innerHTML = '<b>' + view.node.symbol + '</b>';
|
|
}
|
|
ctx.dataset['index'] = view.index;
|
|
if (view.abbrev.length > 0) {
|
|
var abbrev = document.createElement('span');
|
|
abbrev.classList.add('abbrev');
|
|
abbrev.innerText = forest[view.abbrev[0]].symbol;
|
|
ctx.insertBefore(abbrev, ctx.firstChild);
|
|
}
|
|
|
|
ctx.dataset['index'] = view.index;
|
|
ancestors.appendChild(ctx, ancestors.firstChild);
|
|
}
|
|
}
|
|
|
|
remove() {
|
|
this.children.forEach((c) => c.remove());
|
|
splice(this.span.firstChild, null, this.span.parentNode,
|
|
this.span.nextSibling);
|
|
detach(this.span);
|
|
delete views[this.index];
|
|
}
|
|
};
|
|
|
|
var selection = null;
|
|
function selectView(view) {
|
|
var old = selection;
|
|
selection = view;
|
|
if (view == old)
|
|
return;
|
|
|
|
if (old) {
|
|
old.tree.classList.remove('selected');
|
|
old.span.classList.remove('selected');
|
|
}
|
|
document.getElementById('info').hidden = (view == null);
|
|
if (!view)
|
|
return;
|
|
view.tree.classList.add('selected');
|
|
view.span.classList.add('selected');
|
|
view.renderInfo();
|
|
view.scrollVisible();
|
|
}
|
|
|
|
// To highlight nodes on hover, we create dynamic CSS rules of the form
|
|
// .selectable-node[data-index="42"] { background-color: blue; }
|
|
// This avoids needing to find all the related nodes and update their classes.
|
|
var highlightSheet = new CSSStyleSheet();
|
|
document.adoptedStyleSheets.push(highlightSheet);
|
|
function highlightView(view) {
|
|
var text = '';
|
|
for (const color of ['#6af', '#bbb', '#ddd', '#eee']) {
|
|
if (view == null)
|
|
break;
|
|
text += '.selectable-node[data-index="' + view.index + '"] '
|
|
text += '{ background-color: ' + color + '; }\n';
|
|
view = view.parent;
|
|
}
|
|
highlightSheet.replace(text);
|
|
}
|
|
|
|
// Select which branch of an ambiguous node is taken.
|
|
function chooseAlternative(parent, index) {
|
|
var parentView = views[parent];
|
|
parentView.node.selected = index;
|
|
var oldChild = parentView.children[0];
|
|
oldChild.remove();
|
|
var newChild = NodeView.make(index, parentView);
|
|
parentView.children[0] = newChild;
|
|
parentView.tree.lastChild.replaceChild(newChild.tree, oldChild.tree);
|
|
|
|
highlightView(null);
|
|
// Force redraw of the info box.
|
|
selectView(null);
|
|
selectView(parentView);
|
|
}
|
|
|
|
// Attach event listeners and build content once the document is ready.
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
var code = document.getElementById('code');
|
|
var tree = document.getElementById('tree');
|
|
var ancestors = document.getElementById('i_ancestors');
|
|
var alternatives = document.getElementById('i_alternatives');
|
|
|
|
[code, tree, ancestors].forEach(function(container) {
|
|
container.addEventListener('click', function(e) {
|
|
var nodeElt = e.target.closest('.selectable-node');
|
|
selectView(nodeElt && views[Number(nodeElt.dataset['index'])]);
|
|
});
|
|
container.addEventListener('mousemove', function(e) {
|
|
var nodeElt = e.target.closest('.selectable-node');
|
|
highlightView(nodeElt && views[Number(nodeElt.dataset['index'])]);
|
|
});
|
|
});
|
|
|
|
alternatives.addEventListener('click', function(e) {
|
|
var altElt = e.target.closest('.alternative');
|
|
if (altElt)
|
|
chooseAlternative(Number(altElt.dataset['parent']),
|
|
Number(altElt.dataset['index']));
|
|
});
|
|
|
|
// The HTML provides #code content in a hidden DOM element, move it.
|
|
var hiddenCode = document.getElementById('hidden-code');
|
|
splice(hiddenCode.firstChild, hiddenCode.lastChild, code);
|
|
detach(hiddenCode);
|
|
|
|
// Build the tree of NodeViews and attach to #tree.
|
|
tree.firstChild.appendChild(NodeView.make(0).tree);
|
|
});
|
|
|
|
// Helper DOM functions //
|
|
|
|
// Moves the sibling range [first, until) into newParent.
|
|
function splice(first, until, newParent, before) {
|
|
for (var next = first; next != until;) {
|
|
var elt = next;
|
|
next = next.nextSibling;
|
|
newParent.insertBefore(elt, before);
|
|
}
|
|
}
|
|
function detach(node) { node.parentNode.removeChild(node); }
|
|
// Like scrollIntoView, but vertical only!
|
|
function scrollIntoViewV(container, elt) {
|
|
if (container.scrollTop > elt.offsetTop + elt.offsetHeight ||
|
|
container.scrollTop + container.clientHeight < elt.offsetTop)
|
|
container.scrollTo({top : elt.offsetTop, behavior : 'smooth'});
|
|
}
|