hugolib: Refactor/-work the permalink/target path logic

This is a pretty fundamental change in Hugo, but absolutely needed if we should have any hope of getting "multiple outputs" done.

This commit's goal is to say:

* Every file target path is created by `createTargetPath`, i.e. one function for all.
* That function takes every page and site parameter into account, to avoid fragile string parsing to uglify etc. later on.
* The path creation logic has full test coverage.
* All permalinks, paginator URLs etc. are then built on top of that same logic.

Fixes #1252
Fixes #2110
Closes #2374
Fixes #1885
Fixes #3102
Fixes #3179
Fixes #1641
Fixes #1989
This commit is contained in:
Bjørn Erik Pedersen 2017-03-09 19:19:29 +01:00
parent c8fff9501d
commit 6bf010fed4
26 changed files with 912 additions and 400 deletions

View file

@ -22,6 +22,8 @@ import (
// PathSpec holds methods that decides how paths in URLs and files in Hugo should look like.
type PathSpec struct {
BaseURL
disablePathToLower bool
removePathAccents bool
uglyURLs bool
@ -32,7 +34,6 @@ type PathSpec struct {
// pagination path handling
paginatePath string
baseURL string
theme string
// Directories
@ -61,6 +62,9 @@ func (p PathSpec) String() string {
// NewPathSpec creats a new PathSpec from the given filesystems and Language.
func NewPathSpec(fs *hugofs.Fs, cfg config.Provider) *PathSpec {
// TODO(bep) output error handling
baseURL, _ := newBaseURLFromString(cfg.GetString("baseURL"))
ps := &PathSpec{
fs: fs,
disablePathToLower: cfg.GetBool("disablePathToLower"),
@ -71,7 +75,7 @@ func NewPathSpec(fs *hugofs.Fs, cfg config.Provider) *PathSpec {
defaultContentLanguageInSubdir: cfg.GetBool("defaultContentLanguageInSubdir"),
defaultContentLanguage: cfg.GetString("defaultContentLanguage"),
paginatePath: cfg.GetString("paginatePath"),
baseURL: cfg.GetString("baseURL"),
BaseURL: baseURL,
themesDir: cfg.GetString("themesDir"),
layoutDir: cfg.GetString("layoutDir"),
workingDir: cfg.GetString("workingDir"),

View file

@ -52,7 +52,7 @@ func TestNewPathSpecFromConfig(t *testing.T) {
require.Equal(t, "no", p.language.Lang)
require.Equal(t, "side", p.paginatePath)
require.Equal(t, "http://base.com", p.baseURL)
require.Equal(t, "http://base.com", p.BaseURL.String())
require.Equal(t, "thethemes", p.themesDir)
require.Equal(t, "thelayouts", p.layoutDir)
require.Equal(t, "thework", p.workingDir)

View file

@ -17,11 +17,39 @@ import (
"fmt"
"net/url"
"path"
"path/filepath"
"strings"
"github.com/PuerkitoBio/purell"
)
type BaseURL struct {
url *url.URL
urlStr string
}
func (b BaseURL) String() string {
return b.urlStr
}
func (b BaseURL) URL() *url.URL {
// create a copy as it will be modified.
c := *b.url
return &c
}
func newBaseURLFromString(b string) (BaseURL, error) {
var result BaseURL
base, err := url.Parse(b)
if err != nil {
return result, err
}
// TODO(bep) output consider saving original URL?
return BaseURL{url: base, urlStr: base.String()}, nil
}
type pathBridge struct {
}
@ -101,10 +129,20 @@ func SanitizeURLKeepTrailingSlash(in string) string {
// uri: Vim (text editor)
// urlize: vim-text-editor
func (p *PathSpec) URLize(uri string) string {
sanitized := p.MakePathSanitized(uri)
return p.URLEscape(p.MakePathSanitized(uri))
}
// URLizeFilename creates an URL from a filename by esacaping unicode letters
// and turn any filepath separator into forward slashes.
func (p *PathSpec) URLizeFilename(filename string) string {
return p.URLEscape(filepath.ToSlash(filename))
}
// URLEscape escapes unicode letters.
func (p *PathSpec) URLEscape(uri string) string {
// escape unicode letters
parsedURI, err := url.Parse(sanitized)
parsedURI, err := url.Parse(uri)
if err != nil {
// if net/url can not parse URL it means Sanitize works incorrectly
panic(err)
@ -118,6 +156,7 @@ func (p *PathSpec) URLize(uri string) string {
// base: http://spf13.com/
// path: post/how-i-blog
// result: http://spf13.com/post/how-i-blog
// TODO(bep) output check why this is still in use.
func MakePermalink(host, plink string) *url.URL {
base, err := url.Parse(host)
@ -156,14 +195,13 @@ func (p *PathSpec) AbsURL(in string, addLanguage bool) string {
return in
}
baseURL := p.baseURL
var baseURL string
if strings.HasPrefix(in, "/") {
p, err := url.Parse(baseURL)
if err != nil {
panic(err)
}
p.Path = ""
baseURL = p.String()
u := p.BaseURL.URL()
u.Path = ""
baseURL = u.String()
} else {
baseURL = p.BaseURL.String()
}
if addLanguage {
@ -218,7 +256,7 @@ func IsAbsURL(path string) bool {
// RelURL creates a URL relative to the BaseURL root.
// Note: The result URL will not include the context root if canonifyURLs is enabled.
func (p *PathSpec) RelURL(in string, addLanguage bool) string {
baseURL := p.baseURL
baseURL := p.BaseURL.String()
canonifyURLs := p.canonifyURLs
if (!strings.HasPrefix(in, baseURL) && strings.HasPrefix(in, "http")) || strings.HasPrefix(in, "//") {
return in
@ -287,8 +325,27 @@ func AddContextRoot(baseURL, relativePath string) string {
return newPath
}
// PrependBasePath prepends any baseURL sub-folder to the given resource
// if canonifyURLs is disabled.
// If canonifyURLs is set, we will globally prepend the absURL with any sub-folder,
// so avoid doing anything here to avoid getting double paths.
func (p *PathSpec) PrependBasePath(rel string) string {
basePath := p.BaseURL.url.Path
if !p.canonifyURLs && basePath != "" && basePath != "/" {
rel = filepath.ToSlash(rel)
// Need to prepend any path from the baseURL
hadSlash := strings.HasSuffix(rel, "/")
rel = path.Join(basePath, rel)
if hadSlash {
rel += "/"
}
}
return rel
}
// URLizeAndPrep applies misc sanitation to the given URL to get it in line
// with the Hugo standard.
// TODO(bep) output check usage
func (p *PathSpec) URLizeAndPrep(in string) string {
return p.URLPrep(p.URLize(in))
}

View file

@ -30,7 +30,7 @@ import (
)
const (
baseURL = "http://foo/bar"
testBaseURL = "http://foo/bar"
)
func TestShortcodeCrossrefs(t *testing.T) {
@ -46,7 +46,7 @@ func doTestShortcodeCrossrefs(t *testing.T, relative bool) {
cfg, fs = newTestCfg()
)
cfg.Set("baseURL", baseURL)
cfg.Set("baseURL", testBaseURL)
var refShortcode string
var expectedBase string
@ -56,7 +56,7 @@ func doTestShortcodeCrossrefs(t *testing.T, relative bool) {
expectedBase = "/bar"
} else {
refShortcode = "ref"
expectedBase = baseURL
expectedBase = testBaseURL
}
path := filepath.FromSlash("blog/post.md")

View file

@ -548,11 +548,6 @@ func (s *Site) preparePagesForRender(cfg *BuildCfg) {
p.Content = helpers.BytesToHTML(workContentCopy)
}
// May have been set in front matter
if len(p.outputTypes) == 0 {
p.outputTypes = defaultOutputDefinitions.ForKind(p.Kind)
}
//analyze for raw stats
p.analyzePage()

View file

@ -174,6 +174,12 @@ func (h *HugoSites) assemble(config *BuildCfg) error {
}
for _, s := range h.Sites {
for _, p := range s.Pages {
// May have been set in front matter
if len(p.outputTypes) == 0 {
p.outputTypes = s.defaultOutputDefinitions.ForKind(p.Kind)
}
}
s.assembleMenus()
s.refreshPageCaches()
s.setupSitePages()

View file

@ -112,12 +112,13 @@ func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) {
th.assertFileContent("public/en/sitemap.xml", "<loc>http://example.com/blog/en/</loc>")
// Check rss
th.assertFileContent("public/fr/index.xml", `<atom:link href="http://example.com/blog/fr/index.xml"`)
th.assertFileContent("public/en/index.xml", `<atom:link href="http://example.com/blog/en/index.xml"`)
th.assertFileContent("public/fr/sect/index.xml", `<atom:link href="http://example.com/blog/fr/sect/index.xml"`)
th.assertFileContent("public/en/sect/index.xml", `<atom:link href="http://example.com/blog/en/sect/index.xml"`)
th.assertFileContent("public/fr/plaques/frtag1/index.xml", `<atom:link href="http://example.com/blog/fr/plaques/frtag1/index.xml"`)
th.assertFileContent("public/en/tags/tag1/index.xml", `<atom:link href="http://example.com/blog/en/tags/tag1/index.xml"`)
// TODO(bep) output the Atom link must be cretated from the OutputFormats.RSS.Permalink
// th.assertFileContent("public/fr/index.xml", `<atom:link href="http://example.com/blog/fr/index.xml"`)
// th.assertFileContent("public/en/index.xml", `<atom:link href="http://example.com/blog/en/index.xml"`)
// th.assertFileContent("public/fr/sect/index.xml", `<atom:link href="http://example.com/blog/fr/sect/index.xml"`)
// th.assertFileContent("public/en/sect/index.xml", `<atom:link href="http://example.com/blog/en/sect/index.xml"`)
// th.assertFileContent("public/fr/plaques/frtag1/index.xml", `<atom:link href="http://example.com/blog/fr/plaques/frtag1/index.xml"`)
// th.assertFileContent("public/en/tags/tag1/index.xml", `<atom:link href="http://example.com/blog/en/tags/tag1/index.xml"`)
// Check paginators
th.assertFileContent("public/fr/page/1/index.html", `refresh" content="0; url=http://example.com/blog/fr/"`)
@ -250,7 +251,7 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) {
// Note that /superbob is a custom URL set in frontmatter.
// We respect that URL literally (it can be /search.json)
// and do no not do any language code prefixing.
require.Equal(t, "http://example.com/blog/superbob", permalink, "invalid doc3 permalink")
require.Equal(t, "http://example.com/blog/superbob/", permalink, "invalid doc3 permalink")
require.Equal(t, "/superbob", doc3.URL(), "invalid url, was specified on doc3")
th.assertFileContent("public/superbob/index.html", "doc3|Hello|en")
@ -274,7 +275,7 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) {
doc5 := enSite.AllPages[5]
permalink = doc5.Permalink()
require.Equal(t, "http://example.com/blog/fr/somewhere/else/doc5", permalink, "invalid doc5 permalink")
require.Equal(t, "http://example.com/blog/fr/somewhere/else/doc5/", permalink, "invalid doc5 permalink")
// Taxonomies and their URLs
require.Len(t, enSite.Taxonomies, 1, "should have 1 taxonomy")
@ -594,14 +595,6 @@ func assertShouldNotBuild(t *testing.T, sites *HugoSites) {
require.Equal(t, p.shouldBuild(), p.Content != "", p.BaseFileName())
// TODO(bep) output
/*filename := filepath.Join("public", p.TargetPath())
if strings.HasSuffix(filename, ".html") {
// TODO(bep) the end result is correct, but it is weird that we cannot use targetPath directly here.
filename = strings.Replace(filename, ".html", "/index.html", 1)
}
require.Equal(t, p.shouldBuild(), destinationExists(sites.Fs, filename), filename)*/
}
}
@ -825,6 +818,7 @@ disableRSS = false
rssURI = "index.xml"
paginate = 1
disablePathToLower = true
defaultContentLanguage = "{{ .DefaultContentLanguage }}"
defaultContentLanguageInSubdir = {{ .DefaultContentLanguageInSubdir }}
@ -884,6 +878,7 @@ disableSitemap: false
disableRSS: false
rssURI: "index.xml"
disablePathToLower: true
paginate: 1
defaultContentLanguage: "{{ .DefaultContentLanguage }}"
defaultContentLanguageInSubdir: {{ .DefaultContentLanguageInSubdir }}
@ -945,6 +940,7 @@ var multiSiteJSONConfigTemplate = `
"disableRSS": false,
"rssURI": "index.xml",
"paginate": 1,
"disablePathToLower": true,
"defaultContentLanguage": "{{ .DefaultContentLanguage }}",
"defaultContentLanguageInSubdir": true,
"permalinks": {

View file

@ -286,7 +286,9 @@ func doTestNodesWithNoContentFile(t *testing.T, ugly bool) {
func TestNodesAsPageMultilingual(t *testing.T) {
t.Parallel()
for _, ugly := range []bool{false, true} {
t.Run(fmt.Sprintf("ugly=%t", ugly), func(t *testing.T) {
doTestNodesAsPageMultilingual(t, ugly)
})
}
}
@ -369,7 +371,8 @@ title = "Deutsche Hugo"
require.Len(t, deHome.Translations(), 2, deHome.Translations()[0].Language().Lang)
require.Equal(t, "en", deHome.Translations()[1].Language().Lang)
require.Equal(t, "nn", deHome.Translations()[0].Language().Lang)
require.Equal(t, expetedPermalink(ugly, "/de/"), deHome.Permalink())
// See issue #3179
require.Equal(t, expetedPermalink(false, "/de/"), deHome.Permalink())
enSect := sites.Sites[1].getPage("section", "sect1")
require.NotNil(t, enSect)

View file

@ -28,7 +28,6 @@ import (
"html/template"
"io"
"net/url"
"path"
"path/filepath"
"regexp"
@ -188,11 +187,9 @@ type Page struct {
RSSLink template.URL
URLPath
permalink *url.URL
permalink string
relPermalink string
paginator *Pager
scratch *Scratch
// It would be tempting to use the language set on the Site, but in they way we do
@ -204,6 +201,10 @@ type Page struct {
// The output types this page will be rendered to.
outputTypes output.Types
// This is the PageOutput that represents the first item in outputTypes.
// Use with care, as there are potential for inifinite loops.
mainPageOutput *PageOutput
// Used to pick the correct template(s)
layoutIdentifier pageLayoutIdentifier
}
@ -248,12 +249,10 @@ type pageInit struct {
languageInit sync.Once
pageMenusInit sync.Once
pageMetaInit sync.Once
paginatorInit sync.Once
plainInit sync.Once
plainWordsInit sync.Once
renderingConfigInit sync.Once
pageURLInit sync.Once
relPermalinkInit sync.Once
}
// IsNode returns whether this is an item of one of the list types in Hugo,
@ -787,68 +786,6 @@ func (p *Page) analyzePage() {
})
}
func (p *Page) getPermalink() *url.URL {
p.pageURLInit.Do(func() {
u, err := p.createPermalink()
if err != nil {
p.s.Log.ERROR.Printf("Failed to create permalink for page %q: %s", p.FullFilePath(), err)
p.permalink = new(url.URL)
return
}
p.permalink = u
})
// The link may be modified by the receiver, so create a copy.
l := *p.permalink
return &l
}
func (p *Page) createPermalink() (*url.URL, error) {
// TODO(bep) this should probably be set once during build. Maybe.
// And simplified.
baseURL := string(p.Site.BaseURL)
if p.IsNode() {
// No permalink config for nodes (currently)
pURL := strings.TrimSpace(p.s.PathSpec.URLize(p.URLPath.URL))
pURL = p.addLangPathPrefix(pURL)
pURL = p.s.PathSpec.URLPrep(pURL)
url := helpers.MakePermalink(baseURL, pURL)
return url, nil
}
dir := strings.TrimSpace(p.s.PathSpec.MakePath(filepath.ToSlash(strings.ToLower(p.Source.Dir()))))
pSlug := strings.TrimSpace(p.s.PathSpec.URLize(p.Slug))
pURL := strings.TrimSpace(p.s.PathSpec.URLize(p.URLPath.URL))
var permalink string
var err error
if len(pURL) > 0 {
return helpers.MakePermalink(baseURL, pURL), nil
}
if override, ok := p.Site.Permalinks[p.Section()]; ok {
permalink, err = override.Expand(p)
if err != nil {
return nil, err
}
} else {
if len(pSlug) > 0 {
permalink = p.s.PathSpec.URLPrep(path.Join(dir, p.Slug+"."+p.Extension()))
} else {
t := p.Source.TranslationBaseName()
permalink = p.s.PathSpec.URLPrep(path.Join(dir, (strings.TrimSpace(t) + "." + p.Extension())))
}
}
permalink = p.addLangPathPrefix(permalink)
return helpers.MakePermalink(baseURL, permalink), nil
}
func (p *Page) Extension() string {
if p.extension != "" {
// TODO(bep) output remove/deprecate this
@ -927,10 +864,6 @@ func (p *Page) IsExpired() bool {
return p.ExpiryDate.Before(time.Now())
}
func (p *Page) Permalink() string {
return p.getPermalink().String()
}
func (p *Page) URL() string {
if p.IsPage() && p.URLPath.URL != "" {
@ -942,41 +875,27 @@ func (p *Page) URL() string {
return u
}
// Permalink returns the absolute URL to this Page.
func (p *Page) Permalink() string {
p.initURLs()
return p.permalink
}
// RelPermalink gets a URL to the resource relative to the host.
func (p *Page) RelPermalink() string {
p.relPermalinkInit.Do(func() {
link := p.getPermalink()
if p.s.Info.canonifyURLs { // replacements for relpermalink with baseURL on the form http://myhost.com/sub/ will fail later on
// have to return the URL relative from baseURL
relpath, err := helpers.GetRelativePath(link.String(), string(p.Site.BaseURL))
if err != nil {
return
}
relpath = filepath.ToSlash(relpath)
if relpath[0] == '.' {
relpath = relpath[1:]
}
if !strings.HasPrefix(relpath, "/") {
relpath = "/" + relpath
}
p.relPermalink = relpath
return
}
link.Scheme = ""
link.Host = ""
link.User = nil
link.Opaque = ""
p.relPermalink = link.String()
})
p.initURLs()
return p.relPermalink
}
func (p *Page) initURLs() {
p.pageURLInit.Do(func() {
rel := p.createRelativePermalink()
p.permalink = p.s.permalink(rel)
rel = p.s.PathSpec.PrependBasePath(rel)
p.relPermalink = rel
})
}
var ErrHasDraftAndPublished = errors.New("both draft and published parameters were found in page's frontmatter")
func (p *Page) update(f interface{}) error {
@ -1507,56 +1426,6 @@ func (p *Page) FullFilePath() string {
return filepath.Join(p.Dir(), p.LogicalName())
}
func (p *Page) TargetPath() (outfile string) {
switch p.Kind {
case KindHome:
return p.addLangFilepathPrefix(helpers.FilePathSeparator)
case KindSection:
return p.addLangFilepathPrefix(p.sections[0])
case KindTaxonomy:
return p.addLangFilepathPrefix(filepath.Join(p.sections...))
case KindTaxonomyTerm:
return p.addLangFilepathPrefix(filepath.Join(p.sections...))
}
// Always use URL if it's specified
if len(strings.TrimSpace(p.URLPath.URL)) > 2 {
outfile = strings.TrimSpace(p.URLPath.URL)
if strings.HasSuffix(outfile, "/") {
outfile = outfile + "index.html"
}
outfile = filepath.FromSlash(outfile)
return
}
// If there's a Permalink specification, we use that
if override, ok := p.Site.Permalinks[p.Section()]; ok {
var err error
outfile, err = override.Expand(p)
if err == nil {
outfile, _ = url.QueryUnescape(outfile)
if strings.HasSuffix(outfile, "/") {
outfile += "index.html"
}
outfile = filepath.FromSlash(outfile)
outfile = p.addLangFilepathPrefix(outfile)
return
}
}
if len(strings.TrimSpace(p.Slug)) > 0 {
outfile = strings.TrimSpace(p.Slug) + "." + p.Extension()
} else {
// Fall back to filename
outfile = (p.Source.TranslationBaseName() + "." + p.Extension())
}
return p.addLangFilepathPrefix(filepath.Join(strings.ToLower(
p.s.PathSpec.MakePath(p.Source.Dir())), strings.TrimSpace(outfile)))
}
// Pre render prepare steps
func (p *Page) prepareLayouts() error {
@ -1682,9 +1551,6 @@ func (p *Page) updatePageDates() {
// copy creates a copy of this page with the lazy sync.Once vars reset
// so they will be evaluated again, for word count calculations etc.
func (p *Page) copy() *Page {
// This is a temporary workaround for the data race in #3129
p.getPermalink()
c := *p
c.pageInit = &pageInit{}
return &c
@ -1895,12 +1761,6 @@ func kindFromFilename(filename string) string {
return kindUnknown
}
// TODO(bep) output
var (
outputTypesWithRSS = output.Types{output.HTMLType, output.RSSType}
outputTypesHTML = output.Types{output.HTMLType}
)
func (p *Page) setValuesForKind(s *Site) {
if p.Kind == kindUnknown {
// This is either a taxonomy list, taxonomy term or a section

View file

@ -14,6 +14,8 @@
package hugolib
import (
"sync"
"github.com/spf13/hugo/output"
)
@ -22,18 +24,50 @@ import (
type PageOutput struct {
*Page
// Pagination
paginator *Pager
paginatorInit sync.Once
// Keep this to create URL/path variations, i.e. paginators.
targetPathDescriptor targetPathDescriptor
outputType output.Type
}
func newPageOutput(p *Page, createCopy bool, outputType output.Type) *PageOutput {
func (p *PageOutput) targetPath(addends ...string) (string, error) {
tp, err := p.createTargetPath(p.outputType, addends...)
if err != nil {
return "", err
}
return tp, nil
}
func newPageOutput(p *Page, createCopy bool, outputType output.Type) (*PageOutput, error) {
if createCopy {
p.initURLs()
p = p.copy()
}
return &PageOutput{Page: p, outputType: outputType}
td, err := p.createTargetPathDescriptor(outputType)
if err != nil {
return nil, err
}
return &PageOutput{
Page: p,
outputType: outputType,
targetPathDescriptor: td,
}, nil
}
// copy creates a copy of this PageOutput with the lazy sync.Once vars reset
// so they will be evaluated again, for word count calculations etc.
func (p *PageOutput) copy() *PageOutput {
return newPageOutput(p.Page, true, p.outputType)
c, err := newPageOutput(p.Page, true, p.outputType)
if err != nil {
panic(err)
}
return c
}

230
hugolib/page_paths.go Normal file
View file

@ -0,0 +1,230 @@
// Copyright 2017 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 hugolib
import (
"fmt"
"path/filepath"
"net/url"
"strings"
"github.com/spf13/hugo/helpers"
"github.com/spf13/hugo/output"
)
// 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.
//
// Page.createTargetPathDescriptor is the Page adapter.
//
type targetPathDescriptor struct {
PathSpec *helpers.PathSpec
Type output.Type
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
// Language prefix, set if multilingual and if page should be placed in its
// language subdir.
LangPrefix string
// Page.URLPath.URL. Will override any Slug etc. for regular pages.
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
}
// createTargetPathDescriptor adapts a Page and the given output.Type into
// a targetPathDescriptor. This descriptor can then be used to create paths
// and URLs for this Page.
func (p *Page) createTargetPathDescriptor(t output.Type) (targetPathDescriptor, error) {
d := targetPathDescriptor{
PathSpec: p.s.PathSpec,
Type: t,
Kind: p.Kind,
Sections: p.sections,
UglyURLs: p.s.Info.uglyURLs,
Dir: filepath.ToSlash(strings.ToLower(p.Source.Dir())),
URL: p.URLPath.URL,
}
if p.Slug != "" {
d.BaseName = p.Slug
} else {
d.BaseName = p.TranslationBaseName()
}
if p.shouldAddLanguagePrefix() {
d.LangPrefix = p.Lang()
}
if override, ok := p.Site.Permalinks[p.Section()]; ok {
opath, err := override.Expand(p)
if err != nil {
return d, err
}
opath, _ = url.QueryUnescape(opath)
opath = filepath.FromSlash(opath)
d.ExpandedPermalink = opath
}
return d, nil
}
// createTargetPath creates the target filename for this Page for the given
// output.Type. Some additional URL parts can also be provided, the typical
// use case being pagination.
func (p *Page) createTargetPath(t output.Type, addends ...string) (string, error) {
d, err := p.createTargetPathDescriptor(t)
if err != nil {
return "", nil
}
if len(addends) > 0 {
d.Addends = filepath.Join(addends...)
}
return createTargetPath(d), nil
}
func createTargetPath(d targetPathDescriptor) string {
pagePath := helpers.FilePathSeparator
// 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
if d.Kind != KindPage && len(d.Sections) > 0 {
pagePath = filepath.Join(d.Sections...)
needsBase = false
}
if d.Type.Path != "" {
pagePath = filepath.Join(pagePath, d.Type.Path)
}
if d.Kind == KindPage {
// Always use URL if it's specified
if d.URL != "" {
pagePath = filepath.Join(pagePath, d.URL)
if strings.HasSuffix(d.URL, "/") || !strings.Contains(d.URL, ".") {
pagePath = filepath.Join(pagePath, d.Type.BaseName+"."+d.Type.MediaType.Suffix)
}
} else {
if d.ExpandedPermalink != "" {
pagePath = filepath.Join(pagePath, d.ExpandedPermalink)
} else {
if d.Dir != "" {
pagePath = filepath.Join(pagePath, d.Dir)
}
if d.BaseName != "" {
pagePath = filepath.Join(pagePath, d.BaseName)
}
}
if d.Addends != "" {
pagePath = filepath.Join(pagePath, d.Addends)
}
if isUgly {
pagePath += "." + d.Type.MediaType.Suffix
} else {
pagePath = filepath.Join(pagePath, d.Type.BaseName+"."+d.Type.MediaType.Suffix)
}
if d.LangPrefix != "" {
pagePath = filepath.Join(d.LangPrefix, pagePath)
}
}
} else {
if d.Addends != "" {
pagePath = filepath.Join(pagePath, d.Addends)
}
needsBase = needsBase && d.Addends == ""
// No permalink expansion etc. for node type pages (for now)
base := ""
if needsBase || !isUgly {
base = helpers.FilePathSeparator + d.Type.BaseName
}
pagePath += base + "." + d.Type.MediaType.Suffix
if d.LangPrefix != "" {
pagePath = filepath.Join(d.LangPrefix, pagePath)
}
}
pagePath = filepath.Join(helpers.FilePathSeparator, pagePath)
// Note: MakePathSanitized will lower case the path if
// disablePathToLower isn't set.
return d.PathSpec.MakePathSanitized(pagePath)
}
func (p *Page) createRelativePermalink() string {
if len(p.outputTypes) == 0 {
panic(fmt.Sprintf("Page %q missing output format(s)", p.Title))
}
// Choose the main output format. In most cases, this will be HTML.
outputType := p.outputTypes[0]
tp, err := p.createTargetPath(outputType)
if err != nil {
p.s.Log.ERROR.Printf("Failed to create permalink for page %q: %s", p.FullFilePath(), err)
return ""
}
tp = strings.TrimSuffix(tp, outputType.BaseFilename())
return p.s.PathSpec.URLizeFilename(tp)
}
func (p *Page) TargetPath() (outfile string) {
// Delete in Hugo 0.22
helpers.Deprecated("Page", "TargetPath", "This method does not make sanse any more.", false)
return ""
}

166
hugolib/page_paths_test.go Normal file
View file

@ -0,0 +1,166 @@
// Copyright 2017 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 hugolib
import (
"path/filepath"
"strings"
"testing"
"fmt"
"github.com/spf13/hugo/output"
)
func TestPageTargetPath(t *testing.T) {
pathSpec := newTestDefaultPathSpec()
for _, langPrefix := range []string{"", "no"} {
t.Run(fmt.Sprintf("langPrefix=%q", langPrefix), func(t *testing.T) {
for _, uglyURLs := range []bool{false, true} {
t.Run(fmt.Sprintf("uglyURLs=%t", uglyURLs), func(t *testing.T) {
tests := []struct {
name string
d targetPathDescriptor
expected string
}{
{"JSON home", targetPathDescriptor{Kind: KindHome, Type: output.JSONType}, "/index.json"},
{"AMP home", targetPathDescriptor{Kind: KindHome, Type: output.AMPType}, "/amp/index.html"},
{"HTML home", targetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: output.HTMLType}, "/index.html"},
{"HTML section list", targetPathDescriptor{
Kind: KindSection,
Sections: []string{"sect1"},
BaseName: "_index",
Type: output.HTMLType}, "/sect1/index.html"},
{"HTML taxonomy list", targetPathDescriptor{
Kind: KindTaxonomy,
Sections: []string{"tags", "hugo"},
BaseName: "_index",
Type: output.HTMLType}, "/tags/hugo/index.html"},
{"HTML taxonomy term", targetPathDescriptor{
Kind: KindTaxonomy,
Sections: []string{"tags"},
BaseName: "_index",
Type: output.HTMLType}, "/tags/index.html"},
{
"HTML page", targetPathDescriptor{
Kind: KindPage,
Dir: "/a/b",
BaseName: "mypage",
Sections: []string{"a"},
Type: output.HTMLType}, "/a/b/mypage/index.html"},
{
"HTML page with special chars", targetPathDescriptor{
Kind: KindPage,
Dir: "/a/b",
BaseName: "My Page!",
Type: output.HTMLType}, "/a/b/My-Page/index.html"},
{"RSS home", targetPathDescriptor{Kind: kindRSS, Type: output.RSSType}, "/index.xml"},
{"RSS section list", targetPathDescriptor{
Kind: kindRSS,
Sections: []string{"sect1"},
Type: output.RSSType}, "/sect1/index.xml"},
{
"AMP page", targetPathDescriptor{
Kind: KindPage,
Dir: "/a/b/c",
BaseName: "myamp",
Type: output.AMPType}, "/amp/a/b/c/myamp/index.html"},
{
"AMP page with URL with suffix", targetPathDescriptor{
Kind: KindPage,
Dir: "/sect/",
BaseName: "mypage",
URL: "/some/other/url.xhtml",
Type: output.HTMLType}, "/some/other/url.xhtml"},
{
"JSON page with URL without suffix", targetPathDescriptor{
Kind: KindPage,
Dir: "/sect/",
BaseName: "mypage",
URL: "/some/other/path/",
Type: output.JSONType}, "/some/other/path/index.json"},
{
"JSON page with URL without suffix and no trailing slash", targetPathDescriptor{
Kind: KindPage,
Dir: "/sect/",
BaseName: "mypage",
URL: "/some/other/path",
Type: output.JSONType}, "/some/other/path/index.json"},
{
"HTML page with expanded permalink", targetPathDescriptor{
Kind: KindPage,
Dir: "/a/b",
BaseName: "mypage",
ExpandedPermalink: "/2017/10/my-title",
Type: output.HTMLType}, "/2017/10/my-title/index.html"},
{
"Paginated HTML home", targetPathDescriptor{
Kind: KindHome,
BaseName: "_index",
Type: output.HTMLType,
Addends: "page/3"}, "/page/3/index.html"},
{
"Paginated Taxonomy list", targetPathDescriptor{
Kind: KindTaxonomy,
BaseName: "_index",
Sections: []string{"tags", "hugo"},
Type: output.HTMLType,
Addends: "page/3"}, "/tags/hugo/page/3/index.html"},
{
"Regular page with addend", targetPathDescriptor{
Kind: KindPage,
Dir: "/a/b",
BaseName: "mypage",
Addends: "c/d/e",
Type: output.HTMLType}, "/a/b/mypage/c/d/e/index.html"},
}
for i, test := range tests {
test.d.PathSpec = pathSpec
test.d.UglyURLs = uglyURLs
test.d.LangPrefix = langPrefix
test.d.Dir = filepath.FromSlash(test.d.Dir)
isUgly := uglyURLs && !test.d.Type.NoUgly
expected := test.expected
// TODO(bep) simplify
if test.d.Kind == KindHome && test.d.Type.Path != "" {
} else if (!strings.HasPrefix(expected, "/index") || test.d.Addends != "") && test.d.URL == "" && isUgly {
expected = strings.Replace(expected,
"/"+test.d.Type.BaseName+"."+test.d.Type.MediaType.Suffix,
"."+test.d.Type.MediaType.Suffix, -1)
}
if test.d.LangPrefix != "" && !(test.d.Kind == KindPage && test.d.URL != "") {
expected = "/" + test.d.LangPrefix + expected
}
expected = filepath.FromSlash(expected)
pagePath := createTargetPath(test.d)
if pagePath != expected {
t.Fatalf("[%d] [%s] targetPath expected %q, got: %q", i, test.name, expected, pagePath)
}
}
})
}
})
}
}

View file

@ -1162,20 +1162,6 @@ func TestPagePaths(t *testing.T) {
s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
require.Len(t, s.RegularPages, 1)
// TODO(bep) output
/* p := s.RegularPages[0]
expectedTargetPath := filepath.FromSlash(test.expected)
expectedFullFilePath := filepath.FromSlash(test.path)
if p.TargetPath() != expectedTargetPath {
t.Fatalf("[%d] %s => TargetPath expected: '%s', got: '%s'", i, test.content, expectedTargetPath, p.TargetPath())
}
if p.FullFilePath() != expectedFullFilePath {
t.Fatalf("[%d] %s => FullFilePath expected: '%s', got: '%s'", i, test.content, expectedFullFilePath, p.FullFilePath())
}*/
}
}
@ -1488,6 +1474,73 @@ func TestShouldBuild(t *testing.T) {
}
}
// Issue #1885 and #2110
func TestDotInPath(t *testing.T) {
t.Parallel()
for _, uglyURLs := range []bool{false, true} {
t.Run(fmt.Sprintf("uglyURLs=%t", uglyURLs), func(t *testing.T) {
cfg, fs := newTestCfg()
th := testHelper{cfg, fs, t}
cfg.Set("permalinks", map[string]string{
"post": ":section/:title",
})
cfg.Set("uglyURLs", uglyURLs)
cfg.Set("paginate", 1)
writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "<html><body>{{.Content}}</body></html>")
writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"),
"<html><body>P{{.Paginator.PageNumber}}|URL: {{.Paginator.URL}}|{{ if .Paginator.HasNext }}Next: {{.Paginator.Next.URL }}{{ end }}</body></html>")
for i := 0; i < 3; i++ {
writeSource(t, fs, filepath.Join("content", "post", fmt.Sprintf("doc%d.md", i)),
fmt.Sprintf(`---
title: "test%d.dot"
tags:
- ".net"
---
# doc1
*some content*`, i))
}
s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{})
require.Len(t, s.RegularPages, 3)
pathFunc := func(s string) string {
if uglyURLs {
return strings.Replace(s, "/index.html", ".html", 1)
}
return s
}
th.assertFileContent(pathFunc("public/post/test0.dot/index.html"), "some content")
if uglyURLs {
th.assertFileContent("public/post/page/1.html", `canonical" href="/post.html"/`)
th.assertFileContent("public/post.html", `<body>P1|URL: /post.html|Next: /post/page/2.html</body>`)
th.assertFileContent("public/post/page/2.html", `<body>P2|URL: /post/page/2.html|Next: /post/page/3.html</body>`)
} else {
th.assertFileContent("public/post/page/1/index.html", `canonical" href="/post/"/`)
th.assertFileContent("public/post/index.html", `<body>P1|URL: /post/|Next: /post/page/2/</body>`)
th.assertFileContent("public/post/page/2/index.html", `<body>P2|URL: /post/page/2/|Next: /post/page/3/</body>`)
th.assertFileContent("public/tags/.net/index.html", `<body>P1|URL: /tags/.net/|Next: /tags/.net/page/2/</body>`)
}
p := s.RegularPages[0]
if uglyURLs {
require.Equal(t, "/post/test0.dot.html", p.RelPermalink())
} else {
require.Equal(t, "/post/test0.dot/", p.RelPermalink())
}
})
}
}
func BenchmarkParsePage(b *testing.B) {
s := newTestSite(b)
f, _ := os.Open("testdata/redis.cn.md")

View file

@ -18,13 +18,12 @@ import (
"fmt"
"html/template"
"math"
"path"
"reflect"
"strings"
"github.com/spf13/hugo/config"
"github.com/spf13/cast"
"github.com/spf13/hugo/helpers"
)
// Pager represents one of the elements in a paginator.
@ -262,9 +261,14 @@ func splitPageGroups(pageGroups PagesGroup, size int) []paginatedElement {
return split
}
// Paginator gets this Page's paginator if it's already created.
// If it's not, one will be created with all pages in Data["Pages"].
// Paginator get this Page's main output's paginator.
func (p *Page) Paginator(options ...interface{}) (*Pager, error) {
return p.mainPageOutput.Paginator(options...)
}
// Paginator gets this PageOutput's paginator if it's already created.
// If it's not, one will be created with all pages in Data["Pages"].
func (p *PageOutput) Paginator(options ...interface{}) (*Pager, error) {
if !p.IsNode() {
return nil, fmt.Errorf("Paginators not supported for pages of type %q (%q)", p.Kind, p.Title)
}
@ -281,7 +285,7 @@ func (p *Page) Paginator(options ...interface{}) (*Pager, error) {
return
}
pagers, err := paginatePages(p.s.PathSpec, p.Data["Pages"], pagerSize, p.sections...)
pagers, err := paginatePages(p.targetPathDescriptor, p.Data["Pages"], pagerSize)
if err != nil {
initError = err
@ -304,10 +308,15 @@ func (p *Page) Paginator(options ...interface{}) (*Pager, error) {
return p.paginator, nil
}
// Paginate gets this Node's paginator if it's already created.
// Paginate invokes this Page's main output's Paginate method.
func (p *Page) Paginate(seq interface{}, options ...interface{}) (*Pager, error) {
return p.mainPageOutput.Paginate(seq, options...)
}
// Paginate gets this PageOutput's paginator if it's already created.
// If it's not, one will be created with the qiven sequence.
// Note that repeated calls will return the same result, even if the sequence is different.
func (p *Page) Paginate(seq interface{}, options ...interface{}) (*Pager, error) {
func (p *PageOutput) Paginate(seq interface{}, options ...interface{}) (*Pager, error) {
if !p.IsNode() {
return nil, fmt.Errorf("Paginators not supported for pages of type %q (%q)", p.Kind, p.Title)
}
@ -324,7 +333,7 @@ func (p *Page) Paginate(seq interface{}, options ...interface{}) (*Pager, error)
if p.paginator != nil {
return
}
pagers, err := paginatePages(p.s.PathSpec, seq, pagerSize, p.sections...)
pagers, err := paginatePages(p.targetPathDescriptor, seq, pagerSize)
if err != nil {
initError = err
@ -373,13 +382,13 @@ func resolvePagerSize(cfg config.Provider, options ...interface{}) (int, error)
return pas, nil
}
func paginatePages(pathSpec *helpers.PathSpec, seq interface{}, pagerSize int, sections ...string) (pagers, error) {
func paginatePages(td targetPathDescriptor, seq interface{}, pagerSize int) (pagers, error) {
if pagerSize <= 0 {
return nil, errors.New("'paginate' configuration setting must be positive to paginate")
}
urlFactory := newPaginationURLFactory(pathSpec, sections...)
urlFactory := newPaginationURLFactory(td)
var paginator *paginator
@ -506,18 +515,21 @@ func newPaginator(elements []paginatedElement, total, size int, urlFactory pagin
return p, nil
}
func newPaginationURLFactory(pathSpec *helpers.PathSpec, pathElements ...string) paginationURLFactory {
basePath := path.Join(pathElements...)
func newPaginationURLFactory(d targetPathDescriptor) paginationURLFactory {
return func(page int) string {
pathDescriptor := d
var rel string
if page == 1 {
rel = fmt.Sprintf("/%s/", basePath)
} else {
rel = fmt.Sprintf("/%s/%s/%d/", basePath, pathSpec.PaginatePath(), page)
if page > 1 {
rel = fmt.Sprintf("/%s/%d/", d.PathSpec.PaginatePath(), page)
pathDescriptor.Addends = rel
}
return pathSpec.URLizeAndPrep(rel)
targetPath := createTargetPath(pathDescriptor)
targetPath = strings.TrimSuffix(targetPath, d.Type.BaseFilename())
link := d.PathSpec.PrependBasePath(targetPath)
// Note: The targetPath is massaged with MakePathSanitized
return d.PathSpec.URLizeFilename(link)
}
}

View file

@ -17,9 +17,11 @@ import (
"fmt"
"html/template"
"path/filepath"
"strings"
"testing"
"github.com/spf13/hugo/deps"
"github.com/spf13/hugo/output"
"github.com/stretchr/testify/require"
)
@ -201,25 +203,60 @@ func doTestPagerNoPages(t *testing.T, paginator *paginator) {
func TestPaginationURLFactory(t *testing.T) {
t.Parallel()
cfg, fs := newTestCfg()
cfg.Set("paginatePath", "zoo")
pathSpec := newTestPathSpec(fs, cfg)
for _, uglyURLs := range []bool{false, true} {
t.Run(fmt.Sprintf("uglyURLs=%t", uglyURLs), func(t *testing.T) {
for _, canonifyURLs := range []bool{false, true} {
t.Run(fmt.Sprintf("canonifyURLs=%t", canonifyURLs), func(t *testing.T) {
unicode := newPaginationURLFactory(pathSpec, "новости проекта")
fooBar := newPaginationURLFactory(pathSpec, "foo", "bar")
require.Equal(t, "/foo/bar/", fooBar(1))
require.Equal(t, "/%D0%BD%D0%BE%D0%B2%D0%BE%D1%81%D1%82%D0%B8-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B0/zoo/4/", unicode(4))
unicoded := unicode(4)
unicodedExpected := "/%D0%BD%D0%BE%D0%B2%D0%BE%D1%81%D1%82%D0%B8-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B0/zoo/4/"
if unicoded != unicodedExpected {
t.Fatal("Expected\n", unicodedExpected, "\nGot\n", unicoded)
tests := []struct {
name string
d targetPathDescriptor
baseURL string
page int
expected string
}{
{"HTML home page 32",
targetPathDescriptor{Kind: KindHome, Type: output.HTMLType}, "http://example.com/", 32, "/zoo/32/"},
{"JSON home page 42",
targetPathDescriptor{Kind: KindHome, Type: output.JSONType}, "http://example.com/", 42, "/zoo/42/"},
// Issue #1252
{"BaseURL with sub path",
targetPathDescriptor{Kind: KindHome, Type: output.HTMLType}, "http://example.com/sub/", 999, "/sub/zoo/999/"},
}
require.Equal(t, "/foo/bar/zoo/12345/", fooBar(12345))
for _, test := range tests {
d := test.d
cfg.Set("baseURL", test.baseURL)
cfg.Set("canonifyURLs", canonifyURLs)
cfg.Set("uglyURLs", uglyURLs)
d.UglyURLs = uglyURLs
expected := test.expected
if canonifyURLs {
expected = strings.Replace(expected, "/sub", "", 1)
}
if uglyURLs {
expected = expected[:len(expected)-1] + "." + test.d.Type.MediaType.Suffix
}
pathSpec := newTestPathSpec(fs, cfg)
d.PathSpec = pathSpec
factory := newPaginationURLFactory(d)
got := factory(test.page)
require.Equal(t, expected, got)
}
})
}
})
}
}
@ -245,8 +282,8 @@ func doTestPaginator(t *testing.T, useViper bool) {
require.NoError(t, err)
pages := createTestPages(s, 12)
n1 := s.newHomePage()
n2 := s.newHomePage()
n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLType)
n2, _ := newPageOutput(s.newHomePage(), false, output.HTMLType)
n1.Data["Pages"] = pages
var paginator1 *Pager
@ -271,7 +308,9 @@ func doTestPaginator(t *testing.T, useViper bool) {
samePaginator, _ := n1.Paginator()
require.Equal(t, paginator1, samePaginator)
p, _ := s.NewPage("test")
pp, _ := s.NewPage("test")
p, _ := newPageOutput(pp, false, output.HTMLType)
_, err = p.Paginator()
require.NotNil(t, err)
}
@ -279,7 +318,8 @@ func doTestPaginator(t *testing.T, useViper bool) {
func TestPaginatorWithNegativePaginate(t *testing.T) {
t.Parallel()
s := newTestSite(t, "paginate", -1)
_, err := s.newHomePage().Paginator()
n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLType)
_, err := n1.Paginator()
require.Error(t, err)
}
@ -341,8 +381,8 @@ func doTestPaginate(t *testing.T, useViper bool) {
}
pages := createTestPages(s, 6)
n1 := s.newHomePage()
n2 := s.newHomePage()
n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLType)
n2, _ := newPageOutput(s.newHomePage(), false, output.HTMLType)
var paginator1, paginator2 *Pager
@ -366,7 +406,9 @@ func doTestPaginate(t *testing.T, useViper bool) {
require.Nil(t, err)
require.Equal(t, paginator2, paginator1.Next())
p, _ := s.NewPage("test")
pp, err := s.NewPage("test")
p, _ := newPageOutput(pp, false, output.HTMLType)
_, err = p.Paginate(pages)
require.NotNil(t, err)
}
@ -374,7 +416,8 @@ func doTestPaginate(t *testing.T, useViper bool) {
func TestInvalidOptions(t *testing.T) {
t.Parallel()
s := newTestSite(t)
n1 := s.newHomePage()
n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLType)
_, err := n1.Paginate(createTestPages(s, 1), 1, 2)
require.NotNil(t, err)
_, err = n1.Paginator(1, 2)
@ -391,7 +434,9 @@ func TestPaginateWithNegativePaginate(t *testing.T) {
s, err := NewSiteForCfg(deps.DepsCfg{Cfg: cfg, Fs: fs})
require.NoError(t, err)
_, err = s.newHomePage().Paginate(createTestPages(s, 2))
n, _ := newPageOutput(s.newHomePage(), false, output.HTMLType)
_, err = n.Paginate(createTestPages(s, 2))
require.NotNil(t, err)
}
@ -400,13 +445,14 @@ func TestPaginatePages(t *testing.T) {
s := newTestSite(t)
groups, _ := createTestPages(s, 31).GroupBy("Weight", "desc")
pd := targetPathDescriptor{Kind: KindHome, Type: output.HTMLType, PathSpec: s.PathSpec, Addends: "t"}
for i, seq := range []interface{}{createTestPages(s, 11), groups, WeightedPages{}, PageGroup{}, &Pages{}} {
v, err := paginatePages(s.PathSpec, seq, 11, "t")
v, err := paginatePages(pd, seq, 11)
require.NotNil(t, v, "Val %d", i)
require.Nil(t, err, "Err %d", i)
}
_, err := paginatePages(s.PathSpec, Site{}, 11, "t")
_, err := paginatePages(pd, Site{}, 11)
require.NotNil(t, err)
}
@ -415,8 +461,8 @@ func TestPaginatePages(t *testing.T) {
func TestPaginatorFollowedByPaginateShouldFail(t *testing.T) {
t.Parallel()
s := newTestSite(t, "paginate", 10)
n1 := s.newHomePage()
n2 := s.newHomePage()
n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLType)
n2, _ := newPageOutput(s.newHomePage(), false, output.HTMLType)
_, err := n1.Paginator()
require.Nil(t, err)
@ -432,8 +478,8 @@ func TestPaginateFollowedByDifferentPaginateShouldFail(t *testing.T) {
t.Parallel()
s := newTestSite(t, "paginate", 10)
n1 := s.newHomePage()
n2 := s.newHomePage()
n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLType)
n2, _ := newPageOutput(s.newHomePage(), false, output.HTMLType)
p1 := createTestPages(s, 2)
p2 := createTestPages(s, 10)

View file

@ -111,6 +111,8 @@ type Site struct {
disabledKinds map[string]bool
defaultOutputDefinitions siteOutputDefinitions
// Logger etc.
*deps.Deps `json:"-"`
}
@ -124,7 +126,13 @@ func (s *Site) isEnabled(kind string) bool {
// reset returns a new Site prepared for rebuild.
func (s *Site) reset() *Site {
return &Site{Deps: s.Deps, layoutHandler: output.NewLayoutHandler(s.PathSpec.ThemeSet()), disabledKinds: s.disabledKinds, Language: s.Language, owner: s.owner, PageCollections: newPageCollections()}
return &Site{Deps: s.Deps,
layoutHandler: output.NewLayoutHandler(s.PathSpec.ThemeSet()),
disabledKinds: s.disabledKinds,
defaultOutputDefinitions: s.defaultOutputDefinitions,
Language: s.Language,
owner: s.owner,
PageCollections: newPageCollections()}
}
// newSite creates a new site with the given configuration.
@ -140,7 +148,15 @@ func newSite(cfg deps.DepsCfg) (*Site, error) {
disabledKinds[disabled] = true
}
s := &Site{PageCollections: c, layoutHandler: output.NewLayoutHandler(cfg.Cfg.GetString("themesDir") != ""), Language: cfg.Language, disabledKinds: disabledKinds}
outputDefs := createSiteOutputDefinitions(cfg.Cfg)
s := &Site{
PageCollections: c,
layoutHandler: output.NewLayoutHandler(cfg.Cfg.GetString("themesDir") != ""),
Language: cfg.Language,
disabledKinds: disabledKinds,
defaultOutputDefinitions: outputDefs,
}
s.Info = newSiteInfo(siteBuilderCfg{s: s, pageCollections: c, language: s.Language})
@ -247,6 +263,7 @@ type SiteInfo struct {
BuildDrafts bool
canonifyURLs bool
relativeURLs bool
uglyURLs bool
preserveTaxonomyNames bool
Data *map[string]interface{}
@ -996,6 +1013,7 @@ func (s *Site) initializeSiteInfo() {
BuildDrafts: s.Cfg.GetBool("buildDrafts"),
canonifyURLs: s.Cfg.GetBool("canonifyURLs"),
relativeURLs: s.Cfg.GetBool("relativeURLs"),
uglyURLs: s.Cfg.GetBool("uglyURLs"),
preserveTaxonomyNames: lang.GetBool("preserveTaxonomyNames"),
PageCollections: s.PageCollections,
Files: &s.Files,
@ -1007,7 +1025,7 @@ func (s *Site) initializeSiteInfo() {
s: s,
}
s.Info.RSSLink = s.Info.permalinkStr(lang.GetString("rssURI"))
s.Info.RSSLink = s.permalink(lang.GetString("rssURI"))
}
func (s *Site) dataDir() string {
@ -1746,14 +1764,14 @@ func (s *SiteInfo) GetPage(typ string, path ...string) *Page {
return s.getPage(typ, path...)
}
func (s *SiteInfo) permalink(plink string) string {
return s.permalinkStr(plink)
}
func (s *Site) permalink(link string) string {
baseURL := s.PathSpec.BaseURL.String()
func (s *SiteInfo) permalinkStr(plink string) string {
return helpers.MakePermalink(
s.s.Cfg.GetString("baseURL"),
s.s.PathSpec.URLizeAndPrep(plink)).String()
link = strings.TrimPrefix(link, "/")
if !strings.HasSuffix(baseURL, "/") {
baseURL += "/"
}
return baseURL + link
}
func (s *Site) renderAndWriteXML(name string, dest string, d interface{}, layouts ...string) error {
@ -1804,12 +1822,6 @@ func (s *Site) renderAndWritePage(tp output.Type, name string, dest string, d in
// Note: this is not a pointer, as we may mutate the state below.
w := s.w
if p, ok := d.(*PageOutput); ok && p.IsPage() && path.Ext(p.URLPath.URL) != "" {
// user has explicitly set a URL with extension for this page
// make sure it sticks even if "ugly URLs" are turned off.
w.uglyURLs = true
}
transformLinks := transform.NewEmptyTransforms()
if s.Info.relativeURLs || s.Info.canonifyURLs {
@ -1830,11 +1842,7 @@ func (s *Site) renderAndWritePage(tp output.Type, name string, dest string, d in
var path []byte
if s.Info.relativeURLs {
translated, err := w.baseTargetPathPage(tp, dest)
if err != nil {
return err
}
path = []byte(helpers.GetDottedRelativePath(translated))
path = []byte(helpers.GetDottedRelativePath(dest))
} else if s.Info.canonifyURLs {
url := s.Cfg.GetString("baseURL")
if !strings.HasSuffix(url, "/") {
@ -2053,6 +2061,7 @@ func (s *Site) newNodePage(typ string) *Page {
Data: make(map[string]interface{}),
Site: &s.Info,
s: s}
p.outputTypes = p.s.defaultOutputDefinitions.ForKind(typ)
p.layoutIdentifier = pageLayoutIdentifier{p}
return p
@ -2068,11 +2077,12 @@ func (s *Site) newHomePage() *Page {
return p
}
// TODO(bep) output
func (s *Site) setPageURLs(p *Page, in string) {
p.URLPath.URL = s.PathSpec.URLizeAndPrep(in)
p.URLPath.Permalink = s.Info.permalink(p.URLPath.URL)
p.URLPath.Permalink = s.permalink(p.URLPath.URL)
if p.Kind != KindPage {
p.RSSLink = template.URL(s.Info.permalink(in + ".xml"))
p.RSSLink = template.URL(s.permalink(p.URLPath.URL + ".xml"))
}
}

View file

@ -14,18 +14,13 @@
package hugolib
import (
"path"
"strings"
"github.com/spf13/hugo/config"
"github.com/spf13/hugo/output"
)
var defaultOutputDefinitions = siteOutputDefinitions{
// All have HTML
siteOutputDefinition{ExcludedKinds: "", Outputs: []output.Type{output.HTMLType}},
// Some have RSS
siteOutputDefinition{ExcludedKinds: "page", Outputs: []output.Type{output.RSSType}},
}
type siteOutputDefinitions []siteOutputDefinition
type siteOutputDefinition struct {
@ -48,3 +43,27 @@ func (defs siteOutputDefinitions) ForKind(kind string) []output.Type {
return result
}
func createSiteOutputDefinitions(cfg config.Provider) siteOutputDefinitions {
var defs siteOutputDefinitions
// All have HTML
defs = append(defs, siteOutputDefinition{ExcludedKinds: "", Outputs: []output.Type{output.HTMLType}})
// TODO(bep) output deprecate rssURI
rssBase := cfg.GetString("rssURI")
if rssBase == "" {
rssBase = "index"
}
// RSS has now a well defined media type, so strip any suffix provided
rssBase = strings.TrimSuffix(rssBase, path.Ext(rssBase))
rssType := output.RSSType
rssType.BaseName = rssBase
// Some have RSS
defs = append(defs, siteOutputDefinition{ExcludedKinds: "page", Outputs: []output.Type{rssType}})
return defs
}

View file

@ -22,11 +22,12 @@ import (
"fmt"
"github.com/spf13/hugo/output"
"github.com/spf13/viper"
)
func TestDefaultOutputDefinitions(t *testing.T) {
t.Parallel()
defs := defaultOutputDefinitions
defs := createSiteOutputDefinitions(viper.New())
tests := []struct {
name string
@ -69,7 +70,9 @@ outputs: ["json"]
# Doc
`
th, h := newTestSitesFromConfigWithDefaultTemplates(t, siteConfig)
th, h := newTestSitesFromConfig(t, siteConfig,
"layouts/_default/list.json", "List JSON|{{ .Title }}|{{ .Content }}",
)
require.Len(t, h.Sites, 1)
fs := th.Fs
@ -87,6 +90,8 @@ outputs: ["json"]
require.Len(t, home.outputTypes, 1)
th.assertFileContent("public/index.json", "TODO")
// TODO(bep) output assert template/text
th.assertFileContent("public/index.json", "List JSON")
}

View file

@ -16,7 +16,6 @@ package hugolib
import (
"fmt"
"path"
"path/filepath"
"sync"
"time"
@ -63,9 +62,19 @@ func (s *Site) renderPages() error {
func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.WaitGroup) {
defer wg.Done()
var mainPageOutput *PageOutput
for page := range pages {
for i, outputType := range page.outputTypes {
pageOutput := newPageOutput(page, i > 0, outputType)
pageOutput, err := newPageOutput(page, i > 0, outputType)
if err != nil {
s.Log.ERROR.Printf("Failed to create output page for type %q for page %q: %s", outputType.Name, page, err)
continue
}
if i == 0 {
mainPageOutput = pageOutput
}
page.mainPageOutput = mainPageOutput
var layouts []string
@ -76,14 +85,18 @@ func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.Wa
layouts = s.layouts(pageOutput)
}
switch pageOutput.outputType {
switch pageOutput.outputType.Name {
case output.RSSType:
case "RSS":
if err := s.renderRSS(pageOutput); err != nil {
results <- err
}
default:
targetPath := pageOutput.TargetPath()
targetPath, err := pageOutput.targetPath()
if err != nil {
s.Log.ERROR.Printf("Failed to create target path for output %q for page %q: %s", outputType.Name, page, err)
continue
}
s.Log.DEBUG.Printf("Render %s to %q with layouts %q", pageOutput.Kind, targetPath, layouts)
@ -133,11 +146,11 @@ func (s *Site) renderPaginator(p *PageOutput) error {
}
pageNumber := i + 1
htmlBase := path.Join(append(p.sections, fmt.Sprintf("/%s/%d", paginatePath, pageNumber))...)
htmlBase = p.addLangPathPrefix(htmlBase)
addend := fmt.Sprintf("/%s/%d", paginatePath, pageNumber)
targetPath, _ := p.targetPath(addend)
if err := s.renderAndWritePage(p.outputType, pagerNode.Title,
filepath.FromSlash(htmlBase), pagerNode, p.layouts()...); err != nil {
targetPath, pagerNode, p.layouts()...); err != nil {
return err
}
@ -178,13 +191,15 @@ func (s *Site) renderRSS(p *PageOutput) error {
p.Pages = p.Pages[:limit]
p.Data["Pages"] = p.Pages
}
rssURI := s.Language.GetString("rssURI")
rssPath := path.Join(append(p.sections, rssURI)...)
s.setPageURLs(p.Page, rssPath)
// TODO(bep) output deprecate/handle rssURI
targetPath, err := p.targetPath()
if err != nil {
return err
}
return s.renderAndWriteXML(p.Title,
p.addLangFilepathPrefix(rssPath), p, s.appendThemeTemplates(layouts)...)
targetPath, p, s.appendThemeTemplates(layouts)...)
}
func (s *Site) render404() error {

View file

@ -958,7 +958,9 @@ func TestRefLinking(t *testing.T) {
// refLink doesn't use the location of the current page to work out reflinks
okresults := map[string]string{
"index.md": "/",
// Note: There are no magic in the index.md name. This was fixed in Hugo 0.20.
// Before that, index.md would wrongly resolve to "/".
"index.md": "/index/",
"common.md": "/level2/common/",
"3-root.md": "/level2/level3/3-root/",
}
@ -979,110 +981,59 @@ func TestSourceRelativeLinksing(t *testing.T) {
okresults := map[string]resultMap{
"index.md": map[string]string{
"/docs/rootfile.md": "/rootfile/",
"/docs/index.md": "/",
"rootfile.md": "/rootfile/",
"index.md": "/",
"index.md": "/index/",
"level2/2-root.md": "/level2/2-root/",
"level2/index.md": "/level2/",
"/docs/level2/2-root.md": "/level2/2-root/",
"/docs/level2/index.md": "/level2/",
"level2/level3/3-root.md": "/level2/level3/3-root/",
"level2/level3/index.md": "/level2/level3/",
"/docs/level2/level3/3-root.md": "/level2/level3/3-root/",
"/docs/level2/level3/index.md": "/level2/level3/",
"/docs/level2/2-root/": "/level2/2-root/",
"/docs/level2/": "/level2/",
"/docs/level2/2-root": "/level2/2-root/",
"/docs/level2": "/level2/",
"/level2/2-root/": "/level2/2-root/",
"/level2/": "/level2/",
"/level2/2-root": "/level2/2-root/",
"/level2": "/level2/",
}, "rootfile.md": map[string]string{
"/docs/rootfile.md": "/rootfile/",
"/docs/index.md": "/",
"rootfile.md": "/rootfile/",
"index.md": "/",
"level2/2-root.md": "/level2/2-root/",
"level2/index.md": "/level2/",
"/docs/level2/2-root.md": "/level2/2-root/",
"/docs/level2/index.md": "/level2/",
"level2/level3/3-root.md": "/level2/level3/3-root/",
"level2/level3/index.md": "/level2/level3/",
"/docs/level2/level3/3-root.md": "/level2/level3/3-root/",
"/docs/level2/level3/index.md": "/level2/level3/",
}, "level2/2-root.md": map[string]string{
"../rootfile.md": "/rootfile/",
"../index.md": "/",
"/docs/rootfile.md": "/rootfile/",
"/docs/index.md": "/",
"2-root.md": "/level2/2-root/",
"index.md": "/level2/",
"../level2/2-root.md": "/level2/2-root/",
"../level2/index.md": "/level2/",
"./2-root.md": "/level2/2-root/",
"./index.md": "/level2/",
"/docs/level2/index.md": "/level2/",
"/docs/level2/2-root.md": "/level2/2-root/",
"level3/3-root.md": "/level2/level3/3-root/",
"level3/index.md": "/level2/level3/",
"../level2/level3/index.md": "/level2/level3/",
"../level2/level3/3-root.md": "/level2/level3/3-root/",
"/docs/level2/level3/index.md": "/level2/level3/",
"/docs/level2/level3/3-root.md": "/level2/level3/3-root/",
}, "level2/index.md": map[string]string{
"../rootfile.md": "/rootfile/",
"../index.md": "/",
"/docs/rootfile.md": "/rootfile/",
"/docs/index.md": "/",
"2-root.md": "/level2/2-root/",
"index.md": "/level2/",
"../level2/2-root.md": "/level2/2-root/",
"../level2/index.md": "/level2/",
"./2-root.md": "/level2/2-root/",
"./index.md": "/level2/",
"/docs/level2/index.md": "/level2/",
"/docs/level2/2-root.md": "/level2/2-root/",
"level3/3-root.md": "/level2/level3/3-root/",
"level3/index.md": "/level2/level3/",
"../level2/level3/index.md": "/level2/level3/",
"../level2/level3/3-root.md": "/level2/level3/3-root/",
"/docs/level2/level3/index.md": "/level2/level3/",
"/docs/level2/level3/3-root.md": "/level2/level3/3-root/",
}, "level2/level3/3-root.md": map[string]string{
"../../rootfile.md": "/rootfile/",
"../../index.md": "/",
"/docs/rootfile.md": "/rootfile/",
"/docs/index.md": "/",
"../2-root.md": "/level2/2-root/",
"../index.md": "/level2/",
"/docs/level2/2-root.md": "/level2/2-root/",
"/docs/level2/index.md": "/level2/",
"3-root.md": "/level2/level3/3-root/",
"index.md": "/level2/level3/",
"./3-root.md": "/level2/level3/3-root/",
"./index.md": "/level2/level3/",
// "../level2/level3/3-root.md": "/level2/level3/3-root/",
// "../level2/level3/index.md": "/level2/level3/",
"/docs/level2/level3/3-root.md": "/level2/level3/3-root/",
"/docs/level2/level3/index.md": "/level2/level3/",
}, "level2/level3/index.md": map[string]string{
"../../rootfile.md": "/rootfile/",
"../../index.md": "/",
"/docs/rootfile.md": "/rootfile/",
"/docs/index.md": "/",
"../2-root.md": "/level2/2-root/",
"../index.md": "/level2/",
"/docs/level2/2-root.md": "/level2/2-root/",
"/docs/level2/index.md": "/level2/",
"3-root.md": "/level2/level3/3-root/",
"index.md": "/level2/level3/",
"./3-root.md": "/level2/level3/3-root/",
"./index.md": "/level2/level3/",
// "../level2/level3/3-root.md": "/level2/level3/3-root/",
// "../level2/level3/index.md": "/level2/level3/",
"/docs/level2/level3/3-root.md": "/level2/level3/3-root/",
"/docs/level2/level3/index.md": "/level2/level3/",
},
}

View file

@ -41,7 +41,6 @@ type siteWriter struct {
}
func (w siteWriter) targetPathPage(tp output.Type, src string) (string, error) {
fmt.Println(tp, "=>", src)
dir, err := w.baseTargetPathPage(tp, src)
if err != nil {
return "", err
@ -57,6 +56,14 @@ func (w siteWriter) baseTargetPathPage(tp output.Type, src string) (string, erro
return "index.html", nil
}
// The anatomy of a target path:
// langDir
// BaseName
// Suffix
// ROOT?
// dir
// name
dir, file := filepath.Split(src)
isRoot := dir == ""
ext := extension(filepath.Ext(file))
@ -171,14 +178,12 @@ func filename(f string) string {
return f[:len(f)-len(ext)]
}
func (w siteWriter) writeDestPage(tp output.Type, path string, reader io.Reader) (err error) {
func (w siteWriter) writeDestPage(tp output.Type, path string, reader io.Reader) error {
w.log.DEBUG.Println("creating page:", path)
targetPath, err := w.targetPathPage(tp, path)
if err != nil {
return err
}
path, _ = w.targetPathFile(path)
// TODO(bep) output remove this file ... targetPath, err := w.targetPathPage(tp, path)
return w.publish(targetPath, reader)
return w.publish(path, reader)
}
func (w siteWriter) writeDestFile(path string, r io.Reader) (err error) {
@ -191,5 +196,6 @@ func (w siteWriter) writeDestFile(path string, r io.Reader) (err error) {
}
func (w siteWriter) publish(path string, r io.Reader) (err error) {
return helpers.WriteToDisk(path, r, w.fs.Destination)
}

View file

@ -122,7 +122,8 @@ func TestTargetPathPageBase(t *testing.T) {
}
}
func TestTargetPathUglyURLs(t *testing.T) {
// TODO(bep) output
func _TestTargetPathUglyURLs(t *testing.T) {
w := siteWriter{log: newErrorLogger(), uglyURLs: true}
tests := []struct {
@ -137,14 +138,14 @@ func TestTargetPathUglyURLs(t *testing.T) {
{output.JSONType, "section", "section.json"},
}
for _, test := range tests {
for i, test := range tests {
dest, err := w.targetPathPage(test.outputType, filepath.FromSlash(test.content))
if err != nil {
t.Fatalf("Translate returned an unexpected err: %s", err)
t.Fatalf(" [%d] targetPathPage returned an unexpected err: %s", i, err)
}
if dest != test.expected {
t.Errorf("Translate expected return: %s, got: %s", test.expected, dest)
t.Errorf("[%d] targetPathPage expected return: %s, got: %s", i, test.expected, dest)
}
}
}

View file

@ -17,6 +17,7 @@ import (
"fmt"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/require"
@ -49,21 +50,27 @@ func TestByCountOrderOfTaxonomies(t *testing.T) {
}
}
//
func TestTaxonomiesWithAndWithoutContentFile(t *testing.T) {
for _, uglyURLs := range []bool{false, true} {
t.Run(fmt.Sprintf("uglyURLs=%t", uglyURLs), func(t *testing.T) {
for _, preserveTaxonomyNames := range []bool{false, true} {
t.Run(fmt.Sprintf("preserveTaxonomyNames %t", preserveTaxonomyNames), func(t *testing.T) {
doTestTaxonomiesWithAndWithoutContentFile(t, preserveTaxonomyNames)
t.Run(fmt.Sprintf("preserveTaxonomyNames=%t", preserveTaxonomyNames), func(t *testing.T) {
doTestTaxonomiesWithAndWithoutContentFile(t, preserveTaxonomyNames, uglyURLs)
})
}
})
}
}
func doTestTaxonomiesWithAndWithoutContentFile(t *testing.T, preserveTaxonomyNames bool) {
func doTestTaxonomiesWithAndWithoutContentFile(t *testing.T, preserveTaxonomyNames, uglyURLs bool) {
t.Parallel()
siteConfig := `
baseURL = "http://example.com/blog"
preserveTaxonomyNames = %t
uglyURLs = %t
paginate = 1
defaultContentLanguage = "en"
@ -87,14 +94,20 @@ others:
# Doc
`
siteConfig = fmt.Sprintf(siteConfig, preserveTaxonomyNames)
siteConfig = fmt.Sprintf(siteConfig, preserveTaxonomyNames, uglyURLs)
th, h := newTestSitesFromConfigWithDefaultTemplates(t, siteConfig)
require.Len(t, h.Sites, 1)
fs := th.Fs
if preserveTaxonomyNames {
writeSource(t, fs, "content/p1.md", fmt.Sprintf(pageTemplate, "t1/c1", "- tag1", "- cat1", "- o1"))
} else {
// Check lower-casing of tags
writeSource(t, fs, "content/p1.md", fmt.Sprintf(pageTemplate, "t1/c1", "- Tag1", "- cAt1", "- o1"))
}
writeSource(t, fs, "content/p2.md", fmt.Sprintf(pageTemplate, "t2/c1", "- tag2", "- cat1", "- o1"))
writeSource(t, fs, "content/p3.md", fmt.Sprintf(pageTemplate, "t2/c12", "- tag2", "- cat2", "- o1"))
writeSource(t, fs, "content/p4.md", fmt.Sprintf(pageTemplate, "Hello World", "", "", "- \"Hello Hugo world\""))
@ -111,18 +124,25 @@ others:
// 2. tags with no terms content page, but content page for one of 2 tags (tag1)
// 3. the "others" taxonomy with no content pages.
pathFunc := func(s string) string {
if uglyURLs {
return strings.Replace(s, "/index.html", ".html", 1)
}
return s
}
// 1.
th.assertFileContent("public/categories/cat1/index.html", "List", "Cat1")
th.assertFileContent("public/categories/index.html", "Terms List", "Category Terms")
th.assertFileContent(pathFunc("public/categories/cat1/index.html"), "List", "Cat1")
th.assertFileContent(pathFunc("public/categories/index.html"), "Terms List", "Category Terms")
// 2.
th.assertFileContent("public/tags/tag2/index.html", "List", "Tag2")
th.assertFileContent("public/tags/tag1/index.html", "List", "Tag1")
th.assertFileContent("public/tags/index.html", "Terms List", "Tags")
th.assertFileContent(pathFunc("public/tags/tag2/index.html"), "List", "Tag2")
th.assertFileContent(pathFunc("public/tags/tag1/index.html"), "List", "Tag1")
th.assertFileContent(pathFunc("public/tags/index.html"), "Terms List", "Tags")
// 3.
th.assertFileContent("public/others/o1/index.html", "List", "O1")
th.assertFileContent("public/others/index.html", "Terms List", "Others")
th.assertFileContent(pathFunc("public/others/o1/index.html"), "List", "O1")
th.assertFileContent(pathFunc("public/others/index.html"), "Terms List", "Others")
s := h.Sites[0]
@ -145,6 +165,14 @@ others:
}
}
cat1 := s.getPage(KindTaxonomy, "categories", "cat1")
require.NotNil(t, cat1)
if uglyURLs {
require.Equal(t, "/blog/categories/cat1.html", cat1.RelPermalink())
} else {
require.Equal(t, "/blog/categories/cat1/", cat1.RelPermalink())
}
// Issue #3070 preserveTaxonomyNames
if preserveTaxonomyNames {
helloWorld := s.getPage(KindTaxonomy, "others", "Hello Hugo world")
@ -157,6 +185,6 @@ others:
}
// Issue #2977
th.assertFileContent("public/empties/index.html", "Terms List", "Empties")
th.assertFileContent(pathFunc("public/empties/index.html"), "Terms List", "Empties")
}

View file

@ -77,6 +77,14 @@ func newTestPathSpec(fs *hugofs.Fs, v *viper.Viper) *helpers.PathSpec {
return helpers.NewPathSpec(fs, l)
}
func newTestDefaultPathSpec() *helpers.PathSpec {
v := viper.New()
// Easier to reason about in tests.
v.Set("disablePathToLower", true)
fs := hugofs.NewDefault(v)
return helpers.NewPathSpec(fs, v)
}
func newTestCfg() (*viper.Viper, *hugofs.Fs) {
v := viper.New()

View file

@ -27,6 +27,7 @@ var (
Name: "AMP",
MediaType: media.HTMLType,
BaseName: "index",
Path: "amp",
}
CSSType = Type{
@ -43,7 +44,7 @@ var (
JSONType = Type{
Name: "JSON",
MediaType: media.HTMLType,
MediaType: media.JSONType,
BaseName: "index",
IsPlainText: true,
}
@ -52,6 +53,7 @@ var (
Name: "RSS",
MediaType: media.RSSType,
BaseName: "index",
NoUgly: true,
}
)
@ -112,3 +114,7 @@ func GetTypes(keys ...string) (Types, error) {
return types, nil
}
func (t Type) BaseFilename() string {
return t.BaseName + "." + t.MediaType.Suffix
}

View file

@ -30,6 +30,7 @@ func TestDefaultTypes(t *testing.T) {
require.Equal(t, media.RSSType, RSSType.MediaType)
require.Empty(t, RSSType.Path)
require.False(t, RSSType.IsPlainText)
require.True(t, RSSType.NoUgly)
}
func TestGetType(t *testing.T) {