| // 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 ( |
| "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. |
| 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, |
| |
| // 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, |
| "comment_text": comment_textFunc, |
| "sanitize": sanitizeFunc, |
| |
| // support for URL attributes |
| "pkgLink": pkgLinkFunc, |
| "srcLink": srcLinkFunc, |
| "posLink_url": newPosLink_urlFunc(srcPosLinkFunc), |
| "docLink": docLinkFunc, |
| "queryLink": queryLinkFunc, |
| |
| // formatting of Examples |
| "example_html": p.example_htmlFunc, |
| "example_text": p.example_textFunc, |
| "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, |
| } |
| 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 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.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.FSet, node) |
| |
| var buf2 bytes.Buffer |
| if n, _ := node.(ast.Node); n != nil && linkify && p.DeclLinks { |
| LinkifyText(&buf2, buf1.Bytes(), n) |
| } else { |
| FormatText(&buf2, buf1.Bytes(), -1, true, "", nil) |
| } |
| |
| return buf2.String() |
| } |
| |
| 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() |
| } |
| |
| // punchCardWidth is the number of columns of fixed-width |
| // characters to assume when wrapping text. Very few people |
| // use terminals or cards smaller than 80 characters, so 80 it is. |
| // We do not try to sniff the environment or the tty to adapt to |
| // the situation; instead, by using a constant we make sure that |
| // godoc always produces the same output regardless of context, |
| // a consistency that is lost otherwise. For example, if we sniffed |
| // the environment or tty, then http://golang.org/pkg/math/?m=text |
| // would depend on the width of the terminal where godoc started, |
| // which is clearly bogus. More generally, the Unix tools that behave |
| // differently when writing to a tty than when writing to a file have |
| // a history of causing confusion (compare `ls` and `ls | cat`), and we |
| // want to avoid that mistake here. |
| const punchCardWidth = 80 |
| |
| func containsOnlySpace(buf []byte) bool { |
| isNotSpace := func(r rune) bool { return !unicode.IsSpace(r) } |
| return bytes.IndexFunc(buf, isNotSpace) == -1 |
| } |
| |
| func comment_textFunc(comment, indent, preIndent string) string { |
| var buf bytes.Buffer |
| doc.ToText(&buf, comment, indent, preIndent, punchCardWidth-2*len(indent)) |
| if containsOnlySpace(buf.Bytes()) { |
| return "" |
| } |
| 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 |
| |
| // 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 { |
| relpath := path[1:] |
| // because of the irregular mapping under goroot |
| // we need to correct certain relative paths |
| relpath = strings.TrimPrefix(relpath, "src/pkg/") |
| return "pkg/" + relpath // remove trailing '/' for relative URL |
| } |
| |
| 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/pkg/") { |
| s = "/src/pkg" + 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 { |
| s = strings.TrimPrefix(s, "/src") |
| return pathpkg.Clean("/"+s) + "/#" + ident |
| } |
| |
| func (p *Presentation) example_textFunc(info *PageInfo, funcName, indent string) string { |
| if !p.ShowExamples { |
| return "" |
| } |
| |
| var buf bytes.Buffer |
| first := true |
| for _, eg := range info.Examples { |
| name := stripExampleSuffix(eg.Name) |
| if name != funcName { |
| continue |
| } |
| |
| if !first { |
| buf.WriteString("\n") |
| } |
| first = false |
| |
| // print code |
| cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} |
| var buf1 bytes.Buffer |
| p.writeNode(&buf1, info.FSet, cnode) |
| code := buf1.String() |
| // Additional formatting if this is a function body. |
| if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { |
| // remove surrounding braces |
| code = code[1 : n-1] |
| // unindent |
| code = strings.Replace(code, "\n ", "\n", -1) |
| } |
| code = strings.Trim(code, "\n") |
| code = strings.Replace(code, "\n", "\n\t", -1) |
| |
| buf.WriteString(indent) |
| buf.WriteString("Example:\n\t") |
| buf.WriteString(code) |
| buf.WriteString("\n") |
| } |
| return buf.String() |
| } |
| |
| 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 = strings.Replace(code, "\n ", "\n", -1) |
| // 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 |
| 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() |
| } |
| |
| // 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:]]*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 |
| } |
| |
| // Write an AST node to w. |
| func (p *Presentation) writeNode(w io.Writer, 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) |
| mode := printer.TabIndent | printer.UseSpaces |
| err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: w}, fset, x) |
| if err != nil { |
| log.Print(err) |
| } |
| } |
| |
| // 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, fset, x) |
| } |