From 0f65b31aee0c0a77f4b223c295bca8d86b9581d8 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 29 Nov 2016 23:53:36 +0000 Subject: [PATCH] godoc: make struct fields linkable in HTML mode This adds 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 TryBot-Result: Gobot Gobot Reviewed-by: Robert Griesemer --- godoc/godoc.go | 81 +++++++++++++++++++++++++++++++++++++++++++++ godoc/godoc_test.go | 40 ++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/godoc/godoc.go b/godoc/godoc.go index 7bb4829a00..b8a9d0d79a 100644 --- a/godoc/godoc.go +++ b/godoc/godoc.go @@ -190,6 +190,9 @@ func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify b 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 @@ func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify b 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 +// 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(`// `+fieldName+" "), 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(`` + string(sub) + "") + }) + } + } + + buf.Write(v) +} + func comment_htmlFunc(comment string) string { var buf bytes.Buffer // TODO(gri) Provide list of words (e.g. function parameters) diff --git a/godoc/godoc_test.go b/godoc/godoc_test.go index a10a1ab7e2..ce57d9926f 100644 --- a/godoc/godoc_test.go +++ b/godoc/godoc_test.go @@ -5,6 +5,9 @@ package godoc import ( + "go/ast" + "go/parser" + "go/token" "testing" ) @@ -116,3 +119,40 @@ func TestSanitizeFunc(t *testing.T) { } } } + +// Test that we add elements +// to the HTML of struct fields. +func TestStructFieldsIDAttributes(t *testing.T) { + p := &Presentation{ + DeclLinks: true, + } + src := []byte(` +package foo + +type T struct { + NoDoc string + + // Doc has a comment. + Doc string +} +`) + fset := token.NewFileSet() + af, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + genDecl := af.Decls[0].(*ast.GenDecl) + pi := &PageInfo{ + FSet: fset, + } + got := p.node_htmlFunc(pi, genDecl, true) + want := `type T struct { +NoDoc string + +// Doc has a comment. +Doc string +}` + if got != want { + t.Errorf(" got: %q\nwant: %q\n", got, want) + } +}