| // Copyright 2024 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 golang |
| |
| // This file defines a simple HTML rendering of package documentation |
| // in imitation of the style of pkg.go.dev. |
| // |
| // The current implementation is just a starting point and a |
| // placeholder for a more sophisticated one. |
| // |
| // TODO(adonovan): |
| // - rewrite using html/template. |
| // Or factor with golang.org/x/pkgsite/internal/godoc/dochtml. |
| // - emit breadcrumbs for parent + sibling packages. |
| // - list promoted methods---we have type information! |
| // - gather Example tests, following go/doc and pkgsite. |
| // - add option for doc.AllDecls: show non-exported symbols too. |
| // - abbreviate long signatures by replacing parameters 4 onwards with "...". |
| // - style the <li> bullets in the index as invisible. |
| // - add push notifications such as didChange -> reload. |
| // - there appears to be a maximum file size beyond which the |
| // "source.doc" code action is not offered. Remove that. |
| // - modify JS httpGET function to give a transient visual indication |
| // when clicking a source link that the editor is being navigated |
| // (in case it doesn't raise itself, like VS Code). |
| // - move this into a new package, golang/pkgdoc, and then |
| // split out the various helpers without fear of polluting |
| // the golang package namespace. |
| // - show "Deprecated" chip when appropriate. |
| |
| import ( |
| "bytes" |
| "fmt" |
| "go/ast" |
| "go/doc" |
| "go/doc/comment" |
| "go/format" |
| "go/token" |
| "go/types" |
| "html" |
| "path/filepath" |
| "strings" |
| |
| "golang.org/x/tools/gopls/internal/cache" |
| "golang.org/x/tools/gopls/internal/protocol" |
| "golang.org/x/tools/gopls/internal/util/astutil" |
| "golang.org/x/tools/gopls/internal/util/bug" |
| "golang.org/x/tools/gopls/internal/util/safetoken" |
| "golang.org/x/tools/gopls/internal/util/slices" |
| "golang.org/x/tools/gopls/internal/util/typesutil" |
| "golang.org/x/tools/internal/typesinternal" |
| ) |
| |
| // TODO(adonovan): factor these two functions into an interface. |
| type ( |
| // A PkgURLFunc forms URLs of package or symbol documentation. |
| PkgURLFunc = func(path PackagePath, fragment string) protocol.URI |
| |
| // A PosURLFunc forms URLs that cause the editor to navigate to a position. |
| PosURLFunc = func(filename string, line, col8 int) protocol.URI |
| ) |
| |
| // RenderPackageDoc formats the package documentation page. |
| // |
| // The posURL function returns a URL that when visited, has the side |
| // effect of causing gopls to direct the client editor to navigate to |
| // the specified file/line/column position, in UTF-8 coordinates. |
| // |
| // The pkgURL function returns a URL for the documentation of the |
| // specified package and symbol. |
| // |
| // TODO(adonovan): "Render" is a client-side verb; rename to PackageDocHTML. |
| func RenderPackageDoc(pkg *cache.Package, posURL PosURLFunc, pkgURL PkgURLFunc) ([]byte, error) { |
| // We can't use doc.NewFromFiles (even with doc.PreserveAST |
| // mode) as it calls ast.NewPackage which assumes that each |
| // ast.File has an ast.Scope and resolves identifiers to |
| // (deprecated) ast.Objects. (This is golang/go#66290.) |
| // But doc.New only requires pkg.{Name,Files}, |
| // so we just boil it down. |
| // |
| // The only loss is doc.classifyExamples. |
| // TODO(adonovan): simulate that too. |
| fileMap := make(map[string]*ast.File) |
| for _, f := range pkg.Syntax() { |
| fileMap[pkg.FileSet().File(f.Pos()).Name()] = f |
| } |
| astpkg := &ast.Package{ |
| Name: pkg.Types().Name(), |
| Files: fileMap, |
| } |
| // PreserveAST mode only half works (golang/go#66449): it still |
| // mutates ASTs when filtering out non-exported symbols. |
| // As a workaround, enable AllDecls to suppress filtering, |
| // and do it ourselves. |
| mode := doc.PreserveAST | doc.AllDecls |
| docpkg := doc.New(astpkg, pkg.Types().Path(), mode) |
| |
| // Discard non-exported symbols. |
| // TODO(adonovan): do this conditionally, and expose option in UI. |
| const showUnexported = false |
| if !showUnexported { |
| var ( |
| unexported = func(name string) bool { return !token.IsExported(name) } |
| filterValues = func(slice *[]*doc.Value) { |
| delValue := func(v *doc.Value) bool { |
| v.Names = slices.DeleteFunc(v.Names, unexported) |
| return len(v.Names) == 0 |
| } |
| *slice = slices.DeleteFunc(*slice, delValue) |
| } |
| filterFuncs = func(funcs *[]*doc.Func) { |
| *funcs = slices.DeleteFunc(*funcs, func(v *doc.Func) bool { |
| return unexported(v.Name) |
| }) |
| } |
| ) |
| filterValues(&docpkg.Consts) |
| filterValues(&docpkg.Vars) |
| filterFuncs(&docpkg.Funcs) |
| docpkg.Types = slices.DeleteFunc(docpkg.Types, func(t *doc.Type) bool { |
| filterValues(&t.Consts) |
| filterValues(&t.Vars) |
| filterFuncs(&t.Funcs) |
| filterFuncs(&t.Methods) |
| return unexported(t.Name) |
| }) |
| } |
| |
| var docHTML func(comment string) []byte |
| { |
| // Adapt doc comment parser and printer |
| // to our representation of Go packages |
| // so that doc links (e.g. "[fmt.Println]") |
| // become valid links. |
| |
| printer := docpkg.Printer() |
| printer.DocLinkURL = func(link *comment.DocLink) string { |
| path := pkg.Metadata().PkgPath |
| if link.ImportPath != "" { |
| path = PackagePath(link.ImportPath) |
| } |
| fragment := link.Name |
| if link.Recv != "" { |
| fragment = link.Recv + "." + link.Name |
| } |
| return pkgURL(path, fragment) |
| } |
| parser := docpkg.Parser() |
| parser.LookupPackage = func(name string) (importPath string, ok bool) { |
| // Ambiguous: different files in the same |
| // package may have different import mappings, |
| // but the hook doesn't provide the file context. |
| // TODO(adonovan): conspire with docHTML to |
| // pass the doc comment's enclosing file through |
| // a shared variable, so that we can compute |
| // the correct per-file mapping. |
| // |
| // TODO(adonovan): check for PkgName.Name |
| // matches, but also check for |
| // PkgName.Imported.Namer matches, since some |
| // packages are typically imported under a |
| // non-default name (e.g. pathpkg "path") but |
| // may be referred to in doc links using their |
| // canonical name. |
| for _, f := range pkg.Syntax() { |
| for _, imp := range f.Imports { |
| pkgName, ok := typesutil.ImportedPkgName(pkg.TypesInfo(), imp) |
| if ok && pkgName.Name() == name { |
| return pkgName.Imported().Path(), true |
| } |
| } |
| } |
| return "", false |
| } |
| parser.LookupSym = func(recv, name string) (ok bool) { |
| // package-level decl? |
| if recv == "" { |
| return pkg.Types().Scope().Lookup(name) != nil |
| } |
| |
| // method? |
| tname, ok := pkg.Types().Scope().Lookup(recv).(*types.TypeName) |
| if !ok { |
| return false |
| } |
| m, _, _ := types.LookupFieldOrMethod(tname.Type(), true, pkg.Types(), name) |
| return is[*types.Func](m) |
| } |
| docHTML = func(comment string) []byte { |
| return printer.HTML(parser.Parse(comment)) |
| } |
| } |
| |
| scope := pkg.Types().Scope() |
| escape := html.EscapeString |
| |
| title := fmt.Sprintf("%s package - %s - Gopls packages", |
| pkg.Types().Name(), escape(pkg.Types().Path())) |
| |
| var buf bytes.Buffer |
| buf.WriteString(`<!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="UTF-8"> |
| <style>` + pkgDocStyle + `</style> |
| <title>` + title + `</title> |
| <script type='text/javascript'> |
| // httpGET requests a URL for its effects only. |
| function httpGET(url) { |
| var xhttp = new XMLHttpRequest(); |
| xhttp.open("GET", url, true); |
| xhttp.send(); |
| return false; // disable usual <a href=...> behavior |
| } |
| |
| window.onload = () => { |
| // Hook up the navigation selector. |
| document.getElementById('hdr-Selector').onchange = (e) => { |
| window.location.href = e.target.value; |
| }; |
| }; |
| |
| // Start a GET /hang request. If it ever completes, the server |
| // has disconnected. Show a banner in that case. |
| { |
| var x = new XMLHttpRequest(); |
| x.open("GET", "/hang", true); |
| x.onloadend = () => { |
| document.getElementById("disconnected").style.display = 'block'; |
| }; |
| x.send(); |
| }; |
| </script> |
| </head> |
| <body> |
| <header> |
| <div id='disconnected'>Gopls server has terminated. Page is inactive.</div> |
| <select id='hdr-Selector'> |
| <optgroup label="Documentation"> |
| <option label="Overview" value="#hdr-Overview"/> |
| <option label="Index" value="#hdr-Index"/> |
| <option label="Constants" value="#hdr-Constants"/> |
| <option label="Variables" value="#hdr-Variables"/> |
| <option label="Functions" value="#hdr-Functions"/> |
| <option label="Types" value="#hdr-Types"/> |
| <option label="Source Files" value="#hdr-SourceFiles"/> |
| </optgroup> |
| `) |
| |
| // -- header select element -- |
| |
| // option emits an <option> for the specified symbol. |
| // |
| // recvType is the apparent receiver type, which may |
| // differ from ReceiverNamed(obj.Signature.Recv).Name |
| // for promoted methods. |
| option := func(obj types.Object, recvType string) { |
| // Render functions/methods as "(recv) Method(p1, ..., pN)". |
| fragment := obj.Name() |
| |
| // format parameter names (p1, ..., pN) |
| label := obj.Name() // for a type |
| if fn, ok := obj.(*types.Func); ok { |
| var buf strings.Builder |
| sig := fn.Type().(*types.Signature) |
| if sig.Recv() != nil { |
| fmt.Fprintf(&buf, "(%s) ", sig.Recv().Name()) |
| fragment = recvType + "." + fn.Name() |
| } |
| fmt.Fprintf(&buf, "%s(", fn.Name()) |
| for i := 0; i < sig.Params().Len(); i++ { |
| if i > 0 { |
| buf.WriteString(", ") |
| } |
| name := sig.Params().At(i).Name() |
| if name == "" { |
| name = "_" |
| } |
| buf.WriteString(name) |
| } |
| buf.WriteByte(')') |
| label = buf.String() |
| } |
| |
| fmt.Fprintf(&buf, " <option label='%s' value='#%s'/>\n", label, fragment) |
| } |
| |
| // index of functions |
| fmt.Fprintf(&buf, "<optgroup label='Functions'>\n") |
| for _, fn := range docpkg.Funcs { |
| option(scope.Lookup(fn.Name), "") |
| } |
| fmt.Fprintf(&buf, "</optgroup>\n") |
| |
| // index of types |
| fmt.Fprintf(&buf, "<optgroup label='Types'>\n") |
| for _, doctype := range docpkg.Types { |
| option(scope.Lookup(doctype.Name), "") |
| } |
| fmt.Fprintf(&buf, "</optgroup>\n") |
| |
| // index of constructors and methods of each type |
| for _, doctype := range docpkg.Types { |
| tname := scope.Lookup(doctype.Name).(*types.TypeName) |
| if len(doctype.Funcs)+len(doctype.Methods) > 0 { |
| fmt.Fprintf(&buf, "<optgroup label='type %s'>\n", doctype.Name) |
| for _, docfn := range doctype.Funcs { |
| option(scope.Lookup(docfn.Name), "") |
| } |
| for _, docmethod := range doctype.Methods { |
| method, _, _ := types.LookupFieldOrMethod(tname.Type(), true, tname.Pkg(), docmethod.Name) |
| option(method, doctype.Name) |
| } |
| fmt.Fprintf(&buf, "</optgroup>\n") |
| } |
| } |
| fmt.Fprintf(&buf, "</select>\n") |
| fmt.Fprintf(&buf, "</header>\n") |
| |
| // -- main element -- |
| |
| // sourceLink returns HTML for a link to open a file in the client editor. |
| sourceLink := func(text, url string) string { |
| // The /open URL returns nothing but has the side effect |
| // of causing the LSP client to open the requested file. |
| // So we use onclick to prevent the browser from navigating. |
| // We keep the href attribute as it causes the <a> to render |
| // as a link: blue, underlined, with URL hover information. |
| return fmt.Sprintf(`<a href="%[1]s" onclick='return httpGET("%[1]s")'>%[2]s</a>`, |
| escape(url), escape(text)) |
| } |
| |
| // objHTML returns HTML for obj.Name(), possibly as a link. |
| objHTML := func(obj types.Object) string { |
| text := obj.Name() |
| if posn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()); posn.IsValid() { |
| return sourceLink(text, posURL(posn.Filename, posn.Line, posn.Column)) |
| } |
| return text |
| } |
| |
| // nodeHTML returns HTML markup for a syntax tree. |
| // It replaces referring identifiers with links, |
| // and adds style spans for strings and comments. |
| nodeHTML := func(n ast.Node) string { |
| |
| // linkify returns the appropriate URL (if any) for an identifier. |
| linkify := func(id *ast.Ident) protocol.URI { |
| if obj, ok := pkg.TypesInfo().Uses[id]; ok && obj.Pkg() != nil { |
| // imported package name? |
| if pkgname, ok := obj.(*types.PkgName); ok { |
| // TODO(adonovan): do this for Defs of PkgName too. |
| return pkgURL(PackagePath(pkgname.Imported().Path()), "") |
| } |
| |
| // package-level symbol? |
| if obj.Parent() == obj.Pkg().Scope() { |
| if obj.Pkg() == pkg.Types() { |
| return "#" + obj.Name() // intra-package ref |
| } else { |
| return pkgURL(PackagePath(obj.Pkg().Path()), obj.Name()) |
| } |
| } |
| |
| // method of package-level named type? |
| if fn, ok := obj.(*types.Func); ok { |
| sig := fn.Type().(*types.Signature) |
| if sig.Recv() != nil { |
| _, named := typesinternal.ReceiverNamed(sig.Recv()) |
| if named != nil { |
| fragment := named.Obj().Name() + "." + fn.Name() |
| return pkgURL(PackagePath(fn.Pkg().Path()), fragment) |
| } |
| } |
| return "" |
| } |
| |
| // TODO(adonovan): field of package-level named struct type. |
| // (Requires an index, since there's no way to |
| // get from Var to Named.) |
| } |
| return "" |
| } |
| |
| // Splice spans into HTML-escaped segments of the |
| // original source buffer (which is usually but not |
| // necessarily formatted). |
| // |
| // (For expedience we don't use the more sophisticated |
| // approach taken by cmd/godoc and pkgsite's render |
| // package, which emit the text, spans, and comments |
| // in one traversal of the syntax tree.) |
| // |
| // TODO(adonovan): splice styled spans around comments too. |
| // |
| // TODO(adonovan): pkgsite prints specs from grouped |
| // type decls like "type ( T1; T2 )" to make them |
| // appear as separate decls. We should too. |
| var buf bytes.Buffer |
| for _, file := range pkg.CompiledGoFiles() { |
| if astutil.NodeContains(file.File, n.Pos()) { |
| pos := n.Pos() |
| |
| // emit emits source in the interval [pos:to] and updates pos. |
| emit := func(to token.Pos) { |
| // Ident and BasicLit always have a valid pos. |
| // (Failure means the AST has been corrupted.) |
| if !to.IsValid() { |
| bug.Reportf("invalid Pos") |
| } |
| start, err := safetoken.Offset(file.Tok, pos) |
| if err != nil { |
| bug.Reportf("invalid start Pos: %v", err) |
| } |
| end, err := safetoken.Offset(file.Tok, to) |
| if err != nil { |
| bug.Reportf("invalid end Pos: %v", err) |
| } |
| buf.WriteString(escape(string(file.Src[start:end]))) |
| pos = to |
| } |
| ast.Inspect(n, func(n ast.Node) bool { |
| switch n := n.(type) { |
| case *ast.Ident: |
| emit(n.Pos()) |
| pos = n.End() |
| if url := linkify(n); url != "" { |
| fmt.Fprintf(&buf, "<a class='id' href='%s'>%s</a>", url, escape(n.Name)) |
| } else { |
| buf.WriteString(escape(n.Name)) // plain |
| } |
| |
| case *ast.BasicLit: |
| emit(n.Pos()) |
| pos = n.End() |
| fmt.Fprintf(&buf, "<span class='lit'>%s</span>", escape(n.Value)) |
| } |
| return true |
| }) |
| emit(n.End()) |
| return buf.String() |
| } |
| } |
| |
| // Original source not found. |
| // Format the node without adornments. |
| if err := format.Node(&buf, pkg.FileSet(), n); err != nil { |
| // e.g. BadDecl? |
| buf.Reset() |
| fmt.Fprintf(&buf, "formatting error: %v", err) |
| } |
| return escape(buf.String()) |
| } |
| |
| // fnString is like fn.String() except that it: |
| // - shows the receiver name; |
| // - uses space "(T) M()" not dot "(T).M()" after receiver; |
| // - doesn't bother with the special case for interface receivers |
| // since it is unreachable for the methods in go/doc. |
| // - elides parameters after the first three: f(a, b, c, ...). |
| fnString := func(fn *types.Func) string { |
| // pkgRelative qualifies types by package name alone |
| pkgRelative := func(other *types.Package) string { |
| if pkg.Types() == other { |
| return "" // same package; unqualified |
| } |
| return other.Name() |
| } |
| |
| sig := fn.Type().(*types.Signature) |
| |
| // Emit "func (recv T) F". |
| var buf bytes.Buffer |
| buf.WriteString("func ") |
| if recv := sig.Recv(); recv != nil { |
| buf.WriteByte('(') |
| if recv.Name() != "" { |
| buf.WriteString(recv.Name()) |
| buf.WriteByte(' ') |
| } |
| types.WriteType(&buf, recv.Type(), pkgRelative) |
| buf.WriteByte(')') |
| buf.WriteByte(' ') // (ObjectString uses a '.' here) |
| } else if pkg := fn.Pkg(); pkg != nil { |
| if s := pkgRelative(pkg); s != "" { |
| buf.WriteString(s) |
| buf.WriteByte('.') |
| } |
| } |
| buf.WriteString(fn.Name()) |
| |
| // Emit signature. |
| // |
| // Elide parameters after the third one. |
| // WriteSignature is too complex to fork, so we replace |
| // parameters 4+ with "invalid type", format, |
| // then post-process the string. |
| if sig.Params().Len() > 3 { |
| |
| // Clone each TypeParam as NewSignatureType modifies them (#67294). |
| cloneTparams := func(seq *types.TypeParamList) []*types.TypeParam { |
| slice := make([]*types.TypeParam, seq.Len()) |
| for i := range slice { |
| tparam := seq.At(i) |
| slice[i] = types.NewTypeParam(tparam.Obj(), tparam.Constraint()) |
| } |
| return slice |
| } |
| |
| sig = types.NewSignatureType( |
| sig.Recv(), |
| cloneTparams(sig.RecvTypeParams()), |
| cloneTparams(sig.TypeParams()), |
| types.NewTuple(append( |
| typesSeqToSlice[*types.Var](sig.Params())[:3], |
| types.NewVar(0, nil, "", types.Typ[types.Invalid]))...), |
| sig.Results(), |
| false) // any final ...T parameter is truncated |
| } |
| types.WriteSignature(&buf, sig, pkgRelative) |
| return strings.ReplaceAll(buf.String(), ", invalid type)", ", ...)") |
| } |
| |
| fmt.Fprintf(&buf, "<main>\n") |
| |
| // package name |
| fmt.Fprintf(&buf, "<h1 id='hdr-Overview'>Package %s</h1>\n", pkg.Types().Name()) |
| |
| // import path |
| fmt.Fprintf(&buf, "<pre class='code'>import %q</pre>\n", pkg.Types().Path()) |
| |
| // link to same package in pkg.go.dev |
| fmt.Fprintf(&buf, "<div><a href=%q title='View in pkg.go.dev'><img id='pkgsite' src='/assets/go-logo-blue.svg'/></a>\n", |
| "https://pkg.go.dev/"+string(pkg.Types().Path())) |
| |
| // package doc |
| fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(docpkg.Doc)) |
| |
| // symbol index |
| fmt.Fprintf(&buf, "<h2 id='hdr-Index'>Index</h2>\n") |
| fmt.Fprintf(&buf, "<ul>\n") |
| if len(docpkg.Consts) > 0 { |
| fmt.Fprintf(&buf, "<li><a href='#hdr-Constants'>Constants</a></li>\n") |
| } |
| if len(docpkg.Vars) > 0 { |
| fmt.Fprintf(&buf, "<li><a href='#hdr-Variables'>Variables</a></li>\n") |
| } |
| for _, fn := range docpkg.Funcs { |
| obj := scope.Lookup(fn.Name).(*types.Func) |
| fmt.Fprintf(&buf, "<li><a href='#%s'>%s</a></li>\n", |
| obj.Name(), escape(fnString(obj))) |
| } |
| for _, doctype := range docpkg.Types { |
| tname := scope.Lookup(doctype.Name).(*types.TypeName) |
| fmt.Fprintf(&buf, "<li><a href='#%[1]s'>type %[1]s</a></li>\n", |
| tname.Name()) |
| |
| if len(doctype.Funcs)+len(doctype.Methods) > 0 { |
| fmt.Fprintf(&buf, "<ul>\n") |
| |
| // constructors |
| for _, docfn := range doctype.Funcs { |
| obj := scope.Lookup(docfn.Name).(*types.Func) |
| fmt.Fprintf(&buf, "<li><a href='#%s'>%s</a></li>\n", |
| docfn.Name, escape(fnString(obj))) |
| } |
| // methods |
| for _, docmethod := range doctype.Methods { |
| method, _, _ := types.LookupFieldOrMethod(tname.Type(), true, tname.Pkg(), docmethod.Name) |
| fmt.Fprintf(&buf, "<li><a href='#%s.%s'>%s</a></li>\n", |
| doctype.Name, |
| docmethod.Name, |
| escape(fnString(method.(*types.Func)))) |
| } |
| fmt.Fprintf(&buf, "</ul>\n") |
| } |
| } |
| // TODO(adonovan): add index of Examples here. |
| fmt.Fprintf(&buf, "</ul>\n") |
| |
| // constants and variables |
| values := func(vals []*doc.Value) { |
| for _, v := range vals { |
| // anchors |
| for _, name := range v.Names { |
| fmt.Fprintf(&buf, "<a id='%s'></a>\n", escape(name)) |
| } |
| |
| // declaration |
| decl2 := *v.Decl // shallow copy |
| decl2.Doc = nil |
| fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n", nodeHTML(&decl2)) |
| |
| // comment (if any) |
| fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(v.Doc)) |
| } |
| } |
| fmt.Fprintf(&buf, "<h2 id='hdr-Constants'>Constants</h2>\n") |
| if len(docpkg.Consts) == 0 { |
| fmt.Fprintf(&buf, "<div>(no constants)</div>\n") |
| } else { |
| values(docpkg.Consts) |
| } |
| fmt.Fprintf(&buf, "<h2 id='hdr-Variables'>Variables</h2>\n") |
| if len(docpkg.Vars) == 0 { |
| fmt.Fprintf(&buf, "<div>(no variables)</div>\n") |
| } else { |
| values(docpkg.Vars) |
| } |
| |
| // package-level functions |
| fmt.Fprintf(&buf, "<h2 id='hdr-Functions'>Functions</h2>\n") |
| // funcs emits a list of package-level functions, |
| // possibly organized beneath the type they construct. |
| funcs := func(funcs []*doc.Func) { |
| for _, docfn := range funcs { |
| obj := scope.Lookup(docfn.Name).(*types.Func) |
| fmt.Fprintf(&buf, "<h3 id='%s'>func %s</h3>\n", |
| docfn.Name, objHTML(obj)) |
| |
| // decl: func F(params) results |
| fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n", |
| nodeHTML(docfn.Decl.Type)) |
| |
| // comment (if any) |
| fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(docfn.Doc)) |
| } |
| } |
| funcs(docpkg.Funcs) |
| |
| // types and their subelements |
| fmt.Fprintf(&buf, "<h2 id='hdr-Types'>Types</h2>\n") |
| for _, doctype := range docpkg.Types { |
| tname := scope.Lookup(doctype.Name).(*types.TypeName) |
| |
| // title and source link |
| fmt.Fprintf(&buf, "<h3 id='%s'>type %s</a></h3>\n", doctype.Name, objHTML(tname)) |
| |
| // declaration |
| // TODO(adonovan): excise non-exported struct fields somehow. |
| decl2 := *doctype.Decl // shallow copy |
| decl2.Doc = nil |
| fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n", nodeHTML(&decl2)) |
| |
| // comment (if any) |
| fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(doctype.Doc)) |
| |
| // subelements |
| values(doctype.Consts) // constants of type T |
| values(doctype.Vars) // vars of type T |
| funcs(doctype.Funcs) // constructors of T |
| |
| // methods on T |
| for _, docmethod := range doctype.Methods { |
| method, _, _ := types.LookupFieldOrMethod(tname.Type(), true, tname.Pkg(), docmethod.Name) |
| fmt.Fprintf(&buf, "<h4 id='%s.%s'>func (%s) %s</h4>\n", |
| doctype.Name, docmethod.Name, |
| doctype.Name, objHTML(method)) |
| |
| // decl: func (x T) M(params) results |
| fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n", |
| nodeHTML(docmethod.Decl.Type)) |
| |
| // comment (if any) |
| fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", |
| docHTML(docmethod.Doc)) |
| } |
| } |
| |
| // source files |
| fmt.Fprintf(&buf, "<h2 id='hdr-SourceFiles'>Source files</h2>\n") |
| for _, filename := range docpkg.Filenames { |
| fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", |
| sourceLink(filepath.Base(filename), posURL(filename, 1, 1))) |
| } |
| |
| fmt.Fprintf(&buf, "</main>\n") |
| fmt.Fprintf(&buf, "</body>\n") |
| fmt.Fprintf(&buf, "</html>\n") |
| |
| return buf.Bytes(), nil |
| } |
| |
| // typesSeq abstracts various go/types sequence types: |
| // MethodSet, Tuple, TypeParamList, TypeList. |
| // TODO(adonovan): replace with go1.23 iterators. |
| type typesSeq[T any] interface { |
| Len() int |
| At(int) T |
| } |
| |
| func typesSeqToSlice[T any](seq typesSeq[T]) []T { |
| slice := make([]T, seq.Len()) |
| for i := range slice { |
| slice[i] = seq.At(i) |
| } |
| return slice |
| } |
| |
| // (partly taken from pkgsite's typography.css) |
| const pkgDocStyle = ` |
| body { |
| font-family: Helvetica, Arial, sans-serif; |
| font-size: 1rem; |
| line-height: normal; |
| } |
| |
| h1 { |
| font-size: 1.5rem; |
| } |
| |
| h2 { |
| font-size: 1.375rem; |
| } |
| |
| h3 { |
| font-size: 1.25rem; |
| } |
| |
| h4 { |
| font-size: 1.125rem; |
| } |
| |
| h5 { |
| font-size: 1rem; |
| } |
| |
| h6 { |
| font-size: 0.875rem; |
| } |
| |
| h1, |
| h2, |
| h3, |
| h4 { |
| font-weight: 600; |
| line-height: 1.25em; |
| word-break: break-word; |
| } |
| |
| h5, |
| h6 { |
| font-weight: 500; |
| line-height: 1.3em; |
| word-break: break-word; |
| } |
| |
| p { |
| font-size: 1rem; |
| line-height: 1.5rem; |
| max-width: 60rem; |
| } |
| |
| strong { |
| font-weight: 600; |
| } |
| |
| code, |
| pre, |
| textarea.code { |
| font-family: Consolas, 'Liberation Mono', Menlo, monospace; |
| font-size: 0.875rem; |
| line-height: 1.5em; |
| } |
| |
| pre, |
| textarea.code { |
| background-color: #eee; |
| border: 3px; |
| border-radius: 3px |
| color: black; |
| overflow-x: auto; |
| padding: 0.625rem; |
| tab-size: 4; |
| white-space: pre; |
| } |
| |
| button, |
| input, |
| select, |
| textarea { |
| font: inherit; |
| } |
| |
| a, |
| a:link, |
| a:visited { |
| color: rgb(0, 125, 156); |
| text-decoration: none; |
| } |
| |
| a:hover, |
| a:focus { |
| color: rgb(0, 125, 156); |
| text-decoration: underline; |
| } |
| |
| a:hover > * { |
| text-decoration: underline; |
| } |
| |
| .lit { color: darkgreen; } |
| |
| #pkgsite { height: 1.5em; } |
| |
| header { |
| position: sticky; |
| top: 0; |
| left: 0; |
| width: 100%; |
| padding: 0.3em; |
| } |
| |
| #hdr-Selector { |
| margin-right: 0.3em; |
| float: right; |
| min-width: 25em; |
| padding: 0.3em; |
| } |
| |
| #disconnected { |
| position: fixed; |
| top: 1em; |
| left: 1em; |
| display: none; /* initially */ |
| background-color: white; |
| border: thick solid red; |
| padding: 2em; |
| } |
| ` |