github.com/olliephillips/hugo@v0.42.2/hugolib/shortcode_test.go (about)

     1  // Copyright 2016 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  	"regexp"
    21  	"sort"
    22  	"strings"
    23  	"testing"
    24  
    25  	"github.com/spf13/viper"
    26  
    27  	jww "github.com/spf13/jwalterweatherman"
    28  
    29  	"github.com/spf13/afero"
    30  
    31  	"github.com/gohugoio/hugo/output"
    32  
    33  	"github.com/gohugoio/hugo/media"
    34  
    35  	"github.com/gohugoio/hugo/deps"
    36  	"github.com/gohugoio/hugo/helpers"
    37  	"github.com/gohugoio/hugo/langs"
    38  	"github.com/gohugoio/hugo/tpl"
    39  
    40  	"github.com/stretchr/testify/require"
    41  )
    42  
    43  // TODO(bep) remove
    44  func pageFromString(in, filename string, withTemplate ...func(templ tpl.TemplateHandler) error) (*Page, error) {
    45  	s := newTestSite(nil)
    46  	if len(withTemplate) > 0 {
    47  		// Have to create a new site
    48  		var err error
    49  		cfg, fs := newTestCfg()
    50  
    51  		d := deps.DepsCfg{Language: langs.NewLanguage("en", cfg), Cfg: cfg, Fs: fs, WithTemplate: withTemplate[0]}
    52  
    53  		s, err = NewSiteForCfg(d)
    54  		if err != nil {
    55  			return nil, err
    56  		}
    57  	}
    58  	return s.NewPageFrom(strings.NewReader(in), filename)
    59  }
    60  
    61  func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error) {
    62  	CheckShortCodeMatchAndError(t, input, expected, withTemplate, false)
    63  }
    64  
    65  func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error, expectError bool) {
    66  
    67  	cfg, fs := newTestCfg()
    68  
    69  	// Need some front matter, see https://github.com/gohugoio/hugo/issues/2337
    70  	contentFile := `---
    71  title: "Title"
    72  ---
    73  ` + input
    74  
    75  	writeSource(t, fs, "content/simple.md", contentFile)
    76  
    77  	h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate})
    78  
    79  	require.NoError(t, err)
    80  	require.Len(t, h.Sites, 1)
    81  
    82  	err = h.Build(BuildCfg{})
    83  
    84  	if err != nil && !expectError {
    85  		t.Fatalf("Shortcode rendered error %s.", err)
    86  	}
    87  
    88  	if err == nil && expectError {
    89  		t.Fatalf("No error from shortcode")
    90  	}
    91  
    92  	require.Len(t, h.Sites[0].RegularPages, 1)
    93  
    94  	output := strings.TrimSpace(string(h.Sites[0].RegularPages[0].content()))
    95  	output = strings.TrimPrefix(output, "<p>")
    96  	output = strings.TrimSuffix(output, "</p>")
    97  
    98  	expected = strings.TrimSpace(expected)
    99  
   100  	if output != expected {
   101  		t.Fatalf("Shortcode render didn't match. got \n%q but expected \n%q", output, expected)
   102  	}
   103  }
   104  
   105  func TestNonSC(t *testing.T) {
   106  	t.Parallel()
   107  	// notice the syntax diff from 0.12, now comment delims must be added
   108  	CheckShortCodeMatch(t, "{{%/* movie 47238zzb */%}}", "{{% movie 47238zzb %}}", nil)
   109  }
   110  
   111  // Issue #929
   112  func TestHyphenatedSC(t *testing.T) {
   113  	t.Parallel()
   114  	wt := func(tem tpl.TemplateHandler) error {
   115  
   116  		tem.AddTemplate("_internal/shortcodes/hyphenated-video.html", `Playing Video {{ .Get 0 }}`)
   117  		return nil
   118  	}
   119  
   120  	CheckShortCodeMatch(t, "{{< hyphenated-video 47238zzb >}}", "Playing Video 47238zzb", wt)
   121  }
   122  
   123  // Issue #1753
   124  func TestNoTrailingNewline(t *testing.T) {
   125  	t.Parallel()
   126  	wt := func(tem tpl.TemplateHandler) error {
   127  		tem.AddTemplate("_internal/shortcodes/a.html", `{{ .Get 0 }}`)
   128  		return nil
   129  	}
   130  
   131  	CheckShortCodeMatch(t, "ab{{< a c >}}d", "abcd", wt)
   132  }
   133  
   134  func TestPositionalParamSC(t *testing.T) {
   135  	t.Parallel()
   136  	wt := func(tem tpl.TemplateHandler) error {
   137  		tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 0 }}`)
   138  		return nil
   139  	}
   140  
   141  	CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video 47238zzb", wt)
   142  	CheckShortCodeMatch(t, "{{< video 47238zzb 132 >}}", "Playing Video 47238zzb", wt)
   143  	CheckShortCodeMatch(t, "{{<video 47238zzb>}}", "Playing Video 47238zzb", wt)
   144  	CheckShortCodeMatch(t, "{{<video 47238zzb    >}}", "Playing Video 47238zzb", wt)
   145  	CheckShortCodeMatch(t, "{{<   video   47238zzb    >}}", "Playing Video 47238zzb", wt)
   146  }
   147  
   148  func TestPositionalParamIndexOutOfBounds(t *testing.T) {
   149  	t.Parallel()
   150  	wt := func(tem tpl.TemplateHandler) error {
   151  		tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ with .Get 1 }}{{ . }}{{ else }}Missing{{ end }}`)
   152  		return nil
   153  	}
   154  	CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video Missing", wt)
   155  }
   156  
   157  // some repro issues for panics in Go Fuzz testing
   158  
   159  func TestNamedParamSC(t *testing.T) {
   160  	t.Parallel()
   161  	wt := func(tem tpl.TemplateHandler) error {
   162  		tem.AddTemplate("_internal/shortcodes/img.html", `<img{{ with .Get "src" }} src="{{.}}"{{end}}{{with .Get "class"}} class="{{.}}"{{end}}>`)
   163  		return nil
   164  	}
   165  	CheckShortCodeMatch(t, `{{< img src="one" >}}`, `<img src="one">`, wt)
   166  	CheckShortCodeMatch(t, `{{< img class="aspen" >}}`, `<img class="aspen">`, wt)
   167  	CheckShortCodeMatch(t, `{{< img src= "one" >}}`, `<img src="one">`, wt)
   168  	CheckShortCodeMatch(t, `{{< img src ="one" >}}`, `<img src="one">`, wt)
   169  	CheckShortCodeMatch(t, `{{< img src = "one" >}}`, `<img src="one">`, wt)
   170  	CheckShortCodeMatch(t, `{{< img src = "one" class = "aspen grove" >}}`, `<img src="one" class="aspen grove">`, wt)
   171  }
   172  
   173  // Issue #2294
   174  func TestNestedNamedMissingParam(t *testing.T) {
   175  	t.Parallel()
   176  	wt := func(tem tpl.TemplateHandler) error {
   177  		tem.AddTemplate("_internal/shortcodes/acc.html", `<div class="acc">{{ .Inner }}</div>`)
   178  		tem.AddTemplate("_internal/shortcodes/div.html", `<div {{with .Get "class"}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`)
   179  		tem.AddTemplate("_internal/shortcodes/div2.html", `<div {{with .Get 0}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`)
   180  		return nil
   181  	}
   182  	CheckShortCodeMatch(t,
   183  		`{{% acc %}}{{% div %}}d1{{% /div %}}{{% div2 %}}d2{{% /div2 %}}{{% /acc %}}`,
   184  		"<div class=\"acc\"><div >d1</div><div >d2</div>\n</div>", wt)
   185  }
   186  
   187  func TestIsNamedParamsSC(t *testing.T) {
   188  	t.Parallel()
   189  	wt := func(tem tpl.TemplateHandler) error {
   190  		tem.AddTemplate("_internal/shortcodes/bynameorposition.html", `{{ with .Get "id" }}Named: {{ . }}{{ else }}Pos: {{ .Get 0 }}{{ end }}`)
   191  		tem.AddTemplate("_internal/shortcodes/ifnamedparams.html", `<div id="{{ if .IsNamedParams }}{{ .Get "id" }}{{ else }}{{ .Get 0 }}{{end}}">`)
   192  		return nil
   193  	}
   194  	CheckShortCodeMatch(t, `{{< ifnamedparams id="name" >}}`, `<div id="name">`, wt)
   195  	CheckShortCodeMatch(t, `{{< ifnamedparams position >}}`, `<div id="position">`, wt)
   196  	CheckShortCodeMatch(t, `{{< bynameorposition id="name" >}}`, `Named: name`, wt)
   197  	CheckShortCodeMatch(t, `{{< bynameorposition position >}}`, `Pos: position`, wt)
   198  }
   199  
   200  func TestInnerSC(t *testing.T) {
   201  	t.Parallel()
   202  	wt := func(tem tpl.TemplateHandler) error {
   203  		tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)
   204  		return nil
   205  	}
   206  	CheckShortCodeMatch(t, `{{< inside class="aspen" >}}`, `<div class="aspen"></div>`, wt)
   207  	CheckShortCodeMatch(t, `{{< inside class="aspen" >}}More Here{{< /inside >}}`, "<div class=\"aspen\">More Here</div>", wt)
   208  	CheckShortCodeMatch(t, `{{< inside >}}More Here{{< /inside >}}`, "<div>More Here</div>", wt)
   209  }
   210  
   211  func TestInnerSCWithMarkdown(t *testing.T) {
   212  	t.Parallel()
   213  	wt := func(tem tpl.TemplateHandler) error {
   214  		tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)
   215  		return nil
   216  	}
   217  	CheckShortCodeMatch(t, `{{% inside %}}
   218  # More Here
   219  
   220  [link](http://spf13.com) and text
   221  
   222  {{% /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)
   223  }
   224  
   225  func TestInnerSCWithAndWithoutMarkdown(t *testing.T) {
   226  	t.Parallel()
   227  	wt := func(tem tpl.TemplateHandler) error {
   228  		tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)
   229  		return nil
   230  	}
   231  	CheckShortCodeMatch(t, `{{% inside %}}
   232  # More Here
   233  
   234  [link](http://spf13.com) and text
   235  
   236  {{% /inside %}}
   237  
   238  And then:
   239  
   240  {{< inside >}}
   241  # More Here
   242  
   243  This is **plain** text.
   244  
   245  {{< /inside >}}
   246  `, "<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)
   247  }
   248  
   249  func TestEmbeddedSC(t *testing.T) {
   250  	t.Parallel()
   251  	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)
   252  	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)
   253  }
   254  
   255  func TestNestedSC(t *testing.T) {
   256  	t.Parallel()
   257  	wt := func(tem tpl.TemplateHandler) error {
   258  		tem.AddTemplate("_internal/shortcodes/scn1.html", `<div>Outer, inner is {{ .Inner }}</div>`)
   259  		tem.AddTemplate("_internal/shortcodes/scn2.html", `<div>SC2</div>`)
   260  		return nil
   261  	}
   262  	CheckShortCodeMatch(t, `{{% scn1 %}}{{% scn2 %}}{{% /scn1 %}}`, "<div>Outer, inner is <div>SC2</div>\n</div>", wt)
   263  
   264  	CheckShortCodeMatch(t, `{{< scn1 >}}{{% scn2 %}}{{< /scn1 >}}`, "<div>Outer, inner is <div>SC2</div></div>", wt)
   265  }
   266  
   267  func TestNestedComplexSC(t *testing.T) {
   268  	t.Parallel()
   269  	wt := func(tem tpl.TemplateHandler) error {
   270  		tem.AddTemplate("_internal/shortcodes/row.html", `-row-{{ .Inner}}-rowStop-`)
   271  		tem.AddTemplate("_internal/shortcodes/column.html", `-col-{{.Inner    }}-colStop-`)
   272  		tem.AddTemplate("_internal/shortcodes/aside.html", `-aside-{{    .Inner  }}-asideStop-`)
   273  		return nil
   274  	}
   275  	CheckShortCodeMatch(t, `{{< row >}}1-s{{% column %}}2-**s**{{< aside >}}3-**s**{{< /aside >}}4-s{{% /column %}}5-s{{< /row >}}6-s`,
   276  		"-row-1-s-col-2-<strong>s</strong>-aside-3-<strong>s</strong>-asideStop-4-s-colStop-5-s-rowStop-6-s", wt)
   277  
   278  	// turn around the markup flag
   279  	CheckShortCodeMatch(t, `{{% row %}}1-s{{< column >}}2-**s**{{% aside %}}3-**s**{{% /aside %}}4-s{{< /column >}}5-s{{% /row %}}6-s`,
   280  		"-row-1-s-col-2-<strong>s</strong>-aside-3-<strong>s</strong>-asideStop-4-s-colStop-5-s-rowStop-6-s", wt)
   281  }
   282  
   283  func TestParentShortcode(t *testing.T) {
   284  	t.Parallel()
   285  	wt := func(tem tpl.TemplateHandler) error {
   286  		tem.AddTemplate("_internal/shortcodes/r1.html", `1: {{ .Get "pr1" }} {{ .Inner }}`)
   287  		tem.AddTemplate("_internal/shortcodes/r2.html", `2: {{ .Parent.Get "pr1" }}{{ .Get "pr2" }} {{ .Inner }}`)
   288  		tem.AddTemplate("_internal/shortcodes/r3.html", `3: {{ .Parent.Parent.Get "pr1" }}{{ .Parent.Get "pr2" }}{{ .Get "pr3" }} {{ .Inner }}`)
   289  		return nil
   290  	}
   291  	CheckShortCodeMatch(t, `{{< r1 pr1="p1" >}}1: {{< r2 pr2="p2" >}}2: {{< r3 pr3="p3" >}}{{< /r3 >}}{{< /r2 >}}{{< /r1 >}}`,
   292  		"1: p1 1: 2: p1p2 2: 3: p1p2p3 ", wt)
   293  
   294  }
   295  
   296  func TestFigureOnlySrc(t *testing.T) {
   297  	t.Parallel()
   298  	CheckShortCodeMatch(t, `{{< figure src="/found/here" >}}`, "\n<figure>\n    \n        <img src=\"/found/here\" />\n    \n    \n</figure>\n", nil)
   299  }
   300  
   301  func TestFigureImgWidth(t *testing.T) {
   302  	t.Parallel()
   303  	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)
   304  }
   305  
   306  func TestFigureImgHeight(t *testing.T) {
   307  	t.Parallel()
   308  	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)
   309  }
   310  
   311  func TestFigureImgWidthAndHeight(t *testing.T) {
   312  	t.Parallel()
   313  	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)
   314  }
   315  
   316  func TestFigureLinkNoTarget(t *testing.T) {
   317  	t.Parallel()
   318  	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)
   319  }
   320  
   321  func TestFigureLinkWithTarget(t *testing.T) {
   322  	t.Parallel()
   323  	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)
   324  }
   325  
   326  func TestFigureLinkWithTargetAndRel(t *testing.T) {
   327  	t.Parallel()
   328  	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)
   329  }
   330  
   331  const testScPlaceholderRegexp = "HAHAHUGOSHORTCODE-\\d+HBHB"
   332  
   333  func TestExtractShortcodes(t *testing.T) {
   334  	t.Parallel()
   335  	for i, this := range []struct {
   336  		name             string
   337  		input            string
   338  		expectShortCodes string
   339  		expect           interface{}
   340  		expectErrorMsg   string
   341  	}{
   342  		{"text", "Some text.", "map[]", "Some text.", ""},
   343  		{"invalid right delim", "{{< tag }}", "", false, "simple.md:4:.*unrecognized character.*}"},
   344  		{"invalid close", "\n{{< /tag >}}", "", false, "simple.md:5:.*got closing shortcode, but none is open"},
   345  		{"invalid close2", "\n\n{{< tag >}}{{< /anotherTag >}}", "", false, "simple.md:6: closing tag for shortcode 'anotherTag' does not match start tag"},
   346  		{"unterminated quote 1", `{{< figure src="im caption="S" >}}`, "", false, "simple.md:4:.got pos.*"},
   347  		{"unterminated quote 1", `{{< figure src="im" caption="S >}}`, "", false, "simple.md:4:.*unterm.*}"},
   348  		{"one shortcode, no markup", "{{< tag >}}", "", testScPlaceholderRegexp, ""},
   349  		{"one shortcode, markup", "{{% tag %}}", "", testScPlaceholderRegexp, ""},
   350  		{"one pos param", "{{% tag param1 %}}", `tag([\"param1\"], true){[]}"]`, testScPlaceholderRegexp, ""},
   351  		{"two pos params", "{{< tag param1 param2>}}", `tag([\"param1\" \"param2\"], false){[]}"]`, testScPlaceholderRegexp, ""},
   352  		{"one named param", `{{% tag param1="value" %}}`, `tag([\"param1:value\"], true){[]}`, testScPlaceholderRegexp, ""},
   353  		{"two named params", `{{< tag param1="value1" param2="value2" >}}`, `tag([\"param1:value1\" \"param2:value2\"], false){[]}"]`,
   354  			testScPlaceholderRegexp, ""},
   355  		{"inner", `Some text. {{< inner >}}Inner Content{{< / inner >}}. Some more text.`, `inner([], false){[Inner Content]}`,
   356  			fmt.Sprintf("Some text. %s. Some more text.", testScPlaceholderRegexp), ""},
   357  		// issue #934
   358  		{"inner self-closing", `Some text. {{< inner />}}. Some more text.`, `inner([], false){[]}`,
   359  			fmt.Sprintf("Some text. %s. Some more text.", testScPlaceholderRegexp), ""},
   360  		{"close, but not inner", "{{< tag >}}foo{{< /tag >}}", "", false, "Shortcode 'tag' in page 'simple.md' has no .Inner.*"},
   361  		{"nested inner", `Inner->{{< inner >}}Inner Content->{{% inner2 param1 %}}inner2txt{{% /inner2 %}}Inner close->{{< / inner >}}<-done`,
   362  			`inner([], false){[Inner Content-> inner2([\"param1\"], true){[inner2txt]} Inner close->]}`,
   363  			fmt.Sprintf("Inner->%s<-done", testScPlaceholderRegexp), ""},
   364  		{"nested, nested inner", `Inner->{{< inner >}}inner2->{{% inner2 param1 %}}inner2txt->inner3{{< inner3>}}inner3txt{{</ inner3 >}}{{% /inner2 %}}final close->{{< / inner >}}<-done`,
   365  			`inner([], false){[inner2-> inner2([\"param1\"], true){[inner2txt->inner3 inner3(%!q(<nil>), false){[inner3txt]}]} final close->`,
   366  			fmt.Sprintf("Inner->%s<-done", testScPlaceholderRegexp), ""},
   367  		{"two inner", `Some text. {{% inner %}}First **Inner** Content{{% / inner %}} {{< inner >}}Inner **Content**{{< / inner >}}. Some more text.`,
   368  			`map["HAHAHUGOSHORTCODE-1HBHB:inner([], true){[First **Inner** Content]}" "HAHAHUGOSHORTCODE-2HBHB:inner([], false){[Inner **Content**]}"]`,
   369  			fmt.Sprintf("Some text. %s %s. Some more text.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""},
   370  		{"closed without content", `Some text. {{< inner param1 >}}{{< / inner >}}. Some more text.`, `inner([\"param1\"], false){[]}`,
   371  			fmt.Sprintf("Some text. %s. Some more text.", testScPlaceholderRegexp), ""},
   372  		{"two shortcodes", "{{< sc1 >}}{{< sc2 >}}",
   373  			`map["HAHAHUGOSHORTCODE-1HBHB:sc1([], false){[]}" "HAHAHUGOSHORTCODE-2HBHB:sc2([], false){[]}"]`,
   374  			testScPlaceholderRegexp + testScPlaceholderRegexp, ""},
   375  		{"mix of shortcodes", `Hello {{< sc1 >}}world{{% sc2 p2="2"%}}. And that's it.`,
   376  			`map["HAHAHUGOSHORTCODE-1HBHB:sc1([], false){[]}" "HAHAHUGOSHORTCODE-2HBHB:sc2([\"p2:2\"]`,
   377  			fmt.Sprintf("Hello %sworld%s. And that's it.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""},
   378  		{"mix with inner", `Hello {{< sc1 >}}world{{% inner p2="2"%}}Inner{{%/ inner %}}. And that's it.`,
   379  			`map["HAHAHUGOSHORTCODE-1HBHB:sc1([], false){[]}" "HAHAHUGOSHORTCODE-2HBHB:inner([\"p2:2\"], true){[Inner]}"]`,
   380  			fmt.Sprintf("Hello %sworld%s. And that's it.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""},
   381  	} {
   382  
   383  		p, _ := pageFromString(simplePage, "simple.md", func(templ tpl.TemplateHandler) error {
   384  			templ.AddTemplate("_internal/shortcodes/tag.html", `tag`)
   385  			templ.AddTemplate("_internal/shortcodes/sc1.html", `sc1`)
   386  			templ.AddTemplate("_internal/shortcodes/sc2.html", `sc2`)
   387  			templ.AddTemplate("_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`)
   388  			templ.AddTemplate("_internal/shortcodes/inner2.html", `{{.Inner}}`)
   389  			templ.AddTemplate("_internal/shortcodes/inner3.html", `{{.Inner}}`)
   390  			return nil
   391  		})
   392  
   393  		counter := 0
   394  
   395  		s := newShortcodeHandler(p)
   396  
   397  		s.placeholderFunc = func() string {
   398  			counter++
   399  			return fmt.Sprintf("HAHA%s-%dHBHB", shortcodePlaceholderPrefix, counter)
   400  		}
   401  
   402  		content, err := s.extractShortcodes(this.input, p.withoutContent())
   403  
   404  		if b, ok := this.expect.(bool); ok && !b {
   405  			if err == nil {
   406  				t.Fatalf("[%d] %s: ExtractShortcodes didn't return an expected error", i, this.name)
   407  			} else {
   408  				r, _ := regexp.Compile(this.expectErrorMsg)
   409  				if !r.MatchString(err.Error()) {
   410  					t.Fatalf("[%d] %s: ExtractShortcodes didn't return an expected error message, got %s but expected %s",
   411  						i, this.name, err.Error(), this.expectErrorMsg)
   412  				}
   413  			}
   414  			continue
   415  		} else {
   416  			if err != nil {
   417  				t.Fatalf("[%d] %s: failed: %q", i, this.name, err)
   418  			}
   419  		}
   420  
   421  		shortCodes := s.shortcodes
   422  
   423  		var expected string
   424  		av := reflect.ValueOf(this.expect)
   425  		switch av.Kind() {
   426  		case reflect.String:
   427  			expected = av.String()
   428  		}
   429  
   430  		r, err := regexp.Compile(expected)
   431  
   432  		if err != nil {
   433  			t.Fatalf("[%d] %s: Failed to compile regexp %q: %q", i, this.name, expected, err)
   434  		}
   435  
   436  		if strings.Count(content, shortcodePlaceholderPrefix) != shortCodes.Len() {
   437  			t.Fatalf("[%d] %s: Not enough placeholders, found %d", i, this.name, shortCodes.Len())
   438  		}
   439  
   440  		if !r.MatchString(content) {
   441  			t.Fatalf("[%d] %s: Shortcode extract didn't match. got %q but expected %q", i, this.name, content, expected)
   442  		}
   443  
   444  		for _, placeHolder := range shortCodes.Keys() {
   445  			sc := shortCodes.getShortcode(placeHolder)
   446  			if !strings.Contains(content, placeHolder.(string)) {
   447  				t.Fatalf("[%d] %s: Output does not contain placeholder %q", i, this.name, placeHolder)
   448  			}
   449  
   450  			if sc.params == nil {
   451  				t.Fatalf("[%d] %s: Params is nil for shortcode '%s'", i, this.name, sc.name)
   452  			}
   453  		}
   454  
   455  		if this.expectShortCodes != "" {
   456  			shortCodesAsStr := fmt.Sprintf("map%q", collectAndSortShortcodes(shortCodes))
   457  			if !strings.Contains(shortCodesAsStr, this.expectShortCodes) {
   458  				t.Fatalf("[%d] %s: Shortcodes not as expected, got\n%s but expected\n%s", i, this.name, shortCodesAsStr, this.expectShortCodes)
   459  			}
   460  		}
   461  	}
   462  }
   463  
   464  func TestShortcodesInSite(t *testing.T) {
   465  	t.Parallel()
   466  	baseURL := "http://foo/bar"
   467  
   468  	tests := []struct {
   469  		contentPath string
   470  		content     string
   471  		outFile     string
   472  		expected    string
   473  	}{
   474  		{"sect/doc1.md", `a{{< b >}}c`,
   475  			filepath.FromSlash("public/sect/doc1/index.html"), "<p>abc</p>\n"},
   476  		// Issue #1642: Multiple shortcodes wrapped in P
   477  		// Deliberately forced to pass even if they maybe shouldn't.
   478  		{"sect/doc2.md", `a
   479  
   480  {{< b >}}		
   481  {{< c >}}
   482  {{< d >}}
   483  
   484  e`,
   485  			filepath.FromSlash("public/sect/doc2/index.html"),
   486  			"<p>a</p>\n\n<p>b<br />\nc\nd</p>\n\n<p>e</p>\n"},
   487  		{"sect/doc3.md", `a
   488  
   489  {{< b >}}		
   490  {{< c >}}
   491  
   492  {{< d >}}
   493  
   494  e`,
   495  			filepath.FromSlash("public/sect/doc3/index.html"),
   496  			"<p>a</p>\n\n<p>b<br />\nc</p>\n\nd\n\n<p>e</p>\n"},
   497  		{"sect/doc4.md", `a
   498  {{< b >}}
   499  {{< b >}}
   500  {{< b >}}
   501  {{< b >}}
   502  {{< b >}}
   503  
   504  
   505  
   506  
   507  
   508  
   509  
   510  
   511  
   512  
   513  `,
   514  			filepath.FromSlash("public/sect/doc4/index.html"),
   515  			"<p>a\nb\nb\nb\nb\nb</p>\n"},
   516  		// #2192 #2209: Shortcodes in markdown headers
   517  		{"sect/doc5.md", `# {{< b >}}	
   518  ## {{% c %}}`,
   519  			filepath.FromSlash("public/sect/doc5/index.html"), "\n\n<h1 id=\"hahahugoshortcode-1hbhb\">b</h1>\n\n<h2 id=\"hahahugoshortcode-2hbhb\">c</h2>\n"},
   520  		// #2223 pygments
   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  		// #2249
   525  		{"sect/doc7.ad", `_Shortcodes:_ *b: {{< b >}} c: {{% c %}}*`,
   526  			filepath.FromSlash("public/sect/doc7/index.html"),
   527  			"<div class=\"paragraph\">\n<p><em>Shortcodes:</em> <strong>b: b c: c</strong></p>\n</div>\n"},
   528  		{"sect/doc8.rst", `**Shortcodes:** *b: {{< b >}} c: {{% c %}}*`,
   529  			filepath.FromSlash("public/sect/doc8/index.html"),
   530  			"<div class=\"document\">\n\n\n<p><strong>Shortcodes:</strong> <em>b: b c: c</em></p>\n</div>"},
   531  		{"sect/doc9.mmark", `
   532  ---
   533  menu:
   534    main:
   535      parent: 'parent'
   536  ---
   537  **Shortcodes:** *b: {{< b >}} c: {{% c %}}*`,
   538  			filepath.FromSlash("public/sect/doc9/index.html"),
   539  			"<p><strong>Shortcodes:</strong> <em>b: b c: c</em></p>\n"},
   540  		// Issue #1229: Menus not available in shortcode.
   541  		{"sect/doc10.md", `---
   542  menu:
   543    main:
   544      identifier: 'parent'
   545  tags:
   546  - Menu
   547  ---
   548  **Menus:** {{< menu >}}`,
   549  			filepath.FromSlash("public/sect/doc10/index.html"),
   550  			"<p><strong>Menus:</strong> 1</p>\n"},
   551  		// Issue #2323: Taxonomies not available in shortcode.
   552  		{"sect/doc11.md", `---
   553  tags:
   554  - Bugs
   555  ---
   556  **Tags:** {{< tags >}}`,
   557  			filepath.FromSlash("public/sect/doc11/index.html"),
   558  			"<p><strong>Tags:</strong> 2</p>\n"},
   559  	}
   560  
   561  	sources := make([][2]string, len(tests))
   562  
   563  	for i, test := range tests {
   564  		sources[i] = [2]string{filepath.FromSlash(test.contentPath), test.content}
   565  	}
   566  
   567  	addTemplates := func(templ tpl.TemplateHandler) error {
   568  		templ.AddTemplate("_default/single.html", "{{.Content}}")
   569  
   570  		templ.AddTemplate("_internal/shortcodes/b.html", `b`)
   571  		templ.AddTemplate("_internal/shortcodes/c.html", `c`)
   572  		templ.AddTemplate("_internal/shortcodes/d.html", `d`)
   573  		templ.AddTemplate("_internal/shortcodes/menu.html", `{{ len (index .Page.Menus "main").Children }}`)
   574  		templ.AddTemplate("_internal/shortcodes/tags.html", `{{ len .Page.Site.Taxonomies.tags }}`)
   575  
   576  		return nil
   577  
   578  	}
   579  
   580  	cfg, fs := newTestCfg()
   581  
   582  	cfg.Set("defaultContentLanguage", "en")
   583  	cfg.Set("baseURL", baseURL)
   584  	cfg.Set("uglyURLs", false)
   585  	cfg.Set("verbose", true)
   586  
   587  	cfg.Set("pygmentsUseClasses", true)
   588  	cfg.Set("pygmentsCodefences", true)
   589  
   590  	writeSourcesToSource(t, "content", fs, sources...)
   591  
   592  	s := buildSingleSite(t, deps.DepsCfg{WithTemplate: addTemplates, Fs: fs, Cfg: cfg}, BuildCfg{})
   593  	th := testHelper{s.Cfg, s.Fs, t}
   594  
   595  	for _, test := range tests {
   596  		if strings.HasSuffix(test.contentPath, ".ad") && !helpers.HasAsciidoc() {
   597  			fmt.Println("Skip Asciidoc test case as no Asciidoc present.")
   598  			continue
   599  		} else if strings.HasSuffix(test.contentPath, ".rst") && !helpers.HasRst() {
   600  			fmt.Println("Skip Rst test case as no rst2html present.")
   601  			continue
   602  		} else if strings.Contains(test.expected, "code") {
   603  			fmt.Println("Skip Pygments test case as no pygments present.")
   604  			continue
   605  		}
   606  
   607  		th.assertFileContent(test.outFile, test.expected)
   608  	}
   609  
   610  }
   611  
   612  func TestShortcodeMultipleOutputFormats(t *testing.T) {
   613  	t.Parallel()
   614  
   615  	siteConfig := `
   616  baseURL = "http://example.com/blog"
   617  
   618  paginate = 1
   619  
   620  disableKinds = ["section", "taxonomy", "taxonomyTerm", "RSS", "sitemap", "robotsTXT", "404"]
   621  
   622  [outputs]
   623  home = [ "HTML", "AMP", "Calendar" ]
   624  page =  [ "HTML", "AMP", "JSON" ]
   625  
   626  `
   627  
   628  	pageTemplate := `---
   629  title: "%s"
   630  ---
   631  # Doc
   632  
   633  {{< myShort >}}
   634  {{< noExt >}}
   635  {{%% onlyHTML %%}}
   636  
   637  {{< myInner >}}{{< myShort >}}{{< /myInner >}}
   638  
   639  `
   640  
   641  	pageTemplateCSVOnly := `---
   642  title: "%s"
   643  outputs: ["CSV"]
   644  ---
   645  # Doc
   646  
   647  CSV: {{< myShort >}}
   648  `
   649  
   650  	pageTemplateShortcodeNotFound := `---
   651  title: "%s"
   652  outputs: ["CSV"]
   653  ---
   654  # Doc
   655  
   656  NotFound: {{< thisDoesNotExist >}}
   657  `
   658  
   659  	mf := afero.NewMemMapFs()
   660  
   661  	th, h := newTestSitesFromConfig(t, mf, siteConfig,
   662  		"layouts/_default/single.html", `Single HTML: {{ .Title }}|{{ .Content }}`,
   663  		"layouts/_default/single.json", `Single JSON: {{ .Title }}|{{ .Content }}`,
   664  		"layouts/_default/single.csv", `Single CSV: {{ .Title }}|{{ .Content }}`,
   665  		"layouts/index.html", `Home HTML: {{ .Title }}|{{ .Content }}`,
   666  		"layouts/index.amp.html", `Home AMP: {{ .Title }}|{{ .Content }}`,
   667  		"layouts/index.ics", `Home Calendar: {{ .Title }}|{{ .Content }}`,
   668  		"layouts/shortcodes/myShort.html", `ShortHTML`,
   669  		"layouts/shortcodes/myShort.amp.html", `ShortAMP`,
   670  		"layouts/shortcodes/myShort.csv", `ShortCSV`,
   671  		"layouts/shortcodes/myShort.ics", `ShortCalendar`,
   672  		"layouts/shortcodes/myShort.json", `ShortJSON`,
   673  		"layouts/shortcodes/noExt", `ShortNoExt`,
   674  		"layouts/shortcodes/onlyHTML.html", `ShortOnlyHTML`,
   675  		"layouts/shortcodes/myInner.html", `myInner:--{{- .Inner -}}--`,
   676  	)
   677  
   678  	fs := th.Fs
   679  
   680  	writeSource(t, fs, "content/_index.md", fmt.Sprintf(pageTemplate, "Home"))
   681  	writeSource(t, fs, "content/sect/mypage.md", fmt.Sprintf(pageTemplate, "Single"))
   682  	writeSource(t, fs, "content/sect/mycsvpage.md", fmt.Sprintf(pageTemplateCSVOnly, "Single CSV"))
   683  	writeSource(t, fs, "content/sect/notfound.md", fmt.Sprintf(pageTemplateShortcodeNotFound, "Single CSV"))
   684  
   685  	err := h.Build(BuildCfg{})
   686  	require.Equal(t, "logged 1 error(s)", err.Error())
   687  	require.Len(t, h.Sites, 1)
   688  
   689  	s := h.Sites[0]
   690  	home := s.getPage(KindHome)
   691  	require.NotNil(t, home)
   692  	require.Len(t, home.outputFormats, 3)
   693  
   694  	th.assertFileContent("public/index.html",
   695  		"Home HTML",
   696  		"ShortHTML",
   697  		"ShortNoExt",
   698  		"ShortOnlyHTML",
   699  		"myInner:--ShortHTML--",
   700  	)
   701  
   702  	th.assertFileContent("public/amp/index.html",
   703  		"Home AMP",
   704  		"ShortAMP",
   705  		"ShortNoExt",
   706  		"ShortOnlyHTML",
   707  		"myInner:--ShortAMP--",
   708  	)
   709  
   710  	th.assertFileContent("public/index.ics",
   711  		"Home Calendar",
   712  		"ShortCalendar",
   713  		"ShortNoExt",
   714  		"ShortOnlyHTML",
   715  		"myInner:--ShortCalendar--",
   716  	)
   717  
   718  	th.assertFileContent("public/sect/mypage/index.html",
   719  		"Single HTML",
   720  		"ShortHTML",
   721  		"ShortNoExt",
   722  		"ShortOnlyHTML",
   723  		"myInner:--ShortHTML--",
   724  	)
   725  
   726  	th.assertFileContent("public/sect/mypage/index.json",
   727  		"Single JSON",
   728  		"ShortJSON",
   729  		"ShortNoExt",
   730  		"ShortOnlyHTML",
   731  		"myInner:--ShortJSON--",
   732  	)
   733  
   734  	th.assertFileContent("public/amp/sect/mypage/index.html",
   735  		// No special AMP template
   736  		"Single HTML",
   737  		"ShortAMP",
   738  		"ShortNoExt",
   739  		"ShortOnlyHTML",
   740  		"myInner:--ShortAMP--",
   741  	)
   742  
   743  	th.assertFileContent("public/sect/mycsvpage/index.csv",
   744  		"Single CSV",
   745  		"ShortCSV",
   746  	)
   747  
   748  	th.assertFileContent("public/sect/notfound/index.csv",
   749  		"NotFound:",
   750  		"thisDoesNotExist",
   751  	)
   752  
   753  	require.Equal(t, uint64(1), s.Log.LogCountForLevel(jww.LevelError))
   754  
   755  }
   756  
   757  func collectAndSortShortcodes(shortcodes *orderedMap) []string {
   758  	var asArray []string
   759  
   760  	for _, key := range shortcodes.Keys() {
   761  		sc := shortcodes.getShortcode(key)
   762  		asArray = append(asArray, fmt.Sprintf("%s:%s", key, sc))
   763  	}
   764  
   765  	sort.Strings(asArray)
   766  	return asArray
   767  
   768  }
   769  
   770  func BenchmarkReplaceShortcodeTokens(b *testing.B) {
   771  
   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  	var in = make([]input, b.N*len(data))
   791  	var 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, "HUGOSHORTCODE", currIn.replacements)
   806  
   807  			if err != nil {
   808  				b.Fatalf("[%d] failed: %s", i, err)
   809  				continue
   810  			}
   811  			if len(results) != len(currIn.expect) {
   812  				b.Fatalf("[%d] replaceShortcodeTokens, got \n%q but expected \n%q", j, results, currIn.expect)
   813  			}
   814  
   815  		}
   816  
   817  	}
   818  }
   819  
   820  func TestReplaceShortcodeTokens(t *testing.T) {
   821  	t.Parallel()
   822  	for i, this := range []struct {
   823  		input        string
   824  		prefix       string
   825  		replacements map[string]string
   826  		expect       interface{}
   827  	}{
   828  		{"Hello HAHAPREFIX-1HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "Hello World."},
   829  		{"Hello HAHAPREFIX-1@}@.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, false},
   830  		{"HAHAPREFIX2-1HBHB", "PREFIX2", map[string]string{"HAHAPREFIX2-1HBHB": "World"}, "World"},
   831  		{"Hello World!", "PREFIX2", map[string]string{}, "Hello World!"},
   832  		{"!HAHAPREFIX-1HBHB", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "!World"},
   833  		{"HAHAPREFIX-1HBHB!", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "World!"},
   834  		{"!HAHAPREFIX-1HBHB!", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "!World!"},
   835  		{"_{_PREFIX-1HBHB", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "_{_PREFIX-1HBHB"},
   836  		{"Hello HAHAPREFIX-1HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "To You My Old Friend Who Told Me This Fantastic Story"}, "Hello To You My Old Friend Who Told Me This Fantastic Story."},
   837  		{"A HAHAA-1HBHB asdf HAHAA-2HBHB.", "A", map[string]string{"HAHAA-1HBHB": "v1", "HAHAA-2HBHB": "v2"}, "A v1 asdf v2."},
   838  		{"Hello HAHAPREFIX2-1HBHB. Go HAHAPREFIX2-2HBHB, Go, Go HAHAPREFIX2-3HBHB Go Go!.", "PREFIX2", map[string]string{"HAHAPREFIX2-1HBHB": "Europe", "HAHAPREFIX2-2HBHB": "Jonny", "HAHAPREFIX2-3HBHB": "Johnny"}, "Hello Europe. Go Jonny, Go, Go Johnny Go Go!."},
   839  		{"A HAHAPREFIX-2HBHB HAHAPREFIX-1HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B"}, "A B A."},
   840  		{"A HAHAPREFIX-1HBHB HAHAPREFIX-2", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A"}, false},
   841  		{"A HAHAPREFIX-1HBHB but not the second.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B"}, "A A but not the second."},
   842  		{"An HAHAPREFIX-1HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B"}, "An A."},
   843  		{"An HAHAPREFIX-1HBHB HAHAPREFIX-2HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B"}, "An A B."},
   844  		{"A HAHAPREFIX-1HBHB HAHAPREFIX-2HBHB HAHAPREFIX-3HBHB HAHAPREFIX-1HBHB HAHAPREFIX-3HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B", "HAHAPREFIX-3HBHB": "C"}, "A A B C A C."},
   845  		{"A HAHAPREFIX-1HBHB HAHAPREFIX-2HBHB HAHAPREFIX-3HBHB HAHAPREFIX-1HBHB HAHAPREFIX-3HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B", "HAHAPREFIX-3HBHB": "C"}, "A A B C A C."},
   846  		// Issue #1148 remove p-tags 10 =>
   847  		{"Hello <p>HAHAPREFIX-1HBHB</p>. END.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "Hello World. END."},
   848  		{"Hello <p>HAHAPREFIX-1HBHB</p>. <p>HAHAPREFIX-2HBHB</p> END.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World", "HAHAPREFIX-2HBHB": "THE"}, "Hello World. THE END."},
   849  		{"Hello <p>HAHAPREFIX-1HBHB. END</p>.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "Hello <p>World. END</p>."},
   850  		{"<p>Hello HAHAPREFIX-1HBHB</p>. END.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "<p>Hello World</p>. END."},
   851  		{"Hello <p>HAHAPREFIX-1HBHB12", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "Hello <p>World12"},
   852  		{"Hello HAHAP-1HBHB. HAHAP-1HBHB-HAHAP-1HBHB HAHAP-1HBHB HAHAP-1HBHB HAHAP-1HBHB END", "P", map[string]string{"HAHAP-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  		results, err := replaceShortcodeTokens([]byte(this.input), this.prefix, this.replacements)
   858  
   859  		if b, ok := this.expect.(bool); ok && !b {
   860  			if err == nil {
   861  				t.Errorf("[%d] replaceShortcodeTokens didn't return an expected error", i)
   862  			}
   863  		} else {
   864  			if err != nil {
   865  				t.Errorf("[%d] failed: %s", i, err)
   866  				continue
   867  			}
   868  			if !reflect.DeepEqual(results, []byte(this.expect.(string))) {
   869  				t.Errorf("[%d] replaceShortcodeTokens, got \n%q but expected \n%q", i, results, this.expect)
   870  			}
   871  		}
   872  
   873  	}
   874  
   875  }
   876  
   877  func TestScKey(t *testing.T) {
   878  	require.Equal(t, scKey{Suffix: "xml", ShortcodePlaceholder: "ABCD"},
   879  		newScKey(media.XMLType, "ABCD"))
   880  	require.Equal(t, scKey{Lang: "en", Suffix: "html", OutputFormat: "AMP", ShortcodePlaceholder: "EFGH"},
   881  		newScKeyFromLangAndOutputFormat("en", output.AMPFormat, "EFGH"))
   882  	require.Equal(t, scKey{Suffix: "html", ShortcodePlaceholder: "IJKL"},
   883  		newDefaultScKey("IJKL"))
   884  
   885  }
   886  
   887  func TestShortcodeGetContent(t *testing.T) {
   888  	t.Parallel()
   889  	assert := require.New(t)
   890  
   891  	contentShortcode := `
   892  {{- $t := .Get 0 -}}
   893  {{- $p := .Get 1 -}}
   894  {{- $k := .Get 2 -}}
   895  {{- $page := $.Page.Site.GetPage "page" $p -}}
   896  {{ if $page }}
   897  {{- if eq $t "bundle" -}}
   898  {{- .Scratch.Set "p" ($page.Resources.GetMatch (printf "%s*" $k)) -}}
   899  {{- else -}}
   900  {{- $.Scratch.Set "p" $page -}}
   901  {{- end -}}P1:{{ .Page.Content }}|P2:{{ $p := ($.Scratch.Get "p") }}{{ $p.Title }}/{{ $p.Content }}|
   902  {{- else -}}
   903  {{- errorf "Page %s is nil" $p -}}
   904  {{- end -}}
   905  `
   906  
   907  	var templates []string
   908  	var content []string
   909  
   910  	contentWithShortcodeTemplate := `---
   911  title: doc%s
   912  weight: %d
   913  ---
   914  Logo:{{< c "bundle" "b1" "logo.png" >}}:P1: {{< c "page" "section1/p1" "" >}}:BP1:{{< c "bundle" "b1" "bp1" >}}`
   915  
   916  	simpleContentTemplate := `---
   917  title: doc%s
   918  weight: %d
   919  ---
   920  C-%s`
   921  
   922  	v := viper.New()
   923  
   924  	v.Set("timeout", 500)
   925  
   926  	templates = append(templates, []string{"shortcodes/c.html", contentShortcode}...)
   927  	templates = append(templates, []string{"_default/single.html", "Single Content: {{ .Content }}"}...)
   928  	templates = append(templates, []string{"_default/list.html", "List Content: {{ .Content }}"}...)
   929  
   930  	content = append(content, []string{"b1/index.md", fmt.Sprintf(contentWithShortcodeTemplate, "b1", 1)}...)
   931  	content = append(content, []string{"b1/logo.png", "PNG logo"}...)
   932  	content = append(content, []string{"b1/bp1.md", fmt.Sprintf(simpleContentTemplate, "bp1", 1, "bp1")}...)
   933  
   934  	content = append(content, []string{"section1/_index.md", fmt.Sprintf(contentWithShortcodeTemplate, "s1", 2)}...)
   935  	content = append(content, []string{"section1/p1.md", fmt.Sprintf(simpleContentTemplate, "s1p1", 2, "s1p1")}...)
   936  
   937  	content = append(content, []string{"section2/_index.md", fmt.Sprintf(simpleContentTemplate, "b1", 1, "b1")}...)
   938  	content = append(content, []string{"section2/s2p1.md", fmt.Sprintf(contentWithShortcodeTemplate, "bp1", 1)}...)
   939  
   940  	builder := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
   941  
   942  	builder.WithViper(v).WithContent(content...).WithTemplates(templates...).CreateSites().Build(BuildCfg{})
   943  	s := builder.H.Sites[0]
   944  	assert.Equal(3, len(s.RegularPages))
   945  
   946  	builder.AssertFileContent("public/section1/index.html",
   947  		"List Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|",
   948  		"BP1:P1:|P2:docbp1/<p>C-bp1</p>",
   949  	)
   950  
   951  	builder.AssertFileContent("public/b1/index.html",
   952  		"Single Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|",
   953  		"P2:docbp1/<p>C-bp1</p>",
   954  	)
   955  
   956  	builder.AssertFileContent("public/section2/s2p1/index.html",
   957  		"Single Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|",
   958  		"P2:docbp1/<p>C-bp1</p>",
   959  	)
   960  
   961  }
   962  
   963  func TestShortcodePreserveOrder(t *testing.T) {
   964  	t.Parallel()
   965  	assert := require.New(t)
   966  
   967  	contentTemplate := `---
   968  title: doc%d
   969  weight: %d
   970  ---
   971  # doc
   972  
   973  {{< s1 >}}{{< s2 >}}{{< s3 >}}{{< s4 >}}{{< s5 >}}
   974  
   975  {{< nested >}}
   976  {{< ordinal >}} {{< scratch >}}
   977  {{< ordinal >}} {{< scratch >}}
   978  {{< ordinal >}} {{< scratch >}}
   979  {{< /nested >}}
   980  
   981  `
   982  
   983  	ordinalShortcodeTemplate := `ordinal: {{ .Ordinal }}{{ .Page.Scratch.Set "ordinal" .Ordinal }}`
   984  
   985  	nestedShortcode := `outer ordinal: {{ .Ordinal }} inner: {{ .Inner }}`
   986  	scratchGetShortcode := `scratch ordinal: {{ .Ordinal }} scratch get ordinal: {{ .Page.Scratch.Get "ordinal" }}`
   987  	shortcodeTemplate := `v%d: {{ .Ordinal }} sgo: {{ .Page.Scratch.Get "o2" }}{{ .Page.Scratch.Set "o2" .Ordinal }}|`
   988  
   989  	var shortcodes []string
   990  	var content []string
   991  
   992  	shortcodes = append(shortcodes, []string{"shortcodes/nested.html", nestedShortcode}...)
   993  	shortcodes = append(shortcodes, []string{"shortcodes/ordinal.html", ordinalShortcodeTemplate}...)
   994  	shortcodes = append(shortcodes, []string{"shortcodes/scratch.html", scratchGetShortcode}...)
   995  
   996  	for i := 1; i <= 5; i++ {
   997  		sc := fmt.Sprintf(shortcodeTemplate, i)
   998  		sc = strings.Replace(sc, "%%", "%", -1)
   999  		shortcodes = append(shortcodes, []string{fmt.Sprintf("shortcodes/s%d.html", i), sc}...)
  1000  	}
  1001  
  1002  	for i := 1; i <= 3; i++ {
  1003  		content = append(content, []string{fmt.Sprintf("p%d.md", i), fmt.Sprintf(contentTemplate, i, i)}...)
  1004  	}
  1005  
  1006  	builder := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
  1007  
  1008  	builder.WithContent(content...).WithTemplatesAdded(shortcodes...).CreateSites().Build(BuildCfg{})
  1009  
  1010  	s := builder.H.Sites[0]
  1011  	assert.Equal(3, len(s.RegularPages))
  1012  
  1013  	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`)
  1014  	builder.AssertFileContent("public/en/p1/index.html", `outer ordinal: 5 inner: 
  1015  ordinal: 0 scratch ordinal: 1 scratch get ordinal: 0
  1016  ordinal: 2 scratch ordinal: 3 scratch get ordinal: 2
  1017  ordinal: 4 scratch ordinal: 5 scratch get ordinal: 4`)
  1018  
  1019  }