aboutsummaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
authorJonathan Amsterdam <jba@google.com>2026-03-20 13:37:49 -0400
committerJonathan Amsterdam <jba@google.com>2026-03-24 09:32:29 -0700
commit21096dc7108a47eba6d7bb0ada27be396420f88a (patch)
treea429e21446268dfeac0daa6a1a1918760bc8f4ca /internal/api
parentf2497254fdac6fde9ff38a604f6891753b583a20 (diff)
downloadgo-x-pkgsite-21096dc7108a47eba6d7bb0ada27be396420f88a.tar.xz
internal/api: render text documentation
Preliminary attempt to render documentation as text. The output doesn't match either pkgsite or go doc. It's a simplification of the latter. The go doc command turns out to be surprisingly complicated, so it's best to start simple. Change-Id: I7b5a6bf36b1892afb30c212309dd646e3cf8b06a Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/757501 Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com> Reviewed-by: Ethan Lee <ethanalee@google.com> kokoro-CI: kokoro <noreply+kokoro@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/api.go20
-rw-r--r--internal/api/api_test.go2
-rw-r--r--internal/api/render.go170
-rw-r--r--internal/api/render_test.go61
-rw-r--r--internal/api/testdata/pkg.go78
-rw-r--r--internal/api/testdata/text.golden71
6 files changed, 388 insertions, 14 deletions
diff --git a/internal/api/api.go b/internal/api/api.go
index 679cf0ee..d94098e3 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -8,7 +8,6 @@ import (
"bytes"
"encoding/json"
"errors"
- "go/doc"
"net/http"
"strconv"
"strings"
@@ -157,21 +156,22 @@ func ServePackage(w http.ResponseWriter, r *http.Request, ds internal.DataSource
if err != nil {
return err
}
- var formatFunc func(string) []byte
+ var tr renderer
+ var sb strings.Builder
switch params.Doc {
case "text":
- formatFunc = dpkg.Text
+ tr = &textRenderer{fset: gpkg.Fset, w: &sb}
case "md", "markdown":
- formatFunc = dpkg.Markdown
+ return errors.New("unimplemented")
case "html":
- formatFunc = dpkg.HTML
+ return errors.New("unimplemented")
default:
return serveErrorJSON(w, http.StatusBadRequest, "bad doc format: need one of 'text', 'md', 'markdown' or 'html'", nil)
}
- docs, err = renderDoc(dpkg, formatFunc)
- if err != nil {
+ if err := renderDoc(dpkg, tr); err != nil {
return serveErrorJSON(w, http.StatusInternalServerError, err.Error(), nil)
}
+ docs = sb.String()
}
}
@@ -201,12 +201,6 @@ func ServePackage(w http.ResponseWriter, r *http.Request, ds internal.DataSource
return serveJSON(w, http.StatusOK, resp)
}
-// renderDoc renders the documentation for dpkg according to format.
-// TODO(jba): implement
-func renderDoc(dpkg *doc.Package, formatFunc func(string) []byte) (string, error) {
- return string(formatFunc("TODO")), nil
-}
-
// ServeModule handles requests for the v1 module metadata endpoint.
func ServeModule(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) {
defer derrors.Wrap(&err, "ServeModule")
diff --git a/internal/api/api_test.go b/internal/api/api_test.go
index 87d6b8d0..9983a8cf 100644
--- a/internal/api/api_test.go
+++ b/internal/api/api_test.go
@@ -136,7 +136,7 @@ func TestServePackage(t *testing.T) {
Synopsis: "This is a package synopsis for GOOS=linux, GOARCH=amd64",
GOOS: "linux",
GOARCH: "amd64",
- Docs: "TODO\n",
+ Docs: "package p\n\nPackage p is a package.\n\n# Links\n\n- pkg.go.dev, https://pkg.go.dev\n\nVARIABLES\n\nvar V int\n\n",
},
},
} {
diff --git a/internal/api/render.go b/internal/api/render.go
new file mode 100644
index 00000000..47b157af
--- /dev/null
+++ b/internal/api/render.go
@@ -0,0 +1,170 @@
+// Copyright 2026 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 api
+
+import (
+ "fmt"
+ "go/ast"
+ "go/doc"
+ "go/doc/comment"
+ "go/format"
+ "go/token"
+ "io"
+ "slices"
+ "strings"
+)
+
+// A renderer prints symbol documentation for a package.
+// An error that occurs during rendering is saved and returned
+// by the end method.
+type renderer interface {
+ start(*doc.Package)
+ end() error
+ // startSection start a section, like the one for functions.
+ startSection(name string)
+ endSection()
+
+ // emit prints documentation for particular node, like a const
+ // or function.
+ emit(comment string, node ast.Node)
+ // TODO(jba): support examples
+}
+
+type textRenderer struct {
+ fset *token.FileSet
+ w io.Writer
+ pkg *doc.Package
+ parser *comment.Parser
+ printer *comment.Printer
+ err error
+}
+
+func (r *textRenderer) start(pkg *doc.Package) {
+ r.pkg = pkg
+ r.parser = pkg.Parser()
+ // Configure the printer for symbol comments here,
+ // so we only do it once.
+ r.printer = pkg.Printer()
+ r.printer.TextPrefix = "\t"
+ r.printer.TextCodePrefix = "\t\t"
+
+ r.printf("package %s\n", pkg.Name)
+ if pkg.Doc != "" {
+ r.printf("\n")
+ // The package doc is not indented, so don't use r.printer.
+ _, err := r.w.Write(pkg.Text(pkg.Doc))
+ if err != nil {
+ r.err = err
+ }
+ }
+ r.printf("\n")
+}
+
+func (r *textRenderer) end() error { return r.err }
+
+func (r *textRenderer) startSection(name string) {
+ r.printf("%s\n\n", strings.ToUpper(name))
+}
+
+func (r *textRenderer) endSection() {}
+
+func (r *textRenderer) emit(comment string, node ast.Node) {
+ if r.err != nil {
+ return
+ }
+ err := format.Node(r.w, r.fset, node)
+ if err != nil {
+ r.err = err
+ return
+ }
+ r.printf("\n")
+ formatted := r.printer.Text(r.parser.Parse(comment))
+ if len(formatted) > 0 {
+ _, err = r.w.Write(formatted)
+ if err != nil {
+ r.err = err
+ return
+ }
+ }
+ r.printf("\n")
+}
+
+func (r *textRenderer) printf(format string, args ...any) {
+ if r.err != nil {
+ return
+ }
+ _, err := fmt.Fprintf(r.w, format, args...)
+ if err != nil {
+ r.err = err
+ }
+}
+
+// renderDoc renders the documentation for dpkg using the given renderer.
+// TODO(jba): support examples.
+func renderDoc(dpkg *doc.Package, r renderer) error {
+ r.start(dpkg)
+
+ renderValues(dpkg.Consts, r, "constants")
+ renderValues(dpkg.Vars, r, "variables")
+ renderFuncs(dpkg.Funcs, r, "functions")
+
+ started := false
+ for _, t := range dpkg.Types {
+ if !ast.IsExported(t.Name) {
+ continue
+ }
+ if !started {
+ r.startSection("types")
+ started = true
+ }
+ r.emit(t.Doc, t.Decl)
+ renderValues(t.Consts, r, "")
+ renderValues(t.Vars, r, "")
+ renderFuncs(t.Funcs, r, "")
+ renderFuncs(t.Methods, r, "")
+ }
+ if started {
+ r.endSection()
+ }
+ return r.end()
+}
+
+func renderValues(vals []*doc.Value, r renderer, section string) {
+ started := false
+ for _, v := range vals {
+ // Render a group if at least one is exported.
+ if slices.IndexFunc(v.Names, ast.IsExported) >= 0 {
+ if !started {
+ if section != "" {
+ r.startSection(section)
+ }
+ started = true
+ }
+ r.emit(v.Doc, v.Decl)
+ }
+ }
+ if started && section != "" {
+ r.endSection()
+ }
+}
+
+func renderFuncs(funcs []*doc.Func, r renderer, section string) {
+ started := false
+ for _, f := range funcs {
+ if !ast.IsExported(f.Name) {
+ continue
+ }
+ if !started {
+ if section != "" {
+ r.startSection(section)
+ }
+ started = true
+ }
+ r.emit(f.Doc, f.Decl)
+ }
+ if started && section != "" {
+ r.endSection()
+ }
+}
diff --git a/internal/api/render_test.go b/internal/api/render_test.go
new file mode 100644
index 00000000..d77518d9
--- /dev/null
+++ b/internal/api/render_test.go
@@ -0,0 +1,61 @@
+// Copyright 2026 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 api
+
+import (
+ "context"
+ "go/parser"
+ "go/token"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/pkgsite/internal/godoc"
+)
+
+func TestRenderDoc(t *testing.T) {
+ src, err := os.ReadFile("testdata/pkg.go")
+ if err != nil {
+ t.Fatal(err)
+ }
+ fset := token.NewFileSet()
+ pf, err := parser.ParseFile(fset, "p.go", src, parser.ParseComments)
+ if err != nil {
+ t.Fatal(err)
+ }
+ docPkg := godoc.NewPackage(fset, nil)
+ docPkg.AddFile(pf, true)
+ gpkg, err := docPkg.Encode(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ decoded, err := godoc.DecodePackage(gpkg)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dpkg, err := decoded.DocPackage("p", &godoc.ModuleInfo{ModulePath: "p", ResolvedVersion: "v1.0.0"})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var sb strings.Builder
+ tr := &textRenderer{fset: decoded.Fset, w: &sb}
+ if err := renderDoc(dpkg, tr); err != nil {
+ t.Fatal(err)
+ }
+ got := strings.TrimSpace(sb.String())
+ wantBytes, err := os.ReadFile(filepath.FromSlash("testdata/text.golden"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := strings.TrimSpace(string(wantBytes))
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("mismatch (-want +got):\n%s", diff)
+ }
+}
diff --git a/internal/api/testdata/pkg.go b/internal/api/testdata/pkg.go
new file mode 100644
index 00000000..b9650bb8
--- /dev/null
+++ b/internal/api/testdata/pkg.go
@@ -0,0 +1,78 @@
+// Copyright 2026 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 pkg has every form of declaration.
+//
+// # Links
+//
+// - pkgsite repo, https://go.googlesource.com/pkgsite
+// - Play with Go, https://play-with-go.dev
+package pkg
+
+// C is a shorthand for 1.
+const C = 1
+
+// No exported name; should not appear.
+const (
+ a = 1
+ b = 2
+ c = 3
+)
+
+// V is a variable.
+var V = 2
+
+// F is a function.
+func F() {}
+
+// Several constants.
+const (
+ X = 1
+ Y = 2
+)
+
+// CT is a typed constant.
+// They appear after their type.
+const CT T = 3
+
+// TF is a constructor for T.
+func TF() T { return T(0) }
+
+// M is a method of T.
+// BUG(xxx): this verifies that notes are rendered.
+func (T) M() {}
+
+// T is a type.
+type T int
+
+// S1 is a struct.
+type S1 struct {
+ F int // field
+}
+
+// S2 is another struct.
+type S2 struct {
+ S1
+ G int
+}
+
+// I1 is an interface.
+type I1 interface {
+ M1()
+}
+
+type I2 interface {
+ I1
+ M2()
+}
+
+type (
+ A int
+ B bool
+)
+
+// Add adds 1 to x.
+func Add(x int) int {
+ return x + 1
+}
diff --git a/internal/api/testdata/text.golden b/internal/api/testdata/text.golden
new file mode 100644
index 00000000..6497759c
--- /dev/null
+++ b/internal/api/testdata/text.golden
@@ -0,0 +1,71 @@
+package pkg
+
+Package pkg has every form of declaration.
+
+# Links
+
+ - pkgsite repo, https://go.googlesource.com/pkgsite
+ - Play with Go, https://play-with-go.dev
+
+CONSTANTS
+
+const (
+ X = 1
+ Y = 2
+)
+ Several constants.
+
+const C = 1
+ C is a shorthand for 1.
+
+VARIABLES
+
+var V = 2
+ V is a variable.
+
+FUNCTIONS
+
+func Add(x int) int
+ Add adds 1 to x.
+
+func F()
+ F is a function.
+
+TYPES
+
+type A int
+
+type B bool
+
+type I1 interface {
+ M1()
+}
+ I1 is an interface.
+
+type I2 interface {
+ I1
+ M2()
+}
+
+type S1 struct {
+ F int // field
+}
+ S1 is a struct.
+
+type S2 struct {
+ S1
+ G int
+}
+ S2 is another struct.
+
+type T int
+ T is a type.
+
+const CT T = 3
+ CT is a typed constant. They appear after their type.
+
+func TF() T
+ TF is a constructor for T.
+
+func (T) M()
+ M is a method of T. BUG(xxx): this verifies that notes are rendered.