github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/hugolib/shortcode_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 "fmt" 18 "path/filepath" 19 "reflect" 20 "strings" 21 "testing" 22 23 "github.com/gohugoio/hugo/config" 24 "github.com/gohugoio/hugo/markup/asciidocext" 25 "github.com/gohugoio/hugo/markup/rst" 26 27 "github.com/gohugoio/hugo/parser/pageparser" 28 "github.com/gohugoio/hugo/resources/page" 29 30 "github.com/gohugoio/hugo/deps" 31 "github.com/gohugoio/hugo/tpl" 32 "github.com/spf13/cast" 33 34 qt "github.com/frankban/quicktest" 35 ) 36 37 func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateManager) error) { 38 t.Helper() 39 CheckShortCodeMatchAndError(t, input, expected, withTemplate, false) 40 } 41 42 func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateManager) error, expectError bool) { 43 t.Helper() 44 cfg, fs := newTestCfg() 45 46 cfg.Set("markup", map[string]interface{}{ 47 "defaultMarkdownHandler": "blackfriday", // TODO(bep) 48 }) 49 50 c := qt.New(t) 51 52 // Need some front matter, see https://github.com/gohugoio/hugo/issues/2337 53 contentFile := `--- 54 title: "Title" 55 --- 56 ` + input 57 58 writeSource(t, fs, "content/simple.md", contentFile) 59 60 b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}).WithNothingAdded() 61 err := b.BuildE(BuildCfg{}) 62 63 if err != nil && !expectError { 64 t.Fatalf("Shortcode rendered error %s.", err) 65 } 66 67 if expectError { 68 c.Assert(err, qt.ErrorMatches, expected) 69 return 70 } 71 72 h := b.H 73 c.Assert(len(h.Sites), qt.Equals, 1) 74 75 c.Assert(len(h.Sites[0].RegularPages()), qt.Equals, 1) 76 77 output := strings.TrimSpace(content(h.Sites[0].RegularPages()[0])) 78 output = strings.TrimPrefix(output, "<p>") 79 output = strings.TrimSuffix(output, "</p>") 80 81 expected = strings.TrimSpace(expected) 82 83 if output != expected { 84 t.Fatalf("Shortcode render didn't match. got \n%q but expected \n%q", output, expected) 85 } 86 } 87 88 func TestNonSC(t *testing.T) { 89 t.Parallel() 90 // notice the syntax diff from 0.12, now comment delims must be added 91 CheckShortCodeMatch(t, "{{%/* movie 47238zzb */%}}", "{{% movie 47238zzb %}}", nil) 92 } 93 94 // Issue #929 95 func TestHyphenatedSC(t *testing.T) { 96 t.Parallel() 97 wt := func(tem tpl.TemplateManager) error { 98 tem.AddTemplate("_internal/shortcodes/hyphenated-video.html", `Playing Video {{ .Get 0 }}`) 99 return nil 100 } 101 102 CheckShortCodeMatch(t, "{{< hyphenated-video 47238zzb >}}", "Playing Video 47238zzb", wt) 103 } 104 105 // Issue #1753 106 func TestNoTrailingNewline(t *testing.T) { 107 t.Parallel() 108 wt := func(tem tpl.TemplateManager) error { 109 tem.AddTemplate("_internal/shortcodes/a.html", `{{ .Get 0 }}`) 110 return nil 111 } 112 113 CheckShortCodeMatch(t, "ab{{< a c >}}d", "abcd", wt) 114 } 115 116 func TestPositionalParamSC(t *testing.T) { 117 t.Parallel() 118 wt := func(tem tpl.TemplateManager) error { 119 tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 0 }}`) 120 return nil 121 } 122 123 CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video 47238zzb", wt) 124 CheckShortCodeMatch(t, "{{< video 47238zzb 132 >}}", "Playing Video 47238zzb", wt) 125 CheckShortCodeMatch(t, "{{<video 47238zzb>}}", "Playing Video 47238zzb", wt) 126 CheckShortCodeMatch(t, "{{<video 47238zzb >}}", "Playing Video 47238zzb", wt) 127 CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video 47238zzb", wt) 128 } 129 130 func TestPositionalParamIndexOutOfBounds(t *testing.T) { 131 t.Parallel() 132 wt := func(tem tpl.TemplateManager) error { 133 tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ with .Get 1 }}{{ . }}{{ else }}Missing{{ end }}`) 134 return nil 135 } 136 CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video Missing", wt) 137 } 138 139 // #5071 140 func TestShortcodeRelated(t *testing.T) { 141 t.Parallel() 142 wt := func(tem tpl.TemplateManager) error { 143 tem.AddTemplate("_internal/shortcodes/a.html", `{{ len (.Site.RegularPages.Related .Page) }}`) 144 return nil 145 } 146 147 CheckShortCodeMatch(t, "{{< a >}}", "0", wt) 148 } 149 150 func TestShortcodeInnerMarkup(t *testing.T) { 151 t.Parallel() 152 wt := func(tem tpl.TemplateManager) error { 153 tem.AddTemplate("shortcodes/a.html", `<div>{{ .Inner }}</div>`) 154 tem.AddTemplate("shortcodes/b.html", `**Bold**: <div>{{ .Inner }}</div>`) 155 return nil 156 } 157 158 CheckShortCodeMatch(t, 159 "{{< a >}}B: <div>{{% b %}}**Bold**{{% /b %}}</div>{{< /a >}}", 160 // This assertion looks odd, but is correct: for inner shortcodes with 161 // the {{% we treats the .Inner content as markup, but not the shortcode 162 // itself. 163 "<div>B: <div>**Bold**: <div><strong>Bold</strong></div></div></div>", 164 wt) 165 166 CheckShortCodeMatch(t, 167 "{{% b %}}This is **B**: {{< b >}}This is B{{< /b>}}{{% /b %}}", 168 "<strong>Bold</strong>: <div>This is <strong>B</strong>: <strong>Bold</strong>: <div>This is B</div></div>", 169 wt) 170 } 171 172 // some repro issues for panics in Go Fuzz testing 173 174 func TestNamedParamSC(t *testing.T) { 175 t.Parallel() 176 wt := func(tem tpl.TemplateManager) error { 177 tem.AddTemplate("_internal/shortcodes/img.html", `<img{{ with .Get "src" }} src="{{.}}"{{end}}{{with .Get "class"}} class="{{.}}"{{end}}>`) 178 return nil 179 } 180 CheckShortCodeMatch(t, `{{< img src="one" >}}`, `<img src="one">`, wt) 181 CheckShortCodeMatch(t, `{{< img class="aspen" >}}`, `<img class="aspen">`, wt) 182 CheckShortCodeMatch(t, `{{< img src= "one" >}}`, `<img src="one">`, wt) 183 CheckShortCodeMatch(t, `{{< img src ="one" >}}`, `<img src="one">`, wt) 184 CheckShortCodeMatch(t, `{{< img src = "one" >}}`, `<img src="one">`, wt) 185 CheckShortCodeMatch(t, `{{< img src = "one" class = "aspen grove" >}}`, `<img src="one" class="aspen grove">`, wt) 186 } 187 188 // Issue #2294 189 func TestNestedNamedMissingParam(t *testing.T) { 190 t.Parallel() 191 wt := func(tem tpl.TemplateManager) error { 192 tem.AddTemplate("_internal/shortcodes/acc.html", `<div class="acc">{{ .Inner }}</div>`) 193 tem.AddTemplate("_internal/shortcodes/div.html", `<div {{with .Get "class"}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`) 194 tem.AddTemplate("_internal/shortcodes/div2.html", `<div {{with .Get 0}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`) 195 return nil 196 } 197 CheckShortCodeMatch(t, 198 `{{% acc %}}{{% div %}}d1{{% /div %}}{{% div2 %}}d2{{% /div2 %}}{{% /acc %}}`, 199 "<div class=\"acc\"><div >d1</div><div >d2</div></div>", wt) 200 } 201 202 func TestIsNamedParamsSC(t *testing.T) { 203 t.Parallel() 204 wt := func(tem tpl.TemplateManager) error { 205 tem.AddTemplate("_internal/shortcodes/bynameorposition.html", `{{ with .Get "id" }}Named: {{ . }}{{ else }}Pos: {{ .Get 0 }}{{ end }}`) 206 tem.AddTemplate("_internal/shortcodes/ifnamedparams.html", `<div id="{{ if .IsNamedParams }}{{ .Get "id" }}{{ else }}{{ .Get 0 }}{{end}}">`) 207 return nil 208 } 209 CheckShortCodeMatch(t, `{{< ifnamedparams id="name" >}}`, `<div id="name">`, wt) 210 CheckShortCodeMatch(t, `{{< ifnamedparams position >}}`, `<div id="position">`, wt) 211 CheckShortCodeMatch(t, `{{< bynameorposition id="name" >}}`, `Named: name`, wt) 212 CheckShortCodeMatch(t, `{{< bynameorposition position >}}`, `Pos: position`, wt) 213 } 214 215 func TestInnerSC(t *testing.T) { 216 t.Parallel() 217 wt := func(tem tpl.TemplateManager) error { 218 tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`) 219 return nil 220 } 221 CheckShortCodeMatch(t, `{{< inside class="aspen" >}}`, `<div class="aspen"></div>`, wt) 222 CheckShortCodeMatch(t, `{{< inside class="aspen" >}}More Here{{< /inside >}}`, "<div class=\"aspen\">More Here</div>", wt) 223 CheckShortCodeMatch(t, `{{< inside >}}More Here{{< /inside >}}`, "<div>More Here</div>", wt) 224 } 225 226 func TestInnerSCWithMarkdown(t *testing.T) { 227 t.Parallel() 228 wt := func(tem tpl.TemplateManager) error { 229 // Note: In Hugo 0.55 we made it so any outer {{%'s inner content was rendered as part of the surrounding 230 // markup. This solved lots of problems, but it also meant that this test had to be adjusted. 231 tem.AddTemplate("_internal/shortcodes/wrapper.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`) 232 tem.AddTemplate("_internal/shortcodes/inside.html", `{{ .Inner }}`) 233 return nil 234 } 235 CheckShortCodeMatch(t, `{{< wrapper >}}{{% inside %}} 236 # More Here 237 238 [link](http://spf13.com) and text 239 240 {{% /inside %}}{{< /wrapper >}}`, "<div><h1 id=\"more-here\">More Here</h1>\n\n<p><a href=\"http://spf13.com\">link</a> and text</p>\n</div>", wt) 241 } 242 243 func TestEmbeddedSC(t *testing.T) { 244 t.Parallel() 245 CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" %}}`, "<figure class=\"bananas orange\"><img src=\"/found/here\"/>\n</figure>", nil) 246 CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" caption="This is a caption" %}}`, "<figure class=\"bananas orange\"><img src=\"/found/here\"\n alt=\"This is a caption\"/><figcaption>\n <p>This is a caption</p>\n </figcaption>\n</figure>", nil) 247 } 248 249 func TestNestedSC(t *testing.T) { 250 t.Parallel() 251 wt := func(tem tpl.TemplateManager) error { 252 tem.AddTemplate("_internal/shortcodes/scn1.html", `<div>Outer, inner is {{ .Inner }}</div>`) 253 tem.AddTemplate("_internal/shortcodes/scn2.html", `<div>SC2</div>`) 254 return nil 255 } 256 CheckShortCodeMatch(t, `{{% scn1 %}}{{% scn2 %}}{{% /scn1 %}}`, "<div>Outer, inner is <div>SC2</div></div>", wt) 257 258 CheckShortCodeMatch(t, `{{< scn1 >}}{{% scn2 %}}{{< /scn1 >}}`, "<div>Outer, inner is <div>SC2</div></div>", wt) 259 } 260 261 func TestNestedComplexSC(t *testing.T) { 262 t.Parallel() 263 wt := func(tem tpl.TemplateManager) error { 264 tem.AddTemplate("_internal/shortcodes/row.html", `-row-{{ .Inner}}-rowStop-`) 265 tem.AddTemplate("_internal/shortcodes/column.html", `-col-{{.Inner }}-colStop-`) 266 tem.AddTemplate("_internal/shortcodes/aside.html", `-aside-{{ .Inner }}-asideStop-`) 267 return nil 268 } 269 CheckShortCodeMatch(t, `{{< row >}}1-s{{% column %}}2-**s**{{< aside >}}3-**s**{{< /aside >}}4-s{{% /column %}}5-s{{< /row >}}6-s`, 270 "-row-1-s-col-2-<strong>s</strong>-aside-3-<strong>s</strong>-asideStop-4-s-colStop-5-s-rowStop-6-s", wt) 271 272 // turn around the markup flag 273 CheckShortCodeMatch(t, `{{% row %}}1-s{{< column >}}2-**s**{{% aside %}}3-**s**{{% /aside %}}4-s{{< /column >}}5-s{{% /row %}}6-s`, 274 "-row-1-s-col-2-<strong>s</strong>-aside-3-<strong>s</strong>-asideStop-4-s-colStop-5-s-rowStop-6-s", wt) 275 } 276 277 func TestParentShortcode(t *testing.T) { 278 t.Parallel() 279 wt := func(tem tpl.TemplateManager) error { 280 tem.AddTemplate("_internal/shortcodes/r1.html", `1: {{ .Get "pr1" }} {{ .Inner }}`) 281 tem.AddTemplate("_internal/shortcodes/r2.html", `2: {{ .Parent.Get "pr1" }}{{ .Get "pr2" }} {{ .Inner }}`) 282 tem.AddTemplate("_internal/shortcodes/r3.html", `3: {{ .Parent.Parent.Get "pr1" }}{{ .Parent.Get "pr2" }}{{ .Get "pr3" }} {{ .Inner }}`) 283 return nil 284 } 285 CheckShortCodeMatch(t, `{{< r1 pr1="p1" >}}1: {{< r2 pr2="p2" >}}2: {{< r3 pr3="p3" >}}{{< /r3 >}}{{< /r2 >}}{{< /r1 >}}`, 286 "1: p1 1: 2: p1p2 2: 3: p1p2p3 ", wt) 287 } 288 289 func TestFigureOnlySrc(t *testing.T) { 290 t.Parallel() 291 CheckShortCodeMatch(t, `{{< figure src="/found/here" >}}`, "<figure><img src=\"/found/here\"/>\n</figure>", nil) 292 } 293 294 func TestFigureCaptionAttrWithMarkdown(t *testing.T) { 295 t.Parallel() 296 CheckShortCodeMatch(t, `{{< figure src="/found/here" caption="Something **bold** _italic_" >}}`, "<figure><img src=\"/found/here\"\n alt=\"Something bold italic\"/><figcaption>\n <p>Something <strong>bold</strong> <em>italic</em></p>\n </figcaption>\n</figure>", nil) 297 CheckShortCodeMatch(t, `{{< figure src="/found/here" attr="Something **bold** _italic_" >}}`, "<figure><img src=\"/found/here\"/><figcaption>\n <p>Something <strong>bold</strong> <em>italic</em></p>\n </figcaption>\n</figure>", nil) 298 } 299 300 func TestFigureImgWidth(t *testing.T) { 301 t.Parallel() 302 CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" alt="apple" width="100px" %}}`, "<figure class=\"bananas orange\"><img src=\"/found/here\"\n alt=\"apple\" width=\"100px\"/>\n</figure>", nil) 303 } 304 305 func TestFigureImgHeight(t *testing.T) { 306 t.Parallel() 307 CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" alt="apple" height="100px" %}}`, "<figure class=\"bananas orange\"><img src=\"/found/here\"\n alt=\"apple\" height=\"100px\"/>\n</figure>", nil) 308 } 309 310 func TestFigureImgWidthAndHeight(t *testing.T) { 311 t.Parallel() 312 CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" alt="apple" width="50" height="100" %}}`, "<figure class=\"bananas orange\"><img src=\"/found/here\"\n alt=\"apple\" width=\"50\" height=\"100\"/>\n</figure>", nil) 313 } 314 315 func TestFigureLinkNoTarget(t *testing.T) { 316 t.Parallel() 317 CheckShortCodeMatch(t, `{{< figure src="/found/here" link="/jump/here/on/clicking" >}}`, "<figure><a href=\"/jump/here/on/clicking\"><img src=\"/found/here\"/></a>\n</figure>", nil) 318 } 319 320 func TestFigureLinkWithTarget(t *testing.T) { 321 t.Parallel() 322 CheckShortCodeMatch(t, `{{< figure src="/found/here" link="/jump/here/on/clicking" target="_self" >}}`, "<figure><a href=\"/jump/here/on/clicking\" target=\"_self\"><img src=\"/found/here\"/></a>\n</figure>", nil) 323 } 324 325 func TestFigureLinkWithTargetAndRel(t *testing.T) { 326 t.Parallel() 327 CheckShortCodeMatch(t, `{{< figure src="/found/here" link="/jump/here/on/clicking" target="_blank" rel="noopener" >}}`, "<figure><a href=\"/jump/here/on/clicking\" target=\"_blank\" rel=\"noopener\"><img src=\"/found/here\"/></a>\n</figure>", nil) 328 } 329 330 // #1642 331 func TestShortcodeWrappedInPIssue(t *testing.T) { 332 t.Parallel() 333 wt := func(tem tpl.TemplateManager) error { 334 tem.AddTemplate("_internal/shortcodes/bug.html", `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`) 335 return nil 336 } 337 CheckShortCodeMatch(t, ` 338 {{< bug >}} 339 340 {{< bug >}} 341 `, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", wt) 342 } 343 344 // #6866 345 func TestShortcodeIncomplete(t *testing.T) { 346 t.Parallel() 347 CheckShortCodeMatchAndError(t, `{{< >}}`, ".*shortcode has no name.*", nil, true) 348 } 349 350 func TestExtractShortcodes(t *testing.T) { 351 b := newTestSitesBuilder(t).WithSimpleConfigFile() 352 353 b.WithTemplates( 354 "default/single.html", `EMPTY`, 355 "_internal/shortcodes/tag.html", `tag`, 356 "_internal/shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`, 357 "_internal/shortcodes/sc1.html", `sc1`, 358 "_internal/shortcodes/sc2.html", `sc2`, 359 "_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`, 360 "_internal/shortcodes/inner2.html", `{{.Inner}}`, 361 "_internal/shortcodes/inner3.html", `{{.Inner}}`, 362 ).WithContent("page.md", `--- 363 title: "Shortcodes Galore!" 364 --- 365 `) 366 367 b.CreateSites().Build(BuildCfg{}) 368 369 s := b.H.Sites[0] 370 371 /*errCheck := func(s string) func(name string, assert *require.Assertions, shortcode *shortcode, err error) { 372 return func(name string, assert *require.Assertions, shortcode *shortcode, err error) { 373 c.Assert(err, name, qt.Not(qt.IsNil)) 374 c.Assert(err.Error(), name, qt.Equals, s) 375 } 376 }*/ 377 378 // Make it more regexp friendly 379 strReplacer := strings.NewReplacer("[", "{", "]", "}") 380 381 str := func(s *shortcode) string { 382 if s == nil { 383 return "<nil>" 384 } 385 386 var version int 387 if s.info != nil { 388 version = s.info.ParseInfo().Config.Version 389 } 390 return strReplacer.Replace(fmt.Sprintf("%s;inline:%t;closing:%t;inner:%v;params:%v;ordinal:%d;markup:%t;version:%d;pos:%d", 391 s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, version, s.pos)) 392 } 393 394 regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) { 395 return func(c *qt.C, shortcode *shortcode, err error) { 396 c.Assert(err, qt.IsNil) 397 c.Assert(str(shortcode), qt.Matches, ".*"+re+".*") 398 } 399 } 400 401 for _, test := range []struct { 402 name string 403 input string 404 check func(c *qt.C, shortcode *shortcode, err error) 405 }{ 406 {"one shortcode, no markup", "{{< tag >}}", regexpCheck("tag.*closing:false.*markup:false")}, 407 {"one shortcode, markup", "{{% tag %}}", regexpCheck("tag.*closing:false.*markup:true;version:2")}, 408 {"one shortcode, markup, legacy", "{{% legacytag %}}", regexpCheck("tag.*closing:false.*markup:true;version:1")}, 409 {"outer shortcode markup", "{{% inner %}}{{< tag >}}{{% /inner %}}", regexpCheck("inner.*closing:true.*markup:true")}, 410 {"inner shortcode markup", "{{< inner >}}{{% tag %}}{{< /inner >}}", regexpCheck("inner.*closing:true.*;markup:false;version:2")}, 411 {"one pos param", "{{% tag param1 %}}", regexpCheck("tag.*params:{param1}")}, 412 {"two pos params", "{{< tag param1 param2>}}", regexpCheck("tag.*params:{param1 param2}")}, 413 {"one named param", `{{% tag param1="value" %}}`, regexpCheck("tag.*params:map{param1:value}")}, 414 {"two named params", `{{< tag param1="value1" param2="value2" >}}`, regexpCheck("tag.*params:map{param\\d:value\\d param\\d:value\\d}")}, 415 {"inner", `{{< inner >}}Inner Content{{< / inner >}}`, regexpCheck("inner;inline:false;closing:true;inner:{Inner Content};")}, 416 // issue #934 417 {"inner self-closing", `{{< inner />}}`, regexpCheck("inner;.*inner:{}")}, 418 { 419 "nested inner", `{{< inner >}}Inner Content->{{% inner2 param1 %}}inner2txt{{% /inner2 %}}Inner close->{{< / inner >}}`, 420 regexpCheck("inner;.*inner:{Inner Content->.*Inner close->}"), 421 }, 422 { 423 "nested, nested inner", `{{< inner >}}inner2->{{% inner2 param1 %}}inner2txt->inner3{{< inner3>}}inner3txt{{</ inner3 >}}{{% /inner2 %}}final close->{{< / inner >}}`, 424 regexpCheck("inner:{inner2-> inner2.*{{inner2txt->inner3.*final close->}"), 425 }, 426 {"closed without content", `{{< inner param1 >}}{{< / inner >}}`, regexpCheck("inner.*inner:{}")}, 427 {"inline", `{{< my.inline >}}Hi{{< /my.inline >}}`, regexpCheck("my.inline;inline:true;closing:true;inner:{Hi};")}, 428 } { 429 430 test := test 431 432 t.Run(test.name, func(t *testing.T) { 433 t.Parallel() 434 c := qt.New(t) 435 436 counter := 0 437 placeholderFunc := func() string { 438 counter++ 439 return fmt.Sprintf("HAHA%s-%dHBHB", shortcodePlaceholderPrefix, counter) 440 } 441 442 p, err := pageparser.ParseMain(strings.NewReader(test.input), pageparser.Config{}) 443 c.Assert(err, qt.IsNil) 444 handler := newShortcodeHandler(nil, s, placeholderFunc) 445 iter := p.Iterator() 446 447 short, err := handler.extractShortcode(0, 0, iter) 448 449 test.check(c, short, err) 450 }) 451 } 452 } 453 454 func TestShortcodesInSite(t *testing.T) { 455 baseURL := "http://foo/bar" 456 457 tests := []struct { 458 contentPath string 459 content string 460 outFile string 461 expected interface{} 462 }{ 463 { 464 "sect/doc1.md", `a{{< b >}}c`, 465 filepath.FromSlash("public/sect/doc1/index.html"), "<p>abc</p>\n", 466 }, 467 // Issue #1642: Multiple shortcodes wrapped in P 468 // Deliberately forced to pass even if they maybe shouldn't. 469 { 470 "sect/doc2.md", `a 471 472 {{< b >}} 473 {{< c >}} 474 {{< d >}} 475 476 e`, 477 filepath.FromSlash("public/sect/doc2/index.html"), 478 "<p>a</p>\n\n<p>b<br />\nc\nd</p>\n\n<p>e</p>\n", 479 }, 480 { 481 "sect/doc3.md", `a 482 483 {{< b >}} 484 {{< c >}} 485 486 {{< d >}} 487 488 e`, 489 filepath.FromSlash("public/sect/doc3/index.html"), 490 "<p>a</p>\n\n<p>b<br />\nc</p>\n\nd\n\n<p>e</p>\n", 491 }, 492 { 493 "sect/doc4.md", `a 494 {{< b >}} 495 {{< b >}} 496 {{< b >}} 497 {{< b >}} 498 {{< b >}} 499 500 501 502 503 504 505 506 507 508 509 `, 510 filepath.FromSlash("public/sect/doc4/index.html"), 511 "<p>a\nb\nb\nb\nb\nb</p>\n", 512 }, 513 // #2192 #2209: Shortcodes in markdown headers 514 { 515 "sect/doc5.md", `# {{< b >}} 516 ## {{% c %}}`, 517 filepath.FromSlash("public/sect/doc5/index.html"), `-hbhb">b</h1>`, 518 }, 519 // #2223 pygments 520 { 521 "sect/doc6.md", "\n```bash\nb = {{< b >}} c = {{% c %}}\n```\n", 522 filepath.FromSlash("public/sect/doc6/index.html"), 523 `<span class="nv">b</span>`, 524 }, 525 // #2249 526 { 527 "sect/doc7.ad", `_Shortcodes:_ *b: {{< b >}} c: {{% c %}}*`, 528 filepath.FromSlash("public/sect/doc7/index.html"), 529 "<div class=\"paragraph\">\n<p><em>Shortcodes:</em> <strong>b: b c: c</strong></p>\n</div>\n", 530 }, 531 { 532 "sect/doc8.rst", `**Shortcodes:** *b: {{< b >}} c: {{% c %}}*`, 533 filepath.FromSlash("public/sect/doc8/index.html"), 534 "<div class=\"document\">\n\n\n<p><strong>Shortcodes:</strong> <em>b: b c: c</em></p>\n</div>", 535 }, 536 537 // Issue #1229: Menus not available in shortcode. 538 { 539 "sect/doc10.md", `--- 540 menu: 541 main: 542 identifier: 'parent' 543 tags: 544 - Menu 545 --- 546 **Menus:** {{< menu >}}`, 547 filepath.FromSlash("public/sect/doc10/index.html"), 548 "<p><strong>Menus:</strong> 1</p>\n", 549 }, 550 // Issue #2323: Taxonomies not available in shortcode. 551 { 552 "sect/doc11.md", `--- 553 tags: 554 - Bugs 555 menu: 556 main: 557 parent: 'parent' 558 --- 559 **Tags:** {{< tags >}}`, 560 filepath.FromSlash("public/sect/doc11/index.html"), 561 "<p><strong>Tags:</strong> 2</p>\n", 562 }, 563 { 564 "sect/doc12.md", `--- 565 title: "Foo" 566 --- 567 568 {{% html-indented-v1 %}}`, 569 "public/sect/doc12/index.html", 570 "<h1>Hugo!</h1>", 571 }, 572 } 573 574 temp := tests[:0] 575 for _, test := range tests { 576 if strings.HasSuffix(test.contentPath, ".ad") && !asciidocext.Supports() { 577 t.Log("Skip Asciidoc test case as no Asciidoc present.") 578 continue 579 } else if strings.HasSuffix(test.contentPath, ".rst") && !rst.Supports() { 580 t.Log("Skip Rst test case as no rst2html present.") 581 continue 582 } 583 temp = append(temp, test) 584 } 585 tests = temp 586 587 sources := make([][2]string, len(tests)) 588 589 for i, test := range tests { 590 sources[i] = [2]string{filepath.FromSlash(test.contentPath), test.content} 591 } 592 593 addTemplates := func(templ tpl.TemplateManager) error { 594 templ.AddTemplate("_default/single.html", "{{.Content}} Word Count: {{ .WordCount }}") 595 596 templ.AddTemplate("_internal/shortcodes/b.html", `b`) 597 templ.AddTemplate("_internal/shortcodes/c.html", `c`) 598 templ.AddTemplate("_internal/shortcodes/d.html", `d`) 599 templ.AddTemplate("_internal/shortcodes/html-indented-v1.html", "{{ $_hugo_config := `{ \"version\": 1 }` }}"+` 600 <h1>Hugo!</h1> 601 `) 602 templ.AddTemplate("_internal/shortcodes/menu.html", `{{ len (index .Page.Menus "main").Children }}`) 603 templ.AddTemplate("_internal/shortcodes/tags.html", `{{ len .Page.Site.Taxonomies.tags }}`) 604 605 return nil 606 } 607 608 cfg, fs := newTestCfg() 609 610 cfg.Set("defaultContentLanguage", "en") 611 cfg.Set("baseURL", baseURL) 612 cfg.Set("uglyURLs", false) 613 cfg.Set("verbose", true) 614 615 cfg.Set("security", map[string]interface{}{ 616 "exec": map[string]interface{}{ 617 "allow": []string{"^python$", "^rst2html.*", "^asciidoctor$"}, 618 }, 619 }) 620 621 cfg.Set("markup.highlight.noClasses", false) 622 cfg.Set("markup.highlight.codeFences", true) 623 cfg.Set("markup", map[string]interface{}{ 624 "defaultMarkdownHandler": "blackfriday", // TODO(bep) 625 }) 626 627 writeSourcesToSource(t, "content", fs, sources...) 628 629 s := buildSingleSite(t, deps.DepsCfg{WithTemplate: addTemplates, Fs: fs, Cfg: cfg}, BuildCfg{}) 630 631 for i, test := range tests { 632 test := test 633 t.Run(fmt.Sprintf("test=%d;contentPath=%s", i, test.contentPath), func(t *testing.T) { 634 t.Parallel() 635 636 th := newTestHelper(s.Cfg, s.Fs, t) 637 638 expected := cast.ToStringSlice(test.expected) 639 640 th.assertFileContent(filepath.FromSlash(test.outFile), expected...) 641 }) 642 643 } 644 } 645 646 func TestShortcodeMultipleOutputFormats(t *testing.T) { 647 t.Parallel() 648 649 siteConfig := ` 650 baseURL = "http://example.com/blog" 651 652 paginate = 1 653 654 disableKinds = ["section", "term", "taxonomy", "RSS", "sitemap", "robotsTXT", "404"] 655 656 [outputs] 657 home = [ "HTML", "AMP", "Calendar" ] 658 page = [ "HTML", "AMP", "JSON" ] 659 660 ` 661 662 pageTemplate := `--- 663 title: "%s" 664 --- 665 # Doc 666 667 {{< myShort >}} 668 {{< noExt >}} 669 {{%% onlyHTML %%}} 670 671 {{< myInner >}}{{< myShort >}}{{< /myInner >}} 672 673 ` 674 675 pageTemplateCSVOnly := `--- 676 title: "%s" 677 outputs: ["CSV"] 678 --- 679 # Doc 680 681 CSV: {{< myShort >}} 682 ` 683 684 b := newTestSitesBuilder(t).WithConfigFile("toml", siteConfig) 685 b.WithTemplates( 686 "layouts/_default/single.html", `Single HTML: {{ .Title }}|{{ .Content }}`, 687 "layouts/_default/single.json", `Single JSON: {{ .Title }}|{{ .Content }}`, 688 "layouts/_default/single.csv", `Single CSV: {{ .Title }}|{{ .Content }}`, 689 "layouts/index.html", `Home HTML: {{ .Title }}|{{ .Content }}`, 690 "layouts/index.amp.html", `Home AMP: {{ .Title }}|{{ .Content }}`, 691 "layouts/index.ics", `Home Calendar: {{ .Title }}|{{ .Content }}`, 692 "layouts/shortcodes/myShort.html", `ShortHTML`, 693 "layouts/shortcodes/myShort.amp.html", `ShortAMP`, 694 "layouts/shortcodes/myShort.csv", `ShortCSV`, 695 "layouts/shortcodes/myShort.ics", `ShortCalendar`, 696 "layouts/shortcodes/myShort.json", `ShortJSON`, 697 "layouts/shortcodes/noExt", `ShortNoExt`, 698 "layouts/shortcodes/onlyHTML.html", `ShortOnlyHTML`, 699 "layouts/shortcodes/myInner.html", `myInner:--{{- .Inner -}}--`, 700 ) 701 702 b.WithContent("_index.md", fmt.Sprintf(pageTemplate, "Home"), 703 "sect/mypage.md", fmt.Sprintf(pageTemplate, "Single"), 704 "sect/mycsvpage.md", fmt.Sprintf(pageTemplateCSVOnly, "Single CSV"), 705 ) 706 707 b.Build(BuildCfg{}) 708 h := b.H 709 b.Assert(len(h.Sites), qt.Equals, 1) 710 711 s := h.Sites[0] 712 home := s.getPage(page.KindHome) 713 b.Assert(home, qt.Not(qt.IsNil)) 714 b.Assert(len(home.OutputFormats()), qt.Equals, 3) 715 716 b.AssertFileContent("public/index.html", 717 "Home HTML", 718 "ShortHTML", 719 "ShortNoExt", 720 "ShortOnlyHTML", 721 "myInner:--ShortHTML--", 722 ) 723 724 b.AssertFileContent("public/amp/index.html", 725 "Home AMP", 726 "ShortAMP", 727 "ShortNoExt", 728 "ShortOnlyHTML", 729 "myInner:--ShortAMP--", 730 ) 731 732 b.AssertFileContent("public/index.ics", 733 "Home Calendar", 734 "ShortCalendar", 735 "ShortNoExt", 736 "ShortOnlyHTML", 737 "myInner:--ShortCalendar--", 738 ) 739 740 b.AssertFileContent("public/sect/mypage/index.html", 741 "Single HTML", 742 "ShortHTML", 743 "ShortNoExt", 744 "ShortOnlyHTML", 745 "myInner:--ShortHTML--", 746 ) 747 748 b.AssertFileContent("public/sect/mypage/index.json", 749 "Single JSON", 750 "ShortJSON", 751 "ShortNoExt", 752 "ShortOnlyHTML", 753 "myInner:--ShortJSON--", 754 ) 755 756 b.AssertFileContent("public/amp/sect/mypage/index.html", 757 // No special AMP template 758 "Single HTML", 759 "ShortAMP", 760 "ShortNoExt", 761 "ShortOnlyHTML", 762 "myInner:--ShortAMP--", 763 ) 764 765 b.AssertFileContent("public/sect/mycsvpage/index.csv", 766 "Single CSV", 767 "ShortCSV", 768 ) 769 } 770 771 func BenchmarkReplaceShortcodeTokens(b *testing.B) { 772 type input struct { 773 in []byte 774 replacements map[string]string 775 expect []byte 776 } 777 778 data := []struct { 779 input string 780 replacements map[string]string 781 expect []byte 782 }{ 783 {"Hello HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, []byte("Hello World.")}, 784 {strings.Repeat("A", 100) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("A", 100) + " Hello World.")}, 785 {strings.Repeat("A", 500) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("A", 500) + " Hello World.")}, 786 {strings.Repeat("ABCD ", 500) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("ABCD ", 500) + " Hello World.")}, 787 {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.")}, 788 } 789 790 in := make([]input, b.N*len(data)) 791 cnt := 0 792 for i := 0; i < b.N; i++ { 793 for _, this := range data { 794 in[cnt] = input{[]byte(this.input), this.replacements, this.expect} 795 cnt++ 796 } 797 } 798 799 b.ResetTimer() 800 cnt = 0 801 for i := 0; i < b.N; i++ { 802 for j := range data { 803 currIn := in[cnt] 804 cnt++ 805 results, err := replaceShortcodeTokens(currIn.in, currIn.replacements) 806 if err != nil { 807 b.Fatalf("[%d] failed: %s", i, err) 808 continue 809 } 810 if len(results) != len(currIn.expect) { 811 b.Fatalf("[%d] replaceShortcodeTokens, got \n%q but expected \n%q", j, results, currIn.expect) 812 } 813 814 } 815 } 816 } 817 818 func TestReplaceShortcodeTokens(t *testing.T) { 819 t.Parallel() 820 for i, this := range []struct { 821 input string 822 prefix string 823 replacements map[string]string 824 expect interface{} 825 }{ 826 {"Hello HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello World."}, 827 {"Hello HAHAHUGOSHORTCODE-1@}@.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, false}, 828 {"HAHAHUGOSHORTCODE2-1HBHB", "PREFIX2", map[string]string{"HAHAHUGOSHORTCODE2-1HBHB": "World"}, "World"}, 829 {"Hello World!", "PREFIX2", map[string]string{}, "Hello World!"}, 830 {"!HAHAHUGOSHORTCODE-1HBHB", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "!World"}, 831 {"HAHAHUGOSHORTCODE-1HBHB!", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "World!"}, 832 {"!HAHAHUGOSHORTCODE-1HBHB!", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "!World!"}, 833 {"_{_PREFIX-1HBHB", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "_{_PREFIX-1HBHB"}, 834 {"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."}, 835 {"A HAHAHUGOSHORTCODE-1HBHB asdf HAHAHUGOSHORTCODE-2HBHB.", "A", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "v1", "HAHAHUGOSHORTCODE-2HBHB": "v2"}, "A v1 asdf v2."}, 836 {"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!."}, 837 {"A HAHAHUGOSHORTCODE-2HBHB HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "A B A."}, 838 {"A HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-2", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A"}, false}, 839 {"A HAHAHUGOSHORTCODE-1HBHB but not the second.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "A A but not the second."}, 840 {"An HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "An A."}, 841 {"An HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-2HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "An A B."}, 842 {"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."}, 843 {"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."}, 844 // Issue #1148 remove p-tags 10 => 845 {"Hello <p>HAHAHUGOSHORTCODE-1HBHB</p>. END.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello World. END."}, 846 {"Hello <p>HAHAHUGOSHORTCODE-1HBHB</p>. <p>HAHAHUGOSHORTCODE-2HBHB</p> END.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World", "HAHAHUGOSHORTCODE-2HBHB": "THE"}, "Hello World. THE END."}, 847 {"Hello <p>HAHAHUGOSHORTCODE-1HBHB. END</p>.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello <p>World. END</p>."}, 848 {"<p>Hello HAHAHUGOSHORTCODE-1HBHB</p>. END.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "<p>Hello World</p>. END."}, 849 {"Hello <p>HAHAHUGOSHORTCODE-1HBHB12", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello <p>World12"}, 850 { 851 "Hello HAHAHUGOSHORTCODE-1HBHB. HAHAHUGOSHORTCODE-1HBHB-HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-1HBHB END", "P", 852 map[string]string{"HAHAHUGOSHORTCODE-1HBHB": strings.Repeat("BC", 100)}, 853 fmt.Sprintf("Hello %s. %s-%s %s %s %s END", 854 strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100)), 855 }, 856 } { 857 858 results, err := replaceShortcodeTokens([]byte(this.input), this.replacements) 859 860 if b, ok := this.expect.(bool); ok && !b { 861 if err == nil { 862 t.Errorf("[%d] replaceShortcodeTokens didn't return an expected error", i) 863 } 864 } else { 865 if err != nil { 866 t.Errorf("[%d] failed: %s", i, err) 867 continue 868 } 869 if !reflect.DeepEqual(results, []byte(this.expect.(string))) { 870 t.Errorf("[%d] replaceShortcodeTokens, got \n%q but expected \n%q", i, results, this.expect) 871 } 872 } 873 874 } 875 } 876 877 func TestShortcodeGetContent(t *testing.T) { 878 t.Parallel() 879 880 contentShortcode := ` 881 {{- $t := .Get 0 -}} 882 {{- $p := .Get 1 -}} 883 {{- $k := .Get 2 -}} 884 {{- $page := $.Page.Site.GetPage "page" $p -}} 885 {{ if $page }} 886 {{- if eq $t "bundle" -}} 887 {{- .Scratch.Set "p" ($page.Resources.GetMatch (printf "%s*" $k)) -}} 888 {{- else -}} 889 {{- $.Scratch.Set "p" $page -}} 890 {{- end -}}P1:{{ .Page.Content }}|P2:{{ $p := ($.Scratch.Get "p") }}{{ $p.Title }}/{{ $p.Content }}| 891 {{- else -}} 892 {{- errorf "Page %s is nil" $p -}} 893 {{- end -}} 894 ` 895 896 var templates []string 897 var content []string 898 899 contentWithShortcodeTemplate := `--- 900 title: doc%s 901 weight: %d 902 --- 903 Logo:{{< c "bundle" "b1" "logo.png" >}}:P1: {{< c "page" "section1/p1" "" >}}:BP1:{{< c "bundle" "b1" "bp1" >}}` 904 905 simpleContentTemplate := `--- 906 title: doc%s 907 weight: %d 908 --- 909 C-%s` 910 911 templates = append(templates, []string{"shortcodes/c.html", contentShortcode}...) 912 templates = append(templates, []string{"_default/single.html", "Single Content: {{ .Content }}"}...) 913 templates = append(templates, []string{"_default/list.html", "List Content: {{ .Content }}"}...) 914 915 content = append(content, []string{"b1/index.md", fmt.Sprintf(contentWithShortcodeTemplate, "b1", 1)}...) 916 content = append(content, []string{"b1/logo.png", "PNG logo"}...) 917 content = append(content, []string{"b1/bp1.md", fmt.Sprintf(simpleContentTemplate, "bp1", 1, "bp1")}...) 918 919 content = append(content, []string{"section1/_index.md", fmt.Sprintf(contentWithShortcodeTemplate, "s1", 2)}...) 920 content = append(content, []string{"section1/p1.md", fmt.Sprintf(simpleContentTemplate, "s1p1", 2, "s1p1")}...) 921 922 content = append(content, []string{"section2/_index.md", fmt.Sprintf(simpleContentTemplate, "b1", 1, "b1")}...) 923 content = append(content, []string{"section2/s2p1.md", fmt.Sprintf(contentWithShortcodeTemplate, "bp1", 1)}...) 924 925 builder := newTestSitesBuilder(t).WithDefaultMultiSiteConfig() 926 927 builder.WithContent(content...).WithTemplates(templates...).CreateSites().Build(BuildCfg{}) 928 s := builder.H.Sites[0] 929 builder.Assert(len(s.RegularPages()), qt.Equals, 3) 930 931 builder.AssertFileContent("public/en/section1/index.html", 932 "List Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|", 933 "BP1:P1:|P2:docbp1/<p>C-bp1</p>", 934 ) 935 936 builder.AssertFileContent("public/en/b1/index.html", 937 "Single Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|", 938 "P2:docbp1/<p>C-bp1</p>", 939 ) 940 941 builder.AssertFileContent("public/en/section2/s2p1/index.html", 942 "Single Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|", 943 "P2:docbp1/<p>C-bp1</p>", 944 ) 945 } 946 947 // https://github.com/gohugoio/hugo/issues/5833 948 func TestShortcodeParentResourcesOnRebuild(t *testing.T) { 949 t.Parallel() 950 951 b := newTestSitesBuilder(t).Running().WithSimpleConfigFile() 952 b.WithTemplatesAdded( 953 "index.html", ` 954 {{ $b := .Site.GetPage "b1" }} 955 b1 Content: {{ $b.Content }} 956 {{$p := $b.Resources.GetMatch "p1*" }} 957 Content: {{ $p.Content }} 958 {{ $article := .Site.GetPage "blog/article" }} 959 Article Content: {{ $article.Content }} 960 `, 961 "shortcodes/c.html", ` 962 {{ range .Page.Parent.Resources }} 963 * Parent resource: {{ .Name }}: {{ .RelPermalink }} 964 {{ end }} 965 `) 966 967 pageContent := ` 968 --- 969 title: MyPage 970 --- 971 972 SHORTCODE: {{< c >}} 973 974 ` 975 976 b.WithContent("b1/index.md", pageContent, 977 "b1/logo.png", "PNG logo", 978 "b1/p1.md", pageContent, 979 "blog/_index.md", pageContent, 980 "blog/logo-article.png", "PNG logo", 981 "blog/article.md", pageContent, 982 ) 983 984 b.Build(BuildCfg{}) 985 986 assert := func(matchers ...string) { 987 allMatchers := append(matchers, "Parent resource: logo.png: /b1/logo.png", 988 "Article Content: <p>SHORTCODE: \n\n* Parent resource: logo-article.png: /blog/logo-article.png", 989 ) 990 991 b.AssertFileContent("public/index.html", 992 allMatchers..., 993 ) 994 } 995 996 assert() 997 998 b.EditFiles("content/b1/index.md", pageContent+" Edit.") 999 1000 b.Build(BuildCfg{}) 1001 1002 assert("Edit.") 1003 } 1004 1005 func TestShortcodePreserveOrder(t *testing.T) { 1006 t.Parallel() 1007 c := qt.New(t) 1008 1009 contentTemplate := `--- 1010 title: doc%d 1011 weight: %d 1012 --- 1013 # doc 1014 1015 {{< s1 >}}{{< s2 >}}{{< s3 >}}{{< s4 >}}{{< s5 >}} 1016 1017 {{< nested >}} 1018 {{< ordinal >}} {{< scratch >}} 1019 {{< ordinal >}} {{< scratch >}} 1020 {{< ordinal >}} {{< scratch >}} 1021 {{< /nested >}} 1022 1023 ` 1024 1025 ordinalShortcodeTemplate := `ordinal: {{ .Ordinal }}{{ .Page.Scratch.Set "ordinal" .Ordinal }}` 1026 1027 nestedShortcode := `outer ordinal: {{ .Ordinal }} inner: {{ .Inner }}` 1028 scratchGetShortcode := `scratch ordinal: {{ .Ordinal }} scratch get ordinal: {{ .Page.Scratch.Get "ordinal" }}` 1029 shortcodeTemplate := `v%d: {{ .Ordinal }} sgo: {{ .Page.Scratch.Get "o2" }}{{ .Page.Scratch.Set "o2" .Ordinal }}|` 1030 1031 var shortcodes []string 1032 var content []string 1033 1034 shortcodes = append(shortcodes, []string{"shortcodes/nested.html", nestedShortcode}...) 1035 shortcodes = append(shortcodes, []string{"shortcodes/ordinal.html", ordinalShortcodeTemplate}...) 1036 shortcodes = append(shortcodes, []string{"shortcodes/scratch.html", scratchGetShortcode}...) 1037 1038 for i := 1; i <= 5; i++ { 1039 sc := fmt.Sprintf(shortcodeTemplate, i) 1040 sc = strings.Replace(sc, "%%", "%", -1) 1041 shortcodes = append(shortcodes, []string{fmt.Sprintf("shortcodes/s%d.html", i), sc}...) 1042 } 1043 1044 for i := 1; i <= 3; i++ { 1045 content = append(content, []string{fmt.Sprintf("p%d.md", i), fmt.Sprintf(contentTemplate, i, i)}...) 1046 } 1047 1048 builder := newTestSitesBuilder(t).WithDefaultMultiSiteConfig() 1049 1050 builder.WithContent(content...).WithTemplatesAdded(shortcodes...).CreateSites().Build(BuildCfg{}) 1051 1052 s := builder.H.Sites[0] 1053 c.Assert(len(s.RegularPages()), qt.Equals, 3) 1054 1055 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`) 1056 builder.AssertFileContent("public/en/p1/index.html", `outer ordinal: 5 inner: 1057 ordinal: 0 scratch ordinal: 1 scratch get ordinal: 0 1058 ordinal: 2 scratch ordinal: 3 scratch get ordinal: 2 1059 ordinal: 4 scratch ordinal: 5 scratch get ordinal: 4`) 1060 } 1061 1062 func TestShortcodeVariables(t *testing.T) { 1063 t.Parallel() 1064 c := qt.New(t) 1065 1066 builder := newTestSitesBuilder(t).WithSimpleConfigFile() 1067 1068 builder.WithContent("page.md", `--- 1069 title: "Hugo Rocks!" 1070 --- 1071 1072 # doc 1073 1074 {{< s1 >}} 1075 1076 `).WithTemplatesAdded("layouts/shortcodes/s1.html", ` 1077 Name: {{ .Name }} 1078 {{ with .Position }} 1079 File: {{ .Filename }} 1080 Offset: {{ .Offset }} 1081 Line: {{ .LineNumber }} 1082 Column: {{ .ColumnNumber }} 1083 String: {{ . | safeHTML }} 1084 {{ end }} 1085 1086 `).CreateSites().Build(BuildCfg{}) 1087 1088 s := builder.H.Sites[0] 1089 c.Assert(len(s.RegularPages()), qt.Equals, 1) 1090 1091 builder.AssertFileContent("public/page/index.html", 1092 filepath.FromSlash("File: content/page.md"), 1093 "Line: 7", "Column: 4", "Offset: 40", 1094 filepath.FromSlash("String: \"content/page.md:7:4\""), 1095 "Name: s1", 1096 ) 1097 } 1098 1099 func TestInlineShortcodes(t *testing.T) { 1100 for _, enableInlineShortcodes := range []bool{true, false} { 1101 enableInlineShortcodes := enableInlineShortcodes 1102 t.Run(fmt.Sprintf("enableInlineShortcodes=%t", enableInlineShortcodes), 1103 func(t *testing.T) { 1104 t.Parallel() 1105 conf := fmt.Sprintf(` 1106 baseURL = "https://example.com" 1107 enableInlineShortcodes = %t 1108 `, enableInlineShortcodes) 1109 1110 b := newTestSitesBuilder(t) 1111 b.WithConfigFile("toml", conf) 1112 1113 shortcodeContent := `FIRST:{{< myshort.inline "first" >}} 1114 Page: {{ .Page.Title }} 1115 Seq: {{ seq 3 }} 1116 Param: {{ .Get 0 }} 1117 {{< /myshort.inline >}}:END: 1118 1119 SECOND:{{< myshort.inline "second" />}}:END 1120 NEW INLINE: {{< n1.inline "5" >}}W1: {{ seq (.Get 0) }}{{< /n1.inline >}}:END: 1121 INLINE IN INNER: {{< outer >}}{{< n2.inline >}}W2: {{ seq 4 }}{{< /n2.inline >}}{{< /outer >}}:END: 1122 REUSED INLINE IN INNER: {{< outer >}}{{< n1.inline "3" />}}{{< /outer >}}:END: 1123 ## MARKDOWN DELIMITER: {{% mymarkdown.inline %}}**Hugo Rocks!**{{% /mymarkdown.inline %}} 1124 ` 1125 1126 b.WithContent("page-md-shortcode.md", `--- 1127 title: "Hugo" 1128 --- 1129 `+shortcodeContent) 1130 1131 b.WithContent("_index.md", `--- 1132 title: "Hugo Home" 1133 --- 1134 1135 `+shortcodeContent) 1136 1137 b.WithTemplatesAdded("layouts/_default/single.html", ` 1138 CONTENT:{{ .Content }} 1139 TOC: {{ .TableOfContents }} 1140 `) 1141 1142 b.WithTemplatesAdded("layouts/index.html", ` 1143 CONTENT:{{ .Content }} 1144 TOC: {{ .TableOfContents }} 1145 `) 1146 1147 b.WithTemplatesAdded("layouts/shortcodes/outer.html", `Inner: {{ .Inner }}`) 1148 1149 b.CreateSites().Build(BuildCfg{}) 1150 1151 shouldContain := []string{ 1152 "Seq: [1 2 3]", 1153 "Param: first", 1154 "Param: second", 1155 "NEW INLINE: W1: [1 2 3 4 5]", 1156 "INLINE IN INNER: Inner: W2: [1 2 3 4]", 1157 "REUSED INLINE IN INNER: Inner: W1: [1 2 3]", 1158 `<li><a href="#markdown-delimiter-hugo-rocks">MARKDOWN DELIMITER: <strong>Hugo Rocks!</strong></a></li>`, 1159 } 1160 1161 if enableInlineShortcodes { 1162 b.AssertFileContent("public/page-md-shortcode/index.html", 1163 shouldContain..., 1164 ) 1165 b.AssertFileContent("public/index.html", 1166 shouldContain..., 1167 ) 1168 } else { 1169 b.AssertFileContent("public/page-md-shortcode/index.html", 1170 "FIRST::END", 1171 "SECOND::END", 1172 "NEW INLINE: :END", 1173 "INLINE IN INNER: Inner: :END:", 1174 "REUSED INLINE IN INNER: Inner: :END:", 1175 ) 1176 } 1177 }) 1178 1179 } 1180 } 1181 1182 // https://github.com/gohugoio/hugo/issues/5863 1183 func TestShortcodeNamespaced(t *testing.T) { 1184 t.Parallel() 1185 c := qt.New(t) 1186 1187 builder := newTestSitesBuilder(t).WithSimpleConfigFile() 1188 1189 builder.WithContent("page.md", `--- 1190 title: "Hugo Rocks!" 1191 --- 1192 1193 # doc 1194 1195 hello: {{< hello >}} 1196 test/hello: {{< test/hello >}} 1197 1198 `).WithTemplatesAdded( 1199 "layouts/shortcodes/hello.html", `hello`, 1200 "layouts/shortcodes/test/hello.html", `test/hello`).CreateSites().Build(BuildCfg{}) 1201 1202 s := builder.H.Sites[0] 1203 c.Assert(len(s.RegularPages()), qt.Equals, 1) 1204 1205 builder.AssertFileContent("public/page/index.html", 1206 "hello: hello", 1207 "test/hello: test/hello", 1208 ) 1209 } 1210 1211 // https://github.com/gohugoio/hugo/issues/6504 1212 func TestShortcodeEmoji(t *testing.T) { 1213 t.Parallel() 1214 1215 v := config.New() 1216 v.Set("enableEmoji", true) 1217 1218 builder := newTestSitesBuilder(t).WithViper(v) 1219 1220 builder.WithContent("page.md", `--- 1221 title: "Hugo Rocks!" 1222 --- 1223 1224 # doc 1225 1226 {{< event >}}10:30-11:00 My :smile: Event {{< /event >}} 1227 1228 1229 `).WithTemplatesAdded( 1230 "layouts/shortcodes/event.html", `<div>{{ "\u29BE" }} {{ .Inner }} </div>`) 1231 1232 builder.Build(BuildCfg{}) 1233 builder.AssertFileContent("public/page/index.html", 1234 "⦾ 10:30-11:00 My 😄 Event", 1235 ) 1236 } 1237 1238 func TestShortcodeTypedParams(t *testing.T) { 1239 t.Parallel() 1240 c := qt.New(t) 1241 1242 builder := newTestSitesBuilder(t).WithSimpleConfigFile() 1243 1244 builder.WithContent("page.md", `--- 1245 title: "Hugo Rocks!" 1246 --- 1247 1248 # doc 1249 1250 types positional: {{< hello true false 33 3.14 >}} 1251 types named: {{< hello b1=true b2=false i1=33 f1=3.14 >}} 1252 types string: {{< hello "true" trues "33" "3.14" >}} 1253 1254 1255 `).WithTemplatesAdded( 1256 "layouts/shortcodes/hello.html", 1257 `{{ range $i, $v := .Params }} 1258 - {{ printf "%v: %v (%T)" $i $v $v }} 1259 {{ end }} 1260 {{ $b1 := .Get "b1" }} 1261 Get: {{ printf "%v (%T)" $b1 $b1 | safeHTML }} 1262 `).Build(BuildCfg{}) 1263 1264 s := builder.H.Sites[0] 1265 c.Assert(len(s.RegularPages()), qt.Equals, 1) 1266 1267 builder.AssertFileContent("public/page/index.html", 1268 "types positional: - 0: true (bool) - 1: false (bool) - 2: 33 (int) - 3: 3.14 (float64)", 1269 "types named: - b1: true (bool) - b2: false (bool) - f1: 3.14 (float64) - i1: 33 (int) Get: true (bool) ", 1270 "types string: - 0: true (string) - 1: trues (string) - 2: 33 (string) - 3: 3.14 (string) ", 1271 ) 1272 } 1273 1274 func TestShortcodeRef(t *testing.T) { 1275 for _, plainIDAnchors := range []bool{false, true} { 1276 plainIDAnchors := plainIDAnchors 1277 t.Run(fmt.Sprintf("plainIDAnchors=%t", plainIDAnchors), func(t *testing.T) { 1278 t.Parallel() 1279 1280 v := config.New() 1281 v.Set("baseURL", "https://example.org") 1282 v.Set("blackfriday", map[string]interface{}{ 1283 "plainIDAnchors": plainIDAnchors, 1284 }) 1285 v.Set("markup", map[string]interface{}{ 1286 "defaultMarkdownHandler": "blackfriday", // TODO(bep) 1287 }) 1288 1289 builder := newTestSitesBuilder(t).WithViper(v) 1290 1291 for i := 1; i <= 2; i++ { 1292 builder.WithContent(fmt.Sprintf("page%d.md", i), `--- 1293 title: "Hugo Rocks!" 1294 --- 1295 1296 1297 1298 [Page 1]({{< ref "page1.md" >}}) 1299 [Page 1 with anchor]({{< relref "page1.md#doc" >}}) 1300 [Page 2]({{< ref "page2.md" >}}) 1301 [Page 2 with anchor]({{< relref "page2.md#doc" >}}) 1302 1303 1304 ## Doc 1305 1306 1307 `) 1308 } 1309 1310 builder.Build(BuildCfg{}) 1311 1312 if plainIDAnchors { 1313 builder.AssertFileContent("public/page2/index.html", 1314 ` 1315 <a href="/page1/#doc">Page 1 with anchor</a> 1316 <a href="https://example.org/page2/">Page 2</a> 1317 <a href="/page2/#doc">Page 2 with anchor</a></p> 1318 1319 <h2 id="doc">Doc</h2> 1320 `, 1321 ) 1322 } else { 1323 builder.AssertFileContent("public/page2/index.html", 1324 ` 1325 <p><a href="https://example.org/page1/">Page 1</a> 1326 <a href="/page1/#doc:45ca767ba77bc1445a0acab74f80812f">Page 1 with anchor</a> 1327 <a href="https://example.org/page2/">Page 2</a> 1328 <a href="/page2/#doc:8e3cdf52fa21e33270c99433820e46bd">Page 2 with anchor</a></p> 1329 <h2 id="doc:8e3cdf52fa21e33270c99433820e46bd">Doc</h2> 1330 `, 1331 ) 1332 } 1333 }) 1334 } 1335 } 1336 1337 // https://github.com/gohugoio/hugo/issues/6857 1338 func TestShortcodeNoInner(t *testing.T) { 1339 t.Parallel() 1340 1341 b := newTestSitesBuilder(t) 1342 1343 b.WithContent("page.md", `--- 1344 title: "No Inner!" 1345 --- 1346 {{< noinner >}}{{< /noinner >}} 1347 1348 1349 `).WithTemplatesAdded( 1350 "layouts/shortcodes/noinner.html", `No inner here.`) 1351 1352 err := b.BuildE(BuildCfg{}) 1353 b.Assert(err.Error(), qt.Contains, `failed to extract shortcode: shortcode "noinner" has no .Inner, yet a closing tag was provided`) 1354 } 1355 1356 func TestShortcodeStableOutputFormatTemplates(t *testing.T) { 1357 t.Parallel() 1358 1359 for i := 0; i < 5; i++ { 1360 1361 b := newTestSitesBuilder(t) 1362 1363 const numPages = 10 1364 1365 for i := 0; i < numPages; i++ { 1366 b.WithContent(fmt.Sprintf("page%d.md", i), `--- 1367 title: "Page" 1368 outputs: ["html", "css", "csv", "json"] 1369 --- 1370 {{< myshort >}} 1371 1372 `) 1373 } 1374 1375 b.WithTemplates( 1376 "_default/single.html", "{{ .Content }}", 1377 "_default/single.css", "{{ .Content }}", 1378 "_default/single.csv", "{{ .Content }}", 1379 "_default/single.json", "{{ .Content }}", 1380 "shortcodes/myshort.html", `Short-HTML`, 1381 "shortcodes/myshort.csv", `Short-CSV`, 1382 ) 1383 1384 b.Build(BuildCfg{}) 1385 1386 // helpers.PrintFs(b.Fs.Destination, "public", os.Stdout) 1387 1388 for i := 0; i < numPages; i++ { 1389 b.AssertFileContent(fmt.Sprintf("public/page%d/index.html", i), "Short-HTML") 1390 b.AssertFileContent(fmt.Sprintf("public/page%d/index.csv", i), "Short-CSV") 1391 b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-HTML") 1392 1393 } 1394 1395 for i := 0; i < numPages; i++ { 1396 b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-HTML") 1397 } 1398 1399 } 1400 }