// Copyright 2019 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package hugolib import ( "fmt" "path/filepath" "strings" "sync" "testing" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/identity" ) func TestContentMapSite(t *testing.T) { b := newTestSitesBuilder(t) pageTempl := ` --- title: "Page %d" date: "2019-06-%02d" lastMod: "2019-06-%02d" categories: [%q] --- Page content. ` createPage := func(i int) string { return fmt.Sprintf(pageTempl, i, i, i+1, "funny") } createPageInCategory := func(i int, category string) string { return fmt.Sprintf(pageTempl, i, i, i+1, category) } draftTemplate := `--- title: "Draft" draft: true --- ` b.WithContent("_index.md", ` --- title: "Hugo Home" cascade: description: "Common Description" --- Home Content. `) b.WithContent("blog/page1.md", createPage(1)) b.WithContent("blog/page2.md", createPage(2)) b.WithContent("blog/page3.md", createPage(3)) b.WithContent("blog/bundle/index.md", createPage(4)) b.WithContent("blog/bundle/data.json", "data") b.WithContent("blog/bundle/page.md", createPage(5)) b.WithContent("blog/subsection/_index.md", createPage(6)) b.WithContent("blog/subsection/subdata.json", "data") b.WithContent("blog/subsection/page4.md", createPage(7)) b.WithContent("blog/subsection/page5.md", createPage(8)) b.WithContent("blog/subsection/draft/index.md", draftTemplate) b.WithContent("blog/subsection/draft/data.json", "data") b.WithContent("blog/draftsection/_index.md", draftTemplate) b.WithContent("blog/draftsection/page/index.md", createPage(9)) b.WithContent("blog/draftsection/page/folder/data.json", "data") b.WithContent("blog/draftsection/sub/_index.md", createPage(10)) b.WithContent("blog/draftsection/sub/page.md", createPage(11)) b.WithContent("docs/page6.md", createPage(12)) b.WithContent("tags/_index.md", createPageInCategory(13, "sad")) b.WithContent("overlap/_index.md", createPageInCategory(14, "sad")) b.WithContent("overlap2/_index.md", createPage(15)) b.WithTemplatesAdded("layouts/index.html", ` Num Regular: {{ len .Site.RegularPages }}|{{ range .Site.RegularPages }}{{ .RelPermalink }}|{{ end }}$ Main Sections: {{ .Site.Params.mainSections }} Pag Num Pages: {{ len .Paginator.Pages }} {{ $home := .Site.Home }} {{ $blog := .Site.GetPage "blog" }} {{ $categories := .Site.GetPage "categories" }} {{ $funny := .Site.GetPage "categories/funny" }} {{ $blogSub := .Site.GetPage "blog/subsection" }} {{ $page := .Site.GetPage "blog/page1" }} {{ $page2 := .Site.GetPage "blog/page2" }} {{ $page4 := .Site.GetPage "blog/subsection/page4" }} {{ $bundle := .Site.GetPage "blog/bundle" }} {{ $overlap1 := .Site.GetPage "overlap" }} {{ $overlap2 := .Site.GetPage "overlap2" }} Home: {{ template "print-page" $home }} Blog Section: {{ template "print-page" $blog }} Blog Sub Section: {{ template "print-page" $blogSub }} Page: {{ template "print-page" $page }} Bundle: {{ template "print-page" $bundle }} IsDescendant: true: {{ $page.IsDescendant $blog }} true: {{ $blogSub.IsDescendant $blog }} true: {{ $bundle.IsDescendant $blog }} true: {{ $page4.IsDescendant $blog }} true: {{ $blog.IsDescendant $home }} false: {{ $blog.IsDescendant $blog }} false: {{ $home.IsDescendant $blog }} IsAncestor: true: {{ $blog.IsAncestor $page }} true: {{ $home.IsAncestor $blog }} true: {{ $blog.IsAncestor $blogSub }} true: {{ $blog.IsAncestor $bundle }} true: {{ $blog.IsAncestor $page4 }} true: {{ $home.IsAncestor $page }} false: {{ $blog.IsAncestor $blog }} false: {{ $page.IsAncestor $blog }} false: {{ $blog.IsAncestor $home }} false: {{ $blogSub.IsAncestor $blog }} IsDescendant overlap1: false: {{ $overlap1.IsDescendant $overlap2 }} IsDescendant overlap2: false: {{ $overlap2.IsDescendant $overlap1 }} IsAncestor overlap1: false: {{ $overlap1.IsAncestor $overlap2 }} IsAncestor overlap2: false: {{ $overlap2.IsAncestor $overlap1 }} FirstSection: {{ $blogSub.FirstSection.RelPermalink }} {{ $blog.FirstSection.RelPermalink }} {{ $home.FirstSection.RelPermalink }} {{ $page.FirstSection.RelPermalink }} InSection: true: {{ $page.InSection $blog }} false: {{ $page.InSection $blogSub }} Next: {{ $page2.Next.RelPermalink }} NextInSection: {{ $page2.NextInSection.RelPermalink }} Pages: {{ range $blog.Pages }}{{ .RelPermalink }}|{{ end }} Sections: {{ range $home.Sections }}{{ .RelPermalink }}|{{ end }}:END Categories: {{ range .Site.Taxonomies.categories }}{{ .Page.RelPermalink }}; {{ .Page.Title }}; {{ .Count }}|{{ end }}:END Category Terms: {{ $categories.Kind}}: {{ range $categories.Data.Terms.Alphabetical }}{{ .Page.RelPermalink }}; {{ .Page.Title }}; {{ .Count }}|{{ end }}:END Category Funny: {{ $funny.Kind}}; {{ $funny.Data.Term }}: {{ range $funny.Pages }}{{ .RelPermalink }}|{{ end }}:END Pag Num Pages: {{ len .Paginator.Pages }} Pag Blog Num Pages: {{ len $blog.Paginator.Pages }} Blog Num RegularPages: {{ len $blog.RegularPages }}|{{ range $blog.RegularPages }}P: {{ .RelPermalink }}|{{ end }} Blog Num Pages: {{ len $blog.Pages }} Draft1: {{ if (.Site.GetPage "blog/subsection/draft") }}FOUND{{ end }}| Draft2: {{ if (.Site.GetPage "blog/draftsection") }}FOUND{{ end }}| Draft3: {{ if (.Site.GetPage "blog/draftsection/page") }}FOUND{{ end }}| Draft4: {{ if (.Site.GetPage "blog/draftsection/sub") }}FOUND{{ end }}| Draft5: {{ if (.Site.GetPage "blog/draftsection/sub/page") }}FOUND{{ end }}| {{ define "print-page" }}{{ .Title }}|{{ .RelPermalink }}|{{ .Date.Format "2006-01-02" }}|Current Section: {{ with .CurrentSection }}{{ .Path }}{{ else }}NIL{{ end }}|Resources: {{ range .Resources }}{{ .ResourceType }}: {{ .RelPermalink }}|{{ end }}{{ end }} `) b.Build(BuildCfg{}) b.AssertFileContent("public/index.html", ` Num Regular: 9 Main Sections: [blog] Pag Num Pages: 9 Home: Hugo Home|/|2019-06-15|Current Section: /|Resources: Blog Section: Blogs|/blog/|2019-06-11|Current Section: /blog|Resources: Blog Sub Section: Page 6|/blog/subsection/|2019-06-06|Current Section: /blog/subsection|Resources: application: /blog/subsection/subdata.json| Page: Page 1|/blog/page1/|2019-06-01|Current Section: /blog|Resources: Bundle: Page 4|/blog/bundle/|2019-06-04|Current Section: /blog|Resources: application: /blog/bundle/data.json|page: | IsDescendant: true: true true: true true: true true: true true: true false: false false: false IsAncestor: true: true true: true true: true true: true true: true true: true false: false false: false false: false false: false IsDescendant overlap1: false: false IsDescendant overlap2: false: false IsAncestor overlap1: false: false IsAncestor overlap2: false: false FirstSection: /blog/ /blog/ / /blog/ InSection: true: true false: false Next: /blog/page3/ NextInSection: /blog/page3/ Pages: /blog/subsection/|/blog/bundle/|/blog/page3/|/blog/page2/|/blog/page1/| Sections: /overlap2/|/overlap/|/docs/|/blog/|:END Categories: /categories/funny/; Funny; 12|/categories/sad/; Sad; 2|:END Category Terms: taxonomy: /categories/funny/; Funny; 12|/categories/sad/; Sad; 2|:END Category Funny: term; funny: /overlap2/|/docs/page6/|/blog/draftsection/sub/page/|/blog/draftsection/sub/|/blog/draftsection/page/|/blog/subsection/page5/|/blog/subsection/page4/|/blog/subsection/|/blog/bundle/|/blog/page3/|/blog/page2/|/blog/page1/|:END Pag Num Pages: 9 Pag Blog Num Pages: 4 Blog Num RegularPages: 4 Blog Num Pages: 5 Draft1: | Draft2: FOUND| Draft3: FOUND| Draft4: FOUND| Draft5: FOUND| `) } func TestIntegrationTestTemplate(t *testing.T) { t.Parallel() c := qt.New(t) files := ` -- config.toml -- title = "Integration Test" disableKinds=["page", "section", "taxonomy", "term", "sitemap", "robotsTXT", "RSS"] -- layouts/index.html -- Home: {{ .Title }}| ` b := NewIntegrationTestBuilder( IntegrationTestConfig{ T: c, TxtarString: files, }).Build() b.AssertFileContent("public/index.html", "Home: Integration Test|") } // Issue #11840 func TestBundleResourceLanguageBestMatch(t *testing.T) { files := ` -- hugo.toml -- defaultContentLanguage = "fr" defaultContentLanguageInSubdir = true [languages] [languages.en] weight = 1 [languages.fr] weight = 2 [languages.de] weight = 3 -- layouts/index.html -- {{ $bundle := site.GetPage "bundle" }} {{ $r := $bundle.Resources.GetMatch "*.txt" }} {{ .Language.Lang }}: {{ $r.RelPermalink }}|{{ $r.Content }} -- content/bundle/index.fr.md -- --- title: "Bundle Fr" --- -- content/bundle/index.en.md -- --- title: "Bundle En" --- -- content/bundle/index.de.md -- --- title: "Bundle De" --- -- content/bundle/data.fr.txt -- Data fr -- content/bundle/data.en.txt -- Data en ` b := Test(t, files) b.AssertFileContent("public/fr/index.html", "fr: /fr/bundle/data.fr.txt|Data fr") b.AssertFileContent("public/en/index.html", "en: /en/bundle/data.en.txt|Data en") b.AssertFileContent("public/de/index.html", "de: /fr/bundle/data.fr.txt|Data fr") } func TestBundleMultipleContentPageWithSamePath(t *testing.T) { files := ` -- hugo.toml -- -- content/bundle/index.md -- --- title: "Bundle md" foo: md --- -- content/bundle/index.html -- --- title: "Bundle html" foo: html --- -- content/bundle/data.txt -- Data. -- content/p1.md -- --- title: "P1 md" foo: md --- -- content/p1.html -- --- title: "P1 html" foo: html --- -- layouts/index.html -- {{ $bundle := site.GetPage "bundle" }} Bundle: {{ $bundle.Title }}|{{ $bundle.Params.foo }}|{{ $bundle.File.Filename }}| {{ $p1 := site.GetPage "p1" }} P1: {{ $p1.Title }}|{{ $p1.Params.foo }}|{{ $p1.File.Filename }}| ` b := Test(t, files) // There's multiple content files sharing the same logical path and language. // This is a little arbitrary, but we have to pick one and prefer the Markdown version. b.AssertFileContent("public/index.html", filepath.FromSlash("Bundle: Bundle md|md|/content/bundle/index.md|"), filepath.FromSlash("P1: P1 md|md|/content/p1.md|"), ) } // Issue #11944 func TestBundleResourcesGetWithSpacesInFilename(t *testing.T) { files := ` -- hugo.toml -- baseURL = "https://example.com" disableKinds = ["taxonomy", "term"] -- content/bundle/index.md -- -- content/bundle/data with Spaces.txt -- Data. -- layouts/index.html -- {{ $bundle := site.GetPage "bundle" }} {{ $r := $bundle.Resources.Get "data with Spaces.txt" }} R: {{ with $r }}{{ .Content }}{{ end }}| ` b := Test(t, files) b.AssertFileContent("public/index.html", "R: Data.") } // Issue #11946. func TestBundleResourcesGetDuplicateSortOrder(t *testing.T) { files := ` -- hugo.toml -- baseURL = "https://example.com" -- content/bundle/index.md -- -- content/bundle/data-1.txt -- data-1.txt -- content/bundle/data 1.txt -- data 1.txt -- content/bundle/Data 1.txt -- Data 1.txt -- content/bundle/Data-1.txt -- Data-1.txt -- layouts/index.html -- {{ $bundle := site.GetPage "bundle" }} {{ $r := $bundle.Resources.Get "data-1.txt" }} R: {{ with $r }}{{ .Content }}{{ end }}|Len: {{ len $bundle.Resources }}|$ ` for i := 0; i < 3; i++ { b := Test(t, files) b.AssertFileContent("public/index.html", "R: Data 1.txt|", "Len: 1|") } } func TestBundleResourcesNoPublishedIssue12198(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- disableKinds = ['home','rss','sitemap','taxonomy','term'] -- content/s1/p1.md -- --- title: p1 --- -- content/s1/foo.txt -- foo.txt -- content/s1/p1.txt -- p1.txt -- content/s1/p1-foo.txt -- p1-foo.txt -- layouts/_default/list.html -- {{.Title }}| -- layouts/_default/single.html -- {{.Title }}| ` b := Test(t, files) b.Build() b.AssertFileExists("public/s1/index.html", true) b.AssertFileExists("public/s1/foo.txt", true) b.AssertFileExists("public/s1/p1.txt", true) // failing test b.AssertFileExists("public/s1/p1-foo.txt", true) // failing test b.AssertFileExists("public/s1/p1/index.html", true) } func TestSitemapOverrideFilename(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- baseURL = 'https://example.org/' disableKinds = ['page','rss','section','taxonomy','term'] defaultContentLanguage = 'de' defaultContentLanguageInSubdir = true [languages.de] [languages.en] [sitemap] filename = 'foo.xml' -- layouts/index.html -- irrelevant ` b := Test(t, files) b.AssertFileExists("public/de/foo.xml", true) b.AssertFileExists("public/en/foo.xml", true) b.AssertFileContent("public/foo.xml", "https://example.org/de/foo.xml", "https://example.org/en/foo.xml", ) files = strings.ReplaceAll(files, "filename = 'foo.xml'", "") b = Test(t, files) b.AssertFileExists("public/de/sitemap.xml", true) b.AssertFileExists("public/en/sitemap.xml", true) b.AssertFileContent("public/sitemap.xml", "https://example.org/de/sitemap.xml", "https://example.org/en/sitemap.xml", ) } func TestContentTreeReverseIndex(t *testing.T) { t.Parallel() c := qt.New(t) pageReverseIndex := newContentTreeTreverseIndex( func(get func(key any) (contentNodeI, bool), set func(key any, val contentNodeI)) { for i := 0; i < 10; i++ { key := fmt.Sprint(i) set(key, &testContentNode{key: key}) } }, ) for i := 0; i < 10; i++ { key := fmt.Sprint(i) v := pageReverseIndex.Get(key) c.Assert(v, qt.Not(qt.IsNil)) c.Assert(v.Path(), qt.Equals, key) } } // Issue 13019. func TestContentTreeReverseIndexPara(t *testing.T) { t.Parallel() var wg sync.WaitGroup for i := 0; i < 10; i++ { pageReverseIndex := newContentTreeTreverseIndex( func(get func(key any) (contentNodeI, bool), set func(key any, val contentNodeI)) { for i := 0; i < 10; i++ { key := fmt.Sprint(i) set(key, &testContentNode{key: key}) } }, ) for j := 0; j < 10; j++ { wg.Add(1) go func(i int) { defer wg.Done() pageReverseIndex.Get(fmt.Sprint(i)) }(j) } } } type testContentNode struct { key string } func (n *testContentNode) GetIdentity() identity.Identity { return identity.StringIdentity(n.key) } func (n *testContentNode) ForEeachIdentity(cb func(id identity.Identity) bool) bool { panic("not supported") } func (n *testContentNode) Path() string { return n.key } func (n *testContentNode) isContentNodeBranch() bool { return false } func (n *testContentNode) resetBuildState() { } func (n *testContentNode) MarkStale() { }