internal/frontend: trim prefix from nested modules

The directories section is refactored to display content using the new
NestedModule and Subdirectory structs, rather than the old
createDirectory.

The path prefix is now trimmed from nested modules. For example,
golang.org/x/tools/gopls is displayed as "module gopls" on the
golang.org/x/tools page, instead of "module golang.org/x/gopls".

Change-Id: I6e1a160bec42055c78d566e6ed4bc14d542bbfd3
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/259319
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
diff --git a/content/static/html/helpers/_unit_directories.tmpl b/content/static/html/helpers/_unit_directories.tmpl
index ebdfc26..a0772c2 100644
--- a/content/static/html/helpers/_unit_directories.tmpl
+++ b/content/static/html/helpers/_unit_directories.tmpl
@@ -9,16 +9,16 @@
     <h2 class="UnitDirectories-title">
       <img height="25px" width="20px" src="/static/img/pkg-icon-folder_20x16.svg">Directories
     </h2>
-    {{if .Packages}}
+    {{if (or .Subdirectories .NestedModules) }}
       <table class="UnitDirectories-table">
         <tr class="UnitDirectories-tableHeader">
           <th>Path</th>
           <th>Synopsis</th>
         </tr>
-        {{range .Packages}}
+        {{range .Subdirectories}}
           <tr>
             <td>
-              <a href="{{.URL}}">{{.PathAfterDirectory}}</a>
+              <a href="{{.URL}}">{{.Suffix}}</a>
             </td>
             <td>{{.Synopsis}}</td>
           </tr>
@@ -27,7 +27,7 @@
           <tr>
             <td>
               <span class="UnitDirectories-moduleTag">MODULE</span>
-              <a href="/{{.ModulePath}}">{{.ModulePath}}</a>
+              <a href="{{.URL}}">{{.Suffix}}</a>
             </td>
             <td></td>
           </tr>
@@ -35,4 +35,4 @@
       </table>
     {{end}}
   </div>
-{{end}}
\ No newline at end of file
+{{end}}
diff --git a/content/static/html/helpers/_unit_outline.tmpl b/content/static/html/helpers/_unit_outline.tmpl
index dfb2116..00f53ec 100644
--- a/content/static/html/helpers/_unit_outline.tmpl
+++ b/content/static/html/helpers/_unit_outline.tmpl
@@ -39,7 +39,7 @@
       <div class="UnitOutline-panel js-accordionPanel"
           id="files-panel" role="region" aria-labelledby="files-accordion" aria-hidden="true"></div>
     {{end}}
-    {{if (or .Packages .NestedModules)}}
+    {{if (or .Subdirectories .NestedModules)}}
       <a class="UnitOutline-accordion js-accordionTrigger" href="#directories-top"
           aria-expanded="false" aria-controls="directories-panel" id="directories-accordion">
         Directories
diff --git a/content/static/html/pages/unit_details.tmpl b/content/static/html/pages/unit_details.tmpl
index ec48528..53205df 100644
--- a/content/static/html/pages/unit_details.tmpl
+++ b/content/static/html/pages/unit_details.tmpl
@@ -30,7 +30,7 @@
         {{if .SourceFiles}}
           {{block "unit_files" .}}{{end}}
         {{end}}
-        {{if (or .Packages .NestedModules)}}
+        {{if (or .Subdirectories .NestedModules)}}
           {{block "unit_directories" .}}{{end}}
         {{end}}
       {{else}}
diff --git a/internal/frontend/unit.go b/internal/frontend/unit.go
index 67161d3..e03150a 100644
--- a/internal/frontend/unit.go
+++ b/internal/frontend/unit.go
@@ -7,7 +7,9 @@
 import (
 	"context"
 	"net/http"
+	"sort"
 	"strconv"
+	"strings"
 
 	"github.com/google/safehtml"
 	"golang.org/x/pkgsite/internal"
@@ -23,9 +25,15 @@
 // UnitPage contains data needed to render the unit template.
 type UnitPage struct {
 	basePage
-	Unit          *internal.Unit
-	NestedModules []*internal.ModuleInfo
-	Packages      []*Package
+	// Unit is the unit for this page.
+	Unit *internal.Unit
+
+	// NestedModules are nested modules relative to the path for the unit.
+	NestedModules []*NestedModule
+
+	// Subdirectories are packages in subdirectories relative to the path for
+	// the unit.
+	Subdirectories []*Subdirectory
 
 	// Breadcrumb contains data used to render breadcrumb UI elements.
 	Breadcrumb breadcrumb
@@ -94,11 +102,27 @@
 	SourceFiles []*File
 }
 
+// File is a source file for a package.
 type File struct {
 	Name string
 	URL  string
 }
 
+// NestedModule is a nested module relative to the path of a given unit.
+// This content is used in the Directories section of the unit page.
+type NestedModule struct {
+	Suffix string // suffix after the unit path
+	URL    string
+}
+
+// Subdirectory is a package in a subdirectory relative to the path of a given
+// unit. This content is used in the Directories section of the unit page.
+type Subdirectory struct {
+	Suffix   string
+	URL      string
+	Synopsis string
+}
+
 var (
 	unitTabs = []TabSettings{
 		{
@@ -166,12 +190,11 @@
 		}
 	}
 
-	nestedModules, err := ds.GetNestedModules(ctx, unit.ModulePath)
+	nestedModules, err := getNestedModules(ctx, ds, &unit.UnitMeta)
 	if err != nil {
 		return err
 	}
-
-	dir, err := createDirectory(unit.Path, moduleInfo(unit), unit.Subdirectories, nestedModules, unit.Licenses, false)
+	subdirectories := getSubdirectories(&unit.UnitMeta, unit.Subdirectories)
 	if err != nil {
 		return err
 	}
@@ -230,14 +253,14 @@
 	canShowDetails := unit.IsRedistributable || tabSettings.AlwaysShowDetails
 	_, expandReadme := r.URL.Query()["readme"]
 	page := UnitPage{
-		basePage:      basePage,
-		Unit:          unit,
-		Packages:      dir.Packages,
-		NestedModules: nestedModules,
-		Breadcrumb:    displayBreadcrumb(unit, requestedVersion),
-		Title:         title,
-		Tabs:          unitTabs,
-		SelectedTab:   tabSettings,
+		basePage:       basePage,
+		Unit:           unit,
+		Subdirectories: subdirectories,
+		NestedModules:  nestedModules,
+		Breadcrumb:     displayBreadcrumb(unit, requestedVersion),
+		Title:          title,
+		Tabs:           unitTabs,
+		SelectedTab:    tabSettings,
 		CanonicalURLPath: constructPackageURL(
 			unit.Path,
 			unit.ModulePath,
@@ -335,3 +358,40 @@
 	bc.Links = append([]link{{Href: "/", Body: "Discover Packages"}}, bc.Links...)
 	return bc
 }
+
+func getNestedModules(ctx context.Context, ds internal.DataSource, um *internal.UnitMeta) ([]*NestedModule, error) {
+	nestedModules, err := ds.GetNestedModules(ctx, um.ModulePath)
+	if err != nil {
+		return nil, err
+	}
+	var mods []*NestedModule
+	for _, m := range nestedModules {
+		if m.SeriesPath() == internal.SeriesPathForModule(um.ModulePath) {
+			continue
+		}
+		if !strings.HasPrefix(m.ModulePath, um.Path+"/") {
+			continue
+		}
+		mods = append(mods, &NestedModule{
+			URL:    constructPackageURL(m.ModulePath, m.ModulePath, internal.LatestVersion),
+			Suffix: internal.Suffix(m.SeriesPath(), um.Path),
+		})
+	}
+	return mods, nil
+}
+
+func getSubdirectories(um *internal.UnitMeta, pkgs []*internal.PackageMeta) []*Subdirectory {
+	var sdirs []*Subdirectory
+	for _, pm := range pkgs {
+		if um.Path == pm.Path {
+			continue
+		}
+		sdirs = append(sdirs, &Subdirectory{
+			URL:      constructPackageURL(pm.Path, um.ModulePath, um.Version),
+			Suffix:   internal.Suffix(pm.Path, um.Path),
+			Synopsis: pm.Synopsis,
+		})
+	}
+	sort.Slice(sdirs, func(i, j int) bool { return sdirs[i].Suffix < sdirs[j].Suffix })
+	return sdirs
+}
diff --git a/internal/frontend/unit_test.go b/internal/frontend/unit_test.go
new file mode 100644
index 0000000..72a744a
--- /dev/null
+++ b/internal/frontend/unit_test.go
@@ -0,0 +1,88 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package frontend
+
+import (
+	"context"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"golang.org/x/pkgsite/internal"
+	"golang.org/x/pkgsite/internal/postgres"
+	"golang.org/x/pkgsite/internal/testing/sample"
+)
+
+func TestGetNestedModules(t *testing.T) {
+	ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
+	defer cancel()
+	defer postgres.ResetTestDB(testDB, t)
+
+	for _, m := range []*internal.Module{
+		sample.LegacyModule("cloud.google.com/go", "v0.46.2", "storage", "spanner", "pubsub"),
+		sample.LegacyModule("cloud.google.com/go/pubsub", "v1.6.1", sample.Suffix),
+		sample.LegacyModule("cloud.google.com/go/spanner", "v1.9.0", sample.Suffix),
+		sample.LegacyModule("cloud.google.com/go/storage", "v1.10.0", sample.Suffix),
+		sample.LegacyModule("cloud.google.com/go/storage/v11", "v11.0.0", sample.Suffix),
+		sample.LegacyModule("cloud.google.com/go/storage/v9", "v9.0.0", sample.Suffix),
+		sample.LegacyModule("cloud.google.com/go/storage/module", "v1.10.0", sample.Suffix),
+		sample.LegacyModule("cloud.google.com/go/v2", "v2.0.0", "storage", "spanner", "pubsub"),
+	} {
+		if err := testDB.InsertModule(ctx, m); err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	for _, tc := range []struct {
+		modulePath string
+		want       []*NestedModule
+	}{
+		{
+			modulePath: "cloud.google.com/go",
+			want: []*NestedModule{
+				{
+					Suffix: "pubsub",
+					URL:    "/cloud.google.com/go/pubsub",
+				},
+				{
+					Suffix: "spanner",
+					URL:    "/cloud.google.com/go/spanner",
+				},
+				{
+					Suffix: "storage",
+					URL:    "/cloud.google.com/go/storage/v11",
+				},
+				{
+					Suffix: "storage/module",
+					URL:    "/cloud.google.com/go/storage/module",
+				},
+			},
+		},
+		{
+			modulePath: "cloud.google.com/go/spanner",
+		},
+		{
+			modulePath: "cloud.google.com/go/storage",
+			want: []*NestedModule{
+				{
+					Suffix: "module",
+					URL:    "/cloud.google.com/go/storage/module",
+				},
+			},
+		},
+	} {
+		t.Run(tc.modulePath, func(t *testing.T) {
+			got, err := getNestedModules(ctx, testDB, &internal.UnitMeta{
+				Path:       tc.modulePath,
+				ModulePath: tc.modulePath,
+			})
+			if err != nil {
+				t.Fatal(err)
+			}
+			if diff := cmp.Diff(tc.want, got); diff != "" {
+				t.Errorf("mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}