Bump version to 19.1.0 (final)
[llvm-project.git] / clang-tools-extra / pseudo / tool / HTMLForest.js
blob24b88a5c10b471a3d59464a686b39977e3969315
1 // The global map of forest node index => NodeView.
2 views = [];
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
8 class 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   }
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;
32     this.span = this.buildSpan();
33     this.tree = this.buildTree();
34   }
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);
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   }
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);
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);
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   }
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   }
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;
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);
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);
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);
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       }
173       ctx.dataset['index'] = view.index;
174       ancestors.appendChild(ctx, ancestors.firstChild);
175     }
176   }
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   }
187 var selection = null;
188 function selectView(view) {
189   var old = selection;
190   selection = view;
191   if (view == old)
192     return;
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();
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.
210 var highlightSheet = new CSSStyleSheet();
211 document.adoptedStyleSheets.push(highlightSheet);
212 function 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);
224 // Select which branch of an ambiguous node is taken.
225 function 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);
234   highlightView(null);
235   // Force redraw of the info box.
236   selectView(null);
237   selectView(parentView);
240 // Attach event listeners and build content once the document is ready.
241 document.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');
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   });
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   });
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);
270   // Build the tree of NodeViews and attach to #tree.
271   tree.firstChild.appendChild(NodeView.make(0).tree);
274 // Helper DOM functions //
276 // Moves the sibling range [first, until) into newParent.
277 function 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   }
284 function detach(node) { node.parentNode.removeChild(node); }
285 // Like scrollIntoView, but vertical only!
286 function 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'});