--- /dev/null
+// 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
+ 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 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])
+}