blob: b0121867d7a858175d23d698fbc04da015ec34c4 [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"
}
// 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")
}