| // 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" |
| "html" |
| "io" |
| "slices" |
| "strings" |
| |
| "golang.org/x/text/cases" |
| "golang.org/x/text/language" |
| ) |
| |
| // 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 starts 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) |
| |
| // emitExample prints an example. |
| emitExample(ex *doc.Example) |
| } |
| |
| type commonRenderer struct { |
| fset *token.FileSet |
| w io.Writer |
| err error |
| } |
| |
| func (r *commonRenderer) printf(format string, args ...any) { |
| if r.err != nil { |
| return |
| } |
| if _, err := fmt.Fprintf(r.w, format, args...); err != nil { |
| r.err = err |
| } |
| } |
| |
| func (r *commonRenderer) end() error { return r.err } |
| |
| type textRenderer struct { |
| commonRenderer |
| pkg *doc.Package |
| parser *comment.Parser |
| printer *comment.Printer |
| } |
| |
| func newTextRenderer(fset *token.FileSet, w io.Writer) *textRenderer { |
| return &textRenderer{ |
| commonRenderer: commonRenderer{fset: fset, w: w}, |
| } |
| } |
| |
| 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. |
| if _, err := r.w.Write(pkg.Text(pkg.Doc)); err != nil { |
| r.err = err |
| } |
| } |
| r.printf("\n") |
| } |
| |
| 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 { |
| if _, err = r.w.Write(formatted); err != nil { |
| r.err = err |
| return |
| } |
| } |
| 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") |
| } |
| |
| type markdownRenderer struct { |
| commonRenderer |
| pkg *doc.Package |
| parser *comment.Parser |
| printer *comment.Printer |
| caser cases.Caser |
| } |
| |
| func newMarkdownRenderer(fset *token.FileSet, w io.Writer) *markdownRenderer { |
| return &markdownRenderer{ |
| commonRenderer: commonRenderer{fset: fset, w: w}, |
| } |
| } |
| |
| 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") |
| if _, err := r.w.Write(r.printer.Markdown(r.parser.Parse(pkg.Doc))); err != nil { |
| r.err = err |
| } |
| } |
| r.printf("\n") |
| } |
| |
| 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("```go\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 { |
| if _, err = r.w.Write(formatted); err != nil { |
| r.err = err |
| return |
| } |
| } |
| r.printf("\n") |
| } |
| |
| func (r *markdownRenderer) emitExample(ex *doc.Example) { |
| if r.err != nil { |
| return |
| } |
| 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") |
| } |
| |
| type htmlRenderer struct { |
| commonRenderer |
| pkg *doc.Package |
| parser *comment.Parser |
| printer *comment.Printer |
| caser cases.Caser |
| buf strings.Builder |
| } |
| |
| func newHTMLRenderer(fset *token.FileSet, w io.Writer) *htmlRenderer { |
| return &htmlRenderer{ |
| commonRenderer: commonRenderer{fset: fset, w: w}, |
| } |
| } |
| |
| 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") |
| if _, err := r.w.Write(r.printer.HTML(r.parser.Parse(pkg.Doc))); err != nil { |
| r.err = err |
| } |
| } |
| r.printf("\n") |
| } |
| |
| 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 |
| } |
| 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(r.buf.String())) |
| formatted := r.printer.HTML(r.parser.Parse(comment)) |
| if len(formatted) > 0 { |
| if _, err = r.w.Write(formatted); err != nil { |
| r.err = err |
| return |
| } |
| } |
| r.printf("\n") |
| } |
| |
| func (r *htmlRenderer) emitExample(ex *doc.Example) { |
| if r.err != nil { |
| return |
| } |
| 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") |
| } |
| |
| // renderDoc renders the documentation for dpkg using the given renderer. |
| 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", examples) |
| |
| 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) |
| if examples { |
| for _, ex := range t.Examples { |
| r.emitExample(ex) |
| } |
| } |
| renderValues(t.Consts, r, "") |
| renderValues(t.Vars, r, "") |
| renderFuncs(t.Funcs, r, "", examples) |
| renderFuncs(t.Methods, r, "", examples) |
| } |
| 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, examples bool) { |
| 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 examples { |
| for _, ex := range f.Examples { |
| r.emitExample(ex) |
| } |
| } |
| } |
| if started && section != "" { |
| r.endSection() |
| } |
| } |