godoc: make struct fields linkable in HTML mode

This adds <span id="StructName.FieldName"> elements around
field names, starting at the comment if present, so people
can link to /pkg/somepkg/#SomeStruct.SomeField.

Fixes golang/go#16753

Change-Id: I4a8b30605d18e9e33e3d42f273a95067ac491438
Reviewed-on: https://go-review.googlesource.com/33690
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Robert Griesemer <gri@golang.org>
diff --git a/godoc/godoc.go b/godoc/godoc.go
index 7bb4829..b8a9d0d 100644
--- a/godoc/godoc.go
+++ b/godoc/godoc.go
@@ -190,6 +190,9 @@
 	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)
 	}
@@ -197,6 +200,84 @@
 	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
+	}
+
+	v := buf.Bytes()
+	buf.Reset()
+
+	for _, f := range st.Fields.List {
+		if len(f.Names) == 0 {
+			continue
+		}
+		fieldName := f.Names[0].Name
+		commentStart := []byte("// " + fieldName + " ")
+		if bytes.Contains(v, commentStart) {
+			// 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.
+			v = bytes.Replace(v, commentStart, []byte(`<span id="`+name+"."+fieldName+`">// `+fieldName+" </span>"), 1)
+		} else {
+			rx := regexp.MustCompile(`(?m)^\s*` + fieldName + `\b`)
+			var matched bool
+			v = rx.ReplaceAllFunc(v, func(sub []byte) []byte {
+				if matched {
+					return sub
+				}
+				matched = true
+				return []byte(`<span id="` + name + "." + fieldName + `">` + string(sub) + "</span>")
+			})
+		}
+	}
+
+	buf.Write(v)
+}
+
 func comment_htmlFunc(comment string) string {
 	var buf bytes.Buffer
 	// TODO(gri) Provide list of words (e.g. function parameters)