| // Copyright 2017 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 render |
| |
| import ( |
| "go/ast" |
| "go/token" |
| "strings" |
| "unicode" |
| "unicode/utf8" |
| |
| "github.com/google/safehtml" |
| "github.com/google/safehtml/template" |
| "golang.org/x/pkgsite/internal/godoc/internal/doc" |
| ) |
| |
| /* |
| This logic creates data structures needed to map some string (e.g., "io.EOF") |
| to the package that contains those identifiers. |
| |
| The identifierResolver.toHTML method is the primary API used by other logic |
| in this package. The identifierResolver is essentially comprised of: |
| |
| * packageIDs: a collection of top-level identifiers in the package being |
| rendered and any related packages. |
| |
| * declIDs: a collection of parameters related to the specific declaration |
| being rendered. We collect declaration parameters because they provide |
| a useful heuristic for further linking (e.g., "r.Read" can be linked |
| if we know the type of r). |
| |
| This logic is unused when doing non-HTML formatting. |
| */ |
| |
| // forEachPackageDecl iterates though every top-level declaration in a package. |
| func forEachPackageDecl(pkg *doc.Package, fnc func(decl ast.Decl)) { |
| for _, c := range pkg.Consts { |
| fnc(c.Decl) |
| } |
| for _, v := range pkg.Vars { |
| fnc(v.Decl) |
| } |
| for _, f := range pkg.Funcs { |
| fnc(f.Decl) |
| } |
| for _, t := range pkg.Types { |
| fnc(t.Decl) |
| for _, c := range t.Consts { |
| fnc(c.Decl) |
| } |
| for _, v := range t.Vars { |
| fnc(v.Decl) |
| } |
| for _, f := range t.Funcs { |
| fnc(f.Decl) |
| } |
| for _, m := range t.Methods { |
| fnc(m.Decl) |
| } |
| } |
| } |
| |
| // packageIDs is a collection of top-level package identifiers |
| // in the package being rendered and any related packages. |
| type packageIDs struct { |
| // name is the name of the package being rendered. |
| // |
| // E.g., "json" |
| name string |
| |
| // impPaths maps package names to their import paths. |
| // |
| // E.g., impPaths["json"] == "encoding/json" |
| impPaths map[string]string // map[name]pkgPath |
| |
| // pkgIDs is the set of all top-level identifiers in this package and |
| // any related package. |
| // |
| // E.g., pkgIDs["json"]["Encoder.Encode"] == true |
| pkgIDs map[string]map[string]bool // map[name]map[topLevelID]bool |
| |
| // topLevelDecls is the set of all AST declarations for the this package. |
| topLevelDecls map[interface{}]bool // map[T]bool where T is *ast.FuncDecl | *ast.GenDecl | *ast.TypeSpec | *ast.ValueSpec |
| } |
| |
| // newPackageIDs returns a packageIDs that collects all top-level identifiers |
| // for the given package pkg and any related packages. |
| func newPackageIDs(pkg *doc.Package, related ...*doc.Package) *packageIDs { |
| pids := &packageIDs{ |
| name: pkg.Name, |
| impPaths: make(map[string]string), |
| pkgIDs: make(map[string]map[string]bool), |
| topLevelDecls: make(map[interface{}]bool), |
| } |
| |
| // Collect top-level declaration IDs for pkg and related packages. |
| for _, pkg := range append([]*doc.Package{pkg}, related...) { |
| if _, ok := pids.pkgIDs[pkg.Name]; ok { |
| continue // package name conflicts, ignore this package |
| } |
| pids.impPaths[pkg.Name] = pkg.ImportPath |
| pids.pkgIDs[pkg.Name] = make(map[string]bool) |
| forEachPackageDecl(pkg, func(decl ast.Decl) { |
| for _, idk := range generateAnchorPoints(decl) { |
| pids.pkgIDs[pkg.Name][idk.ID.String()] = true // E.g., ["io"]["Reader.Read"] |
| } |
| }) |
| } |
| |
| // Collect AST objects for accurate linking of Go source code. |
| forEachPackageDecl(pkg, func(decl ast.Decl) { |
| pids.topLevelDecls[decl] = true |
| if gd, _ := decl.(*ast.GenDecl); gd != nil { |
| for _, sp := range gd.Specs { |
| pids.topLevelDecls[sp] = true |
| } |
| } |
| }) |
| return pids |
| } |
| |
| // declIDs is a collection of identifiers that are related to the ast.Decl |
| // currently being processed. Using Decl-level variables allows us to provide |
| // greater accuracy in linking when comments refer to the variable names. |
| // |
| // For example, we can link "r.Read" to "io.Reader.Read" because we know that |
| // variable "r" is of type "io.Reader", which has a "Read" method. |
| type declIDs struct { |
| // recvType is the type of the receiver for any methods. |
| // |
| // E.g., "Reader" |
| recvType string |
| |
| // paramTypes is a mapping of parameter names in a ast.FuncDecl |
| // to the type of that parameter. |
| // |
| // E.g., paramTypes["r"] == "io.Reader" |
| paramTypes map[string]string // map[varName]typeName |
| } |
| |
| func newDeclIDs(decl ast.Decl) *declIDs { |
| dids := &declIDs{paramTypes: make(map[string]string)} |
| |
| switch decl := decl.(type) { |
| case *ast.GenDecl: |
| // Note that go/doc.New automatically splits type declaration |
| // blocks into individual specifications. |
| // If there are multiple, it okay to skip this logic, since |
| // all of this information is just to improve the heuristics of toHTML. |
| if decl.Tok == token.TYPE && len(decl.Specs) == 1 { |
| dids.recvType = decl.Specs[0].(*ast.TypeSpec).Name.String() |
| } |
| case *ast.FuncDecl: |
| // Obtain receiver variable and type names. |
| if decl.Recv != nil && len(decl.Recv.List) > 0 { |
| f := decl.Recv.List[0] |
| dids.recvType, _ = nodeName(f.Type) // E.g., "Reader" |
| if len(f.Names) == 1 && dids.recvType != "" { |
| varName, _ := nodeName(f.Names[0]) // E.g., "r" |
| if varName != "" { |
| dids.paramTypes[varName] = dids.recvType |
| } |
| } |
| } |
| |
| // Add mapping of variable names to types names for parameters and results. |
| for _, flist := range []*ast.FieldList{decl.Type.Params, decl.Type.Results} { |
| if flist == nil { |
| continue |
| } |
| for _, field := range flist.List { |
| typeName, _ := nodeName(field.Type) // E.g., "context.Context" |
| if typeName == "" { |
| continue |
| } |
| for _, name := range field.Names { |
| varName, _ := nodeName(name) // E.g., "ctx" |
| if varName != "" { |
| dids.paramTypes[varName] = typeName |
| } |
| } |
| } |
| } |
| } |
| return dids |
| } |
| |
| type identifierResolver struct { |
| *packageIDs |
| *declIDs |
| |
| // packageURL is a function for producing URLs from package paths. |
| // |
| // E.g., packageURL("builtin") == "/pkg/builtin/index.html" |
| packageURL func(string) string |
| } |
| |
| // toURL returns a URL to locate the given package, and |
| // optionally a specific identifier in that package. |
| // The pkgPath may be empty, indicating that this is an anchor only URL. |
| // The id may be empty, indicating that this refers to the package itself. |
| func (r identifierResolver) toURL(pkgPath, id string) (url string) { |
| if pkgPath != "" { |
| url = "/" + pkgPath |
| if r.packageURL != nil { |
| url = r.packageURL(pkgPath) |
| } |
| } |
| if id != "" { |
| url += "#" + id |
| } |
| return url |
| } |
| |
| // toHTML formats a dot-delimited word as HTML with each ID segment converted |
| // to be a link to the relevant declaration. |
| func (r identifierResolver) toHTML(word string) safehtml.HTML { |
| // extraSuffix is extra identifier segments that can't be matched |
| // probably because we lack type information. |
| var extraSuffix safehtml.HTML // E.g., ".Get" for an unknown Get method |
| origIDs := strings.Split(word, ".") |
| |
| // Skip any standalone unexported identifier. |
| if !isExported(word) && len(origIDs) == 1 { |
| return safehtml.HTMLEscaped(word) |
| } |
| |
| // Generate variations on the original word. |
| var altWords []string |
| if vtype, ok := r.paramTypes[origIDs[0]]; ok { |
| altWords = append(altWords, vtype+word[len(origIDs[0]):]) // E.g., "r.Read" => "io.Reader.Read" |
| } else if r.recvType != "" { |
| altWords = append(altWords, r.recvType+"."+word) // E.g., "Read" => "Reader.Read" |
| } |
| altWords = append(altWords, word) |
| |
| var altWord string |
| for _, s := range altWords { |
| if _, _, ok := r.lookup(s); ok { |
| altWord = s // direct match |
| goto linkify |
| } |
| if _, _, ok := r.lookup(strings.TrimSuffix(s, "s")); ok { |
| altWord = s[:len(s)-len("s")] // E.g., "Caches" => "Cache" |
| goto linkify |
| } |
| if _, _, ok := r.lookup(strings.TrimSuffix(s, "es")); ok { |
| altWord = s[:len(s)-len("es")] // E.g., "Boxes" => "Box" |
| goto linkify |
| } |
| |
| // Repeatedly truncate the last segment, searching for a partial match. |
| i, j := len(s), len(word) |
| for i >= 0 && j >= 0 { |
| allowPartial := isExported(word[:j]) || strings.Contains(word[:j], ".") |
| if _, _, ok := r.lookup(s[:i]); ok && allowPartial { |
| altWord = s[:i] |
| origIDs = strings.Split(word[:j], ".") |
| extraSuffix = safehtml.HTMLEscaped(word[j:]) |
| goto linkify |
| } |
| i = strings.LastIndexByte(s[:i], '.') |
| j = strings.LastIndexByte(word[:j], '.') |
| } |
| } |
| return safehtml.HTMLEscaped(word) // no match found |
| |
| linkify: |
| // altWord contains a modified dot-separated identifier. |
| // origIDs contains the segments of the original identifier. |
| // It is possible for Split(altWord, ".") to be longer than origIDs |
| // if an implicit type was prepended. |
| // E.g., altWord="io.Reader.Read", origIDs=["r", "Read"] |
| |
| // Skip past implicit prefix selectors. |
| // E.g., i=3, altWord[i:]="Reader.Read" |
| var i int |
| for strings.Count(altWord[i:], ".")+1 != len(origIDs) { |
| i += strings.IndexByte(altWord[i:], '.') + 1 |
| } |
| |
| var outs []safehtml.HTML |
| for _, s := range origIDs { |
| // Advance to the next segment in altWord. |
| // E.g., i=9, altWord[:i]="io.Reader", s="r" |
| // E.g., i=14, altWord[:i]="io.Reader.Read", s="Read" |
| if n := strings.IndexByte(altWord[i+1:], '.'); n >= 0 { |
| i += 1 + n |
| } else { |
| i = len(altWord) |
| } |
| |
| path, name, _ := r.lookup(altWord[:i]) |
| u := r.toURL(path, name) |
| html, err := LinkTemplate.ExecuteToHTML(Link{Href: u, Text: s}) |
| if err != nil { |
| html = safehtml.HTMLEscaped("[" + err.Error() + "]") |
| } |
| outs = append(outs, html) |
| outs = append(outs, safehtml.HTMLEscaped(".")) |
| } |
| if len(outs) == 0 { |
| return extraSuffix |
| } |
| // Replace final dot with extraSuffix. |
| outs[len(outs)-1] = extraSuffix |
| return safehtml.HTMLConcat(outs...) |
| } |
| |
| // Link is the data passed to LinkTemplate. |
| type Link struct { |
| Href, Text string |
| Class string // class for "a" tag; optional |
| } |
| |
| var LinkTemplate = template.Must(template.New("link").Parse( |
| `<a {{with .Class}}class="{{.}}" {{end}}href="{{.Href}}">{{.Text}}</a>`)) |
| |
| // lookup looks up a dot-separated identifier. |
| // E.g., "pkg", "pkg.Var", "Recv.Method", "Struct.Field", "pkg.Struct.Field" |
| func (r identifierResolver) lookup(id string) (pkgPath, name string, ok bool) { |
| if r.pkgIDs[r.name][id] { |
| return "", id, true // ID refers to local top-level declaration |
| } |
| if path := r.impPaths[id]; path != "" { |
| return path, "", true // ID refers to a package |
| } |
| if i := strings.IndexByte(id, '.'); i >= 0 { |
| prefix, suffix := id[:i], id[i+1:] |
| if r.pkgIDs[prefix][suffix] { |
| if prefix == r.name { |
| prefix = "" |
| } |
| return r.impPaths[prefix], suffix, true // ID refers to a different package's top-level declaration |
| } |
| } |
| return "", "", false // not found |
| } |
| |
| func nodeName(n ast.Node) (string, *ast.Ident) { |
| switch n := n.(type) { |
| case *ast.Ident: |
| return n.String(), n |
| case *ast.StarExpr: |
| return nodeName(n.X) |
| case *ast.SelectorExpr: |
| if prefix, _ := nodeName(n.X); prefix != "" { |
| return prefix + "." + n.Sel.String(), n.Sel |
| } |
| return n.Sel.String(), n.Sel |
| default: |
| return "", nil |
| } |
| } |
| |
| func isExported(id string) bool { |
| r, _ := utf8.DecodeRuneInString(id) |
| return unicode.IsUpper(r) |
| } |