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 }