internal/fetch: add explicit package file links to doc tab
Add a new section for package files containing links to the source code
files. These file links are displayed in at most 3 columns for easier
readability.
Fixes golang/go#37863
Change-Id: Ia70b891a49fc3e27ece655bf895eb18e4c8b2373
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/240007
Reviewed-by: Julie Qiu <julie@golang.org>
Reviewed-by: Andrew Bonventre <andybons@golang.org>
diff --git a/content/static/css/stylesheet.css b/content/static/css/stylesheet.css
index 6647a3f..c96bb3d 100644
--- a/content/static/css/stylesheet.css
+++ b/content/static/css/stylesheet.css
@@ -966,6 +966,7 @@
.Documentation-variablesHeader,
.Documentation-examplesHeader,
.Documentation-examplesPlay,
+.Documentation-filesHeader,
.Documentation-functionHeader,
.Documentation-typeHeader,
.Documentation-typeMethodHeader,
@@ -992,6 +993,13 @@
padding-top: 0.5rem;
text-decoration: none;
}
+.Documentation-files {
+ display: none;
+}
+.Documentation-filesList {
+ column-count: 3;
+ column-width: 12.5rem;
+}
.Documentation-build {
color: var(--gray-3);
font-size: 0.875rem;
diff --git a/internal/fetch/dochtml/dochtml.go b/internal/fetch/dochtml/dochtml.go
index daf10f8..906fb64 100644
--- a/internal/fetch/dochtml/dochtml.go
+++ b/internal/fetch/dochtml/dochtml.go
@@ -17,6 +17,7 @@
"go/ast"
"go/printer"
"go/token"
+ "html"
"html/template"
pathpkg "path"
"sort"
@@ -34,6 +35,13 @@
// RenderOptions are options for Render.
type RenderOptions struct {
+ // FileLinkFunc optionally specifies a function that
+ // returns a URL where file should be linked to.
+ // file is the name component of a .go file in the package,
+ // including the .go qualifier.
+ // As a special case, FileLinkFunc may return the empty
+ // string to indicate that a given file should not be linked.
+ FileLinkFunc func(file string) (url string)
SourceLinkFunc func(ast.Node) string
PlayURLFunc func(*doc.Example) string // If set, returns the Go playground URL for the example
Limit int64 // If zero, a default limit of 10 megabytes is used.
@@ -81,6 +89,9 @@
DisableHotlinking: true,
})
+ fileLink := func(name string) template.HTML {
+ return fileLinkHTML(name, opt.FileLinkFunc(name))
+ }
sourceLink := func(name string, node ast.Node) template.HTML {
link := opt.SourceLinkFunc(node)
if link == "" {
@@ -104,6 +115,7 @@
"render_doc": r.DocHTML,
"render_decl": r.DeclHTML,
"render_code": r.CodeHTML,
+ "file_link": fileLink,
"source_link": sourceLink,
"play_url": playURLFunc,
}).Execute(buf, struct {
@@ -123,6 +135,17 @@
return buf.B.String(), nil
}
+// fileLinkHTML returns an HTML-formatted file name linked to the source URL.
+// If link is the empty string, the file name is not linked.
+func fileLinkHTML(name, link string) template.HTML {
+ name = html.EscapeString(name)
+ if link == "" {
+ return template.HTML(name)
+ }
+ link = html.EscapeString(link)
+ return template.HTML(fmt.Sprintf(`<a class="Documentation-file" href="%s">%s</a>`, link, name))
+}
+
// examples is an internal representation of all package examples.
type examples struct {
List []*example // sorted by ParentID
diff --git a/internal/fetch/dochtml/dochtml_test.go b/internal/fetch/dochtml/dochtml_test.go
index c1455ca..41af635 100644
--- a/internal/fetch/dochtml/dochtml_test.go
+++ b/internal/fetch/dochtml/dochtml_test.go
@@ -8,6 +8,7 @@
"go/ast"
"go/parser"
"go/token"
+ "html/template"
"io/ioutil"
"path/filepath"
"strings"
@@ -23,6 +24,7 @@
fset, d := mustLoadPackage("everydecl")
rawDoc, err := Render(fset, d, RenderOptions{
+ FileLinkFunc: func(string) string { return "file" },
SourceLinkFunc: func(ast.Node) string { return "src" },
})
if err != nil {
@@ -42,6 +44,54 @@
})
}
+func TestFileLinkHTML(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ file string
+ link string
+ want template.HTML
+ }{
+ {
+ name: "file name is escaped",
+ file: `"File & name" <'file@name.com>`,
+ link: "",
+ want: `"File & name" <'file@name.com>`,
+ },
+ {
+ name: "link is escaped",
+ file: "file.go",
+ link: `"abc@go's.com"`,
+ want: `<a class="Documentation-file" href=""abc@go's.com"">file.go</a>`,
+ },
+ {
+ name: "file name and link are escaped",
+ file: `"a's.com@/`,
+ link: `"x@go's.com"`,
+ want: `<a class="Documentation-file" href=""x@go's.com"">"a's.com@/</a>`,
+ },
+ {
+ name: "HTML injection escaped",
+ file: `<a href="gfr.con"></a>`,
+ link: `a.com`,
+ want: `<a class="Documentation-file" href="a.com"><a href="gfr.con"></a></a>`,
+ },
+ {
+ name: "regular file name and link are rendered",
+ file: `escape.go`,
+ link: `https://golang.org/src/html/escape.go`,
+ want: `<a class="Documentation-file" href="https://golang.org/src/html/escape.go">escape.go</a>`,
+ },
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ got := fileLinkHTML(test.file, test.link)
+ diff := cmp.Diff(test.want, got)
+ if diff != "" {
+ t.Errorf("mismatch (-want, +got):\n%s", diff)
+ }
+ })
+ }
+}
+
func testDuplicateIDs(t *testing.T, htmlDoc *html.Node) {
idCounts := map[string]int{}
walk(htmlDoc, func(n *html.Node) {
diff --git a/internal/fetch/dochtml/template.go b/internal/fetch/dochtml/template.go
index aa44e68..1c1c50c 100644
--- a/internal/fetch/dochtml/template.go
+++ b/internal/fetch/dochtml/template.go
@@ -30,6 +30,7 @@
"render_doc": (*render.Renderer)(nil).DocHTML,
"render_decl": (*render.Renderer)(nil).DeclHTML,
"render_code": (*render.Renderer)(nil).CodeHTML,
+ "file_link": func() string { return "" },
"source_link": func() string { return "" },
"play_url": func(*doc.Example) string { return "" },
},
@@ -186,6 +187,15 @@
</section>
{{- end -}}
+ <section class="Documentation-files">
+ <h3 id="pkg-files" class="Documentation-filesHeader">Package Files <a href="#pkg-files">¶</a></h3>
+ <ul class="Documentation-filesList">
+ {{- range .Filenames -}}
+ <li>{{file_link .}}</li>
+ {{- end -}}
+ </ul>
+ </section>
+
{{- if .Consts -}}
<section class="Documentation-constants">
<h3 id="pkg-constants" class="Documentation-constantsHeader">Constants <a href="#pkg-constants">¶</a></h3>{{"\n"}}
diff --git a/internal/fetch/fetch.go b/internal/fetch/fetch.go
index 38b248f..41f6a1c 100644
--- a/internal/fetch/fetch.go
+++ b/internal/fetch/fetch.go
@@ -628,6 +628,12 @@
}
return sourceInfo.LineURL(path.Join(innerPath, p.Filename), p.Line)
}
+ fileLinkFunc := func(filename string) string {
+ if sourceInfo == nil {
+ return ""
+ }
+ return sourceInfo.FileURL(path.Join(innerPath, filename))
+ }
// Fetch Go playground URLs for examples.
playURLs := make(map[*doc.Example]string)
@@ -656,6 +662,7 @@
}
docHTML, err := dochtml.Render(fset, d, dochtml.RenderOptions{
+ FileLinkFunc: fileLinkFunc,
SourceLinkFunc: sourceLinkFunc,
PlayURLFunc: playURLFunc,
Limit: int64(MaxDocumentationHTML),