// 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, "", htmlCSS) for _, root := range t.trees { dot := newDotEncoder() html := htmlTracer{w: w, dot: dot} html.writeTree(root) } fmt.Fprintf(w, "\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 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, `
...
`) break } fmt.Fprintf(t.w, `
%s`, html.EscapeString(child.label)) t.writeTree(child) fmt.Fprintf(t.w, "
\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, `
`) for i, v := range vs { fmt.Fprintf(t.w, `
%s
`, i+1, html.EscapeString(labels[i])) fmt.Fprintf(t.w, `
%s
`, i+1, htmlSVG(t, t.dot.valueSubgraph, v)) } col := len(vs) fmt.Fprintf(t.w, `
in
`, col+1) fmt.Fprintf(t.w, `
%s
`, col+1, htmlSVG(t, t.dot.envSubgraph, env)) fmt.Fprintf(t.w, `
`) }