зеркало из https://github.com/golang/tools.git
938 строки
26 KiB
Go
938 строки
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
|
|
// identifier 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.
|
|
line = bytes.TrimPrefix(line, 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
|
|
|
|
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
|
|
}{eg.Name, eg.Doc, code, play, out})
|
|
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 introduced 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])
|
|
}
|