1// The global map of forest node index => NodeView. 2views = []; 3// NodeView is a visible forest node. 4// It has an entry in the navigation tree, and a span in the code itself. 5// Each NodeView is associated with a forest node, but not all nodes have views: 6// - nodes not reachable though current ambiguity selection 7// - trivial "wrapping" sequence nodes are abbreviated away 8class NodeView { 9 // Builds a node representing forest[index], or its target if it is a wrapper. 10 // Registers the node in the global map. 11 static make(index, parent, abbrev) { 12 var node = forest[index]; 13 if (node.kind == 'sequence' && node.children.length == 1 && 14 forest[node.children[0]].kind != 'ambiguous') { 15 abbrev ||= []; 16 abbrev.push(index); 17 return NodeView.make(node.children[0], parent, abbrev); 18 } 19 return views[index] = new NodeView(index, parent, node, abbrev); 20 } 21 22 constructor(index, parent, node, abbrev) { 23 this.abbrev = abbrev || []; 24 this.parent = parent; 25 this.children = 26 (node.kind == 'ambiguous' ? [ node.selected ] : node.children || []) 27 .map((c) => NodeView.make(c, this)); 28 this.index = index; 29 this.node = node; 30 views[index] = this; 31 32 this.span = this.buildSpan(); 33 this.tree = this.buildTree(); 34 } 35 36 // Replaces the token sequence in #code with a <span class=node>. 37 buildSpan() { 38 var elt = document.createElement('span'); 39 elt.dataset['index'] = this.index; 40 elt.classList.add("node"); 41 elt.classList.add("selectable-node"); 42 elt.classList.add(this.node.kind); 43 44 var begin = null, end = null; 45 if (this.children.length != 0) { 46 begin = this.children[0].span; 47 end = this.children[this.children.length - 1].span.nextSibling; 48 } else if (this.node.kind == 'terminal') { 49 begin = document.getElementById(this.node.token); 50 end = begin.nextSibling; 51 } else if (this.node.kind == 'opaque') { 52 begin = document.getElementById(this.node.firstToken); 53 end = (this.node.lastToken == null) 54 ? begin 55 : document.getElementById(this.node.lastToken).nextSibling; 56 } 57 var parent = begin.parentNode; 58 splice(begin, end, elt); 59 parent.insertBefore(elt, end); 60 return elt; 61 } 62 63 // Returns a (detached) <li class=tree-node> suitable for use in #tree. 64 buildTree() { 65 var elt = document.createElement('li'); 66 elt.dataset['index'] = this.index; 67 elt.classList.add('tree-node'); 68 elt.classList.add('selectable-node'); 69 elt.classList.add(this.node.kind); 70 var header = document.createElement('header'); 71 elt.appendChild(header); 72 73 if (this.abbrev.length > 0) { 74 var abbrev = document.createElement('span'); 75 abbrev.classList.add('abbrev'); 76 abbrev.innerText = forest[this.abbrev[0]].symbol; 77 header.appendChild(abbrev); 78 } 79 var name = document.createElement('span'); 80 name.classList.add('name'); 81 name.innerText = this.node.symbol; 82 header.appendChild(name); 83 84 if (this.children.length != 0) { 85 var sublist = document.createElement('ul'); 86 this.children.forEach((c) => sublist.appendChild(c.tree)); 87 elt.appendChild(sublist); 88 } 89 return elt; 90 } 91 92 // Make this view visible on the screen by scrolling if needed. 93 scrollVisible() { 94 scrollIntoViewV(document.getElementById('tree'), this.tree.firstChild); 95 scrollIntoViewV(document.getElementById('code'), this.span); 96 } 97 98 // Fill #info with details of this node. 99 renderInfo() { 100 document.getElementById('info').classList = this.node.kind; 101 document.getElementById('i_symbol').innerText = this.node.symbol; 102 document.getElementById('i_kind').innerText = this.node.kind; 103 104 // For sequence nodes, add LHS := RHS rule. 105 // If this node abbreviates trivial sequences, we want those rules too. 106 var rules = document.getElementById('i_rules'); 107 rules.textContent = ''; 108 function addRule(i) { 109 var ruleText = forest[i].rule; 110 if (ruleText == null) 111 return; 112 var rule = document.createElement('div'); 113 rule.classList.add('rule'); 114 rule.innerText = ruleText; 115 rules.insertBefore(rule, rules.firstChild); 116 } 117 this.abbrev.forEach(addRule); 118 addRule(this.index); 119 120 // For ambiguous nodes, show a selectable list of alternatives. 121 var alternatives = document.getElementById('i_alternatives'); 122 alternatives.textContent = ''; 123 var that = this; 124 function addAlternative(i) { 125 var altNode = forest[i]; 126 var text = altNode.rule || altNode.kind; 127 var alt = document.createElement('div'); 128 alt.classList.add('alternative'); 129 alt.innerText = text; 130 alt.dataset['index'] = i; 131 alt.dataset['parent'] = that.index; 132 if (i == that.node.selected) 133 alt.classList.add('selected'); 134 alternatives.appendChild(alt); 135 } 136 if (this.node.kind == 'ambiguous') 137 this.node.children.forEach(addAlternative); 138 139 // Show the stack of ancestor nodes. 140 // The part of each rule that leads to the current node is bolded. 141 var ancestors = document.getElementById('i_ancestors'); 142 ancestors.textContent = ''; 143 var child = this; 144 for (var view = this.parent; view != null; 145 child = view, view = view.parent) { 146 var indexInParent = view.children.indexOf(child); 147 148 var ctx = document.createElement('div'); 149 ctx.classList.add('ancestors'); 150 ctx.classList.add('selectable-node'); 151 ctx.classList.add(view.node.kind); 152 if (view.node.rule) { 153 // Rule syntax is LHS := RHS1 [annotation] RHS2. 154 // We walk through the chunks and bold the one at parentInIndex. 155 var chunkCount = 0; 156 ctx.innerHTML = view.node.rule.replaceAll(/[^ ]+/g, function(match) { 157 if (!(match.startsWith('[') && match.endsWith(']')) /*annotations*/ 158 && chunkCount++ == indexInParent + 2 /*skip LHS :=*/) 159 return '<b>' + match + '</b>'; 160 return match; 161 }); 162 } else /*ambiguous*/ { 163 ctx.innerHTML = '<b>' + view.node.symbol + '</b>'; 164 } 165 ctx.dataset['index'] = view.index; 166 if (view.abbrev.length > 0) { 167 var abbrev = document.createElement('span'); 168 abbrev.classList.add('abbrev'); 169 abbrev.innerText = forest[view.abbrev[0]].symbol; 170 ctx.insertBefore(abbrev, ctx.firstChild); 171 } 172 173 ctx.dataset['index'] = view.index; 174 ancestors.appendChild(ctx, ancestors.firstChild); 175 } 176 } 177 178 remove() { 179 this.children.forEach((c) => c.remove()); 180 splice(this.span.firstChild, null, this.span.parentNode, 181 this.span.nextSibling); 182 detach(this.span); 183 delete views[this.index]; 184 } 185}; 186 187var selection = null; 188function selectView(view) { 189 var old = selection; 190 selection = view; 191 if (view == old) 192 return; 193 194 if (old) { 195 old.tree.classList.remove('selected'); 196 old.span.classList.remove('selected'); 197 } 198 document.getElementById('info').hidden = (view == null); 199 if (!view) 200 return; 201 view.tree.classList.add('selected'); 202 view.span.classList.add('selected'); 203 view.renderInfo(); 204 view.scrollVisible(); 205} 206 207// To highlight nodes on hover, we create dynamic CSS rules of the form 208// .selectable-node[data-index="42"] { background-color: blue; } 209// This avoids needing to find all the related nodes and update their classes. 210var highlightSheet = new CSSStyleSheet(); 211document.adoptedStyleSheets.push(highlightSheet); 212function highlightView(view) { 213 var text = ''; 214 for (const color of ['#6af', '#bbb', '#ddd', '#eee']) { 215 if (view == null) 216 break; 217 text += '.selectable-node[data-index="' + view.index + '"] ' 218 text += '{ background-color: ' + color + '; }\n'; 219 view = view.parent; 220 } 221 highlightSheet.replace(text); 222} 223 224// Select which branch of an ambiguous node is taken. 225function chooseAlternative(parent, index) { 226 var parentView = views[parent]; 227 parentView.node.selected = index; 228 var oldChild = parentView.children[0]; 229 oldChild.remove(); 230 var newChild = NodeView.make(index, parentView); 231 parentView.children[0] = newChild; 232 parentView.tree.lastChild.replaceChild(newChild.tree, oldChild.tree); 233 234 highlightView(null); 235 // Force redraw of the info box. 236 selectView(null); 237 selectView(parentView); 238} 239 240// Attach event listeners and build content once the document is ready. 241document.addEventListener("DOMContentLoaded", function() { 242 var code = document.getElementById('code'); 243 var tree = document.getElementById('tree'); 244 var ancestors = document.getElementById('i_ancestors'); 245 var alternatives = document.getElementById('i_alternatives'); 246 247 [code, tree, ancestors].forEach(function(container) { 248 container.addEventListener('click', function(e) { 249 var nodeElt = e.target.closest('.selectable-node'); 250 selectView(nodeElt && views[Number(nodeElt.dataset['index'])]); 251 }); 252 container.addEventListener('mousemove', function(e) { 253 var nodeElt = e.target.closest('.selectable-node'); 254 highlightView(nodeElt && views[Number(nodeElt.dataset['index'])]); 255 }); 256 }); 257 258 alternatives.addEventListener('click', function(e) { 259 var altElt = e.target.closest('.alternative'); 260 if (altElt) 261 chooseAlternative(Number(altElt.dataset['parent']), 262 Number(altElt.dataset['index'])); 263 }); 264 265 // The HTML provides #code content in a hidden DOM element, move it. 266 var hiddenCode = document.getElementById('hidden-code'); 267 splice(hiddenCode.firstChild, hiddenCode.lastChild, code); 268 detach(hiddenCode); 269 270 // Build the tree of NodeViews and attach to #tree. 271 tree.firstChild.appendChild(NodeView.make(0).tree); 272}); 273 274// Helper DOM functions // 275 276// Moves the sibling range [first, until) into newParent. 277function splice(first, until, newParent, before) { 278 for (var next = first; next != until;) { 279 var elt = next; 280 next = next.nextSibling; 281 newParent.insertBefore(elt, before); 282 } 283} 284function detach(node) { node.parentNode.removeChild(node); } 285// Like scrollIntoView, but vertical only! 286function scrollIntoViewV(container, elt) { 287 if (container.scrollTop > elt.offsetTop + elt.offsetHeight || 288 container.scrollTop + container.clientHeight < elt.offsetTop) 289 container.scrollTo({top : elt.offsetTop, behavior : 'smooth'}); 290} 291