summaryrefslogtreecommitdiffstats
path: root/hugolib/shortcode_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'hugolib/shortcode_test.go')
-rw-r--r--hugolib/shortcode_test.go874
1 files changed, 874 insertions, 0 deletions
diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go
new file mode 100644
index 000000000..763394032
--- /dev/null
+++ b/hugolib/shortcode_test.go
@@ -0,0 +1,874 @@
+// Copyright 2016 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hugolib
+
+import (
+ "fmt"
+ "path/filepath"
+ "reflect"
+ "regexp"
+ "sort"
+ "strings"
+ "testing"
+
+ jww "github.com/spf13/jwalterweatherman"
+
+ "github.com/spf13/afero"
+
+ "github.com/gohugoio/hugo/output"
+
+ "github.com/gohugoio/hugo/media"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/stretchr/testify/require"
+)
+
+// TODO(bep) remove
+func pageFromString(in, filename string, withTemplate ...func(templ tpl.TemplateHandler) error) (*Page, error) {
+ s := newTestSite(nil)
+ if len(withTemplate) > 0 {
+ // Have to create a new site
+ var err error
+ cfg, fs := newTestCfg()
+
+ d := deps.DepsCfg{Language: helpers.NewLanguage("en", cfg), Cfg: cfg, Fs: fs, WithTemplate: withTemplate[0]}
+
+ s, err = NewSiteForCfg(d)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return s.NewPageFrom(strings.NewReader(in), filename)
+}
+
+func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error) {
+ CheckShortCodeMatchAndError(t, input, expected, withTemplate, false)
+}
+
+func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error, expectError bool) {
+
+ cfg, fs := newTestCfg()
+
+ // Need some front matter, see https://github.com/gohugoio/hugo/issues/2337
+ contentFile := `---
+title: "Title"
+---
+` + input
+
+ writeSource(t, fs, "content/simple.md", contentFile)
+
+ h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate})
+
+ require.NoError(t, err)
+ require.Len(t, h.Sites, 1)
+
+ err = h.Build(BuildCfg{})
+
+ if err != nil && !expectError {
+ t.Fatalf("Shortcode rendered error %s.", err)
+ }
+
+ if err == nil && expectError {
+ t.Fatalf("No error from shortcode")
+ }
+
+ require.Len(t, h.Sites[0].RegularPages, 1)
+
+ output := strings.TrimSpace(string(h.Sites[0].RegularPages[0].Content))
+ output = strings.TrimPrefix(output, "<p>")
+ output = strings.TrimSuffix(output, "</p>")
+
+ expected = strings.TrimSpace(expected)
+
+ if output != expected {
+ t.Fatalf("Shortcode render didn't match. got \n%q but expected \n%q", output, expected)
+ }
+}
+
+func TestNonSC(t *testing.T) {
+ t.Parallel()
+ // notice the syntax diff from 0.12, now comment delims must be added
+ CheckShortCodeMatch(t, "{{%/* movie 47238zzb */%}}", "{{% movie 47238zzb %}}", nil)
+}
+
+// Issue #929
+func TestHyphenatedSC(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+
+ tem.AddTemplate("_internal/shortcodes/hyphenated-video.html", `Playing Video {{ .Get 0 }}`)
+ return nil
+ }
+
+ CheckShortCodeMatch(t, "{{< hyphenated-video 47238zzb >}}", "Playing Video 47238zzb", wt)
+}
+
+// Issue #1753
+func TestNoTrailingNewline(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/a.html", `{{ .Get 0 }}`)
+ return nil
+ }
+
+ CheckShortCodeMatch(t, "ab{{< a c >}}d", "abcd", wt)
+}
+
+func TestPositionalParamSC(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 0 }}`)
+ return nil
+ }
+
+ CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video 47238zzb", wt)
+ CheckShortCodeMatch(t, "{{< video 47238zzb 132 >}}", "Playing Video 47238zzb", wt)
+ CheckShortCodeMatch(t, "{{<video 47238zzb>}}", "Playing Video 47238zzb", wt)
+ CheckShortCodeMatch(t, "{{<video 47238zzb >}}", "Playing Video 47238zzb", wt)
+ CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video 47238zzb", wt)
+}
+
+func TestPositionalParamIndexOutOfBounds(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 1 }}`)
+ return nil
+ }
+ CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video error: index out of range for positional param at position 1", wt)
+}
+
+// some repro issues for panics in Go Fuzz testing
+
+func TestNamedParamSC(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/img.html", `<img{{ with .Get "src" }} src="{{.}}"{{end}}{{with .Get "class"}} class="{{.}}"{{end}}>`)
+ return nil
+ }
+ CheckShortCodeMatch(t, `{{< img src="one" >}}`, `<img src="one">`, wt)
+ CheckShortCodeMatch(t, `{{< img class="aspen" >}}`, `<img class="aspen">`, wt)
+ CheckShortCodeMatch(t, `{{< img src= "one" >}}`, `<img src="one">`, wt)
+ CheckShortCodeMatch(t, `{{< img src ="one" >}}`, `<img src="one">`, wt)
+ CheckShortCodeMatch(t, `{{< img src = "one" >}}`, `<img src="one">`, wt)
+ CheckShortCodeMatch(t, `{{< img src = "one" class = "aspen grove" >}}`, `<img src="one" class="aspen grove">`, wt)
+}
+
+// Issue #2294
+func TestNestedNamedMissingParam(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/acc.html", `<div class="acc">{{ .Inner }}</div>`)
+ tem.AddTemplate("_internal/shortcodes/div.html", `<div {{with .Get "class"}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`)
+ tem.AddTemplate("_internal/shortcodes/div2.html", `<div {{with .Get 0}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`)
+ return nil
+ }
+ CheckShortCodeMatch(t,
+ `{{% acc %}}{{% div %}}d1{{% /div %}}{{% div2 %}}d2{{% /div2 %}}{{% /acc %}}`,
+ "<div class=\"acc\"><div >d1</div><div >d2</div>\n</div>", wt)
+}
+
+func TestIsNamedParamsSC(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/byposition.html", `<div id="{{ .Get 0 }}">`)
+ tem.AddTemplate("_internal/shortcodes/byname.html", `<div id="{{ .Get "id" }}">`)
+ tem.AddTemplate("_internal/shortcodes/ifnamedparams.html", `<div id="{{ if .IsNamedParams }}{{ .Get "id" }}{{ else }}{{ .Get 0 }}{{end}}">`)
+ return nil
+ }
+ CheckShortCodeMatch(t, `{{< ifnamedparams id="name" >}}`, `<div id="name">`, wt)
+ CheckShortCodeMatch(t, `{{< ifnamedparams position >}}`, `<div id="position">`, wt)
+ CheckShortCodeMatch(t, `{{< byname id="name" >}}`, `<div id="name">`, wt)
+ CheckShortCodeMatch(t, `{{< byname position >}}`, `<div id="error: cannot access positional params by string name">`, wt)
+ CheckShortCodeMatch(t, `{{< byposition position >}}`, `<div id="position">`, wt)
+ CheckShortCodeMatch(t, `{{< byposition id="name" >}}`, `<div id="error: cannot access named params by position">`, wt)
+}
+
+func TestInnerSC(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)
+ return nil
+ }
+ CheckShortCodeMatch(t, `{{< inside class="aspen" >}}`, `<div class="aspen"></div>`, wt)
+ CheckShortCodeMatch(t, `{{< inside class="aspen" >}}More Here{{< /inside >}}`, "<div class=\"aspen\">More Here</div>", wt)
+ CheckShortCodeMatch(t, `{{< inside >}}More Here{{< /inside >}}`, "<div>More Here</div>", wt)
+}
+
+func TestInnerSCWithMarkdown(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)
+ return nil
+ }
+ CheckShortCodeMatch(t, `{{% inside %}}
+# More Here
+
+[link](http://spf13.com) and text
+
+{{% /inside %}}`, "<div><h1 id=\"more-here\">More Here</h1>\n\n<p><a href=\"http://spf13.com\">link</a> and text</p>\n</div>", wt)
+}
+
+func TestInnerSCWithAndWithoutMarkdown(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)
+ return nil
+ }
+ CheckShortCodeMatch(t, `{{% inside %}}
+# More Here
+
+[link](http://spf13.com) and text
+
+{{% /inside %}}
+
+And then:
+
+{{< inside >}}
+# More Here
+
+This is **plain** text.
+
+{{< /inside >}}
+`, "<div><h1 id=\"more-here\">More Here</h1>\n\n<p><a href=\"http://spf13.com\">link</a> and text</p>\n</div>\n\n<p>And then:</p>\n\n<p><div>\n# More Here\n\nThis is **plain** text.\n\n</div>", wt)
+}
+
+func TestEmbeddedSC(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, "{{% test %}}", "This is a simple Test", nil)
+ CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" %}}`, "\n<figure class=\"bananas orange\">\n \n <img src=\"/found/here\" />\n \n \n</figure>\n", nil)
+ CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" caption="This is a caption" %}}`, "\n<figure class=\"bananas orange\">\n \n <img src=\"/found/here\" alt=\"This is a caption\" />\n \n \n <figcaption>\n <p>\n This is a caption\n \n \n \n </p> \n </figcaption>\n \n</figure>\n", nil)
+}
+
+func TestNestedSC(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/scn1.html", `<div>Outer, inner is {{ .Inner }}</div>`)
+ tem.AddTemplate("_internal/shortcodes/scn2.html", `<div>SC2</div>`)
+ return nil
+ }
+ CheckShortCodeMatch(t, `{{% scn1 %}}{{% scn2 %}}{{% /scn1 %}}`, "<div>Outer, inner is <div>SC2</div>\n</div>", wt)
+
+ CheckShortCodeMatch(t, `{{< scn1 >}}{{% scn2 %}}{{< /scn1 >}}`, "<div>Outer, inner is <div>SC2</div></div>", wt)
+}
+
+func TestNestedComplexSC(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/row.html", `-row-{{ .Inner}}-rowStop-`)
+ tem.AddTemplate("_internal/shortcodes/column.html", `-col-{{.Inner }}-colStop-`)
+ tem.AddTemplate("_internal/shortcodes/aside.html", `-aside-{{ .Inner }}-asideStop-`)
+ return nil
+ }
+ CheckShortCodeMatch(t, `{{< row >}}1-s{{% column %}}2-**s**{{< aside >}}3-**s**{{< /aside >}}4-s{{% /column %}}5-s{{< /row >}}6-s`,
+ "-row-1-s-col-2-<strong>s</strong>-aside-3-<strong>s</strong>-asideStop-4-s-colStop-5-s-rowStop-6-s", wt)
+
+ // turn around the markup flag
+ CheckShortCodeMatch(t, `{{% row %}}1-s{{< column >}}2-**s**{{% aside %}}3-**s**{{% /aside %}}4-s{{< /column >}}5-s{{% /row %}}6-s`,
+ "-row-1-s-col-2-<strong>s</strong>-aside-3-<strong>s</strong>-asideStop-4-s-colStop-5-s-rowStop-6-s", wt)
+}
+
+func TestParentShortcode(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/r1.html", `1: {{ .Get "pr1" }} {{ .Inner }}`)
+ tem.AddTemplate("_internal/shortcodes/r2.html", `2: {{ .Parent.Get "pr1" }}{{ .Get "pr2" }} {{ .Inner }}`)
+ tem.AddTemplate("_internal/shortcodes/r3.html", `3: {{ .Parent.Parent.Get "pr1" }}{{ .Parent.Get "pr2" }}{{ .Get "pr3" }} {{ .Inner }}`)
+ return nil
+ }
+ CheckShortCodeMatch(t, `{{< r1 pr1="p1" >}}1: {{< r2 pr2="p2" >}}2: {{< r3 pr3="p3" >}}{{< /r3 >}}{{< /r2 >}}{{< /r1 >}}`,
+ "1: p1 1: 2: p1p2 2: 3: p1p2p3 ", wt)
+
+}
+
+func TestFigureOnlySrc(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{< figure src="/found/here" >}}`, "\n<figure>\n \n <img src=\"/found/here\" />\n \n \n</figure>\n", nil)
+}
+
+func TestFigureImgWidth(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" alt="apple" width="100px" %}}`, "\n<figure class=\"bananas orange\">\n \n <img src=\"/found/here\" alt=\"apple\" width=\"100px\" />\n \n \n</figure>\n", nil)
+}
+
+func TestFigureImgHeight(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" alt="apple" height="100px" %}}`, "\n<figure class=\"bananas orange\">\n \n <img src=\"/found/here\" alt=\"apple\" height=\"100px\" />\n \n \n</figure>\n", nil)
+}
+
+func TestFigureImgWidthAndHeight(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" alt="apple" width="50" height="100" %}}`, "\n<figure class=\"bananas orange\">\n \n <img src=\"/found/here\" alt=\"apple\" width=\"50\" height=\"100\" />\n \n \n</figure>\n", nil)
+}
+
+func TestFigureLinkNoTarget(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{< figure src="/found/here" link="/jump/here/on/clicking" >}}`, "\n<figure>\n <a href=\"/jump/here/on/clicking\">\n <img src=\"/found/here\" />\n </a>\n \n</figure>\n", nil)
+}
+
+func TestFigureLinkWithTarget(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{< figure src="/found/here" link="/jump/here/on/clicking" target="_self" >}}`, "\n<figure>\n <a href=\"/jump/here/on/clicking\" target=\"_self\">\n <img src=\"/found/here\" />\n </a>\n \n</figure>\n", nil)
+}
+
+func TestFigureLinkWithTargetAndRel(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{< figure src="/found/here" link="/jump/here/on/clicking" target="_blank" rel="noopener" >}}`, "\n<figure>\n <a href=\"/jump/here/on/clicking\" target=\"_blank\" rel=\"noopener\">\n <img src=\"/found/here\" />\n </a>\n \n</figure>\n", nil)
+}
+
+const testScPlaceholderRegexp = "HAHAHUGOSHORTCODE-\\d+HBHB"
+
+func TestExtractShortcodes(t *testing.T) {
+ t.Parallel()
+ for i, this := range []struct {
+ name string
+ input string
+ expectShortCodes string
+ expect interface{}
+ expectErrorMsg string
+ }{
+ {"text", "Some text.", "map[]", "Some text.", ""},
+ {"invalid right delim", "{{< tag }}", "", false, "simple.md:4:.*unrecognized character.*}"},
+ {"invalid close", "\n{{< /tag >}}", "", false, "simple.md:5:.*got closing shortcode, but none is open"},
+ {"invalid close2", "\n\n{{< tag >}}{{< /anotherTag >}}", "", false, "simple.md:6: closing tag for shortcode 'anotherTag' does not match start tag"},
+ {"unterminated quote 1", `{{< figure src="im caption="S" >}}`, "", false, "simple.md:4:.got pos.*"},
+ {"unterminated quote 1", `{{< figure src="im" caption="S >}}`, "", false, "simple.md:4:.*unterm.*}"},
+ {"one shortcode, no markup", "{{< tag >}}", "", testScPlaceholderRegexp, ""},
+ {"one shortcode, markup", "{{% tag %}}", "", testScPlaceholderRegexp, ""},
+ {"one pos param", "{{% tag param1 %}}", `tag([\"param1\"], true){[]}"]`, testScPlaceholderRegexp, ""},
+ {"two pos params", "{{< tag param1 param2>}}", `tag([\"param1\" \"param2\"], false){[]}"]`, testScPlaceholderRegexp, ""},
+ {"one named param", `{{% tag param1="value" %}}`, `tag([\"param1:value\"], true){[]}`, testScPlaceholderRegexp, ""},
+ {"two named params", `{{< tag param1="value1" param2="value2" >}}`, `tag([\"param1:value1\" \"param2:value2\"], false){[]}"]`,
+ testScPlaceholderRegexp, ""},
+ {"inner", `Some text. {{< inner >}}Inner Content{{< / inner >}}. Some more text.`, `inner([], false){[Inner Content]}`,
+ fmt.Sprintf("Some text. %s. Some more text.", testScPlaceholderRegexp), ""},
+ // issue #934
+ {"inner self-closing", `Some text. {{< inner />}}. Some more text.`, `inner([], false){[]}`,
+ fmt.Sprintf("Some text. %s. Some more text.", testScPlaceholderRegexp), ""},
+ {"close, but not inner", "{{< tag >}}foo{{< /tag >}}", "", false, "Shortcode 'tag' in page 'simple.md' has no .Inner.*"},
+ {"nested inner", `Inner->{{< inner >}}Inner Content->{{% inner2 param1 %}}inner2txt{{% /inner2 %}}Inner close->{{< / inner >}}<-done`,
+ `inner([], false){[Inner Content-> inner2([\"param1\"], true){[inner2txt]} Inner close->]}`,
+ fmt.Sprintf("Inner->%s<-done", testScPlaceholderRegexp), ""},
+ {"nested, nested inner", `Inner->{{< inner >}}inner2->{{% inner2 param1 %}}inner2txt->inner3{{< inner3>}}inner3txt{{</ inner3 >}}{{% /inner2 %}}final close->{{< / inner >}}<-done`,
+ `inner([], false){[inner2-> inner2([\"param1\"], true){[inner2txt->inner3 inner3(%!q(<nil>), false){[inner3txt]}]} final close->`,
+ fmt.Sprintf("Inner->%s<-done", testScPlaceholderRegexp), ""},
+ {"two inner", `Some text. {{% inner %}}First **Inner** Content{{% / inner %}} {{< inner >}}Inner **Content**{{< / inner >}}. Some more text.`,
+ `map["HAHAHUGOSHORTCODE-1HBHB:inner([], true){[First **Inner** Content]}" "HAHAHUGOSHORTCODE-2HBHB:inner([], false){[Inner **Content**]}"]`,
+ fmt.Sprintf("Some text. %s %s. Some more text.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""},
+ {"closed without content", `Some text. {{< inner param1 >}}{{< / inner >}}. Some more text.`, `inner([\"param1\"], false){[]}`,
+ fmt.Sprintf("Some text. %s. Some more text.", testScPlaceholderRegexp), ""},
+ {"two shortcodes", "{{< sc1 >}}{{< sc2 >}}",
+ `map["HAHAHUGOSHORTCODE-1HBHB:sc1([], false){[]}" "HAHAHUGOSHORTCODE-2HBHB:sc2([], false){[]}"]`,
+ testScPlaceholderRegexp + testScPlaceholderRegexp, ""},
+ {"mix of shortcodes", `Hello {{< sc1 >}}world{{% sc2 p2="2"%}}. And that's it.`,
+ `map["HAHAHUGOSHORTCODE-1HBHB:sc1([], false){[]}" "HAHAHUGOSHORTCODE-2HBHB:sc2([\"p2:2\"]`,
+ fmt.Sprintf("Hello %sworld%s. And that's it.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""},
+ {"mix with inner", `Hello {{< sc1 >}}world{{% inner p2="2"%}}Inner{{%/ inner %}}. And that's it.`,
+ `map["HAHAHUGOSHORTCODE-1HBHB:sc1([], false){[]}" "HAHAHUGOSHORTCODE-2HBHB:inner([\"p2:2\"], true){[Inner]}"]`,
+ fmt.Sprintf("Hello %sworld%s. And that's it.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""},
+ } {
+
+ p, _ := pageFromString(simplePage, "simple.md", func(templ tpl.TemplateHandler) error {
+ templ.AddTemplate("_internal/shortcodes/tag.html", `tag`)
+ templ.AddTemplate("_internal/shortcodes/sc1.html", `sc1`)
+ templ.AddTemplate("_internal/shortcodes/sc2.html", `sc2`)
+ templ.AddTemplate("_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`)
+ templ.AddTemplate("_internal/shortcodes/inner2.html", `{{.Inner}}`)
+ templ.AddTemplate("_internal/shortcodes/inner3.html", `{{.Inner}}`)
+ return nil
+ })
+
+ s := newShortcodeHandler(p)
+ content, err := s.extractShortcodes(this.input, p)
+
+ if b, ok := this.expect.(bool); ok && !b {
+ if err == nil {
+ t.Fatalf("[%d] %s: ExtractShortcodes didn't return an expected error", i, this.name)
+ } else {
+ r, _ := regexp.Compile(this.expectErrorMsg)
+ if !r.MatchString(err.Error()) {
+ t.Fatalf("[%d] %s: ExtractShortcodes didn't return an expected error message, got %s but expected %s",
+ i, this.name, err.Error(), this.expectErrorMsg)
+ }
+ }
+ continue
+ } else {
+ if err != nil {
+ t.Fatalf("[%d] %s: failed: %q", i, this.name, err)
+ }
+ }
+
+ shortCodes := s.shortcodes
+
+ var expected string
+ av := reflect.ValueOf(this.expect)
+ switch av.Kind() {
+ case reflect.String:
+ expected = av.String()
+ }
+
+ r, err := regexp.Compile(expected)
+
+ if err != nil {
+ t.Fatalf("[%d] %s: Failed to compile regexp %q: %q", i, this.name, expected, err)
+ }
+
+ if strings.Count(content, shortcodePlaceholderPrefix) != len(shortCodes) {
+ t.Fatalf("[%d] %s: Not enough placeholders, found %d", i, this.name, len(shortCodes))
+ }
+
+ if !r.MatchString(content) {
+ t.Fatalf("[%d] %s: Shortcode extract didn't match. got %q but expected %q", i, this.name, content, expected)
+ }
+
+ for placeHolder, sc := range shortCodes {
+ if !strings.Contains(content, placeHolder) {
+ t.Fatalf("[%d] %s: Output does not contain placeholder %q", i, this.name, placeHolder)
+ }
+
+ if sc.params == nil {
+ t.Fatalf("[%d] %s: Params is nil for shortcode '%s'", i, this.name, sc.name)
+ }
+ }
+
+ if this.expectShortCodes != "" {
+ shortCodesAsStr := fmt.Sprintf("map%q", collectAndSortShortcodes(shortCodes))
+ if !strings.Contains(shortCodesAsStr, this.expectShortCodes) {
+ t.Fatalf("[%d] %s: Shortcodes not as expected, got %s but expected %s", i, this.name, shortCodesAsStr, this.expectShortCodes)
+ }
+ }
+ }
+}
+
+func TestShortcodesInSite(t *testing.T) {
+ t.Parallel()
+ baseURL := "http://foo/bar"
+
+ tests := []struct {
+ contentPath string
+ content string
+ outFile string
+ expected string
+ }{
+ {"sect/doc1.md", `a{{< b >}}c`,
+ filepath.FromSlash("public/sect/doc1/index.html"), "<p>abc</p>\n"},
+ // Issue #1642: Multiple shortcodes wrapped in P
+ // Deliberately forced to pass even if they maybe shouldn't.
+ {"sect/doc2.md", `a
+
+{{< b >}}
+{{< c >}}
+{{< d >}}
+
+e`,
+ filepath.FromSlash("public/sect/doc2/index.html"),
+ "<p>a</p>\n\n<p>b<br />\nc\nd</p>\n\n<p>e</p>\n"},
+ {"sect/doc3.md", `a
+
+{{< b >}}
+{{< c >}}
+
+{{< d >}}
+
+e`,
+ filepath.FromSlash("public/sect/doc3/index.html"),
+ "<p>a</p>\n\n<p>b<br />\nc</p>\n\nd\n\n<p>e</p>\n"},
+ {"sect/doc4.md", `a
+{{< b >}}
+{{< b >}}
+{{< b >}}
+{{< b >}}
+{{< b >}}
+
+
+
+
+
+
+
+
+
+
+`,
+ filepath.FromSlash("public/sect/doc4/index.html"),
+ "<p>a\nb\nb\nb\nb\nb</p>\n"},
+ // #2192 #2209: Shortcodes in markdown headers
+ {"sect/doc5.md", `# {{< b >}}
+## {{% c %}}`,
+ filepath.FromSlash("public/sect/doc5/index.html"), "\n\n<h1 id=\"hahahugoshortcode-1hbhb\">b</h1>\n\n<h2 id=\"hahahugoshortcode-2hbhb\">c</h2>\n"},
+ // #2223 pygments
+ {"sect/doc6.md", "\n```bash\nb = {{< b >}} c = {{% c %}}\n```\n",
+ filepath.FromSlash("public/sect/doc6/index.html"),
+ `<span class="nv">b</span>`},
+ // #2249
+ {"sect/doc7.ad", `_Shortcodes:_ *b: {{< b >}} c: {{% c %}}*`,
+ filepath.FromSlash("public/sect/doc7/index.html"),
+ "<div class=\"paragraph\">\n<p><em>Shortcodes:</em> <strong>b: b c: c</strong></p>\n</div>\n"},
+ {"sect/doc8.rst", `**Shortcodes:** *b: {{< b >}} c: {{% c %}}*`,
+ filepath.FromSlash("public/sect/doc8/index.html"),
+ "<div class=\"document\">\n\n\n<p><strong>Shortcodes:</strong> <em>b: b c: c</em></p>\n</div>"},
+ {"sect/doc9.mmark", `
+---
+menu:
+ main:
+ parent: 'parent'
+---
+**Shortcodes:** *b: {{< b >}} c: {{% c %}}*`,
+ filepath.FromSlash("public/sect/doc9/index.html"),
+ "<p><strong>Shortcodes:</strong> <em>b: b c: c</em></p>\n"},
+ // Issue #1229: Menus not available in shortcode.
+ {"sect/doc10.md", `---
+menu:
+ main:
+ identifier: 'parent'
+tags:
+- Menu
+---
+**Menus:** {{< menu >}}`,
+ filepath.FromSlash("public/sect/doc10/index.html"),
+ "<p><strong>Menus:</strong> 1</p>\n"},
+ // Issue #2323: Taxonomies not available in shortcode.
+ {"sect/doc11.md", `---
+tags:
+- Bugs
+---
+**Tags:** {{< tags >}}`,
+ filepath.FromSlash("public/sect/doc11/index.html"),
+ "<p><strong>Tags:</strong> 2</p>\n"},
+ }
+
+ sources := make([][2]string, len(tests))
+
+ for i, test := range tests {
+ sources[i] = [2]string{filepath.FromSlash(test.contentPath), test.content}
+ }
+
+ addTemplates := func(templ tpl.TemplateHandler) error {
+ templ.AddTemplate("_default/single.html", "{{.Content}}")
+
+ templ.AddTemplate("_internal/shortcodes/b.html", `b`)
+ templ.AddTemplate("_internal/shortcodes/c.html", `c`)
+ templ.AddTemplate("_internal/shortcodes/d.html", `d`)
+ templ.AddTemplate("_internal/shortcodes/menu.html", `{{ len (index .Page.Menus "main").Children }}`)
+ templ.AddTemplate("_internal/shortcodes/tags.html", `{{ len .Page.Site.Taxonomies.tags }}`)
+
+ return nil
+
+ }
+
+ cfg, fs := newTestCfg()
+
+ cfg.Set("defaultContentLanguage", "en")
+ cfg.Set("baseURL", baseURL)
+ cfg.Set("uglyURLs", false)
+ cfg.Set("verbose", true)
+
+ cfg.Set("pygmentsUseClasses", true)
+ cfg.Set("pygmentsCodefences", true)
+
+ writeSourcesToSource(t, "content", fs, sources...)
+
+ s := buildSingleSite(t, deps.DepsCfg{WithTemplate: addTemplates, Fs: fs, Cfg: cfg}, BuildCfg{})
+ th := testHelper{s.Cfg, s.Fs, t}
+
+ for _, test := range tests {
+ if strings.HasSuffix(test.contentPath, ".ad") && !helpers.HasAsciidoc() {
+ fmt.Println("Skip Asciidoc test case as no Asciidoc present.")
+ continue
+ } else if strings.HasSuffix(test.contentPath, ".rst") && !helpers.HasRst() {
+ fmt.Println("Skip Rst test case as no rst2html present.")
+ continue
+ } else if strings.Contains(test.expected, "code") {
+ fmt.Println("Skip Pygments test case as no pygments present.")
+ continue
+ }
+
+ th.assertFileContent(test.outFile, test.expected)
+ }
+
+}
+
+func TestShortcodeMultipleOutputFormats(t *testing.T) {
+ t.Parallel()
+
+ siteConfig := `
+baseURL = "http://example.com/blog"
+
+paginate = 1
+
+disableKinds = ["section", "taxonomy", "taxonomyTerm", "RSS", "sitemap", "robotsTXT", "404"]
+
+[outputs]
+home = [ "HTML", "AMP", "Calendar" ]
+page = [ "HTML", "AMP", "JSON" ]
+
+`
+
+ pageTemplate := `---
+title: "%s"
+---
+# Doc
+
+{{< myShort >}}
+{{< noExt >}}
+{{%% onlyHTML %%}}
+
+{{< myInner >}}{{< myShort >}}{{< /myInner >}}
+
+`
+
+ pageTemplateCSVOnly := `---
+title: "%s"
+outputs: ["CSV"]
+---
+# Doc
+
+CSV: {{< myShort >}}
+`
+
+ pageTemplateShortcodeNotFound := `---
+title: "%s"
+outputs: ["CSV"]
+---
+# Doc
+
+NotFound: {{< thisDoesNotExist >}}
+`
+
+ mf := afero.NewMemMapFs()
+
+ th, h := newTestSitesFromConfig(t, mf, siteConfig,
+ "layouts/_default/single.html", `Single HTML: {{ .Title }}|{{ .Content }}`,
+ "layouts/_default/single.json", `Single JSON: {{ .Title }}|{{ .Content }}`,
+ "layouts/_default/single.csv", `Single CSV: {{ .Title }}|{{ .Content }}`,
+ "layouts/index.html", `Home HTML: {{ .Title }}|{{ .Content }}`,
+ "layouts/index.amp.html", `Home AMP: {{ .Title }}|{{ .Content }}`,
+ "layouts/index.ics", `Home Calendar: {{ .Title }}|{{ .Content }}`,
+ "layouts/shortcodes/myShort.html", `ShortHTML`,
+ "layouts/shortcodes/myShort.amp.html", `ShortAMP`,
+ "layouts/shortcodes/myShort.csv", `ShortCSV`,
+ "layouts/shortcodes/myShort.ics", `ShortCalendar`,
+ "layouts/shortcodes/myShort.json", `ShortJSON`,
+ "layouts/shortcodes/noExt", `ShortNoExt`,
+ "layouts/shortcodes/onlyHTML.html", `ShortOnlyHTML`,
+ "layouts/shortcodes/myInner.html", `myInner:--{{- .Inner -}}--`,
+ )
+
+ fs := th.Fs
+
+ writeSource(t, fs, "content/_index.md", fmt.Sprintf(pageTemplate, "Home"))
+ writeSource(t, fs, "content/sect/mypage.md", fmt.Sprintf(pageTemplate, "Single"))
+ writeSource(t, fs, "content/sect/mycsvpage.md", fmt.Sprintf(pageTemplateCSVOnly, "Single CSV"))
+ writeSource(t, fs, "content/sect/notfound.md", fmt.Sprintf(pageTemplateShortcodeNotFound, "Single CSV"))
+
+ require.NoError(t, h.Build(BuildCfg{}))
+ require.Len(t, h.Sites, 1)
+
+ s := h.Sites[0]
+ home := s.getPage(KindHome)
+ require.NotNil(t, home)
+ require.Len(t, home.outputFormats, 3)
+
+ th.assertFileContent("public/index.html",
+ "Home HTML",
+ "ShortHTML",
+ "ShortNoExt",
+ "ShortOnlyHTML",
+ "myInner:--ShortHTML--",
+ )
+
+ th.assertFileContent("public/amp/index.html",
+ "Home AMP",
+ "ShortAMP",
+ "ShortNoExt",
+ "ShortOnlyHTML",
+ "myInner:--ShortAMP--",
+ )
+
+ th.assertFileContent("public/index.ics",
+ "Home Calendar",
+ "ShortCalendar",
+ "ShortNoExt",
+ "ShortOnlyHTML",
+ "myInner:--ShortCalendar--",
+ )
+
+ th.assertFileContent("public/sect/mypage/index.html",
+ "Single HTML",
+ "ShortHTML",
+ "ShortNoExt",
+ "ShortOnlyHTML",
+ "myInner:--ShortHTML--",
+ )
+
+ th.assertFileContent("public/sect/mypage/index.json",
+ "Single JSON",
+ "ShortJSON",
+ "ShortNoExt",
+ "ShortOnlyHTML",
+ "myInner:--ShortJSON--",
+ )
+
+ th.assertFileContent("public/amp/sect/mypage/index.html",
+ // No special AMP template
+ "Single HTML",
+ "ShortAMP",
+ "ShortNoExt",
+ "ShortOnlyHTML",
+ "myInner:--ShortAMP--",
+ )
+
+ th.assertFileContent("public/sect/mycsvpage/index.csv",
+ "Single CSV",
+ "ShortCSV",
+ )
+
+ th.assertFileContent("public/sect/notfound/index.csv",
+ "NotFound:",
+ "thisDoesNotExist",
+ )
+
+ require.Equal(t, uint64(1), s.Log.LogCountForLevel(jww.LevelError))
+
+}
+
+func collectAndSortShortcodes(shortcodes map[string]shortcode) []string {
+ var asArray []string
+
+ for key, sc := range shortcodes {
+ asArray = append(asArray, fmt.Sprintf("%s:%s", key, sc))
+ }
+
+ sort.Strings(asArray)
+ return asArray
+
+}
+
+func BenchmarkReplaceShortcodeTokens(b *testing.B) {
+
+ type input struct {
+ in []byte
+ replacements map[string]string
+ expect []byte
+ }
+
+ data := []struct {
+ input string
+ replacements map[string]string
+ expect []byte
+ }{
+ {"Hello HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, []byte("Hello World.")},
+ {strings.Repeat("A", 100) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("A", 100) + " Hello World.")},
+ {strings.Repeat("A", 500) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("A", 500) + " Hello World.")},
+ {strings.Repeat("ABCD ", 500) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("ABCD ", 500) + " Hello World.")},
+ {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.")},
+ }
+
+ var in = make([]input, b.N*len(data))
+ var cnt = 0
+ for i := 0; i <