From 45eaa7ffd525e53fe98b7de0a04f5e0a23150dec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Thu, 9 Mar 2017 19:19:29 +0100 Subject: [PATCH] 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 --- helpers/pathspec.go | 10 +- helpers/pathspec_test.go | 2 +- helpers/url.go | 77 ++++++++-- hugolib/embedded_shortcodes_test.go | 6 +- hugolib/hugo_sites.go | 5 - hugolib/hugo_sites_build.go | 6 + hugolib/hugo_sites_build_test.go | 28 ++-- hugolib/node_as_page_test.go | 7 +- hugolib/page.go | 182 +++------------------- hugolib/page_output.go | 40 ++++- hugolib/page_paths.go | 230 ++++++++++++++++++++++++++++ hugolib/page_paths_test.go | 166 ++++++++++++++++++++ hugolib/page_test.go | 81 ++++++++-- hugolib/pagination.go | 48 +++--- hugolib/pagination_test.go | 108 +++++++++---- hugolib/site.go | 56 ++++--- hugolib/site_output.go | 33 +++- hugolib/site_output_test.go | 11 +- hugolib/site_render.go | 39 +++-- hugolib/site_test.go | 81 ++-------- hugolib/site_writer.go | 20 ++- hugolib/site_writer_test.go | 9 +- hugolib/taxonomy_test.go | 56 +++++-- hugolib/testhelpers_test.go | 8 + output/outputType.go | 8 +- output/outputType_test.go | 1 + 26 files changed, 915 insertions(+), 403 deletions(-) create mode 100644 hugolib/page_paths.go create mode 100644 hugolib/page_paths_test.go diff --git a/helpers/pathspec.go b/helpers/pathspec.go index ddc183380de..ffca4df8ec2 100644 --- a/helpers/pathspec.go +++ b/helpers/pathspec.go @@ -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,8 +34,7 @@ type PathSpec struct { // pagination path handling paginatePath string - baseURL string - theme string + theme string // Directories themesDir string @@ -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"), diff --git a/helpers/pathspec_test.go b/helpers/pathspec_test.go index 07948bb65cc..c67c6fbdcdb 100644 --- a/helpers/pathspec_test.go +++ b/helpers/pathspec_test.go @@ -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) diff --git a/helpers/url.go b/helpers/url.go index bf617ff16b1..a73e54999e8 100644 --- a/helpers/url.go +++ b/helpers/url.go @@ -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)) } diff --git a/hugolib/embedded_shortcodes_test.go b/hugolib/embedded_shortcodes_test.go index 5f0f621f904..92821d0ef2a 100644 --- a/hugolib/embedded_shortcodes_test.go +++ b/hugolib/embedded_shortcodes_test.go @@ -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") diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 89e5c796ea8..d0ad5752566 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -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() diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index 471c1a65dce..ce7271cd332 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -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() diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go index f6b40ec82f6..e5e36bdcdee 100644 --- a/hugolib/hugo_sites_build_test.go +++ b/hugolib/hugo_sites_build_test.go @@ -112,12 +112,13 @@ func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) { th.assertFileContent("public/en/sitemap.xml", "http://example.com/blog/en/") // Check rss - th.assertFileContent("public/fr/index.xml", ` 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,39 +875,25 @@ func (p *Page) URL() string { return u } -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 - } +// Permalink returns the absolute URL to this Page. +func (p *Page) Permalink() string { + p.initURLs() + return p.permalink +} - p.relPermalink = relpath - return - } +// RelPermalink gets a URL to the resource relative to the host. +func (p *Page) RelPermalink() string { + p.initURLs() + return p.relPermalink +} - link.Scheme = "" - link.Host = "" - link.User = nil - link.Opaque = "" - p.relPermalink = link.String() +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 }) - - return p.relPermalink } var ErrHasDraftAndPublished = errors.New("both draft and published parameters were found in page's frontmatter") @@ -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 diff --git a/hugolib/page_output.go b/hugolib/page_output.go index 45df23388ee..88386a6d0e5 100644 --- a/hugolib/page_output.go +++ b/hugolib/page_output.go @@ -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 } diff --git a/hugolib/page_paths.go b/hugolib/page_paths.go new file mode 100644 index 00000000000..1347102b863 --- /dev/null +++ b/hugolib/page_paths.go @@ -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 "" +} diff --git a/hugolib/page_paths_test.go b/hugolib/page_paths_test.go new file mode 100644 index 00000000000..ac322d19c32 --- /dev/null +++ b/hugolib/page_paths_test.go @@ -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) + } + } + }) + } + }) + } + +} diff --git a/hugolib/page_test.go b/hugolib/page_test.go index cda5ccec10c..6a920f5889d 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -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"), "{{.Content}}") + writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), + "P{{.Paginator.PageNumber}}|URL: {{.Paginator.URL}}|{{ if .Paginator.HasNext }}Next: {{.Paginator.Next.URL }}{{ end }}") + + 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", `P1|URL: /post.html|Next: /post/page/2.html`) + th.assertFileContent("public/post/page/2.html", `P2|URL: /post/page/2.html|Next: /post/page/3.html`) + } else { + th.assertFileContent("public/post/page/1/index.html", `canonical" href="/post/"/`) + th.assertFileContent("public/post/index.html", `P1|URL: /post/|Next: /post/page/2/`) + th.assertFileContent("public/post/page/2/index.html", `P2|URL: /post/page/2/|Next: /post/page/3/`) + th.assertFileContent("public/tags/.net/index.html", `P1|URL: /tags/.net/|Next: /tags/.net/page/2/`) + + } + + 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") diff --git a/hugolib/pagination.go b/hugolib/pagination.go index aa20f8d554e..e9ca6816be9 100644 --- a/hugolib/pagination.go +++ b/hugolib/pagination.go @@ -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) } } diff --git a/hugolib/pagination_test.go b/hugolib/pagination_test.go index ba697d50df4..2f64c6c18c0 100644 --- a/hugolib/pagination_test.go +++ b/hugolib/pagination_test.go @@ -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,26 +203,61 @@ 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) - - 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) + 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) { + + 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/"}, + } + + 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) + + } + }) + } + }) } - require.Equal(t, "/foo/bar/zoo/12345/", fooBar(12345)) - } func TestPaginator(t *testing.T) { @@ -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) diff --git a/hugolib/site.go b/hugolib/site.go index 903032d74f8..dbd0ea7c22e 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -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")) } } diff --git a/hugolib/site_output.go b/hugolib/site_output.go index 7f6fa2d4a3b..53d3f979921 100644 --- a/hugolib/site_output.go +++ b/hugolib/site_output.go @@ -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 +} diff --git a/hugolib/site_output_test.go b/hugolib/site_output_test.go index 03c5b7394ff..6ad53c0f8fe 100644 --- a/hugolib/site_output_test.go +++ b/hugolib/site_output_test.go @@ -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") } diff --git a/hugolib/site_render.go b/hugolib/site_render.go index b89cd06a5f9..27fbf7dd7ba 100644 --- a/hugolib/site_render.go +++ b/hugolib/site_render.go @@ -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 { diff --git a/hugolib/site_test.go b/hugolib/site_test.go index b8a5eb21123..8204ee99e56 100644 --- a/hugolib/site_test.go +++ b/hugolib/site_test.go @@ -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/", + "../../rootfile.md": "/rootfile/", + "/docs/rootfile.md": "/rootfile/", + "../2-root.md": "/level2/2-root/", + "/docs/level2/2-root.md": "/level2/2-root/", + "3-root.md": "/level2/level3/3-root/", + "./3-root.md": "/level2/level3/3-root/", "/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/", + "../../rootfile.md": "/rootfile/", + "/docs/rootfile.md": "/rootfile/", + "../2-root.md": "/level2/2-root/", + "/docs/level2/2-root.md": "/level2/2-root/", + "3-root.md": "/level2/level3/3-root/", + "./3-root.md": "/level2/level3/3-root/", "/docs/level2/level3/3-root.md": "/level2/level3/3-root/", - "/docs/level2/level3/index.md": "/level2/level3/", }, } diff --git a/hugolib/site_writer.go b/hugolib/site_writer.go index 4477e9a1280..976f5def0ef 100644 --- a/hugolib/site_writer.go +++ b/hugolib/site_writer.go @@ -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) } diff --git a/hugolib/site_writer_test.go b/hugolib/site_writer_test.go index 0c68db49fe2..a17dae9f9ca 100644 --- a/hugolib/site_writer_test.go +++ b/hugolib/site_writer_test.go @@ -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) } } } diff --git a/hugolib/taxonomy_test.go b/hugolib/taxonomy_test.go index 6843838b9a9..421fde81d57 100644 --- a/hugolib/taxonomy_test.go +++ b/hugolib/taxonomy_test.go @@ -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 _, preserveTaxonomyNames := range []bool{false, true} { - t.Run(fmt.Sprintf("preserveTaxonomyNames %t", preserveTaxonomyNames), func(t *testing.T) { - doTestTaxonomiesWithAndWithoutContentFile(t, preserveTaxonomyNames) + 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, 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 - writeSource(t, fs, "content/p1.md", fmt.Sprintf(pageTemplate, "t1/c1", "- tag1", "- cat1", "- o1")) + 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") } diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index a38a12bdca0..1caa97e4e4c 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -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() diff --git a/output/outputType.go b/output/outputType.go index e3df96f0bf4..ad66b4b68b3 100644 --- a/output/outputType.go +++ b/output/outputType.go @@ -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 +} diff --git a/output/outputType_test.go b/output/outputType_test.go index a55b9a81a3c..3eb56d8d3c5 100644 --- a/output/outputType_test.go +++ b/output/outputType_test.go @@ -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) {