// 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 . 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)
  • 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 '' + match + ''; return match; }); } else /*ambiguous*/ { ctx.innerHTML = '' + view.node.symbol + ''; } 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'}); }