1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
|
// 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'});
}
|