hugo/resources/page/page_paths.go
Bjørn Erik Pedersen 241b21b0fd Create a struct with all of Hugo's config options
Primary motivation is documentation, but it will also hopefully simplify the code.

Also,

* Lower case the default output format names; this is in line with the custom ones (map keys) and how
it's treated all the places. This avoids doing `stringds.EqualFold` everywhere.

Closes #10896
Closes #10620
2023-05-16 18:01:29 +02:00

343 lines
8.3 KiB
Go

// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package page
import (
"path"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/output"
)
const slash = "/"
// TargetPathDescriptor describes how a file path for a given resource
// should look like on the file system. The same descriptor is then later used to
// create both the permalinks and the relative links, paginator URLs etc.
//
// The big motivating behind this is to have only one source of truth for URLs,
// and by that also get rid of most of the fragile string parsing/encoding etc.
type TargetPathDescriptor struct {
PathSpec *helpers.PathSpec
Type output.Format
Kind string
Sections []string
// For regular content pages this is either
// 1) the Slug, if set,
// 2) the file base name (TranslationBaseName).
BaseName string
// Source directory.
Dir string
// Typically a language prefix added to file paths.
PrefixFilePath string
// Typically a language prefix added to links.
PrefixLink string
// If in multihost mode etc., every link/path needs to be prefixed, even
// if set in URL.
ForcePrefix bool
// URL from front matter if set. Will override any Slug etc.
URL string
// Used to create paginator links.
Addends string
// The expanded permalink if defined for the section, ready to use.
ExpandedPermalink string
// Some types cannot have uglyURLs, even if globally enabled, RSS being one example.
UglyURLs bool
}
// TODO(bep) move this type.
type TargetPaths struct {
// Where to store the file on disk relative to the publish dir. OS slashes.
TargetFilename string
// The directory to write sub-resources of the above.
SubResourceBaseTarget string
// The base for creating links to sub-resources of the above.
SubResourceBaseLink string
// The relative permalink to this resources. Unix slashes.
Link string
}
func (p TargetPaths) RelPermalink(s *helpers.PathSpec) string {
return s.PrependBasePath(p.Link, false)
}
func (p TargetPaths) PermalinkForOutputFormat(s *helpers.PathSpec, f output.Format) string {
var baseURL urls.BaseURL
var err error
if f.Protocol != "" {
baseURL, err = s.Cfg.BaseURL().WithProtocol(f.Protocol)
if err != nil {
return ""
}
} else {
baseURL = s.Cfg.BaseURL()
}
baseURLstr := baseURL.String()
return s.PermalinkForBaseURL(p.Link, baseURLstr)
}
func isHtmlIndex(s string) bool {
return strings.HasSuffix(s, "/index.html")
}
func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) {
if d.Type.Name == "" {
panic("CreateTargetPath: missing type")
}
// Normalize all file Windows paths to simplify what's next.
if helpers.FilePathSeparator != slash {
d.Dir = filepath.ToSlash(d.Dir)
d.PrefixFilePath = filepath.ToSlash(d.PrefixFilePath)
}
if d.URL != "" && !strings.HasPrefix(d.URL, "/") {
// Treat this as a context relative URL
d.ForcePrefix = true
}
pagePath := slash
fullSuffix := d.Type.MediaType.FirstSuffix.FullSuffix
var (
pagePathDir string
link string
linkDir string
)
// The top level index files, i.e. the home page etc., needs
// the index base even when uglyURLs is enabled.
needsBase := true
isUgly := d.UglyURLs && !d.Type.NoUgly
baseNameSameAsType := d.BaseName != "" && d.BaseName == d.Type.BaseName
if d.ExpandedPermalink == "" && baseNameSameAsType {
isUgly = true
}
if d.Kind != KindPage && d.URL == "" && len(d.Sections) > 0 {
if d.ExpandedPermalink != "" {
pagePath = pjoin(pagePath, d.ExpandedPermalink)
} else {
pagePath = pjoin(d.Sections...)
}
needsBase = false
}
if d.Type.Path != "" {
pagePath = pjoin(pagePath, d.Type.Path)
}
if d.Kind != KindHome && d.URL != "" {
pagePath = pjoin(pagePath, d.URL)
if d.Addends != "" {
pagePath = pjoin(pagePath, d.Addends)
}
pagePathDir = pagePath
link = pagePath
hasDot := strings.Contains(d.URL, ".")
hasSlash := strings.HasSuffix(d.URL, slash)
if hasSlash || !hasDot {
pagePath = pjoin(pagePath, d.Type.BaseName+fullSuffix)
} else if hasDot {
pagePathDir = path.Dir(pagePathDir)
}
if !isHtmlIndex(pagePath) {
link = pagePath
} else if !hasSlash {
link += slash
}
linkDir = pagePathDir
if d.ForcePrefix {
// Prepend language prefix if not already set in URL
if d.PrefixFilePath != "" && !strings.HasPrefix(d.URL, slash+d.PrefixFilePath) {
pagePath = pjoin(d.PrefixFilePath, pagePath)
pagePathDir = pjoin(d.PrefixFilePath, pagePathDir)
}
if d.PrefixLink != "" && !strings.HasPrefix(d.URL, slash+d.PrefixLink) {
link = pjoin(d.PrefixLink, link)
linkDir = pjoin(d.PrefixLink, linkDir)
}
}
} else if d.Kind == KindPage {
if d.ExpandedPermalink != "" {
pagePath = pjoin(pagePath, d.ExpandedPermalink)
} else {
if d.Dir != "" {
pagePath = pjoin(pagePath, d.Dir)
}
if d.BaseName != "" {
pagePath = pjoin(pagePath, d.BaseName)
}
}
if d.Addends != "" {
pagePath = pjoin(pagePath, d.Addends)
}
link = pagePath
// TODO(bep) this should not happen after the fix in https://github.com/gohugoio/hugo/issues/4870
// but we may need some more testing before we can remove it.
if baseNameSameAsType {
link = strings.TrimSuffix(link, d.BaseName)
}
pagePathDir = link
link = link + slash
linkDir = pagePathDir
if isUgly {
pagePath = addSuffix(pagePath, fullSuffix)
} else {
pagePath = pjoin(pagePath, d.Type.BaseName+fullSuffix)
}
if !isHtmlIndex(pagePath) {
link = pagePath
}
if d.PrefixFilePath != "" {
pagePath = pjoin(d.PrefixFilePath, pagePath)
pagePathDir = pjoin(d.PrefixFilePath, pagePathDir)
}
if d.PrefixLink != "" {
link = pjoin(d.PrefixLink, link)
linkDir = pjoin(d.PrefixLink, linkDir)
}
} else {
if d.Addends != "" {
pagePath = pjoin(pagePath, d.Addends)
}
needsBase = needsBase && d.Addends == ""
// No permalink expansion etc. for node type pages (for now)
base := ""
if needsBase || !isUgly {
base = d.Type.BaseName
}
pagePathDir = pagePath
link = pagePath
linkDir = pagePathDir
if base != "" {
pagePath = path.Join(pagePath, addSuffix(base, fullSuffix))
} else {
pagePath = addSuffix(pagePath, fullSuffix)
}
if !isHtmlIndex(pagePath) {
link = pagePath
} else {
link += slash
}
if d.PrefixFilePath != "" {
pagePath = pjoin(d.PrefixFilePath, pagePath)
pagePathDir = pjoin(d.PrefixFilePath, pagePathDir)
}
if d.PrefixLink != "" {
link = pjoin(d.PrefixLink, link)
linkDir = pjoin(d.PrefixLink, linkDir)
}
}
pagePath = pjoin(slash, pagePath)
pagePathDir = strings.TrimSuffix(path.Join(slash, pagePathDir), slash)
hadSlash := strings.HasSuffix(link, slash)
link = strings.Trim(link, slash)
if hadSlash {
link += slash
}
if !strings.HasPrefix(link, slash) {
link = slash + link
}
linkDir = strings.TrimSuffix(path.Join(slash, linkDir), slash)
// if page URL is explicitly set in frontmatter,
// preserve its value without sanitization
if d.Kind != KindPage || d.URL == "" {
// Note: MakePathSanitized will lower case the path if
// disablePathToLower isn't set.
pagePath = d.PathSpec.MakePathSanitized(pagePath)
pagePathDir = d.PathSpec.MakePathSanitized(pagePathDir)
link = d.PathSpec.MakePathSanitized(link)
linkDir = d.PathSpec.MakePathSanitized(linkDir)
}
tp.TargetFilename = filepath.FromSlash(pagePath)
tp.SubResourceBaseTarget = filepath.FromSlash(pagePathDir)
tp.SubResourceBaseLink = linkDir
tp.Link = d.PathSpec.URLizeFilename(link)
if tp.Link == "" {
tp.Link = slash
}
return
}
func addSuffix(s, suffix string) string {
return strings.Trim(s, slash) + suffix
}
// Like path.Join, but preserves one trailing slash if present.
func pjoin(elem ...string) string {
hadSlash := strings.HasSuffix(elem[len(elem)-1], slash)
joined := path.Join(elem...)
if hadSlash && !strings.HasSuffix(joined, slash) {
return joined + slash
}
return joined
}