hugo/resources/page/page_paths.go
Bjørn Erik Pedersen 597e418cb0
Make Page an interface
The main motivation of this commit is to add a `page.Page` interface to replace the very file-oriented `hugolib.Page` struct.
This is all a preparation step for issue  #5074, "pages from other data sources".

But this also fixes a set of annoying limitations, especially related to custom output formats, and shortcodes.

Most notable changes:

* The inner content of shortcodes using the `{{%` as the outer-most delimiter will now be sent to the content renderer, e.g. Blackfriday.
  This means that any markdown will partake in the global ToC and footnote context etc.
* The Custom Output formats are now "fully virtualized". This removes many of the current limitations.
* The taxonomy list type now has a reference to the `Page` object.
  This improves the taxonomy template `.Title` situation and make common template constructs much simpler.

See #5074
Fixes #5763
Fixes #5758
Fixes #5090
Fixes #5204
Fixes #4695
Fixes #5607
Fixes #5707
Fixes #5719
Fixes #3113
Fixes #5706
Fixes #5767
Fixes #5723
Fixes #5769
Fixes #5770
Fixes #5771
Fixes #5759
Fixes #5776
Fixes #5777
Fixes #5778
2019-03-23 18:51:22 +01:00

335 lines
7.9 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/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 string
var err error
if f.Protocol != "" {
baseURL, err = s.BaseURL.WithProtocol(f.Protocol)
if err != nil {
return ""
}
} else {
baseURL = s.BaseURL.String()
}
return s.PermalinkForBaseURL(p.Link, baseURL)
}
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)
}
pagePath := slash
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+d.Type.MediaType.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
if baseNameSameAsType {
link = strings.TrimSuffix(link, d.BaseName)
}
pagePathDir = link
link = link + slash
linkDir = pagePathDir
if isUgly {
pagePath = addSuffix(pagePath, d.Type.MediaType.FullSuffix())
} else {
pagePath = pjoin(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix())
}
if isUgly && !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, d.Type.MediaType.FullSuffix()))
} else {
pagePath = addSuffix(pagePath, d.Type.MediaType.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)
// 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
}