package main
import (
"bytes"
"go/ast"
"go/doc"
"go/format"
"go/token"
"html/template"
"log"
"net/http"
"sort"
"strings"
)
// IdentIndex is the precomputed identifier index across all targets.
type IdentIndex struct {
Entries []*IdentEntry
ByKey map[string]*IdentEntry
Total int // total number of targets
}
// IdentEntry is one identifier (type, func, method, const, or var) across all targets.
type IdentEntry struct {
Name string
Kind string // "type", "func", "method", "const", "var"
Count int
Key string // URL key: "kind/name"
Groups []IdentGroup
}
// IdentGroup is a set of targets with identical declaration for an identifier.
type IdentGroup struct {
Decl string // representative formatted declaration
Targets []string // sorted target names
}
func buildIdentIndex(results []Result) *IdentIndex {
type occ struct {
target string
decl string // grouping key + display
}
type ikey struct {
name, kind string
}
idx := make(map[ikey][]occ)
addFunc := func(target string, fset *token.FileSet, f *doc.Func, prefix string) {
name := f.Name
if prefix != "" {
name = prefix + "." + f.Name
}
kind := "func"
if f.Recv != "" {
kind = "method"
}
decl := fmtNode(fset, f.Decl)
idx[ikey{name, kind}] = append(idx[ikey{name, kind}], occ{target, decl})
}
addValues := func(target string, fset *token.FileSet, vals []*doc.Value, kind string) {
for _, v := range vals {
for _, name := range v.Names {
typeKey := resolveValueType(fset, v.Decl, name)
idx[ikey{name, kind}] = append(idx[ikey{name, kind}], occ{target, typeKey})
}
}
}
for _, r := range results {
t := r.Target.Name
fset := r.Fset
pkg := r.Pkg
for _, ty := range pkg.Types {
decl := fmtNode(fset, ty.Decl)
idx[ikey{ty.Name, "type"}] = append(idx[ikey{ty.Name, "type"}], occ{t, decl})
for _, m := range ty.Methods {
addFunc(t, fset, m, ty.Name)
}
for _, f := range ty.Funcs {
addFunc(t, fset, f, "")
}
addValues(t, fset, ty.Consts, "const")
addValues(t, fset, ty.Vars, "var")
}
for _, f := range pkg.Funcs {
addFunc(t, fset, f, "")
}
addValues(t, fset, pkg.Consts, "const")
addValues(t, fset, pkg.Vars, "var")
}
index := &IdentIndex{
ByKey: make(map[string]*IdentEntry),
Total: len(results),
}
for key, occs := range idx {
groups := make(map[string][]string) // decl -> targets
for _, o := range occs {
groups[o.decl] = append(groups[o.decl], o.target)
}
var gs []IdentGroup
for decl, targets := range groups {
sort.Strings(targets)
gs = append(gs, IdentGroup{Decl: decl, Targets: targets})
}
sort.Slice(gs, func(i, j int) bool { return len(gs[i].Targets) > len(gs[j].Targets) })
urlKey := key.kind + "/" + key.name
entry := &IdentEntry{
Name: key.name,
Kind: key.kind,
Count: len(occs),
Key: urlKey,
Groups: gs,
}
index.Entries = append(index.Entries, entry)
index.ByKey[urlKey] = entry
}
sort.Slice(index.Entries, func(i, j int) bool {
if index.Entries[i].Count != index.Entries[j].Count {
return index.Entries[i].Count > index.Entries[j].Count
}
return index.Entries[i].Name < index.Entries[j].Name
})
return index
}
// resolveValueType returns the effective type string for a const/var name
// within a GenDecl, handling iota type inheritance for constants.
func resolveValueType(fset *token.FileSet, gd *ast.GenDecl, name string) string {
isConst := gd.Tok == token.CONST
var lastType ast.Expr
for _, spec := range gd.Specs {
vs := spec.(*ast.ValueSpec)
if vs.Type != nil {
lastType = vs.Type
}
for _, n := range vs.Names {
if n.Name != name {
continue
}
effType := vs.Type
if effType == nil && isConst {
effType = lastType
}
if effType != nil {
return fmtNode(fset, effType)
}
return "(untyped)"
}
}
return "(unknown)"
}
// fmtNode formats an AST node to Go source. Separate from formatNode in main.go
// to make it clear this is the server's version (same logic).
func fmtNode(fset *token.FileSet, node ast.Node) string {
var buf bytes.Buffer
format.Node(&buf, fset, node)
return buf.String()
}
// --- HTTP Server ---
func serve(addr string, results []Result) {
index := buildIdentIndex(results)
byTarget := make(map[string]Result, len(results))
for _, r := range results {
byTarget[r.Target.Name] = r
}
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
data := pageData{Index: index, Results: results}
pageTmpl.Execute(w, data)
})
mux.HandleFunc("/id/", func(w http.ResponseWriter, r *http.Request) {
key := strings.TrimPrefix(r.URL.Path, "/id/")
entry, ok := index.ByKey[key]
if !ok {
http.NotFound(w, r)
return
}
data := pageData{Index: index, Results: results, Selected: entry, SelectedKey: key}
pageTmpl.Execute(w, data)
})
mux.HandleFunc("/target/", func(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, "/target/")
res, ok := byTarget[name]
if !ok {
http.NotFound(w, r)
return
}
api := convertPackage(res.Target.Name, res.Pkg, res.Fset)
targetTmpl.Execute(w, api)
})
log.Printf("serving documentation on http://localhost%s", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}
type pageData struct {
Index *IdentIndex
Results []Result
Selected *IdentEntry
SelectedKey string
}
// --- Templates ---
var pageTmpl = template.Must(template.New("page").Funcs(template.FuncMap{
"kindBadge": func(kind string) string {
switch kind {
case "type":
return "T"
case "func":
return "F"
case "method":
return "M"
case "const":
return "C"
case "var":
return "V"
}
return "?"
},
"pre": func(s string) template.HTML {
return template.HTML("
" + template.HTMLEscapeString(s) + "
")
},
"kindClass": func(kind string) string { return "kind-" + kind },
}).Parse(`
tgdoc{{if .Selected}} — {{.Selected.Name}}{{end}}
{{if .Selected}}
{{.Selected.Kind}} {{.Selected.Name}}
Present in {{.Selected.Count}} / {{.Index.Total}} targets
{{if eq (len .Selected.Groups) 1}}— identical across all targets{{else}}— {{len .Selected.Groups}} distinct signatures{{end}}
{{range .Selected.Groups}}
{{pre .Decl}}
{{range .Targets}}
{{.}} {{end}}
{{end}}
{{else}}
tgdoc
{{len .Index.Entries}} identifiers across {{.Index.Total}} targets
Select an identifier from the sidebar to see its appearances across targets.
Browse by target:
{{end}}
`))
var targetTmpl = template.Must(template.New("target").Funcs(template.FuncMap{
"pre": func(s string) template.HTML {
return template.HTML("" + template.HTMLEscapeString(s) + "
")
},
}).Parse(`
{{.Target}} — tgdoc
← identifier index
{{.Target}}
package {{.Package}}
{{if .Doc}}{{.Doc}}
{{end}}
{{if .Constants}}
Constants
{{range .Constants}}
{{if .Doc}}{{.Doc}}
{{end}}
{{pre .Decl}}
{{end}}
{{end}}
{{if .Variables}}
Variables
{{range .Variables}}
{{if .Doc}}{{.Doc}}
{{end}}
{{pre .Decl}}
{{end}}
{{end}}
{{if .Functions}}
Functions
{{range .Functions}}
func {{.Name}}
{{if .Doc}}{{.Doc}}
{{end}}
{{pre .Decl}}
{{end}}
{{end}}
{{if .Types}}
Types
{{range .Types}}
type {{.Name}}
{{if .Doc}}{{.Doc}}
{{end}}
{{pre .Decl}}
{{if .Constants}}Associated Constants
{{range .Constants}}{{pre .Decl}}{{end}}{{end}}
{{if .Variables}}Associated Variables
{{range .Variables}}{{pre .Decl}}{{end}}{{end}}
{{if .Functions}}Constructors
{{range .Functions}}
{{if .Doc}}
{{.Doc}}
{{end}}
{{pre .Decl}}
{{end}}
{{end}}
{{if .Methods}}Methods
{{range .Methods}}
{{if .Doc}}
{{.Doc}}
{{end}}
{{pre .Decl}}
{{end}}
{{end}}
{{end}}
{{end}}
`))