blob: e4add22a10450cce7503542f97977fefbe2bd788 [file] [log] [blame] [edit]
// 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
}