blob: 4001f6ca7e29f7445f8fb9c242757ef0ebefb529 [file]
// 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()
}
}