From 84b31721bfbf86d6e02a1a792884e5e69f62b806 Mon Sep 17 00:00:00 2001 From: Joe Mooring Date: Sat, 21 Jun 2025 06:41:08 -0700 Subject: [PATCH] markup/goldmark: Change link and image render hook enablement to enums Closes #13535 --- config/allconfig/allconfig.go | 57 +++++++-- .../allconfig/allconfig_integration_test.go | 27 +++++ helpers/general.go | 21 ++++ helpers/general_test.go | 26 +++- hugolib/page__per_output.go | 16 ++- hugolib/site.go | 1 + markup/goldmark/goldmark_config/config.go | 32 +++-- tpl/tplimpl/render_hook_integration_test.go | 112 ++++++++++++++++++ tpl/tplimpl/templatestore.go | 36 ++++-- 9 files changed, 293 insertions(+), 35 deletions(-) diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index 0db0be1d8..3366defd7 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -20,6 +20,7 @@ import ( "fmt" "reflect" "regexp" + "slices" "sort" "strconv" "strings" @@ -32,7 +33,6 @@ import ( "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/paths" - "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/urls" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/privacy" @@ -42,6 +42,7 @@ import ( "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugolib/segments" "github.com/gohugoio/hugo/langs" + gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config" "github.com/gohugoio/hugo/markup/markup_config" "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/minifiers" @@ -399,7 +400,6 @@ func (c *Config) CompileConfig(logger loggers.Logger) error { hugo.DeprecateWithLogger("site config key paginate", "Use pagination.pagerSize instead.", "v0.128.0", logger.Logger()) c.Pagination.PagerSize = c.Paginate } - if c.PaginatePath != "" { hugo.DeprecateWithLogger("site config key paginatePath", "Use pagination.path instead.", "v0.128.0", logger.Logger()) c.Pagination.Path = c.PaginatePath @@ -410,12 +410,10 @@ func (c *Config) CompileConfig(logger loggers.Logger) error { hugo.DeprecateWithLogger("site config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0", logger.Logger()) c.Privacy.X.Disable = c.Privacy.Twitter.Disable } - if c.Privacy.Twitter.EnableDNT { hugo.DeprecateWithLogger("site config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0", logger.Logger()) c.Privacy.X.EnableDNT = c.Privacy.Twitter.EnableDNT } - if c.Privacy.Twitter.Simple { hugo.DeprecateWithLogger("site config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0", logger.Logger()) c.Privacy.X.Simple = c.Privacy.Twitter.Simple @@ -436,6 +434,45 @@ func (c *Config) CompileConfig(logger loggers.Logger) error { hugo.DeprecateWithLogger("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0", logger.Logger()) } + // Legacy render hook values. + alternativeDetails := fmt.Sprintf( + "Set to %q if previous value was false, or set to %q if previous value was true.", + gc.RenderHookUseEmbeddedNever, + gc.RenderHookUseEmbeddedFallback, + ) + if c.Markup.Goldmark.RenderHooks.Image.EnableDefault != nil { + alternative := "Use markup.goldmark.renderHooks.image.useEmbedded instead." + " " + alternativeDetails + hugo.DeprecateWithLogger("site config key markup.goldmark.renderHooks.image.enableDefault", alternative, "0.148.0", logger.Logger()) + if *c.Markup.Goldmark.RenderHooks.Image.EnableDefault { + c.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedFallback + } else { + c.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedNever + } + } + if c.Markup.Goldmark.RenderHooks.Link.EnableDefault != nil { + alternative := "Use markup.goldmark.renderHooks.link.useEmbedded instead." + " " + alternativeDetails + hugo.DeprecateWithLogger("site config key markup.goldmark.renderHooks.link.enableDefault", alternative, "0.148.0", logger.Logger()) + if *c.Markup.Goldmark.RenderHooks.Link.EnableDefault { + c.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedFallback + } else { + c.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedNever + } + } + + // Validate render hook configuration. + renderHookUseEmbeddedModes := []string{ + gc.RenderHookUseEmbeddedAlways, + gc.RenderHookUseEmbeddedAuto, + gc.RenderHookUseEmbeddedFallback, + gc.RenderHookUseEmbeddedNever, + } + if !slices.Contains(renderHookUseEmbeddedModes, c.Markup.Goldmark.RenderHooks.Image.UseEmbedded) { + return fmt.Errorf("site config markup.goldmark.renderHooks.image must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or")) + } + if !slices.Contains(renderHookUseEmbeddedModes, c.Markup.Goldmark.RenderHooks.Link.UseEmbedded) { + return fmt.Errorf("site config markup.goldmark.renderHooks.link must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or")) + } + c.C = &ConfigCompiled{ Timeout: timeout, BaseURL: baseURL, @@ -1082,13 +1119,11 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon // Adjust Goldmark config defaults for multilingual, single-host sites. if len(languagesConfig) > 1 && !isMultihost && !clone.Markup.Goldmark.DuplicateResourceFiles { - if !clone.Markup.Goldmark.DuplicateResourceFiles { - if clone.Markup.Goldmark.RenderHooks.Link.EnableDefault == nil { - clone.Markup.Goldmark.RenderHooks.Link.EnableDefault = types.NewBool(true) - } - if clone.Markup.Goldmark.RenderHooks.Image.EnableDefault == nil { - clone.Markup.Goldmark.RenderHooks.Image.EnableDefault = types.NewBool(true) - } + if clone.Markup.Goldmark.RenderHooks.Image.UseEmbedded == gc.RenderHookUseEmbeddedAuto { + clone.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedFallback + } + if clone.Markup.Goldmark.RenderHooks.Link.UseEmbedded == gc.RenderHookUseEmbeddedAuto { + clone.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedFallback } } diff --git a/config/allconfig/allconfig_integration_test.go b/config/allconfig/allconfig_integration_test.go index 8f6cacf84..79d2299de 100644 --- a/config/allconfig/allconfig_integration_test.go +++ b/config/allconfig/allconfig_integration_test.go @@ -2,12 +2,14 @@ package allconfig_test import ( "path/filepath" + "strings" "testing" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/hugolib" + gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config" "github.com/gohugoio/hugo/media" ) @@ -379,3 +381,28 @@ weight = 3 b.Assert(len(b.H.Sites), qt.Equals, 1) } + +// Issue 13535 +// We changed enablement of the embedded link and image render hooks from +// booleans to enums in v0.148.0. +func TestLegacyEmbeddedRenderHookEnablement(t *testing.T) { + files := ` +-- hugo.toml -- +[markup.goldmark.renderHooks.image] +#KEY_VALUE + +[markup.goldmark.renderHooks.link] +#KEY_VALUE +` + f := strings.ReplaceAll(files, "#KEY_VALUE", "enableDefault = false") + b := hugolib.Test(t, f) + c := b.H.Configs.Base.Markup.Goldmark.RenderHooks + b.Assert(c.Link.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedNever) + b.Assert(c.Image.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedNever) + + f = strings.ReplaceAll(files, "#KEY_VALUE", "enableDefault = true") + b = hugolib.Test(t, f) + c = b.H.Configs.Base.Markup.Goldmark.RenderHooks + b.Assert(c.Link.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedFallback) + b.Assert(c.Image.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedFallback) +} diff --git a/helpers/general.go b/helpers/general.go index 76275a6b9..5c3af9712 100644 --- a/helpers/general.go +++ b/helpers/general.go @@ -255,6 +255,27 @@ func SliceToLower(s []string) []string { return l } +// StringSliceToList formats a string slice into a human-readable list. +// It joins the elements of the slice s with commas, using an Oxford comma, +// and precedes the final element with the conjunction c. +func StringSliceToList(s []string, c string) string { + const defaultConjunction = "and" + + if c == "" { + c = defaultConjunction + } + if len(s) == 0 { + return "" + } + if len(s) == 1 { + return s[0] + } + if len(s) == 2 { + return fmt.Sprintf("%s %s %s", s[0], c, s[1]) + } + return fmt.Sprintf("%s, %s %s", strings.Join(s[:len(s)-1], ", "), c, s[len(s)-1]) +} + // IsWhitespace determines if the given rune is whitespace. func IsWhitespace(r rune) bool { return r == ' ' || r == '\t' || r == '\n' || r == '\r' diff --git a/helpers/general_test.go b/helpers/general_test.go index 686e95ded..6d2a78e02 100644 --- a/helpers/general_test.go +++ b/helpers/general_test.go @@ -18,9 +18,8 @@ import ( "strings" "testing" - "github.com/gohugoio/hugo/helpers" - qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/helpers" ) func TestResolveMarkup(t *testing.T) { @@ -304,3 +303,26 @@ func BenchmarkUniqueStrings(b *testing.B) { } }) } + +func TestStringSliceToList(t *testing.T) { + for _, tt := range []struct { + slice []string + conjunction string + want string + }{ + {[]string{}, "", ""}, + {[]string{"foo"}, "", "foo"}, + {[]string{"foo"}, "and", "foo"}, + {[]string{"foo", "bar"}, "", "foo and bar"}, + {[]string{"foo", "bar"}, "and", "foo and bar"}, + {[]string{"foo", "bar"}, "or", "foo or bar"}, + {[]string{"foo", "bar", "baz"}, "", "foo, bar, and baz"}, + {[]string{"foo", "bar", "baz"}, "and", "foo, bar, and baz"}, + {[]string{"foo", "bar", "baz"}, "or", "foo, bar, or baz"}, + } { + got := helpers.StringSliceToList(tt.slice, tt.conjunction) + if got != tt.want { + t.Errorf("StringSliceToList() got: %q, want: %q", got, tt.want) + } + } +} diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index 1f7f3411e..15e9a890c 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -29,6 +29,7 @@ import ( "github.com/spf13/cast" "github.com/gohugoio/hugo/markup/converter/hooks" + gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config" "github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/markup/converter" @@ -304,12 +305,17 @@ func (pco *pageContentOutput) initRenderHooks() error { } renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks - var ignoreInternal bool + var ignoreEmbedded bool + + // For multilingual single-host sites, "auto" becomes "fallback" + // earlier in the process. switch layoutDescriptor.Variant1 { case "link": - ignoreInternal = !renderHookConfig.Link.IsEnableDefault() + ignoreEmbedded = renderHookConfig.Link.UseEmbedded == gc.RenderHookUseEmbeddedNever || + renderHookConfig.Link.UseEmbedded == gc.RenderHookUseEmbeddedAuto case "image": - ignoreInternal = !renderHookConfig.Image.IsEnableDefault() + ignoreEmbedded = renderHookConfig.Image.UseEmbedded == gc.RenderHookUseEmbeddedNever || + renderHookConfig.Image.UseEmbedded == gc.RenderHookUseEmbeddedAuto } candidates := pco.po.p.s.renderFormats @@ -323,8 +329,8 @@ func (pco *pageContentOutput) initRenderHooks() error { return false } - if ignoreInternal && candidate.SubCategory() == tplimpl.SubCategoryEmbedded { - // Don't consider the internal hook templates. + if ignoreEmbedded && candidate.SubCategory() == tplimpl.SubCategoryEmbedded { + // Don't consider the embedded hook templates. return false } diff --git a/hugolib/site.go b/hugolib/site.go index acd3b5410..e37dbd5ef 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -406,6 +406,7 @@ func newHugoSites(cfg deps.DepsCfg, d *deps.Deps, pageTrees *pageTrees, sites [] MediaTypes: s.conf.MediaTypes.Config, DefaultOutputFormat: s.conf.DefaultOutputFormat, TaxonomySingularPlural: s.conf.Taxonomies, + RenderHooks: s.conf.Markup.Goldmark.RenderHooks, }, tplimpl.SiteOptions{ Site: s, TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps), diff --git a/markup/goldmark/goldmark_config/config.go b/markup/goldmark/goldmark_config/config.go index 04eb371d9..168a3b8fa 100644 --- a/markup/goldmark/goldmark_config/config.go +++ b/markup/goldmark/goldmark_config/config.go @@ -15,9 +15,13 @@ package goldmark_config const ( - AutoIDTypeGitHub = "github" - AutoIDTypeGitHubAscii = "github-ascii" - AutoIDTypeBlackfriday = "blackfriday" + AutoIDTypeBlackfriday = "blackfriday" + AutoIDTypeGitHub = "github" + AutoIDTypeGitHubAscii = "github-ascii" + RenderHookUseEmbeddedAlways = "always" + RenderHookUseEmbeddedAuto = "auto" + RenderHookUseEmbeddedFallback = "fallback" + RenderHookUseEmbeddedNever = "never" ) // Default holds the default Goldmark configuration. @@ -87,6 +91,14 @@ var Default = Config{ Block: false, }, }, + RenderHooks: RenderHooks{ + Image: ImageRenderHook{ + UseEmbedded: RenderHookUseEmbeddedAuto, + }, + Link: LinkRenderHook{ + UseEmbedded: RenderHookUseEmbeddedAuto, + }, + }, } // Config configures Goldmark. @@ -118,22 +130,24 @@ type RenderHooks struct { type ImageRenderHook struct { // Enable the default image render hook. // We need to know if it is set or not, hence the pointer. + // Deprecated: Use UseEmbedded instead. EnableDefault *bool -} -func (h ImageRenderHook) IsEnableDefault() bool { - return h.EnableDefault != nil && *h.EnableDefault + // When to use the embedded image render hook. + // One of auto, never, always, or fallback. Default is auto. + UseEmbedded string } // LinkRenderHook contains configuration for the link render hook. type LinkRenderHook struct { // Disable the default image render hook. // We need to know if it is set or not, hence the pointer. + // Deprecated: Use UseEmbedded instead. EnableDefault *bool -} -func (h LinkRenderHook) IsEnableDefault() bool { - return h.EnableDefault != nil && *h.EnableDefault + // When to use the embedded link render hook. + // One of auto, never, always, or fallback. Default is auto. + UseEmbedded string } type Extensions struct { diff --git a/tpl/tplimpl/render_hook_integration_test.go b/tpl/tplimpl/render_hook_integration_test.go index 45c6cced1..e67701594 100644 --- a/tpl/tplimpl/render_hook_integration_test.go +++ b/tpl/tplimpl/render_hook_integration_test.go @@ -192,3 +192,115 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA `alt4`, ) } + +// Issue 13535 +func TestEmbeddedLinkAndImageRenderHookConfig(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['home','rss','section','sitemap','taxonomy','term'] + +[markup.goldmark] +duplicateResourceFiles = false + +[markup.goldmark.renderHooks.image] +#KEY_VALUE + +[markup.goldmark.renderHooks.link] +#KEY_VALUE + +#LANGUAGES +-- content/s1/p1/index.md -- +--- +title: p1 +--- +[p2](p2) + +[a](a.txt) + +![b](b.jpg) +-- content/s1/p1/a.txt -- +-- content/s1/p1/b.jpg -- +-- content/s1/p2.md -- +--- +title: p2 +--- +-- layouts/all.html -- +{{ .Content }} +` + + const customHooks string = ` +-- layouts/s1/_markup/render-link.html -- +custom link render hook: {{ .Text }}|{{ .Destination }} +-- layouts/s1/_markup/render-image.html -- +custom image render hook: {{ .Text }}|{{ .Destination }} +` + + const languages string = ` +[languages.en] +[languages.fr] +` + + const ( + fileToCheck = "public/s1/p1/index.html" + wantCustom string = "

custom link render hook: p2|p2

\n

custom link render hook: a|a.txt

\n

custom image render hook: b|b.jpg

" + wantEmbedded string = "

p2

\n

a

\n

\"b\"

" + wantGoldmark string = "

p2

\n

a

\n

\"b\"

" + ) + + tests := []struct { + id string // the test id + isMultilingual bool // whether the site is multilingual single-host + hasCustomHooks bool // whether the site has custom link and image render hooks + keyValuePair string // the enableDefault (deprecated in v0.148.0) or useEmbedded key-value pair + want string // the expected content of public/s1/p1/index.html + }{ + {"01", false, false, "", wantGoldmark}, // monolingual + {"02", false, false, "enableDefault = false", wantGoldmark}, // monolingual, enableDefault = false + {"03", false, false, "enableDefault = true", wantEmbedded}, // monolingual, enableDefault = true + {"04", false, false, "useEmbedded = 'always'", wantEmbedded}, // monolingual, useEmbedded = 'always' + {"05", false, false, "useEmbedded = 'auto'", wantGoldmark}, // monolingual, useEmbedded = 'auto' + {"06", false, false, "useEmbedded = 'fallback'", wantEmbedded}, // monolingual, useEmbedded = 'fallback' + {"07", false, false, "useEmbedded = 'never'", wantGoldmark}, // monolingual, useEmbedded = 'never' + {"08", false, true, "", wantCustom}, // monolingual, with custom hooks + {"09", false, true, "enableDefault = false", wantCustom}, // monolingual, with custom hooks, enableDefault = false + {"10", false, true, "enableDefault = true", wantCustom}, // monolingual, with custom hooks, enableDefault = true + {"11", false, true, "useEmbedded = 'always'", wantEmbedded}, // monolingual, with custom hooks, useEmbedded = 'always' + {"12", false, true, "useEmbedded = 'auto'", wantCustom}, // monolingual, with custom hooks, useEmbedded = 'auto' + {"13", false, true, "useEmbedded = 'fallback'", wantCustom}, // monolingual, with custom hooks, useEmbedded = 'fallback' + {"14", false, true, "useEmbedded = 'never'", wantCustom}, // monolingual, with custom hooks, useEmbedded = 'never' + {"15", true, false, "", wantEmbedded}, // multilingual + {"16", true, false, "enableDefault = false", wantGoldmark}, // multilingual, enableDefault = false + {"17", true, false, "enableDefault = true", wantEmbedded}, // multilingual, enableDefault = true + {"18", true, false, "useEmbedded = 'always'", wantEmbedded}, // multilingual, useEmbedded = 'always' + {"19", true, false, "useEmbedded = 'auto'", wantEmbedded}, // multilingual, useEmbedded = 'auto' + {"20", true, false, "useEmbedded = 'fallback'", wantEmbedded}, // multilingual, useEmbedded = 'fallback' + {"21", true, false, "useEmbedded = 'never'", wantGoldmark}, // multilingual, useEmbedded = 'never' + {"22", true, true, "", wantCustom}, // multilingual, with custom hooks + {"23", true, true, "enableDefault = false", wantCustom}, // multilingual, with custom hooks, enableDefault = false + {"24", true, true, "enableDefault = true", wantCustom}, // multilingual, with custom hooks, enableDefault = true + {"25", true, true, "useEmbedded = 'always'", wantEmbedded}, // multilingual, with custom hooks, useEmbedded = 'always' + {"26", true, true, "useEmbedded = 'auto'", wantCustom}, // multilingual, with custom hooks, useEmbedded = 'auto' + {"27", true, true, "useEmbedded = 'fallback'", wantCustom}, // multilingual, with custom hooks, useEmbedded = 'fallback' + {"28", true, true, "useEmbedded = 'never'", wantCustom}, // multilingual, with custom hooks, useEmbedded = 'never' + } + + for _, tt := range tests { + t.Run(tt.id, func(t *testing.T) { + t.Parallel() + + f := files + if tt.isMultilingual { + f = strings.ReplaceAll(f, "#LANGUAGES", languages) + } + if tt.hasCustomHooks { + f = f + customHooks + } + f = strings.ReplaceAll(f, "#KEY_VALUE", tt.keyValuePair) + + b := hugolib.Test(t, f) + b.AssertFileContent(fileToCheck, tt.want) + }) + } +} diff --git a/tpl/tplimpl/templatestore.go b/tpl/tplimpl/templatestore.go index 23c821cac..008b743c7 100644 --- a/tpl/tplimpl/templatestore.go +++ b/tpl/tplimpl/templatestore.go @@ -44,6 +44,7 @@ import ( "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugolib/doctree" "github.com/gohugoio/hugo/identity" + gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config" "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/metrics" "github.com/gohugoio/hugo/output" @@ -202,6 +203,9 @@ type StoreOptions struct { // Whether we are in watch or server mode. Watching bool + // The render hook configuration. + RenderHooks gc.RenderHooks + // compiled. legacyMappingTaxonomy map[string]legacyOrdinalMapping legacyMappingTerm map[string]legacyOrdinalMapping @@ -1422,20 +1426,36 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo var ti *TemplInfo var err error - if pi.Type() == paths.TypeShortcode { - ti, err = s.insertShortcode(pi, fi, partialRebuild, s.treeShortcodes) - if err != nil || ti == nil { - return err + var insertFunc func() (*TemplInfo, error) + + insertFunc = func() (*TemplInfo, error) { + return s.insertTemplate(pi, fi, SubCategoryMain, partialRebuild, s.treeMain) + } + + switch pi.Type() { + case paths.TypeShortcode: + insertFunc = func() (*TemplInfo, error) { + return s.insertShortcode(pi, fi, partialRebuild, s.treeShortcodes) } - } else { - ti, err = s.insertTemplate(pi, fi, SubCategoryMain, partialRebuild, s.treeMain) + case paths.TypeMarkup: + skipImageRenderHook := pi.Name() == "render-image.html" && s.opts.RenderHooks.Image.UseEmbedded == "always" + skipLinkRenderHook := pi.Name() == "render-link.html" && s.opts.RenderHooks.Link.UseEmbedded == "always" + if skipImageRenderHook || skipLinkRenderHook { + insertFunc = nil + } + } + + if insertFunc != nil { + ti, err = insertFunc() if err != nil || ti == nil { return err } } - if err := s.tns.readTemplateInto(ti); err != nil { - return err + if ti != nil { + if err := s.tns.readTemplateInto(ti); err != nil { + return err + } } return nil