| // 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(info *PageInfo, comment string) string { |
| var buf bytes.Buffer |
| // TODO(gri) Provide list of words (e.g. function parameters) |
| // to be emphasized by ToHTML. |
| |
| // godocToHTML is: |
| // - buf.Write(info.PDoc.HTML(comment)) on go1.19 |
| // - go/doc.ToHTML(&buf, comment, nil) on other versions |
| godocToHTML(&buf, info.PDoc, comment) |
| |
| 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]) |
| } |