mirror of
https://github.com/golang/go
synced 2024-11-18 14:04:45 -07:00
1da8801a95
At least inside this one package. It's because golang.org/x/tools/cmd/godoc no longer has CLI support as of golang/go#25443, it is now only a web server. Luckily, browsers that display HTML pages are not constrained to the fixed width of punched cards. On the contrary, they seem to embrace a wide variety of arbitrary page widths, ranging from narrow to wide. This is in part due to the proliferation of internet access on device form factors such as the mobile phone, tablet, laptop, and desktop. So the punchCardWidth constant is now unused and can be removed. This is a followup to CL 141397 that removed most of the CLI support code, including the function comment_textFunc, which was its only user. Same goes for containsOnlySpace. Updates golang/go#25443 Change-Id: I61fabe6ea801c88758fb2c6aefa70b53d52e2cb5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/172975 Run-TryBot: Dmitri Shuralyov <dmitshur@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
942 lines
26 KiB
Go
942 lines
26 KiB
Go
// Copyright 2013 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 godoc is a work-in-progress (2013-07-17) package to
|
|
// begin splitting up the godoc binary into multiple pieces.
|
|
//
|
|
// This package comment will evolve over time as this package splits
|
|
// into smaller pieces.
|
|
package godoc // import "golang.org/x/tools/godoc"
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/doc"
|
|
"go/format"
|
|
"go/printer"
|
|
"go/token"
|
|
htmltemplate "html/template"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
pathpkg "path"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// Fake relative package path for built-ins. Documentation for all globals
|
|
// (not just exported ones) will be shown for packages in this directory,
|
|
// and there will be no association of consts, vars, and factory functions
|
|
// with types (see issue 6645).
|
|
const builtinPkgPath = "builtin"
|
|
|
|
// FuncMap defines template functions used in godoc templates.
|
|
//
|
|
// Convention: template function names ending in "_html" or "_url" produce
|
|
// HTML- or URL-escaped strings; all other function results may
|
|
// require explicit escaping in the template.
|
|
func (p *Presentation) FuncMap() template.FuncMap {
|
|
p.initFuncMapOnce.Do(p.initFuncMap)
|
|
return p.funcMap
|
|
}
|
|
|
|
func (p *Presentation) TemplateFuncs() template.FuncMap {
|
|
p.initFuncMapOnce.Do(p.initFuncMap)
|
|
return p.templateFuncs
|
|
}
|
|
|
|
func (p *Presentation) initFuncMap() {
|
|
if p.Corpus == nil {
|
|
panic("nil Presentation.Corpus")
|
|
}
|
|
p.templateFuncs = template.FuncMap{
|
|
"code": p.code,
|
|
}
|
|
p.funcMap = template.FuncMap{
|
|
// various helpers
|
|
"filename": filenameFunc,
|
|
"repeat": strings.Repeat,
|
|
"since": p.Corpus.pkgAPIInfo.sinceVersionFunc,
|
|
|
|
// access to FileInfos (directory listings)
|
|
"fileInfoName": fileInfoNameFunc,
|
|
"fileInfoTime": fileInfoTimeFunc,
|
|
|
|
// access to search result information
|
|
"infoKind_html": infoKind_htmlFunc,
|
|
"infoLine": p.infoLineFunc,
|
|
"infoSnippet_html": p.infoSnippet_htmlFunc,
|
|
|
|
// formatting of AST nodes
|
|
"node": p.nodeFunc,
|
|
"node_html": p.node_htmlFunc,
|
|
"comment_html": comment_htmlFunc,
|
|
"sanitize": sanitizeFunc,
|
|
|
|
// support for URL attributes
|
|
"pkgLink": pkgLinkFunc,
|
|
"srcLink": srcLinkFunc,
|
|
"posLink_url": newPosLink_urlFunc(srcPosLinkFunc),
|
|
"docLink": docLinkFunc,
|
|
"queryLink": queryLinkFunc,
|
|
"srcBreadcrumb": srcBreadcrumbFunc,
|
|
"srcToPkgLink": srcToPkgLinkFunc,
|
|
|
|
// formatting of Examples
|
|
"example_html": p.example_htmlFunc,
|
|
"example_name": p.example_nameFunc,
|
|
"example_suffix": p.example_suffixFunc,
|
|
|
|
// formatting of analysis information
|
|
"callgraph_html": p.callgraph_htmlFunc,
|
|
"implements_html": p.implements_htmlFunc,
|
|
"methodset_html": p.methodset_htmlFunc,
|
|
|
|
// formatting of Notes
|
|
"noteTitle": noteTitle,
|
|
|
|
// Number operation
|
|
"multiply": multiply,
|
|
|
|
// formatting of PageInfoMode query string
|
|
"modeQueryString": modeQueryString,
|
|
|
|
// check whether to display third party section or not
|
|
"hasThirdParty": hasThirdParty,
|
|
|
|
// get the no. of columns to split the toc in search page
|
|
"tocColCount": tocColCount,
|
|
}
|
|
if p.URLForSrc != nil {
|
|
p.funcMap["srcLink"] = p.URLForSrc
|
|
}
|
|
if p.URLForSrcPos != nil {
|
|
p.funcMap["posLink_url"] = newPosLink_urlFunc(p.URLForSrcPos)
|
|
}
|
|
if p.URLForSrcQuery != nil {
|
|
p.funcMap["queryLink"] = p.URLForSrcQuery
|
|
}
|
|
}
|
|
|
|
func multiply(a, b int) int { return a * b }
|
|
|
|
func filenameFunc(path string) string {
|
|
_, localname := pathpkg.Split(path)
|
|
return localname
|
|
}
|
|
|
|
func fileInfoNameFunc(fi os.FileInfo) string {
|
|
name := fi.Name()
|
|
if fi.IsDir() {
|
|
name += "/"
|
|
}
|
|
return name
|
|
}
|
|
|
|
func fileInfoTimeFunc(fi os.FileInfo) string {
|
|
if t := fi.ModTime(); t.Unix() != 0 {
|
|
return t.Local().String()
|
|
}
|
|
return "" // don't return epoch if time is obviously not set
|
|
}
|
|
|
|
// The strings in infoKinds must be properly html-escaped.
|
|
var infoKinds = [nKinds]string{
|
|
PackageClause: "package clause",
|
|
ImportDecl: "import decl",
|
|
ConstDecl: "const decl",
|
|
TypeDecl: "type decl",
|
|
VarDecl: "var decl",
|
|
FuncDecl: "func decl",
|
|
MethodDecl: "method decl",
|
|
Use: "use",
|
|
}
|
|
|
|
func infoKind_htmlFunc(info SpotInfo) string {
|
|
return infoKinds[info.Kind()] // infoKind entries are html-escaped
|
|
}
|
|
|
|
func (p *Presentation) infoLineFunc(info SpotInfo) int {
|
|
line := info.Lori()
|
|
if info.IsIndex() {
|
|
index, _ := p.Corpus.searchIndex.Get()
|
|
if index != nil {
|
|
line = index.(*Index).Snippet(line).Line
|
|
} else {
|
|
// no line information available because
|
|
// we don't have an index - this should
|
|
// never happen; be conservative and don't
|
|
// crash
|
|
line = 0
|
|
}
|
|
}
|
|
return line
|
|
}
|
|
|
|
func (p *Presentation) infoSnippet_htmlFunc(info SpotInfo) string {
|
|
if info.IsIndex() {
|
|
index, _ := p.Corpus.searchIndex.Get()
|
|
// Snippet.Text was HTML-escaped when it was generated
|
|
return index.(*Index).Snippet(info.Lori()).Text
|
|
}
|
|
return `<span class="alert">no snippet text available</span>`
|
|
}
|
|
|
|
func (p *Presentation) nodeFunc(info *PageInfo, node interface{}) string {
|
|
var buf bytes.Buffer
|
|
p.writeNode(&buf, info, info.FSet, node)
|
|
return buf.String()
|
|
}
|
|
|
|
func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string {
|
|
var buf1 bytes.Buffer
|
|
p.writeNode(&buf1, info, info.FSet, node)
|
|
|
|
var buf2 bytes.Buffer
|
|
if n, _ := node.(ast.Node); n != nil && linkify && p.DeclLinks {
|
|
LinkifyText(&buf2, buf1.Bytes(), n)
|
|
if st, name := isStructTypeDecl(n); st != nil {
|
|
addStructFieldIDAttributes(&buf2, name, st)
|
|
}
|
|
} else {
|
|
FormatText(&buf2, buf1.Bytes(), -1, true, "", nil)
|
|
}
|
|
|
|
return buf2.String()
|
|
}
|
|
|
|
// isStructTypeDecl checks whether n is a struct declaration.
|
|
// It either returns a non-nil StructType and its name, or zero values.
|
|
func isStructTypeDecl(n ast.Node) (st *ast.StructType, name string) {
|
|
gd, ok := n.(*ast.GenDecl)
|
|
if !ok || gd.Tok != token.TYPE {
|
|
return nil, ""
|
|
}
|
|
if gd.Lparen > 0 {
|
|
// Parenthesized type. Who does that, anyway?
|
|
// TODO: Reportedly gri does. Fix this to handle that too.
|
|
return nil, ""
|
|
}
|
|
if len(gd.Specs) != 1 {
|
|
return nil, ""
|
|
}
|
|
ts, ok := gd.Specs[0].(*ast.TypeSpec)
|
|
if !ok {
|
|
return nil, ""
|
|
}
|
|
st, ok = ts.Type.(*ast.StructType)
|
|
if !ok {
|
|
return nil, ""
|
|
}
|
|
return st, ts.Name.Name
|
|
}
|
|
|
|
// addStructFieldIDAttributes modifies the contents of buf such that
|
|
// all struct fields of the named struct have <span id='name.Field'>
|
|
// in them, so people can link to /#Struct.Field.
|
|
func addStructFieldIDAttributes(buf *bytes.Buffer, name string, st *ast.StructType) {
|
|
if st.Fields == nil {
|
|
return
|
|
}
|
|
// needsLink is a set of identifiers that still need to be
|
|
// linked, where value == key, to avoid an allocation in func
|
|
// linkedField.
|
|
needsLink := make(map[string]string)
|
|
|
|
for _, f := range st.Fields.List {
|
|
if len(f.Names) == 0 {
|
|
continue
|
|
}
|
|
fieldName := f.Names[0].Name
|
|
needsLink[fieldName] = fieldName
|
|
}
|
|
var newBuf bytes.Buffer
|
|
foreachLine(buf.Bytes(), func(line []byte) {
|
|
if fieldName := linkedField(line, needsLink); fieldName != "" {
|
|
fmt.Fprintf(&newBuf, `<span id="%s.%s"></span>`, name, fieldName)
|
|
delete(needsLink, fieldName)
|
|
}
|
|
newBuf.Write(line)
|
|
})
|
|
buf.Reset()
|
|
buf.Write(newBuf.Bytes())
|
|
}
|
|
|
|
// foreachLine calls fn for each line of in, where a line includes
|
|
// the trailing "\n", except on the last line, if it doesn't exist.
|
|
func foreachLine(in []byte, fn func(line []byte)) {
|
|
for len(in) > 0 {
|
|
nl := bytes.IndexByte(in, '\n')
|
|
if nl == -1 {
|
|
fn(in)
|
|
return
|
|
}
|
|
fn(in[:nl+1])
|
|
in = in[nl+1:]
|
|
}
|
|
}
|
|
|
|
// commentPrefix is the line prefix for comments after they've been HTMLified.
|
|
var commentPrefix = []byte(`<span class="comment">// `)
|
|
|
|
// linkedField determines whether the given line starts with an
|
|
// identifer in the provided ids map (mapping from identifier to the
|
|
// same identifier). The line can start with either an identifier or
|
|
// an identifier in a comment. If one matches, it returns the
|
|
// identifier that matched. Otherwise it returns the empty string.
|
|
func linkedField(line []byte, ids map[string]string) string {
|
|
line = bytes.TrimSpace(line)
|
|
|
|
// For fields with a doc string of the
|
|
// conventional form, we put the new span into
|
|
// the comment instead of the field.
|
|
// The "conventional" form is a complete sentence
|
|
// per https://golang.org/s/style#comment-sentences like:
|
|
//
|
|
// // Foo is an optional Fooer to foo the foos.
|
|
// Foo Fooer
|
|
//
|
|
// In this case, we want the #StructName.Foo
|
|
// link to make the browser go to the comment
|
|
// line "Foo is an optional Fooer" instead of
|
|
// the "Foo Fooer" line, which could otherwise
|
|
// obscure the docs above the browser's "fold".
|
|
//
|
|
// TODO: do this better, so it works for all
|
|
// comments, including unconventional ones.
|
|
if bytes.HasPrefix(line, commentPrefix) {
|
|
line = line[len(commentPrefix):]
|
|
}
|
|
id := scanIdentifier(line)
|
|
if len(id) == 0 {
|
|
// No leading identifier. Avoid map lookup for
|
|
// somewhat common case.
|
|
return ""
|
|
}
|
|
return ids[string(id)]
|
|
}
|
|
|
|
// scanIdentifier scans a valid Go identifier off the front of v and
|
|
// either returns a subslice of v if there's a valid identifier, or
|
|
// returns a zero-length slice.
|
|
func scanIdentifier(v []byte) []byte {
|
|
var n int // number of leading bytes of v belonging to an identifier
|
|
for {
|
|
r, width := utf8.DecodeRune(v[n:])
|
|
if !(isLetter(r) || n > 0 && isDigit(r)) {
|
|
break
|
|
}
|
|
n += width
|
|
}
|
|
return v[:n]
|
|
}
|
|
|
|
func isLetter(ch rune) bool {
|
|
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= utf8.RuneSelf && unicode.IsLetter(ch)
|
|
}
|
|
|
|
func isDigit(ch rune) bool {
|
|
return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch)
|
|
}
|
|
|
|
func comment_htmlFunc(comment string) string {
|
|
var buf bytes.Buffer
|
|
// TODO(gri) Provide list of words (e.g. function parameters)
|
|
// to be emphasized by ToHTML.
|
|
doc.ToHTML(&buf, comment, nil) // does html-escaping
|
|
return buf.String()
|
|
}
|
|
|
|
// sanitizeFunc sanitizes the argument src by replacing newlines with
|
|
// blanks, removing extra blanks, and by removing trailing whitespace
|
|
// and commas before closing parentheses.
|
|
func sanitizeFunc(src string) string {
|
|
buf := make([]byte, len(src))
|
|
j := 0 // buf index
|
|
comma := -1 // comma index if >= 0
|
|
for i := 0; i < len(src); i++ {
|
|
ch := src[i]
|
|
switch ch {
|
|
case '\t', '\n', ' ':
|
|
// ignore whitespace at the beginning, after a blank, or after opening parentheses
|
|
if j == 0 {
|
|
continue
|
|
}
|
|
if p := buf[j-1]; p == ' ' || p == '(' || p == '{' || p == '[' {
|
|
continue
|
|
}
|
|
// replace all whitespace with blanks
|
|
ch = ' '
|
|
case ',':
|
|
comma = j
|
|
case ')', '}', ']':
|
|
// remove any trailing comma
|
|
if comma >= 0 {
|
|
j = comma
|
|
}
|
|
// remove any trailing whitespace
|
|
if j > 0 && buf[j-1] == ' ' {
|
|
j--
|
|
}
|
|
default:
|
|
comma = -1
|
|
}
|
|
buf[j] = ch
|
|
j++
|
|
}
|
|
// remove trailing blank, if any
|
|
if j > 0 && buf[j-1] == ' ' {
|
|
j--
|
|
}
|
|
return string(buf[:j])
|
|
}
|
|
|
|
type PageInfo struct {
|
|
Dirname string // directory containing the package
|
|
Err error // error or nil
|
|
GoogleCN bool // page is being served from golang.google.cn
|
|
|
|
Mode PageInfoMode // display metadata from query string
|
|
|
|
// package info
|
|
FSet *token.FileSet // nil if no package documentation
|
|
PDoc *doc.Package // nil if no package documentation
|
|
Examples []*doc.Example // nil if no example code
|
|
Notes map[string][]*doc.Note // nil if no package Notes
|
|
PAst map[string]*ast.File // nil if no AST with package exports
|
|
IsMain bool // true for package main
|
|
IsFiltered bool // true if results were filtered
|
|
|
|
// analysis info
|
|
TypeInfoIndex map[string]int // index of JSON datum for type T (if -analysis=type)
|
|
AnalysisData htmltemplate.JS // array of TypeInfoJSON values
|
|
CallGraph htmltemplate.JS // array of PCGNodeJSON values (if -analysis=pointer)
|
|
CallGraphIndex map[string]int // maps func name to index in CallGraph
|
|
|
|
// directory info
|
|
Dirs *DirList // nil if no directory information
|
|
DirTime time.Time // directory time stamp
|
|
DirFlat bool // if set, show directory in a flat (non-indented) manner
|
|
}
|
|
|
|
func (info *PageInfo) IsEmpty() bool {
|
|
return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil
|
|
}
|
|
|
|
func pkgLinkFunc(path string) string {
|
|
// because of the irregular mapping under goroot
|
|
// we need to correct certain relative paths
|
|
path = strings.TrimPrefix(path, "/")
|
|
path = strings.TrimPrefix(path, "src/")
|
|
path = strings.TrimPrefix(path, "pkg/")
|
|
return "pkg/" + path
|
|
}
|
|
|
|
// srcToPkgLinkFunc builds an <a> tag linking to the package
|
|
// documentation of relpath.
|
|
func srcToPkgLinkFunc(relpath string) string {
|
|
relpath = pkgLinkFunc(relpath)
|
|
relpath = pathpkg.Dir(relpath)
|
|
if relpath == "pkg" {
|
|
return `<a href="/pkg">Index</a>`
|
|
}
|
|
return fmt.Sprintf(`<a href="/%s">%s</a>`, relpath, relpath[len("pkg/"):])
|
|
}
|
|
|
|
// srcBreadcrumbFun converts each segment of relpath to a HTML <a>.
|
|
// Each segment links to its corresponding src directories.
|
|
func srcBreadcrumbFunc(relpath string) string {
|
|
segments := strings.Split(relpath, "/")
|
|
var buf bytes.Buffer
|
|
var selectedSegment string
|
|
var selectedIndex int
|
|
|
|
if strings.HasSuffix(relpath, "/") {
|
|
// relpath is a directory ending with a "/".
|
|
// Selected segment is the segment before the last slash.
|
|
selectedIndex = len(segments) - 2
|
|
selectedSegment = segments[selectedIndex] + "/"
|
|
} else {
|
|
selectedIndex = len(segments) - 1
|
|
selectedSegment = segments[selectedIndex]
|
|
}
|
|
|
|
for i := range segments[:selectedIndex] {
|
|
buf.WriteString(fmt.Sprintf(`<a href="/%s">%s</a>/`,
|
|
strings.Join(segments[:i+1], "/"),
|
|
segments[i],
|
|
))
|
|
}
|
|
|
|
buf.WriteString(`<span class="text-muted">`)
|
|
buf.WriteString(selectedSegment)
|
|
buf.WriteString(`</span>`)
|
|
return buf.String()
|
|
}
|
|
|
|
func newPosLink_urlFunc(srcPosLinkFunc func(s string, line, low, high int) string) func(info *PageInfo, n interface{}) string {
|
|
// n must be an ast.Node or a *doc.Note
|
|
return func(info *PageInfo, n interface{}) string {
|
|
var pos, end token.Pos
|
|
|
|
switch n := n.(type) {
|
|
case ast.Node:
|
|
pos = n.Pos()
|
|
end = n.End()
|
|
case *doc.Note:
|
|
pos = n.Pos
|
|
end = n.End
|
|
default:
|
|
panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n))
|
|
}
|
|
|
|
var relpath string
|
|
var line int
|
|
var low, high int // selection offset range
|
|
|
|
if pos.IsValid() {
|
|
p := info.FSet.Position(pos)
|
|
relpath = p.Filename
|
|
line = p.Line
|
|
low = p.Offset
|
|
}
|
|
if end.IsValid() {
|
|
high = info.FSet.Position(end).Offset
|
|
}
|
|
|
|
return srcPosLinkFunc(relpath, line, low, high)
|
|
}
|
|
}
|
|
|
|
func srcPosLinkFunc(s string, line, low, high int) string {
|
|
s = srcLinkFunc(s)
|
|
var buf bytes.Buffer
|
|
template.HTMLEscape(&buf, []byte(s))
|
|
// selection ranges are of form "s=low:high"
|
|
if low < high {
|
|
fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping
|
|
// if we have a selection, position the page
|
|
// such that the selection is a bit below the top
|
|
line -= 10
|
|
if line < 1 {
|
|
line = 1
|
|
}
|
|
}
|
|
// line id's in html-printed source are of the
|
|
// form "L%d" where %d stands for the line number
|
|
if line > 0 {
|
|
fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func srcLinkFunc(s string) string {
|
|
s = pathpkg.Clean("/" + s)
|
|
if !strings.HasPrefix(s, "/src/") {
|
|
s = "/src" + s
|
|
}
|
|
return s
|
|
}
|
|
|
|
// queryLinkFunc returns a URL for a line in a source file with a highlighted
|
|
// query term.
|
|
// s is expected to be a path to a source file.
|
|
// query is expected to be a string that has already been appropriately escaped
|
|
// for use in a URL query.
|
|
func queryLinkFunc(s, query string, line int) string {
|
|
url := pathpkg.Clean("/"+s) + "?h=" + query
|
|
if line > 0 {
|
|
url += "#L" + strconv.Itoa(line)
|
|
}
|
|
return url
|
|
}
|
|
|
|
func docLinkFunc(s string, ident string) string {
|
|
return pathpkg.Clean("/pkg/"+s) + "/#" + ident
|
|
}
|
|
|
|
func (p *Presentation) example_htmlFunc(info *PageInfo, funcName string) string {
|
|
var buf bytes.Buffer
|
|
for _, eg := range info.Examples {
|
|
name := stripExampleSuffix(eg.Name)
|
|
|
|
if name != funcName {
|
|
continue
|
|
}
|
|
|
|
// print code
|
|
cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments}
|
|
code := p.node_htmlFunc(info, cnode, true)
|
|
out := eg.Output
|
|
wholeFile := true
|
|
|
|
// Additional formatting if this is a function body.
|
|
if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' {
|
|
wholeFile = false
|
|
// remove surrounding braces
|
|
code = code[1 : n-1]
|
|
// unindent
|
|
code = replaceLeadingIndentation(code, strings.Repeat(" ", p.TabWidth), "")
|
|
// remove output comment
|
|
if loc := exampleOutputRx.FindStringIndex(code); loc != nil {
|
|
code = strings.TrimSpace(code[:loc[0]])
|
|
}
|
|
}
|
|
|
|
// Write out the playground code in standard Go style
|
|
// (use tabs, no comment highlight, etc).
|
|
play := ""
|
|
if eg.Play != nil && p.ShowPlayground {
|
|
var buf bytes.Buffer
|
|
eg.Play.Comments = filterOutBuildAnnotations(eg.Play.Comments)
|
|
if err := format.Node(&buf, info.FSet, eg.Play); err != nil {
|
|
log.Print(err)
|
|
} else {
|
|
play = buf.String()
|
|
}
|
|
}
|
|
|
|
// Drop output, as the output comment will appear in the code.
|
|
if wholeFile && play == "" {
|
|
out = ""
|
|
}
|
|
|
|
if p.ExampleHTML == nil {
|
|
out = ""
|
|
return ""
|
|
}
|
|
|
|
err := p.ExampleHTML.Execute(&buf, struct {
|
|
Name, Doc, Code, Play, Output string
|
|
GoogleCN bool
|
|
}{eg.Name, eg.Doc, code, play, out, info.GoogleCN})
|
|
if err != nil {
|
|
log.Print(err)
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func filterOutBuildAnnotations(cg []*ast.CommentGroup) []*ast.CommentGroup {
|
|
if len(cg) == 0 {
|
|
return cg
|
|
}
|
|
|
|
for i := range cg {
|
|
if !strings.HasPrefix(cg[i].Text(), "+build ") {
|
|
// Found the first non-build tag, return from here until the end
|
|
// of the slice.
|
|
return cg[i:]
|
|
}
|
|
}
|
|
|
|
// There weren't any non-build tags, return an empty slice.
|
|
return []*ast.CommentGroup{}
|
|
}
|
|
|
|
// example_nameFunc takes an example function name and returns its display
|
|
// name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)".
|
|
func (p *Presentation) example_nameFunc(s string) string {
|
|
name, suffix := splitExampleName(s)
|
|
// replace _ with . for method names
|
|
name = strings.Replace(name, "_", ".", 1)
|
|
// use "Package" if no name provided
|
|
if name == "" {
|
|
name = "Package"
|
|
}
|
|
return name + suffix
|
|
}
|
|
|
|
// example_suffixFunc takes an example function name and returns its suffix in
|
|
// parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)".
|
|
func (p *Presentation) example_suffixFunc(name string) string {
|
|
_, suffix := splitExampleName(name)
|
|
return suffix
|
|
}
|
|
|
|
// implements_html returns the "> Implements" toggle for a package-level named type.
|
|
// Its contents are populated from JSON data by client-side JS at load time.
|
|
func (p *Presentation) implements_htmlFunc(info *PageInfo, typeName string) string {
|
|
if p.ImplementsHTML == nil {
|
|
return ""
|
|
}
|
|
index, ok := info.TypeInfoIndex[typeName]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
var buf bytes.Buffer
|
|
err := p.ImplementsHTML.Execute(&buf, struct{ Index int }{index})
|
|
if err != nil {
|
|
log.Print(err)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// methodset_html returns the "> Method set" toggle for a package-level named type.
|
|
// Its contents are populated from JSON data by client-side JS at load time.
|
|
func (p *Presentation) methodset_htmlFunc(info *PageInfo, typeName string) string {
|
|
if p.MethodSetHTML == nil {
|
|
return ""
|
|
}
|
|
index, ok := info.TypeInfoIndex[typeName]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
var buf bytes.Buffer
|
|
err := p.MethodSetHTML.Execute(&buf, struct{ Index int }{index})
|
|
if err != nil {
|
|
log.Print(err)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// callgraph_html returns the "> Call graph" toggle for a package-level func.
|
|
// Its contents are populated from JSON data by client-side JS at load time.
|
|
func (p *Presentation) callgraph_htmlFunc(info *PageInfo, recv, name string) string {
|
|
if p.CallGraphHTML == nil {
|
|
return ""
|
|
}
|
|
if recv != "" {
|
|
// Format must match (*ssa.Function).RelString().
|
|
name = fmt.Sprintf("(%s).%s", recv, name)
|
|
}
|
|
index, ok := info.CallGraphIndex[name]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
var buf bytes.Buffer
|
|
err := p.CallGraphHTML.Execute(&buf, struct{ Index int }{index})
|
|
if err != nil {
|
|
log.Print(err)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func noteTitle(note string) string {
|
|
return strings.Title(strings.ToLower(note))
|
|
}
|
|
|
|
func startsWithUppercase(s string) bool {
|
|
r, _ := utf8.DecodeRuneInString(s)
|
|
return unicode.IsUpper(r)
|
|
}
|
|
|
|
var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*(unordered )?output:`)
|
|
|
|
// stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name
|
|
// while keeping uppercase Braz in Foo_Braz.
|
|
func stripExampleSuffix(name string) string {
|
|
if i := strings.LastIndex(name, "_"); i != -1 {
|
|
if i < len(name)-1 && !startsWithUppercase(name[i+1:]) {
|
|
name = name[:i]
|
|
}
|
|
}
|
|
return name
|
|
}
|
|
|
|
func splitExampleName(s string) (name, suffix string) {
|
|
i := strings.LastIndex(s, "_")
|
|
if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) {
|
|
name = s[:i]
|
|
suffix = " (" + strings.Title(s[i+1:]) + ")"
|
|
return
|
|
}
|
|
name = s
|
|
return
|
|
}
|
|
|
|
// replaceLeadingIndentation replaces oldIndent at the beginning of each line
|
|
// with newIndent. This is used for formatting examples. Raw strings that
|
|
// span multiple lines are handled specially: oldIndent is not removed (since
|
|
// go/printer will not add any indentation there), but newIndent is added
|
|
// (since we may still want leading indentation).
|
|
func replaceLeadingIndentation(body, oldIndent, newIndent string) string {
|
|
// Handle indent at the beginning of the first line. After this, we handle
|
|
// indentation only after a newline.
|
|
var buf bytes.Buffer
|
|
if strings.HasPrefix(body, oldIndent) {
|
|
buf.WriteString(newIndent)
|
|
body = body[len(oldIndent):]
|
|
}
|
|
|
|
// Use a state machine to keep track of whether we're in a string or
|
|
// rune literal while we process the rest of the code.
|
|
const (
|
|
codeState = iota
|
|
runeState
|
|
interpretedStringState
|
|
rawStringState
|
|
)
|
|
searchChars := []string{
|
|
"'\"`\n", // codeState
|
|
`\'`, // runeState
|
|
`\"`, // interpretedStringState
|
|
"`\n", // rawStringState
|
|
// newlineState does not need to search
|
|
}
|
|
state := codeState
|
|
for {
|
|
i := strings.IndexAny(body, searchChars[state])
|
|
if i < 0 {
|
|
buf.WriteString(body)
|
|
break
|
|
}
|
|
c := body[i]
|
|
buf.WriteString(body[:i+1])
|
|
body = body[i+1:]
|
|
switch state {
|
|
case codeState:
|
|
switch c {
|
|
case '\'':
|
|
state = runeState
|
|
case '"':
|
|
state = interpretedStringState
|
|
case '`':
|
|
state = rawStringState
|
|
case '\n':
|
|
if strings.HasPrefix(body, oldIndent) {
|
|
buf.WriteString(newIndent)
|
|
body = body[len(oldIndent):]
|
|
}
|
|
}
|
|
|
|
case runeState:
|
|
switch c {
|
|
case '\\':
|
|
r, size := utf8.DecodeRuneInString(body)
|
|
buf.WriteRune(r)
|
|
body = body[size:]
|
|
case '\'':
|
|
state = codeState
|
|
}
|
|
|
|
case interpretedStringState:
|
|
switch c {
|
|
case '\\':
|
|
r, size := utf8.DecodeRuneInString(body)
|
|
buf.WriteRune(r)
|
|
body = body[size:]
|
|
case '"':
|
|
state = codeState
|
|
}
|
|
|
|
case rawStringState:
|
|
switch c {
|
|
case '`':
|
|
state = codeState
|
|
case '\n':
|
|
buf.WriteString(newIndent)
|
|
}
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// writeNode writes the AST node x to w.
|
|
//
|
|
// The provided fset must be non-nil. The pageInfo is optional. If
|
|
// present, the pageInfo is used to add comments to struct fields to
|
|
// say which version of Go introduced them.
|
|
func (p *Presentation) writeNode(w io.Writer, pageInfo *PageInfo, fset *token.FileSet, x interface{}) {
|
|
// convert trailing tabs into spaces using a tconv filter
|
|
// to ensure a good outcome in most browsers (there may still
|
|
// be tabs in comments and strings, but converting those into
|
|
// the right number of spaces is much harder)
|
|
//
|
|
// TODO(gri) rethink printer flags - perhaps tconv can be eliminated
|
|
// with an another printer mode (which is more efficiently
|
|
// implemented in the printer than here with another layer)
|
|
|
|
var pkgName, structName string
|
|
var apiInfo pkgAPIVersions
|
|
if gd, ok := x.(*ast.GenDecl); ok && pageInfo != nil && pageInfo.PDoc != nil &&
|
|
p.Corpus != nil &&
|
|
gd.Tok == token.TYPE && len(gd.Specs) != 0 {
|
|
pkgName = pageInfo.PDoc.ImportPath
|
|
if ts, ok := gd.Specs[0].(*ast.TypeSpec); ok {
|
|
if _, ok := ts.Type.(*ast.StructType); ok {
|
|
structName = ts.Name.Name
|
|
}
|
|
}
|
|
apiInfo = p.Corpus.pkgAPIInfo[pkgName]
|
|
}
|
|
|
|
var out = w
|
|
var buf bytes.Buffer
|
|
if structName != "" {
|
|
out = &buf
|
|
}
|
|
|
|
mode := printer.TabIndent | printer.UseSpaces
|
|
err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: out}, fset, x)
|
|
if err != nil {
|
|
log.Print(err)
|
|
}
|
|
|
|
// Add comments to struct fields saying which Go version introducd them.
|
|
if structName != "" {
|
|
fieldSince := apiInfo.fieldSince[structName]
|
|
typeSince := apiInfo.typeSince[structName]
|
|
// Add/rewrite comments on struct fields to note which Go version added them.
|
|
var buf2 bytes.Buffer
|
|
buf2.Grow(buf.Len() + len(" // Added in Go 1.n")*10)
|
|
bs := bufio.NewScanner(&buf)
|
|
for bs.Scan() {
|
|
line := bs.Bytes()
|
|
field := firstIdent(line)
|
|
var since string
|
|
if field != "" {
|
|
since = fieldSince[field]
|
|
if since != "" && since == typeSince {
|
|
// Don't highlight field versions if they were the
|
|
// same as the struct itself.
|
|
since = ""
|
|
}
|
|
}
|
|
if since == "" {
|
|
buf2.Write(line)
|
|
} else {
|
|
if bytes.Contains(line, slashSlash) {
|
|
line = bytes.TrimRight(line, " \t.")
|
|
buf2.Write(line)
|
|
buf2.WriteString("; added in Go ")
|
|
} else {
|
|
buf2.Write(line)
|
|
buf2.WriteString(" // Go ")
|
|
}
|
|
buf2.WriteString(since)
|
|
}
|
|
buf2.WriteByte('\n')
|
|
}
|
|
w.Write(buf2.Bytes())
|
|
}
|
|
}
|
|
|
|
var slashSlash = []byte("//")
|
|
|
|
// WriteNode writes x to w.
|
|
// TODO(bgarcia) Is this method needed? It's just a wrapper for p.writeNode.
|
|
func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) {
|
|
p.writeNode(w, nil, fset, x)
|
|
}
|
|
|
|
// firstIdent returns the first identifier in x.
|
|
// This actually parses "identifiers" that begin with numbers too, but we
|
|
// never feed it such input, so it's fine.
|
|
func firstIdent(x []byte) string {
|
|
x = bytes.TrimSpace(x)
|
|
i := bytes.IndexFunc(x, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) })
|
|
if i == -1 {
|
|
return string(x)
|
|
}
|
|
return string(x[:i])
|
|
}
|