content/static,internal/frontend: add more granular page types

This change specifies more granular page types primarily for use
by the details pages to differentiate between the type of page
(module, package, etc.) and the name of the item being viewed.

This change also hides the type of the item in the fixed header
when it is not wide enough to show other information.

Change-Id: I1e25e75dbdbac01b5d54ea96224f3464cf9fa9f9
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/245487
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/content/static/css/stylesheet.css b/content/static/css/stylesheet.css
index f01cee9..fafda51 100644
--- a/content/static/css/stylesheet.css
+++ b/content/static/css/stylesheet.css
@@ -972,6 +972,20 @@
   text-overflow: ellipsis;
   white-space: nowrap;
 }
+.DetailsNavFixed-titleType {
+  display: none;
+}
+.DetailsNavFixed-titleType--small {
+  display: inline;
+}
+@media only screen and (min-width: 25rem) {
+  .DetailsNavFixed-titleType {
+    display: inline;
+  }
+  .DetailsNavFixed-titleType--small {
+    display: none;
+  }
+}
 .DetailsNavFixed .CopyToClipboardButton {
   top: 0.1875rem;
 }
diff --git a/content/static/html/pages/details.tmpl b/content/static/html/pages/details.tmpl
index b3d7cf9..4d6a67c 100644
--- a/content/static/html/pages/details.tmpl
+++ b/content/static/html/pages/details.tmpl
@@ -27,11 +27,26 @@
     {{end}}
     </div>
     <div class="DetailsHeader-main">
-      <h1 class="DetailsHeader-title">{{.Title}}</h1>
+      <h1 class="DetailsHeader-title">
+        {{if eq .PageType "std"}}
+          Standard library
+        {{else}}
+          {{if eq $pageType "mod"}}
+            Module
+          {{else if eq $pageType "dir"}}
+            Directory
+          {{else if eq $pageType "pkg"}}
+            Package
+          {{else if eq $pageType "cmd"}}
+            Command
+          {{end}}
+          {{.Name}}
+        {{end}}
+      </h1>
       <div class="DetailsHeader-version">{{$header.DisplayVersion}}</div>
 
       {{$ppath := ""}}
-      {{if ne $pageType "mod"}}
+      {{if and (ne $pageType "mod") (ne $pageType "std")}}
          {{$ppath = $header.Path}}
       {{end}}
       <!-- Do not reformat the data attributes of the following div: the server uses a regexp to extract them. -->
@@ -54,7 +69,7 @@
           <a href="/license-policy" class="Disclaimer-link"><em>not legal advice</em></a>
         {{end}}
       </span>
-      {{if or (eq $pageType "pkg") (eq $pageType "dir")}}
+      {{if or (eq $pageType "pkg") (eq $pageType "dir") (eq $pageType "cmd")}}
         <span class="DetailsHeader-infoLabelDivider">|</span>
         {{if eq $header.ModulePath "std"}}
           <a data-test-id="DetailsHeader-infoLabelModule" href="{{$header.Module.URL}}">Standard library</a>
@@ -105,7 +120,23 @@
       </a>
       <div class="DetailsNavFixed-moduleInfo">
         <span class="DetailsNavFixed-title">
-          {{.Title}}
+          {{if ne $pageType "std"}}
+            <span class="DetailsNavFixed-titleType">
+              {{if eq $pageType "mod"}}
+                Module
+              {{else if eq $pageType "dir"}}
+                Directory
+              {{else if eq $pageType "pkg"}}
+                Package
+              {{else if eq $pageType "cmd"}}
+                Command
+              {{end}}
+            </span>
+            <span class="DetailsNavFixed-titleName">{{.Name}}</span>
+          {{else}}
+            <span class="DetailsNavFixed-titleType">Standard library</span>
+            <span class="DetailsNavFixed-titleType DetailsNavFixed-titleType--small">StdLib</span>
+          {{end}}
         </span>
         {{with .Breadcrumb}}
           {{if .CopyData}}
diff --git a/internal/frontend/details.go b/internal/frontend/details.go
index 45e0545..7125d28 100644
--- a/internal/frontend/details.go
+++ b/internal/frontend/details.go
@@ -25,19 +25,42 @@
 // DetailsPage contains data for a package of module details template.
 type DetailsPage struct {
 	basePage
-	Title          string
-	CanShowDetails bool
-	Settings       TabSettings
-	Details        interface{}
-	Header         interface{}
-	Breadcrumb     breadcrumb
-	Tabs           []TabSettings
 
-	// PageType is either "mod", "dir", or "pkg" depending on the details
-	// handler.
+	// Name is the name of the package or command name, or the full
+	// directory or module path.
+	Name string
+
+	// PageType is the type of page (pkg, cmd, dir, etc.).
 	PageType string
+
+	// CanShowDetails indicates whether details can be shown or must be
+	// hidden due to issues like license restrictions.
+	CanShowDetails bool
+
+	// Settings contains tab-specific metadata.
+	Settings TabSettings
+
+	// Details contains data specific to the type of page being rendered.
+	Details interface{}
+
+	// Header contains data to be rendered in the heading of all details pages.
+	Header interface{}
+
+	// Breadcrumb contains data used to render breadcrumb UI elements.
+	Breadcrumb breadcrumb
+
+	// Tabs contains data to render the varioius tabs on each details page.
+	Tabs []TabSettings
 }
 
+const (
+	pageTypeModule    = "mod"
+	pageTypeDirectory = "dir"
+	pageTypePackage   = "pkg"
+	pageTypeCommand   = "cmd"
+	pageTypeStdLib    = stdlib.ModulePath
+)
+
 // serveDetails handles requests for package/directory/module details pages. It
 // expects paths of the form "[/mod]/<module-path>[@<version>?tab=<tab>]".
 // stdlib module pages are handled at "/std", and requests to "/mod/std" will
diff --git a/internal/frontend/directory.go b/internal/frontend/directory.go
index 014e3cc..efcf837 100644
--- a/internal/frontend/directory.go
+++ b/internal/frontend/directory.go
@@ -58,14 +58,14 @@
 	}
 	page := &DetailsPage{
 		basePage:       s.newBasePage(r, fmt.Sprintf("%s directory", vdir.Path)),
-		Title:          fmt.Sprintf("directory %s", vdir.Path),
+		Name:           vdir.Path,
 		Settings:       settings,
 		Header:         header,
 		Breadcrumb:     breadcrumbPath(vdir.Path, vdir.ModulePath, linkVersion(vdir.Version, vdir.ModulePath)),
 		Details:        details,
 		CanShowDetails: true,
 		Tabs:           directoryTabSettings,
-		PageType:       "dir",
+		PageType:       pageTypeDirectory,
 	}
 	s.servePage(ctx, w, settings.TemplateName, page)
 	return nil
@@ -97,14 +97,14 @@
 	}
 	page := &DetailsPage{
 		basePage:       s.newBasePage(r, fmt.Sprintf("%s directory", dbDir.Path)),
-		Title:          fmt.Sprintf("directory %s", dbDir.Path),
+		Name:           dbDir.Path,
 		Settings:       settings,
 		Header:         header,
 		Breadcrumb:     breadcrumbPath(dbDir.Path, dbDir.ModulePath, linkVersion(dbDir.Version, dbDir.ModulePath)),
 		Details:        details,
 		CanShowDetails: true,
 		Tabs:           directoryTabSettings,
-		PageType:       "dir",
+		PageType:       pageTypeDirectory,
 	}
 	s.servePage(ctx, w, settings.TemplateName, page)
 	return nil
diff --git a/internal/frontend/header.go b/internal/frontend/header.go
index 8f6b5ec..d154088 100644
--- a/internal/frontend/header.go
+++ b/internal/frontend/header.go
@@ -144,15 +144,6 @@
 	return effectiveName(pkgPath, pkgName) + " command"
 }
 
-// packageTitle returns the package title as it will
-// appear in the heading at the top of the page.
-func packageTitle(pkgPath, pkgName string) string {
-	if pkgName != "main" {
-		return "package " + pkgName
-	}
-	return "command " + effectiveName(pkgPath, pkgName)
-}
-
 type breadcrumb struct {
 	Links    []link
 	Current  string
@@ -220,15 +211,6 @@
 	return modulePath + " module"
 }
 
-// moduleTitle constructs the title that will appear at the top of the module
-// page.
-func moduleTitle(modulePath string) string {
-	if modulePath == stdlib.ModulePath {
-		return "Standard library"
-	}
-	return "module " + modulePath
-}
-
 // elapsedTime takes a date and returns returns human-readable,
 // relative timestamps based on the following rules:
 // (1) 'X hours ago' when X < 6
diff --git a/internal/frontend/latest_version.go b/internal/frontend/latest_version.go
index cf300ad..9a3ce04 100644
--- a/internal/frontend/latest_version.go
+++ b/internal/frontend/latest_version.go
@@ -38,7 +38,7 @@
 	defer derrors.Wrap(&err, "latestVersion(ctx, %q, %q)", modulePath, packagePath)
 	if experiment.IsActive(ctx, internal.ExperimentUsePathInfo) {
 		fullPath := packagePath
-		if pageType == "mod" {
+		if pageType == pageTypeModule || pageType == pageTypeStdLib {
 			fullPath = modulePath
 		}
 		modulePath, version, _, err := ds.GetPathInfo(ctx, fullPath, modulePath, internal.LatestVersion)
@@ -50,12 +50,12 @@
 
 	var mi *internal.LegacyModuleInfo
 	switch pageType {
-	case "mod":
+	case pageTypeModule, pageTypeStdLib:
 		mi, err = ds.LegacyGetModuleInfo(ctx, modulePath, internal.LatestVersion)
 		if err != nil {
 			return "", err
 		}
-	case "pkg":
+	case pageTypePackage, pageTypeCommand:
 		pkg, err := ds.LegacyGetPackage(ctx, packagePath, modulePath, internal.LatestVersion)
 		if err != nil {
 			return "", err
diff --git a/internal/frontend/module.go b/internal/frontend/module.go
index 76f2705..ee55130 100644
--- a/internal/frontend/module.go
+++ b/internal/frontend/module.go
@@ -13,6 +13,7 @@
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/log"
+	"golang.org/x/pkgsite/internal/stdlib"
 )
 
 // legacyServeModulePage serves details pages for the module specified by modulePath
@@ -71,16 +72,21 @@
 			return fmt.Errorf("error fetching page for %q: %v", tab, err)
 		}
 	}
+	pageType := pageTypeModule
+	if mi.ModulePath == stdlib.ModulePath {
+		pageType = pageTypeStdLib
+	}
+
 	page := &DetailsPage{
 		basePage:       s.newBasePage(r, moduleHTMLTitle(mi.ModulePath)),
-		Title:          moduleTitle(mi.ModulePath),
+		Name:           mi.ModulePath,
 		Settings:       settings,
 		Header:         modHeader,
 		Breadcrumb:     breadcrumbPath(modHeader.ModulePath, modHeader.ModulePath, modHeader.LinkVersion),
 		Details:        details,
 		CanShowDetails: canShowDetails,
 		Tabs:           moduleTabSettings,
-		PageType:       "mod",
+		PageType:       pageType,
 	}
 	s.servePage(ctx, w, settings.TemplateName, page)
 	return nil
diff --git a/internal/frontend/package.go b/internal/frontend/package.go
index e8fc0cb..14ab58a 100644
--- a/internal/frontend/package.go
+++ b/internal/frontend/package.go
@@ -118,9 +118,18 @@
 			return fmt.Errorf("fetching page for %q: %v", tab, err)
 		}
 	}
+
+	var (
+		pageType = pageTypePackage
+		pageName = pkg.Name
+	)
+	if pkg.Name == "main" {
+		pageName = effectiveName(pkg.Path, pkg.Name)
+		pageType = pageTypeCommand
+	}
 	page := &DetailsPage{
 		basePage: s.newBasePage(r, packageHTMLTitle(pkg.Path, pkg.Name)),
-		Title:    packageTitle(pkg.Path, pkg.Name),
+		Name:     pageName,
 		Settings: settings,
 		Header:   pkgHeader,
 		Breadcrumb: breadcrumbPath(pkgHeader.Path, pkgHeader.Module.ModulePath,
@@ -128,7 +137,7 @@
 		Details:        details,
 		CanShowDetails: canShowDetails,
 		Tabs:           packageTabSettings,
-		PageType:       "pkg",
+		PageType:       pageType,
 	}
 	s.servePage(r.Context(), w, settings.TemplateName, page)
 	return nil
@@ -194,9 +203,17 @@
 			return fmt.Errorf("fetching page for %q: %v", tab, err)
 		}
 	}
+	var (
+		pageType = pageTypePackage
+		pageName = vdir.Package.Name
+	)
+	if pageName == "main" {
+		pageName = effectiveName(vdir.Path, vdir.Package.Name)
+		pageType = pageTypeCommand
+	}
 	page := &DetailsPage{
 		basePage: s.newBasePage(r, packageHTMLTitle(vdir.Path, vdir.Package.Name)),
-		Title:    packageTitle(vdir.Path, vdir.Package.Name),
+		Name:     pageName,
 		Settings: settings,
 		Header:   pkgHeader,
 		Breadcrumb: breadcrumbPath(pkgHeader.Path, pkgHeader.Module.ModulePath,
@@ -204,7 +221,7 @@
 		Details:        details,
 		CanShowDetails: canShowDetails,
 		Tabs:           packageTabSettings,
-		PageType:       "pkg",
+		PageType:       pageType,
 	}
 	s.servePage(ctx, w, settings.TemplateName, page)
 	return nil
diff --git a/internal/frontend/server_test.go b/internal/frontend/server_test.go
index 5554096..052559e 100644
--- a/internal/frontend/server_test.go
+++ b/internal/frontend/server_test.go
@@ -204,7 +204,7 @@
 	)
 
 	pkgV100 := &pagecheck.Page{
-		Title:            "package foo",
+		Title:            "Package foo",
 		ModulePath:       sample.ModulePath,
 		Version:          "v1.0.0",
 		Suffix:           "foo",
@@ -227,7 +227,7 @@
 	pkgPseudo := &pp
 
 	pkgInc := &pagecheck.Page{
-		Title:            "package inc",
+		Title:            "Package inc",
 		ModulePath:       "github.com/incompatible",
 		Version:          "v1.0.0+incompatible",
 		Suffix:           "dir/inc",
@@ -240,7 +240,7 @@
 	}
 
 	pkgNonRedist := &pagecheck.Page{
-		Title:            "package bar",
+		Title:            "Package bar",
 		ModulePath:       "github.com/non_redistributable",
 		Version:          "v1.0.0",
 		Suffix:           "bar",
@@ -251,7 +251,7 @@
 		ModuleURL:        "/mod/github.com/non_redistributable",
 	}
 	cmdGo := &pagecheck.Page{
-		Title:            "command go",
+		Title:            "Command go",
 		ModulePath:       "std",
 		Suffix:           "cmd/go",
 		Version:          "go1.13",
@@ -264,7 +264,7 @@
 	}
 	mod := &pagecheck.Page{
 		ModulePath:      sample.ModulePath,
-		Title:           "module " + sample.ModulePath,
+		Title:           "Module " + sample.ModulePath,
 		ModuleURL:       "/mod/" + sample.ModulePath,
 		Version:         "v1.0.0",
 		LicenseType:     "MIT",
@@ -280,7 +280,7 @@
 
 	mod2 := &pagecheck.Page{
 		ModulePath:       "github.com/pseudo",
-		Title:            "module github.com/pseudo",
+		Title:            "Module github.com/pseudo",
 		ModuleURL:        "/mod/github.com/pseudo",
 		LatestLink:       "/mod/github.com/pseudo@" + pseudoVersion,
 		Version:          pseudoVersion,
@@ -291,7 +291,7 @@
 	}
 	dirPseudo := &pagecheck.Page{
 		ModulePath:       "github.com/pseudo",
-		Title:            "directory github.com/pseudo/dir",
+		Title:            "Directory github.com/pseudo/dir",
 		ModuleURL:        "/mod/github.com/pseudo",
 		LatestLink:       "/mod/github.com/pseudo@" + pseudoVersion + "/dir",
 		Suffix:           "dir",
@@ -315,7 +315,7 @@
 	}
 
 	netHttp := &pagecheck.Page{
-		Title:           "package http",
+		Title:           "Package http",
 		ModulePath:      "http",
 		Version:         "go1.13",
 		LicenseType:     "MIT",
@@ -326,7 +326,7 @@
 	}
 
 	dir := &pagecheck.Page{
-		Title:            "directory " + sample.ModulePath + "/foo/directory",
+		Title:            "Directory " + sample.ModulePath + "/foo/directory",
 		ModulePath:       sample.ModulePath,
 		Version:          "v1.0.0",
 		Suffix:           "foo/directory",
@@ -337,7 +337,7 @@
 	}
 
 	dirCmd := &pagecheck.Page{
-		Title:            "directory cmd",
+		Title:            "Directory cmd",
 		ModulePath:       "std",
 		Version:          "go1.13",
 		Suffix:           "cmd",
diff --git a/internal/testing/htmlcheck/htmlcheck.go b/internal/testing/htmlcheck/htmlcheck.go
index de49b84..c4bfc1f 100644
--- a/internal/testing/htmlcheck/htmlcheck.go
+++ b/internal/testing/htmlcheck/htmlcheck.go
@@ -144,6 +144,14 @@
 	return HasText("^" + regexp.QuoteMeta(want) + "$")
 }
 
+// HasExactTextCollapsed returns a checker that checks whether the given string
+// matches the node's text with its leading, trailing, and redundant whitespace
+// trimmed.
+func HasExactTextCollapsed(want string) Checker {
+	re := strings.Join(strings.Fields(strings.TrimSpace(regexp.QuoteMeta(want))), `\s*`)
+	return HasText(`^\s*` + re + `\s*$`)
+}
+
 // nodeText appends the text of n's subtree to b. This is the concatenated
 // contents of all text nodes, visited depth-first.
 func nodeText(n *html.Node, b *strings.Builder) {
diff --git a/internal/testing/htmlcheck/htmlcheck_test.go b/internal/testing/htmlcheck/htmlcheck_test.go
index acbdb67..ce46573 100644
--- a/internal/testing/htmlcheck/htmlcheck_test.go
+++ b/internal/testing/htmlcheck/htmlcheck_test.go
@@ -16,7 +16,13 @@
 		<html>
 			<div id="ID" class="CLASS1 CLASS2">
 			    BEFORE <a href="HREF">DURING</a> AFTER
-		    </div>
+				</div>
+				<div class="WHITESPACE">lots
+
+				of
+
+				whitespace
+				</div>
 		</html>`
 
 	doc, err := html.Parse(strings.NewReader(data))
@@ -32,7 +38,8 @@
 			In("div.CLASS1",
 				HasText(`^\s*BEFORE DURING AFTER\s*$`),
 				HasAttr("id", "ID"),
-				HasAttr("class", `\bCLASS2\b`)),
+				HasAttr("class", `\bCLASS2\b`),
+			),
 		},
 		{
 			"a",
@@ -42,6 +49,10 @@
 			"NotIn",
 			NotIn("#foo"),
 		},
+		{
+			"Redundant whitespace",
+			In("div.WHITESPACE", HasExactTextCollapsed("lots of whitespace")),
+		},
 	} {
 		got := test.checker(doc)
 		if got != nil {
diff --git a/internal/testing/pagecheck/pagecheck.go b/internal/testing/pagecheck/pagecheck.go
index 936dd37..4d47915 100644
--- a/internal/testing/pagecheck/pagecheck.go
+++ b/internal/testing/pagecheck/pagecheck.go
@@ -42,12 +42,13 @@
 }
 
 var (
-	in        = htmlcheck.In
-	inAll     = htmlcheck.InAll
-	text      = htmlcheck.HasText
-	exactText = htmlcheck.HasExactText
-	attr      = htmlcheck.HasAttr
-	href      = htmlcheck.HasHref
+	in                 = htmlcheck.In
+	inAll              = htmlcheck.InAll
+	text               = htmlcheck.HasText
+	exactText          = htmlcheck.HasExactText
+	exactTextCollapsed = htmlcheck.HasExactTextCollapsed
+	attr               = htmlcheck.HasAttr
+	href               = htmlcheck.HasHref
 )
 
 // PackageHeader checks a details page header for a package.
@@ -62,7 +63,7 @@
 	}
 	return in("",
 		in("span.DetailsHeader-breadcrumbCurrent", exactText(curBreadcrumb)),
-		in("h1.DetailsHeader-title", exactText(p.Title)),
+		in("h1.DetailsHeader-title", exactTextCollapsed(p.Title)),
 		in("div.DetailsHeader-version", exactText(fv)),
 		versionBadge(p),
 		licenseInfo(p, packageURLPath(p, versionedURL)),
@@ -82,7 +83,7 @@
 	}
 	return in("",
 		in("span.DetailsHeader-breadcrumbCurrent", exactText(curBreadcrumb)),
-		in("h1.DetailsHeader-title", exactText(p.Title)),
+		in("h1.DetailsHeader-title", exactTextCollapsed(p.Title)),
 		in("div.DetailsHeader-version", exactText(fv)),
 		versionBadge(p),
 		licenseInfo(p, moduleURLPath(p, versionedURL)),
@@ -97,7 +98,7 @@
 	}
 	return in("",
 		in("span.DetailsHeader-breadcrumbCurrent", exactText(path.Base(p.Suffix))),
-		in("h1.DetailsHeader-title", exactText(p.Title)),
+		in("h1.DetailsHeader-title", exactTextCollapsed(p.Title)),
 		in("div.DetailsHeader-version", exactText(fv)),
 		// directory pages don't show a header badge
 		in("div.DetailsHeader-badge", in(".DetailsHeader-badge--unknown")),