diff options
| author | Jonathan Amsterdam <jba@google.com> | 2026-03-20 13:37:49 -0400 |
|---|---|---|
| committer | Jonathan Amsterdam <jba@google.com> | 2026-03-24 09:32:29 -0700 |
| commit | 21096dc7108a47eba6d7bb0ada27be396420f88a (patch) | |
| tree | a429e21446268dfeac0daa6a1a1918760bc8f4ca /internal/api | |
| parent | f2497254fdac6fde9ff38a604f6891753b583a20 (diff) | |
| download | go-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.go | 20 | ||||
| -rw-r--r-- | internal/api/api_test.go | 2 | ||||
| -rw-r--r-- | internal/api/render.go | 170 | ||||
| -rw-r--r-- | internal/api/render_test.go | 61 | ||||
| -rw-r--r-- | internal/api/testdata/pkg.go | 78 | ||||
| -rw-r--r-- | internal/api/testdata/text.golden | 71 |
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. |
