1// Copyright 2019 The Hugo Authors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package hugolib
15
16import (
17	"fmt"
18	"path/filepath"
19	"strings"
20	"testing"
21
22	qt "github.com/frankban/quicktest"
23	"github.com/gohugoio/hugo/deps"
24	"github.com/gohugoio/hugo/resources/page"
25)
26
27func TestNestedSections(t *testing.T) {
28	var (
29		c       = qt.New(t)
30		cfg, fs = newTestCfg()
31		th      = newTestHelper(cfg, fs, t)
32	)
33
34	cfg.Set("permalinks", map[string]string{
35		"perm a": ":sections/:title",
36	})
37
38	pageTemplate := `---
39title: T%d_%d
40---
41Content
42`
43
44	// Home page
45	writeSource(t, fs, filepath.Join("content", "_index.md"), fmt.Sprintf(pageTemplate, -1, -1))
46
47	// Top level content page
48	writeSource(t, fs, filepath.Join("content", "mypage.md"), fmt.Sprintf(pageTemplate, 1234, 5))
49
50	// Top level section without index content page
51	writeSource(t, fs, filepath.Join("content", "top", "mypage2.md"), fmt.Sprintf(pageTemplate, 12345, 6))
52	// Just a page in a subfolder, i.e. not a section.
53	writeSource(t, fs, filepath.Join("content", "top", "folder", "mypage3.md"), fmt.Sprintf(pageTemplate, 12345, 67))
54
55	for level1 := 1; level1 < 3; level1++ {
56		writeSource(t, fs, filepath.Join("content", "l1", fmt.Sprintf("page_1_%d.md", level1)),
57			fmt.Sprintf(pageTemplate, 1, level1))
58	}
59
60	// Issue #3586
61	writeSource(t, fs, filepath.Join("content", "post", "0000.md"), fmt.Sprintf(pageTemplate, 1, 2))
62	writeSource(t, fs, filepath.Join("content", "post", "0000", "0001.md"), fmt.Sprintf(pageTemplate, 1, 3))
63	writeSource(t, fs, filepath.Join("content", "elsewhere", "0003.md"), fmt.Sprintf(pageTemplate, 1, 4))
64
65	// Empty nested section, i.e. no regular content pages.
66	writeSource(t, fs, filepath.Join("content", "empty1", "b", "c", "_index.md"), fmt.Sprintf(pageTemplate, 33, -1))
67	// Index content file a the end and in the middle.
68	writeSource(t, fs, filepath.Join("content", "empty2", "b", "_index.md"), fmt.Sprintf(pageTemplate, 40, -1))
69	writeSource(t, fs, filepath.Join("content", "empty2", "b", "c", "d", "_index.md"), fmt.Sprintf(pageTemplate, 41, -1))
70
71	// Empty with content file in the middle.
72	writeSource(t, fs, filepath.Join("content", "empty3", "b", "c", "d", "_index.md"), fmt.Sprintf(pageTemplate, 41, -1))
73	writeSource(t, fs, filepath.Join("content", "empty3", "b", "empty3.md"), fmt.Sprintf(pageTemplate, 3, -1))
74
75	// Section with permalink config
76	writeSource(t, fs, filepath.Join("content", "perm a", "link", "_index.md"), fmt.Sprintf(pageTemplate, 9, -1))
77	for i := 1; i < 4; i++ {
78		writeSource(t, fs, filepath.Join("content", "perm a", "link", fmt.Sprintf("page_%d.md", i)),
79			fmt.Sprintf(pageTemplate, 1, i))
80	}
81	writeSource(t, fs, filepath.Join("content", "perm a", "link", "regular", fmt.Sprintf("page_%d.md", 5)),
82		fmt.Sprintf(pageTemplate, 1, 5))
83
84	writeSource(t, fs, filepath.Join("content", "l1", "l2", "_index.md"), fmt.Sprintf(pageTemplate, 2, -1))
85	writeSource(t, fs, filepath.Join("content", "l1", "l2_2", "_index.md"), fmt.Sprintf(pageTemplate, 22, -1))
86	writeSource(t, fs, filepath.Join("content", "l1", "l2", "l3", "_index.md"), fmt.Sprintf(pageTemplate, 3, -1))
87
88	for level2 := 1; level2 < 4; level2++ {
89		writeSource(t, fs, filepath.Join("content", "l1", "l2", fmt.Sprintf("page_2_%d.md", level2)),
90			fmt.Sprintf(pageTemplate, 2, level2))
91	}
92	for level2 := 1; level2 < 3; level2++ {
93		writeSource(t, fs, filepath.Join("content", "l1", "l2_2", fmt.Sprintf("page_2_2_%d.md", level2)),
94			fmt.Sprintf(pageTemplate, 2, level2))
95	}
96	for level3 := 1; level3 < 3; level3++ {
97		writeSource(t, fs, filepath.Join("content", "l1", "l2", "l3", fmt.Sprintf("page_3_%d.md", level3)),
98			fmt.Sprintf(pageTemplate, 3, level3))
99	}
100
101	writeSource(t, fs, filepath.Join("content", "Spaces in Section", "page100.md"), fmt.Sprintf(pageTemplate, 10, 0))
102
103	writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "<html>Single|{{ .Title }}</html>")
104	writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"),
105		`
106{{ $sect := (.Site.GetPage "l1/l2") }}
107<html>List|{{ .Title }}|L1/l2-IsActive: {{ .InSection $sect }}
108{{ range .Paginator.Pages }}
109PAG|{{ .Title }}|{{ $sect.InSection . }}
110{{ end }}
111{{/* https://github.com/gohugoio/hugo/issues/4989 */}}
112{{ $sections := (.Site.GetPage "section" .Section).Sections.ByWeight }}
113</html>`)
114
115	cfg.Set("paginate", 2)
116
117	s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{})
118
119	c.Assert(len(s.RegularPages()), qt.Equals, 21)
120
121	tests := []struct {
122		sections string
123		verify   func(c *qt.C, p page.Page)
124	}{
125		{"elsewhere", func(c *qt.C, p page.Page) {
126			c.Assert(len(p.Pages()), qt.Equals, 1)
127			for _, p := range p.Pages() {
128				c.Assert(p.SectionsPath(), qt.Equals, "elsewhere")
129			}
130		}},
131		{"post", func(c *qt.C, p page.Page) {
132			c.Assert(len(p.Pages()), qt.Equals, 2)
133			for _, p := range p.Pages() {
134				c.Assert(p.Section(), qt.Equals, "post")
135			}
136		}},
137		{"empty1", func(c *qt.C, p page.Page) {
138			// > b,c
139			c.Assert(getPage(p, "/empty1/b"), qt.IsNil) // No _index.md page.
140			c.Assert(getPage(p, "/empty1/b/c"), qt.Not(qt.IsNil))
141		}},
142		{"empty2", func(c *qt.C, p page.Page) {
143			// > b,c,d where b and d have _index.md files.
144			b := getPage(p, "/empty2/b")
145			c.Assert(b, qt.Not(qt.IsNil))
146			c.Assert(b.Title(), qt.Equals, "T40_-1")
147
148			cp := getPage(p, "/empty2/b/c")
149			c.Assert(cp, qt.IsNil) // No _index.md
150
151			d := getPage(p, "/empty2/b/c/d")
152			c.Assert(d, qt.Not(qt.IsNil))
153			c.Assert(d.Title(), qt.Equals, "T41_-1")
154
155			c.Assert(cp.Eq(d), qt.Equals, false)
156			c.Assert(cp.Eq(cp), qt.Equals, true)
157			c.Assert(cp.Eq("asdf"), qt.Equals, false)
158		}},
159		{"empty3", func(c *qt.C, p page.Page) {
160			// b,c,d with regular page in b
161			b := getPage(p, "/empty3/b")
162			c.Assert(b, qt.IsNil) // No _index.md
163			e3 := getPage(p, "/empty3/b/empty3")
164			c.Assert(e3, qt.Not(qt.IsNil))
165			c.Assert(e3.File().LogicalName(), qt.Equals, "empty3.md")
166		}},
167		{"empty3", func(c *qt.C, p page.Page) {
168			xxx := getPage(p, "/empty3/nil")
169			c.Assert(xxx, qt.IsNil)
170		}},
171		{"top", func(c *qt.C, p page.Page) {
172			c.Assert(p.Title(), qt.Equals, "Tops")
173			c.Assert(len(p.Pages()), qt.Equals, 2)
174			c.Assert(p.Pages()[0].File().LogicalName(), qt.Equals, "mypage2.md")
175			c.Assert(p.Pages()[1].File().LogicalName(), qt.Equals, "mypage3.md")
176			home := p.Parent()
177			c.Assert(home.IsHome(), qt.Equals, true)
178			c.Assert(len(p.Sections()), qt.Equals, 0)
179			c.Assert(home.CurrentSection(), qt.Equals, home)
180			active, err := home.InSection(home)
181			c.Assert(err, qt.IsNil)
182			c.Assert(active, qt.Equals, true)
183			c.Assert(p.FirstSection(), qt.Equals, p)
184		}},
185		{"l1", func(c *qt.C, p page.Page) {
186			c.Assert(p.Title(), qt.Equals, "L1s")
187			c.Assert(len(p.Pages()), qt.Equals, 4) // 2 pages + 2 sections
188			c.Assert(p.Parent().IsHome(), qt.Equals, true)
189			c.Assert(len(p.Sections()), qt.Equals, 2)
190		}},
191		{"l1,l2", func(c *qt.C, p page.Page) {
192			c.Assert(p.Title(), qt.Equals, "T2_-1")
193			c.Assert(len(p.Pages()), qt.Equals, 4) // 3 pages + 1 section
194			c.Assert(p.Pages()[0].Parent(), qt.Equals, p)
195			c.Assert(p.Parent().Title(), qt.Equals, "L1s")
196			c.Assert(p.RelPermalink(), qt.Equals, "/l1/l2/")
197			c.Assert(len(p.Sections()), qt.Equals, 1)
198
199			for _, child := range p.Pages() {
200				if child.IsSection() {
201					c.Assert(child.CurrentSection(), qt.Equals, child)
202					continue
203				}
204
205				c.Assert(child.CurrentSection(), qt.Equals, p)
206				active, err := child.InSection(p)
207				c.Assert(err, qt.IsNil)
208
209				c.Assert(active, qt.Equals, true)
210				active, err = p.InSection(child)
211				c.Assert(err, qt.IsNil)
212				c.Assert(active, qt.Equals, true)
213				active, err = p.InSection(getPage(p, "/"))
214				c.Assert(err, qt.IsNil)
215				c.Assert(active, qt.Equals, false)
216
217				isAncestor, err := p.IsAncestor(child)
218				c.Assert(err, qt.IsNil)
219				c.Assert(isAncestor, qt.Equals, true)
220				isAncestor, err = child.IsAncestor(p)
221				c.Assert(err, qt.IsNil)
222				c.Assert(isAncestor, qt.Equals, false)
223
224				isDescendant, err := p.IsDescendant(child)
225				c.Assert(err, qt.IsNil)
226				c.Assert(isDescendant, qt.Equals, false)
227				isDescendant, err = child.IsDescendant(p)
228				c.Assert(err, qt.IsNil)
229				c.Assert(isDescendant, qt.Equals, true)
230			}
231
232			c.Assert(p.Eq(p.CurrentSection()), qt.Equals, true)
233		}},
234		{"l1,l2_2", func(c *qt.C, p page.Page) {
235			c.Assert(p.Title(), qt.Equals, "T22_-1")
236			c.Assert(len(p.Pages()), qt.Equals, 2)
237			c.Assert(p.Pages()[0].File().Path(), qt.Equals, filepath.FromSlash("l1/l2_2/page_2_2_1.md"))
238			c.Assert(p.Parent().Title(), qt.Equals, "L1s")
239			c.Assert(len(p.Sections()), qt.Equals, 0)
240		}},
241		{"l1,l2,l3", func(c *qt.C, p page.Page) {
242			nilp, _ := p.GetPage("this/does/not/exist")
243
244			c.Assert(p.Title(), qt.Equals, "T3_-1")
245			c.Assert(len(p.Pages()), qt.Equals, 2)
246			c.Assert(p.Parent().Title(), qt.Equals, "T2_-1")
247			c.Assert(len(p.Sections()), qt.Equals, 0)
248
249			l1 := getPage(p, "/l1")
250			isDescendant, err := l1.IsDescendant(p)
251			c.Assert(err, qt.IsNil)
252			c.Assert(isDescendant, qt.Equals, false)
253			isDescendant, err = l1.IsDescendant(nil)
254			c.Assert(err, qt.IsNil)
255			c.Assert(isDescendant, qt.Equals, false)
256			isDescendant, err = nilp.IsDescendant(p)
257			c.Assert(err, qt.IsNil)
258			c.Assert(isDescendant, qt.Equals, false)
259			isDescendant, err = p.IsDescendant(l1)
260			c.Assert(err, qt.IsNil)
261			c.Assert(isDescendant, qt.Equals, true)
262
263			isAncestor, err := l1.IsAncestor(p)
264			c.Assert(err, qt.IsNil)
265			c.Assert(isAncestor, qt.Equals, true)
266			isAncestor, err = p.IsAncestor(l1)
267			c.Assert(err, qt.IsNil)
268			c.Assert(isAncestor, qt.Equals, false)
269			c.Assert(p.FirstSection(), qt.Equals, l1)
270			isAncestor, err = p.IsAncestor(nil)
271			c.Assert(err, qt.IsNil)
272			c.Assert(isAncestor, qt.Equals, false)
273			isAncestor, err = nilp.IsAncestor(l1)
274			c.Assert(err, qt.IsNil)
275			c.Assert(isAncestor, qt.Equals, false)
276		}},
277		{"perm a,link", func(c *qt.C, p page.Page) {
278			c.Assert(p.Title(), qt.Equals, "T9_-1")
279			c.Assert(p.RelPermalink(), qt.Equals, "/perm-a/link/")
280			c.Assert(len(p.Pages()), qt.Equals, 4)
281			first := p.Pages()[0]
282			c.Assert(first.RelPermalink(), qt.Equals, "/perm-a/link/t1_1/")
283			th.assertFileContent("public/perm-a/link/t1_1/index.html", "Single|T1_1")
284
285			last := p.Pages()[3]
286			c.Assert(last.RelPermalink(), qt.Equals, "/perm-a/link/t1_5/")
287		}},
288	}
289
290	home := s.getPage(page.KindHome)
291
292	for _, test := range tests {
293		test := test
294		t.Run(fmt.Sprintf("sections %s", test.sections), func(t *testing.T) {
295			t.Parallel()
296			c := qt.New(t)
297			sections := strings.Split(test.sections, ",")
298			p := s.getPage(page.KindSection, sections...)
299			c.Assert(p, qt.Not(qt.IsNil), qt.Commentf(fmt.Sprint(sections)))
300
301			if p.Pages() != nil {
302				c.Assert(p.Data().(page.Data).Pages(), deepEqualsPages, p.Pages())
303			}
304			c.Assert(p.Parent(), qt.Not(qt.IsNil))
305			test.verify(c, p)
306		})
307	}
308
309	c.Assert(home, qt.Not(qt.IsNil))
310
311	c.Assert(len(home.Sections()), qt.Equals, 9)
312	c.Assert(s.Info.Sections(), deepEqualsPages, home.Sections())
313
314	rootPage := s.getPage(page.KindPage, "mypage.md")
315	c.Assert(rootPage, qt.Not(qt.IsNil))
316	c.Assert(rootPage.Parent().IsHome(), qt.Equals, true)
317	// https://github.com/gohugoio/hugo/issues/6365
318	c.Assert(rootPage.Sections(), qt.HasLen, 0)
319
320	// Add a odd test for this as this looks a little bit off, but I'm not in the mood
321	// to think too hard a out this right now. It works, but people will have to spell
322	// out the directory name as is.
323	// If we later decide to do something about this, we will have to do some normalization in
324	// getPage.
325	// TODO(bep)
326	sectionWithSpace := s.getPage(page.KindSection, "Spaces in Section")
327	c.Assert(sectionWithSpace, qt.Not(qt.IsNil))
328	c.Assert(sectionWithSpace.RelPermalink(), qt.Equals, "/spaces-in-section/")
329
330	th.assertFileContent("public/l1/l2/page/2/index.html", "L1/l2-IsActive: true", "PAG|T2_3|true")
331}
332
333func TestNextInSectionNested(t *testing.T) {
334	t.Parallel()
335
336	pageContent := `---
337title: "The Page"
338weight: %d
339---
340Some content.
341`
342	createPageContent := func(weight int) string {
343		return fmt.Sprintf(pageContent, weight)
344	}
345
346	b := newTestSitesBuilder(t)
347	b.WithSimpleConfigFile()
348	b.WithTemplates("_default/single.html", `
349Prev: {{ with .PrevInSection }}{{ .RelPermalink }}{{ end }}|
350Next: {{ with .NextInSection }}{{ .RelPermalink }}{{ end }}|
351`)
352
353	b.WithContent("blog/page1.md", createPageContent(1))
354	b.WithContent("blog/page2.md", createPageContent(2))
355	b.WithContent("blog/cool/_index.md", createPageContent(1))
356	b.WithContent("blog/cool/cool1.md", createPageContent(1))
357	b.WithContent("blog/cool/cool2.md", createPageContent(2))
358	b.WithContent("root1.md", createPageContent(1))
359	b.WithContent("root2.md", createPageContent(2))
360
361	b.Build(BuildCfg{})
362
363	b.AssertFileContent("public/root1/index.html",
364		"Prev: /root2/|", "Next: |")
365	b.AssertFileContent("public/root2/index.html",
366		"Prev: |", "Next: /root1/|")
367	b.AssertFileContent("public/blog/page1/index.html",
368		"Prev: /blog/page2/|", "Next: |")
369	b.AssertFileContent("public/blog/page2/index.html",
370		"Prev: |", "Next: /blog/page1/|")
371	b.AssertFileContent("public/blog/cool/cool1/index.html",
372		"Prev: /blog/cool/cool2/|", "Next: |")
373	b.AssertFileContent("public/blog/cool/cool2/index.html",
374		"Prev: |", "Next: /blog/cool/cool1/|")
375}
376