shortcode_test.go (31283B)
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 "fmt" 18 "path/filepath" 19 "reflect" 20 "strings" 21 "testing" 22 23 "github.com/gohugoio/hugo/config" 24 25 "github.com/gohugoio/hugo/parser/pageparser" 26 "github.com/gohugoio/hugo/resources/page" 27 28 qt "github.com/frankban/quicktest" 29 ) 30 31 func TestExtractShortcodes(t *testing.T) { 32 b := newTestSitesBuilder(t).WithSimpleConfigFile() 33 34 b.WithTemplates( 35 "default/single.html", `EMPTY`, 36 "_internal/shortcodes/tag.html", `tag`, 37 "_internal/shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`, 38 "_internal/shortcodes/sc1.html", `sc1`, 39 "_internal/shortcodes/sc2.html", `sc2`, 40 "_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`, 41 "_internal/shortcodes/inner2.html", `{{.Inner}}`, 42 "_internal/shortcodes/inner3.html", `{{.Inner}}`, 43 ).WithContent("page.md", `--- 44 title: "Shortcodes Galore!" 45 --- 46 `) 47 48 b.CreateSites().Build(BuildCfg{}) 49 50 s := b.H.Sites[0] 51 52 // Make it more regexp friendly 53 strReplacer := strings.NewReplacer("[", "{", "]", "}") 54 55 str := func(s *shortcode) string { 56 if s == nil { 57 return "<nil>" 58 } 59 60 var version int 61 if s.info != nil { 62 version = s.info.ParseInfo().Config.Version 63 } 64 return strReplacer.Replace(fmt.Sprintf("%s;inline:%t;closing:%t;inner:%v;params:%v;ordinal:%d;markup:%t;version:%d;pos:%d", 65 s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, version, s.pos)) 66 } 67 68 regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) { 69 return func(c *qt.C, shortcode *shortcode, err error) { 70 c.Assert(err, qt.IsNil) 71 c.Assert(str(shortcode), qt.Matches, ".*"+re+".*") 72 } 73 } 74 75 for _, test := range []struct { 76 name string 77 input string 78 check func(c *qt.C, shortcode *shortcode, err error) 79 }{ 80 {"one shortcode, no markup", "{{< tag >}}", regexpCheck("tag.*closing:false.*markup:false")}, 81 {"one shortcode, markup", "{{% tag %}}", regexpCheck("tag.*closing:false.*markup:true;version:2")}, 82 {"one shortcode, markup, legacy", "{{% legacytag %}}", regexpCheck("tag.*closing:false.*markup:true;version:1")}, 83 {"outer shortcode markup", "{{% inner %}}{{< tag >}}{{% /inner %}}", regexpCheck("inner.*closing:true.*markup:true")}, 84 {"inner shortcode markup", "{{< inner >}}{{% tag %}}{{< /inner >}}", regexpCheck("inner.*closing:true.*;markup:false;version:2")}, 85 {"one pos param", "{{% tag param1 %}}", regexpCheck("tag.*params:{param1}")}, 86 {"two pos params", "{{< tag param1 param2>}}", regexpCheck("tag.*params:{param1 param2}")}, 87 {"one named param", `{{% tag param1="value" %}}`, regexpCheck("tag.*params:map{param1:value}")}, 88 {"two named params", `{{< tag param1="value1" param2="value2" >}}`, regexpCheck("tag.*params:map{param\\d:value\\d param\\d:value\\d}")}, 89 {"inner", `{{< inner >}}Inner Content{{< / inner >}}`, regexpCheck("inner;inline:false;closing:true;inner:{Inner Content};")}, 90 // issue #934 91 {"inner self-closing", `{{< inner />}}`, regexpCheck("inner;.*inner:{}")}, 92 { 93 "nested inner", `{{< inner >}}Inner Content->{{% inner2 param1 %}}inner2txt{{% /inner2 %}}Inner close->{{< / inner >}}`, 94 regexpCheck("inner;.*inner:{Inner Content->.*Inner close->}"), 95 }, 96 { 97 "nested, nested inner", `{{< inner >}}inner2->{{% inner2 param1 %}}inner2txt->inner3{{< inner3>}}inner3txt{{</ inner3 >}}{{% /inner2 %}}final close->{{< / inner >}}`, 98 regexpCheck("inner:{inner2-> inner2.*{{inner2txt->inner3.*final close->}"), 99 }, 100 {"closed without content", `{{< inner param1 >}}{{< / inner >}}`, regexpCheck("inner.*inner:{}")}, 101 {"inline", `{{< my.inline >}}Hi{{< /my.inline >}}`, regexpCheck("my.inline;inline:true;closing:true;inner:{Hi};")}, 102 } { 103 104 test := test 105 106 t.Run(test.name, func(t *testing.T) { 107 t.Parallel() 108 c := qt.New(t) 109 110 p, err := pageparser.ParseMain(strings.NewReader(test.input), pageparser.Config{}) 111 c.Assert(err, qt.IsNil) 112 handler := newShortcodeHandler(nil, s) 113 iter := p.Iterator() 114 115 short, err := handler.extractShortcode(0, 0, iter) 116 117 test.check(c, short, err) 118 }) 119 } 120 } 121 122 func TestShortcodeMultipleOutputFormats(t *testing.T) { 123 t.Parallel() 124 125 siteConfig := ` 126 baseURL = "http://example.com/blog" 127 128 paginate = 1 129 130 disableKinds = ["section", "term", "taxonomy", "RSS", "sitemap", "robotsTXT", "404"] 131 132 [outputs] 133 home = [ "HTML", "AMP", "Calendar" ] 134 page = [ "HTML", "AMP", "JSON" ] 135 136 ` 137 138 pageTemplate := `--- 139 title: "%s" 140 --- 141 # Doc 142 143 {{< myShort >}} 144 {{< noExt >}} 145 {{%% onlyHTML %%}} 146 147 {{< myInner >}}{{< myShort >}}{{< /myInner >}} 148 149 ` 150 151 pageTemplateCSVOnly := `--- 152 title: "%s" 153 outputs: ["CSV"] 154 --- 155 # Doc 156 157 CSV: {{< myShort >}} 158 ` 159 160 b := newTestSitesBuilder(t).WithConfigFile("toml", siteConfig) 161 b.WithTemplates( 162 "layouts/_default/single.html", `Single HTML: {{ .Title }}|{{ .Content }}`, 163 "layouts/_default/single.json", `Single JSON: {{ .Title }}|{{ .Content }}`, 164 "layouts/_default/single.csv", `Single CSV: {{ .Title }}|{{ .Content }}`, 165 "layouts/index.html", `Home HTML: {{ .Title }}|{{ .Content }}`, 166 "layouts/index.amp.html", `Home AMP: {{ .Title }}|{{ .Content }}`, 167 "layouts/index.ics", `Home Calendar: {{ .Title }}|{{ .Content }}`, 168 "layouts/shortcodes/myShort.html", `ShortHTML`, 169 "layouts/shortcodes/myShort.amp.html", `ShortAMP`, 170 "layouts/shortcodes/myShort.csv", `ShortCSV`, 171 "layouts/shortcodes/myShort.ics", `ShortCalendar`, 172 "layouts/shortcodes/myShort.json", `ShortJSON`, 173 "layouts/shortcodes/noExt", `ShortNoExt`, 174 "layouts/shortcodes/onlyHTML.html", `ShortOnlyHTML`, 175 "layouts/shortcodes/myInner.html", `myInner:--{{- .Inner -}}--`, 176 ) 177 178 b.WithContent("_index.md", fmt.Sprintf(pageTemplate, "Home"), 179 "sect/mypage.md", fmt.Sprintf(pageTemplate, "Single"), 180 "sect/mycsvpage.md", fmt.Sprintf(pageTemplateCSVOnly, "Single CSV"), 181 ) 182 183 b.Build(BuildCfg{}) 184 h := b.H 185 b.Assert(len(h.Sites), qt.Equals, 1) 186 187 s := h.Sites[0] 188 home := s.getPage(page.KindHome) 189 b.Assert(home, qt.Not(qt.IsNil)) 190 b.Assert(len(home.OutputFormats()), qt.Equals, 3) 191 192 b.AssertFileContent("public/index.html", 193 "Home HTML", 194 "ShortHTML", 195 "ShortNoExt", 196 "ShortOnlyHTML", 197 "myInner:--ShortHTML--", 198 ) 199 200 b.AssertFileContent("public/amp/index.html", 201 "Home AMP", 202 "ShortAMP", 203 "ShortNoExt", 204 "ShortOnlyHTML", 205 "myInner:--ShortAMP--", 206 ) 207 208 b.AssertFileContent("public/index.ics", 209 "Home Calendar", 210 "ShortCalendar", 211 "ShortNoExt", 212 "ShortOnlyHTML", 213 "myInner:--ShortCalendar--", 214 ) 215 216 b.AssertFileContent("public/sect/mypage/index.html", 217 "Single HTML", 218 "ShortHTML", 219 "ShortNoExt", 220 "ShortOnlyHTML", 221 "myInner:--ShortHTML--", 222 ) 223 224 b.AssertFileContent("public/sect/mypage/index.json", 225 "Single JSON", 226 "ShortJSON", 227 "ShortNoExt", 228 "ShortOnlyHTML", 229 "myInner:--ShortJSON--", 230 ) 231 232 b.AssertFileContent("public/amp/sect/mypage/index.html", 233 // No special AMP template 234 "Single HTML", 235 "ShortAMP", 236 "ShortNoExt", 237 "ShortOnlyHTML", 238 "myInner:--ShortAMP--", 239 ) 240 241 b.AssertFileContent("public/sect/mycsvpage/index.csv", 242 "Single CSV", 243 "ShortCSV", 244 ) 245 } 246 247 func BenchmarkReplaceShortcodeTokens(b *testing.B) { 248 type input struct { 249 in []byte 250 replacements map[string]string 251 expect []byte 252 } 253 254 data := []struct { 255 input string 256 replacements map[string]string 257 expect []byte 258 }{ 259 {"Hello HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, []byte("Hello World.")}, 260 {strings.Repeat("A", 100) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("A", 100) + " Hello World.")}, 261 {strings.Repeat("A", 500) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("A", 500) + " Hello World.")}, 262 {strings.Repeat("ABCD ", 500) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("ABCD ", 500) + " Hello World.")}, 263 {strings.Repeat("A ", 3000) + " HAHAHUGOSHORTCODE-1HBHB." + strings.Repeat("BC ", 1000) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("A ", 3000) + " Hello World." + strings.Repeat("BC ", 1000) + " Hello World.")}, 264 } 265 266 in := make([]input, b.N*len(data)) 267 cnt := 0 268 for i := 0; i < b.N; i++ { 269 for _, this := range data { 270 in[cnt] = input{[]byte(this.input), this.replacements, this.expect} 271 cnt++ 272 } 273 } 274 275 b.ResetTimer() 276 cnt = 0 277 for i := 0; i < b.N; i++ { 278 for j := range data { 279 currIn := in[cnt] 280 cnt++ 281 results, err := replaceShortcodeTokens(currIn.in, currIn.replacements) 282 if err != nil { 283 b.Fatalf("[%d] failed: %s", i, err) 284 continue 285 } 286 if len(results) != len(currIn.expect) { 287 b.Fatalf("[%d] replaceShortcodeTokens, got \n%q but expected \n%q", j, results, currIn.expect) 288 } 289 290 } 291 } 292 } 293 294 func BenchmarkShortcodesInSite(b *testing.B) { 295 files := ` 296 -- config.toml -- 297 -- layouts/shortcodes/mark1.md -- 298 {{ .Inner }} 299 -- layouts/shortcodes/mark2.md -- 300 1. Item Mark2 1 301 1. Item Mark2 2 302 1. Item Mark2 2-1 303 1. Item Mark2 3 304 -- layouts/_default/single.html -- 305 {{ .Content }} 306 ` 307 308 content := ` 309 --- 310 title: "Markdown Shortcode" 311 --- 312 313 ## List 314 315 1. List 1 316 {{§ mark1 §}} 317 1. Item Mark1 1 318 1. Item Mark1 2 319 {{§ mark2 §}} 320 {{§ /mark1 §}} 321 322 ` 323 324 for i := 1; i < 100; i++ { 325 files += fmt.Sprintf("\n-- content/posts/p%d.md --\n"+content, i+1) 326 } 327 files = strings.ReplaceAll(files, "§", "%") 328 329 cfg := IntegrationTestConfig{ 330 T: b, 331 TxtarString: files, 332 } 333 builders := make([]*IntegrationTestBuilder, b.N) 334 335 for i := range builders { 336 builders[i] = NewIntegrationTestBuilder(cfg) 337 } 338 339 b.ResetTimer() 340 341 for i := 0; i < b.N; i++ { 342 builders[i].Build() 343 } 344 } 345 346 func TestReplaceShortcodeTokens(t *testing.T) { 347 t.Parallel() 348 for i, this := range []struct { 349 input string 350 prefix string 351 replacements map[string]string 352 expect any 353 }{ 354 {"Hello HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello World."}, 355 {"Hello HAHAHUGOSHORTCODE-1@}@.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, false}, 356 {"HAHAHUGOSHORTCODE2-1HBHB", "PREFIX2", map[string]string{"HAHAHUGOSHORTCODE2-1HBHB": "World"}, "World"}, 357 {"Hello World!", "PREFIX2", map[string]string{}, "Hello World!"}, 358 {"!HAHAHUGOSHORTCODE-1HBHB", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "!World"}, 359 {"HAHAHUGOSHORTCODE-1HBHB!", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "World!"}, 360 {"!HAHAHUGOSHORTCODE-1HBHB!", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "!World!"}, 361 {"_{_PREFIX-1HBHB", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "_{_PREFIX-1HBHB"}, 362 {"Hello HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "To You My Old Friend Who Told Me This Fantastic Story"}, "Hello To You My Old Friend Who Told Me This Fantastic Story."}, 363 {"A HAHAHUGOSHORTCODE-1HBHB asdf HAHAHUGOSHORTCODE-2HBHB.", "A", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "v1", "HAHAHUGOSHORTCODE-2HBHB": "v2"}, "A v1 asdf v2."}, 364 {"Hello HAHAHUGOSHORTCODE2-1HBHB. Go HAHAHUGOSHORTCODE2-2HBHB, Go, Go HAHAHUGOSHORTCODE2-3HBHB Go Go!.", "PREFIX2", map[string]string{"HAHAHUGOSHORTCODE2-1HBHB": "Europe", "HAHAHUGOSHORTCODE2-2HBHB": "Jonny", "HAHAHUGOSHORTCODE2-3HBHB": "Johnny"}, "Hello Europe. Go Jonny, Go, Go Johnny Go Go!."}, 365 {"A HAHAHUGOSHORTCODE-2HBHB HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "A B A."}, 366 {"A HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-2", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A"}, false}, 367 {"A HAHAHUGOSHORTCODE-1HBHB but not the second.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "A A but not the second."}, 368 {"An HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "An A."}, 369 {"An HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-2HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "An A B."}, 370 {"A HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-2HBHB HAHAHUGOSHORTCODE-3HBHB HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-3HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B", "HAHAHUGOSHORTCODE-3HBHB": "C"}, "A A B C A C."}, 371 {"A HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-2HBHB HAHAHUGOSHORTCODE-3HBHB HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-3HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B", "HAHAHUGOSHORTCODE-3HBHB": "C"}, "A A B C A C."}, 372 // Issue #1148 remove p-tags 10 => 373 {"Hello <p>HAHAHUGOSHORTCODE-1HBHB</p>. END.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello World. END."}, 374 {"Hello <p>HAHAHUGOSHORTCODE-1HBHB</p>. <p>HAHAHUGOSHORTCODE-2HBHB</p> END.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World", "HAHAHUGOSHORTCODE-2HBHB": "THE"}, "Hello World. THE END."}, 375 {"Hello <p>HAHAHUGOSHORTCODE-1HBHB. END</p>.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello <p>World. END</p>."}, 376 {"<p>Hello HAHAHUGOSHORTCODE-1HBHB</p>. END.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "<p>Hello World</p>. END."}, 377 {"Hello <p>HAHAHUGOSHORTCODE-1HBHB12", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello <p>World12"}, 378 { 379 "Hello HAHAHUGOSHORTCODE-1HBHB. HAHAHUGOSHORTCODE-1HBHB-HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-1HBHB END", "P", 380 map[string]string{"HAHAHUGOSHORTCODE-1HBHB": strings.Repeat("BC", 100)}, 381 fmt.Sprintf("Hello %s. %s-%s %s %s %s END", 382 strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100)), 383 }, 384 } { 385 386 results, err := replaceShortcodeTokens([]byte(this.input), this.replacements) 387 388 if b, ok := this.expect.(bool); ok && !b { 389 if err == nil { 390 t.Errorf("[%d] replaceShortcodeTokens didn't return an expected error", i) 391 } 392 } else { 393 if err != nil { 394 t.Errorf("[%d] failed: %s", i, err) 395 continue 396 } 397 if !reflect.DeepEqual(results, []byte(this.expect.(string))) { 398 t.Errorf("[%d] replaceShortcodeTokens, got \n%q but expected \n%q", i, results, this.expect) 399 } 400 } 401 402 } 403 } 404 405 func TestShortcodeGetContent(t *testing.T) { 406 t.Parallel() 407 408 contentShortcode := ` 409 {{- $t := .Get 0 -}} 410 {{- $p := .Get 1 -}} 411 {{- $k := .Get 2 -}} 412 {{- $page := $.Page.Site.GetPage "page" $p -}} 413 {{ if $page }} 414 {{- if eq $t "bundle" -}} 415 {{- .Scratch.Set "p" ($page.Resources.GetMatch (printf "%s*" $k)) -}} 416 {{- else -}} 417 {{- $.Scratch.Set "p" $page -}} 418 {{- end -}}P1:{{ .Page.Content }}|P2:{{ $p := ($.Scratch.Get "p") }}{{ $p.Title }}/{{ $p.Content }}| 419 {{- else -}} 420 {{- errorf "Page %s is nil" $p -}} 421 {{- end -}} 422 ` 423 424 var templates []string 425 var content []string 426 427 contentWithShortcodeTemplate := `--- 428 title: doc%s 429 weight: %d 430 --- 431 Logo:{{< c "bundle" "b1" "logo.png" >}}:P1: {{< c "page" "section1/p1" "" >}}:BP1:{{< c "bundle" "b1" "bp1" >}}` 432 433 simpleContentTemplate := `--- 434 title: doc%s 435 weight: %d 436 --- 437 C-%s` 438 439 templates = append(templates, []string{"shortcodes/c.html", contentShortcode}...) 440 templates = append(templates, []string{"_default/single.html", "Single Content: {{ .Content }}"}...) 441 templates = append(templates, []string{"_default/list.html", "List Content: {{ .Content }}"}...) 442 443 content = append(content, []string{"b1/index.md", fmt.Sprintf(contentWithShortcodeTemplate, "b1", 1)}...) 444 content = append(content, []string{"b1/logo.png", "PNG logo"}...) 445 content = append(content, []string{"b1/bp1.md", fmt.Sprintf(simpleContentTemplate, "bp1", 1, "bp1")}...) 446 447 content = append(content, []string{"section1/_index.md", fmt.Sprintf(contentWithShortcodeTemplate, "s1", 2)}...) 448 content = append(content, []string{"section1/p1.md", fmt.Sprintf(simpleContentTemplate, "s1p1", 2, "s1p1")}...) 449 450 content = append(content, []string{"section2/_index.md", fmt.Sprintf(simpleContentTemplate, "b1", 1, "b1")}...) 451 content = append(content, []string{"section2/s2p1.md", fmt.Sprintf(contentWithShortcodeTemplate, "bp1", 1)}...) 452 453 builder := newTestSitesBuilder(t).WithDefaultMultiSiteConfig() 454 455 builder.WithContent(content...).WithTemplates(templates...).CreateSites().Build(BuildCfg{}) 456 s := builder.H.Sites[0] 457 builder.Assert(len(s.RegularPages()), qt.Equals, 3) 458 459 builder.AssertFileContent("public/en/section1/index.html", 460 "List Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|", 461 "BP1:P1:|P2:docbp1/<p>C-bp1</p>", 462 ) 463 464 builder.AssertFileContent("public/en/b1/index.html", 465 "Single Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|", 466 "P2:docbp1/<p>C-bp1</p>", 467 ) 468 469 builder.AssertFileContent("public/en/section2/s2p1/index.html", 470 "Single Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|", 471 "P2:docbp1/<p>C-bp1</p>", 472 ) 473 } 474 475 // https://github.com/gohugoio/hugo/issues/5833 476 func TestShortcodeParentResourcesOnRebuild(t *testing.T) { 477 t.Parallel() 478 479 b := newTestSitesBuilder(t).Running().WithSimpleConfigFile() 480 b.WithTemplatesAdded( 481 "index.html", ` 482 {{ $b := .Site.GetPage "b1" }} 483 b1 Content: {{ $b.Content }} 484 {{$p := $b.Resources.GetMatch "p1*" }} 485 Content: {{ $p.Content }} 486 {{ $article := .Site.GetPage "blog/article" }} 487 Article Content: {{ $article.Content }} 488 `, 489 "shortcodes/c.html", ` 490 {{ range .Page.Parent.Resources }} 491 * Parent resource: {{ .Name }}: {{ .RelPermalink }} 492 {{ end }} 493 `) 494 495 pageContent := ` 496 --- 497 title: MyPage 498 --- 499 500 SHORTCODE: {{< c >}} 501 502 ` 503 504 b.WithContent("b1/index.md", pageContent, 505 "b1/logo.png", "PNG logo", 506 "b1/p1.md", pageContent, 507 "blog/_index.md", pageContent, 508 "blog/logo-article.png", "PNG logo", 509 "blog/article.md", pageContent, 510 ) 511 512 b.Build(BuildCfg{}) 513 514 assert := func(matchers ...string) { 515 allMatchers := append(matchers, "Parent resource: logo.png: /b1/logo.png", 516 "Article Content: <p>SHORTCODE: \n\n* Parent resource: logo-article.png: /blog/logo-article.png", 517 ) 518 519 b.AssertFileContent("public/index.html", 520 allMatchers..., 521 ) 522 } 523 524 assert() 525 526 b.EditFiles("content/b1/index.md", pageContent+" Edit.") 527 528 b.Build(BuildCfg{}) 529 530 assert("Edit.") 531 } 532 533 func TestShortcodePreserveOrder(t *testing.T) { 534 t.Parallel() 535 c := qt.New(t) 536 537 contentTemplate := `--- 538 title: doc%d 539 weight: %d 540 --- 541 # doc 542 543 {{< s1 >}}{{< s2 >}}{{< s3 >}}{{< s4 >}}{{< s5 >}} 544 545 {{< nested >}} 546 {{< ordinal >}} {{< scratch >}} 547 {{< ordinal >}} {{< scratch >}} 548 {{< ordinal >}} {{< scratch >}} 549 {{< /nested >}} 550 551 ` 552 553 ordinalShortcodeTemplate := `ordinal: {{ .Ordinal }}{{ .Page.Scratch.Set "ordinal" .Ordinal }}` 554 555 nestedShortcode := `outer ordinal: {{ .Ordinal }} inner: {{ .Inner }}` 556 scratchGetShortcode := `scratch ordinal: {{ .Ordinal }} scratch get ordinal: {{ .Page.Scratch.Get "ordinal" }}` 557 shortcodeTemplate := `v%d: {{ .Ordinal }} sgo: {{ .Page.Scratch.Get "o2" }}{{ .Page.Scratch.Set "o2" .Ordinal }}|` 558 559 var shortcodes []string 560 var content []string 561 562 shortcodes = append(shortcodes, []string{"shortcodes/nested.html", nestedShortcode}...) 563 shortcodes = append(shortcodes, []string{"shortcodes/ordinal.html", ordinalShortcodeTemplate}...) 564 shortcodes = append(shortcodes, []string{"shortcodes/scratch.html", scratchGetShortcode}...) 565 566 for i := 1; i <= 5; i++ { 567 sc := fmt.Sprintf(shortcodeTemplate, i) 568 sc = strings.Replace(sc, "%%", "%", -1) 569 shortcodes = append(shortcodes, []string{fmt.Sprintf("shortcodes/s%d.html", i), sc}...) 570 } 571 572 for i := 1; i <= 3; i++ { 573 content = append(content, []string{fmt.Sprintf("p%d.md", i), fmt.Sprintf(contentTemplate, i, i)}...) 574 } 575 576 builder := newTestSitesBuilder(t).WithDefaultMultiSiteConfig() 577 578 builder.WithContent(content...).WithTemplatesAdded(shortcodes...).CreateSites().Build(BuildCfg{}) 579 580 s := builder.H.Sites[0] 581 c.Assert(len(s.RegularPages()), qt.Equals, 3) 582 583 builder.AssertFileContent("public/en/p1/index.html", `v1: 0 sgo: |v2: 1 sgo: 0|v3: 2 sgo: 1|v4: 3 sgo: 2|v5: 4 sgo: 3`) 584 builder.AssertFileContent("public/en/p1/index.html", `outer ordinal: 5 inner: 585 ordinal: 0 scratch ordinal: 1 scratch get ordinal: 0 586 ordinal: 2 scratch ordinal: 3 scratch get ordinal: 2 587 ordinal: 4 scratch ordinal: 5 scratch get ordinal: 4`) 588 } 589 590 func TestShortcodeVariables(t *testing.T) { 591 t.Parallel() 592 c := qt.New(t) 593 594 builder := newTestSitesBuilder(t).WithSimpleConfigFile() 595 596 builder.WithContent("page.md", `--- 597 title: "Hugo Rocks!" 598 --- 599 600 # doc 601 602 {{< s1 >}} 603 604 `).WithTemplatesAdded("layouts/shortcodes/s1.html", ` 605 Name: {{ .Name }} 606 {{ with .Position }} 607 File: {{ .Filename }} 608 Offset: {{ .Offset }} 609 Line: {{ .LineNumber }} 610 Column: {{ .ColumnNumber }} 611 String: {{ . | safeHTML }} 612 {{ end }} 613 614 `).CreateSites().Build(BuildCfg{}) 615 616 s := builder.H.Sites[0] 617 c.Assert(len(s.RegularPages()), qt.Equals, 1) 618 619 builder.AssertFileContent("public/page/index.html", 620 filepath.FromSlash("File: content/page.md"), 621 "Line: 7", "Column: 4", "Offset: 40", 622 filepath.FromSlash("String: \"content/page.md:7:4\""), 623 "Name: s1", 624 ) 625 } 626 627 func TestInlineShortcodes(t *testing.T) { 628 for _, enableInlineShortcodes := range []bool{true, false} { 629 enableInlineShortcodes := enableInlineShortcodes 630 t.Run(fmt.Sprintf("enableInlineShortcodes=%t", enableInlineShortcodes), 631 func(t *testing.T) { 632 t.Parallel() 633 conf := fmt.Sprintf(` 634 baseURL = "https://example.com" 635 enableInlineShortcodes = %t 636 `, enableInlineShortcodes) 637 638 b := newTestSitesBuilder(t) 639 b.WithConfigFile("toml", conf) 640 641 shortcodeContent := `FIRST:{{< myshort.inline "first" >}} 642 Page: {{ .Page.Title }} 643 Seq: {{ seq 3 }} 644 Param: {{ .Get 0 }} 645 {{< /myshort.inline >}}:END: 646 647 SECOND:{{< myshort.inline "second" />}}:END 648 NEW INLINE: {{< n1.inline "5" >}}W1: {{ seq (.Get 0) }}{{< /n1.inline >}}:END: 649 INLINE IN INNER: {{< outer >}}{{< n2.inline >}}W2: {{ seq 4 }}{{< /n2.inline >}}{{< /outer >}}:END: 650 REUSED INLINE IN INNER: {{< outer >}}{{< n1.inline "3" />}}{{< /outer >}}:END: 651 ## MARKDOWN DELIMITER: {{% mymarkdown.inline %}}**Hugo Rocks!**{{% /mymarkdown.inline %}} 652 ` 653 654 b.WithContent("page-md-shortcode.md", `--- 655 title: "Hugo" 656 --- 657 `+shortcodeContent) 658 659 b.WithContent("_index.md", `--- 660 title: "Hugo Home" 661 --- 662 663 `+shortcodeContent) 664 665 b.WithTemplatesAdded("layouts/_default/single.html", ` 666 CONTENT:{{ .Content }} 667 TOC: {{ .TableOfContents }} 668 `) 669 670 b.WithTemplatesAdded("layouts/index.html", ` 671 CONTENT:{{ .Content }} 672 TOC: {{ .TableOfContents }} 673 `) 674 675 b.WithTemplatesAdded("layouts/shortcodes/outer.html", `Inner: {{ .Inner }}`) 676 677 b.CreateSites().Build(BuildCfg{}) 678 679 shouldContain := []string{ 680 "Seq: [1 2 3]", 681 "Param: first", 682 "Param: second", 683 "NEW INLINE: W1: [1 2 3 4 5]", 684 "INLINE IN INNER: Inner: W2: [1 2 3 4]", 685 "REUSED INLINE IN INNER: Inner: W1: [1 2 3]", 686 `<li><a href="#markdown-delimiter-hugo-rocks">MARKDOWN DELIMITER: <strong>Hugo Rocks!</strong></a></li>`, 687 } 688 689 if enableInlineShortcodes { 690 b.AssertFileContent("public/page-md-shortcode/index.html", 691 shouldContain..., 692 ) 693 b.AssertFileContent("public/index.html", 694 shouldContain..., 695 ) 696 } else { 697 b.AssertFileContent("public/page-md-shortcode/index.html", 698 "FIRST::END", 699 "SECOND::END", 700 "NEW INLINE: :END", 701 "INLINE IN INNER: Inner: :END:", 702 "REUSED INLINE IN INNER: Inner: :END:", 703 ) 704 } 705 }) 706 707 } 708 } 709 710 // https://github.com/gohugoio/hugo/issues/5863 711 func TestShortcodeNamespaced(t *testing.T) { 712 t.Parallel() 713 c := qt.New(t) 714 715 builder := newTestSitesBuilder(t).WithSimpleConfigFile() 716 717 builder.WithContent("page.md", `--- 718 title: "Hugo Rocks!" 719 --- 720 721 # doc 722 723 hello: {{< hello >}} 724 test/hello: {{< test/hello >}} 725 726 `).WithTemplatesAdded( 727 "layouts/shortcodes/hello.html", `hello`, 728 "layouts/shortcodes/test/hello.html", `test/hello`).CreateSites().Build(BuildCfg{}) 729 730 s := builder.H.Sites[0] 731 c.Assert(len(s.RegularPages()), qt.Equals, 1) 732 733 builder.AssertFileContent("public/page/index.html", 734 "hello: hello", 735 "test/hello: test/hello", 736 ) 737 } 738 739 // https://github.com/gohugoio/hugo/issues/6504 740 func TestShortcodeEmoji(t *testing.T) { 741 t.Parallel() 742 743 v := config.NewWithTestDefaults() 744 v.Set("enableEmoji", true) 745 746 builder := newTestSitesBuilder(t).WithViper(v) 747 748 builder.WithContent("page.md", `--- 749 title: "Hugo Rocks!" 750 --- 751 752 # doc 753 754 {{< event >}}10:30-11:00 My :smile: Event {{< /event >}} 755 756 757 `).WithTemplatesAdded( 758 "layouts/shortcodes/event.html", `<div>{{ "\u29BE" }} {{ .Inner }} </div>`) 759 760 builder.Build(BuildCfg{}) 761 builder.AssertFileContent("public/page/index.html", 762 "⦾ 10:30-11:00 My 😄 Event", 763 ) 764 } 765 766 func TestShortcodeTypedParams(t *testing.T) { 767 t.Parallel() 768 c := qt.New(t) 769 770 builder := newTestSitesBuilder(t).WithSimpleConfigFile() 771 772 builder.WithContent("page.md", `--- 773 title: "Hugo Rocks!" 774 --- 775 776 # doc 777 778 types positional: {{< hello true false 33 3.14 >}} 779 types named: {{< hello b1=true b2=false i1=33 f1=3.14 >}} 780 types string: {{< hello "true" trues "33" "3.14" >}} 781 782 783 `).WithTemplatesAdded( 784 "layouts/shortcodes/hello.html", 785 `{{ range $i, $v := .Params }} 786 - {{ printf "%v: %v (%T)" $i $v $v }} 787 {{ end }} 788 {{ $b1 := .Get "b1" }} 789 Get: {{ printf "%v (%T)" $b1 $b1 | safeHTML }} 790 `).Build(BuildCfg{}) 791 792 s := builder.H.Sites[0] 793 c.Assert(len(s.RegularPages()), qt.Equals, 1) 794 795 builder.AssertFileContent("public/page/index.html", 796 "types positional: - 0: true (bool) - 1: false (bool) - 2: 33 (int) - 3: 3.14 (float64)", 797 "types named: - b1: true (bool) - b2: false (bool) - f1: 3.14 (float64) - i1: 33 (int) Get: true (bool) ", 798 "types string: - 0: true (string) - 1: trues (string) - 2: 33 (string) - 3: 3.14 (string) ", 799 ) 800 } 801 802 func TestShortcodeRef(t *testing.T) { 803 t.Parallel() 804 805 v := config.NewWithTestDefaults() 806 v.Set("baseURL", "https://example.org") 807 808 builder := newTestSitesBuilder(t).WithViper(v) 809 810 for i := 1; i <= 2; i++ { 811 builder.WithContent(fmt.Sprintf("page%d.md", i), `--- 812 title: "Hugo Rocks!" 813 --- 814 815 816 817 [Page 1]({{< ref "page1.md" >}}) 818 [Page 1 with anchor]({{< relref "page1.md#doc" >}}) 819 [Page 2]({{< ref "page2.md" >}}) 820 [Page 2 with anchor]({{< relref "page2.md#doc" >}}) 821 822 823 ## Doc 824 825 826 `) 827 } 828 829 builder.Build(BuildCfg{}) 830 831 builder.AssertFileContent("public/page2/index.html", ` 832 <a href="/page1/#doc">Page 1 with anchor</a> 833 <a href="https://example.org/page2/">Page 2</a> 834 <a href="/page2/#doc">Page 2 with anchor</a></p> 835 836 <h2 id="doc">Doc</h2> 837 `, 838 ) 839 840 } 841 842 // https://github.com/gohugoio/hugo/issues/6857 843 func TestShortcodeNoInner(t *testing.T) { 844 t.Parallel() 845 846 b := newTestSitesBuilder(t) 847 848 b.WithContent("mypage.md", `--- 849 title: "No Inner!" 850 --- 851 {{< noinner >}}{{< /noinner >}} 852 853 854 `).WithTemplatesAdded( 855 "layouts/shortcodes/noinner.html", `No inner here.`) 856 857 err := b.BuildE(BuildCfg{}) 858 b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`"content/mypage.md:4:21": failed to extract shortcode: shortcode "noinner" has no .Inner, yet a closing tag was provided`)) 859 } 860 861 func TestShortcodeStableOutputFormatTemplates(t *testing.T) { 862 t.Parallel() 863 864 for i := 0; i < 5; i++ { 865 866 b := newTestSitesBuilder(t) 867 868 const numPages = 10 869 870 for i := 0; i < numPages; i++ { 871 b.WithContent(fmt.Sprintf("page%d.md", i), `--- 872 title: "Page" 873 outputs: ["html", "css", "csv", "json"] 874 --- 875 {{< myshort >}} 876 877 `) 878 } 879 880 b.WithTemplates( 881 "_default/single.html", "{{ .Content }}", 882 "_default/single.css", "{{ .Content }}", 883 "_default/single.csv", "{{ .Content }}", 884 "_default/single.json", "{{ .Content }}", 885 "shortcodes/myshort.html", `Short-HTML`, 886 "shortcodes/myshort.csv", `Short-CSV`, 887 ) 888 889 b.Build(BuildCfg{}) 890 891 // helpers.PrintFs(b.Fs.Destination, "public", os.Stdout) 892 893 for i := 0; i < numPages; i++ { 894 b.AssertFileContent(fmt.Sprintf("public/page%d/index.html", i), "Short-HTML") 895 b.AssertFileContent(fmt.Sprintf("public/page%d/index.csv", i), "Short-CSV") 896 b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-HTML") 897 898 } 899 900 for i := 0; i < numPages; i++ { 901 b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-HTML") 902 } 903 904 } 905 } 906 907 // #9821 908 func TestShortcodeMarkdownOutputFormat(t *testing.T) { 909 t.Parallel() 910 911 files := ` 912 -- config.toml -- 913 -- content/p1.md -- 914 --- 915 title: "p1" 916 --- 917 {{< foo >}} 918 -- layouts/shortcodes/foo.md -- 919 §§§ 920 <x 921 §§§ 922 -- layouts/_default/single.html -- 923 {{ .Content }} 924 ` 925 926 b := NewIntegrationTestBuilder( 927 IntegrationTestConfig{ 928 T: t, 929 TxtarString: files, 930 Running: true, 931 }, 932 ).Build() 933 934 b.AssertFileContent("public/p1/index.html", ` 935 <x 936 `) 937 938 } 939 940 func TestShortcodePreserveIndentation(t *testing.T) { 941 t.Parallel() 942 943 files := ` 944 -- config.toml -- 945 -- content/p1.md -- 946 --- 947 title: "p1" 948 --- 949 950 ## List With Indented Shortcodes 951 952 1. List 1 953 {{% mark1 %}} 954 1. Item Mark1 1 955 1. Item Mark1 2 956 {{% mark2 %}} 957 {{% /mark1 %}} 958 -- layouts/shortcodes/mark1.md -- 959 {{ .Inner }} 960 -- layouts/shortcodes/mark2.md -- 961 1. Item Mark2 1 962 1. Item Mark2 2 963 1. Item Mark2 2-1 964 1. Item Mark2 3 965 -- layouts/_default/single.html -- 966 {{ .Content }} 967 ` 968 969 b := NewIntegrationTestBuilder( 970 IntegrationTestConfig{ 971 T: t, 972 TxtarString: files, 973 Running: true, 974 }, 975 ).Build() 976 977 b.AssertFileContent("public/p1/index.html", "<ol>\n<li>\n<p>List 1</p>\n<ol>\n<li>Item Mark1 1</li>\n<li>Item Mark1 2</li>\n<li>Item Mark2 1</li>\n<li>Item Mark2 2\n<ol>\n<li>Item Mark2 2-1</li>\n</ol>\n</li>\n<li>Item Mark2 3</li>\n</ol>\n</li>\n</ol>") 978 979 } 980 981 func TestShortcodeCodeblockIndent(t *testing.T) { 982 t.Parallel() 983 984 files := ` 985 -- config.toml -- 986 -- content/p1.md -- 987 --- 988 title: "p1" 989 --- 990 991 ## Code block 992 993 {{% code %}} 994 995 -- layouts/shortcodes/code.md -- 996 echo "foo"; 997 -- layouts/_default/single.html -- 998 {{ .Content }} 999 ` 1000 1001 b := NewIntegrationTestBuilder( 1002 IntegrationTestConfig{ 1003 T: t, 1004 TxtarString: files, 1005 Running: true, 1006 }, 1007 ).Build() 1008 1009 b.AssertFileContent("public/p1/index.html", "<pre><code>echo "foo";\n</code></pre>") 1010 1011 } 1012 1013 func TestShortcodeHighlightDeindent(t *testing.T) { 1014 t.Parallel() 1015 1016 files := ` 1017 -- config.toml -- 1018 [markup] 1019 [markup.highlight] 1020 codeFences = true 1021 noClasses = false 1022 -- content/p1.md -- 1023 --- 1024 title: "p1" 1025 --- 1026 1027 ## Indent 5 Spaces 1028 1029 {{< highlight bash >}} 1030 line 1; 1031 line 2; 1032 line 3; 1033 {{< /highlight >}} 1034 1035 -- layouts/_default/single.html -- 1036 {{ .Content }} 1037 ` 1038 1039 b := NewIntegrationTestBuilder( 1040 IntegrationTestConfig{ 1041 T: t, 1042 TxtarString: files, 1043 Running: true, 1044 }, 1045 ).Build() 1046 1047 b.AssertFileContent("public/p1/index.html", ` 1048 <pre><code> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">line 1<span class="p">;</span> 1049 </span></span><span class="line"><span class="cl">line 2<span class="p">;</span> 1050 </span></span><span class="line"><span class="cl">line 3<span class="p">;</span></span></span></code></pre></div> 1051 </code></pre> 1052 1053 `) 1054 1055 }