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