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