blob: ae6e87ed3da380b1f1b1f4069ff1cc3a993f3316 [file] [log] [blame]
// 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"
"net/http"
"path"
"sort"
"strconv"
"strings"
"github.com/google/safehtml"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/experiment"
"golang.org/x/pkgsite/internal/godoc"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/middleware"
"golang.org/x/pkgsite/internal/postgres"
"golang.org/x/pkgsite/internal/stdlib"
)
// UnitPage contains data needed to render the unit template.
type UnitPage struct {
basePage
// 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
// Title is the title of the page.
Title string
// CanonicalURLPath is the representation of the URL path for the details
// page, after the requested version and module path have been resolved.
// For example, if the latest version of /my.module/pkg is version v1.5.2,
// the canonical url for that path would be /my.module@v1.5.2/pkg
CanonicalURLPath string
// Licenses contains license metadata used in the header.
Licenses []LicenseMetadata
// Elapsed time since this version was committed.
LastCommitTime string
// The version string formatted for display.
DisplayVersion string
// LinkVersion is version string suitable for links used to compute
// latest badges.
LinkVersion string
// LatestURL is a url pointing to the latest version of a unit.
LatestURL 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
// UnitContentName is the display name of the selected unit content template".
UnitContentName string
// Readme is the rendered readme HTML.
Readme safehtml.HTML
// ExpandReadme is holds the expandable readme state.
ExpandReadme bool
// Tabs contains data to render the varioius tabs on each details page.
Tabs []TabSettings
// Settings contains settings for the selected tab.
SelectedTab TabSettings
// PackageDetails contains data used to render the
// versions, licenses, imports, and importedby tabs.
PackageDetails interface{}
// ImportedByCount is the number of packages that import this path.
// When the count is > limit it will read as 'limit+'. This field
// is not supported when using a datasource proxy.
ImportedByCount string
DocBody safehtml.HTML
DocOutline safehtml.HTML
MobileOutline safehtml.HTML
// SourceFiles contains .go files for the package.
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{
{
Name: tabDetails,
DisplayName: "Details",
TemplateName: "unit_details.tmpl",
},
{
Name: tabVersions,
AlwaysShowDetails: true,
DisplayName: "Versions",
TemplateName: "unit_versions.tmpl",
},
{
Name: tabImports,
AlwaysShowDetails: true,
DisplayName: "Imports",
TemplateName: "unit_imports.tmpl",
},
{
Name: tabImportedBy,
AlwaysShowDetails: true,
DisplayName: "Imported By",
TemplateName: "unit_importedby.tmpl",
},
{
Name: tabLicenses,
DisplayName: "Licenses",
TemplateName: "unit_licenses.tmpl",
},
}
unitTabLookup = make(map[string]TabSettings, len(unitTabs))
)
func init() {
for _, t := range unitTabs {
unitTabLookup[t.Name] = t
}
}
// serveUnitPage serves a unit page for a path using the paths,
// modules, documentation, readmes, licenses, and package_imports tables.
func (s *Server) serveUnitPage(ctx context.Context, w http.ResponseWriter, r *http.Request,
ds internal.DataSource, um *internal.UnitMeta, requestedVersion string) (err error) {
defer derrors.Wrap(&err, "serveUnitPage(ctx, w, r, ds, %v, %q)", um, requestedVersion)
unit, err := ds.GetUnit(ctx, um, internal.AllFields)
if err != nil {
return err
}
// importedByCount is not supported when using a datasource proxy.
importedByCount := "0"
db, ok := ds.(*postgres.DB)
if ok {
importedBy, err := db.GetImportedBy(ctx, unit.Path, unit.ModulePath, importedByLimit)
if err != nil {
return err
}
// If we reached the query limit, then we don't know the total
// and we'll indicate that with a '+'. For example, if the limit
// is 101 and we get 101 results, then we'll show '100+ Imported by'.
importedByCount = strconv.Itoa(len(importedBy))
if len(importedBy) == importedByLimit {
importedByCount = strconv.Itoa(len(importedBy)-1) + "+"
}
}
nestedModules, err := getNestedModules(ctx, ds, &unit.UnitMeta)
if err != nil {
return err
}
subdirectories := getSubdirectories(&unit.UnitMeta, unit.Subdirectories)
if err != nil {
return err
}
readme, err := readmeContent(ctx, unit)
if err != nil {
return err
}
var (
docBody, docOutline, mobileOutline safehtml.HTML
files []*File
)
if unit.Documentation != nil {
docHTML := getHTML(ctx, unit)
// TODO: Deprecate godoc.Parse. The sidenav and body can
// either be rendered using separate functions, or all this content can
// be passed to the template via the UnitPage struct.
b, err := godoc.Parse(docHTML, godoc.BodySection)
if err != nil {
return err
}
docBody = b
o, err := godoc.Parse(docHTML, godoc.SidenavSection)
if err != nil {
return err
}
docOutline = o
m, err := godoc.Parse(docHTML, godoc.SidenavMobileSection)
if err != nil {
return err
}
mobileOutline = m
files, err = sourceFiles(unit)
if err != nil {
return err
}
}
tab := r.FormValue("tab")
if tab == "" {
// Default to details tab when there is no tab param.
tab = tabDetails
}
tabSettings, ok := unitTabLookup[tab]
if !ok {
// Redirect to clean URL path when tab param is invalid.
http.Redirect(w, r, r.URL.Path, http.StatusFound)
return nil
}
title, pageType := pageInfo(unit)
basePage := s.newBasePage(r, title)
basePage.AllowWideContent = true
canShowDetails := unit.IsRedistributable || tabSettings.AlwaysShowDetails
_, expandReadme := r.URL.Query()["readme"]
page := UnitPage{
basePage: basePage,
Unit: unit,
Subdirectories: subdirectories,
NestedModules: nestedModules,
Breadcrumb: displayBreadcrumb(unit, requestedVersion),
Title: title,
Tabs: unitTabs,
SelectedTab: tabSettings,
CanonicalURLPath: constructPackageURL(
unit.Path,
unit.ModulePath,
requestedVersion,
),
Licenses: transformLicenseMetadata(unit.Licenses),
LastCommitTime: elapsedTime(unit.CommitTime),
DisplayVersion: displayVersion(unit.Version, unit.ModulePath),
LinkVersion: linkVersion(unit.Version, unit.ModulePath),
LatestURL: constructPackageURL(unit.Path, unit.ModulePath, middleware.LatestMinorVersionPlaceholder),
PageType: pageType,
CanShowDetails: canShowDetails,
UnitContentName: tabSettings.DisplayName,
Readme: readme,
ExpandReadme: expandReadme,
DocOutline: docOutline,
DocBody: docBody,
SourceFiles: files,
MobileOutline: mobileOutline,
ImportedByCount: importedByCount,
}
if tab != tabDetails {
packageDetails, err := fetchDetailsForPackage(r, tab, ds, &unit.UnitMeta)
if err != nil {
return err
}
page.PackageDetails = packageDetails
}
s.servePage(ctx, w, tabSettings.TemplateName, page)
return nil
}
func getHTML(ctx context.Context, u *internal.Unit) safehtml.HTML {
if experiment.IsActive(ctx, internal.ExperimentFrontendRenderDoc) && len(u.Documentation.Source) > 0 {
dd, err := renderDoc(ctx, u)
if err != nil {
log.Errorf(ctx, "render doc failed: %v", err)
// Fall through to use stored doc.
} else {
return dd.Documentation
}
}
return u.Documentation.HTML
}
// moduleInfo extracts module info from a unit. This is a shim
// for functions ReadmeHTML and createDirectory that will be removed
// when we complete the switch to units.
func moduleInfo(unit *internal.Unit) *internal.ModuleInfo {
return &internal.ModuleInfo{
ModulePath: unit.ModulePath,
Version: unit.Version,
CommitTime: unit.CommitTime,
IsRedistributable: unit.IsRedistributable,
SourceInfo: unit.SourceInfo,
}
}
// readmeContent renders the readme to html.
func readmeContent(ctx context.Context, unit *internal.Unit) (safehtml.HTML, error) {
if unit.IsRedistributable && unit.Readme != nil {
mi := moduleInfo(unit)
readme, err := ReadmeHTML(ctx, mi, unit.Readme)
if err != nil {
return safehtml.HTML{}, err
}
return readme, nil
}
return safehtml.HTML{}, nil
}
// pageInfo determines the title and pageType for a given unit.
func pageInfo(unit *internal.Unit) (title string, pageType string) {
if unit.Path == stdlib.ModulePath {
return "Standard library", pageTypeStdLib
}
if unit.IsCommand() {
return effectiveName(unit.Path, unit.Name), pageTypeCommand
}
if unit.IsPackage() {
return unit.Name, pageTypePackage
}
if unit.IsModule() {
return path.Base(unit.Path), pageTypeModule
}
return path.Base(unit.Path) + "/", pageTypeDirectory
}
// displayBreadcrumbs appends additional breadcrumb links for display
// to those for the given unit.
func displayBreadcrumb(unit *internal.Unit, requestedVersion string) breadcrumb {
bc := breadcrumbPath(unit.Path, unit.ModulePath, requestedVersion)
if unit.ModulePath == stdlib.ModulePath && unit.Path != stdlib.ModulePath {
bc.Links = append([]link{{Href: "/std", Body: "Standard library"}}, bc.Links...)
}
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
}