aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan Amsterdam <jba@google.com>2026-03-21 16:52:49 -0400
committerJonathan Amsterdam <jba@google.com>2026-03-27 11:54:39 -0700
commit0f438ccc7dbe2f891f5f97594f7b6510e490c742 (patch)
tree616118e6c6178e26d3268dab682c27e1fe773898
parentc59880de3cd54955ef11b4187daf7f930badb5e2 (diff)
downloadgo-x-pkgsite-0f438ccc7dbe2f891f5f97594f7b6510e490c742.tar.xz
internal/api: render markdown documentation
Change-Id: I8adf1e62d19cf592cc8204264d99953e12da5843 Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/760260 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> kokoro-CI: kokoro <noreply+kokoro@google.com> Reviewed-by: Ethan Lee <ethanalee@google.com>
-rw-r--r--internal/api/api.go8
-rw-r--r--internal/api/render.go74
-rw-r--r--internal/api/render_test.go31
-rw-r--r--internal/api/testdata/markdown.golden105
-rw-r--r--internal/api/testdata/pkg.go6
-rw-r--r--internal/api/testdata/text.golden6
6 files changed, 214 insertions, 16 deletions
diff --git a/internal/api/api.go b/internal/api/api.go
index edd758e1..b2be2327 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -106,19 +106,19 @@ func ServePackage(w http.ResponseWriter, r *http.Request, ds internal.DataSource
if err != nil {
return err
}
- var tr renderer
+ var r renderer
var sb strings.Builder
switch params.Doc {
case "text":
- tr = &textRenderer{fset: gpkg.Fset, w: &sb}
+ r = &textRenderer{fset: gpkg.Fset, w: &sb}
case "md", "markdown":
- return errors.New("unimplemented")
+ r = &markdownRenderer{fset: gpkg.Fset, w: &sb}
case "html":
return errors.New("unimplemented")
default:
return serveErrorJSON(w, http.StatusBadRequest, "bad doc format: need one of 'text', 'md', 'markdown' or 'html'", nil)
}
- if err := renderDoc(dpkg, tr); err != nil {
+ if err := renderDoc(dpkg, r); err != nil {
return serveErrorJSON(w, http.StatusInternalServerError, err.Error(), nil)
}
docs = sb.String()
diff --git a/internal/api/render.go b/internal/api/render.go
index 47b157af..656a8405 100644
--- a/internal/api/render.go
+++ b/internal/api/render.go
@@ -14,6 +14,9 @@ import (
"io"
"slices"
"strings"
+
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
)
// A renderer prints symbol documentation for a package.
@@ -101,6 +104,77 @@ func (r *textRenderer) printf(format string, args ...any) {
}
}
+type markdownRenderer struct {
+ fset *token.FileSet
+ w io.Writer
+ pkg *doc.Package
+ parser *comment.Parser
+ printer *comment.Printer
+ caser cases.Caser
+ err error
+}
+
+func (r *markdownRenderer) start(pkg *doc.Package) {
+ r.pkg = pkg
+ r.parser = pkg.Parser()
+ r.printer = pkg.Printer()
+ r.printer.HeadingLevel = 3
+ r.caser = cases.Title(language.English)
+
+ r.printf("# package %s\n", pkg.Name)
+ if pkg.Doc != "" {
+ r.printf("\n")
+ _, err := r.w.Write(r.printer.Markdown(r.parser.Parse(pkg.Doc)))
+ if err != nil {
+ r.err = err
+ }
+ }
+ r.printf("\n")
+}
+
+func (r *markdownRenderer) end() error { return r.err }
+
+func (r *markdownRenderer) startSection(name string) {
+ if name == "" {
+ return
+ }
+ r.printf("## %s\n\n", r.caser.String(name))
+}
+
+func (r *markdownRenderer) endSection() {}
+
+func (r *markdownRenderer) emit(comment string, node ast.Node) {
+ if r.err != nil {
+ return
+ }
+ r.printf("```\n")
+ err := format.Node(r.w, r.fset, node)
+ if err != nil {
+ r.err = err
+ return
+ }
+ r.printf("\n```\n")
+ formatted := r.printer.Markdown(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 *markdownRenderer) 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 {
diff --git a/internal/api/render_test.go b/internal/api/render_test.go
index d77518d9..d163b26d 100644
--- a/internal/api/render_test.go
+++ b/internal/api/render_test.go
@@ -45,17 +45,24 @@ func TestRenderDoc(t *testing.T) {
}
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)
+ check := func(t *testing.T, name string, r renderer) {
+ sb.Reset()
+ t.Run(name, func(t *testing.T) {
+ if err := renderDoc(dpkg, r); err != nil {
+ t.Fatal(err)
+ }
+ got := strings.TrimSpace(sb.String())
+ wantBytes, err := os.ReadFile(filepath.FromSlash("testdata/" + name + ".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)
+ }
+ })
}
+
+ check(t, "text", &textRenderer{fset: decoded.Fset, w: &sb})
+ check(t, "markdown", &markdownRenderer{fset: decoded.Fset, w: &sb})
}
diff --git a/internal/api/testdata/markdown.golden b/internal/api/testdata/markdown.golden
new file mode 100644
index 00000000..205546d0
--- /dev/null
+++ b/internal/api/testdata/markdown.golden
@@ -0,0 +1,105 @@
+# package pkg
+
+Package pkg has every form of declaration.
+
+### Heading {#hdr-Heading}
+
+Search [Google](https://google.com) for details.
+
+### Links {#hdr-Links}
+
+ - pkgsite repo, [https://go.googlesource.com/pkgsite](https://go.googlesource.com/pkgsite)
+ - Play with Go, [https://play-with-go.dev](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.
diff --git a/internal/api/testdata/pkg.go b/internal/api/testdata/pkg.go
index b9650bb8..6d62abad 100644
--- a/internal/api/testdata/pkg.go
+++ b/internal/api/testdata/pkg.go
@@ -4,10 +4,16 @@
// Package pkg has every form of declaration.
//
+// # Heading
+//
+// Search [Google] for details.
+//
// # Links
//
// - pkgsite repo, https://go.googlesource.com/pkgsite
// - Play with Go, https://play-with-go.dev
+//
+// [Google]: https://google.com
package pkg
// C is a shorthand for 1.
diff --git a/internal/api/testdata/text.golden b/internal/api/testdata/text.golden
index 6497759c..75998ec5 100644
--- a/internal/api/testdata/text.golden
+++ b/internal/api/testdata/text.golden
@@ -2,11 +2,17 @@ package pkg
Package pkg has every form of declaration.
+# Heading
+
+Search Google for details.
+
# Links
- pkgsite repo, https://go.googlesource.com/pkgsite
- Play with Go, https://play-with-go.dev
+[Google]: https://google.com
+
CONSTANTS
const (