| // Copyright 2025 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 unify |
| |
| import ( |
| "fmt" |
| "html" |
| "io" |
| "strings" |
| ) |
| |
| func (t *tracer) writeHTML(w io.Writer) { |
| if !t.saveTree { |
| panic("writeHTML called without tracer.saveTree") |
| } |
| |
| fmt.Fprintf(w, "<html><head><style>%s</style></head>", htmlCSS) |
| for _, root := range t.trees { |
| dot := newDotEncoder() |
| html := htmlTracer{w: w, dot: dot} |
| html.writeTree(root) |
| } |
| fmt.Fprintf(w, "</html>\n") |
| } |
| |
| const htmlCSS = ` |
| .unify { |
| display: grid; |
| grid-auto-columns: min-content; |
| text-align: center; |
| } |
| |
| .header { |
| grid-row: 1; |
| font-weight: bold; |
| padding: 0.25em; |
| position: sticky; |
| top: 0; |
| background: white; |
| } |
| |
| .envFactor { |
| display: grid; |
| grid-auto-rows: min-content; |
| grid-template-columns: subgrid; |
| text-align: center; |
| } |
| ` |
| |
| type htmlTracer struct { |
| w io.Writer |
| dot *dotEncoder |
| svgs map[any]string |
| } |
| |
| func (t *htmlTracer) writeTree(node *traceTree) { |
| // TODO: This could be really nice. |
| // |
| // - Put nodes that were unified on the same rank with {rank=same; a; b} |
| // |
| // - On hover, highlight nodes that node was unified with and the result. If |
| // it's a variable, highlight it in the environment, too. |
| // |
| // - On click, show the details of unifying that node. |
| // |
| // This could be the only way to navigate, without necessarily needing the |
| // whole nest of <detail> nodes. |
| |
| // TODO: It might be possible to write this out on the fly. |
| |
| t.emit([]*Value{node.v, node.w}, []string{"v", "w"}, node.envIn) |
| |
| // Render children. |
| for i, child := range node.children { |
| if i >= 10 { |
| fmt.Fprintf(t.w, `<div style="margin-left: 4em">...</div>`) |
| break |
| } |
| fmt.Fprintf(t.w, `<details style="margin-left: 4em"><summary>%s</summary>`, html.EscapeString(child.label)) |
| t.writeTree(child) |
| fmt.Fprintf(t.w, "</details>\n") |
| } |
| |
| // Render result. |
| if node.err != nil { |
| fmt.Fprintf(t.w, "Error: %s\n", html.EscapeString(node.err.Error())) |
| } else { |
| t.emit([]*Value{node.res}, []string{"res"}, node.env) |
| } |
| } |
| |
| func htmlSVG[Key comparable](t *htmlTracer, f func(Key), arg Key) string { |
| if s, ok := t.svgs[arg]; ok { |
| return s |
| } |
| var buf strings.Builder |
| f(arg) |
| t.dot.writeSvg(&buf) |
| t.dot.clear() |
| svg := buf.String() |
| if t.svgs == nil { |
| t.svgs = make(map[any]string) |
| } |
| t.svgs[arg] = svg |
| buf.Reset() |
| return svg |
| } |
| |
| func (t *htmlTracer) emit(vs []*Value, labels []string, env envSet) { |
| fmt.Fprintf(t.w, `<div class="unify">`) |
| for i, v := range vs { |
| fmt.Fprintf(t.w, `<div class="header" style="grid-column: %d">%s</div>`, i+1, html.EscapeString(labels[i])) |
| fmt.Fprintf(t.w, `<div style="grid-area: 2 / %d">%s</div>`, i+1, htmlSVG(t, t.dot.valueSubgraph, v)) |
| } |
| col := len(vs) |
| |
| fmt.Fprintf(t.w, `<div class="header" style="grid-column: %d">in</div>`, col+1) |
| fmt.Fprintf(t.w, `<div style="grid-area: 2 / %d">%s</div>`, col+1, htmlSVG(t, t.dot.envSubgraph, env)) |
| |
| fmt.Fprintf(t.w, `</div>`) |
| } |