aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan Amsterdam <jba@google.com>2026-03-27 16:00:47 -0400
committerJonathan Amsterdam <jba@google.com>2026-03-30 13:24:30 -0700
commit46b63f3ffddc9657bc2c45fb116dc49c0b381a34 (patch)
tree972d64dd1124f3ec1b4cbfd3afc2f1b8aba44f4c
parent764f79e85197be12b4ab14db0a118c4931d50d2a (diff)
downloadgo-x-pkgsite-46b63f3ffddc9657bc2c45fb116dc49c0b381a34.tar.xz
internal/api: add htmlRenderer
Change-Id: Ib25c913ce7641dea58e2eefb6d65f5cc7dec813a Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/760820 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>
-rw-r--r--internal/api/render.go79
-rw-r--r--internal/api/render_test.go1
-rw-r--r--internal/api/testdata/html.golden75
3 files changed, 154 insertions, 1 deletions
diff --git a/internal/api/render.go b/internal/api/render.go
index 656a8405..8654de2c 100644
--- a/internal/api/render.go
+++ b/internal/api/render.go
@@ -2,6 +2,10 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+// TODO(jba)? Consider rendering notes separately. Now they appear
+// with the doc for each symbol, which is probably better for LLMs,
+// but be open to evidence to the contrary.
+
package api
import (
@@ -11,6 +15,7 @@ import (
"go/doc/comment"
"go/format"
"go/token"
+ "html"
"io"
"slices"
"strings"
@@ -25,7 +30,7 @@ import (
type renderer interface {
start(*doc.Package)
end() error
- // startSection start a section, like the one for functions.
+ // startSection starts a section, like the one for functions.
startSection(name string)
endSection()
@@ -94,6 +99,7 @@ func (r *textRenderer) emit(comment string, node ast.Node) {
r.printf("\n")
}
+// TODO(jba): consolidate this function to avoid duplication.
func (r *textRenderer) printf(format string, args ...any) {
if r.err != nil {
return
@@ -175,6 +181,77 @@ func (r *markdownRenderer) printf(format string, args ...any) {
}
}
+type htmlRenderer struct {
+ fset *token.FileSet
+ w io.Writer
+ pkg *doc.Package
+ parser *comment.Parser
+ printer *comment.Printer
+ caser cases.Caser
+ err error
+}
+
+func (r *htmlRenderer) 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("<h1>package %s</h1>\n", pkg.Name)
+ if pkg.Doc != "" {
+ r.printf("\n")
+ _, err := r.w.Write(r.printer.HTML(r.parser.Parse(pkg.Doc)))
+ if err != nil {
+ r.err = err
+ }
+ }
+ r.printf("\n")
+}
+
+func (r *htmlRenderer) end() error { return r.err }
+
+func (r *htmlRenderer) startSection(name string) {
+ if name == "" {
+ return
+ }
+ r.printf("<h2>%s</h2>\n\n", r.caser.String(name))
+}
+
+func (r *htmlRenderer) endSection() {}
+
+func (r *htmlRenderer) emit(comment string, node ast.Node) {
+ if r.err != nil {
+ return
+ }
+ var buf strings.Builder
+ err := format.Node(&buf, r.fset, node)
+ if err != nil {
+ r.err = err
+ return
+ }
+ r.printf("<pre><code>%s</code></pre>\n", html.EscapeString(buf.String()))
+ formatted := r.printer.HTML(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 *htmlRenderer) 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 d163b26d..14e06d6d 100644
--- a/internal/api/render_test.go
+++ b/internal/api/render_test.go
@@ -65,4 +65,5 @@ func TestRenderDoc(t *testing.T) {
check(t, "text", &textRenderer{fset: decoded.Fset, w: &sb})
check(t, "markdown", &markdownRenderer{fset: decoded.Fset, w: &sb})
+ check(t, "html", &htmlRenderer{fset: decoded.Fset, w: &sb})
}
diff --git a/internal/api/testdata/html.golden b/internal/api/testdata/html.golden
new file mode 100644
index 00000000..d77b329c
--- /dev/null
+++ b/internal/api/testdata/html.golden
@@ -0,0 +1,75 @@
+<h1>package pkg</h1>
+
+<p>Package pkg has every form of declaration.
+<h3 id="hdr-Heading">Heading</h3>
+<p>Search <a href="https://google.com">Google</a> for details.
+<h3 id="hdr-Links">Links</h3>
+<ul>
+<li>pkgsite repo, <a href="https://go.googlesource.com/pkgsite">https://go.googlesource.com/pkgsite</a>
+<li>Play with Go, <a href="https://play-with-go.dev">https://play-with-go.dev</a>
+</ul>
+
+<h2>Constants</h2>
+
+<pre><code>const (
+ X = 1
+ Y = 2
+)</code></pre>
+<p>Several constants.
+
+<pre><code>const C = 1</code></pre>
+<p>C is a shorthand for 1.
+
+<h2>Variables</h2>
+
+<pre><code>var V = 2</code></pre>
+<p>V is a variable.
+
+<h2>Functions</h2>
+
+<pre><code>func Add(x int) int</code></pre>
+<p>Add adds 1 to x.
+
+<pre><code>func F()</code></pre>
+<p>F is a function.
+
+<h2>Types</h2>
+
+<pre><code>type A int</code></pre>
+
+<pre><code>type B bool</code></pre>
+
+<pre><code>type I1 interface {
+ M1()
+}</code></pre>
+<p>I1 is an interface.
+
+<pre><code>type I2 interface {
+ I1
+ M2()
+}</code></pre>
+
+<pre><code>type S1 struct {
+ F int // field
+}</code></pre>
+<p>S1 is a struct.
+
+<pre><code>type S2 struct {
+ S1
+ G int
+}</code></pre>
+<p>S2 is another struct.
+
+<pre><code>type T int</code></pre>
+<p>T is a type.
+
+<pre><code>const CT T = 3</code></pre>
+<p>CT is a typed constant.
+They appear after their type.
+
+<pre><code>func TF() T</code></pre>
+<p>TF is a constructor for T.
+
+<pre><code>func (T) M()</code></pre>
+<p>M is a method of T.
+BUG(xxx): this verifies that notes are rendered. \ No newline at end of file