godoc: add links to docs in text and dir pages

Fixes golang/go#17125

Change-Id: I22dd0561cd1c8eb30524797b6c0488d08a65285b
Reviewed-on: https://go-review.googlesource.com/29279
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/godoc/godoc.go b/godoc/godoc.go
index e7868ee..a9e8b3b 100644
--- a/godoc/godoc.go
+++ b/godoc/godoc.go
@@ -79,11 +79,13 @@
 		"sanitize":     sanitizeFunc,
 
 		// support for URL attributes
-		"pkgLink":     pkgLinkFunc,
-		"srcLink":     srcLinkFunc,
-		"posLink_url": newPosLink_urlFunc(srcPosLinkFunc),
-		"docLink":     docLinkFunc,
-		"queryLink":   queryLinkFunc,
+		"pkgLink":       pkgLinkFunc,
+		"srcLink":       srcLinkFunc,
+		"posLink_url":   newPosLink_urlFunc(srcPosLinkFunc),
+		"docLink":       docLinkFunc,
+		"queryLink":     queryLinkFunc,
+		"srcBreadcrumb": srcBreadcrumbFunc,
+		"srcToPkgLink":  srcToPkgLinkFunc,
 
 		// formatting of Examples
 		"example_html":   p.example_htmlFunc,
@@ -459,6 +461,51 @@
 	return "pkg/" + path
 }
 
+// srcToPkgLinkFunc builds an <a> tag linking to
+// the package documentation of relpath.
+func srcToPkgLinkFunc(relpath string) string {
+	relpath = pkgLinkFunc(relpath)
+	if relpath == "pkg/" {
+		return `<a href="/pkg">Index</a>`
+	}
+	if i := strings.LastIndex(relpath, "/"); i != -1 {
+		// Remove filename after last slash.
+		relpath = relpath[:i]
+	}
+	return fmt.Sprintf(`<a href="/%s">%s</a>`, relpath, relpath[len("pkg/"):])
+}
+
+// srcBreadcrumbFun converts each segment of relpath to a HTML <a>.
+// Each segment links to its corresponding src directories.
+func srcBreadcrumbFunc(relpath string) string {
+	segments := strings.Split(relpath, "/")
+	var buf bytes.Buffer
+	var selectedSegment string
+	var selectedIndex int
+
+	if strings.HasSuffix(relpath, "/") {
+		// relpath is a directory ending with a "/".
+		// Selected segment is the segment before the last slash.
+		selectedIndex = len(segments) - 2
+		selectedSegment = segments[selectedIndex] + "/"
+	} else {
+		selectedIndex = len(segments) - 1
+		selectedSegment = segments[selectedIndex]
+	}
+
+	for i := range segments[:selectedIndex] {
+		buf.WriteString(fmt.Sprintf(`<a href="/%s">%s</a>/`,
+			strings.Join(segments[:i+1], "/"),
+			segments[i],
+		))
+	}
+
+	buf.WriteString(`<span class="text-muted">`)
+	buf.WriteString(selectedSegment)
+	buf.WriteString(`</span>`)
+	return buf.String()
+}
+
 func newPosLink_urlFunc(srcPosLinkFunc func(s string, line, low, high int) string) func(info *PageInfo, n interface{}) string {
 	// n must be an ast.Node or a *doc.Note
 	return func(info *PageInfo, n interface{}) string {
diff --git a/godoc/godoc_test.go b/godoc/godoc_test.go
index ef5790e..dca1c95 100644
--- a/godoc/godoc_test.go
+++ b/godoc/godoc_test.go
@@ -290,3 +290,32 @@
 		}
 	}
 }
+
+func TestSrcBreadcrumbFunc(t *testing.T) {
+	for _, tc := range []struct {
+		path string
+		want string
+	}{
+		{"src/", `<span class="text-muted">src/</span>`},
+		{"src/fmt/", `<a href="/src">src</a>/<span class="text-muted">fmt/</span>`},
+		{"src/fmt/print.go", `<a href="/src">src</a>/<a href="/src/fmt">fmt</a>/<span class="text-muted">print.go</span>`},
+	} {
+		if got := srcBreadcrumbFunc(tc.path); got != tc.want {
+			t.Errorf("srcBreadcrumbFunc(%v) = %v; want %v", tc.path, got, tc.want)
+		}
+	}
+}
+
+func TestSrcToPkgLinkFunc(t *testing.T) {
+	for _, tc := range []struct {
+		path string
+		want string
+	}{
+		{"src/", `<a href="/pkg">Index</a>`},
+		{"src/fmt/", `<a href="/pkg/fmt">fmt</a>`},
+	} {
+		if got := srcToPkgLinkFunc(tc.path); got != tc.want {
+			t.Errorf("srcToPkgLinkFunc(%v) = %v; want %v", tc.path, got, tc.want)
+		}
+	}
+}
diff --git a/godoc/page.go b/godoc/page.go
index 35d6cfb..0c7bf00 100644
--- a/godoc/page.go
+++ b/godoc/page.go
@@ -16,6 +16,7 @@
 	Title    string
 	Tabtitle string
 	Subtitle string
+	SrcPath  string
 	Query    string
 	Body     []byte
 	Share    bool
diff --git a/godoc/server.go b/godoc/server.go
index ffe5997..c9b4056 100644
--- a/godoc/server.go
+++ b/godoc/server.go
@@ -579,7 +579,8 @@
 	fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
 
 	p.ServePage(w, Page{
-		Title:    title + " " + relpath,
+		Title:    title,
+		SrcPath:  relpath,
 		Tabtitle: relpath,
 		Body:     buf.Bytes(),
 		Share:    allowShare(r),
@@ -649,7 +650,8 @@
 	}
 
 	p.ServePage(w, Page{
-		Title:    "Directory " + relpath,
+		Title:    "Directory",
+		SrcPath:  relpath,
 		Tabtitle: relpath,
 		Body:     applyTemplate(p.DirlistHTML, "dirlistHTML", list),
 		Share:    allowShare(r),
diff --git a/godoc/static/godoc.html b/godoc/static/godoc.html
index a700982..92b10aa 100644
--- a/godoc/static/godoc.html
+++ b/godoc/static/godoc.html
@@ -65,13 +65,23 @@
 <div id="page"{{if .Title}} class="wide"{{end}}>
 <div class="container">
 
-{{with .Title}}
-  <h1>{{html .}}</h1>
+{{if or .Title .SrcPath}}
+  <h1>
+    {{html .Title}}
+    {{html .SrcPath | srcBreadcrumb}}
+  </h1>
 {{end}}
+
 {{with .Subtitle}}
   <h2>{{html .}}</h2>
 {{end}}
 
+{{with .SrcPath}}
+  <h2>
+    Documentation: {{html . | srcToPkgLink}}
+  </h2>
+{{end}}
+
 {{/* The Table of Contents is automatically inserted in this <div>.
      Do not delete this <div>. */}}
 <div id="nav"></div>
diff --git a/godoc/static/static.go b/godoc/static/static.go
index c8915765..862ea10 100644
--- a/godoc/static/static.go
+++ b/godoc/static/static.go
@@ -529,13 +529,23 @@
 <div id="page"{{if .Title}} class="wide"{{end}}>
 <div class="container">
 
-{{with .Title}}
-  <h1>{{html .}}</h1>
+{{if or .Title .SrcPath}}
+  <h1>
+    {{html .Title}}
+    {{html .SrcPath | srcBreadcrumb}}
+  </h1>
 {{end}}
+
 {{with .Subtitle}}
   <h2>{{html .}}</h2>
 {{end}}
 
+{{with .SrcPath}}
+  <h2>
+    Documentation: {{html . | srcToPkgLink}}
+  </h2>
+{{end}}
+
 {{/* The Table of Contents is automatically inserted in this <div>.
      Do not delete this <div>. */}}
 <div id="nav"></div>
@@ -2996,6 +3006,9 @@
 	font-size: 28px;
 	line-height: 1;
 }
+h1 .text-muted {
+  color:#777;
+}
 h2 {
 	font-size: 20px;
 	background: #E0EBF5;
diff --git a/godoc/static/style.css b/godoc/static/style.css
index e89ac29..25933a3 100644
--- a/godoc/static/style.css
+++ b/godoc/static/style.css
@@ -101,6 +101,9 @@
 	font-size: 28px;
 	line-height: 1;
 }
+h1 .text-muted {
+	color:#777;
+}
 h2 {
 	font-size: 20px;
 	background: #E0EBF5;