| // 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") |
| } |
| } |