diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/api/api.go | 4 | ||||
| -rw-r--r-- | internal/api/render.go | 167 | ||||
| -rw-r--r-- | internal/api/render_test.go | 36 | ||||
| -rw-r--r-- | internal/api/testdata/examples.go | 14 | ||||
| -rw-r--r-- | internal/api/testdata/html-examples.golden | 155 | ||||
| -rw-r--r-- | internal/api/testdata/html.golden | 2 | ||||
| -rw-r--r-- | internal/api/testdata/markdown-examples.golden | 185 | ||||
| -rw-r--r-- | internal/api/testdata/markdown.golden | 30 | ||||
| -rw-r--r-- | internal/api/testdata/pkg_test.go | 46 | ||||
| -rw-r--r-- | internal/api/testdata/text-examples.golden | 122 |
10 files changed, 704 insertions, 57 deletions
diff --git a/internal/api/api.go b/internal/api/api.go index 867d1d1d..cf771e82 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -112,7 +112,9 @@ func ServePackage(w http.ResponseWriter, r *http.Request, ds internal.DataSource default: return fmt.Errorf("%w: bad doc format: need one of 'text', 'md', 'markdown' or 'html'", derrors.InvalidArgument) } - if err := renderDoc(dpkg, r); err != nil { + // TODO(jba): add a param to omit examples + const includeExamples = true + if err := renderDoc(dpkg, r, includeExamples); err != nil { return fmt.Errorf("%w: %s", derrors.Unknown, err.Error()) } docs = sb.String() diff --git a/internal/api/render.go b/internal/api/render.go index 8654de2c..f8d9dbc7 100644 --- a/internal/api/render.go +++ b/internal/api/render.go @@ -37,7 +37,9 @@ type renderer interface { // emit prints documentation for particular node, like a const // or function. emit(comment string, node ast.Node) - // TODO(jba): support examples + + // emitExample prints an example. + emitExample(ex *doc.Example) } type textRenderer struct { @@ -62,8 +64,7 @@ func (r *textRenderer) start(pkg *doc.Package) { 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 { + if _, err := r.w.Write(pkg.Text(pkg.Doc)); err != nil { r.err = err } } @@ -90,8 +91,7 @@ func (r *textRenderer) emit(comment string, node ast.Node) { r.printf("\n") formatted := r.printer.Text(r.parser.Parse(comment)) if len(formatted) > 0 { - _, err = r.w.Write(formatted) - if err != nil { + if _, err = r.w.Write(formatted); err != nil { r.err = err return } @@ -99,13 +99,54 @@ func (r *textRenderer) emit(comment string, node ast.Node) { r.printf("\n") } +func (r *textRenderer) emitExample(ex *doc.Example) { + if r.err != nil { + return + } + r.printf("Example") + if ex.Suffix != "" { + r.printf(" (%s)", ex.Suffix) + } + r.printf(":\n") + if ex.Doc != "" { + formatted := r.printer.Text(r.parser.Parse(ex.Doc)) + if len(formatted) > 0 { + if _, err := r.w.Write(formatted); err != nil { + r.err = err + return + } + r.printf("\n") + } + } + var buf strings.Builder + if err := format.Node(&buf, r.fset, ex.Code); err != nil { + r.err = err + return + } + // Indent the code and output. + lines := strings.Split(strings.TrimSpace(buf.String()), "\n") + for i, line := range lines { + // Omit blank line before close brace. + if i == len(lines)-2 && line == "" { + continue + } + r.printf("\t%s\n", line) + } + if ex.Output != "" { + r.printf("\n\tOutput:\n") + for _, line := range strings.Split(strings.TrimSpace(ex.Output), "\n") { + r.printf("\t%s\n", line) + } + } + r.printf("\n") +} + // TODO(jba): consolidate this function to avoid duplication. func (r *textRenderer) printf(format string, args ...any) { if r.err != nil { return } - _, err := fmt.Fprintf(r.w, format, args...) - if err != nil { + if _, err := fmt.Fprintf(r.w, format, args...); err != nil { r.err = err } } @@ -130,8 +171,7 @@ func (r *markdownRenderer) start(pkg *doc.Package) { 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 { + if _, err := r.w.Write(r.printer.Markdown(r.parser.Parse(pkg.Doc))); err != nil { r.err = err } } @@ -153,7 +193,7 @@ func (r *markdownRenderer) emit(comment string, node ast.Node) { if r.err != nil { return } - r.printf("```\n") + r.printf("```go\n") err := format.Node(r.w, r.fset, node) if err != nil { r.err = err @@ -162,8 +202,7 @@ func (r *markdownRenderer) emit(comment string, node ast.Node) { r.printf("\n```\n") formatted := r.printer.Markdown(r.parser.Parse(comment)) if len(formatted) > 0 { - _, err = r.w.Write(formatted) - if err != nil { + if _, err = r.w.Write(formatted); err != nil { r.err = err return } @@ -171,13 +210,41 @@ func (r *markdownRenderer) emit(comment string, node ast.Node) { r.printf("\n") } -func (r *markdownRenderer) printf(format string, args ...any) { +func (r *markdownRenderer) emitExample(ex *doc.Example) { if r.err != nil { return } - _, err := fmt.Fprintf(r.w, format, args...) + r.printf("#### Example") + if ex.Suffix != "" { + r.printf(" (%s)", ex.Suffix) + } + r.printf("\n\n") + if ex.Doc != "" { + if _, err := r.w.Write(r.printer.Markdown(r.parser.Parse(ex.Doc))); err != nil { + r.err = err + return + } + r.printf("\n") + } + r.printf("```go\n") + err := format.Node(r.w, r.fset, ex.Code) if err != nil { r.err = err + return + } + r.printf("\n```\n") + if ex.Output != "" { + r.printf("Output:\n\n```\n%s\n```\n", ex.Output) + } + r.printf("\n") +} + +func (r *markdownRenderer) printf(format string, args ...any) { + if r.err != nil { + return + } + if _, err := fmt.Fprintf(r.w, format, args...); err != nil { + r.err = err } } @@ -188,6 +255,7 @@ type htmlRenderer struct { parser *comment.Parser printer *comment.Printer caser cases.Caser + buf strings.Builder err error } @@ -201,8 +269,7 @@ func (r *htmlRenderer) start(pkg *doc.Package) { 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 { + if _, err := r.w.Write(r.printer.HTML(r.parser.Parse(pkg.Doc))); err != nil { r.err = err } } @@ -224,17 +291,16 @@ 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) + r.buf.Reset() + err := format.Node(&r.buf, r.fset, node) if err != nil { r.err = err return } - r.printf("<pre><code>%s</code></pre>\n", html.EscapeString(buf.String())) + r.printf("<pre><code>%s</code></pre>\n", html.EscapeString(r.buf.String())) formatted := r.printer.HTML(r.parser.Parse(comment)) if len(formatted) > 0 { - _, err = r.w.Write(formatted) - if err != nil { + if _, err = r.w.Write(formatted); err != nil { r.err = err return } @@ -242,24 +308,57 @@ func (r *htmlRenderer) emit(comment string, node ast.Node) { r.printf("\n") } -func (r *htmlRenderer) printf(format string, args ...any) { +func (r *htmlRenderer) emitExample(ex *doc.Example) { if r.err != nil { return } - _, err := fmt.Fprintf(r.w, format, args...) + r.printf("<h4>Example") + if ex.Suffix != "" { + r.printf(" (%s)", ex.Suffix) + } + r.printf("</h4>\n") + r.printf("\n") + if ex.Doc != "" { + if _, err := r.w.Write(r.printer.Markdown(r.parser.Parse(ex.Doc))); err != nil { + r.err = err + return + } + r.printf("\n") + } + r.printf("<pre><code>\n") + err := format.Node(r.w, r.fset, ex.Code) if err != nil { r.err = err + return + } + r.printf("\n</code></pre>\n") + if ex.Output != "" { + r.printf("Output:\n\n<pre><code>\n%s\n</code></pre>\n", html.EscapeString(ex.Output)) + } + r.printf("\n") +} + +func (r *htmlRenderer) printf(format string, args ...any) { + if r.err != nil { + return + } + if _, err := fmt.Fprintf(r.w, format, args...); 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 { +func renderDoc(dpkg *doc.Package, r renderer, examples bool) error { r.start(dpkg) + if examples { + for _, ex := range dpkg.Examples { + r.emitExample(ex) + } + } renderValues(dpkg.Consts, r, "constants") renderValues(dpkg.Vars, r, "variables") - renderFuncs(dpkg.Funcs, r, "functions") + renderFuncs(dpkg.Funcs, r, "functions", examples) started := false for _, t := range dpkg.Types { @@ -271,10 +370,15 @@ func renderDoc(dpkg *doc.Package, r renderer) error { started = true } r.emit(t.Doc, t.Decl) + if examples { + for _, ex := range t.Examples { + r.emitExample(ex) + } + } renderValues(t.Consts, r, "") renderValues(t.Vars, r, "") - renderFuncs(t.Funcs, r, "") - renderFuncs(t.Methods, r, "") + renderFuncs(t.Funcs, r, "", examples) + renderFuncs(t.Methods, r, "", examples) } if started { r.endSection() @@ -301,7 +405,7 @@ func renderValues(vals []*doc.Value, r renderer, section string) { } } -func renderFuncs(funcs []*doc.Func, r renderer, section string) { +func renderFuncs(funcs []*doc.Func, r renderer, section string, examples bool) { started := false for _, f := range funcs { if !ast.IsExported(f.Name) { @@ -314,6 +418,11 @@ func renderFuncs(funcs []*doc.Func, r renderer, section string) { started = true } r.emit(f.Doc, f.Decl) + if examples { + for _, ex := range f.Examples { + r.emitExample(ex) + } + } } if started && section != "" { r.endSection() diff --git a/internal/api/render_test.go b/internal/api/render_test.go index 14e06d6d..fdfb8612 100644 --- a/internal/api/render_test.go +++ b/internal/api/render_test.go @@ -6,6 +6,7 @@ package api import ( "context" + "fmt" "go/parser" "go/token" "os" @@ -18,17 +19,24 @@ import ( ) func TestRenderDoc(t *testing.T) { - src, err := os.ReadFile("testdata/pkg.go") - if err != nil { - t.Fatal(err) - } + t.Run("examples", func(t *testing.T) { + testRenderDoc(t, true) + }) + t.Run("no examples", func(t *testing.T) { + testRenderDoc(t, false) + }) +} + +func testRenderDoc(t *testing.T, examples bool) { 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) + for _, file := range []string{"pkg.go", "pkg_test.go"} { + pf, err := parser.ParseFile(fset, filepath.Join("testdata", file), nil, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + docPkg.AddFile(pf, true) + } gpkg, err := docPkg.Encode(context.Background()) if err != nil { t.Fatal(err) @@ -48,17 +56,23 @@ func TestRenderDoc(t *testing.T) { 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 { + if err := renderDoc(dpkg, r, examples); err != nil { t.Fatal(err) } got := strings.TrimSpace(sb.String()) - wantBytes, err := os.ReadFile(filepath.FromSlash("testdata/" + name + ".golden")) + goldenName := name + if examples { + goldenName += "-examples" + } + goldenFile := filepath.Join("testdata", goldenName+".golden") + wantBytes, err := os.ReadFile(goldenFile) 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) + fmt.Println(got) } }) } diff --git a/internal/api/testdata/examples.go b/internal/api/testdata/examples.go new file mode 100644 index 00000000..93baaa17 --- /dev/null +++ b/internal/api/testdata/examples.go @@ -0,0 +1,14 @@ +// 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 examples + +// F is a function. +func F() {} + +// T is a type. +type T struct{} + +// M is a method. +func (T) M() {} diff --git a/internal/api/testdata/html-examples.golden b/internal/api/testdata/html-examples.golden new file mode 100644 index 00000000..fd6e0682 --- /dev/null +++ b/internal/api/testdata/html-examples.golden @@ -0,0 +1,155 @@ +<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> + +<h4>Example</h4> + +<pre><code> +{ + fmt.Println("Package example") + +} +</code></pre> +Output: + +<pre><code> +Package example + +</code></pre> + +<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. + +<h4>Example</h4> + +<pre><code> +{ + pkg.F() + fmt.Println("F example") + +} +</code></pre> +Output: + +<pre><code> +F example + +</code></pre> + +<h4>Example (second)</h4> + +<pre><code> +{ + pkg.F() + fmt.Println("F second example") + +} +</code></pre> +Output: + +<pre><code> +F second example + +</code></pre> + +<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. + +<h4>Example</h4> + +<pre><code> +{ + _ = pkg.T(0) + fmt.Println("T example") + +} +</code></pre> +Output: + +<pre><code> +T example + +</code></pre> + +<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. + +<h4>Example</h4> + +<pre><code> +{ + var t pkg.T + t.M() + fmt.Println("M example") + +} +</code></pre> +Output: + +<pre><code> +M example + +</code></pre> diff --git a/internal/api/testdata/html.golden b/internal/api/testdata/html.golden index d77b329c..dac7514b 100644 --- a/internal/api/testdata/html.golden +++ b/internal/api/testdata/html.golden @@ -72,4 +72,4 @@ They appear after their type. <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 +BUG(xxx): this verifies that notes are rendered. diff --git a/internal/api/testdata/markdown-examples.golden b/internal/api/testdata/markdown-examples.golden new file mode 100644 index 00000000..c552629b --- /dev/null +++ b/internal/api/testdata/markdown-examples.golden @@ -0,0 +1,185 @@ +# 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) + +#### Example + +```go +{ + fmt.Println("Package example") + +} +``` +Output: + +``` +Package example + +``` + +## Constants + +```go +const ( + X = 1 + Y = 2 +) +``` +Several constants. + +```go +const C = 1 +``` +C is a shorthand for 1. + +## Variables + +```go +var V = 2 +``` +V is a variable. + +## Functions + +```go +func Add(x int) int +``` +Add adds 1 to x. + +```go +func F() +``` +F is a function. + +#### Example + +```go +{ + pkg.F() + fmt.Println("F example") + +} +``` +Output: + +``` +F example + +``` + +#### Example (second) + +```go +{ + pkg.F() + fmt.Println("F second example") + +} +``` +Output: + +``` +F second example + +``` + +## Types + +```go +type A int +``` + +```go +type B bool +``` + +```go +type I1 interface { + M1() +} +``` +I1 is an interface. + +```go +type I2 interface { + I1 + M2() +} +``` + +```go +type S1 struct { + F int // field +} +``` +S1 is a struct. + +```go +type S2 struct { + S1 + G int +} +``` +S2 is another struct. + +```go +type T int +``` +T is a type. + +#### Example + +```go +{ + _ = pkg.T(0) + fmt.Println("T example") + +} +``` +Output: + +``` +T example + +``` + +```go +const CT T = 3 +``` +CT is a typed constant. They appear after their type. + +```go +func TF() T +``` +TF is a constructor for T. + +```go +func (T) M() +``` +M is a method of T. BUG(xxx): this verifies that notes are rendered. + +#### Example + +```go +{ + var t pkg.T + t.M() + fmt.Println("M example") + +} +``` +Output: + +``` +M example + +``` diff --git a/internal/api/testdata/markdown.golden b/internal/api/testdata/markdown.golden index 205546d0..8e19e5db 100644 --- a/internal/api/testdata/markdown.golden +++ b/internal/api/testdata/markdown.golden @@ -13,7 +13,7 @@ Search [Google](https://google.com) for details. ## Constants -``` +```go const ( X = 1 Y = 2 @@ -21,62 +21,62 @@ const ( ``` Several constants. -``` +```go const C = 1 ``` C is a shorthand for 1. ## Variables -``` +```go var V = 2 ``` V is a variable. ## Functions -``` +```go func Add(x int) int ``` Add adds 1 to x. -``` +```go func F() ``` F is a function. ## Types -``` +```go type A int ``` -``` +```go type B bool ``` -``` +```go type I1 interface { M1() } ``` I1 is an interface. -``` +```go type I2 interface { I1 M2() } ``` -``` +```go type S1 struct { F int // field } ``` S1 is a struct. -``` +```go type S2 struct { S1 G int @@ -84,22 +84,22 @@ type S2 struct { ``` S2 is another struct. -``` +```go type T int ``` T is a type. -``` +```go const CT T = 3 ``` CT is a typed constant. They appear after their type. -``` +```go func TF() T ``` TF is a constructor for T. -``` +```go func (T) M() ``` M is a method of T. BUG(xxx): this verifies that notes are rendered. diff --git a/internal/api/testdata/pkg_test.go b/internal/api/testdata/pkg_test.go new file mode 100644 index 00000000..4697bec4 --- /dev/null +++ b/internal/api/testdata/pkg_test.go @@ -0,0 +1,46 @@ +// 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_test + +import ( + "fmt" + + "golang.org/x/pkgsite/internal/api/testdata" +) + +func Example() { + fmt.Println("Package example") + // Output: + // Package example +} + +func ExampleF() { + pkg.F() + fmt.Println("F example") + // Output: + // F example +} + +func ExampleF_second() { + pkg.F() + fmt.Println("F second example") + // Output: + // F second example +} + +func ExampleT() { + _ = pkg.T(0) + fmt.Println("T example") + // Output: + // T example +} + +func ExampleT_M() { + var t pkg.T + t.M() + fmt.Println("M example") + // Output: + // M example +} diff --git a/internal/api/testdata/text-examples.golden b/internal/api/testdata/text-examples.golden new file mode 100644 index 00000000..c8c10435 --- /dev/null +++ b/internal/api/testdata/text-examples.golden @@ -0,0 +1,122 @@ +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 + +Example: + { + fmt.Println("Package example") + } + + Output: + Package example + +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. + +Example: + { + pkg.F() + fmt.Println("F example") + } + + Output: + F example + +Example (second): + { + pkg.F() + fmt.Println("F second example") + } + + Output: + F second example + +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. + +Example: + { + _ = pkg.T(0) + fmt.Println("T example") + } + + Output: + T example + +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. + +Example: + { + var t pkg.T + t.M() + fmt.Println("M example") + } + + Output: + M example |
