blob: 117f069b9d1868021238df74a1787658c46d2f85 [file] [log] [blame]
// Copyright 2019 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 dochtml renders Go package documentation into HTML.
//
// This package and its API are under development (see b/137567588).
// It currently relies on copies of external packages with active CLs applied.
// The plan is to iterate on the development internally for x/pkgsite
// needs first, before factoring it out somewhere non-internal where its
// API can no longer be easily modified.
package dochtml
import (
"bytes"
"errors"
"fmt"
"go/ast"
"go/printer"
"go/token"
"html/template"
pathpkg "path"
"sort"
"strings"
"golang.org/x/pkgsite/internal/fetch/dochtml/internal/render"
"golang.org/x/pkgsite/internal/fetch/internal/doc"
)
var (
// ErrTooLarge represents an error where the rendered documentation HTML
// size exceeded the specified limit. See the RenderOptions.Limit field.
ErrTooLarge = errors.New("rendered documentation HTML size exceeded the specified limit")
)
// RenderOptions are options for Render.
type RenderOptions struct {
SourceLinkFunc func(ast.Node) string
PlayURLFunc func(*doc.Example) string // If set, returns the Go playground URL for the example
Limit int64 // If zero, a default limit of 10 megabytes is used.
}
// Render renders package documentation HTML for the
// provided file set and package.
//
// If the rendered documentation HTML size exceeds the specified limit,
// an error with ErrTooLarge in its chain will be returned.
func Render(fset *token.FileSet, p *doc.Package, opt RenderOptions) (string, error) {
if opt.Limit == 0 {
const megabyte = 1000 * 1000
opt.Limit = 10 * megabyte
}
// Make a copy to avoid modifying caller's *doc.Package.
p2 := *p
p = &p2
// When rendering documentation for commands, display
// the package comment and notes, but no declarations.
if p.Name == "main" {
// Clear top-level declarations.
p.Consts = nil
p.Types = nil
p.Vars = nil
p.Funcs = nil
p.Examples = nil
}
// Remove everything from the notes section that is not a bug. This
// includes TODOs and other arbitrary notes.
for k := range p.Notes {
if k == "BUG" {
continue
}
delete(p.Notes, k)
}
r := render.New(fset, p, &render.Options{
PackageURL: func(path string) (url string) {
return pathpkg.Join("/pkg", path)
},
DisableHotlinking: true,
})
sourceLink := func(name string, node ast.Node) template.HTML {
link := opt.SourceLinkFunc(node)
if link == "" {
return template.HTML(name)
}
return template.HTML(fmt.Sprintf(`<a class="Documentation-source" href="%s">%s</a>`, link, name))
}
playURLFunc := opt.PlayURLFunc
if playURLFunc == nil {
playURLFunc = func(*doc.Example) string {
return ""
}
}
buf := &limitBuffer{
B: new(bytes.Buffer),
Remain: opt.Limit,
}
err := template.Must(htmlPackage.Clone()).Funcs(map[string]interface{}{
"render_short_synopsis": r.ShortSynopsis,
"render_synopsis": r.Synopsis,
"render_doc": r.DocHTML,
"render_decl": r.DeclHTML,
"render_code": r.CodeHTML,
"source_link": sourceLink,
"play_url": playURLFunc,
}).Execute(buf, struct {
RootURL string
*doc.Package
Examples *examples
}{
RootURL: "/pkg",
Package: p,
Examples: collectExamples(p),
})
if buf.Remain < 0 {
return "", fmt.Errorf("dochtml.Render: %w", ErrTooLarge)
} else if err != nil {
return "", fmt.Errorf("dochtml.Render: %v", err)
}
return buf.B.String(), nil
}
// examples is an internal representation of all package examples.
type examples struct {
List []*example // sorted by ParentID
Map map[string][]*example // keyed by top-level ID (e.g., "NewRing" or "PubSub.Receive") or empty string for package examples
}
// example is an internal representation of a single example.
type example struct {
*doc.Example
ID string // ID of example
ParentID string // ID of top-level declaration this example is attached to
Suffix string // optional suffix name in title case
}
// Code returns an printer.CommentedNode if ex.Comments is non-nil,
// otherwise it returns ex.Code as is.
func (ex *example) Code() interface{} {
if len(ex.Comments) > 0 {
return &printer.CommentedNode{Node: ex.Example.Code, Comments: ex.Comments}
}
return ex.Example.Code
}
// WalkExamples calls fn for each Example in p,
// setting id to the name of the parent structure.
func WalkExamples(p *doc.Package, fn func(id string, ex *doc.Example)) {
for _, ex := range p.Examples {
fn("", ex)
}
for _, f := range p.Funcs {
for _, ex := range f.Examples {
fn(f.Name, ex)
}
}
for _, t := range p.Types {
for _, ex := range t.Examples {
fn(t.Name, ex)
}
for _, f := range t.Funcs {
for _, ex := range f.Examples {
fn(f.Name, ex)
}
}
for _, m := range t.Methods {
for _, ex := range m.Examples {
fn(t.Name+"."+m.Name, ex)
}
}
}
}
// collectExamples extracts examples from p
// into the internal examples representation.
func collectExamples(p *doc.Package) *examples {
exs := &examples{
List: nil,
Map: make(map[string][]*example),
}
WalkExamples(p, func(id string, ex *doc.Example) {
suffix := strings.Title(ex.Suffix)
ex0 := &example{
Example: ex,
ID: exampleID(id, suffix),
ParentID: id,
Suffix: suffix,
}
exs.List = append(exs.List, ex0)
exs.Map[id] = append(exs.Map[id], ex0)
})
sort.SliceStable(exs.List, func(i, j int) bool {
// TODO: Break ties by sorting by suffix, unless
// not needed because of upstream slice order.
return exs.List[i].ParentID < exs.List[j].ParentID
})
return exs
}
func exampleID(id, suffix string) string {
switch {
case id == "" && suffix == "":
return "example-package"
case id == "" && suffix != "":
return "example-package-" + suffix
case id != "" && suffix == "":
return "example-" + id
case id != "" && suffix != "":
return "example-" + id + "-" + suffix
default:
panic("unreachable")
}
}