blob: cdf3c14aeb574a601c4eb7270ce861c32898e47f [file] [log] [blame]
// Copyright 2019 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 (
"fmt"
"path"
"strings"
"time"
"golang.org/x/mod/module"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/licenses"
"golang.org/x/pkgsite/internal/middleware"
"golang.org/x/pkgsite/internal/stdlib"
)
// Package contains information for an individual package.
type Package struct {
Module
Path string // full import path
URL string // relative to this site
LatestURL string // link with latest-version placeholder, relative to this site
IsRedistributable bool
Licenses []LicenseMetadata
PathAfterDirectory string // for display on the directories tab; used by Directory
Synopsis string // for display on the directories tab; used by Directory
}
// Module contains information for an individual module.
type Module struct {
DisplayVersion string
LinkVersion string
ModulePath string
CommitTime string
IsRedistributable bool
URL string // relative to this site
LatestURL string // link with latest-version placeholder, relative to this site
Licenses []LicenseMetadata
}
// createPackage returns a *Package based on the fields of the specified
// internal package and version info.
//
// latestRequested indicates whether the user requested the latest
// version of the package. If so, the returned Package.URL will have the
// structure /<path> instead of /<path>@<version>.
func createPackage(pkg *internal.PackageMeta, mi *internal.ModuleInfo, latestRequested bool) (_ *Package, err error) {
defer derrors.Wrap(&err, "createPackage(%v, %v, %t)", pkg, mi, latestRequested)
var modLicenses []*licenses.Metadata
for _, lm := range pkg.Licenses {
if path.Dir(lm.FilePath) == "." {
modLicenses = append(modLicenses, lm)
}
}
m := createModule(mi, modLicenses, latestRequested)
urlVersion := m.LinkVersion
if latestRequested {
urlVersion = internal.LatestVersion
}
return &Package{
Path: pkg.Path,
IsRedistributable: pkg.IsRedistributable,
Licenses: transformLicenseMetadata(pkg.Licenses),
Module: *m,
URL: constructPackageURL(pkg.Path, mi.ModulePath, urlVersion),
LatestURL: constructPackageURL(pkg.Path, mi.ModulePath, middleware.LatestMinorVersionPlaceholder),
}, nil
}
// createModule returns a *Module based on the fields of the specified
// versionInfo.
//
// latestRequested indicates whether the user requested the latest
// version of the package. If so, the returned Module.URL will have the
// structure /<path> instead of /<path>@<version>.
func createModule(mi *internal.ModuleInfo, licmetas []*licenses.Metadata, latestRequested bool) *Module {
urlVersion := linkVersion(mi.Version, mi.ModulePath)
if latestRequested {
urlVersion = internal.LatestVersion
}
return &Module{
DisplayVersion: displayVersion(mi.Version, mi.ModulePath),
LinkVersion: linkVersion(mi.Version, mi.ModulePath),
ModulePath: mi.ModulePath,
CommitTime: elapsedTime(mi.CommitTime),
IsRedistributable: mi.IsRedistributable,
Licenses: transformLicenseMetadata(licmetas),
URL: constructModuleURL(mi.ModulePath, urlVersion),
LatestURL: constructModuleURL(mi.ModulePath, middleware.LatestMinorVersionPlaceholder),
}
}
func constructModuleURL(modulePath, linkVersion string) string {
url := "/"
if modulePath != stdlib.ModulePath {
url += "mod/"
}
url += modulePath
if linkVersion != internal.LatestVersion {
url += "@" + linkVersion
}
return url
}
func constructPackageURL(pkgPath, modulePath, linkVersion string) string {
if linkVersion == internal.LatestVersion {
return "/" + pkgPath
}
if pkgPath == modulePath || modulePath == stdlib.ModulePath {
return fmt.Sprintf("/%s@%s", pkgPath, linkVersion)
}
return fmt.Sprintf("/%s@%s/%s", modulePath, linkVersion, strings.TrimPrefix(pkgPath, modulePath+"/"))
}
// effectiveName returns either the command name or package name.
func effectiveName(pkgPath, pkgName string) string {
if pkgName != "main" {
return pkgName
}
var prefix string // package path without version
if pkgPath[len(pkgPath)-3:] == "/v1" {
prefix = pkgPath[:len(pkgPath)-3]
} else {
prefix, _, _ = module.SplitPathVersion(pkgPath)
}
_, base := path.Split(prefix)
return base
}
// packageHTMLTitle constructs the details page title for pkg.
// The string will appear in the <title> element (and thus
// the browser tab).
func packageHTMLTitle(pkgPath, pkgName string) string {
if pkgName != "main" {
return pkgName + " package"
}
return effectiveName(pkgPath, pkgName) + " command"
}
type breadcrumb struct {
Links []link
Current string
CopyData string
}
type link struct {
Href, Body string
}
// breadcrumbPath builds HTML that displays pkgPath as a sequence of links
// to its parents.
// pkgPath is a slash-separated path, and may be a package import path or a directory.
// modPath is the package's module path. This will be a prefix of pkgPath, except
// within the standard library.
// version is the version for the module, or LatestVersion.
//
// See TestBreadcrumbPath for examples.
func breadcrumbPath(pkgPath, modPath, requestedVersion string) breadcrumb {
if pkgPath == stdlib.ModulePath {
return breadcrumb{Current: "Standard library"}
}
// Obtain successive prefixes of pkgPath, stopping at modPath,
// or for the stdlib, at the end.
minLen := len(modPath) - 1
if modPath == stdlib.ModulePath {
minLen = 1
}
var dirs []string
for dir := pkgPath; len(dir) > minLen && len(path.Dir(dir)) < len(dir); dir = path.Dir(dir) {
dirs = append(dirs, dir)
}
// Construct the path elements of the result.
// They will be in reverse order of dirs.
// The first dir is the current page. If it is the only one, leave it
// as is. Otherwise, use its base. In neither case does it get a link.
d := dirs[0]
if len(dirs) > 1 {
d = path.Base(d)
}
b := breadcrumb{Current: d}
// Make all the other parts into links.
b.Links = make([]link, len(dirs)-1)
for i := 1; i < len(dirs); i++ {
href := "/" + dirs[i]
if requestedVersion != internal.LatestVersion {
href += "@" + linkVersion(requestedVersion, modPath)
}
el := dirs[i]
if i != len(dirs)-1 {
el = path.Base(el)
}
b.Links[len(b.Links)-i] = link{href, el}
}
// Add a "copy" button for the path.
b.CopyData = pkgPath
return b
}
// moduleHTMLTitle constructs the <title> contents, for tabs in the browser.
func moduleHTMLTitle(modulePath string) string {
if modulePath == stdlib.ModulePath {
return "stdlib"
}
return modulePath + " module"
}
// elapsedTime takes a date and returns returns human-readable,
// relative timestamps based on the following rules:
// (1) 'X hours ago' when X < 6
// (2) 'today' between 6 hours and 1 day ago
// (3) 'Y days ago' when Y < 6
// (4) A date formatted like "Jan 2, 2006" for anything further back
func elapsedTime(date time.Time) string {
elapsedHours := int(time.Since(date).Hours())
if elapsedHours == 1 {
return "1 hour ago"
} else if elapsedHours < 6 {
return fmt.Sprintf("%d hours ago", elapsedHours)
}
elapsedDays := elapsedHours / 24
if elapsedDays < 1 {
return "today"
} else if elapsedDays == 1 {
return "1 day ago"
} else if elapsedDays < 6 {
return fmt.Sprintf("%d days ago", elapsedDays)
}
return date.Format("Jan _2, 2006")
}