// Copyright 2025 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 tplimpl_test import ( "strings" "testing" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/hugolib" ) func TestCommentShortcode(t *testing.T) { // This cannot be parallel as it depends on output from the global logger. files := ` -- hugo.toml -- disableKinds = ['page','rss','section','sitemap','taxonomy','term'] -- layouts/home.html -- {{ .Content }} -- content/_index.md -- --- title: home --- a{{< comment >}}b{{< /comment >}}c ` b := hugolib.Test(t, files, hugolib.TestOptWarn()) b.AssertFileContent("public/index.html", "

ac

") b.AssertLogContains(`WARN The "comment" shortcode was deprecated in v0.143.0 and will be removed in a future release. Please use HTML comments instead.`) } func TestDetailsShortcode(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- disableKinds = ['page','rss','section','sitemap','taxonomy','term'] -- layouts/home.html -- {{ .Content }} -- content/_index.md -- --- title: home --- {{< details >}} A: An _emphasized_ word. {{< /details >}} {{< details class="my-class" name="my-name" open=true summary="A **bold** word" title="my-title" >}} B: An _emphasized_ word. {{< /details >}} {{< details open=false >}} C: An _emphasized_ word. {{< /details >}} {{< details open="false" >}} D: An _emphasized_ word. {{< /details >}} {{< details open=0 >}} E: An _emphasized_ word. {{< /details >}} ` b := hugolib.Test(t, files) b.AssertFileContent("public/index.html", "
\n Details\n

A: An emphasized word.

\n
", "
\n A bold word\n

B: An emphasized word.

\n
", "
\n Details\n

C: An emphasized word.

\n
", "
\n Details\n

D: An emphasized word.

\n
", "
\n Details\n

D: An emphasized word.

\n
", ) } func TestFigureShortcode(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- disableKinds = ['page','rss','section','sitemap','taxonomy','term'] -- content/_index.md -- --- title: home --- {{< figure src="a.jpg" alt="alternate text" width=600 height=400 loading="lazy" class="my-class" link="https://example.org" target="_blank" rel="noopener" title="my-title" caption="a **bold** word" attr="an _emphasized_ word" attrlink="https://example.org/foo" >}} -- layouts/home.html -- Hash: {{ .Content | hash.XxHash }} Content: {{ .Content }} ` b := hugolib.Test(t, files) b.AssertFileContent("public/index.html", "c18e12c59a2f9bdb") } func TestHighlightShortcode(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- disableKinds = ['home','rss','section','sitemap','taxonomy','term'] -- layouts/single.html -- Hash: {{ .Content | hash.XxHash }} Content: {{ .Content }} -- content/p1.md -- --- title: p1 --- {{< highlight go >}} func main() { for i := 0; i < 3; i++ { fmt.Println("Value of i:", i) } } {{< /highlight >}} -- content/p2.md -- --- title: p2 --- {{< highlight go "noClasses=false" >}} func main() { for i := 0; i < 3; i++ { fmt.Println("Value of i:", i) } } {{< /highlight >}} -- content/p3.md -- --- title: p3 --- {{< highlight go "lineNos=inline" >}} func main() { for i := 0; i < 3; i++ { fmt.Println("Value of i:", i) } } {{< /highlight >}} -- content/p4.md -- --- title: p4 --- {{< highlight go "lineNos=table" >}} func main() { for i := 0; i < 3; i++ { fmt.Println("Value of i:", i) } } {{< /highlight >}} -- content/p5.md -- --- title: p5 --- {{< highlight go "anchorLineNos=true, hl_Lines=2-4, lineAnchors=foo, lineNoStart=42, lineNos=true, lineNumbersInTable=false, style=emacs, wrapperClass=my-class" >}} func main() { for i := 0; i < 3; i++ { fmt.Println("Value of i:", i) } } {{< /highlight >}} -- content/p6.md -- --- title: p6 --- {{< highlight go "anchorlinenos=true, hl_lines=2-4, lineanchors=foo, linenostart=42, linenos=true, linenumbersintable=false, style=emacs, wrapperclass=my-class" >}} func main() { for i := 0; i < 3; i++ { fmt.Println("Value of i:", i) } } {{< /highlight >}} -- content/p7.md -- --- title: p7 --- An inline {{< highlight go "hl_inline=true" >}}fmt.Println("Value of i:", i)Hello world!{{< /highlight >}} statement. ` b := hugolib.Test(t, files) b.AssertFileContent("public/p1/index.html", "4f7e70e3bc7702fc") b.AssertFileContent("public/p2/index.html", "3f97c98e654f23a2") b.AssertFileContent("public/p3/index.html", "21d109753ff1b086") b.AssertFileContent("public/p4/index.html", "a6df524f21db7044") b.AssertFileContent("public/p5/index.html", "db2d5cc19d163217") b.AssertFileContent("public/p6/index.html", "db2d5cc19d163217") b.AssertFileContent("public/p7/index.html", "f12eeaa4d6d9c7ac") } func TestInstagramShortcode(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- disableKinds = ['page','rss','section','sitemap','taxonomy','term'] privacy.instagram.simple = false -- content/_index.md -- --- title: home --- {{< instagram CxOWiQNP2MO >}} -- layouts/home.html -- Hash: {{ .Content | hash.XxHash }} Content: {{ .Content }} ` // Regular mode b := hugolib.Test(t, files) b.AssertFileContent("public/index.html", "336bb64b13c24575") // Simple mode files = strings.ReplaceAll(files, "privacy.instagram.simple = false", "privacy.instagram.simple = true") b = hugolib.Test(t, files) b.AssertFileContent("public/index.html", "4c094707f37b2340") } func TestParamShortcode(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- disableKinds = ['page','rss','section','sitemap','taxonomy','term'] [params] b = 2 -- layouts/home.html -- {{ .Content }} -- content/_index.md -- --- title: home params: a: 1 --- A: {{% param "a" %}} B: {{% param "b" %}} ` b := hugolib.Test(t, files) b.AssertFileExists("public/index.html", true) b.AssertFileContent("public/index.html", "A: 1", "B: 2", ) } func TestQRShortcode(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- disableKinds = ['page','rss','section','sitemap','taxonomy','term'] -- layouts/home.html -- {{ .Content }} -- content/_index.md -- --- title: home --- {{< qr text="https://gohugo.io" level="high" scale=4 targetDir="codes" alt="QR code linking to https://gohugo.io" class="my-class" id="my-id" title="My Title" />}} {{< qr >}} https://gohugo.io" {{< /qr >}} ` b := hugolib.Test(t, files) b.AssertFileContent("public/index.html", `QR code linking to https://gohugo.io`, ``, ) } func TestRefShortcode(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- baseURL = 'https://example.org/' disableKinds = ['rss','section','sitemap','taxonomy','term'] defaultContentLanguageInSubdir = true [markup.goldmark.extensions] linkify = false [languages.en] weight = 1 [languages.de] weight = 2 [outputs] page = ['html','json'] -- layouts/home.html -- {{ .Content }} -- layouts/single.html.html -- {{ .Title }} -- layouts/single.json.json -- {{ .Title }} -- content/_index.en.md -- --- title: home --- A: {{% ref "/s1/p1.md" %}} B: {{% ref path="/s1/p1.md" %}} C: {{% ref path="/s1/p1.md" lang="en" %}} D: {{% ref path="/s1/p1.md" lang="de" %}} E: {{% ref path="/s1/p1.md" lang="en" outputFormat="html" %}} F: {{% ref path="/s1/p1.md" lang="de" outputFormat="html" %}} G: {{% ref path="/s1/p1.md" lang="en" outputFormat="json" %}} H: {{% ref path="/s1/p1.md" lang="de" outputFormat="json" %}} -- content/s1/p1.en.md -- --- title: p1 (en) --- -- content/s1/p1.de.md -- --- title: p1 (de) --- ` b := hugolib.Test(t, files) b.AssertFileContent("public/en/index.html", `p>A: https://example.org/en/s1/p1/

`, `p>B: https://example.org/en/s1/p1/

`, `p>C: https://example.org/en/s1/p1/

`, `p>D: https://example.org/de/s1/p1/

`, `p>E: https://example.org/en/s1/p1/

`, `p>F: https://example.org/de/s1/p1/

`, `p>G: https://example.org/en/s1/p1/index.json

`, `p>H: https://example.org/de/s1/p1/index.json

`, ) } func TestRelRefShortcode(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- baseURL = 'https://example.org/' disableKinds = ['rss','section','sitemap','taxonomy','term'] defaultContentLanguageInSubdir = true [markup.goldmark.extensions] linkify = false [languages.en] weight = 1 [languages.de] weight = 2 [outputs] page = ['html','json'] -- layouts/home.html -- {{ .Content }} -- layouts/single.html.html -- {{ .Title }} -- layouts/single.json.json -- {{ .Title }} -- content/_index.en.md -- --- title: home --- A: {{% relref "/s1/p1.md" %}} B: {{% relref path="/s1/p1.md" %}} C: {{% relref path="/s1/p1.md" lang="en" %}} D: {{% relref path="/s1/p1.md" lang="de" %}} E: {{% relref path="/s1/p1.md" lang="en" outputFormat="html" %}} F: {{% relref path="/s1/p1.md" lang="de" outputFormat="html" %}} G: {{% relref path="/s1/p1.md" lang="en" outputFormat="json" %}} H: {{% relref path="/s1/p1.md" lang="de" outputFormat="json" %}} -- content/s1/p1.en.md -- --- title: p1 (en) --- -- content/s1/p1.de.md -- --- title: p1 (de) --- ` b := hugolib.Test(t, files) b.AssertFileContent("public/en/index.html", `p>A: /en/s1/p1/

`, `p>B: /en/s1/p1/

`, `p>C: /en/s1/p1/

`, `p>D: /de/s1/p1/

`, `p>E: /en/s1/p1/

`, `p>F: /de/s1/p1/

`, `p>G: /en/s1/p1/index.json

`, `p>H: /de/s1/p1/index.json

`, ) } func TestVimeoShortcode(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- disableKinds = ['home','rss','section','sitemap','taxonomy','term'] privacy.vimeo.simple = false -- content/p1.md -- --- title: p1 --- {{< vimeo 19899678 >}} -- content/p2.md -- --- title: p2 --- {{< vimeo id=19899678 allowFullScreen=true >}} -- content/p3.md -- --- title: p3 --- {{< vimeo id=19899678 allowFullScreen=false >}} -- layouts/single.html -- Hash: {{ .Content | hash.XxHash }} Content: {{ .Content }} ` // Regular mode b := hugolib.Test(t, files) b.AssertFileContent("public/p1/index.html", "2144844b57ee39d8") b.AssertFileContent("public/p2/index.html", "2144844b57ee39d8") b.AssertFileContent("public/p3/index.html", "a131e93ce53bc318") // Simple mode files = strings.ReplaceAll(files, "privacy.vimeo.simple = false", "privacy.vimeo.simple = true") b = hugolib.Test(t, files) b.AssertFileContent("public/p1/index.html", "582bde2225dc43c0") // Simple mode with non-existent id files = strings.ReplaceAll(files, "{{< vimeo 19899678 >}}", "{{< vimeo __id_does_not_exist__ >}}") b = hugolib.Test(t, files, hugolib.TestOptWarn()) b.AssertLogContains(`WARN The "vimeo" shortcode was unable to retrieve the remote data.`) } func TestXShortcodes(t *testing.T) { t.Parallel() htesting.SkipSlowTestUnlessCI(t) files := ` -- hugo.toml -- disableKinds = ['home','rss','section','sitemap','taxonomy','term'] #CONFIG -- content/p1.md -- --- title: p1 --- {{< x user="SanDiegoZoo" id="1453110110599868418" >}} -- content/p2.md -- --- title: p2 --- {{< x_simple user="SanDiegoZoo" id="1453110110599868418" >}} -- layouts/single.html -- {{ .Content | strings.TrimSpace | safeHTML }} -- ` b := hugolib.Test(t, files) // Test x shortcode want := `

Owl bet you'll lose this staring contest 🦉 pic.twitter.com/eJh4f2zncC

— San Diego Zoo Wildlife Alliance (@sandiegozoo) October 26, 2021
` b.AssertFileContent("public/p1/index.html", want) // Test x_simple shortcodes wantSimple := "

Owl bet you'll lose this staring contest 🦉 pic.twitter.com/eJh4f2zncC

— San Diego Zoo Wildlife Alliance (@sandiegozoo) October 26, 2021
\n--" b.AssertFileContent("public/p2/index.html", wantSimple) // Test privacy.x.simple f := strings.ReplaceAll(files, "#CONFIG", "privacy.x.simple=true") b = hugolib.Test(t, f) b.AssertFileContent("public/p1/index.html", wantSimple) b.AssertFileContent("public/p2/index.html", wantSimple) // Test privacy.x.disable f = strings.ReplaceAll(files, "#CONFIG", "privacy.x.disable = true") b = hugolib.Test(t, f) b.AssertFileContent("public/p1/index.html", "") b.AssertFileContent("public/p2/index.html", "") // Test warnings files = ` -- hugo.toml -- disableKinds = ['page','rss','section','sitemap','taxonomy','term'] -- content/_index.md -- --- title: home --- {{< x user="__user_does_not_exist__" id="__id_does_not_exist__" >}} {{< x_simple user="__user_does_not_exist__" id="__id_does_not_exist__" >}} -- layouts/home.html -- {{ .Content }} ` b = hugolib.Test(t, files, hugolib.TestOptWarn()) b.AssertLogContains( `WARN The "x" shortcode was unable to retrieve the remote data.`, `WARN The "x_simple" shortcode was unable to retrieve the remote data.`, ) } func TestYouTubeShortcode(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- disableKinds = ['home','rss','section','sitemap','taxonomy','term'] privacy.youtube.privacyEnhanced = false -- layouts/single.html -- Hash: {{ .Content | hash.XxHash }} Content: {{ .Content }} -- content/p1.md -- --- title: p1 --- {{< youtube 0RKpf3rK57I >}} -- content/p2.md -- --- title: p2 --- {{< youtube id="0RKpf3rK57I" allowFullScreen=false autoplay=true class="my-class" controls="false" end=42 loading="lazy" loop=true mute=true start=6 title="my title" >}} ` b := hugolib.Test(t, files) b.AssertFileContent("public/p1/index.html", "365fe481b0377cd9") b.AssertFileContent("public/p2/index.html", "82d7f05fb1fefb3b") files = strings.ReplaceAll(files, "privacy.youtube.privacyEnhanced = false", "privacy.youtube.privacyEnhanced = true") b = hugolib.Test(t, files) b.AssertFileContent("public/p1/index.html", "48398992f5ff0afc") b.AssertFileContent("public/p2/index.html", "116436ffc3f62191") } func TestShortcodePlainTextVsHTMLTemplateIssue13698(t *testing.T) { t.Parallel() filesTemplate := ` -- hugo.toml -- markup.goldmark.renderer.unsafe = true -- layouts/all.html -- Content: {{ .Content }}| -- layouts/_shortcodes/mymarkdown.md --
Foo bar
-- content/p1.md -- --- title: p1 --- ## A shortcode SHORTCODE ` files := strings.ReplaceAll(filesTemplate, "SHORTCODE", "{{% mymarkdown %}}") b := hugolib.Test(t, files) b.AssertFileContent("public/p1/index.html", "
Foo bar
") files = strings.ReplaceAll(filesTemplate, "SHORTCODE", "{{< mymarkdown >}}") var err error b, err = hugolib.TestE(t, files) b.Assert(err, qt.IsNotNil) b.Assert(err.Error(), qt.Contains, `no compatible template found for shortcode "mymarkdown" in [/_shortcodes/mymarkdown.md]; note that to use plain text template shortcodes in HTML you need to use the shortcode {{% delimiter`) } func TestShortcodeOnlyLanguageInBaseIssue13699And13740(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- baseURL = 'https://example.org/' disableLanguages = ['de'] [languages] [languages.en] weight = 1 [languages.de] weight = 2 -- layouts/_shortcodes/de.html -- de.html -- layouts/all.html -- {{ .Content }} -- content/_index.md -- --- title: home --- {{< de >}} ` b := hugolib.Test(t, files) b.AssertFileContent("public/index.html", "de.html") } func TestShortcodeLanguage13767(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- defaultContentLanguage = 'pl' defaultContentLanguageInSubdir = true [languages.pl] weight = 1 [languages.en] weight = 2 -- content/_index.md -- --- title: dom --- {{< myshortcode >}} -- content/_index.en.md -- --- title: home --- {{< myshortcode >}} -- layouts/_shortcodes/myshortcode.html -- myshortcode.html -- layouts/_shortcodes/myshortcode.en.html -- myshortcode.en.html -- layouts/all.html -- {{ .Content }} ` b := hugolib.Test(t, files) b.AssertFileContent("public/pl/index.html", "myshortcode.html") b.AssertFileContent("public/en/index.html", "myshortcode.en.html") } func TestHasShortcodeSamePageSourceDifferentShortcodes(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- disableKinds = ['home','rss','section','sitemap','taxonomy','term', '404'] defaultContentLanguage = 'en' defaultContentLanguageInSubdir = true markup.goldmark.renderer.unsafe = true [languages] [languages.en] weight = 1 [languages.sv] weight = 2 -- content/inc/i1.md -- --- headless: true sites: matrix: languages: - '**' --- {{% sc1 %}} -- content/inc/i2.md -- --- headless: true sites: matrix: languages: - '**' --- {{% sc2 %}} -- content/p1.md -- --- title: p1 --- P1. {{% inc %}} -- content/p1.sv.md -- --- title: p1 sv --- P1. {{% inc %}} -- layouts/_shortcodes/sc1.html -- sc1 -- layouts/_shortcodes/sc2.html -- sc2 -- layouts/_shortcodes/inc.html -- {{ with site.GetPage "inc/i1.md" }}i1.RenderShortcodes: {{ .RenderShortcodes }}{{ end }} -- layouts/_shortcodes/inc.sv.html -- {{ with site.GetPage "inc/i2.md" }}i1.RenderShortcodes: {{ .RenderShortcodes }}{{ end }} -- layouts/all.html -- Content: {{ .Content }}|sc1: {{ .HasShortcode "sc1" }}|sc2: {{ .HasShortcode "sc2" }}|inc: {{ .HasShortcode "inc" }}| ` b := hugolib.Test(t, files) b.AssertFileContent("public/en/p1/index.html", " |sc1: true|sc2: false|inc: true|") b.AssertFileContent("public/sv/p1/index.html", " |sc1: false|sc2: true|inc: true|") } func TestShortcodeMultilingualHTML(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- baseURL = 'https://example.org/' defaultContentLanguage = 'tr' defaultContentLanguageInSubdir = true [languages.tr] weight = 1 [languages.en] weight = 2 -- content/posts/test-post.tr.md -- +++ title = 'Test Post TR' +++ {{< myhtml >}} -- content/posts/test-post.en.md -- +++ title = 'Test Post EN' +++ {{< myhtml >}} -- layouts/_shortcodes/myhtml.en.html -- HTML-EN -- layouts/_shortcodes/myhtml.tr.html -- HTML-TR -- layouts/_default/single.html -- {{ .Content }} ` b := hugolib.Test(t, files) b.AssertFileContent("public/tr/posts/test-post/index.html", "HTML-TR", ) b.AssertFileContent("public/en/posts/test-post/index.html", "HTML-EN", ) } func TestShortcodeMultilingualMarkdown(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- baseURL = 'https://example.org/' defaultContentLanguage = 'tr' defaultContentLanguageInSubdir = true [languages.tr] weight = 1 [languages.en] weight = 2 -- content/posts/test-post.tr.md -- +++ title = 'Test Post TR' +++ {{% mymd %}} -- content/posts/test-post.en.md -- +++ title = 'Test Post EN' +++ {{% mymd %}} -- layouts/_shortcodes/mymd.en.md -- MD-EN -- layouts/_shortcodes/mymd.tr.md -- MD-TR -- layouts/_default/single.html -- {{ .Content }} ` b := hugolib.Test(t, files) b.AssertFileContent("public/tr/posts/test-post/index.html", "MD-TR", ) b.AssertFileContent("public/en/posts/test-post/index.html", "MD-EN", ) } // Issue 14488 func TestTwitterShortcodeDeprecationError14488(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- disableKinds = ['home','rss','section','sitemap','taxonomy','term'] -- content/p1.md -- --- title: p1 --- {{< SHORTCODE user="SanDiegoZoo" id="1453110110599868418" >}} -- layouts/page.html -- {{ .Content }} ` want := `ERROR The "twitter", "tweet", and "twitter_simple" shortcodes were deprecated in v0.142.0 and removed in v0.156.0. Please use the "x" shortcode instead.` shortcodes := []string{"twitter", "twitter_simple", "tweet"} for _, sc := range shortcodes { f := strings.ReplaceAll(files, "SHORTCODE", sc) b, err := hugolib.TestE(t, f, hugolib.TestOptWarn()) b.Assert(err, qt.IsNotNil) b.AssertLogContains(want) } } // Issue 14491 func TestGistShortcodeDeprecationError14491(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- disableKinds = ['page','rss','section','sitemap','taxonomy','term'] -- layouts/home.html -- {{ .Content }} -- content/_index.md -- --- title: home --- {{< gist user 23932424365401ffa5e9d9810102a477 >}} ` b, err := hugolib.TestE(t, files, hugolib.TestOptWarn()) b.Assert(err, qt.IsNotNil) b.AssertLogContains(`ERROR The "gist" shortcode was deprecated in v0.143.0 and removed in v0.156.0.`) }