blob: 8a111c1dd471b2b464bb8718a7f1b93cb9fb5f98 [file] [log] [blame]
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ir
import (
"bufio"
"cmd/compile/internal/base"
"cmd/compile/internal/types"
"cmd/internal/src"
"crypto/sha256"
"encoding/hex"
"fmt"
"html"
"io"
"os"
"path/filepath"
"reflect"
"strings"
)
// An HTMLWriter dumps IR to multicolumn HTML, similar to what the
// ssa backend does for GOSSAFUNC. This is not the format used for
// the ast column in GOSSAFUNC output.
type HTMLWriter struct {
w *BufferedWriterCloser
Func *Func
canonIdMap map[Node]int
prevCanonId int
path string
prevHash []byte
pendingPhases []string
pendingTitles []string
}
// BufferedWriterCloser is here to help avoid pre-buffering the whole
// rendered HTML in memory, which can cause problems for large inputs.
type BufferedWriterCloser struct {
file io.Closer
w *bufio.Writer
}
func (b *BufferedWriterCloser) Write(p []byte) (n int, err error) {
return b.w.Write(p)
}
func (b *BufferedWriterCloser) Close() error {
b.w.Flush()
b.w = nil
return b.file.Close()
}
func NewBufferedWriterCloser(f io.WriteCloser) *BufferedWriterCloser {
return &BufferedWriterCloser{file: f, w: bufio.NewWriter(f)}
}
func NewHTMLWriter(path string, f *Func, cfgMask string) *HTMLWriter {
path = strings.ReplaceAll(path, "/", string(filepath.Separator))
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
base.Fatalf("%v", err)
}
reportPath := path
if !filepath.IsAbs(reportPath) {
pwd, err := os.Getwd()
if err != nil {
base.Fatalf("%v", err)
}
reportPath = filepath.Join(pwd, path)
}
h := HTMLWriter{
w: NewBufferedWriterCloser(out),
Func: f,
path: reportPath,
canonIdMap: make(map[Node]int),
}
h.start()
return &h
}
// canonId assigns indices to nodes based on pointer identity.
// this helps ensure that output html files don't gratuitously
// differ from run to run.
func (h *HTMLWriter) canonId(n Node) int {
if id := h.canonIdMap[n]; id > 0 {
return id
}
h.prevCanonId++
h.canonIdMap[n] = h.prevCanonId
return h.prevCanonId
}
// Fatalf reports an error and exits.
func (w *HTMLWriter) Fatalf(msg string, args ...any) {
base.FatalfAt(src.NoXPos, msg, args...)
}
const (
RIGHT_ARROW = "\u25BA" // click-to-open (is closed)
DOWN_ARROW = "\u25BC" // click-to-close (is open)
)
func (w *HTMLWriter) start() {
if w == nil {
return
}
escName := html.EscapeString(PkgFuncName(w.Func))
w.Print("<!DOCTYPE html>")
w.Print("<html>")
w.Printf(`<head>
<meta name="generator" content="AST display for %s">
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
%s
%s
<title>AST display for %s</title>
</head>`, escName, CSS, JS, escName)
w.Print("<body>")
w.Print("<h1>")
w.Print(html.EscapeString(w.Func.Sym().Name))
w.Print("</h1>")
w.Print(`
<a href="#" onclick="toggle_visibility('help');return false;" id="helplink">help</a>
<div id="help">
<p>
Click anywhere on a node (with "cell" cursor) to outline a node and all of its subtrees.
</p>
<p>
Click on a name (with "crosshair" cursor) to highlight every occurrence of a name.
(Note that all the name nodes are the same node, so those also all outline together).
</p>
<p>
Click on a file, line, or column (with "crosshair" cursor) to highlight positions
in that file, at that file:line, or at that file:line:column, respectively.<br>Inlined
locations are not treated as a single location, but as a sequence of locations that
can be independently highlighted.
</p>
<p>
Click on a ` + DOWN_ARROW + ` to collapse a subtree, or on a ` + RIGHT_ARROW + ` to expand a subtree.
</p>
</div>
<label for="dark-mode-button" style="margin-left: 15px; cursor: pointer;">darkmode</label>
<input type="checkbox" onclick="toggleDarkMode();" id="dark-mode-button" style="cursor: pointer" />
`)
w.Print("<table>")
w.Print("<tr>")
}
func (w *HTMLWriter) Close() {
if w == nil {
return
}
w.Print("</tr>")
w.Print("</table>")
w.Print("</body>")
w.Print("</html>\n")
w.w.Close()
fmt.Fprintf(os.Stderr, "Writing html ast output for %s to %s\n", PkgFuncName(w.Func), w.path)
}
// WritePhase writes f in a column headed by title.
// phase is used for collapsing columns and should be unique across the table.
func (w *HTMLWriter) WritePhase(phase, title string) {
if w == nil {
return // avoid generating HTML just to discard it
}
w.pendingPhases = append(w.pendingPhases, phase)
w.pendingTitles = append(w.pendingTitles, title)
w.flushPhases()
}
// flushPhases collects any pending phases and titles, writes them to the html, and resets the pending slices.
func (w *HTMLWriter) flushPhases() {
phaseLen := len(w.pendingPhases)
if phaseLen == 0 {
return
}
phases := strings.Join(w.pendingPhases, " + ")
w.WriteMultiTitleColumn(
phases,
w.pendingTitles,
"allow-x-scroll",
w.FuncHTML(w.pendingPhases[phaseLen-1]),
)
w.pendingPhases = w.pendingPhases[:0]
w.pendingTitles = w.pendingTitles[:0]
}
func (w *HTMLWriter) WriteMultiTitleColumn(phase string, titles []string, class string, writeContent func()) {
if w == nil {
return
}
id := strings.ReplaceAll(phase, " ", "-")
// collapsed column
w.Printf("<td id=\"%v-col\" class=\"collapsed\"><div>%v</div></td>", id, phase)
if class == "" {
w.Printf("<td id=\"%v-exp\">", id)
} else {
w.Printf("<td id=\"%v-exp\" class=\"%v\">", id, class)
}
for _, title := range titles {
w.Print("<h2>" + title + "</h2>")
}
writeContent()
w.Print("<div class=\"resizer\"></div>")
w.Print("</td>\n")
}
func (w *HTMLWriter) Printf(msg string, v ...any) {
if _, err := fmt.Fprintf(w.w, msg, v...); err != nil {
w.Fatalf("%v", err)
}
}
func (w *HTMLWriter) Print(s string) {
if _, err := fmt.Fprint(w.w, s); err != nil {
w.Fatalf("%v", err)
}
}
func (w *HTMLWriter) indent(n int) {
indent(w.w, n)
}
func (w *HTMLWriter) FuncHTML(phase string) func() {
return func() {
w.Print("<pre>") // use pre for formatting to preserve indentation
w.dumpNodesHTML(w.Func.Body, 1)
w.Print("</pre>")
}
}
func (h *HTMLWriter) dumpNodesHTML(list Nodes, depth int) {
if len(list) == 0 {
h.Print(" <nil>")
return
}
for _, n := range list {
h.dumpNodeHTML(n, depth)
}
}
// indent prints indentation to w.
func (h *HTMLWriter) indentForToggle(depth int, hasChildren bool) {
h.Print("\n")
if depth == 0 {
return
}
for i := 0; i < depth-1; i++ {
h.Print(". ")
}
if hasChildren {
h.Print(". ")
} else {
h.Print(". ")
}
}
func (h *HTMLWriter) dumpNodeHTML(n Node, depth int) {
hasChildren := nodeHasChildren(n)
h.indentForToggle(depth, hasChildren)
if depth > 40 {
h.Print("...")
return
}
if n == nil {
h.Print("NilIrNode")
return
}
// For HTML, we want to wrap the node and its details in a span that can be highlighted
// across all occurrences of the span in all columns, so it has to be linked to the node ID,
// which is its address. Canonicalize the address to a counter so that repeated compiler
// runs yield the same html.
//
// JS Equivalence logic:
// var c = elem.classList.item(0);
// var x = document.getElementsByClassName(c);
//
// Tag each class with its canonicalized index.
h.Printf("<span class=\"n%d ir-node\">", h.canonId(n))
defer h.Printf("</span>")
if hasChildren {
h.Print(`<span class="toggle" onclick="toggle_node(this)">` + DOWN_ARROW + `</span> `) // NOTE TRAILING SPACE after </span>!
}
if len(n.Init()) != 0 {
h.Print(`<span class="node-body">`)
h.Printf("%+v-init", n.Op())
h.dumpNodesHTML(n.Init(), depth+1)
h.indent(depth)
h.Print(`</span>`)
}
switch n.Op() {
default:
h.Printf("%+v", n.Op())
h.dumpNodeHeaderHTML(n)
case OLITERAL:
h.Printf("%+v-%v", n.Op(), html.EscapeString(fmt.Sprintf("%v", n.Val())))
h.dumpNodeHeaderHTML(n)
return
case ONAME, ONONAME:
if n.Sym() != nil {
// Name highlighting:
// Create a hash for the symbol name to use as a class
// We use the same irValueClicked logic which uses the first class as the identifier
name := fmt.Sprintf("%v", n.Sym())
hash := sha256.Sum256([]byte(name))
symID := "sym-" + hex.EncodeToString(hash[:6])
h.Printf("%+v-<span class=\"%s variable-name\">%+v</span>", n.Op(), symID, html.EscapeString(name))
} else {
h.Printf("%+v", n.Op())
}
h.dumpNodeHeaderHTML(n)
return
case OLINKSYMOFFSET:
n := n.(*LinksymOffsetExpr)
h.Printf("%+v-%v", n.Op(), html.EscapeString(fmt.Sprintf("%v", n.Linksym)))
if n.Offset_ != 0 {
h.Printf("%+v", n.Offset_)
}
h.dumpNodeHeaderHTML(n)
case OASOP:
n := n.(*AssignOpStmt)
h.Printf("%+v-%+v", n.Op(), n.AsOp)
h.dumpNodeHeaderHTML(n)
case OTYPE:
h.Printf("%+v %+v", n.Op(), html.EscapeString(fmt.Sprintf("%v", n.Sym())))
h.dumpNodeHeaderHTML(n)
return
case OCLOSURE:
h.Printf("%+v", n.Op())
h.dumpNodeHeaderHTML(n)
case ODCLFUNC:
n := n.(*Func)
h.Printf("%+v", n.Op())
h.dumpNodeHeaderHTML(n)
if hasChildren {
h.Print(`<span class="node-body">`)
defer h.Print(`</span>`)
}
fn := n
if len(fn.Dcl) > 0 {
h.indent(depth)
h.Printf("%+v-Dcl", n.Op())
for _, dcl := range n.Dcl {
h.dumpNodeHTML(dcl, depth+1)
}
}
if len(fn.ClosureVars) > 0 {
h.indent(depth)
h.Printf("%+v-ClosureVars", n.Op())
for _, cv := range fn.ClosureVars {
h.dumpNodeHTML(cv, depth+1)
}
}
if len(fn.Body) > 0 {
h.indent(depth)
h.Printf("%+v-body", n.Op())
h.dumpNodesHTML(fn.Body, depth+1)
}
return
}
if hasChildren {
h.Print(`<span class="node-body">`)
defer h.Print(`</span>`)
}
v := reflect.ValueOf(n).Elem()
t := reflect.TypeOf(n).Elem()
nf := t.NumField()
for i := 0; i < nf; i++ {
tf := t.Field(i)
vf := v.Field(i)
if tf.PkgPath != "" {
continue
}
switch tf.Type.Kind() {
case reflect.Interface, reflect.Ptr, reflect.Slice:
if vf.IsNil() {
continue
}
}
name := strings.TrimSuffix(tf.Name, "_")
switch name {
case "X", "Y", "Index", "Chan", "Value", "Call":
name = ""
}
switch val := vf.Interface().(type) {
case Node:
if name != "" {
h.indent(depth)
h.Printf("%+v-%s", n.Op(), name)
}
h.dumpNodeHTML(val, depth+1)
case Nodes:
if len(val) == 0 {
continue
}
if name != "" {
h.indent(depth)
h.Printf("%+v-%s", n.Op(), name)
}
h.dumpNodesHTML(val, depth+1)
default:
if vf.Kind() == reflect.Slice && vf.Type().Elem().Implements(nodeType) {
if vf.Len() == 0 {
continue
}
if name != "" {
h.indent(depth)
h.Printf("%+v-%s", n.Op(), name)
}
for i, n := 0, vf.Len(); i < n; i++ {
h.dumpNodeHTML(vf.Index(i).Interface().(Node), depth+1)
}
}
}
}
}
func nodeHasChildren(n Node) bool {
if n == nil {
return false
}
if len(n.Init()) != 0 {
return true
}
switch n.Op() {
case OLITERAL, ONAME, ONONAME, OTYPE:
return false
case ODCLFUNC:
n := n.(*Func)
return len(n.Dcl) > 0 || len(n.ClosureVars) > 0 || len(n.Body) > 0
}
v := reflect.ValueOf(n).Elem()
t := reflect.TypeOf(n).Elem()
nf := t.NumField()
for i := 0; i < nf; i++ {
tf := t.Field(i)
vf := v.Field(i)
if tf.PkgPath != "" {
continue
}
switch tf.Type.Kind() {
case reflect.Interface, reflect.Ptr, reflect.Slice:
if vf.IsNil() {
continue
}
}
switch val := vf.Interface().(type) {
case Node:
return true
case Nodes:
if len(val) > 0 {
return true
}
default:
if vf.Kind() == reflect.Slice && vf.Type().Elem().Implements(nodeType) {
if vf.Len() > 0 {
return true
}
}
}
}
return false
}
func (h *HTMLWriter) dumpNodeHeaderHTML(n Node) {
// print pointer to be able to see identical nodes
if base.Debug.DumpPtrs != 0 {
h.Printf(" p(%p)", n)
}
if base.Debug.DumpPtrs != 0 && n.Name() != nil && n.Name().Defn != nil {
h.Printf(" defn(%p)", n.Name().Defn)
}
if base.Debug.DumpPtrs != 0 && n.Name() != nil && n.Name().Curfn != nil {
h.Printf(" curfn(%p)", n.Name().Curfn)
}
if base.Debug.DumpPtrs != 0 && n.Name() != nil && n.Name().Outer != nil {
h.Printf(" outer(%p)", n.Name().Outer)
}
if EscFmt != nil {
if esc := EscFmt(n); esc != "" {
h.Printf(" %s", html.EscapeString(esc))
}
}
if n.Sym() != nil && n.Op() != ONAME && n.Op() != ONONAME && n.Op() != OTYPE {
h.Printf(" %+v", html.EscapeString(fmt.Sprintf("%v", n.Sym())))
}
v := reflect.ValueOf(n).Elem()
t := v.Type()
nf := t.NumField()
for i := 0; i < nf; i++ {
tf := t.Field(i)
if tf.PkgPath != "" {
continue
}
k := tf.Type.Kind()
if reflect.Bool <= k && k <= reflect.Complex128 {
name := strings.TrimSuffix(tf.Name, "_")
vf := v.Field(i)
vfi := vf.Interface()
if name == "Offset" && vfi == types.BADWIDTH || name != "Offset" && vf.IsZero() {
continue
}
if vfi == true {
h.Printf(" %s", name)
} else {
h.Printf(" %s:%+v", name, html.EscapeString(fmt.Sprintf("%v", vf.Interface())))
}
}
}
v = reflect.ValueOf(n)
t = v.Type()
nm := t.NumMethod()
for i := 0; i < nm; i++ {
tm := t.Method(i)
if tm.PkgPath != "" {
continue
}
m := v.Method(i)
mt := m.Type()
if mt.NumIn() == 0 && mt.NumOut() == 1 && mt.Out(0).Kind() == reflect.Bool {
func() {
defer func() { recover() }()
if m.Call(nil)[0].Bool() {
name := strings.TrimSuffix(tm.Name, "_")
h.Printf(" %s", name)
}
}()
}
}
if n.Op() == OCLOSURE {
n := n.(*ClosureExpr)
if fn := n.Func; fn != nil && fn.Nname.Sym() != nil {
h.Printf(" fnName(%+v)", html.EscapeString(fmt.Sprintf("%v", fn.Nname.Sym())))
}
}
if n.Type() != nil {
if n.Op() == OTYPE {
h.Printf(" type")
}
h.Printf(" %+v", html.EscapeString(fmt.Sprintf("%v", n.Type())))
}
if n.Typecheck() != 0 {
h.Printf(" tc(%d)", n.Typecheck())
}
if n.Pos().IsKnown() {
h.Print(" <span class=\"line-number\">")
switch n.Pos().IsStmt() {
case src.PosNotStmt:
h.Print("_")
case src.PosIsStmt:
h.Print("+")
}
sep := ""
base.Ctxt.AllPos(n.Pos(), func(pos src.Pos) {
h.Print(sep)
sep = " "
// Hierarchical highlighting:
// Click file -> highlight all ranges in this file
// Click line -> highlight all ranges at this line (in this file)
// Click col -> highlight this specific range
file := pos.Filename()
// Create a hash for the filename to use as a class
hash := sha256.Sum256([]byte(file))
fileID := "loc-" + hex.EncodeToString(hash[:6])
lineID := fmt.Sprintf("%s-L%d", fileID, pos.Line())
colID := fmt.Sprintf("%s-C%d", lineID, pos.Col())
// File part: triggers fileID
h.Printf("<span class=\"%s line-number\">%s</span>:", fileID, html.EscapeString(filepath.Base(file)))
// Line part: triggers lineID (and fileID via class list)
h.Printf("<span class=\"%s %s line-number\">%d</span>:", lineID, fileID, pos.Line())
// Col part: triggers colID (and lineID, fileID)
h.Printf("<span class=\"%s %s %s line-number\">%d</span>", colID, lineID, fileID, pos.Col())
})
h.Print("</span>")
}
}
const (
CSS = `<style>
body {
font-size: 14px;
font-family: Arial, sans-serif;
}
h1 {
font-size: 18px;
display: inline-block;
margin: 0 1em .5em 0;
}
#helplink {
display: inline-block;
}
#help {
display: none;
}
table {
border: 1px solid black;
table-layout: fixed;
width: 300px;
}
th, td {
border: 1px solid black;
overflow: hidden;
width: 400px;
vertical-align: top;
padding: 5px;
position: relative;
}
.resizer {
display: inline-block;
background: transparent;
width: 10px;
height: 100%;
position: absolute;
right: 0;
top: 0;
cursor: col-resize;
z-index: 100;
}
td > h2 {
cursor: pointer;
font-size: 120%;
margin: 5px 0px 5px 0px;
}
td.collapsed {
font-size: 12px;
width: 12px;
border: 1px solid white;
padding: 2px;
cursor: pointer;
background: #fafafa;
}
td.collapsed div {
text-align: right;
transform: rotate(180deg);
writing-mode: vertical-lr;
white-space: pre;
}
pre {
font-family: Menlo, monospace;
font-size: 12px;
}
pre {
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
}
.allow-x-scroll {
overflow-x: scroll;
}
.ir-node {
cursor: cell;
}
.variable-name {
cursor: crosshair;
}
.line-number {
font-size: 11px;
cursor: crosshair;
}
body.darkmode {
background-color: rgb(21, 21, 21);
color: rgb(230, 255, 255);
opacity: 100%;
}
td.darkmode {
background-color: rgb(21, 21, 21);
border: 1px solid gray;
}
body.darkmode table, th {
border: 1px solid gray;
}
body.darkmode text {
fill: white;
}
.highlight-aquamarine { background-color: aquamarine; color: black; }
.highlight-coral { background-color: coral; color: black; }
.highlight-lightpink { background-color: lightpink; color: black; }
.highlight-lightsteelblue { background-color: lightsteelblue; color: black; }
.highlight-palegreen { background-color: palegreen; color: black; }
.highlight-skyblue { background-color: skyblue; color: black; }
.highlight-lightgray { background-color: lightgray; color: black; }
.highlight-yellow { background-color: yellow; color: black; }
.highlight-lime { background-color: lime; color: black; }
.highlight-khaki { background-color: khaki; color: black; }
.highlight-aqua { background-color: aqua; color: black; }
.highlight-salmon { background-color: salmon; color: black; }
.outline-blue { outline: #2893ff solid 2px; }
.outline-red { outline: red solid 2px; }
.outline-blueviolet { outline: blueviolet solid 2px; }
.outline-darkolivegreen { outline: darkolivegreen solid 2px; }
.outline-fuchsia { outline: fuchsia solid 2px; }
.outline-sienna { outline: sienna solid 2px; }
.outline-gold { outline: gold solid 2px; }
.outline-orangered { outline: orangered solid 2px; }
.outline-teal { outline: teal solid 2px; }
.outline-maroon { outline: maroon solid 2px; }
.outline-black { outline: black solid 2px; }
/* Capture alternative for outline-black and ellipse.outline-black when in dark mode */
body.darkmode .outline-black { outline: gray solid 2px; }
.toggle {
cursor: pointer;
display: inline-block;
text-align: center;
user-select: none;
font-size: 12px; // hand-tweaked
}
</style>
`
JS = `<script type="text/javascript">
// Contains phase names which are expanded by default. Other columns are collapsed.
let expandedDefault = [
"bloop",
"loopvar",
"escape",
"slice",
"walk",
];
if (history.state === null) {
history.pushState({expandedDefault}, "", location.href);
}
// ordered list of all available highlight colors
var highlights = [
"highlight-aquamarine",
"highlight-coral",
"highlight-lightpink",
"highlight-lightsteelblue",
"highlight-palegreen",
"highlight-skyblue",
"highlight-lightgray",
"highlight-yellow",
"highlight-lime",
"highlight-khaki",
"highlight-aqua",
"highlight-salmon"
];
// state: which value is highlighted this color?
var highlighted = {};
for (var i = 0; i < highlights.length; i++) {
highlighted[highlights[i]] = "";
}
// ordered list of all available outline colors
var outlines = [
"outline-blue",
"outline-red",
"outline-blueviolet",
"outline-darkolivegreen",
"outline-fuchsia",
"outline-sienna",
"outline-gold",
"outline-orangered",
"outline-teal",
"outline-maroon",
"outline-black"
];
// state: which value is outlined this color?
var outlined = {};
for (var i = 0; i < outlines.length; i++) {
outlined[outlines[i]] = "";
}
window.onload = function() {
if (history.state !== null) {
expandedDefault = history.state.expandedDefault;
}
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
toggleDarkMode();
document.getElementById("dark-mode-button").checked = true;
}
var irElemClicked = function(elem, event, selections, selected) {
event.stopPropagation();
// find all values with the same name
var c = elem.classList.item(0);
var x = document.getElementsByClassName(c);
// if selected, remove selections from all of them
// otherwise, attempt to add
var remove = "";
for (var i = 0; i < selections.length; i++) {
var color = selections[i];
if (selected[color] == c) {
remove = color;
break;
}
}
if (remove != "") {
for (var i = 0; i < x.length; i++) {
x[i].classList.remove(remove);
}
selected[remove] = "";
return;
}
// we're adding a selection
// find first available color
var avail = "";
for (var i = 0; i < selections.length; i++) {
var color = selections[i];
if (selected[color] == "") {
avail = color;
break;
}
}
if (avail == "") {
alert("out of selection colors; go add more");
return;
}
// set that as the selection
for (var i = 0; i < x.length; i++) {
x[i].classList.add(avail);
}
selected[avail] = c;
};
var irValueClicked = function(event) {
irElemClicked(this, event, highlights, highlighted);
};
var irTreeClicked = function(event) {
irElemClicked(this, event, outlines, outlined);
};
var irValues = document.getElementsByClassName("ir-node");
for (var i = 0; i < irValues.length; i++) {
irValues[i].addEventListener('click', irTreeClicked);
}
var lines = document.getElementsByClassName("line-number");
for (var i = 0; i < lines.length; i++) {
lines[i].addEventListener('click', irValueClicked);
}
var variableNames = document.getElementsByClassName("variable-name");
for (var i = 0; i < variableNames.length; i++) {
variableNames[i].addEventListener('click', irValueClicked);
}
function toggler(phase) {
return function() {
toggle_cell(phase+'-col');
toggle_cell(phase+'-exp');
const i = expandedDefault.indexOf(phase);
if (i !== -1) {
expandedDefault.splice(i, 1);
} else {
expandedDefault.push(phase);
}
history.pushState({expandedDefault}, "", location.href);
};
}
function toggle_cell(id) {
var e = document.getElementById(id);
if (e.style.display == 'table-cell') {
e.style.display = 'none';
} else {
e.style.display = 'table-cell';
}
}
// Go through all columns and collapse needed phases.
const td = document.getElementsByTagName("td");
for (let i = 0; i < td.length; i++) {
const id = td[i].id;
const phase = id.substr(0, id.length-4);
let show = expandedDefault.indexOf(phase) !== -1
// If show == false, check to see if this is a combined column (multiple phases).
// If combined, check each of the phases to see if they are in our expandedDefaults.
// If any are found, that entire combined column gets shown.
if (!show) {
const combined = phase.split('--+--');
const len = combined.length;
if (len > 1) {
for (let i = 0; i < len; i++) {
const num = expandedDefault.indexOf(combined[i]);
if (num !== -1) {
expandedDefault.splice(num, 1);
if (expandedDefault.indexOf(phase) === -1) {
expandedDefault.push(phase);
show = true;
}
}
}
}
}
if (id.endsWith("-exp")) {
const h2Els = td[i].getElementsByTagName("h2");
const len = h2Els.length;
if (len > 0) {
for (let i = 0; i < len; i++) {
h2Els[i].addEventListener('click', toggler(phase));
}
}
} else {
td[i].addEventListener('click', toggler(phase));
}
if (id.endsWith("-col") && show || id.endsWith("-exp") && !show) {
td[i].style.display = 'none';
continue;
}
td[i].style.display = 'table-cell';
}
var resizers = document.getElementsByClassName("resizer");
for (var i = 0; i < resizers.length; i++) {
var resizer = resizers[i];
resizer.addEventListener('mousedown', initDrag, false);
}
};
var startX, startWidth, resizableCol;
function initDrag(e) {
resizableCol = this.parentElement;
startX = e.clientX;
startWidth = parseInt(document.defaultView.getComputedStyle(resizableCol).width, 10);
document.documentElement.addEventListener('mousemove', doDrag, false);
document.documentElement.addEventListener('mouseup', stopDrag, false);
}
function doDrag(e) {
resizableCol.style.width = (startWidth + e.clientX - startX) + 'px';
}
function stopDrag(e) {
document.documentElement.removeEventListener('mousemove', doDrag, false);
document.documentElement.removeEventListener('mouseup', stopDrag, false);
}
function toggle_visibility(id) {
var e = document.getElementById(id);
if (e.style.display == 'block') {
e.style.display = 'none';
} else {
e.style.display = 'block';
}
}
function toggleDarkMode() {
document.body.classList.toggle('darkmode');
// Collect all of the "collapsed" elements and apply dark mode on each collapsed column
const collapsedEls = document.getElementsByClassName('collapsed');
const len = collapsedEls.length;
for (let i = 0; i < len; i++) {
collapsedEls[i].classList.toggle('darkmode');
}
}
function toggle_node(e) {
event.stopPropagation();
var parent = e.parentNode;
var children = parent.children;
for (var i = 0; i < children.length; i++) {
if (children[i].classList.contains("node-body")) {
if (children[i].style.display == "none") {
children[i].style.display = "";
} else {
children[i].style.display = "none";
}
}
}
if (e.innerText == "` + RIGHT_ARROW + `") {
e.innerText = "` + DOWN_ARROW + `";
} else {
e.innerText = "` + RIGHT_ARROW + `";
}
}
</script>
`
)