blob: 036b80e276b978be534fdb45b44f92db71ce7745 [file] [log] [blame]
// 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>`)
}