godoc: update struct field anchor code

Now without regexps and allocations.

And also match comments like:

    // Foo, if non-nil, ...

The comma confused the old pattern.

Updates golang/go#16753

Change-Id: I9016ee7b5933ea343950a39989952804c74a598b
Reviewed-on: https://go-review.googlesource.com/33755
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Chris Broadfoot <cbro@golang.org>
diff --git a/godoc/godoc.go b/godoc/godoc.go
index b8a9d0d..8bda89a 100644
--- a/godoc/godoc.go
+++ b/godoc/godoc.go
@@ -233,49 +233,94 @@
 	if st.Fields == nil {
 		return
 	}
-
-	v := buf.Bytes()
-	buf.Reset()
-
+	var scratch bytes.Buffer
 	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>")
-			})
+		scratch.Reset()
+		var added bool
+		foreachLine(buf.Bytes(), func(line []byte) {
+			if !added && isLineForStructFieldID(line, fieldName) {
+				added = true
+				fmt.Fprintf(&scratch, `<span id="%s.%s"></span>`, name, fieldName)
+			}
+			scratch.Write(line)
+		})
+		buf.Reset()
+		buf.Write(scratch.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">// `)
+
+// isLineForStructFieldID reports whether line is a line we should
+// add a <span id="#StructName.FieldName"> to. Only the fieldName is provided.
+func isLineForStructFieldID(line []byte, fieldName string) bool {
+	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.
+	// For comments
+	if bytes.HasPrefix(line, commentPrefix) {
+		if matchesIdentBoundary(line[len(commentPrefix):], fieldName) {
+			return true
 		}
 	}
+	return matchesIdentBoundary(line, fieldName)
+}
 
-	buf.Write(v)
+// matchesIdentBoundary reports whether line matches /^ident\b/.
+// A boundary is considered either none, or an ASCII non-alphanum.
+func matchesIdentBoundary(line []byte, ident string) bool {
+	if len(line) < len(ident) {
+		return false
+	}
+	if string(line[:len(ident)]) != ident {
+		return false
+	}
+	rest := line[len(ident):]
+	return len(rest) == 0 || !isASCIIWordChar(rest[0])
+}
+
+// isASCIIWordChar reports whether b is an ASCII "word"
+// character. (Matching /\w/ in ASCII mode)
+func isASCIIWordChar(b byte) bool {
+	return 'a' <= b && b <= 'z' ||
+		'A' <= b && b <= 'Z' ||
+		'0' <= b && b <= '0' ||
+		b == '_'
 }
 
 func comment_htmlFunc(comment string) string {