|
|
// 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.
// This file implements LinkifyText which introduces
// links for identifiers pointing to their declarations.
// The approach does not cover all cases because godoc
// doesn't have complete type information, but it's
// reasonably good for browsing.
package godoc
import ( "fmt" "go/ast" "go/doc" "go/token" "io" "strconv" )
// LinkifyText HTML-escapes source text and writes it to w.
// Identifiers that are in a "use" position (i.e., that are
// not being declared), are wrapped with HTML links pointing
// to the respective declaration, if possible. Comments are
// formatted the same way as with FormatText.
//
func LinkifyText(w io.Writer, text []byte, n ast.Node) { links := linksFor(n)
i := 0 // links index
prev := "" // prev HTML tag
linkWriter := func(w io.Writer, _ int, start bool) { // end tag
if !start { if prev != "" { fmt.Fprintf(w, `</%s>`, prev) prev = "" } return }
// start tag
prev = "" if i < len(links) { switch info := links[i]; { case info.path != "" && info.name == "": // package path
fmt.Fprintf(w, `<a href="/pkg/%s/">`, info.path) prev = "a" case info.path != "" && info.name != "": // qualified identifier
fmt.Fprintf(w, `<a href="/pkg/%s/#%s">`, info.path, info.name) prev = "a" case info.path == "" && info.name != "": // local identifier
if info.isVal { fmt.Fprintf(w, `<span id="%s">`, info.name) prev = "span" } else if ast.IsExported(info.name) { fmt.Fprintf(w, `<a href="#%s">`, info.name) prev = "a" } } i++ } }
idents := tokenSelection(text, token.IDENT) comments := tokenSelection(text, token.COMMENT) FormatSelections(w, text, linkWriter, idents, selectionTag, comments) }
// A link describes the (HTML) link information for an identifier.
// The zero value of a link represents "no link".
//
type link struct { path, name string // package path, identifier name
isVal bool // identifier is defined in a const or var declaration
}
// linksFor returns the list of links for the identifiers used
// by node in the same order as they appear in the source.
//
func linksFor(node ast.Node) (links []link) { // linkMap tracks link information for each ast.Ident node. Entries may
// be created out of source order (for example, when we visit a parent
// definition node). These links are appended to the returned slice when
// their ast.Ident nodes are visited.
linkMap := make(map[*ast.Ident]link)
ast.Inspect(node, func(node ast.Node) bool { switch n := node.(type) { case *ast.Field: for _, n := range n.Names { linkMap[n] = link{} } case *ast.ImportSpec: if name := n.Name; name != nil { linkMap[name] = link{} } case *ast.ValueSpec: for _, n := range n.Names { linkMap[n] = link{name: n.Name, isVal: true} } case *ast.FuncDecl: linkMap[n.Name] = link{} case *ast.TypeSpec: linkMap[n.Name] = link{} case *ast.AssignStmt: // Short variable declarations only show up if we apply
// this code to all source code (as opposed to exported
// declarations only).
if n.Tok == token.DEFINE { // Some of the lhs variables may be re-declared,
// so technically they are not defs. We don't
// care for now.
for _, x := range n.Lhs { // Each lhs expression should be an
// ident, but we are conservative and check.
if n, _ := x.(*ast.Ident); n != nil { linkMap[n] = link{isVal: true} } } } case *ast.SelectorExpr: // Detect qualified identifiers of the form pkg.ident.
// If anything fails we return true and collect individual
// identifiers instead.
if x, _ := n.X.(*ast.Ident); x != nil { // Create links only if x is a qualified identifier.
if obj := x.Obj; obj != nil && obj.Kind == ast.Pkg { if spec, _ := obj.Decl.(*ast.ImportSpec); spec != nil { // spec.Path.Value is the import path
if path, err := strconv.Unquote(spec.Path.Value); err == nil { // Register two links, one for the package
// and one for the qualified identifier.
linkMap[x] = link{path: path} linkMap[n.Sel] = link{path: path, name: n.Sel.Name} } } } } case *ast.CompositeLit: // Detect field names within composite literals. These links should
// be prefixed by the type name.
fieldPath := "" prefix := "" switch typ := n.Type.(type) { case *ast.Ident: prefix = typ.Name + "." case *ast.SelectorExpr: if x, _ := typ.X.(*ast.Ident); x != nil { // Create links only if x is a qualified identifier.
if obj := x.Obj; obj != nil && obj.Kind == ast.Pkg { if spec, _ := obj.Decl.(*ast.ImportSpec); spec != nil { // spec.Path.Value is the import path
if path, err := strconv.Unquote(spec.Path.Value); err == nil { // Register two links, one for the package
// and one for the qualified identifier.
linkMap[x] = link{path: path} linkMap[typ.Sel] = link{path: path, name: typ.Sel.Name} fieldPath = path prefix = typ.Sel.Name + "." } } } } } for _, e := range n.Elts { if kv, ok := e.(*ast.KeyValueExpr); ok { if k, ok := kv.Key.(*ast.Ident); ok { // Note: there is some syntactic ambiguity here. We cannot determine
// if this is a struct literal or a map literal without type
// information. We assume struct literal.
name := prefix + k.Name linkMap[k] = link{path: fieldPath, name: name} } } } case *ast.Ident: if l, ok := linkMap[n]; ok { links = append(links, l) } else { l := link{name: n.Name} if n.Obj == nil && doc.IsPredeclared(n.Name) { l.path = builtinPkgPath } links = append(links, l) } } return true }) return }
|