File: HTMLForest.js

package info (click to toggle)
llvm-toolchain-15 1%3A15.0.6-4
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 1,554,644 kB
  • sloc: cpp: 5,922,452; ansic: 1,012,136; asm: 674,362; python: 191,568; objc: 73,855; f90: 42,327; lisp: 31,913; pascal: 11,973; javascript: 10,144; sh: 9,421; perl: 7,447; ml: 5,527; awk: 3,523; makefile: 2,520; xml: 885; cs: 573; fortran: 567
file content (290 lines) | stat: -rw-r--r-- 10,243 bytes parent folder | download | duplicates (9)
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'});
}