cascade_test.go (15597B)
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
14 package hugolib
15
16 import (
17 "bytes"
18 "fmt"
19 "path"
20 "strings"
21 "testing"
22
23 "github.com/gohugoio/hugo/common/maps"
24
25 qt "github.com/frankban/quicktest"
26 "github.com/gohugoio/hugo/parser"
27 "github.com/gohugoio/hugo/parser/metadecoders"
28 )
29
30 func BenchmarkCascade(b *testing.B) {
31 allLangs := []string{"en", "nn", "nb", "sv", "ab", "aa", "af", "sq", "kw", "da"}
32
33 for i := 1; i <= len(allLangs); i += 2 {
34 langs := allLangs[0:i]
35 b.Run(fmt.Sprintf("langs-%d", len(langs)), func(b *testing.B) {
36 c := qt.New(b)
37 b.StopTimer()
38 builders := make([]*sitesBuilder, b.N)
39 for i := 0; i < b.N; i++ {
40 builders[i] = newCascadeTestBuilder(b, langs)
41 }
42 b.StartTimer()
43
44 for i := 0; i < b.N; i++ {
45 builder := builders[i]
46 err := builder.BuildE(BuildCfg{})
47 c.Assert(err, qt.IsNil)
48 first := builder.H.Sites[0]
49 c.Assert(first, qt.Not(qt.IsNil))
50 }
51 })
52 }
53 }
54
55 func BenchmarkCascadeTarget(b *testing.B) {
56 files := `
57 -- content/_index.md --
58 background = 'yosemite.jpg'
59 [cascade._target]
60 kind = '{section,term}'
61 -- content/posts/_index.md --
62 -- content/posts/funny/_index.md --
63 `
64
65 for i := 1; i < 100; i++ {
66 files += fmt.Sprintf("\n-- content/posts/p%d.md --\n", i+1)
67 }
68
69 for i := 1; i < 100; i++ {
70 files += fmt.Sprintf("\n-- content/posts/funny/pf%d.md --\n", i+1)
71 }
72
73 b.Run("Kind", func(b *testing.B) {
74 cfg := IntegrationTestConfig{
75 T: b,
76 TxtarString: files,
77 }
78 builders := make([]*IntegrationTestBuilder, b.N)
79
80 for i := range builders {
81 builders[i] = NewIntegrationTestBuilder(cfg)
82 }
83
84 b.ResetTimer()
85
86 for i := 0; i < b.N; i++ {
87 builders[i].Build()
88 }
89 })
90 }
91
92 func TestCascadeConfig(t *testing.T) {
93 c := qt.New(t)
94
95 // Make sure the cascade from config gets applied even if we're not
96 // having a content file for the home page.
97 for _, withHomeContent := range []bool{true, false} {
98 testName := "Home content file"
99 if !withHomeContent {
100 testName = "No home content file"
101 }
102 c.Run(testName, func(c *qt.C) {
103 b := newTestSitesBuilder(c)
104
105 b.WithConfigFile("toml", `
106 baseURL="https://example.org"
107
108 [cascade]
109 img1 = "img1-config.jpg"
110 imgconfig = "img-config.jpg"
111
112 `)
113
114 if withHomeContent {
115 b.WithContent("_index.md", `
116 ---
117 title: "Home"
118 cascade:
119 img1: "img1-home.jpg"
120 img2: "img2-home.jpg"
121 ---
122 `)
123 }
124
125 b.WithContent("p1.md", ``)
126
127 b.Build(BuildCfg{})
128
129 p1 := b.H.Sites[0].getPage("p1")
130
131 if withHomeContent {
132 b.Assert(p1.Params(), qt.DeepEquals, maps.Params{
133 "imgconfig": "img-config.jpg",
134 "draft": bool(false),
135 "iscjklanguage": bool(false),
136 "img1": "img1-home.jpg",
137 "img2": "img2-home.jpg",
138 })
139 } else {
140 b.Assert(p1.Params(), qt.DeepEquals, maps.Params{
141 "img1": "img1-config.jpg",
142 "imgconfig": "img-config.jpg",
143 "draft": bool(false),
144 "iscjklanguage": bool(false),
145 })
146 }
147 })
148
149 }
150 }
151
152 func TestCascade(t *testing.T) {
153 allLangs := []string{"en", "nn", "nb", "sv"}
154
155 langs := allLangs[:3]
156
157 t.Run(fmt.Sprintf("langs-%d", len(langs)), func(t *testing.T) {
158 b := newCascadeTestBuilder(t, langs)
159 b.Build(BuildCfg{})
160
161 b.AssertFileContent("public/index.html", `
162 12|term|categories/cool/_index.md|Cascade Category|cat.png|categories|HTML-|
163 12|term|categories/catsect1|catsect1|cat.png|categories|HTML-|
164 12|term|categories/funny|funny|cat.png|categories|HTML-|
165 12|taxonomy|categories/_index.md|My Categories|cat.png|categories|HTML-|
166 32|term|categories/sad/_index.md|Cascade Category|sad.png|categories|HTML-|
167 42|term|tags/blue|blue|home.png|tags|HTML-|
168 42|taxonomy|tags|Cascade Home|home.png|tags|HTML-|
169 42|section|sectnocontent|Cascade Home|home.png|sectnocontent|HTML-|
170 42|section|sect3|Cascade Home|home.png|sect3|HTML-|
171 42|page|bundle1/index.md|Cascade Home|home.png|page|HTML-|
172 42|page|p2.md|Cascade Home|home.png|page|HTML-|
173 42|page|sect2/p2.md|Cascade Home|home.png|sect2|HTML-|
174 42|page|sect3/nofrontmatter.md|Cascade Home|home.png|sect3|HTML-|
175 42|page|sect3/p1.md|Cascade Home|home.png|sect3|HTML-|
176 42|page|sectnocontent/p1.md|Cascade Home|home.png|sectnocontent|HTML-|
177 42|section|sectnofrontmatter/_index.md|Cascade Home|home.png|sectnofrontmatter|HTML-|
178 42|term|tags/green|green|home.png|tags|HTML-|
179 42|home|_index.md|Home|home.png|page|HTML-|
180 42|page|p1.md|p1|home.png|page|HTML-|
181 42|section|sect1/_index.md|Sect1|sect1.png|stype|HTML-|
182 42|section|sect1/s1_2/_index.md|Sect1_2|sect1.png|stype|HTML-|
183 42|page|sect1/s1_2/p1.md|Sect1_2_p1|sect1.png|stype|HTML-|
184 42|page|sect1/s1_2/p2.md|Sect1_2_p2|sect1.png|stype|HTML-|
185 42|section|sect2/_index.md|Sect2|home.png|sect2|HTML-|
186 42|page|sect2/p1.md|Sect2_p1|home.png|sect2|HTML-|
187 52|page|sect4/p1.md|Cascade Home|home.png|sect4|RSS-|
188 52|section|sect4/_index.md|Sect4|home.png|sect4|RSS-|
189 `)
190
191 // Check that type set in cascade gets the correct layout.
192 b.AssertFileContent("public/sect1/index.html", `stype list: Sect1`)
193 b.AssertFileContent("public/sect1/s1_2/p2/index.html", `stype single: Sect1_2_p2`)
194
195 // Check output formats set in cascade
196 b.AssertFileContent("public/sect4/index.xml", `<link>https://example.org/sect4/index.xml</link>`)
197 b.AssertFileContent("public/sect4/p1/index.xml", `<link>https://example.org/sect4/p1/index.xml</link>`)
198 b.C.Assert(b.CheckExists("public/sect2/index.xml"), qt.Equals, false)
199
200 // Check cascade into bundled page
201 b.AssertFileContent("public/bundle1/index.html", `Resources: bp1.md|home.png|`)
202 })
203 }
204
205 func TestCascadeEdit(t *testing.T) {
206 p1Content := `---
207 title: P1
208 ---
209 `
210
211 indexContentNoCascade := `
212 ---
213 title: Home
214 ---
215 `
216
217 indexContentCascade := `
218 ---
219 title: Section
220 cascade:
221 banner: post.jpg
222 layout: postlayout
223 type: posttype
224 ---
225 `
226
227 layout := `Banner: {{ .Params.banner }}|Layout: {{ .Layout }}|Type: {{ .Type }}|Content: {{ .Content }}`
228
229 newSite := func(t *testing.T, cascade bool) *sitesBuilder {
230 b := newTestSitesBuilder(t).Running()
231 b.WithTemplates("_default/single.html", layout)
232 b.WithTemplates("_default/list.html", layout)
233 if cascade {
234 b.WithContent("post/_index.md", indexContentCascade)
235 } else {
236 b.WithContent("post/_index.md", indexContentNoCascade)
237 }
238 b.WithContent("post/dir/p1.md", p1Content)
239
240 return b
241 }
242
243 t.Run("Edit descendant", func(t *testing.T) {
244 t.Parallel()
245
246 b := newSite(t, true)
247 b.Build(BuildCfg{})
248
249 assert := func() {
250 b.Helper()
251 b.AssertFileContent("public/post/dir/p1/index.html",
252 `Banner: post.jpg|`,
253 `Layout: postlayout`,
254 `Type: posttype`,
255 )
256 }
257
258 assert()
259
260 b.EditFiles("content/post/dir/p1.md", p1Content+"\ncontent edit")
261 b.Build(BuildCfg{})
262
263 assert()
264 b.AssertFileContent("public/post/dir/p1/index.html",
265 `content edit
266 Banner: post.jpg`,
267 )
268 })
269
270 t.Run("Edit ancestor", func(t *testing.T) {
271 t.Parallel()
272
273 b := newSite(t, true)
274 b.Build(BuildCfg{})
275
276 b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg|Layout: postlayout|Type: posttype|Content:`)
277
278 b.EditFiles("content/post/_index.md", strings.Replace(indexContentCascade, "post.jpg", "edit.jpg", 1))
279
280 b.Build(BuildCfg{})
281
282 b.AssertFileContent("public/post/index.html", `Banner: edit.jpg|Layout: postlayout|Type: posttype|`)
283 b.AssertFileContent("public/post/dir/p1/index.html", `Banner: edit.jpg|Layout: postlayout|Type: posttype|`)
284 })
285
286 t.Run("Edit ancestor, add cascade", func(t *testing.T) {
287 t.Parallel()
288
289 b := newSite(t, true)
290 b.Build(BuildCfg{})
291
292 b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg`)
293
294 b.EditFiles("content/post/_index.md", indexContentCascade)
295
296 b.Build(BuildCfg{})
297
298 b.AssertFileContent("public/post/index.html", `Banner: post.jpg|Layout: postlayout|Type: posttype|`)
299 b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg|Layout: postlayout|`)
300 })
301
302 t.Run("Edit ancestor, remove cascade", func(t *testing.T) {
303 t.Parallel()
304
305 b := newSite(t, false)
306 b.Build(BuildCfg{})
307
308 b.AssertFileContent("public/post/dir/p1/index.html", `Banner: |Layout: |`)
309
310 b.EditFiles("content/post/_index.md", indexContentNoCascade)
311
312 b.Build(BuildCfg{})
313
314 b.AssertFileContent("public/post/index.html", `Banner: |Layout: |Type: post|`)
315 b.AssertFileContent("public/post/dir/p1/index.html", `Banner: |Layout: |`)
316 })
317
318 t.Run("Edit ancestor, content only", func(t *testing.T) {
319 t.Parallel()
320
321 b := newSite(t, true)
322 b.Build(BuildCfg{})
323
324 b.EditFiles("content/post/_index.md", indexContentCascade+"\ncontent edit")
325
326 counters := &testCounters{}
327 b.Build(BuildCfg{testCounters: counters})
328 // As we only changed the content, not the cascade front matter,
329 // only the home page is re-rendered.
330 b.Assert(int(counters.contentRenderCounter), qt.Equals, 1)
331
332 b.AssertFileContent("public/post/index.html", `Banner: post.jpg|Layout: postlayout|Type: posttype|Content: <p>content edit</p>`)
333 b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg|Layout: postlayout|`)
334 })
335 }
336
337 func newCascadeTestBuilder(t testing.TB, langs []string) *sitesBuilder {
338 p := func(m map[string]any) string {
339 var yamlStr string
340
341 if len(m) > 0 {
342 var b bytes.Buffer
343
344 parser.InterfaceToConfig(m, metadecoders.YAML, &b)
345 yamlStr = b.String()
346 }
347
348 metaStr := "---\n" + yamlStr + "\n---"
349
350 return metaStr
351 }
352
353 createLangConfig := func(lang string) string {
354 const langEntry = `
355 [languages.%s]
356 `
357 return fmt.Sprintf(langEntry, lang)
358 }
359
360 createMount := func(lang string) string {
361 const mountsTempl = `
362 [[module.mounts]]
363 source="content/%s"
364 target="content"
365 lang="%s"
366 `
367 return fmt.Sprintf(mountsTempl, lang, lang)
368 }
369
370 config := `
371 baseURL = "https://example.org"
372 defaultContentLanguage = "en"
373 defaultContentLanguageInSubDir = false
374
375 [languages]`
376 for _, lang := range langs {
377 config += createLangConfig(lang)
378 }
379
380 config += "\n\n[module]\n"
381 for _, lang := range langs {
382 config += createMount(lang)
383 }
384
385 b := newTestSitesBuilder(t).WithConfigFile("toml", config)
386
387 createContentFiles := func(lang string) {
388 withContent := func(filenameContent ...string) {
389 for i := 0; i < len(filenameContent); i += 2 {
390 b.WithContent(path.Join(lang, filenameContent[i]), filenameContent[i+1])
391 }
392 }
393
394 withContent(
395 "_index.md", p(map[string]any{
396 "title": "Home",
397 "cascade": map[string]any{
398 "title": "Cascade Home",
399 "ICoN": "home.png",
400 "outputs": []string{"HTML"},
401 "weight": 42,
402 },
403 }),
404 "p1.md", p(map[string]any{
405 "title": "p1",
406 }),
407 "p2.md", p(map[string]any{}),
408 "sect1/_index.md", p(map[string]any{
409 "title": "Sect1",
410 "type": "stype",
411 "cascade": map[string]any{
412 "title": "Cascade Sect1",
413 "icon": "sect1.png",
414 "type": "stype",
415 "categories": []string{"catsect1"},
416 },
417 }),
418 "sect1/s1_2/_index.md", p(map[string]any{
419 "title": "Sect1_2",
420 }),
421 "sect1/s1_2/p1.md", p(map[string]any{
422 "title": "Sect1_2_p1",
423 }),
424 "sect1/s1_2/p2.md", p(map[string]any{
425 "title": "Sect1_2_p2",
426 }),
427 "sect2/_index.md", p(map[string]any{
428 "title": "Sect2",
429 }),
430 "sect2/p1.md", p(map[string]any{
431 "title": "Sect2_p1",
432 "categories": []string{"cool", "funny", "sad"},
433 "tags": []string{"blue", "green"},
434 }),
435 "sect2/p2.md", p(map[string]any{}),
436 "sect3/p1.md", p(map[string]any{}),
437
438 // No front matter, see #6855
439 "sect3/nofrontmatter.md", `**Hello**`,
440 "sectnocontent/p1.md", `**Hello**`,
441 "sectnofrontmatter/_index.md", `**Hello**`,
442
443 "sect4/_index.md", p(map[string]any{
444 "title": "Sect4",
445 "cascade": map[string]any{
446 "weight": 52,
447 "outputs": []string{"RSS"},
448 },
449 }),
450 "sect4/p1.md", p(map[string]any{}),
451 "p2.md", p(map[string]any{}),
452 "bundle1/index.md", p(map[string]any{}),
453 "bundle1/bp1.md", p(map[string]any{}),
454 "categories/_index.md", p(map[string]any{
455 "title": "My Categories",
456 "cascade": map[string]any{
457 "title": "Cascade Category",
458 "icoN": "cat.png",
459 "weight": 12,
460 },
461 }),
462 "categories/cool/_index.md", p(map[string]any{}),
463 "categories/sad/_index.md", p(map[string]any{
464 "cascade": map[string]any{
465 "icon": "sad.png",
466 "weight": 32,
467 },
468 }),
469 )
470 }
471
472 createContentFiles("en")
473
474 b.WithTemplates("index.html", `
475
476 {{ range .Site.Pages }}
477 {{- .Weight }}|{{ .Kind }}|{{ path.Join .Path }}|{{ .Title }}|{{ .Params.icon }}|{{ .Type }}|{{ range .OutputFormats }}{{ .Name }}-{{ end }}|
478 {{ end }}
479 `,
480
481 "_default/single.html", "default single: {{ .Title }}|{{ .RelPermalink }}|{{ .Content }}|Resources: {{ range .Resources }}{{ .Name }}|{{ .Params.icon }}|{{ .Content }}{{ end }}",
482 "_default/list.html", "default list: {{ .Title }}",
483 "stype/single.html", "stype single: {{ .Title }}|{{ .RelPermalink }}|{{ .Content }}",
484 "stype/list.html", "stype list: {{ .Title }}",
485 )
486
487 return b
488 }
489
490 func TestCascadeTarget(t *testing.T) {
491 t.Parallel()
492
493 c := qt.New(t)
494
495 newBuilder := func(c *qt.C) *sitesBuilder {
496 b := newTestSitesBuilder(c)
497
498 b.WithTemplates("index.html", `
499 {{ $p1 := site.GetPage "s1/p1" }}
500 {{ $s1 := site.GetPage "s1" }}
501
502 P1|p1:{{ $p1.Params.p1 }}|p2:{{ $p1.Params.p2 }}|
503 S1|p1:{{ $s1.Params.p1 }}|p2:{{ $s1.Params.p2 }}|
504 `)
505 b.WithContent("s1/_index.md", "---\ntitle: s1 section\n---")
506 b.WithContent("s1/p1/index.md", "---\ntitle: p1\n---")
507 b.WithContent("s1/p2/index.md", "---\ntitle: p2\n---")
508 b.WithContent("s2/p1/index.md", "---\ntitle: p1_2\n---")
509
510 return b
511 }
512
513 c.Run("slice", func(c *qt.C) {
514 b := newBuilder(c)
515 b.WithContent("_index.md", `+++
516 title = "Home"
517 [[cascade]]
518 p1 = "p1"
519 [[cascade]]
520 p2 = "p2"
521 +++
522 `)
523
524 b.Build(BuildCfg{})
525
526 b.AssertFileContent("public/index.html", "P1|p1:p1|p2:p2")
527 })
528
529 c.Run("slice with _target", func(c *qt.C) {
530 b := newBuilder(c)
531
532 b.WithContent("_index.md", `+++
533 title = "Home"
534 [[cascade]]
535 p1 = "p1"
536 [cascade._target]
537 path="**p1**"
538 [[cascade]]
539 p2 = "p2"
540 [cascade._target]
541 kind="section"
542 +++
543 `)
544
545 b.Build(BuildCfg{})
546
547 b.AssertFileContent("public/index.html", `
548 P1|p1:p1|p2:|
549 S1|p1:|p2:p2|
550 `)
551 })
552
553 c.Run("slice with environment _target", func(c *qt.C) {
554 b := newBuilder(c)
555
556 b.WithContent("_index.md", `+++
557 title = "Home"
558 [[cascade]]
559 p1 = "p1"
560 [cascade._target]
561 path="**p1**"
562 environment="testing"
563 [[cascade]]
564 p2 = "p2"
565 [cascade._target]
566 kind="section"
567 environment="production"
568 +++
569 `)
570
571 b.Build(BuildCfg{})
572
573 b.AssertFileContent("public/index.html", `
574 P1|p1:|p2:|
575 S1|p1:|p2:p2|
576 `)
577 })
578
579 c.Run("slice with yaml _target", func(c *qt.C) {
580 b := newBuilder(c)
581
582 b.WithContent("_index.md", `---
583 title: "Home"
584 cascade:
585 - p1: p1
586 _target:
587 path: "**p1**"
588 - p2: p2
589 _target:
590 kind: "section"
591 ---
592 `)
593
594 b.Build(BuildCfg{})
595
596 b.AssertFileContent("public/index.html", `
597 P1|p1:p1|p2:|
598 S1|p1:|p2:p2|
599 `)
600 })
601
602 c.Run("slice with json _target", func(c *qt.C) {
603 b := newBuilder(c)
604
605 b.WithContent("_index.md", `{
606 "title": "Home",
607 "cascade": [
608 {
609 "p1": "p1",
610 "_target": {
611 "path": "**p1**"
612 }
613 },{
614 "p2": "p2",
615 "_target": {
616 "kind": "section"
617 }
618 }
619 ]
620 }
621 `)
622
623 b.Build(BuildCfg{})
624
625 b.AssertFileContent("public/index.html", `
626 P1|p1:p1|p2:|
627 S1|p1:|p2:p2|
628 `)
629 })
630 }