Reimplement and simplify Hugo's template system - hugo - [fork] hugo port for 9front
(HTM) git clone git@git.drkhsh.at/hugo.git
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) Submodules
(DIR) README
(DIR) LICENSE
---
(DIR) commit 83cfdd78ca6469e6d7265323d9fad1448880e559
(DIR) parent 812ea0b325b084eacee7492390b4f9d1aba5b6cf
(HTM) Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Date: Sun, 6 Apr 2025 19:55:35 +0200
Reimplement and simplify Hugo's template system
See #13541 for details.
Fixes #13545
Fixes #13515
Closes #7964
Closes #13365
Closes #12988
Closes #4891
Diffstat:
M commands/server.go | 16 ++++++++--------
M common/constants/constants.go | 1 +
M common/hstrings/strings.go | 10 +++++-----
M common/maps/cache.go | 19 +++++++++++++++++++
M common/maps/ordered.go | 12 +++++++++++-
M common/paths/pathparser.go | 350 +++++++++++++++++++++++--------
M common/paths/pathparser_test.go | 168 ++++++++++++++++++++++++++++---
D common/paths/pathtype_string.go | 27 ---------------------------
A common/paths/type_string.go | 32 +++++++++++++++++++++++++++++++
M common/types/types.go | 10 ++++++++++
M config/allconfig/allconfig.go | 19 ++++++++++++++++++-
M create/content.go | 4 ++--
M deps/deps.go | 47 +++++++++++++------------------
M hugolib/alias.go | 39 +++++++++++++++++++------------
M hugolib/alias_test.go | 21 +++++++++++++++++----
M hugolib/content_factory.go | 4 ++--
M hugolib/content_map.go | 9 ++++-----
M hugolib/content_map_page.go | 18 ++++++++++--------
M hugolib/content_map_test.go | 23 ++++++++++++++++-------
M hugolib/content_render_hooks_test.… | 93 +++++++++++++++++++++++++++++++
M hugolib/doctree/simpletree.go | 182 ++++++++++++++++++++++++++++---
M hugolib/doctree/support.go | 10 +++-------
M hugolib/doctree/treeshifttree.go | 18 ++++++++++++++----
M hugolib/hugo_sites.go | 2 --
M hugolib/hugo_sites_build.go | 56 ++++++++++++++++++++++---------
M hugolib/hugo_smoke_test.go | 13 +++++++------
M hugolib/integrationtest_builder.go | 18 +++++++++++++++---
M hugolib/page.go | 81 ++++++++++++++-----------------
M hugolib/page__common.go | 4 ----
M hugolib/page__content.go | 25 +++++++++++--------------
M hugolib/page__meta.go | 11 +++++++----
M hugolib/page__per_output.go | 153 +++++++++++++++----------------
M hugolib/pages_capture.go | 17 ++++++++++-------
M hugolib/pagesfromdata/pagesfromgot… | 8 ++++----
M hugolib/pagesfromdata/pagesfromgot… | 3 ++-
M hugolib/paginator_test.go | 21 ++++++++++++++-------
M hugolib/rebuild_test.go | 147 +++++++++++++++++++++++++++----
M hugolib/shortcode.go | 94 +++++++++++++++++--------------
M hugolib/shortcode_test.go | 28 ++++++++++++++--------------
M hugolib/site.go | 76 ++++++++++++++++++++++++-------
M hugolib/site_output.go | 3 ++-
M hugolib/site_output_test.go | 3 ++-
M hugolib/site_render.go | 8 ++++----
M hugolib/site_test.go | 3 +--
M hugolib/taxonomy_test.go | 2 ++
M hugolib/template_test.go | 57 ++++++++++++++++++-------------
M identity/identity.go | 4 ++++
M internal/js/esbuild/batch.go | 8 ++++----
M langs/i18n/i18n_test.go | 3 ---
M markup/goldmark/codeblocks/codeblo… | 6 +++---
M media/builtin.go | 3 +++
M media/config.go | 5 ++++-
M media/config_test.go | 2 +-
M media/mediaType.go | 10 +++++-----
M output/docshelper.go | 86 ++-----------------------------
D output/layouts/layout.go | 336 -------------------------------
D output/layouts/layout_test.go | 982 -------------------------------
M output/outputFormat.go | 26 ++++++++++++++++++++++++--
M output/outputFormat_test.go | 4 ++--
M resources/kinds/kinds.go | 1 +
M resources/page/page.go | 4 ++--
M resources/page/page_paths.go | 2 +-
M resources/page/pages_related.go | 2 +-
M resources/page/testhelpers_test.go | 2 +-
M resources/resource_spec.go | 3 ---
M resources/resource_transformers/te… | 15 +++++++--------
M testscripts/commands/hugo_printunu… | 2 +-
M tpl/collections/apply.go | 4 +---
D tpl/collections/apply_test.go | 104 -------------------------------
M tpl/internal/go_templates/htmltemp… | 27 +++++++++++++++++++++++++++
M tpl/internal/go_templates/htmltemp… | 2 +-
M tpl/internal/go_templates/texttemp… | 2 +-
M tpl/math/init.go | 2 +-
M tpl/math/math.go | 16 +++++++++-------
M tpl/math/math_test.go | 48 ++++++++++++++++----------------
M tpl/partials/partials.go | 50 +++++++++++--------------------
M tpl/partials/partials_integration_… | 10 +++++-----
M tpl/template.go | 134 ++-----------------------------
D tpl/template_info.go | 57 -------------------------------
M tpl/template_test.go | 14 +-------------
M tpl/templates/defer_integration_te… | 75 +++++++++++++++++++++++++++++++
M tpl/templates/templates.go | 4 ++--
A tpl/tplimpl/category_string.go | 30 ++++++++++++++++++++++++++++++
R tpl/tplimpl/embedded/templates/_de… | 0
R tpl/tplimpl/embedded/templates/_de… | 0
R tpl/tplimpl/embedded/templates/_de… | 0
R tpl/tplimpl/embedded/templates/_de… | 0
R tpl/tplimpl/embedded/templates/par… | 0
A tpl/tplimpl/embedded/templates/_pa… | 23 +++++++++++++++++++++++
R tpl/tplimpl/embedded/templates/goo… | 0
R tpl/tplimpl/embedded/templates/ope… | 0
A tpl/tplimpl/embedded/templates/_pa… | 154 +++++++++++++++++++++++++++++++
R tpl/tplimpl/embedded/templates/sch… | 0
R tpl/tplimpl/embedded/templates/twi… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
R tpl/tplimpl/embedded/templates/sho… | 0
D tpl/tplimpl/embedded/templates/dis… | 23 -----------------------
D tpl/tplimpl/embedded/templates/pag… | 154 -------------------------------
R tpl/tplimpl/embedded/templates/_de… | 0
R tpl/tplimpl/embedded/templates/_de… | 0
R tpl/tplimpl/embedded/templates/_de… | 0
R tpl/tplimpl/embedded/templates/_de… | 0
A tpl/tplimpl/legacy.go | 130 +++++++++++++++++++++++++++++++
M tpl/tplimpl/render_hook_integratio… | 2 +-
D tpl/tplimpl/shortcodes.go | 153 -------------------------------
D tpl/tplimpl/shortcodes_test.go | 91 -------------------------------
A tpl/tplimpl/subcategory_string.go | 25 +++++++++++++++++++++++++
D tpl/tplimpl/template.go | 1235 -------------------------------
D tpl/tplimpl/templateFuncster.go | 14 --------------
D tpl/tplimpl/templateProvider.go | 51 -------------------------------
D tpl/tplimpl/template_ast_transform… | 381 -------------------------------
D tpl/tplimpl/template_ast_transform… | 161 -------------------------------
D tpl/tplimpl/template_errors.go | 64 -------------------------------
M tpl/tplimpl/template_funcs.go | 127 +------------------------------
M tpl/tplimpl/template_funcs_test.go | 2 +-
A tpl/tplimpl/template_info.go | 46 +++++++++++++++++++++++++++++++
D tpl/tplimpl/template_test.go | 40 -------------------------------
A tpl/tplimpl/templatedescriptor.go | 225 +++++++++++++++++++++++++++++++
A tpl/tplimpl/templatedescriptor_tes… | 104 +++++++++++++++++++++++++++++++
A tpl/tplimpl/templates.go | 331 +++++++++++++++++++++++++++++++
A tpl/tplimpl/templatestore.go | 1854 +++++++++++++++++++++++++++++++
A tpl/tplimpl/templatestore_integrat… | 842 +++++++++++++++++++++++++++++++
A tpl/tplimpl/templatetransform.go | 349 +++++++++++++++++++++++++++++++
M tpl/tplimpl/tplimpl_integration_te… | 59 ++-----------------------------
A tpl/tplimplinit/tplimplinit.go | 96 +++++++++++++++++++++++++++++++
142 files changed, 5866 insertions(+), 4920 deletions(-)
---
(DIR) diff --git a/commands/server.go b/commands/server.go
@@ -23,6 +23,7 @@ import (
"errors"
"fmt"
"io"
+ "maps"
"net"
"net/http"
_ "net/http/pprof"
@@ -48,6 +49,7 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls"
@@ -57,7 +59,6 @@ import (
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/livereload"
- "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/transform"
"github.com/gohugoio/hugo/transform/livereloadinject"
"github.com/spf13/afero"
@@ -65,7 +66,6 @@ import (
"github.com/spf13/fsync"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
- "maps"
)
var (
@@ -897,16 +897,16 @@ func (c *serverCommand) serve() error {
// To allow the en user to change the error template while the server is running, we use
// the freshest template we can provide.
var (
- errTempl tpl.Template
- templHandler tpl.TemplateHandler
+ errTempl *tplimpl.TemplInfo
+ templHandler *tplimpl.TemplateStore
)
- getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (tpl.Template, tpl.TemplateHandler) {
+ getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (*tplimpl.TemplInfo, *tplimpl.TemplateStore) {
if h == nil {
return errTempl, templHandler
}
- templHandler := h.Tmpl()
- errTempl, found := templHandler.Lookup("_server/error.html")
- if !found {
+ templHandler := h.GetTemplateStore()
+ errTempl := templHandler.LookupByPath("/_server/error.html")
+ if errTempl == nil {
panic("template server/error.html not found")
}
return errTempl, templHandler
(DIR) diff --git a/common/constants/constants.go b/common/constants/constants.go
@@ -23,6 +23,7 @@ const (
WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html"
WarnGoldmarkRawHTML = "warning-goldmark-raw-html"
+ WarnPartialSuperfluousPrefix = "warning-partial-superfluous-prefix"
)
// Field/method names with special meaning.
(DIR) diff --git a/common/hstrings/strings.go b/common/hstrings/strings.go
@@ -16,11 +16,11 @@ package hstrings
import (
"fmt"
"regexp"
+ "slices"
"strings"
"sync"
"github.com/gohugoio/hugo/compare"
- "slices"
)
var _ compare.Eqer = StringEqualFold("")
@@ -128,7 +128,7 @@ func ToString(v any) (string, bool) {
return "", false
}
-type Tuple struct {
- First string
- Second string
-}
+type (
+ Strings2 [2]string
+ Strings3 [3]string
+)
(DIR) diff --git a/common/maps/cache.go b/common/maps/cache.go
@@ -69,6 +69,14 @@ func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
return v, nil
}
+// Contains returns whether the given key exists in the cache.
+func (c *Cache[K, T]) Contains(key K) bool {
+ c.RLock()
+ _, found := c.m[key]
+ c.RUnlock()
+ return found
+}
+
// InitAndGet initializes the cache if not already done and returns the value for the given key.
// The init state will be reset on Reset or Drain.
func (c *Cache[K, T]) InitAndGet(key K, init func(get func(key K) (T, bool), set func(key K, value T)) error) (T, error) {
@@ -108,6 +116,17 @@ func (c *Cache[K, T]) Set(key K, value T) {
c.Unlock()
}
+// SetIfAbsent sets the given key to the given value if the key does not already exist in the cache.
+func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
+ c.RLock()
+ if _, found := c.get(key); !found {
+ c.RUnlock()
+ c.Set(key, value)
+ } else {
+ c.RUnlock()
+ }
+}
+
func (c *Cache[K, T]) set(key K, value T) {
c.m[key] = value
}
(DIR) diff --git a/common/maps/ordered.go b/common/maps/ordered.go
@@ -14,8 +14,9 @@
package maps
import (
- "github.com/gohugoio/hugo/common/hashing"
"slices"
+
+ "github.com/gohugoio/hugo/common/hashing"
)
// Ordered is a map that can be iterated in the order of insertion.
@@ -57,6 +58,15 @@ func (m *Ordered[K, T]) Get(key K) (T, bool) {
return value, found
}
+// Has returns whether the given key exists in the map.
+func (m *Ordered[K, T]) Has(key K) bool {
+ if m == nil {
+ return false
+ }
+ _, found := m.values[key]
+ return found
+}
+
// Delete deletes the value for the given key.
func (m *Ordered[K, T]) Delete(key K) {
if m == nil {
(DIR) diff --git a/common/paths/pathparser.go b/common/paths/pathparser.go
@@ -23,6 +23,11 @@ import (
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/resources/kinds"
+)
+
+const (
+ identifierBaseof = "baseof"
)
// PathParser parses a path into a Path.
@@ -33,6 +38,10 @@ type PathParser struct {
// Reports whether the given language is disabled.
IsLangDisabled func(string) bool
+ // IsOutputFormat reports whether the given name is a valid output format.
+ // The second argument is optional.
+ IsOutputFormat func(name, ext string) bool
+
// Reports whether the given ext is a content file.
IsContentExt func(string) bool
}
@@ -83,13 +92,10 @@ func (pp *PathParser) Parse(c, s string) *Path {
}
func (pp *PathParser) newPath(component string) *Path {
- return &Path{
- component: component,
- posContainerLow: -1,
- posContainerHigh: -1,
- posSectionHigh: -1,
- posIdentifierLanguage: -1,
- }
+ p := &Path{}
+ p.reset()
+ p.component = component
+ return p
}
func (pp *PathParser) parse(component, s string) (*Path, error) {
@@ -114,10 +120,91 @@ func (pp *PathParser) parse(component, s string) (*Path, error) {
return p, nil
}
-func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
- hasLang := pp.LanguageIndex != nil
- hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
+func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot int) {
+ if p.posContainerHigh != -1 {
+ return
+ }
+ mayHaveLang := pp.LanguageIndex != nil
+ mayHaveLang = mayHaveLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
+ mayHaveOutputFormat := component == files.ComponentFolderLayouts
+ mayHaveKind := mayHaveOutputFormat
+
+ var found bool
+ var high int
+ if len(p.identifiers) > 0 {
+ high = lastDot
+ } else {
+ high = len(p.s)
+ }
+ id := types.LowHigh[string]{Low: i + 1, High: high}
+ sid := p.s[id.Low:id.High]
+
+ if len(p.identifiers) == 0 {
+ // The first is always the extension.
+ p.identifiers = append(p.identifiers, id)
+ found = true
+
+ // May also be the output format.
+ if mayHaveOutputFormat && pp.IsOutputFormat(sid, "") {
+ p.posIdentifierOutputFormat = 0
+ }
+ } else {
+
+ var langFound bool
+
+ if mayHaveLang {
+ var disabled bool
+ _, langFound = pp.LanguageIndex[sid]
+ if !langFound {
+ disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(sid)
+ if disabled {
+ p.disabled = true
+ langFound = true
+ }
+ }
+ found = langFound
+ if langFound {
+ p.identifiers = append(p.identifiers, id)
+ p.posIdentifierLanguage = len(p.identifiers) - 1
+
+ }
+ }
+
+ if !found && mayHaveOutputFormat {
+ // At this point we may already have resolved an output format,
+ // but we need to keep looking for a more specific one, e.g. amp before html.
+ // Use both name and extension to prevent
+ // false positives on the form css.html.
+ if pp.IsOutputFormat(sid, p.Ext()) {
+ found = true
+ p.identifiers = append(p.identifiers, id)
+ p.posIdentifierOutputFormat = len(p.identifiers) - 1
+ }
+ }
+
+ if !found && mayHaveKind {
+ if kinds.GetKindMain(sid) != "" {
+ found = true
+ p.identifiers = append(p.identifiers, id)
+ p.posIdentifierKind = len(p.identifiers) - 1
+ }
+ }
+
+ if !found && sid == identifierBaseof {
+ found = true
+ p.identifiers = append(p.identifiers, id)
+ p.posIdentifierBaseof = len(p.identifiers) - 1
+ }
+
+ if !found {
+ p.identifiers = append(p.identifiers, id)
+ p.identifiersUnknown = append(p.identifiersUnknown, len(p.identifiers)-1)
+ }
+
+ }
+}
+func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
if runtime.GOOS == "windows" {
s = path.Clean(filepath.ToSlash(s))
if s == "." {
@@ -140,46 +227,21 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
p.s = s
slashCount := 0
+ lastDot := 0
for i := len(s) - 1; i >= 0; i-- {
c := s[i]
switch c {
case '.':
- if p.posContainerHigh == -1 {
- var high int
- if len(p.identifiers) > 0 {
- high = p.identifiers[len(p.identifiers)-1].Low - 1
- } else {
- high = len(p.s)
- }
- id := types.LowHigh[string]{Low: i + 1, High: high}
- if len(p.identifiers) == 0 {
- p.identifiers = append(p.identifiers, id)
- } else if len(p.identifiers) == 1 {
- // Check for a valid language.
- s := p.s[id.Low:id.High]
-
- if hasLang {
- var disabled bool
- _, langFound := pp.LanguageIndex[s]
- if !langFound {
- disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(s)
- if disabled {
- p.disabled = true
- langFound = true
- }
- }
- if langFound {
- p.posIdentifierLanguage = 1
- p.identifiers = append(p.identifiers, id)
- }
- }
- }
- }
+ pp.parseIdentifier(component, s, p, i, lastDot)
+ lastDot = i
case '/':
slashCount++
if p.posContainerHigh == -1 {
+ if lastDot > 0 {
+ pp.parseIdentifier(component, s, p, i, lastDot)
+ }
p.posContainerHigh = i + 1
} else if p.posContainerLow == -1 {
p.posContainerLow = i + 1
@@ -194,22 +256,41 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
isContent := isContentComponent && pp.IsContentExt(p.Ext())
id := p.identifiers[len(p.identifiers)-1]
- b := p.s[p.posContainerHigh : id.Low-1]
- if isContent {
- switch b {
- case "index":
- p.bundleType = PathTypeLeaf
- case "_index":
- p.bundleType = PathTypeBranch
- default:
- p.bundleType = PathTypeContentSingle
+
+ if id.High > p.posContainerHigh {
+ b := p.s[p.posContainerHigh:id.High]
+ if isContent {
+ switch b {
+ case "index":
+ p.pathType = TypeLeaf
+ case "_index":
+ p.pathType = TypeBranch
+ default:
+ p.pathType = TypeContentSingle
+ }
+
+ if slashCount == 2 && p.IsLeafBundle() {
+ p.posSectionHigh = 0
+ }
+ } else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
+ p.pathType = TypeContentData
}
+ }
+
+ }
- if slashCount == 2 && p.IsLeafBundle() {
- p.posSectionHigh = 0
+ if component == files.ComponentFolderLayouts {
+ if p.posIdentifierBaseof != -1 {
+ p.pathType = TypeBaseof
+ } else {
+ pth := p.Path()
+ if strings.Contains(pth, "/_shortcodes/") {
+ p.pathType = TypeShortcode
+ } else if strings.Contains(pth, "/_markup/") {
+ p.pathType = TypeMarkup
+ } else if strings.HasPrefix(pth, "/_partials/") {
+ p.pathType = TypePartial
}
- } else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
- p.bundleType = PathTypeContentData
}
}
@@ -218,35 +299,44 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
func ModifyPathBundleTypeResource(p *Path) {
if p.IsContent() {
- p.bundleType = PathTypeContentResource
+ p.pathType = TypeContentResource
} else {
- p.bundleType = PathTypeFile
+ p.pathType = TypeFile
}
}
-type PathType int
+//go:generate stringer -type Type
+
+type Type int
const (
+
// A generic resource, e.g. a JSON file.
- PathTypeFile PathType = iota
+ TypeFile Type = iota
// All below are content files.
// A resource of a content type with front matter.
- PathTypeContentResource
+ TypeContentResource
// E.g. /blog/my-post.md
- PathTypeContentSingle
+ TypeContentSingle
// All below are bundled content files.
// Leaf bundles, e.g. /blog/my-post/index.md
- PathTypeLeaf
+ TypeLeaf
// Branch bundles, e.g. /blog/_index.md
- PathTypeBranch
+ TypeBranch
// Content data file, _content.gotmpl.
- PathTypeContentData
+ TypeContentData
+
+ // Layout types.
+ TypeMarkup
+ TypeShortcode
+ TypePartial
+ TypeBaseof
)
type Path struct {
@@ -257,13 +347,17 @@ type Path struct {
posContainerHigh int
posSectionHigh int
- component string
- bundleType PathType
+ component string
+ pathType Type
identifiers []types.LowHigh[string]
- posIdentifierLanguage int
- disabled bool
+ posIdentifierLanguage int
+ posIdentifierOutputFormat int
+ posIdentifierKind int
+ posIdentifierBaseof int
+ identifiersUnknown []int
+ disabled bool
trimLeadingSlash bool
@@ -293,9 +387,12 @@ func (p *Path) reset() {
p.posContainerHigh = -1
p.posSectionHigh = -1
p.component = ""
- p.bundleType = 0
+ p.pathType = 0
p.identifiers = p.identifiers[:0]
p.posIdentifierLanguage = -1
+ p.posIdentifierOutputFormat = -1
+ p.posIdentifierKind = -1
+ p.posIdentifierBaseof = -1
p.disabled = false
p.trimLeadingSlash = false
p.unnormalized = nil
@@ -316,6 +413,9 @@ func (p *Path) norm(s string) string {
// IdentifierBase satisfies identity.Identity.
func (p *Path) IdentifierBase() string {
+ if p.Component() == files.ComponentFolderLayouts {
+ return p.Path()
+ }
return p.Base()
}
@@ -332,6 +432,13 @@ func (p *Path) Container() string {
return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1])
}
+func (p *Path) String() string {
+ if p == nil {
+ return "<nil>"
+ }
+ return p.Path()
+}
+
// ContainerDir returns the container directory for this path.
// For content bundles this will be the parent directory.
func (p *Path) ContainerDir() string {
@@ -352,13 +459,13 @@ func (p *Path) Section() string {
// IsContent returns true if the path is a content file (e.g. mypost.md).
// Note that this will also return true for content files in a bundle.
func (p *Path) IsContent() bool {
- return p.BundleType() >= PathTypeContentResource
+ return p.Type() >= TypeContentResource && p.Type() <= TypeContentData
}
// isContentPage returns true if the path is a content file (e.g. mypost.md),
// but nof if inside a leaf bundle.
func (p *Path) isContentPage() bool {
- return p.BundleType() >= PathTypeContentSingle
+ return p.Type() >= TypeContentSingle && p.Type() <= TypeContentData
}
// Name returns the last element of path.
@@ -398,10 +505,26 @@ func (p *Path) BaseNameNoIdentifier() string {
// NameNoIdentifier returns the last element of path without any identifier (e.g. no extension).
func (p *Path) NameNoIdentifier() string {
+ lowHigh := p.nameLowHigh()
+ return p.s[lowHigh.Low:lowHigh.High]
+}
+
+func (p *Path) nameLowHigh() types.LowHigh[string] {
if len(p.identifiers) > 0 {
- return p.s[p.posContainerHigh : p.identifiers[len(p.identifiers)-1].Low-1]
+ lastID := p.identifiers[len(p.identifiers)-1]
+ if p.posContainerHigh == lastID.Low {
+ // The last identifier is the name.
+ return lastID
+ }
+ return types.LowHigh[string]{
+ Low: p.posContainerHigh,
+ High: p.identifiers[len(p.identifiers)-1].Low - 1,
+ }
+ }
+ return types.LowHigh[string]{
+ Low: p.posContainerHigh,
+ High: len(p.s),
}
- return p.s[p.posContainerHigh:]
}
// Dir returns all but the last element of path, typically the path's directory.
@@ -421,6 +544,11 @@ func (p *Path) Path() (d string) {
return p.norm(p.s)
}
+// PathNoLeadingSlash returns the full path without the leading slash.
+func (p *Path) PathNoLeadingSlash() string {
+ return p.Path()[1:]
+}
+
// Unnormalized returns the Path with the original case preserved.
func (p *Path) Unnormalized() *Path {
return p.unnormalized
@@ -436,6 +564,28 @@ func (p *Path) PathNoIdentifier() string {
return p.base(false, false)
}
+// PathBeforeLangAndOutputFormatAndExt returns the path up to the first identifier that is not a language or output format.
+func (p *Path) PathBeforeLangAndOutputFormatAndExt() string {
+ if len(p.identifiers) == 0 {
+ return p.norm(p.s)
+ }
+ i := p.identifierIndex(0)
+
+ if j := p.posIdentifierOutputFormat; i == -1 || (j != -1 && j < i) {
+ i = j
+ }
+ if j := p.posIdentifierLanguage; i == -1 || (j != -1 && j < i) {
+ i = j
+ }
+
+ if i == -1 {
+ return p.norm(p.s)
+ }
+
+ id := p.identifiers[i]
+ return p.norm(p.s[:id.Low-1])
+}
+
// PathRel returns the path relative to the given owner.
func (p *Path) PathRel(owner *Path) string {
ob := owner.Base()
@@ -462,6 +612,21 @@ func (p *Path) Base() string {
return p.base(!p.isContentPage(), p.IsBundle())
}
+// Used in template lookups.
+// For pages with Type set, we treat that as the section.
+func (p *Path) BaseReTyped(typ string) (d string) {
+ base := p.Base()
+ if typ == "" || p.Section() == typ {
+ return base
+ }
+ d = "/" + typ
+ if p.posSectionHigh != -1 {
+ d += base[p.posSectionHigh:]
+ }
+ d = p.norm(d)
+ return
+}
+
// BaseNoLeadingSlash returns the base path without the leading slash.
func (p *Path) BaseNoLeadingSlash() string {
return p.Base()[1:]
@@ -477,11 +642,12 @@ func (p *Path) base(preserveExt, isBundle bool) string {
return p.norm(p.s)
}
- id := p.identifiers[len(p.identifiers)-1]
- high := id.Low - 1
+ var high int
if isBundle {
high = p.posContainerHigh - 1
+ } else {
+ high = p.nameLowHigh().High
}
if high == 0 {
@@ -493,7 +659,7 @@ func (p *Path) base(preserveExt, isBundle bool) string {
}
// For txt files etc. we want to preserve the extension.
- id = p.identifiers[0]
+ id := p.identifiers[0]
return p.norm(p.s[:high] + p.s[id.Low-1:id.High])
}
@@ -502,8 +668,16 @@ func (p *Path) Ext() string {
return p.identifierAsString(0)
}
+func (p *Path) OutputFormat() string {
+ return p.identifierAsString(p.posIdentifierOutputFormat)
+}
+
+func (p *Path) Kind() string {
+ return p.identifierAsString(p.posIdentifierKind)
+}
+
func (p *Path) Lang() string {
- return p.identifierAsString(1)
+ return p.identifierAsString(p.posIdentifierLanguage)
}
func (p *Path) Identifier(i int) string {
@@ -522,28 +696,36 @@ func (p *Path) Identifiers() []string {
return ids
}
-func (p *Path) BundleType() PathType {
- return p.bundleType
+func (p *Path) IdentifiersUnknown() []string {
+ ids := make([]string, len(p.identifiersUnknown))
+ for i, id := range p.identifiersUnknown {
+ ids[i] = p.s[p.identifiers[id].Low:p.identifiers[id].High]
+ }
+ return ids
+}
+
+func (p *Path) Type() Type {
+ return p.pathType
}
func (p *Path) IsBundle() bool {
- return p.bundleType >= PathTypeLeaf
+ return p.pathType >= TypeLeaf && p.pathType <= TypeContentData
}
func (p *Path) IsBranchBundle() bool {
- return p.bundleType == PathTypeBranch
+ return p.pathType == TypeBranch
}
func (p *Path) IsLeafBundle() bool {
- return p.bundleType == PathTypeLeaf
+ return p.pathType == TypeLeaf
}
func (p *Path) IsContentData() bool {
- return p.bundleType == PathTypeContentData
+ return p.pathType == TypeContentData
}
-func (p Path) ForBundleType(t PathType) *Path {
- p.bundleType = t
+func (p Path) ForBundleType(t Type) *Path {
+ p.pathType = t
return &p
}
(DIR) diff --git a/common/paths/pathparser_test.go b/common/paths/pathparser_test.go
@@ -18,6 +18,7 @@ import (
"testing"
"github.com/gohugoio/hugo/hugofs/files"
+ "github.com/gohugoio/hugo/resources/kinds"
qt "github.com/frankban/quicktest"
)
@@ -26,10 +27,18 @@ var testParser = &PathParser{
LanguageIndex: map[string]int{
"no": 0,
"en": 1,
+ "fr": 2,
},
IsContentExt: func(ext string) bool {
return ext == "md"
},
+ IsOutputFormat: func(name, ext string) bool {
+ switch name {
+ case "html", "amp", "csv", "rss":
+ return true
+ }
+ return false
+ },
}
func TestParse(t *testing.T) {
@@ -105,17 +114,19 @@ func TestParse(t *testing.T) {
"Basic Markdown file",
"/a/b/c.md",
func(c *qt.C, p *Path) {
+ c.Assert(p.Ext(), qt.Equals, "md")
+ c.Assert(p.Type(), qt.Equals, TypeContentSingle)
c.Assert(p.IsContent(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsFalse)
c.Assert(p.Name(), qt.Equals, "c.md")
c.Assert(p.Base(), qt.Equals, "/a/b/c")
+ c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b/c")
c.Assert(p.Section(), qt.Equals, "a")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c")
c.Assert(p.Path(), qt.Equals, "/a/b/c.md")
c.Assert(p.Dir(), qt.Equals, "/a/b")
c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a/b")
- c.Assert(p.Ext(), qt.Equals, "md")
},
},
{
@@ -130,7 +141,7 @@ func TestParse(t *testing.T) {
// Reclassify it as a content resource.
ModifyPathBundleTypeResource(p)
- c.Assert(p.BundleType(), qt.Equals, PathTypeContentResource)
+ c.Assert(p.Type(), qt.Equals, TypeContentResource)
c.Assert(p.IsContent(), qt.IsTrue)
c.Assert(p.Name(), qt.Equals, "b.md")
c.Assert(p.Base(), qt.Equals, "/a/b.md")
@@ -160,15 +171,16 @@ func TestParse(t *testing.T) {
"/a/b.a.b.no.txt",
func(c *qt.C, p *Path) {
c.Assert(p.Name(), qt.Equals, "b.a.b.no.txt")
- c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b")
+ c.Assert(p.NameNoIdentifier(), qt.Equals, "b")
c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt")
- c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
- c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt")
- c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt")
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no", "b", "a", "b"})
+ c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"b", "a", "b"})
+ c.Assert(p.Base(), qt.Equals, "/a/b.txt")
+ c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.txt")
c.Assert(p.Path(), qt.Equals, "/a/b.a.b.no.txt")
- c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt")
+ c.Assert(p.PathNoLang(), qt.Equals, "/a/b.txt")
c.Assert(p.Ext(), qt.Equals, "txt")
- c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b")
+ c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b")
},
},
{
@@ -176,6 +188,7 @@ func TestParse(t *testing.T) {
"/_index.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/")
+ c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo")
c.Assert(p.Path(), qt.Equals, "/_index.md")
c.Assert(p.Container(), qt.Equals, "")
c.Assert(p.ContainerDir(), qt.Equals, "/")
@@ -186,13 +199,14 @@ func TestParse(t *testing.T) {
"/a/index.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a")
+ c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/a")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "a")
c.Assert(p.Container(), qt.Equals, "a")
c.Assert(p.Container(), qt.Equals, "a")
c.Assert(p.ContainerDir(), qt.Equals, "")
c.Assert(p.Dir(), qt.Equals, "/a")
c.Assert(p.Ext(), qt.Equals, "md")
- c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "index"})
c.Assert(p.IsBranchBundle(), qt.IsFalse)
c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsTrue)
@@ -209,11 +223,12 @@ func TestParse(t *testing.T) {
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
+ c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b")
c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a")
c.Assert(p.Dir(), qt.Equals, "/a/b")
c.Assert(p.Ext(), qt.Equals, "md")
- c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no", "index"})
c.Assert(p.IsBranchBundle(), qt.IsFalse)
c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsTrue)
@@ -235,7 +250,7 @@ func TestParse(t *testing.T) {
c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a")
c.Assert(p.Ext(), qt.Equals, "md")
- c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no", "_index"})
c.Assert(p.IsBranchBundle(), qt.IsTrue)
c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsFalse)
@@ -274,7 +289,7 @@ func TestParse(t *testing.T) {
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/index.txt")
c.Assert(p.Ext(), qt.Equals, "txt")
- c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no", "index"})
c.Assert(p.IsLeafBundle(), qt.IsFalse)
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b/index")
},
@@ -357,11 +372,140 @@ func TestParse(t *testing.T) {
}
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
+ if test.name != "Basic Markdown file" {
+ // return
+ }
test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
})
}
}
+func TestParseLayouts(t *testing.T) {
+ c := qt.New(t)
+
+ tests := []struct {
+ name string
+ path string
+ assert func(c *qt.C, p *Path)
+ }{
+ {
+ "Basic",
+ "/list.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/list.html")
+ c.Assert(p.OutputFormat(), qt.Equals, "html")
+ },
+ },
+ {
+ "Lang",
+ "/list.no.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "list"})
+ c.Assert(p.Base(), qt.Equals, "/list.html")
+ c.Assert(p.Lang(), qt.Equals, "no")
+ },
+ },
+ {
+ "Lang and output format",
+ "/list.no.amp.not.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "not", "amp", "no", "list"})
+ c.Assert(p.OutputFormat(), qt.Equals, "amp")
+ c.Assert(p.Ext(), qt.Equals, "html")
+ c.Assert(p.Lang(), qt.Equals, "no")
+ c.Assert(p.Base(), qt.Equals, "/list.html")
+ },
+ },
+ {
+ "Term",
+ "/term.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/term.html")
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "term"})
+ c.Assert(p.PathNoIdentifier(), qt.Equals, "/term")
+ c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/term")
+ c.Assert(p.Lang(), qt.Equals, "")
+ c.Assert(p.Kind(), qt.Equals, "term")
+ c.Assert(p.OutputFormat(), qt.Equals, "html")
+ },
+ },
+ {
+ "Sub dir",
+ "/pages/home.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "home"})
+ c.Assert(p.Lang(), qt.Equals, "")
+ c.Assert(p.Kind(), qt.Equals, "home")
+ c.Assert(p.OutputFormat(), qt.Equals, "html")
+ c.Assert(p.Dir(), qt.Equals, "/pages")
+ },
+ },
+ {
+ "Baseof",
+ "/pages/baseof.list.section.fr.amp.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "amp", "fr", "section", "list", "baseof"})
+ c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"list"})
+ c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
+ c.Assert(p.Lang(), qt.Equals, "fr")
+ c.Assert(p.OutputFormat(), qt.Equals, "amp")
+ c.Assert(p.Dir(), qt.Equals, "/pages")
+ c.Assert(p.NameNoIdentifier(), qt.Equals, "baseof")
+ c.Assert(p.Type(), qt.Equals, TypeBaseof)
+ c.Assert(p.IdentifierBase(), qt.Equals, "/pages/baseof.list.section.fr.amp.html")
+ },
+ },
+ {
+ "Markup",
+ "/_markup/render-link.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Type(), qt.Equals, TypeMarkup)
+ },
+ },
+ {
+ "Markup nested",
+ "/foo/_markup/render-link.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Type(), qt.Equals, TypeMarkup)
+ },
+ },
+ {
+ "Shortcode",
+ "/_shortcodes/myshortcode.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Type(), qt.Equals, TypeShortcode)
+ },
+ },
+ {
+ "Shortcode nested",
+ "/foo/_shortcodes/myshortcode.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Type(), qt.Equals, TypeShortcode)
+ },
+ },
+ {
+ "Shortcode nested sub",
+ "/foo/_shortcodes/foo/myshortcode.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Type(), qt.Equals, TypeShortcode)
+ },
+ },
+ {
+ "Partials",
+ "/_partials/foo.bar",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Type(), qt.Equals, TypePartial)
+ },
+ },
+ }
+
+ for _, test := range tests {
+ c.Run(test.name, func(c *qt.C) {
+ test.assert(c, testParser.Parse(files.ComponentFolderLayouts, test.path))
+ })
+ }
+}
+
func TestHasExt(t *testing.T) {
c := qt.New(t)
(DIR) diff --git a/common/paths/pathtype_string.go b/common/paths/pathtype_string.go
@@ -1,27 +0,0 @@
-// Code generated by "stringer -type=PathType"; DO NOT EDIT.
-
-package paths
-
-import "strconv"
-
-func _() {
- // An "invalid array index" compiler error signifies that the constant values have changed.
- // Re-run the stringer command to generate them again.
- var x [1]struct{}
- _ = x[PathTypeFile-0]
- _ = x[PathTypeContentResource-1]
- _ = x[PathTypeContentSingle-2]
- _ = x[PathTypeLeaf-3]
- _ = x[PathTypeBranch-4]
-}
-
-const _PathType_name = "PathTypeFilePathTypeContentResourcePathTypeContentSinglePathTypeLeafPathTypeBranch"
-
-var _PathType_index = [...]uint8{0, 12, 35, 56, 68, 82}
-
-func (i PathType) String() string {
- if i < 0 || i >= PathType(len(_PathType_index)-1) {
- return "PathType(" + strconv.FormatInt(int64(i), 10) + ")"
- }
- return _PathType_name[_PathType_index[i]:_PathType_index[i+1]]
-}
(DIR) diff --git a/common/paths/type_string.go b/common/paths/type_string.go
@@ -0,0 +1,32 @@
+// Code generated by "stringer -type Type"; DO NOT EDIT.
+
+package paths
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[TypeFile-0]
+ _ = x[TypeContentResource-1]
+ _ = x[TypeContentSingle-2]
+ _ = x[TypeLeaf-3]
+ _ = x[TypeBranch-4]
+ _ = x[TypeContentData-5]
+ _ = x[TypeMarkup-6]
+ _ = x[TypeShortcode-7]
+ _ = x[TypePartial-8]
+ _ = x[TypeBaseof-9]
+}
+
+const _Type_name = "TypeFileTypeContentResourceTypeContentSingleTypeLeafTypeBranchTypeContentDataTypeMarkupTypeShortcodeTypePartialTypeBaseof"
+
+var _Type_index = [...]uint8{0, 8, 27, 44, 52, 62, 77, 87, 100, 111, 121}
+
+func (i Type) String() string {
+ if i < 0 || i >= Type(len(_Type_index)-1) {
+ return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _Type_name[_Type_index[i]:_Type_index[i+1]]
+}
(DIR) diff --git a/common/types/types.go b/common/types/types.go
@@ -28,6 +28,16 @@ type RLocker interface {
RUnlock()
}
+type Locker interface {
+ Lock()
+ Unlock()
+}
+
+type RWLocker interface {
+ RLocker
+ Locker
+}
+
// KeyValue is a interface{} tuple.
type KeyValue struct {
Key any
(DIR) diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go
@@ -849,7 +849,24 @@ func (c *Configs) Init() error {
c.Languages = languages
c.LanguagesDefaultFirst = languagesDefaultFirst
- c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix}
+ c.ContentPathParser = &paths.PathParser{
+ LanguageIndex: languagesDefaultFirst.AsIndexSet(),
+ IsLangDisabled: c.Base.IsLangDisabled,
+ IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix,
+ IsOutputFormat: func(name, ext string) bool {
+ if name == "" {
+ return false
+ }
+
+ if of, ok := c.Base.OutputFormats.Config.GetByName(name); ok {
+ if ext != "" && !of.MediaType.HasSuffix(ext) {
+ return false
+ }
+ return true
+ }
+ return false
+ },
+ }
c.configLangs = make([]config.AllProvider, len(c.Languages))
for i, l := range c.LanguagesDefaultFirst {
(DIR) diff --git a/create/content.go b/create/content.go
@@ -291,7 +291,7 @@ func (b *contentBuilder) applyArcheType(contentFilename string, archetypeFi hugo
func (b *contentBuilder) mapArcheTypeDir() error {
var m archetypeMap
- seen := map[hstrings.Tuple]bool{}
+ seen := map[hstrings.Strings2]bool{}
walkFn := func(path string, fim hugofs.FileMetaInfo) error {
if fim.IsDir() {
@@ -301,7 +301,7 @@ func (b *contentBuilder) mapArcheTypeDir() error {
pi := fim.Meta().PathInfo
if pi.IsContent() {
- pathLang := hstrings.Tuple{First: pi.PathNoIdentifier(), Second: fim.Meta().Lang}
+ pathLang := hstrings.Strings2{pi.PathBeforeLangAndOutputFormatAndExt(), fim.Meta().Lang}
if seen[pathLang] {
// Duplicate content file, e.g. page.md and page.html.
// In the regular build, we will filter out the duplicates, but
(DIR) diff --git a/deps/deps.go b/deps/deps.go
@@ -29,6 +29,7 @@ import (
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/postpub"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/metrics"
"github.com/gohugoio/hugo/resources"
@@ -46,12 +47,6 @@ type Deps struct {
ExecHelper *hexec.Exec
- // The templates to use. This will usually implement the full tpl.TemplateManager.
- tmplHandlers *tpl.TemplateHandlers
-
- // The template funcs.
- TmplFuncMap map[string]any
-
// The file systems to use.
Fs *hugofs.Fs `json:"-"`
@@ -79,7 +74,8 @@ type Deps struct {
// The site building.
Site page.Site
- TemplateProvider ResourceProvider
+ TemplateStore *tplimpl.TemplateStore
+
// Used in tests
OverloadedTemplateFuncs map[string]any
@@ -102,6 +98,9 @@ type Deps struct {
// This is common/global for all sites.
BuildState *BuildState
+ // Misc counters.
+ Counters *Counters
+
// Holds RPC dispatchers for Katex etc.
// TODO(bep) rethink this re. a plugin setup, but this will have to do for now.
WasmDispatchers *warpc.Dispatchers
@@ -109,9 +108,6 @@ type Deps struct {
// The JS batcher client.
JSBatcherClient js.BatcherClient
- // The JS batcher client.
- // JSBatcherClient *esbuild.BatcherClient
-
isClosed bool
*globalErrHandler
@@ -130,8 +126,8 @@ func (d Deps) Clone(s page.Site, conf config.AllProvider) (*Deps, error) {
return &d, nil
}
-func (d *Deps) SetTempl(t *tpl.TemplateHandlers) {
- d.tmplHandlers = t
+func (d *Deps) GetTemplateStore() *tplimpl.TemplateStore {
+ return d.TemplateStore
}
func (d *Deps) Init() error {
@@ -153,10 +149,12 @@ func (d *Deps) Init() error {
logger: d.Log,
}
}
-
if d.BuildState == nil {
d.BuildState = &BuildState{}
}
+ if d.Counters == nil {
+ d.Counters = &Counters{}
+ }
if d.BuildState.DeferredExecutions == nil {
if d.BuildState.DeferredExecutionsGroupedByRenderingContext == nil {
d.BuildState.DeferredExecutionsGroupedByRenderingContext = make(map[tpl.RenderingContext]*DeferredExecutions)
@@ -263,22 +261,17 @@ func (d *Deps) Init() error {
return nil
}
+// TODO(bep) rework this to get it in line with how we manage templates.
func (d *Deps) Compile(prototype *Deps) error {
var err error
if prototype == nil {
- if err = d.TemplateProvider.NewResource(d); err != nil {
- return err
- }
+
if err = d.TranslationProvider.NewResource(d); err != nil {
return err
}
return nil
}
- if err = d.TemplateProvider.CloneResource(d, prototype); err != nil {
- return err
- }
-
if err = d.TranslationProvider.CloneResource(d, prototype); err != nil {
return err
}
@@ -378,14 +371,6 @@ type ResourceProvider interface {
CloneResource(dst, src *Deps) error
}
-func (d *Deps) Tmpl() tpl.TemplateHandler {
- return d.tmplHandlers.Tmpl
-}
-
-func (d *Deps) TextTmpl() tpl.TemplateParseFinder {
- return d.tmplHandlers.TxtTmpl
-}
-
func (d *Deps) Close() error {
if d.isClosed {
return nil
@@ -454,6 +439,12 @@ type BuildState struct {
DeferredExecutionsGroupedByRenderingContext map[tpl.RenderingContext]*DeferredExecutions
}
+// Misc counters.
+type Counters struct {
+ // Counter for the math.Counter function.
+ MathCounter atomic.Uint64
+}
+
type DeferredExecutions struct {
// A set of filenames in /public that
// contains a post-processing prefix.
(DIR) diff --git a/hugolib/alias.go b/hugolib/alias.go
@@ -29,16 +29,17 @@ import (
"github.com/gohugoio/hugo/publisher"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/tpl"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
)
type aliasHandler struct {
- t tpl.TemplateHandler
+ ts *tplimpl.TemplateStore
log loggers.Logger
allowRoot bool
}
-func newAliasHandler(t tpl.TemplateHandler, l loggers.Logger, allowRoot bool) aliasHandler {
- return aliasHandler{t, l, allowRoot}
+func newAliasHandler(ts *tplimpl.TemplateStore, l loggers.Logger, allowRoot bool) aliasHandler {
+ return aliasHandler{ts, l, allowRoot}
}
type aliasPage struct {
@@ -47,16 +48,24 @@ type aliasPage struct {
}
func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, error) {
- var templ tpl.Template
- var found bool
-
- templ, found = a.t.Lookup("alias.html")
- if !found {
- // TODO(bep) consolidate
- templ, found = a.t.Lookup("_internal/alias.html")
- if !found {
- return nil, errors.New("no alias template found")
- }
+ var templateDesc tplimpl.TemplateDescriptor
+ var base string = ""
+ if ps, ok := p.(*pageState); ok {
+ base, templateDesc = ps.getTemplateBasePathAndDescriptor()
+ }
+ templateDesc.Layout = ""
+ templateDesc.Kind = ""
+ templateDesc.OutputFormat = output.AliasHTMLFormat.Name
+
+ q := tplimpl.TemplateQuery{
+ Path: base,
+ Category: tplimpl.CategoryLayout,
+ Desc: templateDesc,
+ }
+
+ t := a.ts.LookupPagesLayout(q)
+ if t == nil {
+ return nil, errors.New("no alias template found")
}
data := aliasPage{
@@ -67,7 +76,7 @@ func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, err
ctx := tpl.Context.Page.Set(context.Background(), p)
buffer := new(bytes.Buffer)
- err := a.t.ExecuteWithContext(ctx, templ, buffer, data)
+ err := a.ts.ExecuteWithContext(ctx, t, buffer, data)
if err != nil {
return nil, err
}
@@ -79,7 +88,7 @@ func (s *Site) writeDestAlias(path, permalink string, outputFormat output.Format
}
func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) {
- handler := newAliasHandler(s.Tmpl(), s.Log, allowRoot)
+ handler := newAliasHandler(s.GetTemplateStore(), s.Log, allowRoot)
targetPath, err := handler.targetPathAlias(path)
if err != nil {
(DIR) diff --git a/hugolib/alias_test.go b/hugolib/alias_test.go
@@ -107,13 +107,26 @@ func TestAliasMultipleOutputFormats(t *testing.T) {
func TestAliasTemplate(t *testing.T) {
t.Parallel()
- b := newTestSitesBuilder(t)
- b.WithSimpleConfigFile().WithContent("page.md", pageWithAlias).WithTemplatesAdded("alias.html", aliasTemplate)
+ files := `
+-- hugo.toml --
+baseURL = "http://example.com"
+-- layouts/single.html --
+Single.
+-- layouts/home.html --
+Home.
+-- layouts/alias.html --
+ALIASTEMPLATE
+-- content/page.md --
+---
+title: "Page"
+aliases: ["/foo/bar/"]
+---
+`
- b.CreateSites().Build(BuildCfg{})
+ b := Test(t, files)
// the real page
- b.AssertFileContent("public/page/index.html", "For some moments the old man")
+ b.AssertFileContent("public/page/index.html", "Single.")
// the alias redirector
b.AssertFileContent("public/foo/bar/index.html", "ALIASTEMPLATE")
}
(DIR) diff --git a/hugolib/content_factory.go b/hugolib/content_factory.go
@@ -72,12 +72,12 @@ func (f ContentFactory) ApplyArchetypeTemplate(w io.Writer, p page.Page, archety
templateSource = f.shortcodeReplacerPre.Replace(templateSource)
- templ, err := ps.s.TextTmpl().Parse("archetype.md", string(templateSource))
+ templ, err := ps.s.TemplateStore.TextParse("archetype.md", templateSource)
if err != nil {
return fmt.Errorf("failed to parse archetype template: %s: %w", err, err)
}
- result, err := executeToString(context.Background(), ps.s.Tmpl(), templ, d)
+ result, err := executeToString(context.Background(), ps.s.GetTemplateStore(), templ, d)
if err != nil {
return fmt.Errorf("failed to execute archetype template: %s: %w", err, err)
}
(DIR) diff --git a/hugolib/content_map.go b/hugolib/content_map.go
@@ -264,8 +264,8 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCoun
meta := fi.Meta()
pi := meta.PathInfo
- switch pi.BundleType() {
- case paths.PathTypeFile, paths.PathTypeContentResource:
+ switch pi.Type() {
+ case paths.TypeFile, paths.TypeContentResource:
m.s.Log.Trace(logg.StringFunc(
func() string {
return fmt.Sprintf("insert resource: %q", fi.Meta().Filename)
@@ -275,7 +275,7 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCoun
addErr = err
return
}
- case paths.PathTypeContentData:
+ case paths.TypeContentData:
pc, rc, err := m.addPagesFromGoTmplFi(fi, buildConfig)
pageCount += pc
resourceCount += rc
@@ -349,8 +349,7 @@ func (m *pageMap) addPagesFromGoTmplFi(fi hugofs.FileMetaInfo, buildConfig *Buil
DepsFromSite: func(s page.Site) pagesfromdata.PagesFromTemplateDeps {
ss := s.(*Site)
return pagesfromdata.PagesFromTemplateDeps{
- TmplFinder: ss.TextTmpl(),
- TmplExec: ss.Tmpl(),
+ TemplateStore: ss.GetTemplateStore(),
}
},
DependencyManager: s.Conf.NewIdentityManager("pagesfromdata"),
(DIR) diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go
@@ -180,7 +180,7 @@ func (t *pageTrees) collectAndMarkStaleIdentities(p *paths.Path) []identity.Iden
if p.Component() == files.ComponentFolderContent {
// It may also be a bundled content resource.
- key := p.ForBundleType(paths.PathTypeContentResource).Base()
+ key := p.ForBundleType(paths.TypeContentResource).Base()
tree = t.treeResources
nCount = 0
tree.ForEeachInDimension(key, doctree.DimensionLanguage.Index(),
@@ -1304,14 +1304,14 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
checkedCounter atomic.Int64
)
- resetPo := func(po *pageOutput, r identity.FinderResult) {
- if po.pco != nil {
+ resetPo := func(po *pageOutput, rebuildContent bool, r identity.FinderResult) {
+ if rebuildContent && po.pco != nil {
po.pco.Reset() // Will invalidate content cache.
}
po.renderState = 0
po.p.resourcesPublishInit = &sync.Once{}
- if r == identity.FinderFoundOneOfMany || po.f.Name == output.HTTPStatusHTMLFormat.Name {
+ if r == identity.FinderFoundOneOfMany || po.f.Name == output.HTTPStatus404HTMLFormat.Name {
// Will force a re-render even in fast render mode.
po.renderOnce = false
}
@@ -1323,7 +1323,7 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
}
// This can be a relativeley expensive operations, so we do it in parallel.
- g := rungroup.Run[*pageState](ctx, rungroup.Config[*pageState]{
+ g := rungroup.Run(ctx, rungroup.Config[*pageState]{
NumWorkers: h.numWorkers,
Handle: func(ctx context.Context, p *pageState) error {
if !p.isRenderedAny() {
@@ -1335,7 +1335,8 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
checkedCounter.Add(1)
if r := depsFinder.Contains(id, p.dependencyManager, 2); r > identity.FinderFoundOneOfManyRepetition {
for _, po := range p.pageOutputs {
- resetPo(po, r)
+ // Note that p.dependencyManager is used when rendering content, so reset that.
+ resetPo(po, true, r)
}
// Done.
return nil
@@ -1351,7 +1352,8 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
for _, id := range changes {
checkedCounter.Add(1)
if r := depsFinder.Contains(id, po.dependencyManagerOutput, 50); r > identity.FinderFoundOneOfManyRepetition {
- resetPo(po, r)
+ // Note that dependencyManagerOutput is not used when rendering content, so don't reset that.
+ resetPo(po, false, r)
continue OUTPUTS
}
}
@@ -1954,7 +1956,7 @@ func (sa *sitePagesAssembler) addStandalonePages() error {
tree.InsertIntoValuesDimension(key, p)
}
- addStandalone("/404", kinds.KindStatus404, output.HTTPStatusHTMLFormat)
+ addStandalone("/404", kinds.KindStatus404, output.HTTPStatus404HTMLFormat)
if s.conf.EnableRobotsTXT {
if m.i == 0 || s.Conf.IsMultihost() {
(DIR) diff --git a/hugolib/content_map_test.go b/hugolib/content_map_test.go
@@ -242,8 +242,13 @@ Data en
}
func TestBundleMultipleContentPageWithSamePath(t *testing.T) {
+ t.Parallel()
+
files := `
-- hugo.toml --
+printPathWarnings = true
+-- layouts/all.html --
+All.
-- content/bundle/index.md --
---
title: "Bundle md"
@@ -273,14 +278,18 @@ Bundle: {{ $bundle.Title }}|{{ $bundle.Params.foo }}|{{ $bundle.File.Filename }}
P1: {{ $p1.Title }}|{{ $p1.Params.foo }}|{{ $p1.File.Filename }}|
`
- b := Test(t, files)
+ for range 3 {
+ b := Test(t, files, TestOptWarn())
- // There's multiple content files sharing the same logical path and language.
- // This is a little arbitrary, but we have to pick one and prefer the Markdown version.
- b.AssertFileContent("public/index.html",
- filepath.FromSlash("Bundle: Bundle md|md|/content/bundle/index.md|"),
- filepath.FromSlash("P1: P1 md|md|/content/p1.md|"),
- )
+ b.AssertLogContains("WARN Duplicate content path: \"/p1\"")
+
+ // There's multiple content files sharing the same logical path and language.
+ // This is a little arbitrary, but we have to pick one and prefer the Markdown version.
+ b.AssertFileContent("public/index.html",
+ filepath.FromSlash("Bundle: Bundle md|md|/content/bundle/index.md|"),
+ filepath.FromSlash("P1: P1 md|md|/content/p1.md|"),
+ )
+ }
}
// Issue #11944
(DIR) diff --git a/hugolib/content_render_hooks_test.go b/hugolib/content_render_hooks_test.go
@@ -17,6 +17,8 @@ import (
"fmt"
"strings"
"testing"
+
+ qt "github.com/frankban/quicktest"
)
func TestRenderHooksRSS(t *testing.T) {
@@ -129,6 +131,7 @@ P1: <p>P1. <a href="https://www.gohugo.io">I’m an inline-style link</a></p
<h1 id="heading-in-p1">Heading in p1</h1>
<h1 id="heading-in-p2">Heading in p2</h1>
`)
+
b.AssertFileContent("public/index.xml", `
P2: <p>P2. xml-link: https://www.bep.is|</p>
P3: <p>P3. xml-link: https://www.example.org|</p>
@@ -378,3 +381,93 @@ Content: {{ .Content}}|
"|Text: First line.\nSecond line.||\n",
)
}
+
+func TestContentOutputReuseRenderHooksAndShortcodesHTMLOnly(t *testing.T) {
+ files := `
+-- hugo.toml --
+-- layouts/index.html --
+HTML: {{ .Title }}|{{ .Content }}|
+-- layouts/index.xml --
+XML: {{ .Title }}|{{ .Content }}|
+-- layouts/_markup/render-heading.html --
+Render heading.
+-- layouts/shortcodes/myshortcode.html --
+My shortcode.
+-- content/_index.md --
+---
+title: "Home"
+---
+## Heading
+
+{{< myshortcode >}}
+`
+ b := Test(t, files)
+
+ s := b.H.Sites[0]
+ b.Assert(s.home.pageOutputTemplateVariationsState.Load(), qt.Equals, uint32(1))
+ b.AssertFileContent("public/index.html", "HTML: Home|Render heading.\nMy shortcode.\n|")
+ b.AssertFileContent("public/index.xml", "XML: Home|Render heading.\nMy shortcode.\n|")
+}
+
+func TestContentOutputNoReuseRenderHooksInBothHTMLAnXML(t *testing.T) {
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+-- layouts/index.html --
+HTML: {{ .Title }}|{{ .Content }}|
+-- layouts/index.xml --
+XML: {{ .Title }}|{{ .Content }}|
+-- layouts/_markup/render-heading.html --
+Render heading.
+-- layouts/_markup/render-heading.xml --
+Render heading XML.
+-- content/_index.md --
+---
+title: "Home"
+---
+## Heading
+
+
+`
+ b := Test(t, files)
+
+ s := b.H.Sites[0]
+ b.Assert(s.home.pageOutputTemplateVariationsState.Load() > 1, qt.IsTrue)
+ b.AssertFileContentExact("public/index.xml", "XML: Home|Render heading XML.|")
+ b.AssertFileContentExact("public/index.html", "HTML: Home|Render heading.|")
+}
+
+func TestContentOutputNoReuseShortcodesInBothHTMLAnXML(t *testing.T) {
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+-- layouts/index.html --
+HTML: {{ .Title }}|{{ .Content }}|
+-- layouts/index.xml --
+XML: {{ .Title }}|{{ .Content }}|
+-- layouts/_markup/render-heading.html --
+Render heading.
+
+-- layouts/shortcodes/myshortcode.html --
+My shortcode HTML.
+-- layouts/shortcodes/myshortcode.xml --
+My shortcode XML.
+-- content/_index.md --
+---
+title: "Home"
+---
+## Heading
+
+{{< myshortcode >}}
+
+
+`
+ b := Test(t, files)
+
+ // b.DebugPrint("", tplimpl.CategoryShortcode)
+
+ b.AssertFileContentExact("public/index.xml", "My shortcode XML.")
+ b.AssertFileContentExact("public/index.html", "My shortcode HTML.")
+ s := b.H.Sites[0]
+ b.Assert(s.home.pageOutputTemplateVariationsState.Load() > 1, qt.IsTrue)
+}
(DIR) diff --git a/hugolib/doctree/simpletree.go b/hugolib/doctree/simpletree.go
@@ -14,35 +14,46 @@
package doctree
import (
+ "iter"
"sync"
radix "github.com/armon/go-radix"
)
-// Tree is a radix tree that holds T.
+// Tree is a non thread safe radix tree that holds T.
type Tree[T any] interface {
+ TreeCommon[T]
+ WalkPrefix(s string, f func(s string, v T) (bool, error)) error
+ WalkPath(s string, f func(s string, v T) (bool, error)) error
+ All() iter.Seq2[string, T]
+}
+
+// TreeThreadSafe is a thread safe radix tree that holds T.
+type TreeThreadSafe[T any] interface {
+ TreeCommon[T]
+ WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error
+ WalkPath(lockType LockType, s string, f func(s string, v T) (bool, error)) error
+ All(lockType LockType) iter.Seq2[string, T]
+}
+
+type TreeCommon[T any] interface {
Get(s string) T
LongestPrefix(s string) (string, T)
Insert(s string, v T) T
- WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error
}
-// NewSimpleTree creates a new SimpleTree.
-func NewSimpleTree[T comparable]() *SimpleTree[T] {
+func NewSimpleTree[T any]() *SimpleTree[T] {
return &SimpleTree[T]{tree: radix.New()}
}
-// SimpleTree is a thread safe radix tree that holds T.
-type SimpleTree[T comparable] struct {
- mu sync.RWMutex
+// SimpleTree is a radix tree that holds T.
+// This tree is not thread safe.
+type SimpleTree[T any] struct {
tree *radix.Tree
zero T
}
func (tree *SimpleTree[T]) Get(s string) T {
- tree.mu.RLock()
- defer tree.mu.RUnlock()
-
if v, ok := tree.tree.Get(s); ok {
return v.(T)
}
@@ -50,9 +61,6 @@ func (tree *SimpleTree[T]) Get(s string) T {
}
func (tree *SimpleTree[T]) LongestPrefix(s string) (string, T) {
- tree.mu.RLock()
- defer tree.mu.RUnlock()
-
if s, v, ok := tree.tree.LongestPrefix(s); ok {
return s, v.(T)
}
@@ -60,17 +68,121 @@ func (tree *SimpleTree[T]) LongestPrefix(s string) (string, T) {
}
func (tree *SimpleTree[T]) Insert(s string, v T) T {
+ tree.tree.Insert(s, v)
+ return v
+}
+
+func (tree *SimpleTree[T]) Walk(f func(s string, v T) (bool, error)) error {
+ var err error
+ tree.tree.Walk(func(s string, v any) bool {
+ var b bool
+ b, err = f(s, v.(T))
+ if err != nil {
+ return true
+ }
+ return b
+ })
+ return err
+}
+
+func (tree *SimpleTree[T]) WalkPrefix(s string, f func(s string, v T) (bool, error)) error {
+ var err error
+ tree.tree.WalkPrefix(s, func(s string, v any) bool {
+ var b bool
+ b, err = f(s, v.(T))
+ if err != nil {
+ return true
+ }
+ return b
+ })
+
+ return err
+}
+
+func (tree *SimpleTree[T]) WalkPath(s string, f func(s string, v T) (bool, error)) error {
+ var err error
+ tree.tree.WalkPath(s, func(s string, v any) bool {
+ var b bool
+ b, err = f(s, v.(T))
+ if err != nil {
+ return true
+ }
+ return b
+ })
+ return err
+}
+
+func (tree *SimpleTree[T]) All() iter.Seq2[string, T] {
+ return func(yield func(s string, v T) bool) {
+ tree.tree.Walk(func(s string, v any) bool {
+ return !yield(s, v.(T))
+ })
+ }
+}
+
+// NewSimpleThreadSafeTree creates a new SimpleTree.
+func NewSimpleThreadSafeTree[T any]() *SimpleThreadSafeTree[T] {
+ return &SimpleThreadSafeTree[T]{tree: radix.New(), mu: new(sync.RWMutex)}
+}
+
+// SimpleThreadSafeTree is a thread safe radix tree that holds T.
+type SimpleThreadSafeTree[T any] struct {
+ mu *sync.RWMutex
+ noLock bool
+ tree *radix.Tree
+ zero T
+}
+
+var noopFunc = func() {}
+
+func (tree *SimpleThreadSafeTree[T]) readLock() func() {
+ if tree.noLock {
+ return noopFunc
+ }
+ tree.mu.RLock()
+ return tree.mu.RUnlock
+}
+
+func (tree *SimpleThreadSafeTree[T]) writeLock() func() {
+ if tree.noLock {
+ return noopFunc
+ }
tree.mu.Lock()
- defer tree.mu.Unlock()
+ return tree.mu.Unlock
+}
+
+func (tree *SimpleThreadSafeTree[T]) Get(s string) T {
+ unlock := tree.readLock()
+ defer unlock()
+
+ if v, ok := tree.tree.Get(s); ok {
+ return v.(T)
+ }
+ return tree.zero
+}
+
+func (tree *SimpleThreadSafeTree[T]) LongestPrefix(s string) (string, T) {
+ unlock := tree.readLock()
+ defer unlock()
+
+ if s, v, ok := tree.tree.LongestPrefix(s); ok {
+ return s, v.(T)
+ }
+ return "", tree.zero
+}
+
+func (tree *SimpleThreadSafeTree[T]) Insert(s string, v T) T {
+ unlock := tree.writeLock()
+ defer unlock()
tree.tree.Insert(s, v)
return v
}
-func (tree *SimpleTree[T]) Lock(lockType LockType) func() {
+func (tree *SimpleThreadSafeTree[T]) Lock(lockType LockType) func() {
switch lockType {
case LockTypeNone:
- return func() {}
+ return noopFunc
case LockTypeRead:
tree.mu.RLock()
return tree.mu.RUnlock
@@ -78,10 +190,16 @@ func (tree *SimpleTree[T]) Lock(lockType LockType) func() {
tree.mu.Lock()
return tree.mu.Unlock
}
- return func() {}
+ return noopFunc
}
-func (tree *SimpleTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
+func (tree SimpleThreadSafeTree[T]) LockTree(lockType LockType) (TreeThreadSafe[T], func()) {
+ unlock := tree.Lock(lockType)
+ tree.noLock = true
+ return &tree, unlock // create a copy of tree with the noLock flag set to true.
+}
+
+func (tree *SimpleThreadSafeTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
commit := tree.Lock(lockType)
defer commit()
var err error
@@ -96,3 +214,31 @@ func (tree *SimpleTree[T]) WalkPrefix(lockType LockType, s string, f func(s stri
return err
}
+
+func (tree *SimpleThreadSafeTree[T]) WalkPath(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
+ commit := tree.Lock(lockType)
+ defer commit()
+ var err error
+ tree.tree.WalkPath(s, func(s string, v any) bool {
+ var b bool
+ b, err = f(s, v.(T))
+ if err != nil {
+ return true
+ }
+ return b
+ })
+
+ return err
+}
+
+func (tree *SimpleThreadSafeTree[T]) All(lockType LockType) iter.Seq2[string, T] {
+ commit := tree.Lock(lockType)
+ defer commit()
+ return func(yield func(s string, v T) bool) {
+ tree.tree.Walk(func(s string, v any) bool {
+ return !yield(s, v.(T))
+ })
+ }
+}
+
+// iter.Seq[*TemplWithBaseApplied]
(DIR) diff --git a/hugolib/doctree/support.go b/hugolib/doctree/support.go
@@ -17,8 +17,6 @@ import (
"fmt"
"strings"
"sync"
-
- radix "github.com/armon/go-radix"
)
var _ MutableTrees = MutableTrees{}
@@ -60,11 +58,9 @@ func (ctx *WalkContext[T]) AddPostHook(handler func() error) {
ctx.HooksPost = append(ctx.HooksPost, handler)
}
-func (ctx *WalkContext[T]) Data() *SimpleTree[any] {
+func (ctx *WalkContext[T]) Data() *SimpleThreadSafeTree[any] {
ctx.dataInit.Do(func() {
- ctx.data = &SimpleTree[any]{
- tree: radix.New(),
- }
+ ctx.data = NewSimpleThreadSafeTree[any]()
})
return ctx.data
}
@@ -191,7 +187,7 @@ func (t MutableTrees) CanLock() bool {
// WalkContext is passed to the Walk callback.
type WalkContext[T any] struct {
- data *SimpleTree[any]
+ data *SimpleThreadSafeTree[any]
dataInit sync.Once
eventHandlers eventHandlers[T]
events []*Event[T]
(DIR) diff --git a/hugolib/doctree/treeshifttree.go b/hugolib/doctree/treeshifttree.go
@@ -13,7 +13,9 @@
package doctree
-var _ Tree[string] = (*TreeShiftTree[string])(nil)
+import "iter"
+
+var _ TreeThreadSafe[string] = (*TreeShiftTree[string])(nil)
type TreeShiftTree[T comparable] struct {
// This tree is shiftable in one dimension.
@@ -26,16 +28,16 @@ type TreeShiftTree[T comparable] struct {
zero T
// Will be of length equal to the length of the dimension.
- trees []*SimpleTree[T]
+ trees []*SimpleThreadSafeTree[T]
}
func NewTreeShiftTree[T comparable](d, length int) *TreeShiftTree[T] {
if length <= 0 {
panic("length must be > 0")
}
- trees := make([]*SimpleTree[T], length)
+ trees := make([]*SimpleThreadSafeTree[T], length)
for i := range length {
- trees[i] = NewSimpleTree[T]()
+ trees[i] = NewSimpleThreadSafeTree[T]()
}
return &TreeShiftTree[T]{d: d, trees: trees}
}
@@ -91,6 +93,14 @@ func (t *TreeShiftTree[T]) WalkPrefixRaw(lockType LockType, s string, f func(s s
return nil
}
+func (t *TreeShiftTree[T]) WalkPath(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
+ return t.trees[t.v].WalkPath(lockType, s, f)
+}
+
+func (t *TreeShiftTree[T]) All(lockType LockType) iter.Seq2[string, T] {
+ return t.trees[t.v].All(lockType)
+}
+
func (t *TreeShiftTree[T]) LenRaw() int {
var count int
for _, tt := range t.trees {
(DIR) diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go
@@ -134,7 +134,6 @@ func (h *HugoSites) resolveSite(lang string) *Site {
return nil
}
-// Only used in tests.
type buildCounters struct {
contentRenderCounter atomic.Uint64
pageRenderCounter atomic.Uint64
@@ -557,7 +556,6 @@ func (h *HugoSites) handleDataFile(r *source.File) error {
higherPrecedentData := current[r.BaseFileName()]
switch data.(type) {
- case nil:
case map[string]any:
switch higherPrecedentData.(type) {
(DIR) diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go
@@ -494,17 +494,17 @@ func (s *Site) executeDeferredTemplates(de *deps.DeferredExecutions) error {
defer deferred.Mu.Unlock()
if !deferred.Executed {
- tmpl := s.Deps.Tmpl()
- templ, found := tmpl.Lookup(deferred.TemplateName)
- if !found {
- panic(fmt.Sprintf("template %q not found", deferred.TemplateName))
+ tmpl := s.Deps.GetTemplateStore()
+ ti := s.TemplateStore.LookupByPath(deferred.TemplatePath)
+ if ti == nil {
+ panic(fmt.Sprintf("template %q not found", deferred.TemplatePath))
}
if err := func() error {
buf := bufferpool.GetBuffer()
defer bufferpool.PutBuffer(buf)
- err = tmpl.ExecuteWithContext(deferred.Ctx, templ, buf, deferred.Data)
+ err = tmpl.ExecuteWithContext(deferred.Ctx, ti, buf, deferred.Data)
if err != nil {
return err
}
@@ -577,9 +577,13 @@ func (h *HugoSites) printUnusedTemplatesOnce() error {
h.printUnusedTemplatesInit.Do(func() {
conf := h.Configs.Base
if conf.PrintUnusedTemplates {
- unusedTemplates := h.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
+ unusedTemplates := h.GetTemplateStore().UnusedTemplates()
for _, unusedTemplate := range unusedTemplates {
- h.Log.Warnf("Template %s is unused, source file %s", unusedTemplate.Name(), unusedTemplate.Filename())
+ if unusedTemplate.Fi != nil {
+ h.Log.Warnf("Template %s is unused, source %q", unusedTemplate.PathInfo.Path(), unusedTemplate.Fi.Meta().Filename)
+ } else {
+ h.Log.Warnf("Template %s is unused", unusedTemplate.PathInfo.Path())
+ }
}
}
})
@@ -954,7 +958,7 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
case files.ComponentFolderLayouts:
tmplChanged = true
templatePath := pathInfo.Unnormalized().TrimLeadingSlash().PathNoLang()
- if !h.Tmpl().HasTemplate(templatePath) {
+ if !h.GetTemplateStore().HasTemplate(templatePath) {
tmplAdded = true
}
@@ -974,8 +978,9 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
}
} else {
logger.Println("Template changed", pathInfo.Path())
- if templ, found := h.Tmpl().GetIdentity(templatePath); found {
- changes = append(changes, templ)
+ id := h.GetTemplateStore().GetIdentity(pathInfo.Path())
+ if id != nil {
+ changes = append(changes, id)
} else {
changes = append(changes, pathInfo)
}
@@ -1084,7 +1089,6 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
changed := &WhatChanged{
needsPagesAssembly: needsPagesAssemble,
- identitySet: make(identity.Identities),
}
changed.Add(changes...)
@@ -1106,17 +1110,39 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
}
}
- h.Deps.OnChangeListeners.Notify(changed.Changes()...)
+ changes2 := changed.Changes()
+ h.Deps.OnChangeListeners.Notify(changes2...)
if err := h.resolveAndClearStateForIdentities(ctx, l, cacheBusterOr, changed.Drain()); err != nil {
return err
}
- if tmplChanged || i18nChanged {
+ if tmplChanged {
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
- // TODO(bep) this could probably be optimized to somehow
- // only load the changed templates and its dependencies, but that is non-trivial.
+ depsFinder := identity.NewFinder(identity.FinderConfig{})
ll := l.WithField("substep", "rebuild templates")
+ s := h.Sites[0]
+ if err := s.Deps.TemplateStore.RefreshFiles(func(fi hugofs.FileMetaInfo) bool {
+ pi := fi.Meta().PathInfo
+ for _, id := range changes2 {
+ if depsFinder.Contains(pi, id, -1) > 0 {
+ return true
+ }
+ }
+ return false
+ }); err != nil {
+ return ll, err
+ }
+
+ return ll, nil
+ }); err != nil {
+ return err
+ }
+ }
+
+ if i18nChanged {
+ if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
+ ll := l.WithField("substep", "rebuild i18n")
var prototype *deps.Deps
for i, s := range h.Sites {
if err := s.Deps.Compile(prototype); err != nil {
(DIR) diff --git a/hugolib/hugo_smoke_test.go b/hugolib/hugo_smoke_test.go
@@ -76,12 +76,13 @@ Single: {{ .Title }}|{{ .RelPermalink}}|{{ range .OutputFormats }}{{ .Name }}: {
`
- b := Test(t, files)
-
- b.AssertFileContent("public/index.html", `List: |/|html: /|rss: /index.xml|$`)
- b.AssertFileContent("public/index.xml", `List xml: |/|html: /|rss: /index.xml|$`)
- b.AssertFileContent("public/p1/index.html", `Single: Page|/p1/|html: /p1/|$`)
- b.AssertFileExists("public/p1/index.xml", false)
+ for i := 0; i < 2; i++ {
+ b := Test(t, files)
+ b.AssertFileContent("public/index.html", `List: |/|html: /|rss: /index.xml|$`)
+ b.AssertFileContent("public/index.xml", `List xml: |/|html: /|rss: /index.xml|$`)
+ b.AssertFileContent("public/p1/index.html", `Single: Page|/p1/|html: /p1/|$`)
+ b.AssertFileExists("public/p1/index.xml", false)
+ }
}
func TestSmoke(t *testing.T) {
(DIR) diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go
@@ -219,19 +219,31 @@ type IntegrationTestBuilder struct {
type lockingBuffer struct {
sync.Mutex
- bytes.Buffer
+ buf bytes.Buffer
+}
+
+func (b *lockingBuffer) String() string {
+ b.Lock()
+ defer b.Unlock()
+ return b.buf.String()
+}
+
+func (b *lockingBuffer) Reset() {
+ b.Lock()
+ defer b.Unlock()
+ b.buf.Reset()
}
func (b *lockingBuffer) ReadFrom(r io.Reader) (n int64, err error) {
b.Lock()
- n, err = b.Buffer.ReadFrom(r)
+ n, err = b.buf.ReadFrom(r)
b.Unlock()
return
}
func (b *lockingBuffer) Write(p []byte) (n int, err error) {
b.Lock()
- n, err = b.Buffer.Write(p)
+ n, err = b.buf.Write(p)
b.Unlock()
return
}
(DIR) diff --git a/hugolib/page.go b/hugolib/page.go
@@ -28,15 +28,13 @@ import (
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/output"
- "github.com/gohugoio/hugo/output/layouts"
"github.com/gohugoio/hugo/related"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/tableofcontents"
- "github.com/gohugoio/hugo/tpl"
-
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/types"
@@ -116,6 +114,14 @@ type pageState struct {
resourcesPublishInit *sync.Once
}
+func (p *pageState) incrPageOutputTemplateVariation() {
+ p.pageOutputTemplateVariationsState.Add(1)
+}
+
+func (p *pageState) canReusePageOutputContent() bool {
+ return p.pageOutputTemplateVariationsState.Load() == 1
+}
+
func (p *pageState) IdentifierBase() string {
return p.Path()
}
@@ -169,10 +175,6 @@ func (p *pageState) resetBuildState() {
// Nothing to do for now.
}
-func (p *pageState) reusePageOutputContent() bool {
- return p.pageOutputTemplateVariationsState.Load() == 1
-}
-
func (p *pageState) skipRender() bool {
b := p.s.conf.C.SegmentFilter.ShouldExcludeFine(
segments.SegmentMatcherFields{
@@ -474,49 +476,40 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
return nil
}
-func (p *pageState) getLayoutDescriptor() layouts.LayoutDescriptor {
- p.layoutDescriptorInit.Do(func() {
- var section string
- sections := p.SectionsEntries()
-
- switch p.Kind() {
- case kinds.KindSection:
- if len(sections) > 0 {
- section = sections[0]
- }
- case kinds.KindTaxonomy, kinds.KindTerm:
-
- if p.m.singular != "" {
- section = p.m.singular
- } else if len(sections) > 0 {
- section = sections[0]
- }
- default:
- }
-
- p.layoutDescriptor = layouts.LayoutDescriptor{
- Kind: p.Kind(),
- Type: p.Type(),
- Lang: p.Language().Lang,
- Layout: p.Layout(),
- Section: section,
- }
- })
-
- return p.layoutDescriptor
+func (po *pageOutput) getTemplateBasePathAndDescriptor() (string, tplimpl.TemplateDescriptor) {
+ p := po.p
+ f := po.f
+ base := p.PathInfo().BaseReTyped(p.m.pageConfig.Type)
+ return base, tplimpl.TemplateDescriptor{
+ Kind: p.Kind(),
+ Lang: p.Language().Lang,
+ Layout: p.Layout(),
+ OutputFormat: f.Name,
+ MediaType: f.MediaType.Type,
+ IsPlainText: f.IsPlainText,
+ }
}
-func (p *pageState) resolveTemplate(layouts ...string) (tpl.Template, bool, error) {
- f := p.outputFormat()
-
- d := p.getLayoutDescriptor()
+func (p *pageState) resolveTemplate(layouts ...string) (*tplimpl.TemplInfo, bool, error) {
+ dir, d := p.getTemplateBasePathAndDescriptor()
if len(layouts) > 0 {
d.Layout = layouts[0]
- d.LayoutOverride = true
+ d.LayoutMustMatch = true
+ }
+
+ q := tplimpl.TemplateQuery{
+ Path: dir,
+ Category: tplimpl.CategoryLayout,
+ Desc: d,
+ }
+
+ tinfo := p.s.TemplateStore.LookupPagesLayout(q)
+ if tinfo == nil {
+ return nil, false, nil
}
- return p.s.Tmpl().LookupLayout(d, f)
+ return tinfo, true, nil
}
// Must be run after the site section tree etc. is built and ready.
@@ -705,7 +698,7 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
if isRenderingSite {
cp := p.pageOutput.pco
- if cp == nil && p.reusePageOutputContent() {
+ if cp == nil && p.canReusePageOutputContent() {
// Look for content to reuse.
for i := range p.pageOutputs {
if i == idx {
(DIR) diff --git a/hugolib/page__common.go b/hugolib/page__common.go
@@ -21,7 +21,6 @@ import (
"github.com/gohugoio/hugo/lazy"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/navigation"
- "github.com/gohugoio/hugo/output/layouts"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/source"
@@ -86,9 +85,6 @@ type pageCommon struct {
// should look like.
targetPathDescriptor page.TargetPathDescriptor
- layoutDescriptor layouts.LayoutDescriptor
- layoutDescriptorInit sync.Once
-
// Set if feature enabled and this is in a Git repo.
gitInfo source.GitInfo
codeowners []string
(DIR) diff --git a/hugolib/page__content.go b/hugolib/page__content.go
@@ -24,6 +24,8 @@ import (
"strings"
"unicode/utf8"
+ maps0 "maps"
+
"github.com/bep/logg"
"github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/common/herrors"
@@ -32,7 +34,6 @@ import (
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types/hstring"
"github.com/gohugoio/hugo/helpers"
- "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
@@ -45,7 +46,6 @@ import (
"github.com/gohugoio/hugo/tpl"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
- maps0 "maps"
)
const (
@@ -600,7 +600,7 @@ func (c *cachedContentScope) contentRendered(ctx context.Context) (contentSummar
return nil, err
}
if hasShortcodeVariants {
- cp.po.p.pageOutputTemplateVariationsState.Add(1)
+ cp.po.p.incrPageOutputTemplateVariation()
}
var result contentSummary
@@ -684,10 +684,9 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
if err := cp.initRenderHooks(); err != nil {
return nil, err
}
- f := cp.po.f
po := cp.po
p := po.p
- ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, p, f, false)
+ ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, po, false)
if err != nil {
return nil, err
}
@@ -701,16 +700,14 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
if p.s.conf.Internal.Watch {
for _, s := range cp2.po.p.m.content.shortcodeState.shortcodes {
- for _, templ := range s.templs {
- cp.trackDependency(templ.(identity.IdentityProvider))
- }
+ cp.trackDependency(s.templ)
}
}
// Transfer shortcode names so HasShortcode works for shortcodes from included pages.
cp.po.p.m.content.shortcodeState.transferNames(cp2.po.p.m.content.shortcodeState)
if cp2.po.p.pageOutputTemplateVariationsState.Load() > 0 {
- cp.po.p.pageOutputTemplateVariationsState.Add(1)
+ cp.po.p.incrPageOutputTemplateVariation()
}
}
@@ -723,7 +720,7 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
}
if hasVariants {
- p.pageOutputTemplateVariationsState.Add(1)
+ p.incrPageOutputTemplateVariation()
}
isHTML := cp.po.p.m.pageConfig.ContentMediaType.IsHTML()
@@ -980,7 +977,7 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
return "", err
}
- placeholders, err := s.prepareShortcodesForPage(ctx, pco.po.p, pco.po.f, true)
+ placeholders, err := s.prepareShortcodesForPage(ctx, pco.po, true)
if err != nil {
return "", err
}
@@ -990,7 +987,7 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
return "", err
}
if hasVariants {
- pco.po.p.pageOutputTemplateVariationsState.Add(1)
+ pco.po.p.incrPageOutputTemplateVariation()
}
b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false)
if err != nil {
@@ -1028,7 +1025,7 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
return "", err
}
if hasShortcodeVariants {
- pco.po.p.pageOutputTemplateVariationsState.Add(1)
+ pco.po.p.incrPageOutputTemplateVariation()
}
}
@@ -1110,7 +1107,7 @@ func (c *cachedContentScope) RenderShortcodes(ctx context.Context) (template.HTM
}
if hasVariants {
- pco.po.p.pageOutputTemplateVariationsState.Add(1)
+ pco.po.p.incrPageOutputTemplateVariation()
}
if cb != nil {
(DIR) diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go
@@ -72,8 +72,11 @@ type pageMeta struct {
// Prepare for a rebuild of the data passed in from front matter.
func (m *pageMeta) setMetaPostPrepareRebuild() {
- params := xmaps.Clone[map[string]any](m.paramsOriginal)
+ params := xmaps.Clone(m.paramsOriginal)
m.pageMetaParams.pageConfig = &pagemeta.PageConfig{
+ Kind: m.pageConfig.Kind,
+ Lang: m.pageConfig.Lang,
+ Path: m.pageConfig.Path,
Params: params,
}
m.pageMetaFrontMatter = pageMetaFrontMatter{}
@@ -108,10 +111,10 @@ func (p *pageMeta) Aliases() []string {
}
func (p *pageMeta) BundleType() string {
- switch p.pathInfo.BundleType() {
- case paths.PathTypeLeaf:
+ switch p.pathInfo.Type() {
+ case paths.TypeLeaf:
return "leaf"
- case paths.PathTypeBranch:
+ case paths.TypeBranch:
return "branch"
default:
return ""
(DIR) diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
@@ -19,23 +19,21 @@ import (
"errors"
"fmt"
"html/template"
- "strings"
"sync"
"sync/atomic"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/markup/converter/hooks"
- "github.com/gohugoio/hugo/markup/highlight/chromalexers"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/gohugoio/hugo/markup/converter"
bp "github.com/gohugoio/hugo/bufferpool"
- "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources/page"
@@ -120,9 +118,9 @@ func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (tem
}
// Make sure to send the *pageState and not the *pageContentOutput to the template.
- res, err := executeToString(ctx, pco.po.p.s.Tmpl(), templ, pco.po.p)
+ res, err := executeToString(ctx, pco.po.p.s.GetTemplateStore(), templ, pco.po.p)
if err != nil {
- return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err))
+ return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Template.Name(), err))
}
return template.HTML(res), nil
}
@@ -274,103 +272,100 @@ func (pco *pageContentOutput) initRenderHooks() error {
return r
}
- layoutDescriptor := pco.po.p.getLayoutDescriptor()
- layoutDescriptor.RenderingHook = true
- layoutDescriptor.LayoutOverride = false
- layoutDescriptor.Layout = ""
+ // Inherit the descriptor from the page/current output format.
+ // This allows for fine-grained control of the template used for
+ // rendering of e.g. links.
+ base, layoutDescriptor := pco.po.p.getTemplateBasePathAndDescriptor()
switch tp {
case hooks.LinkRendererType:
- layoutDescriptor.Kind = "render-link"
+ layoutDescriptor.Variant1 = "link"
case hooks.ImageRendererType:
- layoutDescriptor.Kind = "render-image"
+ layoutDescriptor.Variant1 = "image"
case hooks.HeadingRendererType:
- layoutDescriptor.Kind = "render-heading"
+ layoutDescriptor.Variant1 = "heading"
case hooks.PassthroughRendererType:
- layoutDescriptor.Kind = "render-passthrough"
+ layoutDescriptor.Variant1 = "passthrough"
if id != nil {
- layoutDescriptor.KindVariants = id.(string)
+ layoutDescriptor.Variant2 = id.(string)
}
case hooks.BlockquoteRendererType:
- layoutDescriptor.Kind = "render-blockquote"
+ layoutDescriptor.Variant1 = "blockquote"
if id != nil {
- layoutDescriptor.KindVariants = id.(string)
+ layoutDescriptor.Variant2 = id.(string)
}
case hooks.TableRendererType:
- layoutDescriptor.Kind = "render-table"
+ layoutDescriptor.Variant1 = "table"
case hooks.CodeBlockRendererType:
- layoutDescriptor.Kind = "render-codeblock"
+ layoutDescriptor.Variant1 = "codeblock"
if id != nil {
- lang := id.(string)
- lexer := chromalexers.Get(lang)
- if lexer != nil {
- layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",")
- } else {
- layoutDescriptor.KindVariants = lang
- }
+ layoutDescriptor.Variant2 = id.(string)
}
}
- getHookTemplate := func(f output.Format) (tpl.Template, bool) {
- templ, found, err := pco.po.p.s.Tmpl().LookupLayout(layoutDescriptor, f)
- if err != nil {
- panic(err)
+ renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
+ var ignoreInternal bool
+ switch layoutDescriptor.Variant1 {
+ case "link":
+ ignoreInternal = !renderHookConfig.Link.IsEnableDefault()
+ case "image":
+ ignoreInternal = !renderHookConfig.Image.IsEnableDefault()
+ }
+
+ candidates := pco.po.p.s.renderFormats
+ var numCandidatesFound int
+ consider := func(candidate *tplimpl.TemplInfo) bool {
+ if layoutDescriptor.Variant1 != candidate.D.Variant1 {
+ return false
}
- if found {
- if isitp, ok := templ.(tpl.IsInternalTemplateProvider); ok && isitp.IsInternalTemplate() {
-
- renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
-
- switch templ.Name() {
- case "_default/_markup/render-link.html":
- if !renderHookConfig.Link.IsEnableDefault() {
- return nil, false
- }
- case "_default/_markup/render-image.html":
- if !renderHookConfig.Image.IsEnableDefault() {
- return nil, false
- }
- }
- }
+
+ if layoutDescriptor.Variant2 != "" && candidate.D.Variant2 != "" && layoutDescriptor.Variant2 != candidate.D.Variant2 {
+ return false
}
- return templ, found
- }
- templ, found1 := getHookTemplate(pco.po.f)
- if !found1 || pco.po.p.reusePageOutputContent() {
- defaultOutputFormat := pco.po.p.s.conf.C.DefaultOutputFormat
+ if ignoreInternal && candidate.SubCategory == tplimpl.SubCategoryEmbedded {
+ // Don't consider the internal hook templates.
+ return false
+ }
- candidates := pco.po.p.s.renderFormats
+ if pco.po.p.pageOutputTemplateVariationsState.Load() > 1 {
+ return true
+ }
- // Some hooks may only be available in HTML, and if
- // this site is configured to not have HTML output, we need to
- // make sure we have a fallback. This should be very rare.
- if pco.po.f.MediaType.FirstSuffix.Suffix != "html" {
- if _, found := candidates.GetBySuffix("html"); !found {
- candidates = append(candidates, output.HTMLFormat)
- }
+ if candidate.D.OutputFormat == "" {
+ numCandidatesFound++
+ } else if _, found := candidates.GetByName(candidate.D.OutputFormat); found {
+ numCandidatesFound++
}
- // Check if some of the other output formats would give a different template.
- for _, f := range candidates {
- if f.Name == pco.po.f.Name {
- continue
- }
- templ2, found2 := getHookTemplate(f)
-
- if found2 {
- if !found1 && f.Name == defaultOutputFormat.Name {
- templ = templ2
- found1 = true
- break
- }
-
- if templ != templ2 {
- pco.po.p.pageOutputTemplateVariationsState.Add(1)
- break
- }
- }
+ return true
+ }
+
+ getHookTemplate := func() (*tplimpl.TemplInfo, bool) {
+ q := tplimpl.TemplateQuery{
+ Path: base,
+ Category: tplimpl.CategoryMarkup,
+ Desc: layoutDescriptor,
+ Consider: consider,
}
+
+ v := pco.po.p.s.TemplateStore.LookupPagesLayout(q)
+ return v, v != nil
+ }
+
+ templ, found1 := getHookTemplate()
+ if found1 && templ == nil {
+ panic("found1 is true, but templ is nil")
+ }
+
+ if !found1 && layoutDescriptor.OutputFormat == pco.po.p.s.conf.DefaultOutputFormat {
+ numCandidatesFound++
+ }
+
+ if numCandidatesFound > 1 {
+ // More than one output format candidate found for this hook temoplate,
+ // so we cannot reuse the same rendered content.
+ pco.po.p.incrPageOutputTemplateVariation()
}
if !found1 {
@@ -384,7 +379,7 @@ func (pco *pageContentOutput) initRenderHooks() error {
}
r := hookRendererTemplate{
- templateHandler: pco.po.p.s.Tmpl(),
+ templateHandler: pco.po.p.s.GetTemplateStore(),
templ: templ,
resolvePosition: resolvePosition,
}
@@ -488,7 +483,7 @@ func (t targetPathsHolder) targetPaths() page.TargetPaths {
return t.paths
}
-func executeToString(ctx context.Context, h tpl.TemplateHandler, templ tpl.Template, data any) (string, error) {
+func executeToString(ctx context.Context, h *tplimpl.TemplateStore, templ *tplimpl.TemplInfo, data any) (string, error) {
b := bp.GetBuffer()
defer bp.PutBuffer(b)
if err := h.ExecuteWithContext(ctx, templ, b, data); err != nil {
(DIR) diff --git a/hugolib/pages_capture.go b/hugolib/pages_capture.go
@@ -195,7 +195,7 @@ func (c *pagesCollector) Collect() (collectErr error) {
return id.p.Dir() == fim.Meta().PathInfo.Dir()
}
- if fim.Meta().PathInfo.IsLeafBundle() && id.p.BundleType() == paths.PathTypeContentSingle {
+ if fim.Meta().PathInfo.IsLeafBundle() && id.p.Type() == paths.TypeContentSingle {
return id.p.Dir() == fim.Meta().PathInfo.Dir()
}
@@ -314,7 +314,7 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
return nil, filepath.SkipDir
}
- seen := map[hstrings.Tuple]bool{}
+ seen := map[hstrings.Strings2]hugofs.FileMetaInfo{}
for _, fi := range readdir {
if fi.IsDir() {
continue
@@ -327,11 +327,14 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
// These would eventually have been filtered out as duplicates when
// inserting them into the document store,
// but doing it here will preserve a consistent ordering.
- baseLang := hstrings.Tuple{First: pi.Base(), Second: meta.Lang}
- if seen[baseLang] {
+ baseLang := hstrings.Strings2{pi.Base(), meta.Lang}
+ if fi2, ok := seen[baseLang]; ok {
+ if c.h.Configs.Base.PrintPathWarnings && !c.h.isRebuild() {
+ c.logger.Warnf("Duplicate content path: %q file: %q file: %q", pi.Base(), fi2.Meta().Filename, meta.Filename)
+ }
continue
}
- seen[baseLang] = true
+ seen[baseLang] = fi
if pi == nil {
panic(fmt.Sprintf("no path info for %q", meta.Filename))
@@ -374,7 +377,7 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
func (c *pagesCollector) handleBundleLeaf(dir, bundle hugofs.FileMetaInfo, inPath string, readdir []hugofs.FileMetaInfo) error {
bundlePi := bundle.Meta().PathInfo
- seen := map[hstrings.Tuple]bool{}
+ seen := map[hstrings.Strings2]bool{}
walk := func(path string, info hugofs.FileMetaInfo) error {
if info.IsDir() {
@@ -396,7 +399,7 @@ func (c *pagesCollector) handleBundleLeaf(dir, bundle hugofs.FileMetaInfo, inPat
// These would eventually have been filtered out as duplicates when
// inserting them into the document store,
// but doing it here will preserve a consistent ordering.
- baseLang := hstrings.Tuple{First: pi.Base(), Second: info.Meta().Lang}
+ baseLang := hstrings.Strings2{pi.Base(), info.Meta().Lang}
if seen[baseLang] {
return nil
}
(DIR) diff --git a/hugolib/pagesfromdata/pagesfromgotmpl.go b/hugolib/pagesfromdata/pagesfromgotmpl.go
@@ -29,6 +29,7 @@ import (
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/tpl"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
)
@@ -167,8 +168,7 @@ type PagesFromTemplateOptions struct {
}
type PagesFromTemplateDeps struct {
- TmplFinder tpl.TemplateParseFinder
- TmplExec tpl.TemplateExecutor
+ TemplateStore *tplimpl.TemplateStore
}
var _ resource.Staler = (*PagesFromTemplate)(nil)
@@ -303,7 +303,7 @@ func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
}
defer f.Close()
- tmpl, err := p.TmplFinder.Parse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
+ tmpl, err := p.TemplateStore.TextParse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
if err != nil {
return BuildInfo{}, err
}
@@ -314,7 +314,7 @@ func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p)
- if err := p.TmplExec.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil {
+ if err := p.TemplateStore.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil {
return BuildInfo{}, err
}
(DIR) diff --git a/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go b/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go
@@ -98,7 +98,8 @@ ADD_MORE_PLACEHOLDER
func TestPagesFromGoTmplMisc(t *testing.T) {
t.Parallel()
- b := hugolib.Test(t, filesPagesFromDataTempleBasic)
+ b := hugolib.Test(t, filesPagesFromDataTempleBasic, hugolib.TestOptWarn())
+ b.AssertLogContains("! WARN")
b.AssertPublishDir(`
docs/p1/mytext.txt
docs/p1/sub/mytex2.tx
(DIR) diff --git a/hugolib/paginator_test.go b/hugolib/paginator_test.go
@@ -15,7 +15,6 @@ package hugolib
import (
"fmt"
- "path/filepath"
"testing"
qt "github.com/frankban/quicktest"
@@ -102,10 +101,18 @@ URL: {{ $pag.URL }}
// Issue 6023
func TestPaginateWithSort(t *testing.T) {
- b := newTestSitesBuilder(t).WithSimpleConfigFile()
- b.WithTemplatesAdded("index.html", `{{ range (.Paginate (sort .Site.RegularPages ".File.Filename" "desc")).Pages }}|{{ .File.Filename }}{{ end }}`)
- b.Build(BuildCfg{}).AssertFileContent("public/index.html",
- filepath.FromSlash("|content/sect/doc1.nn.md|content/sect/doc1.nb.md|content/sect/doc1.fr.md|content/sect/doc1.en.md"))
+ files := `
+-- hugo.toml --
+-- content/a/a.md --
+-- content/z/b.md --
+-- content/x/b.md --
+-- content/x/a.md --
+-- layouts/home.html --
+Paginate: {{ range (.Paginate (sort .Site.RegularPages ".File.Filename" "desc")).Pages }}|{{ .Path }}{{ end }}
+`
+ b := Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Paginate: |/z/b|/x/b|/x/a|/a/a")
}
// https://github.com/gohugoio/hugo/issues/6797
@@ -176,12 +183,12 @@ Paginator: {{ .Paginator }}
func TestNilPointerErrorMessage(t *testing.T) {
files := `
--- hugo.toml --
+-- hugo.toml --
-- content/p1.md --
-- layouts/_default/single.html --
Home Filename: {{ site.Home.File.Filename }}
`
b, err := TestE(t, files)
b.Assert(err, qt.IsNotNil)
- b.Assert(err.Error(), qt.Contains, `_default/single.html:1:22: executing "_default/single.html" – File is nil; wrap it in if or with: {{ with site.Home.File }}{{ .Filename }}{{ end }}`)
+ b.Assert(err.Error(), qt.Contains, `single.html:1:22: executing "single.html" – File is nil; wrap it in if or with: {{ with site.Home.File }}{{ .Filename }}{{ end }}`)
}
(DIR) diff --git a/hugolib/rebuild_test.go b/hugolib/rebuild_test.go
@@ -51,6 +51,7 @@ My Section Bundle Content Content.
title: "My Section"
---
-- content/mysection/mysectiontext.txt --
+Content.
-- content/_index.md --
---
title: "Home"
@@ -99,15 +100,17 @@ My Other Text: {{ $r.Content }}|{{ $r.Permalink }}|
`
func TestRebuildEditLeafBundleHeaderOnly(t *testing.T) {
- b := TestRunning(t, rebuildFilesSimple)
- b.AssertFileContent("public/mysection/mysectionbundle/index.html",
- "My Section Bundle Content Content.")
-
- b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "My Section Bundle Content.", "My Section Bundle Content Edited.").Build()
- b.AssertFileContent("public/mysection/mysectionbundle/index.html",
- "My Section Bundle Content Edited.")
- b.AssertRenderCountPage(2) // home (rss) + bundle.
- b.AssertRenderCountContent(1)
+ t.Parallel()
+ for i := 0; i < 3; i++ {
+ b := TestRunning(t, rebuildFilesSimple)
+ b.AssertFileContent("public/mysection/mysectionbundle/index.html",
+ "My Section Bundle Content Content.")
+ b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "My Section Bundle Content.", "My Section Bundle Content Edited.").Build()
+ b.AssertFileContent("public/mysection/mysectionbundle/index.html",
+ "My Section Bundle Content Edited.")
+ b.AssertRenderCountPage(2) // home (rss) + bundle.
+ b.AssertRenderCountContent(1)
+ }
}
func TestRebuildEditTextFileInLeafBundle(t *testing.T) {
@@ -119,7 +122,7 @@ func TestRebuildEditTextFileInLeafBundle(t *testing.T) {
b.AssertFileContent("public/mysection/mysectionbundle/index.html",
"Text 2 Content Edited")
b.AssertRenderCountPage(1)
- b.AssertRenderCountContent(1)
+ b.AssertRenderCountContent(0)
}
func TestRebuildEditTextFileInShortcode(t *testing.T) {
@@ -180,17 +183,17 @@ func TestRebuildEditTextFileInHomeBundle(t *testing.T) {
b.AssertFileContent("public/index.html", "Home Content.")
b.AssertFileContent("public/index.html", "Home Text Content Edited.")
b.AssertRenderCountPage(1)
- b.AssertRenderCountContent(1)
+ b.AssertRenderCountContent(0)
}
func TestRebuildEditTextFileInBranchBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
- b.AssertFileContent("public/mysection/index.html", "My Section")
+ b.AssertFileContent("public/mysection/index.html", "My Section", "0:/mysection/mysectiontext.txt|Content.|")
b.EditFileReplaceAll("content/mysection/mysectiontext.txt", "Content.", "Content Edited.").Build()
- b.AssertFileContent("public/mysection/index.html", "My Section")
+ b.AssertFileContent("public/mysection/index.html", "My Section", "0:/mysection/mysectiontext.txt|Content Edited.|")
b.AssertRenderCountPage(1)
- b.AssertRenderCountContent(1)
+ b.AssertRenderCountContent(0)
}
func testRebuildBothWatchingAndRunning(t *testing.T, files string, withB func(b *IntegrationTestBuilder)) {
@@ -484,7 +487,43 @@ Home: {{ .Title }}|{{ .Content }}|
})
}
-func TestRebuildSingleWithBaseof(t *testing.T) {
+func TestRebuildSingle(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+title = "Hugo Site"
+baseURL = "https://example.com"
+disableKinds = ["term", "taxonomy", "sitemap", "robotstxt", "404"]
+disableLiveReload = true
+-- content/p1.md --
+---
+title: "P1"
+---
+P1 Content.
+-- layouts/index.html --
+Home.
+-- layouts/single.html --
+Single: {{ .Title }}|{{ .Content }}|
+{{ with (templates.Defer (dict "key" "global")) }}
+Defer.
+{{ end }}
+`
+ b := Test(t, files, TestOptRunning())
+ b.AssertFileContent("public/p1/index.html", "Single: P1|", "Defer.")
+ b.AssertRenderCountPage(3)
+ b.AssertRenderCountContent(1)
+ b.EditFileReplaceFunc("layouts/single.html", func(s string) string {
+ s = strings.Replace(s, "Single", "Single Edited", 1)
+ s = strings.Replace(s, "Defer.", "Defer Edited.", 1)
+ return s
+ }).Build()
+ b.AssertFileContent("public/p1/index.html", "Single Edited: P1|", "Defer Edited.")
+ b.AssertRenderCountPage(1)
+ b.AssertRenderCountContent(0)
+}
+
+func TestRebuildSingleWithBaseofEditSingle(t *testing.T) {
t.Parallel()
files := `
@@ -498,9 +537,13 @@ disableLiveReload = true
title: "P1"
---
P1 Content.
+[foo](/foo)
-- layouts/_default/baseof.html --
Baseof: {{ .Title }}|
{{ block "main" . }}default{{ end }}
+{{ with (templates.Defer (dict "foo" "bar")) }}
+Defer.
+{{ end }}
-- layouts/index.html --
Home.
-- layouts/_default/single.html --
@@ -509,11 +552,81 @@ Single: {{ .Title }}|{{ .Content }}|
{{ end }}
`
b := Test(t, files, TestOptRunning())
- b.AssertFileContent("public/p1/index.html", "Baseof: P1|\n\nSingle: P1|<p>P1 Content.</p>\n|")
+ b.AssertFileContent("public/p1/index.html", "Single: P1|")
b.EditFileReplaceFunc("layouts/_default/single.html", func(s string) string {
return strings.Replace(s, "Single", "Single Edited", 1)
}).Build()
- b.AssertFileContent("public/p1/index.html", "Baseof: P1|\n\nSingle Edited: P1|<p>P1 Content.</p>\n|")
+ b.AssertFileContent("public/p1/index.html", "Single Edited")
+}
+
+func TestRebuildSingleWithBaseofEditBaseof(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+title = "Hugo Site"
+baseURL = "https://example.com"
+disableKinds = ["term", "taxonomy"]
+disableLiveReload = true
+-- content/p1.md --
+---
+title: "P1"
+---
+P1 Content.
+[foo](/foo)
+-- layouts/_default/baseof.html --
+Baseof: {{ .Title }}|
+{{ block "main" . }}default{{ end }}
+{{ with (templates.Defer (dict "foo" "bar")) }}
+Defer.
+{{ end }}
+-- layouts/index.html --
+Home.
+-- layouts/_default/single.html --
+{{ define "main" }}
+Single: {{ .Title }}|{{ .Content }}|
+{{ end }}
+`
+ b := Test(t, files, TestOptRunning())
+ b.AssertFileContent("public/p1/index.html", "Single: P1|")
+ fmt.Println("===============")
+ b.EditFileReplaceAll("layouts/_default/baseof.html", "Baseof", "Baseof Edited").Build()
+ b.AssertFileContent("public/p1/index.html", "Baseof Edited")
+}
+
+func TestRebuildWithDeferEditRenderHook(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+title = "Hugo Site"
+baseURL = "https://example.com"
+disableKinds = ["term", "taxonomy"]
+disableLiveReload = true
+-- content/p1.md --
+---
+title: "P1"
+---
+P1 Content.
+[foo](/foo)
+-- layouts/_default/baseof.html --
+Baseof: {{ .Title }}|
+{{ block "main" . }}default{{ end }}
+ {{ with (templates.Defer (dict "foo" "bar")) }}
+Defer.
+{{ end }}
+-- layouts/single.html --
+{{ define "main" }}
+Single: {{ .Title }}|{{ .Content }}|
+{{ end }}
+-- layouts/_default/_markup/render-link.html --
+Render Link.
+`
+ b := Test(t, files, TestOptRunning())
+ // Edit render hook.
+ b.EditFileReplaceAll("layouts/_default/_markup/render-link.html", "Render Link", "Render Link Edited").Build()
+
+ b.AssertFileContent("public/p1/index.html", "Render Link Edited")
}
func TestRebuildFromString(t *testing.T) {
(DIR) diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// 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.
@@ -29,6 +29,7 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/gohugoio/hugo/resources/page"
@@ -36,7 +37,6 @@ import (
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/common/urls"
- "github.com/gohugoio/hugo/output"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/tpl"
@@ -205,8 +205,7 @@ type shortcode struct {
indentation string // indentation from source.
- info tpl.Info // One of the output formats (arbitrary)
- templs []tpl.Template // All output formats
+ templ *tplimpl.TemplInfo
// If set, the rendered shortcode is sent as part of the surrounding content
// to Goldmark and similar.
@@ -230,16 +229,15 @@ func (s shortcode) insertPlaceholder() bool {
}
func (s shortcode) needsInner() bool {
- return s.info != nil && s.info.ParseInfo().IsInner
+ return s.templ != nil && s.templ.ParseInfo.IsInner
}
func (s shortcode) configVersion() int {
- if s.info == nil {
+ if s.templ == nil {
// Not set for inline shortcodes.
return 2
}
-
- return s.info.ParseInfo().Config.Version
+ return s.templ.ParseInfo.Config.Version
}
func (s shortcode) innerString() string {
@@ -315,12 +313,12 @@ func prepareShortcode(
ctx context.Context,
level int,
s *Site,
- tplVariants tpl.TemplateVariants,
sc *shortcode,
parent *ShortcodeWithPage,
- p *pageState,
+ po *pageOutput,
isRenderString bool,
) (shortcodeRenderer, error) {
+ p := po.p
toParseErr := func(err error) error {
source := p.m.content.mustSource()
return p.parseError(fmt.Errorf("failed to render shortcode %q: %w", sc.name, err), source, sc.pos)
@@ -333,7 +331,7 @@ func prepareShortcode(
// parsed and rendered by Goldmark.
ctx = tpl.Context.IsInGoldmark.Set(ctx, true)
}
- r, err := doRenderShortcode(ctx, level, s, tplVariants, sc, parent, p, isRenderString)
+ r, err := doRenderShortcode(ctx, level, s, sc, parent, po, isRenderString)
if err != nil {
return nil, false, toParseErr(err)
}
@@ -352,30 +350,29 @@ func doRenderShortcode(
ctx context.Context,
level int,
s *Site,
- tplVariants tpl.TemplateVariants,
sc *shortcode,
parent *ShortcodeWithPage,
- p *pageState,
+ po *pageOutput,
isRenderString bool,
) (shortcodeRenderer, error) {
- var tmpl tpl.Template
+ var tmpl *tplimpl.TemplInfo
+ p := po.p
// Tracks whether this shortcode or any of its children has template variations
// in other languages or output formats. We are currently only interested in
- // the output formats, so we may get some false positives -- we
- // should improve on that.
+ // the output formats.
var hasVariants bool
if sc.isInline {
if !p.s.ExecHelper.Sec().EnableInlineShortcodes {
return zeroShortcode, nil
}
- templName := path.Join("_inline_shortcode", p.Path(), sc.name)
+ templatePath := path.Join("_inline_shortcode", p.Path(), sc.name)
if sc.isClosing {
templStr := sc.innerString()
var err error
- tmpl, err = s.TextTmpl().Parse(templName, templStr)
+ tmpl, err = s.TemplateStore.TextParse(templatePath, templStr)
if err != nil {
if isRenderString {
return zeroShortcode, p.wrapError(err)
@@ -389,21 +386,32 @@ func doRenderShortcode(
} else {
// Re-use of shortcode defined earlier in the same page.
- var found bool
- tmpl, found = s.TextTmpl().Lookup(templName)
- if !found {
+ tmpl = s.TemplateStore.TextLookup(templatePath)
+ if tmpl == nil {
return zeroShortcode, fmt.Errorf("no earlier definition of shortcode %q found", sc.name)
}
}
- tmpl = tpl.AddIdentity(tmpl)
} else {
- var found, more bool
- tmpl, found, more = s.Tmpl().LookupVariant(sc.name, tplVariants)
- if !found {
+ ofCount := map[string]int{}
+ include := func(match *tplimpl.TemplInfo) bool {
+ ofCount[match.D.OutputFormat]++
+ return true
+ }
+ base, layoutDescriptor := po.getTemplateBasePathAndDescriptor()
+ q := tplimpl.TemplateQuery{
+ Path: base,
+ Name: sc.name,
+ Category: tplimpl.CategoryShortcode,
+ Desc: layoutDescriptor,
+ Consider: include,
+ }
+ v := s.TemplateStore.LookupShortcode(q)
+ if v == nil {
s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path())
return zeroShortcode, nil
}
- hasVariants = hasVariants || more
+ tmpl = v
+ hasVariants = hasVariants || len(ofCount) > 1
}
data := &ShortcodeWithPage{
@@ -427,7 +435,7 @@ func doRenderShortcode(
case string:
inner += innerData
case *shortcode:
- s, err := prepareShortcode(ctx, level+1, s, tplVariants, innerData, data, p, isRenderString)
+ s, err := prepareShortcode(ctx, level+1, s, innerData, data, po, isRenderString)
if err != nil {
return zeroShortcode, err
}
@@ -484,7 +492,7 @@ func doRenderShortcode(
}
- result, err := renderShortcodeWithPage(ctx, s.Tmpl(), tmpl, data)
+ result, err := renderShortcodeWithPage(ctx, s.GetTemplateStore(), tmpl, data)
if err != nil && sc.isInline {
fe := herrors.NewFileErrorFromName(err, p.File().Filename())
@@ -534,16 +542,11 @@ func (s *shortcodeHandler) hasName(name string) bool {
return ok
}
-func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, p *pageState, f output.Format, isRenderString bool) (map[string]shortcodeRenderer, error) {
+func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, po *pageOutput, isRenderString bool) (map[string]shortcodeRenderer, error) {
rendered := make(map[string]shortcodeRenderer)
- tplVariants := tpl.TemplateVariants{
- Language: p.Language().Lang,
- OutputFormat: f,
- }
-
for _, v := range s.shortcodes {
- s, err := prepareShortcode(ctx, 0, s.s, tplVariants, v, nil, p, isRenderString)
+ s, err := prepareShortcode(ctx, 0, s.s, v, nil, po, isRenderString)
if err != nil {
return nil, err
}
@@ -636,7 +639,7 @@ Loop:
// we trust the template on this:
// if there's no inner, we're done
if !sc.isInline {
- if !sc.info.ParseInfo().IsInner {
+ if !sc.templ.ParseInfo.IsInner {
return sc, nil
}
}
@@ -672,14 +675,19 @@ Loop:
sc.name = currItem.ValStr(source)
- // Used to check if the template expects inner content.
- templs := s.s.Tmpl().LookupVariants(sc.name)
- if templs == nil {
+ // Used to check if the template expects inner content,
+ // so just pick one arbitrarily with the same name.
+ q := tplimpl.TemplateQuery{
+ Path: "",
+ Name: sc.name,
+ Category: tplimpl.CategoryShortcode,
+ Consider: nil,
+ }
+ templ := s.s.TemplateStore.LookupShortcode(q)
+ if templ == nil {
return nil, fmt.Errorf("%s: template for shortcode %q not found", errorPrefix, sc.name)
}
-
- sc.info = templs[0].(tpl.Info)
- sc.templs = templs
+ sc.templ = templ
case currItem.IsInlineShortcodeName():
sc.name = currItem.ValStr(source)
sc.isInline = true
@@ -778,7 +786,7 @@ func expandShortcodeTokens(
return source, nil
}
-func renderShortcodeWithPage(ctx context.Context, h tpl.TemplateHandler, tmpl tpl.Template, data *ShortcodeWithPage) (string, error) {
+func renderShortcodeWithPage(ctx context.Context, h *tplimpl.TemplateStore, tmpl *tplimpl.TemplInfo, data *ShortcodeWithPage) (string, error) {
buffer := bp.GetBuffer()
defer bp.PutBuffer(buffer)
(DIR) diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go
@@ -33,14 +33,14 @@ func TestExtractShortcodes(t *testing.T) {
b := newTestSitesBuilder(t).WithSimpleConfigFile()
b.WithTemplates(
- "default/single.html", `EMPTY`,
- "_internal/shortcodes/tag.html", `tag`,
- "_internal/shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`,
- "_internal/shortcodes/sc1.html", `sc1`,
- "_internal/shortcodes/sc2.html", `sc2`,
- "_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`,
- "_internal/shortcodes/inner2.html", `{{.Inner}}`,
- "_internal/shortcodes/inner3.html", `{{.Inner}}`,
+ "pages/single.html", `EMPTY`,
+ "shortcodes/tag.html", `tag`,
+ "shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`,
+ "shortcodes/sc1.html", `sc1`,
+ "shortcodes/sc2.html", `sc2`,
+ "shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`,
+ "shortcodes/inner2.html", `{{.Inner}}`,
+ "shortcodes/inner3.html", `{{.Inner}}`,
).WithContent("page.md", `---
title: "Shortcodes Galore!"
---
@@ -57,10 +57,9 @@ title: "Shortcodes Galore!"
if s == nil {
return "<nil>"
}
-
var version int
- if s.info != nil {
- version = s.info.ParseInfo().Config.Version
+ if s.templ != nil {
+ version = s.templ.ParseInfo.Config.Version
}
return strReplacer.Replace(fmt.Sprintf("%s;inline:%t;closing:%t;inner:%v;params:%v;ordinal:%d;markup:%t;version:%d;pos:%d",
s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, version, s.pos))
@@ -69,7 +68,7 @@ title: "Shortcodes Galore!"
regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) {
return func(c *qt.C, shortcode *shortcode, err error) {
c.Assert(err, qt.IsNil)
- c.Assert(str(shortcode), qt.Matches, ".*"+re+".*")
+ c.Assert(str(shortcode), qt.Matches, ".*"+re+".*", qt.Commentf("%s", shortcode.name))
}
}
@@ -888,6 +887,7 @@ outputs: ["html", "css", "csv", "json"]
"_default/single.json", "{{ .Content }}",
"shortcodes/myshort.html", `Short-HTML`,
"shortcodes/myshort.csv", `Short-CSV`,
+ "shortcodes/myshort.txt", `Short-TXT`,
)
b.Build(BuildCfg{})
@@ -897,12 +897,12 @@ outputs: ["html", "css", "csv", "json"]
for i := range numPages {
b.AssertFileContent(fmt.Sprintf("public/page%d/index.html", i), "Short-HTML")
b.AssertFileContent(fmt.Sprintf("public/page%d/index.csv", i), "Short-CSV")
- b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-HTML")
+ b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-CSV")
}
for i := range numPages {
- b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-HTML")
+ b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-CSV")
}
}
(DIR) diff --git a/hugolib/site.go b/hugolib/site.go
@@ -47,7 +47,13 @@ import (
"github.com/gohugoio/hugo/langs/i18n"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/resources"
+
"github.com/gohugoio/hugo/tpl/tplimpl"
+ "github.com/gohugoio/hugo/tpl/tplimplinit"
+ xmaps "golang.org/x/exp/maps"
+
+ // Loads the template funcs namespaces.
+
"golang.org/x/text/unicode/norm"
"github.com/gohugoio/hugo/common/paths"
@@ -188,8 +194,8 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
BuildState: &deps.BuildState{
OnSignalRebuild: onSignalRebuild,
},
+ Counters: &deps.Counters{},
MemCache: memCache,
- TemplateProvider: tplimpl.DefaultTemplateProvider,
TranslationProvider: i18n.NewTranslationProvider(),
WasmDispatchers: warpc.AllDispatchers(
warpc.Options{
@@ -385,6 +391,34 @@ func newHugoSites(cfg deps.DepsCfg, d *deps.Deps, pageTrees *pageTrees, sites []
var prototype *deps.Deps
for i, s := range sites {
s.h = h
+ // The template store needs to be initialized after the h container is set on s.
+ if i == 0 {
+ templateStore, err := tplimpl.NewStore(
+ tplimpl.StoreOptions{
+ Fs: s.BaseFs.Layouts.Fs,
+ DefaultContentLanguage: s.Conf.DefaultContentLanguage(),
+ Watching: s.Conf.Watching(),
+ PathParser: s.Conf.PathParser(),
+ Metrics: d.Metrics,
+ OutputFormats: s.conf.OutputFormats.Config,
+ MediaTypes: s.conf.MediaTypes.Config,
+ DefaultOutputFormat: s.conf.DefaultOutputFormat,
+ TaxonomySingularPlural: s.conf.Taxonomies,
+ }, tplimpl.SiteOptions{
+ Site: s,
+ TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps),
+ })
+ if err != nil {
+ return nil, err
+ }
+ s.Deps.TemplateStore = templateStore
+ } else {
+ s.Deps.TemplateStore = prototype.TemplateStore.WithSiteOpts(
+ tplimpl.SiteOptions{
+ Site: s,
+ TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps),
+ })
+ }
if err := s.Deps.Compile(prototype); err != nil {
return nil, err
}
@@ -464,7 +498,10 @@ func (s *Site) MainSections() []string {
// Returns a struct with some information about the build.
func (s *Site) Hugo() hugo.HugoInfo {
- if s.h == nil || s.h.hugoInfo.Environment == "" {
+ if s.h == nil {
+ panic("site: hugo: h not initialized")
+ }
+ if s.h.hugoInfo.Environment == "" {
panic("site: hugo: hugoInfo not initialized")
}
return s.h.hugoInfo
@@ -797,7 +834,7 @@ func (s *Site) initRenderFormats() {
s.renderFormats = formats
}
-func (s *Site) GetRelatedDocsHandler() *page.RelatedDocsHandler {
+func (s *Site) GetInternalRelatedDocsHandler() *page.RelatedDocsHandler {
return s.relatedDocsHandler
}
@@ -923,19 +960,24 @@ type WhatChanged struct {
mu sync.Mutex
needsPagesAssembly bool
- identitySet identity.Identities
+
+ ids map[identity.Identity]bool
+}
+
+func (w *WhatChanged) init() {
+ if w.ids == nil {
+ w.ids = make(map[identity.Identity]bool)
+ }
}
func (w *WhatChanged) Add(ids ...identity.Identity) {
w.mu.Lock()
defer w.mu.Unlock()
- if w.identitySet == nil {
- w.identitySet = make(identity.Identities)
- }
+ w.init()
for _, id := range ids {
- w.identitySet[id] = true
+ w.ids[id] = true
}
}
@@ -946,20 +988,20 @@ func (w *WhatChanged) Clear() {
}
func (w *WhatChanged) clear() {
- w.identitySet = identity.Identities{}
+ w.ids = nil
}
func (w *WhatChanged) Changes() []identity.Identity {
- if w == nil || w.identitySet == nil {
+ if w == nil || w.ids == nil {
return nil
}
- return w.identitySet.AsSlice()
+ return xmaps.Keys(w.ids)
}
func (w *WhatChanged) Drain() []identity.Identity {
w.mu.Lock()
defer w.mu.Unlock()
- ids := w.identitySet.AsSlice()
+ ids := w.Changes()
w.clear()
return ids
}
@@ -1394,7 +1436,7 @@ const (
pageDependencyScopeGlobal
)
-func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ tpl.Template) error {
+func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ *tplimpl.TemplInfo) error {
s.h.buildCounters.pageRenderCounter.Add(1)
renderBuffer := bp.GetBuffer()
defer bp.PutBuffer(renderBuffer)
@@ -1453,8 +1495,8 @@ var infoOnMissingLayout = map[string]bool{
// hookRendererTemplate is the canonical implementation of all hooks.ITEMRenderer,
// where ITEM is the thing being hooked.
type hookRendererTemplate struct {
- templateHandler tpl.TemplateHandler
- templ tpl.Template
+ templateHandler *tplimpl.TemplateStore
+ templ *tplimpl.TemplInfo
resolvePosition func(ctx any) text.Position
}
@@ -1490,7 +1532,7 @@ func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool {
return false
}
-func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ tpl.Template) (err error) {
+func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ *tplimpl.TemplInfo) (err error) {
if templ == nil {
s.logMissingLayout(name, "", "", outputFormat)
return nil
@@ -1500,7 +1542,7 @@ func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string,
panic("nil context")
}
- if err = s.Tmpl().ExecuteWithContext(ctx, templ, w, d); err != nil {
+ if err = s.GetTemplateStore().ExecuteWithContext(ctx, templ, w, d); err != nil {
filename := name
if p, ok := d.(*pageState); ok {
filename = p.String()
(DIR) diff --git a/hugolib/site_output.go b/hugolib/site_output.go
@@ -27,6 +27,7 @@ func createDefaultOutputFormats(allFormats output.Formats) map[string]output.For
htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name)
robotsOut, _ := allFormats.GetByName(output.RobotsTxtFormat.Name)
sitemapOut, _ := allFormats.GetByName(output.SitemapFormat.Name)
+ httpStatus404Out, _ := allFormats.GetByName(output.HTTPStatus404HTMLFormat.Name)
defaultListTypes := output.Formats{htmlOut}
if rssFound {
@@ -42,7 +43,7 @@ func createDefaultOutputFormats(allFormats output.Formats) map[string]output.For
// Below are for consistency. They are currently not used during rendering.
kinds.KindSitemap: {sitemapOut},
kinds.KindRobotsTXT: {robotsOut},
- kinds.KindStatus404: {htmlOut},
+ kinds.KindStatus404: {httpStatus404Out},
}
// May be disabled
(DIR) diff --git a/hugolib/site_output_test.go b/hugolib/site_output_test.go
@@ -387,7 +387,7 @@ func TestCreateSiteOutputFormats(t *testing.T) {
c.Assert(outputs[kinds.KindRSS], deepEqualsOutputFormats, output.Formats{output.RSSFormat})
c.Assert(outputs[kinds.KindSitemap], deepEqualsOutputFormats, output.Formats{output.SitemapFormat})
c.Assert(outputs[kinds.KindRobotsTXT], deepEqualsOutputFormats, output.Formats{output.RobotsTxtFormat})
- c.Assert(outputs[kinds.KindStatus404], deepEqualsOutputFormats, output.Formats{output.HTMLFormat})
+ c.Assert(outputs[kinds.KindStatus404], deepEqualsOutputFormats, output.Formats{output.HTTPStatus404HTMLFormat})
})
// Issue #4528
@@ -481,6 +481,7 @@ permalinkable = true
[outputFormats.nobase]
mediaType = "application/json"
permalinkable = true
+isPlainText = true
`
(DIR) diff --git a/hugolib/site_render.go b/hugolib/site_render.go
@@ -23,9 +23,9 @@ import (
"github.com/bep/logg"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugolib/doctree"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/config"
- "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/resources/kinds"
"github.com/gohugoio/hugo/resources/page"
@@ -57,7 +57,7 @@ func (s siteRenderContext) shouldRenderStandalonePage(kind string) bool {
return s.outIdx == 0
}
- if kind == kinds.KindStatus404 {
+ if kind == kinds.KindTemporary || kind == kinds.KindStatus404 {
// 1 for all output formats
return s.outIdx == 0
}
@@ -168,7 +168,7 @@ func pageRenderer(
s.Log.Trace(
func() string {
- return fmt.Sprintf("rendering outputFormat %q kind %q using layout %q to %q", p.pageOutput.f.Name, p.Kind(), templ.Name(), targetPath)
+ return fmt.Sprintf("rendering outputFormat %q kind %q using layout %q to %q", p.pageOutput.f.Name, p.Kind(), templ.Template.Name(), targetPath)
},
)
@@ -225,7 +225,7 @@ func (s *Site) logMissingLayout(name, layout, kind, outputFormat string) {
}
// renderPaginator must be run after the owning Page has been rendered.
-func (s *Site) renderPaginator(p *pageState, templ tpl.Template) error {
+func (s *Site) renderPaginator(p *pageState, templ *tplimpl.TemplInfo) error {
paginatePath := s.Conf.Pagination().Path
d := p.targetPathDescriptor
(DIR) diff --git a/hugolib/site_test.go b/hugolib/site_test.go
@@ -978,8 +978,7 @@ func TestRefLinking(t *testing.T) {
{".", "", true, "/level2/level3/"},
{"./", "", true, "/level2/level3/"},
- // try to confuse parsing
- {"embedded.dot.md", "", true, "/level2/level3/embedded.dot/"},
+ {"embedded.dot.md", "", true, "/level2/level3/embedded/"},
// test empty link, as well as fragment only link
{"", "", true, ""},
(DIR) diff --git a/hugolib/taxonomy_test.go b/hugolib/taxonomy_test.go
@@ -76,6 +76,8 @@ func TestTaxonomiesWithAndWithoutContentFile(t *testing.T) {
}
func doTestTaxonomiesWithAndWithoutContentFile(t *testing.T, uglyURLs bool) {
+ t.Helper()
+
siteConfig := `
baseURL = "http://example.com/blog"
titleCaseStyle = "firstupper"
(DIR) diff --git a/hugolib/template_test.go b/hugolib/template_test.go
@@ -26,6 +26,8 @@ import (
"github.com/gohugoio/hugo/hugofs"
)
+// TODO(bep) keep this until we release v0.146.0 as a security against breaking changes, but it's rather messy and mostly duplicate of
+// tests in the tplimpl package, so eventually just remove it.
func TestTemplateLookupOrder(t *testing.T) {
var (
fs *hugofs.Fs
@@ -185,6 +187,9 @@ func TestTemplateLookupOrder(t *testing.T) {
} {
this := this
+ if this.name != "Variant 1" {
+ continue
+ }
t.Run(this.name, func(t *testing.T) {
// TODO(bep) there are some function vars need to pull down here to enable => t.Parallel()
cfg, fs = newTestCfg()
@@ -200,7 +205,7 @@ Some content
}
buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{})
- // helpers.PrintFs(s.BaseFs.Layouts.Fs, "", os.Stdout)
+ // s.TemplateStore.PrintDebug("", 0, os.Stdout)
this.assert(t)
})
@@ -270,11 +275,11 @@ func TestTemplateNoBasePlease(t *testing.T) {
b := newTestSitesBuilder(t).WithSimpleConfigFile()
b.WithTemplates("_default/list.html", `
- {{ define "main" }}
- Bonjour
- {{ end }}
+{{ define "main" }}
+ Bonjour
+{{ end }}
- {{ printf "list" }}
+{{ printf "list" }}
`)
@@ -344,33 +349,36 @@ title: %s
b.AssertFileContent("public/p1/index.html", `Single: P1`)
})
- t.Run("baseof", func(t *testing.T) {
- t.Parallel()
- b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
+ {
+ }
+}
- b.WithTemplatesAdded(
- "index.html", `{{ define "main" }}Main Home En{{ end }}`,
- "index.fr.html", `{{ define "main" }}Main Home Fr{{ end }}`,
- "baseof.html", `Baseof en: {{ block "main" . }}main block{{ end }}`,
- "baseof.fr.html", `Baseof fr: {{ block "main" . }}main block{{ end }}`,
- "mysection/baseof.html", `Baseof mysection: {{ block "main" . }}mysection block{{ end }}`,
- "_default/single.html", `{{ define "main" }}Main Default Single{{ end }}`,
- "_default/list.html", `{{ define "main" }}Main Default List{{ end }}`,
- )
+func TestTemplateLookupSitBaseOf(t *testing.T) {
+ t.Parallel()
+ b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
+
+ b.WithTemplatesAdded(
+ "index.html", `{{ define "main" }}Main Home En{{ end }}`,
+ "index.fr.html", `{{ define "main" }}Main Home Fr{{ end }}`,
+ "baseof.html", `Baseof en: {{ block "main" . }}main block{{ end }}`,
+ "baseof.fr.html", `Baseof fr: {{ block "main" . }}main block{{ end }}`,
+ "mysection/baseof.html", `Baseof mysection: {{ block "main" . }}mysection block{{ end }}`,
+ "_default/single.html", `{{ define "main" }}Main Default Single{{ end }}`,
+ "_default/list.html", `{{ define "main" }}Main Default List{{ end }}`,
+ )
- b.WithContent("mysection/p1.md", `---
+ b.WithContent("mysection/p1.md", `---
title: My Page
---
`)
- b.CreateSites().Build(BuildCfg{})
+ b.CreateSites().Build(BuildCfg{})
- b.AssertFileContent("public/en/index.html", `Baseof en: Main Home En`)
- b.AssertFileContent("public/fr/index.html", `Baseof fr: Main Home Fr`)
- b.AssertFileContent("public/en/mysection/index.html", `Baseof mysection: Main Default List`)
- b.AssertFileContent("public/en/mysection/p1/index.html", `Baseof mysection: Main Default Single`)
- })
+ b.AssertFileContent("public/en/index.html", `Baseof en: Main Home En`)
+ b.AssertFileContent("public/fr/index.html", `Baseof fr: Main Home Fr`)
+ b.AssertFileContent("public/en/mysection/index.html", `Baseof mysection: Main Default List`)
+ b.AssertFileContent("public/en/mysection/p1/index.html", `Baseof mysection: Main Default Single`)
}
func TestTemplateFuncs(t *testing.T) {
@@ -707,6 +715,7 @@ a: {{ $a }}
b.AssertFileContent("public/index.html", `a: [a b c]`)
}
+// Legacy behavior for internal templates.
func TestOverrideInternalTemplate(t *testing.T) {
files := `
-- hugo.toml --
(DIR) diff --git a/identity/identity.go b/identity/identity.go
@@ -509,6 +509,10 @@ func probablyEq(a, b Identity) bool {
return true
}
+ if a2, ok := a.(compare.ProbablyEqer); ok && a2.ProbablyEq(b) {
+ return true
+ }
+
if a2, ok := a.(IsProbablyDependentProvider); ok {
return a2.IsProbablyDependent(b)
}
(DIR) diff --git a/internal/js/esbuild/batch.go b/internal/js/esbuild/batch.go
@@ -43,7 +43,7 @@ import (
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/resources/resource_factories/create"
- "github.com/gohugoio/hugo/tpl"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
)
@@ -192,7 +192,7 @@ type BatcherClient struct {
d *deps.Deps
once sync.Once
- runnerTemplate tpl.Template
+ runnerTemplate *tplimpl.TemplInfo
createClient *create.Client
buildClient *BuildClient
@@ -208,7 +208,7 @@ func (c *BatcherClient) New(id string) (js.Batcher, error) {
c.once.Do(func() {
// We should fix the initialization order here (or use the Go template package directly), but we need to wait
// for the Hugo templates to be ready.
- tmpl, err := c.d.TextTmpl().Parse("batch-esm-runner", runnerTemplateStr)
+ tmpl, err := c.d.TemplateStore.TextParse("batch-esm-runner", runnerTemplateStr)
if err != nil {
initErr = err
return
@@ -287,7 +287,7 @@ func (c *BatcherClient) Store() *maps.Cache[string, js.Batcher] {
func (c *BatcherClient) buildBatchGroup(ctx context.Context, t *batchGroupTemplateContext) (resource.Resource, string, error) {
var buf bytes.Buffer
- if err := c.d.Tmpl().ExecuteWithContext(ctx, c.runnerTemplate, &buf, t); err != nil {
+ if err := c.d.GetTemplateStore().ExecuteWithContext(ctx, c.runnerTemplate, &buf, t); err != nil {
return nil, "", err
}
(DIR) diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go
@@ -23,8 +23,6 @@ import (
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config/testconfig"
- "github.com/gohugoio/hugo/tpl/tplimpl"
-
"github.com/gohugoio/hugo/resources/page"
"github.com/spf13/afero"
@@ -472,7 +470,6 @@ func prepareTranslationProvider(t testing.TB, test i18nTest, cfg config.Provider
func prepareDeps(afs afero.Fs, cfg config.Provider) (*deps.Deps, *TranslationProvider) {
d := testconfig.GetTestDeps(afs, cfg)
translationProvider := NewTranslationProvider()
- d.TemplateProvider = tplimpl.DefaultTemplateProvider
d.TranslationProvider = translationProvider
d.Site = page.NewDummyHugoSite(d.Conf)
if err := d.Compile(nil); err != nil {
(DIR) diff --git a/markup/goldmark/codeblocks/codeblocks_integration_test.go b/markup/goldmark/codeblocks/codeblocks_integration_test.go
@@ -69,7 +69,7 @@ fmt.Println("Hello, World!");
## Golang Code
-§§§golang
+§§§go
fmt.Println("Hello, Golang!");
§§§
@@ -97,14 +97,14 @@ Go Language: go|
Go Code: fmt.Println("Hello, World!");
Go Code: fmt.Println("Hello, Golang!");
-Go Language: golang|
+Go Language: go|
`,
"Goat SVG:<svg class='diagram' xmlns='http://www.w3.org/2000/svg' version='1.1' height='25' width='40'",
"Goat Attribute: 600|",
"<h2 id=\"go-code\">Go Code</h2>\nGo Code: fmt.Println(\"Hello, World!\");\n|\nGo Language: go|",
- "<h2 id=\"golang-code\">Golang Code</h2>\nGo Code: fmt.Println(\"Hello, Golang!\");\n|\nGo Language: golang|",
+ "<h2 id=\"golang-code\">Golang Code</h2>\nGo Code: fmt.Println(\"Hello, Golang!\");\n|\nGo Language: go|",
"<h2 id=\"bash-code\">Bash Code</h2>\n<div class=\"highlight blue\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"ln\">32</span><span class=\"cl\"><span class=\"nb\">echo</span> <span class=\"s2\">"l1"</span><span class=\"p\">;</span>\n</span></span><span class=\"line hl\"><span class=\"ln\">33</span>",
)
}
(DIR) diff --git a/media/builtin.go b/media/builtin.go
@@ -5,6 +5,7 @@ type BuiltinTypes struct {
CSSType Type
SCSSType Type
SASSType Type
+ GotmplType Type
CSVType Type
HTMLType Type
JavascriptType Type
@@ -60,6 +61,7 @@ var Builtin = BuiltinTypes{
CSSType: Type{Type: "text/css"},
SCSSType: Type{Type: "text/x-scss"},
SASSType: Type{Type: "text/x-sass"},
+ GotmplType: Type{Type: "text/x-gotmpl"},
CSVType: Type{Type: "text/csv"},
HTMLType: Type{Type: "text/html"},
JavascriptType: Type{Type: "text/javascript"},
@@ -121,6 +123,7 @@ var defaultMediaTypesConfig = map[string]any{
"text/typescript": map[string]any{"suffixes": []string{"ts"}},
"text/tsx": map[string]any{"suffixes": []string{"tsx"}},
"text/jsx": map[string]any{"suffixes": []string{"jsx"}},
+ "text/x-gotmpl": map[string]any{"suffixes": []string{"gotmpl"}},
"application/json": map[string]any{"suffixes": []string{"json"}},
"application/manifest+json": map[string]any{"suffixes": []string{"webmanifest"}},
(DIR) diff --git a/media/config.go b/media/config.go
@@ -17,6 +17,7 @@ import (
"fmt"
"path/filepath"
"reflect"
+ "slices"
"sort"
"strings"
@@ -26,7 +27,6 @@ import (
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
- "slices"
)
// DefaultTypes is the default media types supported by Hugo.
@@ -271,4 +271,7 @@ var DefaultPathParser = &paths.PathParser{
IsContentExt: func(ext string) bool {
panic("not supported")
},
+ IsOutputFormat: func(name, ext string) bool {
+ panic("DefaultPathParser: not supported")
+ },
}
(DIR) diff --git a/media/config_test.go b/media/config_test.go
@@ -151,5 +151,5 @@ func TestDefaultTypes(t *testing.T) {
}
- c.Assert(len(DefaultTypes), qt.Equals, 40)
+ c.Assert(len(DefaultTypes), qt.Equals, 41)
}
(DIR) diff --git a/media/mediaType.go b/media/mediaType.go
@@ -282,7 +282,7 @@ func (t Types) BySuffix(suffix string) []Type {
suffix = t.normalizeSuffix(suffix)
var types []Type
for _, tt := range t {
- if tt.hasSuffix(suffix) {
+ if tt.HasSuffix(suffix) {
types = append(types, tt)
}
}
@@ -293,7 +293,7 @@ func (t Types) BySuffix(suffix string) []Type {
func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
- if tt.hasSuffix(suffix) {
+ if tt.HasSuffix(suffix) {
return tt, SuffixInfo{
FullSuffix: tt.Delimiter + suffix,
Suffix: suffix,
@@ -310,7 +310,7 @@ func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
- if tt.hasSuffix(suffix) {
+ if tt.HasSuffix(suffix) {
if found {
// ambiguous
found = false
@@ -330,14 +330,14 @@ func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
func (t Types) IsTextSuffix(suffix string) bool {
suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
- if tt.hasSuffix(suffix) {
+ if tt.HasSuffix(suffix) {
return tt.IsText()
}
}
return false
}
-func (m Type) hasSuffix(suffix string) bool {
+func (m Type) HasSuffix(suffix string) bool {
return strings.Contains(","+m.SuffixesCSV+",", ","+suffix+",")
}
(DIR) diff --git a/output/docshelper.go b/output/docshelper.go
@@ -1,12 +1,10 @@
package output
import (
- "strings"
// "fmt"
"github.com/gohugoio/hugo/docshelper"
- "github.com/gohugoio/hugo/output/layouts"
)
// This is is just some helpers used to create some JSON used in the Hugo docs.
@@ -14,90 +12,12 @@ func init() {
docsProvider := func() docshelper.DocProvider {
return docshelper.DocProvider{
"output": map[string]any{
- "layouts": createLayoutExamples(),
+ // TODO(bep), maybe revisit this later, but I hope this isn't needed.
+ // "layouts": createLayoutExamples(),
+ "layouts": map[string]any{},
},
}
}
docshelper.AddDocProviderFunc(docsProvider)
}
-
-func createLayoutExamples() any {
- type Example struct {
- Example string
- Kind string
- OutputFormat string
- Suffix string
- Layouts []string `json:"Template Lookup Order"`
- }
-
- var (
- basicExamples []Example
- demoLayout = "demolayout"
- demoType = "demotype"
- )
-
- for _, example := range []struct {
- name string
- d layouts.LayoutDescriptor
- }{
- // Taxonomy layouts.LayoutDescriptor={categories category taxonomy en false Type Section
- {"Single page in \"posts\" section", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Base template for single page in \"posts\" section", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Single page in \"posts\" section with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
- {"Base template for single page in \"posts\" section with layout set to \"demolayout\"", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
- {"AMP single page in \"posts\" section", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "amp", Suffix: "html"}},
- {"AMP single page in \"posts\" section, French language", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}},
- // Typeless pages get "page" as type
- {"Home page", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
- {"Base template for home page", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
- {"Home page with type set to \"demotype\"", layouts.LayoutDescriptor{Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}},
- {"Base template for home page with type set to \"demotype\"", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}},
- {"Home page with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "home", Type: "page", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
- {"AMP home, French language", layouts.LayoutDescriptor{Kind: "home", Type: "page", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}},
- {"JSON home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "json", Suffix: "json"}},
- {"RSS home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "rss", Suffix: "xml"}},
-
- {"Section list for \"posts\"", layouts.LayoutDescriptor{Kind: "section", Type: "posts", Section: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Section list for \"posts\" with type set to \"blog\"", layouts.LayoutDescriptor{Kind: "section", Type: "blog", Section: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Section list for \"posts\" with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "section", Layout: demoLayout, Section: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Section list for \"posts\"", layouts.LayoutDescriptor{Kind: "section", Type: "posts", OutputFormatName: "rss", Suffix: "xml"}},
-
- {"Taxonomy list for \"categories\"", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}},
- {"Taxonomy list for \"categories\"", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "xml"}},
-
- {"Term list for \"categories\"", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}},
- {"Term list for \"categories\"", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "xml"}},
- } {
-
- l := layouts.NewLayoutHandler()
- layouts, _ := l.For(example.d)
-
- basicExamples = append(basicExamples, Example{
- Example: example.name,
- Kind: example.d.Kind,
- OutputFormat: example.d.OutputFormatName,
- Suffix: example.d.Suffix,
- Layouts: makeLayoutsPresentable(layouts),
- })
- }
-
- return basicExamples
-}
-
-func makeLayoutsPresentable(l []string) []string {
- var filtered []string
- for _, ll := range l {
- if strings.Contains(ll, "page/") {
- // This is a valid lookup, but it's more confusing than useful.
- continue
- }
- ll = "layouts/" + strings.TrimPrefix(ll, "_text/")
-
- if !strings.Contains(ll, "indexes") {
- filtered = append(filtered, ll)
- }
- }
-
- return filtered
-}
(DIR) diff --git a/output/layouts/layout.go b/output/layouts/layout.go
@@ -1,336 +0,0 @@
-// Copyright 2024 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 layouts
-
-import (
- "strings"
- "sync"
-)
-
-// These may be used as content sections with potential conflicts. Avoid that.
-var reservedSections = map[string]bool{
- "shortcodes": true,
- "partials": true,
-}
-
-// LayoutDescriptor describes how a layout should be chosen. This is
-// typically built from a Page.
-type LayoutDescriptor struct {
- Type string
- Section string
-
- // E.g. "page", but also used for the _markup render kinds, e.g. "render-image".
- Kind string
-
- // Comma-separated list of kind variants, e.g. "go,json" as variants which would find "render-codeblock-go.html"
- KindVariants string
-
- Lang string
- Layout string
- // LayoutOverride indicates what we should only look for the above layout.
- LayoutOverride bool
-
- // From OutputFormat and MediaType.
- OutputFormatName string
- Suffix string
-
- RenderingHook bool
- Baseof bool
-}
-
-func (d LayoutDescriptor) isList() bool {
- return !d.RenderingHook && d.Kind != "page" && d.Kind != "404" && d.Kind != "sitemap" && d.Kind != "sitemapindex"
-}
-
-// LayoutHandler calculates the layout template to use to render a given output type.
-type LayoutHandler struct {
- mu sync.RWMutex
- cache map[LayoutDescriptor][]string
-}
-
-// NewLayoutHandler creates a new LayoutHandler.
-func NewLayoutHandler() *LayoutHandler {
- return &LayoutHandler{cache: make(map[LayoutDescriptor][]string)}
-}
-
-// For returns a layout for the given LayoutDescriptor and options.
-// Layouts are rendered and cached internally.
-func (l *LayoutHandler) For(d LayoutDescriptor) ([]string, error) {
- // We will get lots of requests for the same layouts, so avoid recalculations.
- l.mu.RLock()
- if cacheVal, found := l.cache[d]; found {
- l.mu.RUnlock()
- return cacheVal, nil
- }
- l.mu.RUnlock()
-
- layouts := resolvePageTemplate(d)
-
- layouts = uniqueStringsReuse(layouts)
-
- l.mu.Lock()
- l.cache[d] = layouts
- l.mu.Unlock()
-
- return layouts, nil
-}
-
-type layoutBuilder struct {
- layoutVariations []string
- typeVariations []string
- d LayoutDescriptor
- // f Format
-}
-
-func (l *layoutBuilder) addLayoutVariations(vars ...string) {
- for _, layoutVar := range vars {
- if l.d.Baseof && layoutVar != "baseof" {
- l.layoutVariations = append(l.layoutVariations, layoutVar+"-baseof")
- continue
- }
- if !l.d.RenderingHook && !l.d.Baseof && l.d.LayoutOverride && layoutVar != l.d.Layout {
- continue
- }
- l.layoutVariations = append(l.layoutVariations, layoutVar)
- }
-}
-
-func (l *layoutBuilder) addTypeVariations(vars ...string) {
- for _, typeVar := range vars {
- if !reservedSections[typeVar] {
- if l.d.RenderingHook {
- typeVar = typeVar + renderingHookRoot
- }
- l.typeVariations = append(l.typeVariations, typeVar)
- }
- }
-}
-
-func (l *layoutBuilder) addSectionType() {
- if l.d.Section != "" {
- l.addTypeVariations(l.d.Section)
- }
-}
-
-func (l *layoutBuilder) addKind() {
- l.addLayoutVariations(l.d.Kind)
- l.addTypeVariations(l.d.Kind)
-}
-
-const renderingHookRoot = "/_markup"
-
-func resolvePageTemplate(d LayoutDescriptor) []string {
- b := &layoutBuilder{d: d}
-
- if !d.RenderingHook && d.Layout != "" {
- b.addLayoutVariations(d.Layout)
- }
- if d.Type != "" {
- b.addTypeVariations(d.Type)
- }
-
- if d.RenderingHook {
- if d.KindVariants != "" {
- // Add the more specific variants first.
- for _, variant := range strings.Split(d.KindVariants, ",") {
- b.addLayoutVariations(d.Kind + "-" + variant)
- }
- }
- b.addLayoutVariations(d.Kind)
- b.addSectionType()
- }
-
- switch d.Kind {
- case "page":
- b.addLayoutVariations("single")
- b.addSectionType()
- case "home":
- b.addLayoutVariations("index", "home")
- // Also look in the root
- b.addTypeVariations("")
- case "section":
- if d.Section != "" {
- b.addLayoutVariations(d.Section)
- }
- b.addSectionType()
- b.addKind()
- case "term":
- b.addKind()
- if d.Section != "" {
- b.addLayoutVariations(d.Section)
- }
- b.addLayoutVariations("taxonomy")
- b.addTypeVariations("taxonomy")
- b.addSectionType()
- case "taxonomy":
- if d.Section != "" {
- b.addLayoutVariations(d.Section + ".terms")
- }
- b.addSectionType()
- b.addLayoutVariations("terms")
- // For legacy reasons this is deliberately put last.
- b.addKind()
- case "404":
- b.addLayoutVariations("404")
- b.addTypeVariations("")
- case "robotstxt":
- b.addLayoutVariations("robots")
- b.addTypeVariations("")
- case "sitemap":
- b.addLayoutVariations("sitemap")
- b.addTypeVariations("")
- case "sitemapindex":
- b.addLayoutVariations("sitemapindex")
- b.addTypeVariations("")
- }
-
- isRSS := d.OutputFormatName == "rss"
- if !d.RenderingHook && !d.Baseof && isRSS {
- // The historic and common rss.xml case
- b.addLayoutVariations("")
- }
-
- if d.Baseof || d.Kind != "404" {
- // Most have _default in their lookup path
- b.addTypeVariations("_default")
- }
-
- if d.isList() {
- // Add the common list type
- b.addLayoutVariations("list")
- }
-
- if d.Baseof {
- b.addLayoutVariations("baseof")
- }
-
- layouts := b.resolveVariations()
-
- if !d.RenderingHook && !d.Baseof && isRSS {
- layouts = append(layouts, "_internal/_default/rss.xml")
- }
-
- switch d.Kind {
- case "robotstxt":
- layouts = append(layouts, "_internal/_default/robots.txt")
- case "sitemap":
- layouts = append(layouts, "_internal/_default/sitemap.xml")
- case "sitemapindex":
- layouts = append(layouts, "_internal/_default/sitemapindex.xml")
- }
-
- return layouts
-}
-
-func (l *layoutBuilder) resolveVariations() []string {
- var layouts []string
-
- var variations []string
- name := strings.ToLower(l.d.OutputFormatName)
-
- if l.d.Lang != "" {
- // We prefer the most specific type before language.
- variations = append(variations, []string{l.d.Lang + "." + name, name, l.d.Lang}...)
- } else {
- variations = append(variations, name)
- }
-
- variations = append(variations, "")
-
- for _, typeVar := range l.typeVariations {
- for _, variation := range variations {
- for _, layoutVar := range l.layoutVariations {
- if variation == "" && layoutVar == "" {
- continue
- }
-
- s := constructLayoutPath(typeVar, layoutVar, variation, l.d.Suffix)
- if s != "" {
- layouts = append(layouts, s)
- }
- }
- }
- }
-
- return layouts
-}
-
-// constructLayoutPath constructs a layout path given a type, layout,
-// variations, and extension. The path constructed follows the pattern of
-// type/layout.variations.extension. If any value is empty, it will be left out
-// of the path construction.
-//
-// Path construction requires at least 2 of 3 out of layout, variations, and extension.
-// If more than one of those is empty, an empty string is returned.
-func constructLayoutPath(typ, layout, variations, extension string) string {
- // we already know that layout and variations are not both empty because of
- // checks in resolveVariants().
- if extension == "" && (layout == "" || variations == "") {
- return ""
- }
-
- // Commence valid path construction...
-
- var (
- p strings.Builder
- needDot bool
- )
-
- if typ != "" {
- p.WriteString(typ)
- p.WriteString("/")
- }
-
- if layout != "" {
- p.WriteString(layout)
- needDot = true
- }
-
- if variations != "" {
- if needDot {
- p.WriteString(".")
- }
- p.WriteString(variations)
- needDot = true
- }
-
- if extension != "" {
- if needDot {
- p.WriteString(".")
- }
- p.WriteString(extension)
- }
-
- return p.String()
-}
-
-// Inline this here so we can use tinygo to compile a wasm binary of this package.
-func uniqueStringsReuse(s []string) []string {
- result := s[:0]
- for i, val := range s {
- var seen bool
-
- for j := range i {
- if s[j] == val {
- seen = true
- break
- }
- }
-
- if !seen {
- result = append(result, val)
- }
- }
- return result
-}
(DIR) diff --git a/output/layouts/layout_test.go b/output/layouts/layout_test.go
@@ -1,982 +0,0 @@
-// Copyright 2017-present 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 layouts
-
-import (
- "fmt"
- "reflect"
- "strings"
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/kylelemons/godebug/diff"
-)
-
-func TestLayout(t *testing.T) {
- c := qt.New(t)
-
- for _, this := range []struct {
- name string
- layoutDescriptor LayoutDescriptor
- layoutOverride string
- expect []string
- }{
- {
- "Home",
- LayoutDescriptor{Kind: "home", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "index.amp.html",
- "home.amp.html",
- "list.amp.html",
- "index.html",
- "home.html",
- "list.html",
- "_default/index.amp.html",
- "_default/home.amp.html",
- "_default/list.amp.html",
- "_default/index.html",
- "_default/home.html",
- "_default/list.html",
- },
- },
- {
- "Home baseof",
- LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "index-baseof.amp.html",
- "home-baseof.amp.html",
- "list-baseof.amp.html",
- "baseof.amp.html",
- "index-baseof.html",
- "home-baseof.html",
- "list-baseof.html",
- "baseof.html",
- "_default/index-baseof.amp.html",
- "_default/home-baseof.amp.html",
- "_default/list-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/index-baseof.html",
- "_default/home-baseof.html",
- "_default/list-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Home, HTML",
- LayoutDescriptor{Kind: "home", OutputFormatName: "html", Suffix: "html"},
- "",
- // We will eventually get to index.html. This looks stuttery, but makes the lookup logic easy to understand.
- []string{
- "index.html.html",
- "home.html.html",
- "list.html.html",
- "index.html",
- "home.html",
- "list.html",
- "_default/index.html.html",
- "_default/home.html.html",
- "_default/list.html.html",
- "_default/index.html",
- "_default/home.html",
- "_default/list.html",
- },
- },
- {
- "Home, HTML, baseof",
- LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "html", Suffix: "html"},
- "",
- []string{
- "index-baseof.html.html",
- "home-baseof.html.html",
- "list-baseof.html.html",
- "baseof.html.html",
- "index-baseof.html",
- "home-baseof.html",
- "list-baseof.html",
- "baseof.html",
- "_default/index-baseof.html.html",
- "_default/home-baseof.html.html",
- "_default/list-baseof.html.html",
- "_default/baseof.html.html",
- "_default/index-baseof.html",
- "_default/home-baseof.html",
- "_default/list-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Home, french language",
- LayoutDescriptor{Kind: "home", Lang: "fr", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "index.fr.amp.html",
- "home.fr.amp.html",
- "list.fr.amp.html",
- "index.amp.html",
- "home.amp.html",
- "list.amp.html",
- "index.fr.html",
- "home.fr.html",
- "list.fr.html",
- "index.html",
- "home.html",
- "list.html",
- "_default/index.fr.amp.html",
- "_default/home.fr.amp.html",
- "_default/list.fr.amp.html",
- "_default/index.amp.html",
- "_default/home.amp.html",
- "_default/list.amp.html",
- "_default/index.fr.html",
- "_default/home.fr.html",
- "_default/list.fr.html",
- "_default/index.html",
- "_default/home.html",
- "_default/list.html",
- },
- },
- {
- "Home, no ext or delim",
- LayoutDescriptor{Kind: "home", OutputFormatName: "nem", Suffix: ""},
- "",
- []string{
- "index.nem",
- "home.nem",
- "list.nem",
- "_default/index.nem",
- "_default/home.nem",
- "_default/list.nem",
- },
- },
- {
- "Home, no ext",
- LayoutDescriptor{Kind: "home", OutputFormatName: "nex", Suffix: ""},
- "",
- []string{
- "index.nex",
- "home.nex",
- "list.nex",
- "_default/index.nex",
- "_default/home.nex",
- "_default/list.nex",
- },
- },
- {
- "Page, no ext or delim",
- LayoutDescriptor{Kind: "page", OutputFormatName: "nem", Suffix: ""},
- "",
- []string{"_default/single.nem"},
- },
- {
- "Section",
- LayoutDescriptor{Kind: "section", Section: "sect1", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "sect1/sect1.amp.html",
- "sect1/section.amp.html",
- "sect1/list.amp.html",
- "sect1/sect1.html",
- "sect1/section.html",
- "sect1/list.html",
- "section/sect1.amp.html",
- "section/section.amp.html",
- "section/list.amp.html",
- "section/sect1.html",
- "section/section.html",
- "section/list.html",
- "_default/sect1.amp.html",
- "_default/section.amp.html",
- "_default/list.amp.html",
- "_default/sect1.html",
- "_default/section.html",
- "_default/list.html",
- },
- },
- {
- "Section, baseof",
- LayoutDescriptor{Kind: "section", Section: "sect1", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "sect1/sect1-baseof.amp.html",
- "sect1/section-baseof.amp.html",
- "sect1/list-baseof.amp.html",
- "sect1/baseof.amp.html",
- "sect1/sect1-baseof.html",
- "sect1/section-baseof.html",
- "sect1/list-baseof.html",
- "sect1/baseof.html",
- "section/sect1-baseof.amp.html",
- "section/section-baseof.amp.html",
- "section/list-baseof.amp.html",
- "section/baseof.amp.html",
- "section/sect1-baseof.html",
- "section/section-baseof.html",
- "section/list-baseof.html",
- "section/baseof.html",
- "_default/sect1-baseof.amp.html",
- "_default/section-baseof.amp.html",
- "_default/list-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/sect1-baseof.html",
- "_default/section-baseof.html",
- "_default/list-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Section, baseof, French, AMP",
- LayoutDescriptor{Kind: "section", Section: "sect1", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "sect1/sect1-baseof.fr.amp.html",
- "sect1/section-baseof.fr.amp.html",
- "sect1/list-baseof.fr.amp.html",
- "sect1/baseof.fr.amp.html",
- "sect1/sect1-baseof.amp.html",
- "sect1/section-baseof.amp.html",
- "sect1/list-baseof.amp.html",
- "sect1/baseof.amp.html",
- "sect1/sect1-baseof.fr.html",
- "sect1/section-baseof.fr.html",
- "sect1/list-baseof.fr.html",
- "sect1/baseof.fr.html",
- "sect1/sect1-baseof.html",
- "sect1/section-baseof.html",
- "sect1/list-baseof.html",
- "sect1/baseof.html",
- "section/sect1-baseof.fr.amp.html",
- "section/section-baseof.fr.amp.html",
- "section/list-baseof.fr.amp.html",
- "section/baseof.fr.amp.html",
- "section/sect1-baseof.amp.html",
- "section/section-baseof.amp.html",
- "section/list-baseof.amp.html",
- "section/baseof.amp.html",
- "section/sect1-baseof.fr.html",
- "section/section-baseof.fr.html",
- "section/list-baseof.fr.html",
- "section/baseof.fr.html",
- "section/sect1-baseof.html",
- "section/section-baseof.html",
- "section/list-baseof.html",
- "section/baseof.html",
- "_default/sect1-baseof.fr.amp.html",
- "_default/section-baseof.fr.amp.html",
- "_default/list-baseof.fr.amp.html",
- "_default/baseof.fr.amp.html",
- "_default/sect1-baseof.amp.html",
- "_default/section-baseof.amp.html",
- "_default/list-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/sect1-baseof.fr.html",
- "_default/section-baseof.fr.html",
- "_default/list-baseof.fr.html",
- "_default/baseof.fr.html",
- "_default/sect1-baseof.html",
- "_default/section-baseof.html",
- "_default/list-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Section with layout",
- LayoutDescriptor{Kind: "section", Section: "sect1", Layout: "mylayout", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "sect1/mylayout.amp.html",
- "sect1/sect1.amp.html",
- "sect1/section.amp.html",
- "sect1/list.amp.html",
- "sect1/mylayout.html",
- "sect1/sect1.html",
- "sect1/section.html",
- "sect1/list.html",
- "section/mylayout.amp.html",
- "section/sect1.amp.html",
- "section/section.amp.html",
- "section/list.amp.html",
- "section/mylayout.html",
- "section/sect1.html",
- "section/section.html",
- "section/list.html",
- "_default/mylayout.amp.html",
- "_default/sect1.amp.html",
- "_default/section.amp.html",
- "_default/list.amp.html",
- "_default/mylayout.html",
- "_default/sect1.html",
- "_default/section.html",
- "_default/list.html",
- },
- },
- {
- "Term, French, AMP",
- LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "term/term.fr.amp.html",
- "term/tags.fr.amp.html",
- "term/taxonomy.fr.amp.html",
- "term/list.fr.amp.html",
- "term/term.amp.html",
- "term/tags.amp.html",
- "term/taxonomy.amp.html",
- "term/list.amp.html",
- "term/term.fr.html",
- "term/tags.fr.html",
- "term/taxonomy.fr.html",
- "term/list.fr.html",
- "term/term.html",
- "term/tags.html",
- "term/taxonomy.html",
- "term/list.html",
- "taxonomy/term.fr.amp.html",
- "taxonomy/tags.fr.amp.html",
- "taxonomy/taxonomy.fr.amp.html",
- "taxonomy/list.fr.amp.html",
- "taxonomy/term.amp.html",
- "taxonomy/tags.amp.html",
- "taxonomy/taxonomy.amp.html",
- "taxonomy/list.amp.html",
- "taxonomy/term.fr.html",
- "taxonomy/tags.fr.html",
- "taxonomy/taxonomy.fr.html",
- "taxonomy/list.fr.html",
- "taxonomy/term.html",
- "taxonomy/tags.html",
- "taxonomy/taxonomy.html",
- "taxonomy/list.html",
- "tags/term.fr.amp.html",
- "tags/tags.fr.amp.html",
- "tags/taxonomy.fr.amp.html",
- "tags/list.fr.amp.html",
- "tags/term.amp.html",
- "tags/tags.amp.html",
- "tags/taxonomy.amp.html",
- "tags/list.amp.html",
- "tags/term.fr.html",
- "tags/tags.fr.html",
- "tags/taxonomy.fr.html",
- "tags/list.fr.html",
- "tags/term.html",
- "tags/tags.html",
- "tags/taxonomy.html",
- "tags/list.html",
- "_default/term.fr.amp.html",
- "_default/tags.fr.amp.html",
- "_default/taxonomy.fr.amp.html",
- "_default/list.fr.amp.html",
- "_default/term.amp.html",
- "_default/tags.amp.html",
- "_default/taxonomy.amp.html",
- "_default/list.amp.html",
- "_default/term.fr.html",
- "_default/tags.fr.html",
- "_default/taxonomy.fr.html",
- "_default/list.fr.html",
- "_default/term.html",
- "_default/tags.html",
- "_default/taxonomy.html",
- "_default/list.html",
- },
- },
- {
- "Term, baseof, French, AMP",
- LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "term/term-baseof.fr.amp.html",
- "term/tags-baseof.fr.amp.html",
- "term/taxonomy-baseof.fr.amp.html",
- "term/list-baseof.fr.amp.html",
- "term/baseof.fr.amp.html",
- "term/term-baseof.amp.html",
- "term/tags-baseof.amp.html",
- "term/taxonomy-baseof.amp.html",
- "term/list-baseof.amp.html",
- "term/baseof.amp.html",
- "term/term-baseof.fr.html",
- "term/tags-baseof.fr.html",
- "term/taxonomy-baseof.fr.html",
- "term/list-baseof.fr.html",
- "term/baseof.fr.html",
- "term/term-baseof.html",
- "term/tags-baseof.html",
- "term/taxonomy-baseof.html",
- "term/list-baseof.html",
- "term/baseof.html",
- "taxonomy/term-baseof.fr.amp.html",
- "taxonomy/tags-baseof.fr.amp.html",
- "taxonomy/taxonomy-baseof.fr.amp.html",
- "taxonomy/list-baseof.fr.amp.html",
- "taxonomy/baseof.fr.amp.html",
- "taxonomy/term-baseof.amp.html",
- "taxonomy/tags-baseof.amp.html",
- "taxonomy/taxonomy-baseof.amp.html",
- "taxonomy/list-baseof.amp.html",
- "taxonomy/baseof.amp.html",
- "taxonomy/term-baseof.fr.html",
- "taxonomy/tags-baseof.fr.html",
- "taxonomy/taxonomy-baseof.fr.html",
- "taxonomy/list-baseof.fr.html",
- "taxonomy/baseof.fr.html",
- "taxonomy/term-baseof.html",
- "taxonomy/tags-baseof.html",
- "taxonomy/taxonomy-baseof.html",
- "taxonomy/list-baseof.html",
- "taxonomy/baseof.html",
- "tags/term-baseof.fr.amp.html",
- "tags/tags-baseof.fr.amp.html",
- "tags/taxonomy-baseof.fr.amp.html",
- "tags/list-baseof.fr.amp.html",
- "tags/baseof.fr.amp.html",
- "tags/term-baseof.amp.html",
- "tags/tags-baseof.amp.html",
- "tags/taxonomy-baseof.amp.html",
- "tags/list-baseof.amp.html",
- "tags/baseof.amp.html",
- "tags/term-baseof.fr.html",
- "tags/tags-baseof.fr.html",
- "tags/taxonomy-baseof.fr.html",
- "tags/list-baseof.fr.html",
- "tags/baseof.fr.html",
- "tags/term-baseof.html",
- "tags/tags-baseof.html",
- "tags/taxonomy-baseof.html",
- "tags/list-baseof.html",
- "tags/baseof.html",
- "_default/term-baseof.fr.amp.html",
- "_default/tags-baseof.fr.amp.html",
- "_default/taxonomy-baseof.fr.amp.html",
- "_default/list-baseof.fr.amp.html",
- "_default/baseof.fr.amp.html",
- "_default/term-baseof.amp.html",
- "_default/tags-baseof.amp.html",
- "_default/taxonomy-baseof.amp.html",
- "_default/list-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/term-baseof.fr.html",
- "_default/tags-baseof.fr.html",
- "_default/taxonomy-baseof.fr.html",
- "_default/list-baseof.fr.html",
- "_default/baseof.fr.html",
- "_default/term-baseof.html",
- "_default/tags-baseof.html",
- "_default/taxonomy-baseof.html",
- "_default/list-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Term",
- LayoutDescriptor{Kind: "term", Section: "tags", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "term/term.amp.html",
- "term/tags.amp.html",
- "term/taxonomy.amp.html",
- "term/list.amp.html",
- "term/term.html",
- "term/tags.html",
- "term/taxonomy.html",
- "term/list.html",
- "taxonomy/term.amp.html",
- "taxonomy/tags.amp.html",
- "taxonomy/taxonomy.amp.html",
- "taxonomy/list.amp.html",
- "taxonomy/term.html",
- "taxonomy/tags.html",
- "taxonomy/taxonomy.html",
- "taxonomy/list.html",
- "tags/term.amp.html",
- "tags/tags.amp.html",
- "tags/taxonomy.amp.html",
- "tags/list.amp.html",
- "tags/term.html",
- "tags/tags.html",
- "tags/taxonomy.html",
- "tags/list.html",
- "_default/term.amp.html",
- "_default/tags.amp.html",
- "_default/taxonomy.amp.html",
- "_default/list.amp.html",
- "_default/term.html",
- "_default/tags.html",
- "_default/taxonomy.html",
- "_default/list.html",
- },
- },
- {
- "Taxonomy",
- LayoutDescriptor{Kind: "taxonomy", Section: "categories", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "categories/categories.terms.amp.html",
- "categories/terms.amp.html",
- "categories/taxonomy.amp.html",
- "categories/list.amp.html",
- "categories/categories.terms.html",
- "categories/terms.html",
- "categories/taxonomy.html",
- "categories/list.html",
- "taxonomy/categories.terms.amp.html",
- "taxonomy/terms.amp.html",
- "taxonomy/taxonomy.amp.html",
- "taxonomy/list.amp.html",
- "taxonomy/categories.terms.html",
- "taxonomy/terms.html",
- "taxonomy/taxonomy.html",
- "taxonomy/list.html",
- "_default/categories.terms.amp.html",
- "_default/terms.amp.html",
- "_default/taxonomy.amp.html",
- "_default/list.amp.html",
- "_default/categories.terms.html",
- "_default/terms.html",
- "_default/taxonomy.html",
- "_default/list.html",
- },
- },
- {
- "Page",
- LayoutDescriptor{Kind: "page", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "_default/single.amp.html",
- "_default/single.html",
- },
- },
- {
- "Page, baseof",
- LayoutDescriptor{Kind: "page", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "_default/single-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/single-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Page with layout",
- LayoutDescriptor{Kind: "page", Layout: "mylayout", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "_default/mylayout.amp.html",
- "_default/single.amp.html",
- "_default/mylayout.html",
- "_default/single.html",
- },
- },
- {
- "Page with layout, baseof",
- LayoutDescriptor{Kind: "page", Layout: "mylayout", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "_default/mylayout-baseof.amp.html",
- "_default/single-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/mylayout-baseof.html",
- "_default/single-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Page with layout and type",
- LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "myttype/mylayout.amp.html",
- "myttype/single.amp.html",
- "myttype/mylayout.html",
- "myttype/single.html",
- "_default/mylayout.amp.html",
- "_default/single.amp.html",
- "_default/mylayout.html",
- "_default/single.html",
- },
- },
- {
- "Page baseof with layout and type",
- LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "myttype/mylayout-baseof.amp.html",
- "myttype/single-baseof.amp.html",
- "myttype/baseof.amp.html",
- "myttype/mylayout-baseof.html",
- "myttype/single-baseof.html",
- "myttype/baseof.html",
- "_default/mylayout-baseof.amp.html",
- "_default/single-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/mylayout-baseof.html",
- "_default/single-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Page baseof with layout and type in French",
- LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "myttype/mylayout-baseof.fr.amp.html",
- "myttype/single-baseof.fr.amp.html",
- "myttype/baseof.fr.amp.html",
- "myttype/mylayout-baseof.amp.html",
- "myttype/single-baseof.amp.html",
- "myttype/baseof.amp.html",
- "myttype/mylayout-baseof.fr.html",
- "myttype/single-baseof.fr.html",
- "myttype/baseof.fr.html",
- "myttype/mylayout-baseof.html",
- "myttype/single-baseof.html",
- "myttype/baseof.html",
- "_default/mylayout-baseof.fr.amp.html",
- "_default/single-baseof.fr.amp.html",
- "_default/baseof.fr.amp.html",
- "_default/mylayout-baseof.amp.html",
- "_default/single-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/mylayout-baseof.fr.html",
- "_default/single-baseof.fr.html",
- "_default/baseof.fr.html",
- "_default/mylayout-baseof.html",
- "_default/single-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Page with layout and type with subtype",
- LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype/mysubtype", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "myttype/mysubtype/mylayout.amp.html",
- "myttype/mysubtype/single.amp.html",
- "myttype/mysubtype/mylayout.html",
- "myttype/mysubtype/single.html",
- "_default/mylayout.amp.html",
- "_default/single.amp.html",
- "_default/mylayout.html",
- "_default/single.html",
- },
- },
- // RSS
- {
- "RSS Home",
- LayoutDescriptor{Kind: "home", OutputFormatName: "rss", Suffix: "xml"},
- "",
- []string{
- "index.rss.xml",
- "home.rss.xml",
- "rss.xml",
- "list.rss.xml",
- "index.xml",
- "home.xml",
- "list.xml",
- "_default/index.rss.xml",
- "_default/home.rss.xml",
- "_default/rss.xml",
- "_default/list.rss.xml",
- "_default/index.xml",
- "_default/home.xml",
- "_default/list.xml",
- "_internal/_default/rss.xml",
- },
- },
- {
- "RSS Home, baseof",
- LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "rss", Suffix: "xml"},
- "",
- []string{
- "index-baseof.rss.xml",
- "home-baseof.rss.xml",
- "list-baseof.rss.xml",
- "baseof.rss.xml",
- "index-baseof.xml",
- "home-baseof.xml",
- "list-baseof.xml",
- "baseof.xml",
- "_default/index-baseof.rss.xml",
- "_default/home-baseof.rss.xml",
- "_default/list-baseof.rss.xml",
- "_default/baseof.rss.xml",
- "_default/index-baseof.xml",
- "_default/home-baseof.xml",
- "_default/list-baseof.xml",
- "_default/baseof.xml",
- },
- },
- {
- "RSS Section",
- LayoutDescriptor{Kind: "section", Section: "sect1", OutputFormatName: "rss", Suffix: "xml"},
- "",
- []string{
- "sect1/sect1.rss.xml",
- "sect1/section.rss.xml",
- "sect1/rss.xml",
- "sect1/list.rss.xml",
- "sect1/sect1.xml",
- "sect1/section.xml",
- "sect1/list.xml",
- "section/sect1.rss.xml",
- "section/section.rss.xml",
- "section/rss.xml",
- "section/list.rss.xml",
- "section/sect1.xml",
- "section/section.xml",
- "section/list.xml",
- "_default/sect1.rss.xml",
- "_default/section.rss.xml",
- "_default/rss.xml",
- "_default/list.rss.xml",
- "_default/sect1.xml",
- "_default/section.xml",
- "_default/list.xml",
- "_internal/_default/rss.xml",
- },
- },
- {
- "RSS Term",
- LayoutDescriptor{Kind: "term", Section: "tag", OutputFormatName: "rss", Suffix: "xml"},
- "",
- []string{
- "term/term.rss.xml",
- "term/tag.rss.xml",
- "term/taxonomy.rss.xml",
- "term/rss.xml",
- "term/list.rss.xml",
- "term/term.xml",
- "term/tag.xml",
- "term/taxonomy.xml",
- "term/list.xml",
- "taxonomy/term.rss.xml",
- "taxonomy/tag.rss.xml",
- "taxonomy/taxonomy.rss.xml",
- "taxonomy/rss.xml",
- "taxonomy/list.rss.xml",
- "taxonomy/term.xml",
- "taxonomy/tag.xml",
- "taxonomy/taxonomy.xml",
- "taxonomy/list.xml",
- "tag/term.rss.xml",
- "tag/tag.rss.xml",
- "tag/taxonomy.rss.xml",
- "tag/rss.xml",
- "tag/list.rss.xml",
- "tag/term.xml",
- "tag/tag.xml",
- "tag/taxonomy.xml",
- "tag/list.xml",
- "_default/term.rss.xml",
- "_default/tag.rss.xml",
- "_default/taxonomy.rss.xml",
- "_default/rss.xml",
- "_default/list.rss.xml",
- "_default/term.xml",
- "_default/tag.xml",
- "_default/taxonomy.xml",
- "_default/list.xml",
- "_internal/_default/rss.xml",
- },
- },
- {
- "RSS Taxonomy",
- LayoutDescriptor{Kind: "taxonomy", Section: "tag", OutputFormatName: "rss", Suffix: "xml"},
- "",
- []string{
- "tag/tag.terms.rss.xml",
- "tag/terms.rss.xml",
- "tag/taxonomy.rss.xml",
- "tag/rss.xml",
- "tag/list.rss.xml",
- "tag/tag.terms.xml",
- "tag/terms.xml",
- "tag/taxonomy.xml",
- "tag/list.xml",
- "taxonomy/tag.terms.rss.xml",
- "taxonomy/terms.rss.xml",
- "taxonomy/taxonomy.rss.xml",
- "taxonomy/rss.xml",
- "taxonomy/list.rss.xml",
- "taxonomy/tag.terms.xml",
- "taxonomy/terms.xml",
- "taxonomy/taxonomy.xml",
- "taxonomy/list.xml",
- "_default/tag.terms.rss.xml",
- "_default/terms.rss.xml",
- "_default/taxonomy.rss.xml",
- "_default/rss.xml",
- "_default/list.rss.xml",
- "_default/tag.terms.xml",
- "_default/terms.xml",
- "_default/taxonomy.xml",
- "_default/list.xml",
- "_internal/_default/rss.xml",
- },
- },
- {
- "Home plain text",
- LayoutDescriptor{Kind: "home", OutputFormatName: "json", Suffix: "json"},
- "",
- []string{
- "index.json.json",
- "home.json.json",
- "list.json.json",
- "index.json",
- "home.json",
- "list.json",
- "_default/index.json.json",
- "_default/home.json.json",
- "_default/list.json.json",
- "_default/index.json",
- "_default/home.json",
- "_default/list.json",
- },
- },
- {
- "Page plain text",
- LayoutDescriptor{Kind: "page", OutputFormatName: "json", Suffix: "json"},
- "",
- []string{
- "_default/single.json.json",
- "_default/single.json",
- },
- },
- {
- "Reserved section, shortcodes",
- LayoutDescriptor{Kind: "section", Section: "shortcodes", Type: "shortcodes", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "section/shortcodes.amp.html",
- "section/section.amp.html",
- "section/list.amp.html",
- "section/shortcodes.html",
- "section/section.html",
- "section/list.html",
- "_default/shortcodes.amp.html",
- "_default/section.amp.html",
- "_default/list.amp.html",
- "_default/shortcodes.html",
- "_default/section.html",
- "_default/list.html",
- },
- },
- {
- "Reserved section, partials",
- LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "section/partials.amp.html",
- "section/section.amp.html",
- "section/list.amp.html",
- "section/partials.html",
- "section/section.html",
- "section/list.html",
- "_default/partials.amp.html",
- "_default/section.amp.html",
- "_default/list.amp.html",
- "_default/partials.html",
- "_default/section.html",
- "_default/list.html",
- },
- },
- // This is currently always HTML only
- {
- "404, HTML",
- LayoutDescriptor{Kind: "404", OutputFormatName: "html", Suffix: "html"},
- "",
- []string{
- "404.html.html",
- "404.html",
- },
- },
- {
- "404, HTML baseof",
- LayoutDescriptor{Kind: "404", Baseof: true, OutputFormatName: "html", Suffix: "html"},
- "",
- []string{
- "404-baseof.html.html",
- "baseof.html.html",
- "404-baseof.html",
- "baseof.html",
- "_default/404-baseof.html.html",
- "_default/baseof.html.html",
- "_default/404-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Content hook",
- LayoutDescriptor{Kind: "render-link", RenderingHook: true, Layout: "mylayout", Section: "blog", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "blog/_markup/render-link.amp.html",
- "blog/_markup/render-link.html",
- "_default/_markup/render-link.amp.html",
- "_default/_markup/render-link.html",
- },
- },
- } {
- c.Run(this.name, func(c *qt.C) {
- l := NewLayoutHandler()
-
- layouts, err := l.For(this.layoutDescriptor)
-
- c.Assert(err, qt.IsNil)
- c.Assert(layouts, qt.Not(qt.IsNil), qt.Commentf(this.layoutDescriptor.Kind))
-
- if !reflect.DeepEqual(layouts, this.expect) {
- r := strings.NewReplacer(
- "[", "\t\"",
- "]", "\",",
- " ", "\",\n\t\"",
- )
- fmtGot := r.Replace(fmt.Sprintf("%v", layouts))
- fmtExp := r.Replace(fmt.Sprintf("%v", this.expect))
-
- c.Fatalf("got %d items, expected %d:\nGot:\n\t%v\nExpected:\n\t%v\nDiff:\n%s", len(layouts), len(this.expect), layouts, this.expect, diff.Diff(fmtExp, fmtGot))
-
- }
- })
- }
-}
-
-/*
-func BenchmarkLayout(b *testing.B) {
- descriptor := LayoutDescriptor{Kind: "taxonomy", Section: "categories"}
- l := NewLayoutHandler()
-
- for i := 0; i < b.N; i++ {
- _, err := l.For(descriptor, HTMLFormat)
- if err != nil {
- panic(err)
- }
- }
-}
-
-func BenchmarkLayoutUncached(b *testing.B) {
- for i := 0; i < b.N; i++ {
- descriptor := LayoutDescriptor{Kind: "taxonomy", Section: "categories"}
- l := NewLayoutHandler()
-
- _, err := l.For(descriptor, HTMLFormat)
- if err != nil {
- panic(err)
- }
- }
-}
-*/
(DIR) diff --git a/output/outputFormat.go b/output/outputFormat.go
@@ -133,6 +133,15 @@ var (
Weight: 10,
}
+ // Alias is the output format used for alias redirects.
+ AliasHTMLFormat = Format{
+ Name: "alias",
+ MediaType: media.Builtin.HTMLType,
+ IsHTML: true,
+ Ugly: true,
+ Permalinkable: false,
+ }
+
MarkdownFormat = Format{
Name: "markdown",
MediaType: media.Builtin.MarkdownType,
@@ -192,8 +201,17 @@ var (
Rel: "sitemap",
}
- HTTPStatusHTMLFormat = Format{
- Name: "httpstatus",
+ GotmplFormat = Format{
+ Name: "gotmpl",
+ MediaType: media.Builtin.GotmplType,
+ IsPlainText: true,
+ NotAlternative: true,
+ }
+
+ // I'm not sure having a 404 format is a good idea,
+ // for one, we would want to have multiple formats for this.
+ HTTPStatus404HTMLFormat = Format{
+ Name: "404",
MediaType: media.Builtin.HTMLType,
NotAlternative: true,
Ugly: true,
@@ -209,12 +227,16 @@ var DefaultFormats = Formats{
CSSFormat,
CSVFormat,
HTMLFormat,
+ GotmplFormat,
+ HTTPStatus404HTMLFormat,
+ AliasHTMLFormat,
JSONFormat,
MarkdownFormat,
WebAppManifestFormat,
RobotsTxtFormat,
RSSFormat,
SitemapFormat,
+ SitemapIndexFormat,
}
func init() {
(DIR) diff --git a/output/outputFormat_test.go b/output/outputFormat_test.go
@@ -68,7 +68,7 @@ func TestDefaultTypes(t *testing.T) {
c.Assert(RSSFormat.NoUgly, qt.Equals, true)
c.Assert(CalendarFormat.IsHTML, qt.Equals, false)
- c.Assert(len(DefaultFormats), qt.Equals, 11)
+ c.Assert(len(DefaultFormats), qt.Equals, 15)
}
func TestGetFormatByName(t *testing.T) {
@@ -140,7 +140,7 @@ func TestGetFormatByFilename(t *testing.T) {
func TestSort(t *testing.T) {
c := qt.New(t)
c.Assert(DefaultFormats[0].Name, qt.Equals, "html")
- c.Assert(DefaultFormats[1].Name, qt.Equals, "amp")
+ c.Assert(DefaultFormats[1].Name, qt.Equals, "404")
json := JSONFormat
json.Weight = 1
(DIR) diff --git a/resources/kinds/kinds.go b/resources/kinds/kinds.go
@@ -34,6 +34,7 @@ const (
// The following are (currently) temporary nodes,
// i.e. nodes we create just to render in isolation.
+ KindTemporary = "temporary"
KindRSS = "rss"
KindSitemap = "sitemap"
KindSitemapIndex = "sitemapindex"
(DIR) diff --git a/resources/page/page.go b/resources/page/page.go
@@ -150,8 +150,8 @@ type InSectionPositioner interface {
// InternalDependencies is considered an internal interface.
type InternalDependencies interface {
- // GetRelatedDocsHandler is for internal use only.
- GetRelatedDocsHandler() *RelatedDocsHandler
+ // GetInternalRelatedDocsHandler is for internal use only.
+ GetInternalRelatedDocsHandler() *RelatedDocsHandler
}
// OutputFormatsProvider provides the OutputFormats of a Page.
(DIR) diff --git a/resources/page/page_paths.go b/resources/page/page_paths.go
@@ -145,7 +145,7 @@ func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) {
pb.isUgly = true
}
- if d.Type == output.HTTPStatusHTMLFormat || d.Type == output.SitemapFormat || d.Type == output.RobotsTxtFormat {
+ if d.Type == output.HTTPStatus404HTMLFormat || d.Type == output.SitemapFormat || d.Type == output.RobotsTxtFormat {
pb.noSubResources = true
} else if d.Kind != kinds.KindPage && d.URL == "" && d.Section.Base() != "/" {
if d.ExpandedPermalink != "" {
(DIR) diff --git a/resources/page/pages_related.go b/resources/page/pages_related.go
@@ -129,7 +129,7 @@ func (p Pages) withInvertedIndex(ctx context.Context, search func(idx *related.I
return nil, fmt.Errorf("invalid type %T in related search", p[0])
}
- cache := d.GetRelatedDocsHandler()
+ cache := d.GetInternalRelatedDocsHandler()
searchIndex, err := cache.getOrCreateIndex(ctx, p)
if err != nil {
(DIR) diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go
@@ -221,7 +221,7 @@ func (p *testPage) GetTerms(taxonomy string) Pages {
panic("testpage: not implemented")
}
-func (p *testPage) GetRelatedDocsHandler() *RelatedDocsHandler {
+func (p *testPage) GetInternalRelatedDocsHandler() *RelatedDocsHandler {
return relatedDocsHandler
}
(DIR) diff --git a/resources/resource_spec.go b/resources/resource_spec.go
@@ -42,7 +42,6 @@ import (
"github.com/gohugoio/hugo/resources/images"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
- "github.com/gohugoio/hugo/tpl"
)
func NewSpec(
@@ -123,8 +122,6 @@ type Spec struct {
BuildClosers types.CloseAdder
Rebuilder identity.SignalRebuilder
- TextTemplates tpl.TemplateParseFinder
-
Permalinks page.PermalinkExpander
ImageCache *ImageCache
(DIR) diff --git a/resources/resource_transformers/templates/execute_as_template.go b/resources/resource_transformers/templates/execute_as_template.go
@@ -23,17 +23,17 @@ import (
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/internal"
"github.com/gohugoio/hugo/resources/resource"
- "github.com/gohugoio/hugo/tpl"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
)
// Client contains methods to perform template processing of Resource objects.
type Client struct {
rs *resources.Spec
- t tpl.TemplatesProvider
+ t tplimpl.TemplateStoreProvider
}
// New creates a new Client with the given specification.
-func New(rs *resources.Spec, t tpl.TemplatesProvider) *Client {
+func New(rs *resources.Spec, t tplimpl.TemplateStoreProvider) *Client {
if rs == nil {
panic("must provide a resource Spec")
}
@@ -45,7 +45,7 @@ func New(rs *resources.Spec, t tpl.TemplatesProvider) *Client {
type executeAsTemplateTransform struct {
rs *resources.Spec
- t tpl.TemplatesProvider
+ t tplimpl.TemplateStoreProvider
targetPath string
data any
}
@@ -56,14 +56,13 @@ func (t *executeAsTemplateTransform) Key() internal.ResourceTransformationKey {
func (t *executeAsTemplateTransform) Transform(ctx *resources.ResourceTransformationCtx) error {
tplStr := helpers.ReaderToString(ctx.From)
- templ, err := t.t.TextTmpl().Parse(ctx.InPath, tplStr)
+ th := t.t.GetTemplateStore()
+ ti, err := th.TextParse(ctx.InPath, tplStr)
if err != nil {
return fmt.Errorf("failed to parse Resource %q as Template:: %w", ctx.InPath, err)
}
-
ctx.OutPath = t.targetPath
-
- return t.t.Tmpl().ExecuteWithContext(ctx.Ctx, templ, ctx.To, t.data)
+ return th.ExecuteWithContext(ctx.Ctx, ti, ctx.To, t.data)
}
func (c *Client) ExecuteAsTemplate(ctx context.Context, res resources.ResourceTransformer, targetPath string, data any) (resource.Resource, error) {
(DIR) diff --git a/testscripts/commands/hugo_printunusedtemplates.txt b/testscripts/commands/hugo_printunusedtemplates.txt
@@ -1,6 +1,6 @@
hugo --printUnusedTemplates
-stderr 'Template _default/list.html is unused'
+stderr 'Template /list.html is unused'
-- hugo.toml --
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "404", "section", "page"]
(DIR) diff --git a/tpl/collections/apply.go b/tpl/collections/apply.go
@@ -21,7 +21,6 @@ import (
"strings"
"github.com/gohugoio/hugo/common/hreflect"
- "github.com/gohugoio/hugo/tpl"
)
// Apply takes an array or slice c and returns a new slice with the function fname applied over it.
@@ -109,8 +108,7 @@ func applyFnToThis(ctx context.Context, fn, this reflect.Value, args ...any) (re
func (ns *Namespace) lookupFunc(ctx context.Context, fname string) (reflect.Value, bool) {
namespace, methodName, ok := strings.Cut(fname, ".")
if !ok {
- templ := ns.deps.Tmpl().(tpl.TemplateFuncGetter)
- return templ.GetFunc(fname)
+ return ns.deps.GetTemplateStore().GetFunc(fname)
}
// Namespace
(DIR) diff --git a/tpl/collections/apply_test.go b/tpl/collections/apply_test.go
@@ -1,104 +0,0 @@
-// Copyright 2019 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 collections
-
-import (
- "context"
- "fmt"
- "io"
- "reflect"
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/config/testconfig"
- "github.com/gohugoio/hugo/identity"
- "github.com/gohugoio/hugo/output"
- "github.com/gohugoio/hugo/output/layouts"
- "github.com/gohugoio/hugo/tpl"
-)
-
-type templateFinder int
-
-func (templateFinder) GetIdentity(string) (identity.Identity, bool) {
- return identity.StringIdentity("test"), true
-}
-
-func (templateFinder) Lookup(name string) (tpl.Template, bool) {
- return nil, false
-}
-
-func (templateFinder) HasTemplate(name string) bool {
- return false
-}
-
-func (templateFinder) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
- return nil, false, false
-}
-
-func (templateFinder) LookupVariants(name string) []tpl.Template {
- return nil
-}
-
-func (templateFinder) LookupLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
- return nil, false, nil
-}
-
-func (templateFinder) Execute(t tpl.Template, wr io.Writer, data any) error {
- return nil
-}
-
-func (templateFinder) ExecuteWithContext(ctx context.Context, t tpl.Template, wr io.Writer, data any) error {
- return nil
-}
-
-func (templateFinder) GetFunc(name string) (reflect.Value, bool) {
- if name == "dobedobedo" {
- return reflect.Value{}, false
- }
-
- return reflect.ValueOf(fmt.Sprint), true
-}
-
-func TestApply(t *testing.T) {
- t.Parallel()
- c := qt.New(t)
- d := testconfig.GetTestDeps(nil, nil)
- d.SetTempl(&tpl.TemplateHandlers{
- Tmpl: new(templateFinder),
- })
- ns := New(d)
-
- strings := []any{"a\n", "b\n"}
-
- ctx := context.Background()
-
- result, err := ns.Apply(ctx, strings, "print", "a", "b", "c")
- c.Assert(err, qt.IsNil)
- c.Assert(result, qt.DeepEquals, []any{"abc", "abc"})
-
- _, err = ns.Apply(ctx, strings, "apply", ".")
- c.Assert(err, qt.Not(qt.IsNil))
-
- var nilErr *error
- _, err = ns.Apply(ctx, nilErr, "chomp", ".")
- c.Assert(err, qt.Not(qt.IsNil))
-
- _, err = ns.Apply(ctx, strings, "dobedobedo", ".")
- c.Assert(err, qt.Not(qt.IsNil))
-
- _, err = ns.Apply(ctx, strings, "foo.Chomp", "c\n")
- if err == nil {
- t.Errorf("apply with unknown func should fail")
- }
-}
(DIR) diff --git a/tpl/internal/go_templates/htmltemplate/hugo_template.go b/tpl/internal/go_templates/htmltemplate/hugo_template.go
@@ -14,6 +14,8 @@
package template
import (
+ "fmt"
+
"github.com/gohugoio/hugo/common/types"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
)
@@ -51,3 +53,28 @@ func indirect(a any) any {
return in
}
+
+// CloneShallow creates a shallow copy of the template. It does not clone or copy the nested templates.
+func (t *Template) CloneShallow() (*Template, error) {
+ t.nameSpace.mu.Lock()
+ defer t.nameSpace.mu.Unlock()
+ if t.escapeErr != nil {
+ return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
+ }
+ textClone, err := t.text.Clone()
+ if err != nil {
+ return nil, err
+ }
+ ns := &nameSpace{set: make(map[string]*Template)}
+ ns.esc = makeEscaper(ns)
+ ret := &Template{
+ nil,
+ textClone,
+ textClone.Tree,
+ ns,
+ }
+ ret.set[ret.Name()] = ret
+
+ // Return the template associated with the name of this template.
+ return ret.set[ret.Name()], nil
+}
(DIR) diff --git a/tpl/internal/go_templates/htmltemplate/template.go b/tpl/internal/go_templates/htmltemplate/template.go
@@ -267,7 +267,7 @@ func (t *Template) Clone() (*Template, error) {
name := x.Name()
src := t.set[name]
if src == nil || src.escapeErr != nil {
- return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
+ return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed, %q not found", t.Name(), name)
}
x.Tree = x.Tree.Copy()
ret.set[name] = &Template{
(DIR) diff --git a/tpl/internal/go_templates/texttemplate/example_test.go b/tpl/internal/go_templates/texttemplate/example_test.go
@@ -35,7 +35,7 @@ Josie
Name, Gift string
Attended bool
}
- var recipients = []Recipient{
+ recipients := []Recipient{
{"Aunt Mildred", "bone china tea set", true},
{"Uncle John", "moleskin pants", false},
{"Cousin Rodney", "", false},
(DIR) diff --git a/tpl/math/init.go b/tpl/math/init.go
@@ -24,7 +24,7 @@ const name = "math"
func init() {
f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
- ctx := New()
+ ctx := New(d)
ns := &internal.TemplateFuncsNamespace{
Name: name,
(DIR) diff --git a/tpl/math/math.go b/tpl/math/math.go
@@ -20,9 +20,9 @@ import (
"math"
"math/rand"
"reflect"
- "sync/atomic"
_math "github.com/gohugoio/hugo/common/math"
+ "github.com/gohugoio/hugo/deps"
"github.com/spf13/cast"
)
@@ -32,12 +32,16 @@ var (
)
// New returns a new instance of the math-namespaced template functions.
-func New() *Namespace {
- return &Namespace{}
+func New(d *deps.Deps) *Namespace {
+ return &Namespace{
+ d: d,
+ }
}
// Namespace provides template functions for the "math" namespace.
-type Namespace struct{}
+type Namespace struct {
+ d *deps.Deps
+}
// Abs returns the absolute value of n.
func (ns *Namespace) Abs(n any) (float64, error) {
@@ -345,8 +349,6 @@ func (ns *Namespace) doArithmetic(inputs []any, operation rune) (value any, err
return
}
-var counter uint64
-
// Counter increments and returns a global counter.
// This was originally added to be used in tests where now.UnixNano did not
// have the needed precision (especially on Windows).
@@ -354,5 +356,5 @@ var counter uint64
// and the counter will reset on new builds.
// <docsmeta>{"identifiers": ["now.UnixNano"] }</docsmeta>
func (ns *Namespace) Counter() uint64 {
- return atomic.AddUint64(&counter, uint64(1))
+ return ns.d.Counters.MathCounter.Add(1)
}
(DIR) diff --git a/tpl/math/math_test.go b/tpl/math/math_test.go
@@ -24,7 +24,7 @@ func TestBasicNSArithmetic(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
type TestCase struct {
fn func(inputs ...any) (any, error)
@@ -66,7 +66,7 @@ func TestBasicNSArithmetic(t *testing.T) {
func TestAbs(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
@@ -93,7 +93,7 @@ func TestAbs(t *testing.T) {
func TestCeil(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
@@ -126,7 +126,7 @@ func TestFloor(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
@@ -159,7 +159,7 @@ func TestLog(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
@@ -200,7 +200,7 @@ func TestSqrt(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
@@ -239,7 +239,7 @@ func TestMod(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
@@ -279,7 +279,7 @@ func TestModBool(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
@@ -325,7 +325,7 @@ func TestRound(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
@@ -358,7 +358,7 @@ func TestPow(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
@@ -398,7 +398,7 @@ func TestMax(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
type TestCase struct {
values []any
@@ -452,7 +452,7 @@ func TestMin(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
type TestCase struct {
values []any
@@ -507,7 +507,7 @@ func TestSum(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
mustSum := func(values ...any) any {
result, err := ns.Sum(values...)
@@ -530,7 +530,7 @@ func TestProduct(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
mustProduct := func(values ...any) any {
result, err := ns.Product(values...)
@@ -554,7 +554,7 @@ func TestPi(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
expect := 3.1415
result := ns.Pi()
@@ -570,7 +570,7 @@ func TestSin(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
@@ -604,7 +604,7 @@ func TestCos(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
@@ -638,7 +638,7 @@ func TestTan(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
@@ -680,7 +680,7 @@ func TestTan(t *testing.T) {
func TestAsin(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
@@ -715,7 +715,7 @@ func TestAsin(t *testing.T) {
func TestAcos(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
@@ -751,7 +751,7 @@ func TestAcos(t *testing.T) {
func TestAtan(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
@@ -782,7 +782,7 @@ func TestAtan(t *testing.T) {
func TestAtan2(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
@@ -821,7 +821,7 @@ func TestAtan2(t *testing.T) {
func TestToDegrees(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
@@ -852,7 +852,7 @@ func TestToDegrees(t *testing.T) {
func TestToRadians(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
(DIR) diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go
@@ -25,12 +25,12 @@ import (
"github.com/bep/lazycache"
+ "github.com/gohugoio/hugo/common/constants"
"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/identity"
- texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-
"github.com/gohugoio/hugo/tpl"
+ texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/deps"
@@ -54,13 +54,6 @@ func (k partialCacheKey) Key() string {
return hashing.HashString(append([]any{k.Name}, k.Variants...)...)
}
-func (k partialCacheKey) templateName() string {
- if !strings.HasPrefix(k.Name, "partials/") {
- return "partials/" + k.Name
- }
- return k.Name
-}
-
// partialCache represents a LRU cache of partials.
type partialCache struct {
cache *lazycache.Cache[string, includeResult]
@@ -129,6 +122,11 @@ func (ns *Namespace) Include(ctx context.Context, name string, contextList ...an
}
func (ns *Namespace) includWithTimeout(ctx context.Context, name string, dataList ...any) includeResult {
+ if strings.HasPrefix(name, "partials/") {
+ // This is most likely not what the user intended.
+ // This worked before Hugo 0.146.0.
+ ns.deps.Log.Warnidf(constants.WarnPartialSuperfluousPrefix, "Partial name %q starting with 'partials/' (as in {{ partial \"%s\"}}) is most likely not what you want. Before 0.146.0 we did a double lookup in this situation.", name, name)
+ }
// Create a new context with a timeout not connected to the incoming context.
timeoutCtx, cancel := context.WithTimeout(context.Background(), ns.deps.Conf.Timeout())
defer cancel()
@@ -159,28 +157,14 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
if len(dataList) > 0 {
data = dataList[0]
}
-
- var n string
- if strings.HasPrefix(name, "partials/") {
- n = name
- } else {
- n = "partials/" + name
- }
-
- templ, found := ns.deps.Tmpl().Lookup(n)
- if !found {
- // For legacy reasons.
- templ, found = ns.deps.Tmpl().Lookup(n + ".html")
- }
-
- if !found {
+ name, desc := ns.deps.TemplateStore.TemplateDescriptorFromPath(name)
+ v := ns.deps.TemplateStore.LookupPartial(name, desc)
+ if v == nil {
return includeResult{err: fmt.Errorf("partial %q not found", name)}
}
- var info tpl.ParseInfo
- if ip, ok := templ.(tpl.Info); ok {
- info = ip.ParseInfo()
- }
+ templ := v
+ info := v.ParseInfo
var w io.Writer
@@ -200,7 +184,7 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
w = b
}
- if err := ns.deps.Tmpl().ExecuteWithContext(ctx, templ, w, data); err != nil {
+ if err := ns.deps.GetTemplateStore().ExecuteWithContext(ctx, templ, w, data); err != nil {
return includeResult{err: err}
}
@@ -208,14 +192,14 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
if ctx, ok := data.(*contextWrapper); ok {
result = ctx.Result
- } else if _, ok := templ.(*texttemplate.Template); ok {
+ } else if _, ok := templ.Template.(*texttemplate.Template); ok {
result = w.(fmt.Stringer).String()
} else {
result = template.HTML(w.(fmt.Stringer).String())
}
return includeResult{
- name: templ.Name(),
+ name: templ.Template.Name(),
result: result,
}
}
@@ -253,9 +237,9 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
// The templates that gets executed is measured in Execute.
// We need to track the time spent in the cache to
// get the totals correct.
- ns.deps.Metrics.MeasureSince(key.templateName(), start)
+ ns.deps.Metrics.MeasureSince(r.name, start)
}
- ns.deps.Metrics.TrackValue(key.templateName(), r.result, found)
+ ns.deps.Metrics.TrackValue(r.name, r.result, found)
}
if r.mangager != nil && depsManagerIn != nil {
(DIR) diff --git a/tpl/partials/partials_integration_test.go b/tpl/partials/partials_integration_test.go
@@ -170,7 +170,7 @@ D1
got := buf.String()
// Get rid of all the durations, they are never the same.
- durationRe := regexp.MustCompile(`\b[\.\d]*(ms|µs|s)\b`)
+ durationRe := regexp.MustCompile(`\b[\.\d]*(ms|ns|µs|s)\b`)
normalize := func(s string) string {
s = durationRe.ReplaceAllString(s, "")
@@ -193,10 +193,10 @@ D1
expect := `
0 0 0 1 index.html
- 100 0 0 1 partials/static2.html
- 100 50 1 2 partials/static1.html
- 25 50 2 4 partials/dynamic1.html
- 66 33 1 3 partials/halfdynamic1.html
+ 100 0 0 1 _partials/static2.html
+ 100 50 1 2 _partials/static1.html
+ 25 50 2 4 _partials/dynamic1.html
+ 66 33 1 3 _partials/halfdynamic1.html
`
b.Assert(got, hqt.IsSameString, expect)
(DIR) diff --git a/tpl/template.go b/tpl/template.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// 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.
@@ -16,9 +16,6 @@ package tpl
import (
"context"
- "io"
- "reflect"
- "regexp"
"strings"
"sync"
"unicode"
@@ -27,140 +24,18 @@ import (
"github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/langs"
- "github.com/gohugoio/hugo/output/layouts"
-
- "github.com/gohugoio/hugo/output"
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
)
-// TemplateManager manages the collection of templates.
-type TemplateManager interface {
- TemplateHandler
- TemplateFuncGetter
- AddTemplate(name, tpl string) error
-}
-
-// TemplateVariants describes the possible variants of a template.
-// All of these may be empty.
-type TemplateVariants struct {
- Language string
- OutputFormat output.Format
-}
-
-// TemplateFinder finds templates.
-type TemplateFinder interface {
- TemplateLookup
- TemplateLookupVariant
-}
-
-// UnusedTemplatesProvider lists unused templates if the build is configured to track those.
-type UnusedTemplatesProvider interface {
- UnusedTemplates() []FileInfo
-}
-
-// TemplateHandlers holds the templates needed by Hugo.
-type TemplateHandlers struct {
- Tmpl TemplateHandler
- TxtTmpl TemplateParseFinder
-}
-
-type TemplateExecutor interface {
- ExecuteWithContext(ctx context.Context, t Template, wr io.Writer, data any) error
-}
-
-// TemplateHandler finds and executes templates.
-type TemplateHandler interface {
- TemplateFinder
- TemplateExecutor
- LookupLayout(d layouts.LayoutDescriptor, f output.Format) (Template, bool, error)
- HasTemplate(name string) bool
- GetIdentity(name string) (identity.Identity, bool)
-}
-
-type TemplateLookup interface {
- Lookup(name string) (Template, bool)
-}
-
-type TemplateLookupVariant interface {
- // TODO(bep) this currently only works for shortcodes.
- // We may unify and expand this variant pattern to the
- // other templates, but we need this now for the shortcodes to
- // quickly determine if a shortcode has a template for a given
- // output format.
- // It returns the template, if it was found or not and if there are
- // alternative representations (output format, language).
- // We are currently only interested in output formats, so we should improve
- // this for speed.
- LookupVariant(name string, variants TemplateVariants) (Template, bool, bool)
- LookupVariants(name string) []Template
-}
-
// Template is the common interface between text/template and html/template.
type Template interface {
Name() string
Prepare() (*texttemplate.Template, error)
}
-// AddIdentity checks if t is an identity.Identity and returns it if so.
-// Else it wraps it in a templateIdentity using its name as the base.
-func AddIdentity(t Template) Template {
- if _, ok := t.(identity.IdentityProvider); ok {
- return t
- }
- return templateIdentityProvider{
- Template: t,
- id: identity.StringIdentity(t.Name()),
- }
-}
-
-type templateIdentityProvider struct {
- Template
- id identity.Identity
-}
-
-func (t templateIdentityProvider) GetIdentity() identity.Identity {
- return t.id
-}
-
-// TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain.
-type TemplateParser interface {
- Parse(name, tpl string) (Template, error)
-}
-
-// TemplateParseFinder provides both parsing and finding.
-type TemplateParseFinder interface {
- TemplateParser
- TemplateFinder
-}
-
-// TemplateDebugger prints some debug info to stdout.
-type TemplateDebugger interface {
- Debug()
-}
-
-// TemplatesProvider as implemented by deps.Deps.
-type TemplatesProvider interface {
- Tmpl() TemplateHandler
- TextTmpl() TemplateParseFinder
-}
-
-var baseOfRe = regexp.MustCompile("template: (.*?):")
-
-func extractBaseOf(err string) string {
- m := baseOfRe.FindStringSubmatch(err)
- if len(m) == 2 {
- return m[1]
- }
- return ""
-}
-
-// TemplateFuncGetter allows to find a template func by name.
-type TemplateFuncGetter interface {
- GetFunc(name string) (reflect.Value, bool)
-}
-
+// RenderingContext represents the currently rendered site/language.
type RenderingContext struct {
Site site
SiteOutIdx int
@@ -201,7 +76,9 @@ type site interface {
}
const (
+ // HugoDeferredTemplatePrefix is the prefix for placeholders for deferred templates.
HugoDeferredTemplatePrefix = "__hdeferred/"
+ // HugoDeferredTemplateSuffix is the suffix for placeholders for deferred templates.
HugoDeferredTemplateSuffix = "__d="
)
@@ -243,10 +120,11 @@ func StripHTML(s string) string {
return s
}
+// DeferredExecution holds the template and data for a deferred execution.
type DeferredExecution struct {
Mu sync.Mutex
Ctx context.Context
- TemplateName string
+ TemplatePath string
Data any
Executed bool
(DIR) diff --git a/tpl/template_info.go b/tpl/template_info.go
@@ -1,57 +0,0 @@
-// Copyright 2019 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 tpl
-
-// Increments on breaking changes.
-const TemplateVersion = 2
-
-type Info interface {
- ParseInfo() ParseInfo
-}
-
-type FileInfo interface {
- Name() string
- Filename() string
-}
-
-type IsInternalTemplateProvider interface {
- IsInternalTemplate() bool
-}
-
-type ParseInfo struct {
- // Set for shortcode templates with any {{ .Inner }}
- IsInner bool
-
- // Set for partials with a return statement.
- HasReturn bool
-
- // Config extracted from template.
- Config ParseConfig
-}
-
-func (info ParseInfo) IsZero() bool {
- return info.Config.Version == 0
-}
-
-type ParseConfig struct {
- Version int
-}
-
-var DefaultParseConfig = ParseConfig{
- Version: TemplateVersion,
-}
-
-var DefaultParseInfo = ParseInfo{
- Config: DefaultParseConfig,
-}
(DIR) diff --git a/tpl/template_test.go b/tpl/template_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Hugo Authors. All rights reserved.
+// 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.
@@ -15,20 +15,8 @@ package tpl
import (
"testing"
-
- qt "github.com/frankban/quicktest"
)
-func TestExtractBaseof(t *testing.T) {
- c := qt.New(t)
-
- replaced := extractBaseOf(`failed: template: _default/baseof.html:37:11: executing "_default/baseof.html" at <.Parents>: can't evaluate field Parents in type *hugolib.PageOutput`)
-
- c.Assert(replaced, qt.Equals, "_default/baseof.html")
- c.Assert(extractBaseOf("not baseof for you"), qt.Equals, "")
- c.Assert(extractBaseOf("template: blog/baseof.html:23:11:"), qt.Equals, "blog/baseof.html")
-}
-
func TestStripHTML(t *testing.T) {
type test struct {
input, expected string
(DIR) diff --git a/tpl/templates/defer_integration_test.go b/tpl/templates/defer_integration_test.go
@@ -71,6 +71,81 @@ AMP.
`
+func TestDeferNoBaseof(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/index.html --
+Home.
+{{ with (templates.Defer (dict "key" "foo")) }}
+ Defer
+{{ end }}
+-- content/_index.md --
+---
+title: "Home"
+---
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Home.\n\n Defer")
+}
+
+func TestDeferBaseof(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/baseof.html --
+{{ with (templates.Defer (dict "key" "foo")) }}
+Defer
+{{ end }}
+Block:{{ block "main" . }}{{ end }}$
+-- layouts/index.html --
+{{ define "main" }}
+Home.
+{{ end }}
+-- content/_index.md --
+---
+title: "Home"
+---
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Home.\n\n Defer")
+}
+
+func TestDeferMain(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/baseof.html --
+
+Block:{{ block "main" . }}{{ end }}$
+-- layouts/index.html --
+{{ define "main" }}
+Home.
+{{ with (templates.Defer (dict "key" "foo")) }}
+Defer
+{{ end }}
+{{ end }}
+-- content/_index.md --
+---
+title: "Home"
+---
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Home.\n\n Defer")
+}
+
func TestDeferBasic(t *testing.T) {
t.Parallel()
(DIR) diff --git a/tpl/templates/templates.go b/tpl/templates/templates.go
@@ -44,7 +44,7 @@ type Namespace struct {
// Note that this is the Unix-styled relative path including filename suffix,
// e.g. partials/header.html
func (ns *Namespace) Exists(name string) bool {
- return ns.deps.Tmpl().HasTemplate(name)
+ return ns.deps.GetTemplateStore().HasTemplate(name)
}
// Defer defers the execution of a template block.
@@ -93,7 +93,7 @@ func (ns *Namespace) DoDefer(ctx context.Context, id string, optsv any) string {
_, _ = ns.deps.BuildState.DeferredExecutions.Executions.GetOrCreate(id,
func() (*tpl.DeferredExecution, error) {
return &tpl.DeferredExecution{
- TemplateName: templateName,
+ TemplatePath: templateName,
Ctx: ctx,
Data: opts.Data,
Executed: false,
(DIR) diff --git a/tpl/tplimpl/category_string.go b/tpl/tplimpl/category_string.go
@@ -0,0 +1,30 @@
+// Code generated by "stringer -type Category"; DO NOT EDIT.
+
+package tplimpl
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[CategoryLayout-1]
+ _ = x[CategoryBaseof-2]
+ _ = x[CategoryMarkup-3]
+ _ = x[CategoryShortcode-4]
+ _ = x[CategoryPartial-5]
+ _ = x[CategoryServer-6]
+ _ = x[CategoryHugo-7]
+}
+
+const _Category_name = "CategoryLayoutCategoryBaseofCategoryMarkupCategoryShortcodeCategoryPartialCategoryServerCategoryHugo"
+
+var _Category_index = [...]uint8{0, 14, 28, 42, 59, 74, 88, 100}
+
+func (i Category) String() string {
+ i -= 1
+ if i < 0 || i >= Category(len(_Category_index)-1) {
+ return "Category(" + strconv.FormatInt(int64(i+1), 10) + ")"
+ }
+ return _Category_name[_Category_index[i]:_Category_index[i+1]]
+}
(DIR) diff --git a/tpl/tplimpl/embedded/templates/_default/_markup/render-codeblock-goat.html b/tpl/tplimpl/embedded/templates/_markup/render-codeblock-goat.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/_default/_markup/render-image.html b/tpl/tplimpl/embedded/templates/_markup/render-image.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/_default/_markup/render-link.html b/tpl/tplimpl/embedded/templates/_markup/render-link.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/_default/_markup/render-table.html b/tpl/tplimpl/embedded/templates/_markup/render-table.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/partials/_funcs/get-page-images.html b/tpl/tplimpl/embedded/templates/_partials/_funcs/get-page-images.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/_partials/disqus.html b/tpl/tplimpl/embedded/templates/_partials/disqus.html
@@ -0,0 +1,23 @@
+{{- $pc := .Site.Config.Privacy.Disqus -}}
+{{- if not $pc.Disable -}}
+{{ if .Site.Config.Services.Disqus.Shortname }}<div id="disqus_thread"></div>
+<script>
+ window.disqus_config = function () {
+ {{with .Params.disqus_identifier }}this.page.identifier = '{{ . }}';{{end}}
+ {{with .Params.disqus_title }}this.page.title = '{{ . }}';{{end}}
+ {{with .Params.disqus_url }}this.page.url = '{{ . | transform.HTMLEscape | safeURL }}';{{end}}
+ };
+ (function() {
+ if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) {
+ document.getElementById('disqus_thread').innerHTML = 'Disqus comments not available by default when the website is previewed locally.';
+ return;
+ }
+ var d = document, s = d.createElement('script'); s.async = true;
+ s.src = '//' + {{ .Site.Config.Services.Disqus.Shortname }} + '.disqus.com/embed.js';
+ s.setAttribute('data-timestamp', +new Date());
+ (d.head || d.body).appendChild(s);
+ })();
+</script>
+<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
+<a href="https://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>{{end}}
+{{- end -}}
(DIR) diff --git a/tpl/tplimpl/embedded/templates/google_analytics.html b/tpl/tplimpl/embedded/templates/_partials/google_analytics.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/opengraph.html b/tpl/tplimpl/embedded/templates/_partials/opengraph.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/_partials/pagination.html b/tpl/tplimpl/embedded/templates/_partials/pagination.html
@@ -0,0 +1,154 @@
+{{- $validFormats := slice "default" "terse" }}
+
+{{- $msg1 := "When passing a map to the internal pagination template, one of the elements must be named 'page', and it must be set to the context of the current page." }}
+{{- $msg2 := "The 'format' specified in the map passed to the internal pagination template is invalid. Valid choices are: %s." }}
+
+{{- $page := . }}
+{{- $format := "default" }}
+
+{{- if reflect.IsMap . }}
+ {{- with .page }}
+ {{- $page = . }}
+ {{- else }}
+ {{- errorf $msg1 }}
+ {{- end }}
+ {{- with .format }}
+ {{- $format = lower . }}
+ {{- end }}
+{{- end }}
+
+{{- if in $validFormats $format }}
+ {{- if gt $page.Paginator.TotalPages 1 }}
+ <ul class="pagination pagination-{{ $format }}">
+ {{- partial (printf "inline/pagination/%s" $format) $page }}
+ </ul>
+ {{- end }}
+{{- else }}
+ {{- errorf $msg2 (delimit $validFormats ", ") }}
+{{- end -}}
+
+{{/* Format: default
+{{/* --------------------------------------------------------------------- */}}
+{{- define "partials/inline/pagination/default" }}
+ {{- with .Paginator }}
+ {{- $currentPageNumber := .PageNumber }}
+
+ {{- with .First }}
+ {{- if ne $currentPageNumber .PageNumber }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="First" class="page-link" role="button"><span aria-hidden="true">««</span></a>
+ </li>
+ {{- else }}
+ <li class="page-item disabled">
+ <a aria-disabled="true" aria-label="First" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">««</span></a>
+ </li>
+ {{- end }}
+ {{- end }}
+
+ {{- with .Prev }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="Previous" class="page-link" role="button"><span aria-hidden="true">«</span></a>
+ </li>
+ {{- else }}
+ <li class="page-item disabled">
+ <a aria-disabled="true" aria-label="Previous" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">«</span></a>
+ </li>
+ {{- end }}
+
+ {{- $slots := 5 }}
+ {{- $start := math.Max 1 (sub .PageNumber (math.Floor (div $slots 2))) }}
+ {{- $end := math.Min .TotalPages (sub (add $start $slots) 1) }}
+ {{- if lt (add (sub $end $start) 1) $slots }}
+ {{- $start = math.Max 1 (add (sub $end $slots) 1) }}
+ {{- end }}
+
+ {{- range $k := seq $start $end }}
+ {{- if eq $.Paginator.PageNumber $k }}
+ <li class="page-item active">
+ <a aria-current="page" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
+ </li>
+ {{- else }}
+ <li class="page-item">
+ <a href="{{ (index $.Paginator.Pagers (sub $k 1)).URL }}" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
+ </li>
+ {{- end }}
+ {{- end }}
+
+ {{- with .Next }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="Next" class="page-link" role="button"><span aria-hidden="true">»</span></a>
+ </li>
+ {{- else }}
+ <li class="page-item disabled">
+ <a aria-disabled="true" aria-label="Next" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">»</span></a>
+ </li>
+ {{- end }}
+
+ {{- with .Last }}
+ {{- if ne $currentPageNumber .PageNumber }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="Last" class="page-link" role="button"><span aria-hidden="true">»»</span></a>
+ </li>
+ {{- else }}
+ <li class="page-item disabled">
+ <a aria-disabled="true" aria-label="Last" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">»»</span></a>
+ </li>
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end -}}
+
+{{/* Format: terse
+{{/* --------------------------------------------------------------------- */}}
+{{- define "partials/inline/pagination/terse" }}
+ {{- with .Paginator }}
+ {{- $currentPageNumber := .PageNumber }}
+
+ {{- with .First }}
+ {{- if ne $currentPageNumber .PageNumber }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="First" class="page-link" role="button"><span aria-hidden="true">««</span></a>
+ </li>
+ {{- end }}
+ {{- end }}
+
+ {{- with .Prev }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="Previous" class="page-link" role="button"><span aria-hidden="true">«</span></a>
+ </li>
+ {{- end }}
+
+ {{- $slots := 3 }}
+ {{- $start := math.Max 1 (sub .PageNumber (math.Floor (div $slots 2))) }}
+ {{- $end := math.Min .TotalPages (sub (add $start $slots) 1) }}
+ {{- if lt (add (sub $end $start) 1) $slots }}
+ {{- $start = math.Max 1 (add (sub $end $slots) 1) }}
+ {{- end }}
+
+ {{- range $k := seq $start $end }}
+ {{- if eq $.Paginator.PageNumber $k }}
+ <li class="page-item active">
+ <a aria-current="page" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
+ </li>
+ {{- else }}
+ <li class="page-item">
+ <a href="{{ (index $.Paginator.Pagers (sub $k 1)).URL }}" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
+ </li>
+ {{- end }}
+ {{- end }}
+
+ {{- with .Next }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="Next" class="page-link" role="button"><span aria-hidden="true">»</span></a>
+ </li>
+ {{- end }}
+
+ {{- with .Last }}
+ {{- if ne $currentPageNumber .PageNumber }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="Last" class="page-link" role="button"><span aria-hidden="true">»»</span></a>
+ </li>
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end -}}
(DIR) diff --git a/tpl/tplimpl/embedded/templates/schema.html b/tpl/tplimpl/embedded/templates/_partials/schema.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/twitter_cards.html b/tpl/tplimpl/embedded/templates/_partials/twitter_cards.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/1__h_simple_assets.html b/tpl/tplimpl/embedded/templates/_shortcodes/1__h_simple_assets.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/comment.html b/tpl/tplimpl/embedded/templates/_shortcodes/comment.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/details.html b/tpl/tplimpl/embedded/templates/_shortcodes/details.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/figure.html b/tpl/tplimpl/embedded/templates/_shortcodes/figure.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/gist.html b/tpl/tplimpl/embedded/templates/_shortcodes/gist.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/highlight.html b/tpl/tplimpl/embedded/templates/_shortcodes/highlight.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/instagram.html b/tpl/tplimpl/embedded/templates/_shortcodes/instagram.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/instagram_simple.html b/tpl/tplimpl/embedded/templates/_shortcodes/instagram_simple.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/param.html b/tpl/tplimpl/embedded/templates/_shortcodes/param.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/qr.html b/tpl/tplimpl/embedded/templates/_shortcodes/qr.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/ref.html b/tpl/tplimpl/embedded/templates/_shortcodes/ref.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/relref.html b/tpl/tplimpl/embedded/templates/_shortcodes/relref.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/twitter.html b/tpl/tplimpl/embedded/templates/_shortcodes/twitter.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html b/tpl/tplimpl/embedded/templates/_shortcodes/twitter_simple.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/vimeo.html b/tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html b/tpl/tplimpl/embedded/templates/_shortcodes/vimeo_simple.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/x.html b/tpl/tplimpl/embedded/templates/_shortcodes/x.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/x_simple.html b/tpl/tplimpl/embedded/templates/_shortcodes/x_simple.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/shortcodes/youtube.html b/tpl/tplimpl/embedded/templates/_shortcodes/youtube.html
(DIR) diff --git a/tpl/tplimpl/embedded/templates/disqus.html b/tpl/tplimpl/embedded/templates/disqus.html
@@ -1,23 +0,0 @@
-{{- $pc := .Site.Config.Privacy.Disqus -}}
-{{- if not $pc.Disable -}}
-{{ if .Site.Config.Services.Disqus.Shortname }}<div id="disqus_thread"></div>
-<script>
- window.disqus_config = function () {
- {{with .Params.disqus_identifier }}this.page.identifier = '{{ . }}';{{end}}
- {{with .Params.disqus_title }}this.page.title = '{{ . }}';{{end}}
- {{with .Params.disqus_url }}this.page.url = '{{ . | html }}';{{end}}
- };
- (function() {
- if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) {
- document.getElementById('disqus_thread').innerHTML = 'Disqus comments not available by default when the website is previewed locally.';
- return;
- }
- var d = document, s = d.createElement('script'); s.async = true;
- s.src = '//' + {{ .Site.Config.Services.Disqus.Shortname }} + '.disqus.com/embed.js';
- s.setAttribute('data-timestamp', +new Date());
- (d.head || d.body).appendChild(s);
- })();
-</script>
-<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
-<a href="https://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>{{end}}
-{{- end -}}
(DIR) diff --git a/tpl/tplimpl/embedded/templates/pagination.html b/tpl/tplimpl/embedded/templates/pagination.html
@@ -1,154 +0,0 @@
-{{- $validFormats := slice "default" "terse" }}
-
-{{- $msg1 := "When passing a map to the internal pagination template, one of the elements must be named 'page', and it must be set to the context of the current page." }}
-{{- $msg2 := "The 'format' specified in the map passed to the internal pagination template is invalid. Valid choices are: %s." }}
-
-{{- $page := . }}
-{{- $format := "default" }}
-
-{{- if reflect.IsMap . }}
- {{- with .page }}
- {{- $page = . }}
- {{- else }}
- {{- errorf $msg1 }}
- {{- end }}
- {{- with .format }}
- {{- $format = lower . }}
- {{- end }}
-{{- end }}
-
-{{- if in $validFormats $format }}
- {{- if gt $page.Paginator.TotalPages 1 }}
- <ul class="pagination pagination-{{ $format }}">
- {{- partial (printf "partials/inline/pagination/%s" $format) $page }}
- </ul>
- {{- end }}
-{{- else }}
- {{- errorf $msg2 (delimit $validFormats ", ") }}
-{{- end -}}
-
-{{/* Format: default
-{{/* --------------------------------------------------------------------- */}}
-{{- define "partials/inline/pagination/default" }}
- {{- with .Paginator }}
- {{- $currentPageNumber := .PageNumber }}
-
- {{- with .First }}
- {{- if ne $currentPageNumber .PageNumber }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="First" class="page-link" role="button"><span aria-hidden="true">««</span></a>
- </li>
- {{- else }}
- <li class="page-item disabled">
- <a aria-disabled="true" aria-label="First" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">««</span></a>
- </li>
- {{- end }}
- {{- end }}
-
- {{- with .Prev }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="Previous" class="page-link" role="button"><span aria-hidden="true">«</span></a>
- </li>
- {{- else }}
- <li class="page-item disabled">
- <a aria-disabled="true" aria-label="Previous" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">«</span></a>
- </li>
- {{- end }}
-
- {{- $slots := 5 }}
- {{- $start := math.Max 1 (sub .PageNumber (math.Floor (div $slots 2))) }}
- {{- $end := math.Min .TotalPages (sub (add $start $slots) 1) }}
- {{- if lt (add (sub $end $start) 1) $slots }}
- {{- $start = math.Max 1 (add (sub $end $slots) 1) }}
- {{- end }}
-
- {{- range $k := seq $start $end }}
- {{- if eq $.Paginator.PageNumber $k }}
- <li class="page-item active">
- <a aria-current="page" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
- </li>
- {{- else }}
- <li class="page-item">
- <a href="{{ (index $.Paginator.Pagers (sub $k 1)).URL }}" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
- </li>
- {{- end }}
- {{- end }}
-
- {{- with .Next }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="Next" class="page-link" role="button"><span aria-hidden="true">»</span></a>
- </li>
- {{- else }}
- <li class="page-item disabled">
- <a aria-disabled="true" aria-label="Next" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">»</span></a>
- </li>
- {{- end }}
-
- {{- with .Last }}
- {{- if ne $currentPageNumber .PageNumber }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="Last" class="page-link" role="button"><span aria-hidden="true">»»</span></a>
- </li>
- {{- else }}
- <li class="page-item disabled">
- <a aria-disabled="true" aria-label="Last" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">»»</span></a>
- </li>
- {{- end }}
- {{- end }}
- {{- end }}
-{{- end -}}
-
-{{/* Format: terse
-{{/* --------------------------------------------------------------------- */}}
-{{- define "partials/inline/pagination/terse" }}
- {{- with .Paginator }}
- {{- $currentPageNumber := .PageNumber }}
-
- {{- with .First }}
- {{- if ne $currentPageNumber .PageNumber }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="First" class="page-link" role="button"><span aria-hidden="true">««</span></a>
- </li>
- {{- end }}
- {{- end }}
-
- {{- with .Prev }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="Previous" class="page-link" role="button"><span aria-hidden="true">«</span></a>
- </li>
- {{- end }}
-
- {{- $slots := 3 }}
- {{- $start := math.Max 1 (sub .PageNumber (math.Floor (div $slots 2))) }}
- {{- $end := math.Min .TotalPages (sub (add $start $slots) 1) }}
- {{- if lt (add (sub $end $start) 1) $slots }}
- {{- $start = math.Max 1 (add (sub $end $slots) 1) }}
- {{- end }}
-
- {{- range $k := seq $start $end }}
- {{- if eq $.Paginator.PageNumber $k }}
- <li class="page-item active">
- <a aria-current="page" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
- </li>
- {{- else }}
- <li class="page-item">
- <a href="{{ (index $.Paginator.Pagers (sub $k 1)).URL }}" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
- </li>
- {{- end }}
- {{- end }}
-
- {{- with .Next }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="Next" class="page-link" role="button"><span aria-hidden="true">»</span></a>
- </li>
- {{- end }}
-
- {{- with .Last }}
- {{- if ne $currentPageNumber .PageNumber }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="Last" class="page-link" role="button"><span aria-hidden="true">»»</span></a>
- </li>
- {{- end }}
- {{- end }}
- {{- end }}
-{{- end -}}
(DIR) diff --git a/tpl/tplimpl/embedded/templates/_default/robots.txt b/tpl/tplimpl/embedded/templates/robots.txt
(DIR) diff --git a/tpl/tplimpl/embedded/templates/_default/rss.xml b/tpl/tplimpl/embedded/templates/rss.xml
(DIR) diff --git a/tpl/tplimpl/embedded/templates/_default/sitemap.xml b/tpl/tplimpl/embedded/templates/sitemap.xml
(DIR) diff --git a/tpl/tplimpl/embedded/templates/_default/sitemapindex.xml b/tpl/tplimpl/embedded/templates/sitemapindex.xml
(DIR) diff --git a/tpl/tplimpl/legacy.go b/tpl/tplimpl/legacy.go
@@ -0,0 +1,130 @@
+// 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
+
+import (
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/resources/kinds"
+)
+
+type layoutLegacyMapping struct {
+ sourcePath string
+ target layoutLegacyMappingTarget
+}
+
+type layoutLegacyMappingTarget struct {
+ targetPath string
+ targetDesc TemplateDescriptor
+ targetCategory Category
+}
+
+var (
+ ltermPlural = layoutLegacyMappingTarget{
+ targetPath: "/PLURAL",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindTerm},
+ targetCategory: CategoryLayout,
+ }
+ ltermBase = layoutLegacyMappingTarget{
+ targetPath: "",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindTerm},
+ targetCategory: CategoryLayout,
+ }
+
+ ltaxPlural = layoutLegacyMappingTarget{
+ targetPath: "/PLURAL",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindTaxonomy},
+ targetCategory: CategoryLayout,
+ }
+ ltaxBase = layoutLegacyMappingTarget{
+ targetPath: "",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindTaxonomy},
+ targetCategory: CategoryLayout,
+ }
+
+ lsectBase = layoutLegacyMappingTarget{
+ targetPath: "",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindSection},
+ targetCategory: CategoryLayout,
+ }
+ lsectTheSection = layoutLegacyMappingTarget{
+ targetPath: "/THESECTION",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindSection},
+ targetCategory: CategoryLayout,
+ }
+)
+
+type legacyTargetPathIdentifiers struct {
+ targetPath string
+ targetCategory Category
+ kind string
+ lang string
+ outputFormat string
+ ext string
+}
+
+type legacyOrdinalMapping struct {
+ ordinal int
+ mapping layoutLegacyMappingTarget
+}
+
+type legacyOrdinalMappingFi struct {
+ m legacyOrdinalMapping
+ fi hugofs.FileMetaInfo
+}
+
+var legacyTermMappings = []layoutLegacyMapping{
+ {sourcePath: "/PLURAL/term", target: ltermPlural},
+ {sourcePath: "/PLURAL/SINGULAR", target: ltermPlural},
+ {sourcePath: "/term/term", target: ltermBase},
+ {sourcePath: "/term/SINGULAR", target: ltermPlural},
+ {sourcePath: "/term/taxonomy", target: ltermPlural},
+ {sourcePath: "/term/list", target: ltermBase},
+ {sourcePath: "/taxonomy/term", target: ltermBase},
+ {sourcePath: "/taxonomy/SINGULAR", target: ltermPlural},
+ {sourcePath: "/SINGULAR/term", target: ltermPlural},
+ {sourcePath: "/SINGULAR/SINGULAR", target: ltermPlural},
+ {sourcePath: "/_default/SINGULAR", target: ltermPlural},
+ {sourcePath: "/_default/taxonomy", target: ltermBase},
+}
+
+var legacyTaxonomyMappings = []layoutLegacyMapping{
+ {sourcePath: "/PLURAL/SINGULAR.terms", target: ltaxPlural},
+ {sourcePath: "/PLURAL/terms", target: ltaxPlural},
+ {sourcePath: "/PLURAL/taxonomy", target: ltaxPlural},
+ {sourcePath: "/PLURAL/list", target: ltaxPlural},
+ {sourcePath: "/SINGULAR/SINGULAR.terms", target: ltaxPlural},
+ {sourcePath: "/SINGULAR/terms", target: ltaxPlural},
+ {sourcePath: "/SINGULAR/taxonomy", target: ltaxPlural},
+ {sourcePath: "/SINGULAR/list", target: ltaxPlural},
+ {sourcePath: "/taxonomy/SINGULAR.terms", target: ltaxPlural},
+ {sourcePath: "/taxonomy/terms", target: ltaxBase},
+ {sourcePath: "/taxonomy/taxonomy", target: ltaxBase},
+ {sourcePath: "/taxonomy/list", target: ltaxBase},
+ {sourcePath: "/_default/SINGULAR.terms", target: ltaxBase},
+ {sourcePath: "/_default/terms", target: ltaxBase},
+ {sourcePath: "/_default/taxonomy", target: ltaxBase},
+}
+
+var legacySectionMappings = []layoutLegacyMapping{
+ // E.g. /mysection/mysection.html
+ {sourcePath: "/THESECTION/THESECTION", target: lsectTheSection},
+ // E.g. /section/mysection.html
+ {sourcePath: "/SECTIONKIND/THESECTION", target: lsectTheSection},
+ // E.g. /section/section.html
+ {sourcePath: "/SECTIONKIND/SECTIONKIND", target: lsectBase},
+ // E.g. /section/list.html
+ {sourcePath: "/SECTIONKIND/list", target: lsectBase},
+ // E.g. /_default/mysection.html
+ {sourcePath: "/_default/THESECTION", target: lsectTheSection},
+}
(DIR) diff --git a/tpl/tplimpl/render_hook_integration_test.go b/tpl/tplimpl/render_hook_integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Hugo Authors. All rights reserved.
+// 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.
(DIR) diff --git a/tpl/tplimpl/shortcodes.go b/tpl/tplimpl/shortcodes.go
@@ -1,153 +0,0 @@
-// Copyright 2019 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
-
-import (
- "strings"
-
- "github.com/gohugoio/hugo/tpl"
-)
-
-// Currently lang, outFormat, suffix
-const numTemplateVariants = 3
-
-type shortcodeVariant struct {
- // The possible variants: lang, outFormat, suffix
- // gtag
- // gtag.html
- // gtag.no.html
- // gtag.no.amp.html
- // A slice of length numTemplateVariants.
- variants []string
-
- ts *templateState
-}
-
-type shortcodeTemplates struct {
- variants []shortcodeVariant
-}
-
-func (s *shortcodeTemplates) indexOf(variants []string) int {
-L:
- for i, v1 := range s.variants {
- for i, v2 := range v1.variants {
- if v2 != variants[i] {
- continue L
- }
- }
- return i
- }
- return -1
-}
-
-func (s *shortcodeTemplates) fromVariants(variants tpl.TemplateVariants) (shortcodeVariant, bool) {
- return s.fromVariantsSlice([]string{
- variants.Language,
- strings.ToLower(variants.OutputFormat.Name),
- variants.OutputFormat.MediaType.FirstSuffix.Suffix,
- })
-}
-
-func (s *shortcodeTemplates) fromVariantsSlice(variants []string) (shortcodeVariant, bool) {
- var (
- bestMatch shortcodeVariant
- bestMatchWeight int
- )
-
- for _, variant := range s.variants {
- w := s.compareVariants(variants, variant.variants)
- if bestMatchWeight == 0 || w > bestMatchWeight {
- bestMatch = variant
- bestMatchWeight = w
- }
- }
-
- return bestMatch, true
-}
-
-// calculate a weight for two string slices of same length.
-// higher value means "better match".
-func (s *shortcodeTemplates) compareVariants(a, b []string) int {
- weight := 0
- k := len(a)
- for i, av := range a {
- bv := b[i]
- if av == bv {
- // Add more weight to the left side (language...).
- weight = weight + k - i
- } else {
- weight--
- }
- }
- return weight
-}
-
-func templateVariants(name string) []string {
- _, variants := templateNameAndVariants(name)
- return variants
-}
-
-func templateNameAndVariants(name string) (string, []string) {
- variants := make([]string, numTemplateVariants)
-
- parts := strings.Split(name, ".")
-
- if len(parts) <= 1 {
- // No variants.
- return name, variants
- }
-
- name = parts[0]
- parts = parts[1:]
- lp := len(parts)
- start := len(variants) - lp
-
- for i, j := start, 0; i < len(variants); i, j = i+1, j+1 {
- variants[i] = parts[j]
- }
-
- if lp > 1 && lp < len(variants) {
- for i := lp - 1; i > 0; i-- {
- variants[i-1] = variants[i]
- }
- }
-
- if lp == 1 {
- // Suffix only. Duplicate it into the output format field to
- // make HTML win over AMP.
- variants[len(variants)-2] = variants[len(variants)-1]
- }
-
- return name, variants
-}
-
-func resolveTemplateType(name string) templateType {
- if isShortcode(name) {
- return templateShortcode
- }
-
- if strings.Contains(name, "partials/") {
- return templatePartial
- }
-
- return templateUndefined
-}
-
-func isShortcode(name string) bool {
- return strings.Contains(name, shortcodesPathPrefix)
-}
-
-func isInternal(name string) bool {
- return strings.HasPrefix(name, internalPathPrefix)
-}
(DIR) diff --git a/tpl/tplimpl/shortcodes_test.go b/tpl/tplimpl/shortcodes_test.go
@@ -1,91 +0,0 @@
-// Copyright 2019 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
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
-)
-
-func TestShortcodesTemplate(t *testing.T) {
- t.Run("isShortcode", func(t *testing.T) {
- c := qt.New(t)
- c.Assert(isShortcode("shortcodes/figures.html"), qt.Equals, true)
- c.Assert(isShortcode("_internal/shortcodes/figures.html"), qt.Equals, true)
- c.Assert(isShortcode("shortcodes\\figures.html"), qt.Equals, false)
- c.Assert(isShortcode("myshortcodes"), qt.Equals, false)
- })
-
- t.Run("variantsFromName", func(t *testing.T) {
- c := qt.New(t)
- c.Assert(templateVariants("figure.html"), qt.DeepEquals, []string{"", "html", "html"})
- c.Assert(templateVariants("figure.no.html"), qt.DeepEquals, []string{"no", "no", "html"})
- c.Assert(templateVariants("figure.no.amp.html"), qt.DeepEquals, []string{"no", "amp", "html"})
- c.Assert(templateVariants("figure.amp.html"), qt.DeepEquals, []string{"amp", "amp", "html"})
-
- name, variants := templateNameAndVariants("figure.html")
- c.Assert(name, qt.Equals, "figure")
- c.Assert(variants, qt.DeepEquals, []string{"", "html", "html"})
- })
-
- t.Run("compareVariants", func(t *testing.T) {
- c := qt.New(t)
- var s *shortcodeTemplates
-
- tests := []struct {
- name string
- name1 string
- name2 string
- expected int
- }{
- {"Same suffix", "figure.html", "figure.html", 6},
- {"Same suffix and output format", "figure.html.html", "figure.html.html", 6},
- {"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 6},
- {"No suffix", "figure", "figure", 6},
- {"Different output format", "figure.amp.html", "figure.html.html", -1},
- {"One with output format, one without", "figure.amp.html", "figure.html", -1},
- }
-
- for _, test := range tests {
- w := s.compareVariants(templateVariants(test.name1), templateVariants(test.name2))
- c.Assert(w, qt.Equals, test.expected)
- }
- })
-
- t.Run("indexOf", func(t *testing.T) {
- c := qt.New(t)
-
- s := &shortcodeTemplates{
- variants: []shortcodeVariant{
- {variants: []string{"a", "b", "c"}},
- {variants: []string{"a", "b", "d"}},
- },
- }
-
- c.Assert(s.indexOf([]string{"a", "b", "c"}), qt.Equals, 0)
- c.Assert(s.indexOf([]string{"a", "b", "d"}), qt.Equals, 1)
- c.Assert(s.indexOf([]string{"a", "b", "x"}), qt.Equals, -1)
- })
-
- t.Run("Name", func(t *testing.T) {
- c := qt.New(t)
-
- c.Assert(templateBaseName(templateShortcode, "shortcodes/foo.html"), qt.Equals, "foo.html")
- c.Assert(templateBaseName(templateShortcode, "_internal/shortcodes/foo.html"), qt.Equals, "foo.html")
- c.Assert(templateBaseName(templateShortcode, "shortcodes/test/foo.html"), qt.Equals, "test/foo.html")
-
- c.Assert(true, qt.Equals, true)
- })
-}
(DIR) diff --git a/tpl/tplimpl/subcategory_string.go b/tpl/tplimpl/subcategory_string.go
@@ -0,0 +1,25 @@
+// Code generated by "stringer -type SubCategory"; DO NOT EDIT.
+
+package tplimpl
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[SubCategoryMain-0]
+ _ = x[SubCategoryEmbedded-1]
+ _ = x[SubCategoryInline-2]
+}
+
+const _SubCategory_name = "SubCategoryMainSubCategoryEmbeddedSubCategoryInline"
+
+var _SubCategory_index = [...]uint8{0, 15, 34, 51}
+
+func (i SubCategory) String() string {
+ if i < 0 || i >= SubCategory(len(_SubCategory_index)-1) {
+ return "SubCategory(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _SubCategory_name[_SubCategory_index[i]:_SubCategory_index[i+1]]
+}
(DIR) diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go
@@ -1,1235 +0,0 @@
-// Copyright 2019 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
-
-import (
- "bytes"
- "context"
- "embed"
- "fmt"
- "io"
- "io/fs"
- "path/filepath"
- "reflect"
- "regexp"
- "sort"
- "strings"
- "sync"
- "time"
- "unicode"
- "unicode/utf8"
-
- "github.com/gohugoio/hugo/common/maps"
- "github.com/gohugoio/hugo/common/types"
- "github.com/gohugoio/hugo/output/layouts"
-
- "github.com/gohugoio/hugo/helpers"
-
- "github.com/gohugoio/hugo/output"
-
- "github.com/gohugoio/hugo/deps"
- "github.com/spf13/afero"
-
- "github.com/gohugoio/hugo/common/herrors"
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
-
- htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
- texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-
- "github.com/gohugoio/hugo/identity"
- "github.com/gohugoio/hugo/tpl"
-)
-
-const (
- textTmplNamePrefix = "_text/"
-
- shortcodesPathPrefix = "shortcodes/"
- internalPathPrefix = "_internal/"
- embeddedPathPrefix = "_embedded/"
- baseFileBase = "baseof"
-)
-
-// The identifiers may be truncated in the log, e.g.
-// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image"
-// We need this to identify position in templates with base templates applied.
-var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`)
-
-// The tweet and twitter shortcodes were deprecated in favor of the x shortcode
-// in v0.141.0. We can remove these aliases in v0.155.0 or later.
-var embeddedTemplatesAliases = map[string][]string{
- "shortcodes/twitter.html": {"shortcodes/tweet.html"},
-}
-
-var (
- _ tpl.TemplateManager = (*templateExec)(nil)
- _ tpl.TemplateHandler = (*templateExec)(nil)
- _ tpl.TemplateFuncGetter = (*templateExec)(nil)
- _ tpl.TemplateFinder = (*templateExec)(nil)
- _ tpl.UnusedTemplatesProvider = (*templateExec)(nil)
-
- _ tpl.Template = (*templateState)(nil)
- _ tpl.Info = (*templateState)(nil)
-)
-
-var baseTemplateDefineRe = regexp.MustCompile(`^{{-?\s*define`)
-
-// needsBaseTemplate returns true if the first non-comment template block is a
-// define block.
-// If a base template does not exist, we will handle that when it's used.
-func needsBaseTemplate(templ string) bool {
- idx := -1
- inComment := false
- for i := 0; i < len(templ); {
- if !inComment && strings.HasPrefix(templ[i:], "{{/*") {
- inComment = true
- i += 4
- } else if !inComment && strings.HasPrefix(templ[i:], "{{- /*") {
- inComment = true
- i += 6
- } else if inComment && strings.HasPrefix(templ[i:], "*/}}") {
- inComment = false
- i += 4
- } else if inComment && strings.HasPrefix(templ[i:], "*/ -}}") {
- inComment = false
- i += 6
- } else {
- r, size := utf8.DecodeRuneInString(templ[i:])
- if !inComment {
- if strings.HasPrefix(templ[i:], "{{") {
- idx = i
- break
- } else if !unicode.IsSpace(r) {
- break
- }
- }
- i += size
- }
- }
-
- if idx == -1 {
- return false
- }
-
- return baseTemplateDefineRe.MatchString(templ[idx:])
-}
-
-func newStandaloneTextTemplate(funcs map[string]any) tpl.TemplateParseFinder {
- return &textTemplateWrapperWithLock{
- RWMutex: &sync.RWMutex{},
- Template: texttemplate.New("").Funcs(funcs),
- }
-}
-
-func newTemplateHandlers(d *deps.Deps) (*tpl.TemplateHandlers, error) {
- exec, funcs := newTemplateExecuter(d)
- funcMap := make(map[string]any)
- for k, v := range funcs {
- funcMap[k] = v.Interface()
- }
-
- var templateUsageTracker map[string]templateInfo
- if d.Conf.PrintUnusedTemplates() {
- templateUsageTracker = make(map[string]templateInfo)
- }
-
- h := &templateHandler{
- nameBaseTemplateName: make(map[string]string),
- transformNotFound: make(map[string]*templateState),
-
- shortcodes: make(map[string]*shortcodeTemplates),
- templateInfo: make(map[string]tpl.Info),
- baseof: make(map[string]templateInfo),
- needsBaseof: make(map[string]templateInfo),
-
- main: newTemplateNamespace(funcMap),
-
- Deps: d,
- layoutHandler: layouts.NewLayoutHandler(),
- layoutsFs: d.BaseFs.Layouts.Fs,
- layoutTemplateCache: make(map[layoutCacheKey]layoutCacheEntry),
-
- templateUsageTracker: templateUsageTracker,
- }
-
- if err := h.loadEmbedded(); err != nil {
- return nil, err
- }
-
- if err := h.loadTemplates(); err != nil {
- return nil, err
- }
-
- if err := h.main.createPrototypes(); err != nil {
- return nil, err
- }
-
- e := &templateExec{
- d: d,
- executor: exec,
- funcs: funcs,
- templateHandler: h,
- }
-
- if err := e.postTransform(); err != nil {
- return nil, err
- }
-
- return &tpl.TemplateHandlers{
- Tmpl: e,
- TxtTmpl: newStandaloneTextTemplate(funcMap),
- }, nil
-}
-
-func newTemplateNamespace(funcs map[string]any) *templateNamespace {
- return &templateNamespace{
- prototypeHTML: htmltemplate.New("").Funcs(funcs),
- prototypeText: texttemplate.New("").Funcs(funcs),
- prototypeHTMLCloneCache: maps.NewCache[prototypeCloneID, *htmltemplate.Template](),
- prototypeTextCloneCache: maps.NewCache[prototypeCloneID, *texttemplate.Template](),
- templateStateMap: &templateStateMap{
- templates: make(map[string]*templateState),
- },
- }
-}
-
-func newTemplateState(owner *templateState, templ tpl.Template, info templateInfo, id identity.Identity) *templateState {
- if id == nil {
- id = info
- }
- return &templateState{
- owner: owner,
- info: info,
- typ: info.resolveType(),
- Template: templ,
- parseInfo: tpl.DefaultParseInfo,
- id: id,
- }
-}
-
-type layoutCacheKey struct {
- d layouts.LayoutDescriptor
- f string
-}
-
-type templateExec struct {
- d *deps.Deps
- executor texttemplate.Executer
- funcs map[string]reflect.Value
-
- *templateHandler
-}
-
-func (t templateExec) Clone(d *deps.Deps) *templateExec {
- exec, funcs := newTemplateExecuter(d)
- t.executor = exec
- t.funcs = funcs
- t.d = d
- return &t
-}
-
-func (t *templateExec) Execute(templ tpl.Template, wr io.Writer, data any) error {
- return t.ExecuteWithContext(context.Background(), templ, wr, data)
-}
-
-func (t *templateExec) ExecuteWithContext(ctx context.Context, templ tpl.Template, wr io.Writer, data any) error {
- if rlocker, ok := templ.(types.RLocker); ok {
- rlocker.RLock()
- defer rlocker.RUnlock()
- }
- if t.Metrics != nil {
- defer t.Metrics.MeasureSince(templ.Name(), time.Now())
- }
-
- if t.templateUsageTracker != nil {
- if ts, ok := templ.(*templateState); ok {
-
- t.templateUsageTrackerMu.Lock()
- if _, found := t.templateUsageTracker[ts.Name()]; !found {
- t.templateUsageTracker[ts.Name()] = ts.info
- }
-
- if !ts.baseInfo.IsZero() {
- if _, found := t.templateUsageTracker[ts.baseInfo.name]; !found {
- t.templateUsageTracker[ts.baseInfo.name] = ts.baseInfo
- }
- }
- t.templateUsageTrackerMu.Unlock()
- }
- }
-
- execErr := t.executor.ExecuteWithContext(ctx, templ, wr, data)
- if execErr != nil {
- owner := templ
- if ts, ok := templ.(*templateState); ok && ts.owner != nil {
- owner = ts.owner
- }
- execErr = t.addFileContext(owner, execErr)
- }
- return execErr
-}
-
-func (t *templateExec) UnusedTemplates() []tpl.FileInfo {
- if t.templateUsageTracker == nil {
- return nil
- }
- var unused []tpl.FileInfo
-
- for _, ti := range t.needsBaseof {
- if _, found := t.templateUsageTracker[ti.name]; !found {
- unused = append(unused, ti)
- }
- }
-
- for _, ti := range t.baseof {
- if _, found := t.templateUsageTracker[ti.name]; !found {
- unused = append(unused, ti)
- }
- }
-
- for _, ts := range t.main.templates {
- ti := ts.info
- if strings.HasPrefix(ti.name, "_internal/") || ti.meta == nil {
- continue
- }
-
- if _, found := t.templateUsageTracker[ti.name]; !found {
- unused = append(unused, ti)
- }
- }
-
- sort.Slice(unused, func(i, j int) bool {
- return unused[i].Name() < unused[j].Name()
- })
-
- return unused
-}
-
-func (t *templateExec) GetFunc(name string) (reflect.Value, bool) {
- v, found := t.funcs[name]
- return v, found
-}
-
-type templateHandler struct {
- main *templateNamespace
- needsBaseof map[string]templateInfo
- baseof map[string]templateInfo
-
- // This is the filesystem to load the templates from. All the templates are
- // stored in the root of this filesystem.
- layoutsFs afero.Fs
-
- layoutHandler *layouts.LayoutHandler
-
- layoutTemplateCache map[layoutCacheKey]layoutCacheEntry
- layoutTemplateCacheMu sync.RWMutex
-
- *deps.Deps
-
- // Used to get proper filenames in errors
- nameBaseTemplateName map[string]string
-
- // Holds name and source of template definitions not found during the first
- // AST transformation pass.
- transformNotFound map[string]*templateState
-
- // shortcodes maps shortcode name to template variants
- // (language, output format etc.) of that shortcode.
- shortcodes map[string]*shortcodeTemplates
-
- // templateInfo maps template name to some additional information about that template.
- // Note that for shortcodes that same information is embedded in the
- // shortcodeTemplates type.
- templateInfo map[string]tpl.Info
-
- // May be nil.
- templateUsageTracker map[string]templateInfo
- templateUsageTrackerMu sync.Mutex
-}
-
-type layoutCacheEntry struct {
- found bool
- templ tpl.Template
- err error
-}
-
-// AddTemplate parses and adds a template to the collection.
-// Templates with name prefixed with "_text" will be handled as plain
-// text templates.
-func (t *templateHandler) AddTemplate(name, tpl string) error {
- templ, err := t.addTemplateTo(t.newTemplateInfo(name, tpl), t.main)
- if err == nil {
- _, err = t.applyTemplateTransformers(t.main, templ)
- }
- return err
-}
-
-func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
- templ, found := t.main.Lookup(name)
- if found {
- return templ, true
- }
-
- return nil, false
-}
-
-func (t *templateHandler) LookupLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
- key := layoutCacheKey{d, f.Name}
- t.layoutTemplateCacheMu.RLock()
- if cacheVal, found := t.layoutTemplateCache[key]; found {
- t.layoutTemplateCacheMu.RUnlock()
- return cacheVal.templ, cacheVal.found, cacheVal.err
- }
-
- t.layoutTemplateCacheMu.RUnlock()
-
- t.layoutTemplateCacheMu.Lock()
- defer t.layoutTemplateCacheMu.Unlock()
-
- templ, found, err := t.findLayout(d, f)
- cacheVal := layoutCacheEntry{found: found, templ: templ, err: err}
- t.layoutTemplateCache[key] = cacheVal
- return cacheVal.templ, cacheVal.found, cacheVal.err
-}
-
-// This currently only applies to shortcodes and what we get here is the
-// shortcode name.
-func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
- name = templateBaseName(templateShortcode, name)
- s, found := t.shortcodes[name]
- if !found {
- return nil, false, false
- }
-
- sv, found := s.fromVariants(variants)
- if !found {
- return nil, false, false
- }
-
- more := len(s.variants) > 1
-
- return sv.ts, true, more
-}
-
-// LookupVariants returns all variants of name, nil if none found.
-func (t *templateHandler) LookupVariants(name string) []tpl.Template {
- name = templateBaseName(templateShortcode, name)
- s, found := t.shortcodes[name]
- if !found {
- return nil
- }
-
- variants := make([]tpl.Template, len(s.variants))
- for i := range variants {
- variants[i] = s.variants[i].ts
- }
-
- return variants
-}
-
-func (t *templateHandler) HasTemplate(name string) bool {
- if _, found := t.baseof[name]; found {
- return true
- }
-
- if _, found := t.needsBaseof[name]; found {
- return true
- }
-
- _, found := t.Lookup(name)
- return found
-}
-
-func (t *templateHandler) GetIdentity(name string) (identity.Identity, bool) {
- if _, found := t.needsBaseof[name]; found {
- return identity.StringIdentity(name), true
- }
-
- if _, found := t.baseof[name]; found {
- return identity.StringIdentity(name), true
- }
-
- tt, found := t.Lookup(name)
- if !found {
- return nil, false
- }
- return tt.(identity.IdentityProvider).GetIdentity(), found
-}
-
-func (t *templateHandler) findLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
- d.OutputFormatName = f.Name
- d.Suffix = f.MediaType.FirstSuffix.Suffix
- layouts, _ := t.layoutHandler.For(d)
- for _, name := range layouts {
- templ, found := t.main.Lookup(name)
- if found {
- return templ, true, nil
- }
-
- overlay, found := t.needsBaseof[name]
-
- if !found {
- continue
- }
-
- d.Baseof = true
- baseLayouts, _ := t.layoutHandler.For(d)
- var base templateInfo
- found = false
- for _, l := range baseLayouts {
- base, found = t.baseof[l]
- if found {
- break
- }
- }
-
- templ, err := t.applyBaseTemplate(overlay, base)
- if err != nil {
- return nil, false, err
- }
-
- ts := newTemplateState(nil, templ, overlay, identity.Or(base, overlay))
-
- if found {
- ts.baseInfo = base
- }
-
- if _, err := t.applyTemplateTransformers(t.main, ts); err != nil {
- return nil, false, err
- }
-
- if err := t.extractPartials(ts.Template); err != nil {
- return nil, false, err
- }
-
- return ts, true, nil
-
- }
-
- return nil, false, nil
-}
-
-func (t *templateHandler) newTemplateInfo(name, tpl string) templateInfo {
- var isText bool
- var isEmbedded bool
-
- if strings.HasPrefix(name, embeddedPathPrefix) {
- isEmbedded = true
- name = strings.TrimPrefix(name, embeddedPathPrefix)
- }
-
- name, isText = t.nameIsText(name)
- return templateInfo{
- name: name,
- isText: isText,
- isEmbedded: isEmbedded,
- template: tpl,
- }
-}
-
-func (t *templateHandler) addFileContext(templ tpl.Template, inerr error) error {
- if strings.HasPrefix(templ.Name(), "_internal") {
- return inerr
- }
-
- ts, ok := templ.(*templateState)
- if !ok {
- return inerr
- }
-
- identifiers := t.extractIdentifiers(inerr.Error())
-
- checkFilename := func(info templateInfo, inErr error) (error, bool) {
- if info.meta == nil {
- return inErr, false
- }
-
- lineMatcher := func(m herrors.LineMatcher) int {
- if m.Position.LineNumber != m.LineNumber {
- return -1
- }
-
- for _, id := range identifiers {
- if strings.Contains(m.Line, id) {
- // We found the line, but return a 0 to signal to
- // use the column from the error message.
- return 0
- }
- }
- return -1
- }
-
- f, err := info.meta.Open()
- if err != nil {
- return inErr, false
- }
- defer f.Close()
-
- fe := herrors.NewFileErrorFromName(inErr, info.meta.Filename)
- fe.UpdateContent(f, lineMatcher)
-
- if !fe.ErrorContext().Position.IsValid() {
- return inErr, false
- }
- return fe, true
- }
-
- inerr = fmt.Errorf("execute of template failed: %w", inerr)
-
- if err, ok := checkFilename(ts.info, inerr); ok {
- return err
- }
-
- err, _ := checkFilename(ts.baseInfo, inerr)
-
- return err
-}
-
-func (t *templateHandler) extractIdentifiers(line string) []string {
- m := identifiersRe.FindAllStringSubmatch(line, -1)
- identifiers := make([]string, len(m))
- for i := range m {
- identifiers[i] = m[i][1]
- }
- return identifiers
-}
-
-func (t *templateHandler) addShortcodeVariant(ts *templateState) {
- name := ts.Name()
- base := templateBaseName(templateShortcode, name)
-
- shortcodename, variants := templateNameAndVariants(base)
-
- templs, found := t.shortcodes[shortcodename]
- if !found {
- templs = &shortcodeTemplates{}
- t.shortcodes[shortcodename] = templs
- }
-
- sv := shortcodeVariant{variants: variants, ts: ts}
-
- i := templs.indexOf(variants)
-
- if i != -1 {
- // Only replace if it's an override of an internal template.
- if !isInternal(name) {
- templs.variants[i] = sv
- }
- } else {
- templs.variants = append(templs.variants, sv)
- }
-}
-
-func (t *templateHandler) addTemplateFile(name string, fim hugofs.FileMetaInfo) error {
- getTemplate := func(fim hugofs.FileMetaInfo) (templateInfo, error) {
- meta := fim.Meta()
- f, err := meta.Open()
- if err != nil {
- return templateInfo{meta: meta}, err
- }
- defer f.Close()
- b, err := io.ReadAll(f)
- if err != nil {
- return templateInfo{meta: meta}, err
- }
-
- s := removeLeadingBOM(string(b))
-
- var isText bool
- name, isText = t.nameIsText(name)
-
- return templateInfo{
- name: name,
- isText: isText,
- template: s,
- meta: meta,
- }, nil
- }
-
- tinfo, err := getTemplate(fim)
- if err != nil {
- return err
- }
-
- if isBaseTemplatePath(name) {
- // Store it for later.
- t.baseof[name] = tinfo
- return nil
- }
-
- needsBaseof := !t.noBaseNeeded(name) && needsBaseTemplate(tinfo.template)
- if needsBaseof {
- t.needsBaseof[name] = tinfo
- return nil
- }
-
- templ, err := t.addTemplateTo(tinfo, t.main)
- if err != nil {
- return tinfo.errWithFileContext("parse failed", err)
- }
-
- if _, err = t.applyTemplateTransformers(t.main, templ); err != nil {
- return tinfo.errWithFileContext("transform failed", err)
- }
-
- return nil
-}
-
-func (t *templateHandler) addTemplateTo(info templateInfo, to *templateNamespace) (*templateState, error) {
- return to.parse(info)
-}
-
-func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) {
- if overlay.isText {
- var (
- templ = t.main.getPrototypeText(prototypeCloneIDBaseof).New(overlay.name)
- err error
- )
-
- if !base.IsZero() {
- templ, err = templ.Parse(base.template)
- if err != nil {
- return nil, base.errWithFileContext("text: base: parse failed", err)
- }
- }
-
- templ, err = texttemplate.Must(templ.Clone()).Parse(overlay.template)
- if err != nil {
- return nil, overlay.errWithFileContext("text: overlay: parse failed", err)
- }
-
- // The extra lookup is a workaround, see
- // * https://github.com/golang/go/issues/16101
- // * https://github.com/gohugoio/hugo/issues/2549
- // templ = templ.Lookup(templ.Name())
-
- return templ, nil
- }
-
- var (
- templ = t.main.getPrototypeHTML(prototypeCloneIDBaseof).New(overlay.name)
- err error
- )
-
- if !base.IsZero() {
- templ, err = templ.Parse(base.template)
- if err != nil {
- return nil, base.errWithFileContext("html: base: parse failed", err)
- }
- }
-
- templ, err = htmltemplate.Must(templ.Clone()).Parse(overlay.template)
- if err != nil {
- return nil, overlay.errWithFileContext("html: overlay: parse failed", err)
- }
-
- // The extra lookup is a workaround, see
- // * https://github.com/golang/go/issues/16101
- // * https://github.com/gohugoio/hugo/issues/2549
- templ = templ.Lookup(templ.Name())
-
- return templ, err
-}
-
-func (t *templateHandler) applyTemplateTransformers(ns *templateNamespace, ts *templateState) (*templateContext, error) {
- c, err := applyTemplateTransformers(ts, ns.newTemplateLookup(ts))
- if err != nil {
- return nil, err
- }
-
- for k := range c.templateNotFound {
- t.transformNotFound[k] = ts
- }
-
- for k, v := range c.deferNodes {
- if err = t.main.addDeferredTemplate(ts, k, v); err != nil {
- return nil, err
- }
- }
-
- return c, err
-}
-
-//go:embed all:embedded/templates/*
-//go:embed embedded/templates/_default/*
-//go:embed embedded/templates/_server/*
-var embeddedTemplatesFs embed.FS
-
-func (t *templateHandler) loadEmbedded() error {
- return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error {
- if err != nil {
- return err
- }
- if d == nil || d.IsDir() {
- return nil
- }
-
- templb, err := embeddedTemplatesFs.ReadFile(path)
- if err != nil {
- return err
- }
-
- // Get the newlines on Windows in line with how we had it back when we used Go Generate
- // to write the templates to Go files.
- templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
- name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/")
- templateName := name
-
- // For the render hooks and the server templates it does not make sense to preserve the
- // double _internal double book-keeping,
- // just add it if its now provided by the user.
- if !strings.Contains(path, "_default/_markup") && !strings.HasPrefix(name, "_server/") && !strings.HasPrefix(name, "partials/_funcs/") {
- templateName = internalPathPrefix + name
- }
-
- if _, found := t.Lookup(templateName); !found {
- if err := t.AddTemplate(embeddedPathPrefix+templateName, templ); err != nil {
- return err
- }
- }
-
- if aliases, found := embeddedTemplatesAliases[name]; found {
- // TODO(bep) avoid reparsing these aliases
- for _, alias := range aliases {
- alias = internalPathPrefix + alias
- if err := t.AddTemplate(embeddedPathPrefix+alias, templ); err != nil {
- return err
- }
- }
- }
-
- return nil
- })
-}
-
-func (t *templateHandler) loadTemplates() error {
- walker := func(path string, fi hugofs.FileMetaInfo) error {
- if fi.IsDir() {
- return nil
- }
-
- if isDotFile(path) || isBackupFile(path) {
- return nil
- }
-
- name := strings.TrimPrefix(filepath.ToSlash(path), "/")
- filename := filepath.Base(path)
- outputFormats := t.Conf.GetConfigSection("outputFormats").(output.Formats)
- outputFormat, found := outputFormats.FromFilename(filename)
-
- if found && outputFormat.IsPlainText {
- name = textTmplNamePrefix + name
- }
-
- if err := t.addTemplateFile(name, fi); err != nil {
- return err
- }
-
- return nil
- }
-
- if err := helpers.Walk(t.Layouts.Fs, "", walker); err != nil {
- if !herrors.IsNotExist(err) {
- return err
- }
- return nil
- }
-
- return nil
-}
-
-func (t *templateHandler) nameIsText(name string) (string, bool) {
- isText := strings.HasPrefix(name, textTmplNamePrefix)
- if isText {
- name = strings.TrimPrefix(name, textTmplNamePrefix)
- }
- return name, isText
-}
-
-func (t *templateHandler) noBaseNeeded(name string) bool {
- if strings.HasPrefix(name, "shortcodes/") || strings.HasPrefix(name, "partials/") {
- return true
- }
- return strings.Contains(name, "_markup/")
-}
-
-func (t *templateHandler) extractPartials(templ tpl.Template) error {
- templs := templates(templ)
- for _, templ := range templs {
- if templ.Name() == "" || !strings.HasPrefix(templ.Name(), "partials/") {
- continue
- }
-
- ts := newTemplateState(nil, templ, templateInfo{name: templ.Name()}, nil)
- ts.typ = templatePartial
-
- t.main.mu.RLock()
- _, found := t.main.templates[templ.Name()]
- t.main.mu.RUnlock()
-
- if !found {
- t.main.mu.Lock()
- // This is a template defined inline.
- _, err := applyTemplateTransformers(ts, t.main.newTemplateLookup(ts))
- if err != nil {
- t.main.mu.Unlock()
- return err
- }
- t.main.templates[templ.Name()] = ts
- t.main.mu.Unlock()
-
- }
- }
-
- return nil
-}
-
-func (t *templateHandler) postTransform() error {
- defineCheckedHTML := false
- defineCheckedText := false
-
- for _, v := range t.main.templates {
- if v.typ == templateShortcode {
- t.addShortcodeVariant(v)
- }
-
- if defineCheckedHTML && defineCheckedText {
- continue
- }
-
- isText := isText(v.Template)
- if isText {
- if defineCheckedText {
- continue
- }
- defineCheckedText = true
- } else {
- if defineCheckedHTML {
- continue
- }
- defineCheckedHTML = true
- }
-
- if err := t.extractPartials(v.Template); err != nil {
- return err
- }
- }
-
- for name, source := range t.transformNotFound {
- lookup := t.main.newTemplateLookup(source)
- templ := lookup(name)
- if templ != nil {
- _, err := applyTemplateTransformers(templ, lookup)
- if err != nil {
- return err
- }
- }
- }
-
- for _, v := range t.shortcodes {
- sort.Slice(v.variants, func(i, j int) bool {
- v1, v2 := v.variants[i], v.variants[j]
- name1, name2 := v1.ts.Name(), v2.ts.Name()
- isHTMl1, isHTML2 := strings.HasSuffix(name1, "html"), strings.HasSuffix(name2, "html")
-
- // There will be a weighted selection later, but make
- // sure these are sorted to get a stable selection for
- // output formats missing specific templates.
- // Prefer HTML.
- if isHTMl1 || isHTML2 && !(isHTMl1 && isHTML2) {
- return isHTMl1
- }
-
- return name1 < name2
- })
- }
-
- return nil
-}
-
-type prototypeCloneID uint16
-
-const (
- prototypeCloneIDBaseof prototypeCloneID = iota + 1
- prototypeCloneIDDefer
-)
-
-type templateNamespace struct {
- prototypeText *texttemplate.Template
- prototypeHTML *htmltemplate.Template
-
- prototypeHTMLCloneCache *maps.Cache[prototypeCloneID, *htmltemplate.Template]
- prototypeTextCloneCache *maps.Cache[prototypeCloneID, *texttemplate.Template]
-
- *templateStateMap
-}
-
-func (t *templateNamespace) getPrototypeText(id prototypeCloneID) *texttemplate.Template {
- v, ok := t.prototypeTextCloneCache.Get(id)
- if !ok {
- return t.prototypeText
- }
- return v
-}
-
-func (t *templateNamespace) getPrototypeHTML(id prototypeCloneID) *htmltemplate.Template {
- v, ok := t.prototypeHTMLCloneCache.Get(id)
- if !ok {
- return t.prototypeHTML
- }
- return v
-}
-
-func (t *templateNamespace) Lookup(name string) (tpl.Template, bool) {
- t.mu.RLock()
- defer t.mu.RUnlock()
-
- templ, found := t.templates[name]
- if !found {
- return nil, false
- }
-
- return templ, found
-}
-
-func (t *templateNamespace) createPrototypes() error {
- for _, id := range []prototypeCloneID{prototypeCloneIDBaseof, prototypeCloneIDDefer} {
- t.prototypeHTMLCloneCache.Set(id, htmltemplate.Must(t.prototypeHTML.Clone()))
- t.prototypeTextCloneCache.Set(id, texttemplate.Must(t.prototypeText.Clone()))
- }
- return nil
-}
-
-func (t *templateNamespace) newTemplateLookup(in *templateState) func(name string) *templateState {
- return func(name string) *templateState {
- if templ, found := t.templates[name]; found {
- if templ.isText() != in.isText() {
- return nil
- }
- return templ
- }
- if templ, found := findTemplateIn(name, in); found {
- return newTemplateState(nil, templ, templateInfo{name: templ.Name()}, nil)
- }
- return nil
- }
-}
-
-func (t *templateNamespace) addDeferredTemplate(owner *templateState, name string, n *parse.ListNode) error {
- t.mu.Lock()
- defer t.mu.Unlock()
-
- if _, found := t.templates[name]; found {
- return nil
- }
-
- var templ tpl.Template
-
- if owner.isText() {
- prototype := t.getPrototypeText(prototypeCloneIDDefer)
- tt, err := prototype.New(name).Parse("")
- if err != nil {
- return fmt.Errorf("failed to parse empty text template %q: %w", name, err)
- }
- tt.Tree.Root = n
- templ = tt
- } else {
- prototype := t.getPrototypeHTML(prototypeCloneIDDefer)
- tt, err := prototype.New(name).Parse("")
- if err != nil {
- return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err)
- }
- tt.Tree.Root = n
- templ = tt
- }
-
- dts := newTemplateState(owner, templ, templateInfo{name: name}, nil)
- t.templates[name] = dts
-
- return nil
-}
-
-func (t *templateNamespace) parse(info templateInfo) (*templateState, error) {
- t.mu.Lock()
- defer t.mu.Unlock()
-
- if info.isText {
- prototype := t.prototypeText
-
- templ, err := prototype.New(info.name).Parse(info.template)
- if err != nil {
- return nil, err
- }
-
- ts := newTemplateState(nil, templ, info, nil)
-
- t.templates[info.name] = ts
-
- return ts, nil
- }
-
- prototype := t.prototypeHTML
-
- templ, err := prototype.New(info.name).Parse(info.template)
- if err != nil {
- return nil, err
- }
-
- ts := newTemplateState(nil, templ, info, nil)
-
- t.templates[info.name] = ts
-
- return ts, nil
-}
-
-var _ tpl.IsInternalTemplateProvider = (*templateState)(nil)
-
-type templateState struct {
- tpl.Template
-
- // Set for deferred templates.
- owner *templateState
-
- typ templateType
- parseInfo tpl.ParseInfo
- id identity.Identity
-
- info templateInfo
- baseInfo templateInfo // Set when a base template is used.
-}
-
-func (t *templateState) IsInternalTemplate() bool {
- return t.info.isEmbedded
-}
-
-func (t *templateState) GetIdentity() identity.Identity {
- return t.id
-}
-
-func (t *templateState) ParseInfo() tpl.ParseInfo {
- return t.parseInfo
-}
-
-func (t *templateState) isText() bool {
- return isText(t.Template)
-}
-
-func (t *templateState) String() string {
- return t.Name()
-}
-
-func isText(templ tpl.Template) bool {
- _, isText := templ.(*texttemplate.Template)
- return isText
-}
-
-type templateStateMap struct {
- mu sync.RWMutex
- templates map[string]*templateState
-}
-
-type textTemplateWrapperWithLock struct {
- *sync.RWMutex
- *texttemplate.Template
-}
-
-func (t *textTemplateWrapperWithLock) Lookup(name string) (tpl.Template, bool) {
- t.RLock()
- templ := t.Template.Lookup(name)
- t.RUnlock()
- if templ == nil {
- return nil, false
- }
- return &textTemplateWrapperWithLock{
- RWMutex: t.RWMutex,
- Template: templ,
- }, true
-}
-
-func (t *textTemplateWrapperWithLock) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
- panic("not supported")
-}
-
-func (t *textTemplateWrapperWithLock) LookupVariants(name string) []tpl.Template {
- panic("not supported")
-}
-
-func (t *textTemplateWrapperWithLock) Parse(name, tpl string) (tpl.Template, error) {
- t.Lock()
- defer t.Unlock()
- return t.Template.New(name).Parse(tpl)
-}
-
-func isBackupFile(path string) bool {
- return path[len(path)-1] == '~'
-}
-
-func isBaseTemplatePath(path string) bool {
- return strings.Contains(filepath.Base(path), baseFileBase)
-}
-
-func isDotFile(path string) bool {
- return filepath.Base(path)[0] == '.'
-}
-
-func removeLeadingBOM(s string) string {
- const bom = '\ufeff'
-
- for i, r := range s {
- if i == 0 && r != bom {
- return s
- }
- if i > 0 {
- return s[i:]
- }
- }
-
- return s
-}
-
-// resolves _internal/shortcodes/param.html => param.html etc.
-func templateBaseName(typ templateType, name string) string {
- name = strings.TrimPrefix(name, internalPathPrefix)
- switch typ {
- case templateShortcode:
- return strings.TrimPrefix(name, shortcodesPathPrefix)
- default:
- panic("not implemented")
- }
-}
-
-func unwrap(templ tpl.Template) tpl.Template {
- if ts, ok := templ.(*templateState); ok {
- return ts.Template
- }
- return templ
-}
-
-func templates(in tpl.Template) []tpl.Template {
- var templs []tpl.Template
- in = unwrap(in)
- if textt, ok := in.(*texttemplate.Template); ok {
- for _, t := range textt.Templates() {
- templs = append(templs, t)
- }
- }
-
- if htmlt, ok := in.(*htmltemplate.Template); ok {
- for _, t := range htmlt.Templates() {
- templs = append(templs, t)
- }
- }
-
- return templs
-}
(DIR) diff --git a/tpl/tplimpl/templateFuncster.go b/tpl/tplimpl/templateFuncster.go
@@ -1,14 +0,0 @@
-// Copyright 2019 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
(DIR) diff --git a/tpl/tplimpl/templateProvider.go b/tpl/tplimpl/templateProvider.go
@@ -1,51 +0,0 @@
-// Copyright 2017-present 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
-
-import (
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/tpl"
-)
-
-// TemplateProvider manages templates.
-type TemplateProvider struct{}
-
-// DefaultTemplateProvider is a globally available TemplateProvider.
-var DefaultTemplateProvider *TemplateProvider
-
-// Update updates the Hugo Template System in the provided Deps
-// with all the additional features, templates & functions.
-func (*TemplateProvider) NewResource(dst *deps.Deps) error {
- handlers, err := newTemplateHandlers(dst)
- if err != nil {
- return err
- }
- dst.SetTempl(handlers)
- return nil
-}
-
-// Clone clones.
-func (*TemplateProvider) CloneResource(dst, src *deps.Deps) error {
- t := src.Tmpl().(*templateExec)
- c := t.Clone(dst)
- funcMap := make(map[string]any)
- for k, v := range c.funcs {
- funcMap[k] = v.Interface()
- }
- dst.SetTempl(&tpl.TemplateHandlers{
- Tmpl: c,
- TxtTmpl: newStandaloneTextTemplate(funcMap),
- })
- return nil
-}
(DIR) diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go
@@ -1,381 +0,0 @@
-// 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 tplimpl
-
-import (
- "errors"
- "fmt"
- "strings"
-
- htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
- texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-
- "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
-
- "github.com/gohugoio/hugo/common/hashing"
- "github.com/gohugoio/hugo/common/maps"
- "github.com/gohugoio/hugo/tpl"
- "github.com/mitchellh/mapstructure"
- "slices"
-)
-
-type templateType int
-
-const (
- templateUndefined templateType = iota
- templateShortcode
- templatePartial
-)
-
-type templateContext struct {
- visited map[string]bool
- templateNotFound map[string]bool
- deferNodes map[string]*parse.ListNode
- lookupFn func(name string) *templateState
-
- // The last error encountered.
- err error
-
- // Set when we're done checking for config header.
- configChecked bool
-
- t *templateState
-
- // Store away the return node in partials.
- returnNode *parse.CommandNode
-}
-
-func (c templateContext) getIfNotVisited(name string) *templateState {
- if c.visited[name] {
- return nil
- }
- c.visited[name] = true
- templ := c.lookupFn(name)
- if templ == nil {
- // This may be a inline template defined outside of this file
- // and not yet parsed. Unusual, but it happens.
- // Store the name to try again later.
- c.templateNotFound[name] = true
- }
-
- return templ
-}
-
-func newTemplateContext(
- t *templateState,
- lookupFn func(name string) *templateState,
-) *templateContext {
- return &templateContext{
- t: t,
- lookupFn: lookupFn,
- visited: make(map[string]bool),
- templateNotFound: make(map[string]bool),
- deferNodes: make(map[string]*parse.ListNode),
- }
-}
-
-func applyTemplateTransformers(
- t *templateState,
- lookupFn func(name string) *templateState,
-) (*templateContext, error) {
- if t == nil {
- return nil, errors.New("expected template, but none provided")
- }
-
- c := newTemplateContext(t, lookupFn)
- tree := getParseTree(t.Template)
-
- _, err := c.applyTransformations(tree.Root)
-
- if err == nil && c.returnNode != nil {
- // This is a partial with a return statement.
- c.t.parseInfo.HasReturn = true
- tree.Root = c.wrapInPartialReturnWrapper(tree.Root)
- }
-
- return c, err
-}
-
-func getParseTree(templ tpl.Template) *parse.Tree {
- templ = unwrap(templ)
- if text, ok := templ.(*texttemplate.Template); ok {
- return text.Tree
- }
- return templ.(*htmltemplate.Template).Tree
-}
-
-const (
- // We parse this template and modify the nodes in order to assign
- // the return value of a partial to a contextWrapper via Set. We use
- // "range" over a one-element slice so we can shift dot to the
- // partial's argument, Arg, while allowing Arg to be falsy.
- partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ range (slice .Arg) }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
-
- doDeferTempl = `{{ doDefer ("PLACEHOLDER1") ("PLACEHOLDER2") }}`
-)
-
-var (
- partialReturnWrapper *parse.ListNode
- doDefer *parse.ListNode
-)
-
-func init() {
- templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
- if err != nil {
- panic(err)
- }
- partialReturnWrapper = templ.Tree.Root
-
- templ, err = texttemplate.New("").Funcs(texttemplate.FuncMap{"doDefer": func(string, string) string { return "" }}).Parse(doDeferTempl)
- if err != nil {
- panic(err)
- }
- doDefer = templ.Tree.Root
-}
-
-// wrapInPartialReturnWrapper copies and modifies the parsed nodes of a
-// predefined partial return wrapper to insert those of a user-defined partial.
-func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
- wrapper := partialReturnWrapper.CopyList()
- rangeNode := wrapper.Nodes[2].(*parse.RangeNode)
- retn := rangeNode.List.Nodes[0]
- setCmd := retn.(*parse.ActionNode).Pipe.Cmds[0]
- setPipe := setCmd.Args[1].(*parse.PipeNode)
- // Replace PLACEHOLDER with the real return value.
- // Note that this is a PipeNode, so it will be wrapped in parens.
- setPipe.Cmds = []*parse.CommandNode{c.returnNode}
- rangeNode.List.Nodes = append(n.Nodes, retn)
-
- return wrapper
-}
-
-// applyTransformations do 2 things:
-// 1) Parses partial return statement.
-// 2) Tracks template (partial) dependencies and some other info.
-func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
- switch x := n.(type) {
- case *parse.ListNode:
- if x != nil {
- c.applyTransformationsToNodes(x.Nodes...)
- }
- case *parse.ActionNode:
- c.applyTransformationsToNodes(x.Pipe)
- case *parse.IfNode:
- c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
- case *parse.WithNode:
- c.handleDefer(x)
- c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
- case *parse.RangeNode:
- c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
- case *parse.TemplateNode:
- subTempl := c.getIfNotVisited(x.Name)
- if subTempl != nil {
- c.applyTransformationsToNodes(getParseTree(subTempl.Template).Root)
- }
- case *parse.PipeNode:
- c.collectConfig(x)
- for i, cmd := range x.Cmds {
- keep, _ := c.applyTransformations(cmd)
- if !keep {
- x.Cmds = slices.Delete(x.Cmds, i, i+1)
- }
- }
-
- case *parse.CommandNode:
- c.collectInner(x)
- keep := c.collectReturnNode(x)
-
- for _, elem := range x.Args {
- switch an := elem.(type) {
- case *parse.PipeNode:
- c.applyTransformations(an)
- }
- }
- return keep, c.err
- }
-
- return true, c.err
-}
-
-func (c *templateContext) handleDefer(withNode *parse.WithNode) {
- if len(withNode.Pipe.Cmds) != 1 {
- return
- }
- cmd := withNode.Pipe.Cmds[0]
- if len(cmd.Args) != 1 {
- return
- }
- idArg := cmd.Args[0]
-
- p, ok := idArg.(*parse.PipeNode)
- if !ok {
- return
- }
-
- if len(p.Cmds) != 1 {
- return
- }
-
- cmd = p.Cmds[0]
-
- if len(cmd.Args) != 2 {
- return
- }
-
- idArg = cmd.Args[0]
-
- id, ok := idArg.(*parse.ChainNode)
- if !ok || len(id.Field) != 1 || id.Field[0] != "Defer" {
- return
- }
- if id2, ok := id.Node.(*parse.IdentifierNode); !ok || id2.Ident != "templates" {
- return
- }
-
- deferArg := cmd.Args[1]
- cmd.Args = []parse.Node{idArg}
-
- l := doDefer.CopyList()
- n := l.Nodes[0].(*parse.ActionNode)
-
- inner := withNode.List.CopyList()
- s := inner.String()
- if strings.Contains(s, "resources.PostProcess") {
- c.err = errors.New("resources.PostProcess cannot be used in a deferred template")
- return
- }
- innerHash := hashing.XxHashFromStringHexEncoded(s)
- deferredID := tpl.HugoDeferredTemplatePrefix + innerHash
-
- c.deferNodes[deferredID] = inner
- withNode.List = l
-
- n.Pipe.Cmds[0].Args[1].(*parse.PipeNode).Cmds[0].Args[0].(*parse.StringNode).Text = deferredID
- n.Pipe.Cmds[0].Args[2] = deferArg
-}
-
-func (c *templateContext) applyTransformationsToNodes(nodes ...parse.Node) {
- for _, node := range nodes {
- c.applyTransformations(node)
- }
-}
-
-func (c *templateContext) hasIdent(idents []string, ident string) bool {
- return slices.Contains(idents, ident)
-}
-
-// collectConfig collects and parses any leading template config variable declaration.
-// This will be the first PipeNode in the template, and will be a variable declaration
-// on the form:
-//
-// {{ $_hugo_config:= `{ "version": 1 }` }}
-func (c *templateContext) collectConfig(n *parse.PipeNode) {
- if c.t.typ != templateShortcode {
- return
- }
- if c.configChecked {
- return
- }
- c.configChecked = true
-
- if len(n.Decl) != 1 || len(n.Cmds) != 1 {
- // This cannot be a config declaration
- return
- }
-
- v := n.Decl[0]
-
- if len(v.Ident) == 0 || v.Ident[0] != "$_hugo_config" {
- return
- }
-
- cmd := n.Cmds[0]
-
- if len(cmd.Args) == 0 {
- return
- }
-
- if s, ok := cmd.Args[0].(*parse.StringNode); ok {
- errMsg := "failed to decode $_hugo_config in template: %w"
- m, err := maps.ToStringMapE(s.Text)
- if err != nil {
- c.err = fmt.Errorf(errMsg, err)
- return
- }
- if err := mapstructure.WeakDecode(m, &c.t.parseInfo.Config); err != nil {
- c.err = fmt.Errorf(errMsg, err)
- }
- }
-}
-
-// collectInner determines if the given CommandNode represents a
-// shortcode call to its .Inner.
-func (c *templateContext) collectInner(n *parse.CommandNode) {
- if c.t.typ != templateShortcode {
- return
- }
- if c.t.parseInfo.IsInner || len(n.Args) == 0 {
- return
- }
-
- for _, arg := range n.Args {
- var idents []string
- switch nt := arg.(type) {
- case *parse.FieldNode:
- idents = nt.Ident
- case *parse.VariableNode:
- idents = nt.Ident
- }
-
- if c.hasIdent(idents, "Inner") || c.hasIdent(idents, "InnerDeindent") {
- c.t.parseInfo.IsInner = true
- break
- }
- }
-}
-
-func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
- if c.t.typ != templatePartial || c.returnNode != nil {
- return true
- }
-
- if len(n.Args) < 2 {
- return true
- }
-
- ident, ok := n.Args[0].(*parse.IdentifierNode)
- if !ok || ident.Ident != "return" {
- return true
- }
-
- c.returnNode = n
- // Remove the "return" identifiers
- c.returnNode.Args = c.returnNode.Args[1:]
-
- return false
-}
-
-func findTemplateIn(name string, in tpl.Template) (tpl.Template, bool) {
- in = unwrap(in)
- if text, ok := in.(*texttemplate.Template); ok {
- if templ := text.Lookup(name); templ != nil {
- return templ, true
- }
- return nil, false
- }
- if templ := in.(*htmltemplate.Template).Lookup(name); templ != nil {
- return templ, true
- }
- return nil, false
-}
(DIR) diff --git a/tpl/tplimpl/template_ast_transformers_test.go b/tpl/tplimpl/template_ast_transformers_test.go
@@ -1,161 +0,0 @@
-// Copyright 2019 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
-
-import (
- "testing"
-
- template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/tpl"
-)
-
-// Issue #2927
-func TestTransformRecursiveTemplate(t *testing.T) {
- c := qt.New(t)
-
- recursive := `
-{{ define "menu-nodes" }}
-{{ template "menu-node" }}
-{{ end }}
-{{ define "menu-node" }}
-{{ template "menu-node" }}
-{{ end }}
-{{ template "menu-nodes" }}
-`
-
- templ, err := template.New("foo").Parse(recursive)
- c.Assert(err, qt.IsNil)
- ts := newTestTemplate(templ)
-
- ctx := newTemplateContext(
- ts,
- newTestTemplateLookup(ts),
- )
- ctx.applyTransformations(templ.Tree.Root)
-}
-
-func newTestTemplate(templ tpl.Template) *templateState {
- return newTemplateState(nil,
- templ,
- templateInfo{
- name: templ.Name(),
- },
- nil,
- )
-}
-
-func newTestTemplateLookup(in *templateState) func(name string) *templateState {
- m := make(map[string]*templateState)
- return func(name string) *templateState {
- if in.Name() == name {
- return in
- }
-
- if ts, found := m[name]; found {
- return ts
- }
-
- if templ, found := findTemplateIn(name, in); found {
- ts := newTestTemplate(templ)
- m[name] = ts
- return ts
- }
-
- return nil
- }
-}
-
-func TestCollectInfo(t *testing.T) {
- configStr := `{ "version": 42 }`
-
- tests := []struct {
- name string
- tplString string
- expected tpl.ParseInfo
- }{
- {"Basic Inner", `{{ .Inner }}`, tpl.ParseInfo{IsInner: true, Config: tpl.DefaultParseConfig}},
- {"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.ParseInfo{Config: tpl.ParseConfig{Version: 42}}},
- }
-
- echo := func(in any) any {
- return in
- }
-
- funcs := template.FuncMap{
- "highlight": echo,
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- c := qt.New(t)
-
- templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
- c.Assert(err, qt.IsNil)
- ts := newTestTemplate(templ)
- ts.typ = templateShortcode
- ctx := newTemplateContext(
- ts,
- newTestTemplateLookup(ts),
- )
- ctx.applyTransformations(templ.Tree.Root)
- c.Assert(ctx.t.parseInfo, qt.DeepEquals, test.expected)
- })
- }
-}
-
-func TestPartialReturn(t *testing.T) {
- tests := []struct {
- name string
- tplString string
- expected bool
- }{
- {"Basic", `
-{{ $a := "Hugo Rocks!" }}
-{{ return $a }}
-`, true},
- {"Expression", `
-{{ return add 32 }}
-`, true},
- }
-
- echo := func(in any) any {
- return in
- }
-
- funcs := template.FuncMap{
- "return": echo,
- "add": echo,
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- c := qt.New(t)
-
- templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
- c.Assert(err, qt.IsNil)
- ts := newTestTemplate(templ)
- ctx := newTemplateContext(
- ts,
- newTestTemplateLookup(ts),
- )
-
- _, err = ctx.applyTransformations(templ.Tree.Root)
-
- // Just check that it doesn't fail in this test. We have functional tests
- // in hugoblib.
- c.Assert(err, qt.IsNil)
- })
- }
-}
(DIR) diff --git a/tpl/tplimpl/template_errors.go b/tpl/tplimpl/template_errors.go
@@ -1,64 +0,0 @@
-// Copyright 2018 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
-
-import (
- "fmt"
-
- "github.com/gohugoio/hugo/common/herrors"
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/identity"
-)
-
-var _ identity.Identity = (*templateInfo)(nil)
-
-type templateInfo struct {
- name string
- template string
- isText bool // HTML or plain text template.
- isEmbedded bool
-
- meta *hugofs.FileMeta
-}
-
-func (t templateInfo) IdentifierBase() string {
- return t.name
-}
-
-func (t templateInfo) Name() string {
- return t.name
-}
-
-func (t templateInfo) Filename() string {
- return t.meta.Filename
-}
-
-func (t templateInfo) IsZero() bool {
- return t.name == ""
-}
-
-func (t templateInfo) resolveType() templateType {
- return resolveTemplateType(t.name)
-}
-
-func (info templateInfo) errWithFileContext(what string, err error) error {
- err = fmt.Errorf(what+": %w", err)
- fe := herrors.NewFileErrorFromName(err, info.meta.Filename)
- f, err := info.meta.Open()
- if err != nil {
- return err
- }
- defer f.Close()
- return fe.UpdateContent(f, nil)
-}
(DIR) diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go
@@ -1,4 +1,4 @@
-// Copyright 2017-present The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Portions Copyright The Go Authors.
@@ -25,46 +25,7 @@ import (
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/tpl"
- template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-
- "github.com/gohugoio/hugo/deps"
-
- "github.com/gohugoio/hugo/tpl/internal"
-
- // Init the namespaces
- _ "github.com/gohugoio/hugo/tpl/cast"
- _ "github.com/gohugoio/hugo/tpl/collections"
- _ "github.com/gohugoio/hugo/tpl/compare"
- _ "github.com/gohugoio/hugo/tpl/crypto"
- _ "github.com/gohugoio/hugo/tpl/css"
- _ "github.com/gohugoio/hugo/tpl/data"
- _ "github.com/gohugoio/hugo/tpl/debug"
- _ "github.com/gohugoio/hugo/tpl/diagrams"
- _ "github.com/gohugoio/hugo/tpl/encoding"
- _ "github.com/gohugoio/hugo/tpl/fmt"
- _ "github.com/gohugoio/hugo/tpl/hash"
- _ "github.com/gohugoio/hugo/tpl/hugo"
- _ "github.com/gohugoio/hugo/tpl/images"
- _ "github.com/gohugoio/hugo/tpl/inflect"
- _ "github.com/gohugoio/hugo/tpl/js"
- _ "github.com/gohugoio/hugo/tpl/lang"
- _ "github.com/gohugoio/hugo/tpl/math"
- _ "github.com/gohugoio/hugo/tpl/openapi/openapi3"
- _ "github.com/gohugoio/hugo/tpl/os"
- _ "github.com/gohugoio/hugo/tpl/page"
- _ "github.com/gohugoio/hugo/tpl/partials"
- _ "github.com/gohugoio/hugo/tpl/path"
- _ "github.com/gohugoio/hugo/tpl/reflect"
- _ "github.com/gohugoio/hugo/tpl/resources"
- _ "github.com/gohugoio/hugo/tpl/safe"
- _ "github.com/gohugoio/hugo/tpl/site"
- _ "github.com/gohugoio/hugo/tpl/strings"
- _ "github.com/gohugoio/hugo/tpl/templates"
- _ "github.com/gohugoio/hugo/tpl/time"
- _ "github.com/gohugoio/hugo/tpl/transform"
- _ "github.com/gohugoio/hugo/tpl/urls"
- maps0 "maps"
)
var (
@@ -212,89 +173,3 @@ func (t *templateExecHelper) trackDependencies(ctx context.Context, tmpl texttem
return ctx
}
-
-func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflect.Value) {
- funcs := createFuncMap(d)
- funcsv := make(map[string]reflect.Value)
-
- for k, v := range funcs {
- vv := reflect.ValueOf(v)
- funcsv[k] = vv
- }
-
- // Duplicate Go's internal funcs here for faster lookups.
- for k, v := range template.GoFuncs {
- if _, exists := funcsv[k]; !exists {
- vv, ok := v.(reflect.Value)
- if !ok {
- vv = reflect.ValueOf(v)
- }
- funcsv[k] = vv
- }
- }
-
- for k, v := range texttemplate.GoFuncs {
- if _, exists := funcsv[k]; !exists {
- funcsv[k] = v
- }
- }
-
- exeHelper := &templateExecHelper{
- watching: d.Conf.Watching(),
- funcs: funcsv,
- site: reflect.ValueOf(d.Site),
- siteParams: reflect.ValueOf(d.Site.Params()),
- }
-
- return texttemplate.NewExecuter(
- exeHelper,
- ), funcsv
-}
-
-func createFuncMap(d *deps.Deps) map[string]any {
- if d.TmplFuncMap != nil {
- return d.TmplFuncMap
- }
- funcMap := template.FuncMap{}
-
- nsMap := make(map[string]any)
- var onCreated []func(namespaces map[string]any)
-
- // Merge the namespace funcs
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns := nsf(d)
- if _, exists := funcMap[ns.Name]; exists {
- panic(ns.Name + " is a duplicate template func")
- }
- funcMap[ns.Name] = ns.Context
- contextV, err := ns.Context(context.Background())
- if err != nil {
- panic(err)
- }
- nsMap[ns.Name] = contextV
- for _, mm := range ns.MethodMappings {
- for _, alias := range mm.Aliases {
- if _, exists := funcMap[alias]; exists {
- panic(alias + " is a duplicate template func")
- }
- funcMap[alias] = mm.Method
- }
- }
-
- if ns.OnCreated != nil {
- onCreated = append(onCreated, ns.OnCreated)
- }
- }
-
- for _, f := range onCreated {
- f(nsMap)
- }
-
- if d.OverloadedTemplateFuncs != nil {
- maps0.Copy(funcMap, d.OverloadedTemplateFuncs)
- }
-
- d.TmplFuncMap = funcMap
-
- return d.TmplFuncMap
-}
(DIR) diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// 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.
(DIR) diff --git a/tpl/tplimpl/template_info.go b/tpl/tplimpl/template_info.go
@@ -0,0 +1,46 @@
+// 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
+
+// Increments on breaking changes.
+const TemplateVersion = 2
+
+// ParseInfo holds information about a parsed ntemplate.
+type ParseInfo struct {
+ // Set for shortcode templates with any {{ .Inner }}
+ IsInner bool
+
+ // Set for partials with a return statement.
+ HasReturn bool
+
+ // Config extracted from template.
+ Config ParseConfig
+}
+
+func (info ParseInfo) IsZero() bool {
+ return info.Config.Version == 0
+}
+
+// ParseConfig holds configuration extracted from the template.
+type ParseConfig struct {
+ Version int
+}
+
+var defaultParseConfig = ParseConfig{
+ Version: TemplateVersion,
+}
+
+var defaultParseInfo = ParseInfo{
+ Config: defaultParseConfig,
+}
(DIR) diff --git a/tpl/tplimpl/template_test.go b/tpl/tplimpl/template_test.go
@@ -1,40 +0,0 @@
-// Copyright 2019 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
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
-)
-
-func TestNeedsBaseTemplate(t *testing.T) {
- c := qt.New(t)
-
- c.Assert(needsBaseTemplate(`{{ define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`{{define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`{{- define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`{{-define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`
-
- {{-define "main" }}
-
- `), qt.Equals, true)
- c.Assert(needsBaseTemplate(` {{ define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`
- {{ define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(` A {{ define "main" }}`), qt.Equals, false)
- c.Assert(needsBaseTemplate(` {{ printf "foo" }}`), qt.Equals, false)
- c.Assert(needsBaseTemplate(`{{/* comment */}} {{ define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(` {{/* comment */}} A {{ define "main" }}`), qt.Equals, false)
-}
(DIR) diff --git a/tpl/tplimpl/templatedescriptor.go b/tpl/tplimpl/templatedescriptor.go
@@ -0,0 +1,225 @@
+// 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
+
+import (
+ "github.com/gohugoio/hugo/resources/kinds"
+)
+
+const baseNameBaseof = "baseof"
+
+// This is used both as a key and in lookups.
+type TemplateDescriptor struct {
+ // Group 1.
+ Kind string // page, home, section, taxonomy, term (and only those)
+ Layout string // list, single, baseof, mycustomlayout.
+
+ // Group 2.
+ OutputFormat string // rss, csv ...
+ MediaType string // text/html, text/plain, ...
+ Lang string // en, nn, fr, ...
+
+ Variant1 string // contextual variant, e.g. "link" in render hooks."
+ Variant2 string // contextual variant, e.g. "id" in render.
+
+ // Misc.
+ LayoutMustMatch bool // If set, we only look for the exact layout.
+ IsPlainText bool // Whether this is a plain text template.
+}
+
+func (d *TemplateDescriptor) normalizeFromFile() {
+ // fmt.Println("normalizeFromFile", "kind:", d.Kind, "layout:", d.Layout, "of:", d.OutputFormat)
+
+ if d.Layout == d.OutputFormat {
+ d.Layout = ""
+ }
+
+ if d.Kind == kinds.KindTemporary {
+ d.Kind = ""
+ }
+
+ if d.Layout == d.Kind {
+ d.Layout = ""
+ }
+}
+
+type descriptorHandler struct {
+ opts StoreOptions
+}
+
+// Note that this in this setup is usually a descriptor constructed from a page,
+// so we want to find the best match for that page.
+func (s descriptorHandler) compareDescriptors(category Category, this, other TemplateDescriptor) weight {
+ if this.LayoutMustMatch && this.Layout != other.Layout {
+ return weightNoMatch
+ }
+
+ w := this.doCompare(category, other)
+ if w.w1 <= 0 {
+ if category == CategoryMarkup && (this.Variant1 == other.Variant1) && (this.Variant2 == other.Variant2 || this.Variant2 != "" && other.Variant2 == "") {
+ // See issue 13242.
+ if this.OutputFormat != other.OutputFormat && this.OutputFormat == s.opts.DefaultOutputFormat {
+ return w
+ }
+
+ w.w1 = 1
+ return w
+ }
+ }
+
+ return w
+}
+
+//lint:ignore ST1006 this vs other makes it easier to reason about.
+func (this TemplateDescriptor) doCompare(category Category, other TemplateDescriptor) weight {
+ w := weightNoMatch
+
+ // HTML in plain text is OK, but not the other way around.
+ if other.IsPlainText && !this.IsPlainText {
+ return w
+ }
+ if other.Kind != "" && other.Kind != this.Kind {
+ return w
+ }
+ if other.Layout != "" && other.Layout != layoutAll && other.Layout != this.Layout {
+ if isLayoutCustom(this.Layout) {
+ if this.Kind == "" {
+ this.Layout = ""
+ } else if this.Kind == kinds.KindPage {
+ this.Layout = layoutSingle
+ } else {
+ this.Layout = layoutList
+ }
+ }
+
+ // Test again.
+ if other.Layout != this.Layout {
+ return w
+ }
+ }
+ if other.Lang != "" && other.Lang != this.Lang {
+ return w
+ }
+
+ if other.OutputFormat != "" && other.OutputFormat != this.OutputFormat {
+ if this.MediaType != other.MediaType {
+ return w
+ }
+
+ // We want e.g. home page in amp output format (media type text/html) to
+ // find a template even if one isn't specified for that output format,
+ // when one exist for the html output format (same media type).
+ if category != CategoryBaseof && (this.Kind == "" || (this.Kind != other.Kind && this.Layout != other.Layout)) {
+ return w
+ }
+
+ // Continue.
+ }
+
+ // One example of variant1 and 2 is for render codeblocks:
+ // variant1=codeblock, variant2=go (language).
+ if other.Variant1 != "" && other.Variant1 != this.Variant1 {
+ return w
+ }
+
+ // If both are set and different, no match.
+ if other.Variant2 != "" && this.Variant2 != "" && other.Variant2 != this.Variant2 {
+ return w
+ }
+
+ const (
+ weightKind = 3 // page, home, section, taxonomy, term (and only those)
+ weightcustomLayout = 4 // custom layout (mylayout, set in e.g. front matter)
+ weightLayout = 2 // standard layouts (single,list,all)
+ weightOutputFormat = 2 // a configured output format (e.g. rss, html, json)
+ weightMediaType = 1 // a configured media type (e.g. text/html, text/plain)
+ weightLang = 1 // a configured language (e.g. en, nn, fr, ...)
+ weightVariant1 = 4 // currently used for render hooks, e.g. "link", "image"
+ weightVariant2 = 2 // currently used for render hooks, e.g. the language "go" in code blocks.
+
+ // We will use the values for group 2 and 3
+ // if the distance up to the template is shorter than
+ // the one we're comparing with.
+ // E.g for a page in /posts/mypage.md with the
+ // two templates /layouts/posts/single.html and /layouts/page.html,
+ // the first one is the best match even if the second one
+ // has a higher w1 value.
+ weight2Group1 = 1 // kind, standardl layout (single,list,all)
+ weight2Group2 = 2 // custom layout (mylayout)
+
+ weight3 = 1 // for media type, lang, output format.
+ )
+
+ // Now we now know that the other descriptor is a subset of this.
+ // Now calculate the weights.
+ w.w1++
+
+ if other.Kind != "" && other.Kind == this.Kind {
+ w.w1 += weightKind
+ w.w2 = weight2Group1
+ }
+
+ if other.Layout != "" && other.Layout == this.Layout || other.Layout == layoutAll {
+ if isLayoutCustom(this.Layout) {
+ w.w1 += weightcustomLayout
+ w.w2 = weight2Group2
+ } else {
+ w.w1 += weightLayout
+ w.w2 = weight2Group1
+ }
+ }
+
+ if other.Lang != "" && other.Lang == this.Lang {
+ w.w1 += weightLang
+ w.w3 += weight3
+ }
+
+ if other.OutputFormat != "" && other.OutputFormat == this.OutputFormat {
+ w.w1 += weightOutputFormat
+ w.w3 += weight3
+ }
+
+ if other.MediaType != "" && other.MediaType == this.MediaType {
+ w.w1 += weightMediaType
+ w.w3 += weight3
+ }
+
+ if other.Variant1 != "" && other.Variant1 == this.Variant1 {
+ w.w1 += weightVariant1
+ }
+
+ if other.Variant2 != "" && other.Variant2 == this.Variant2 {
+ w.w1 += weightVariant2
+ }
+ if other.Variant2 != "" && this.Variant2 == "" {
+ w.w1--
+ }
+
+ return w
+}
+
+func (d TemplateDescriptor) IsZero() bool {
+ return d == TemplateDescriptor{}
+}
+
+//lint:ignore ST1006 this vs other makes it easier to reason about.
+func (this TemplateDescriptor) isKindInLayout(layout string) bool {
+ if this.Kind == "" {
+ return true
+ }
+ if this.Kind != kinds.KindPage {
+ return layout != layoutSingle
+ }
+ return layout != layoutList
+}
(DIR) diff --git a/tpl/tplimpl/templatedescriptor_test.go b/tpl/tplimpl/templatedescriptor_test.go
@@ -0,0 +1,104 @@
+package tplimpl
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/resources/kinds"
+)
+
+func TestTemplateDescriptorCompare(t *testing.T) {
+ c := qt.New(t)
+
+ dh := descriptorHandler{
+ opts: StoreOptions{
+ OutputFormats: output.DefaultFormats,
+ DefaultOutputFormat: "html",
+ },
+ }
+
+ less := func(category Category, this, other1, other2 TemplateDescriptor) {
+ c.Helper()
+ result1 := dh.compareDescriptors(category, this, other1)
+ result2 := dh.compareDescriptors(category, this, other2)
+ c.Assert(result1.w1 < result2.w1, qt.IsTrue, qt.Commentf("%d < %d", result1, result2))
+ }
+
+ check := func(category Category, this, other TemplateDescriptor, less bool) {
+ c.Helper()
+ result := dh.compareDescriptors(category, this, other)
+ if less {
+ c.Assert(result.w1 < 0, qt.IsTrue, qt.Commentf("%d", result))
+ } else {
+ c.Assert(result.w1 >= 0, qt.IsTrue, qt.Commentf("%d", result))
+ }
+ }
+
+ check(
+
+ CategoryBaseof,
+ TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "404", MediaType: "text/html"},
+ TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "html", MediaType: "text/html"},
+ false,
+ )
+
+ check(
+ CategoryLayout,
+ TemplateDescriptor{Kind: "", Lang: "en", OutputFormat: "404", MediaType: "text/html"},
+ TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "alias", MediaType: "text/html"},
+ true,
+ )
+
+ less(
+ CategoryLayout,
+ TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "html"},
+ TemplateDescriptor{Layout: "list", OutputFormat: "html"},
+ TemplateDescriptor{Kind: kinds.KindHome, OutputFormat: "html"},
+ )
+
+ check(
+ CategoryLayout,
+ TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "html", MediaType: "text/html"},
+ TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "myformat", MediaType: "text/html"},
+ false,
+ )
+}
+
+// INFO timer: name resolveTemplate count 779 duration 5.482274ms average 7.037µs median 4µs
+func BenchmarkCompareDescriptors(b *testing.B) {
+ dh := descriptorHandler{
+ opts: StoreOptions{
+ OutputFormats: output.DefaultFormats,
+ DefaultOutputFormat: "html",
+ },
+ }
+
+ pairs := []struct {
+ d1, d2 TemplateDescriptor
+ }{
+ {
+ TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "404", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ },
+ {
+ TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ TemplateDescriptor{Kind: "", Layout: "list", OutputFormat: "", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ },
+ {
+ TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "alias", MediaType: "text/html", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ },
+ {
+ TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ TemplateDescriptor{Kind: "", Layout: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "nn", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ },
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ for _, pair := range pairs {
+ _ = dh.compareDescriptors(CategoryLayout, pair.d1, pair.d2)
+ }
+ }
+}
(DIR) diff --git a/tpl/tplimpl/templates.go b/tpl/tplimpl/templates.go
@@ -0,0 +1,331 @@
+package tplimpl
+
+import (
+ "io"
+ "regexp"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/gohugoio/hugo/common/paths"
+ "github.com/gohugoio/hugo/tpl"
+ htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
+ texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
+)
+
+func (t *templateNamespace) readTemplateInto(templ *TemplInfo) error {
+ if err := func() error {
+ meta := templ.Fi.Meta()
+ f, err := meta.Open()
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ b, err := io.ReadAll(f)
+ if err != nil {
+ return err
+ }
+ templ.Content = removeLeadingBOM(string(b))
+ if !templ.NoBaseOf {
+ templ.NoBaseOf = !needsBaseTemplate(templ.Content)
+ }
+ return nil
+ }(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// The tweet and twitter shortcodes were deprecated in favor of the x shortcode
+// in v0.141.0. We can remove these aliases in v0.155.0 or later.
+var embeddedTemplatesAliases = map[string][]string{
+ "_shortcodes/twitter.html": {"_shortcodes/tweet.html"},
+}
+
+func (t *templateNamespace) parseTemplate(ti *TemplInfo) error {
+ if !ti.NoBaseOf || ti.Category == CategoryBaseof {
+ // Delay parsing until we have the base template.
+ return nil
+ }
+ pi := ti.PathInfo
+ name := pi.PathNoLeadingSlash()
+ if ti.isLegacyMapped {
+ // When mapping the old taxonomy structure to the new one, we may map the same path to multiple templates per kind.
+ // Append the kind here to make the name unique.
+ name += ("-" + ti.D.Kind)
+ }
+
+ var (
+ templ tpl.Template
+ err error
+ )
+
+ if ti.D.IsPlainText {
+ prototype := t.parseText
+ templ, err = prototype.New(name).Parse(ti.Content)
+ if err != nil {
+ return err
+ }
+ } else {
+ prototype := t.parseHTML
+ templ, err = prototype.New(name).Parse(ti.Content)
+ if err != nil {
+ return err
+ }
+
+ if ti.SubCategory == SubCategoryEmbedded {
+ // In Hugo 0.146.0 we moved the internal templates around.
+ // For the "_internal/twitter_cards.html" style templates, they
+ // were moved to the _partials directory.
+ // But we need to make them accessible from the old path for a while.
+ if pi.Type() == paths.TypePartial {
+ aliasName := strings.TrimPrefix(name, "_partials/")
+ aliasName = "_internal/" + aliasName
+ _, err = prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree)
+ if err != nil {
+ return err
+ }
+ }
+
+ // This was also possible before Hugo 0.146.0, but this should be deprecated.
+ if pi.Type() == paths.TypeShortcode {
+ aliasName := strings.TrimPrefix(name, "_shortcodes/")
+ aliasName = "_internal/shortcodes/" + aliasName
+ _, err = prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree)
+ if err != nil {
+ return err
+ }
+ }
+
+ }
+ }
+
+ ti.Template = templ
+
+ return nil
+}
+
+func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTemplateInfo) error {
+ tb := &TemplWithBaseApplied{
+ Overlay: overlay,
+ Base: base.Info,
+ }
+
+ base.Info.Overlays = append(base.Info.Overlays, overlay)
+
+ var templ tpl.Template
+ if overlay.D.IsPlainText {
+ tt := texttemplate.Must(t.parseText.Clone()).New(overlay.PathInfo.PathNoLeadingSlash())
+ var err error
+ tt, err = tt.Parse(base.Info.Content)
+ if err != nil {
+ return err
+ }
+ tt, err = tt.Parse(overlay.Content)
+ if err != nil {
+ return err
+ }
+ templ = tt
+ t.baseofTextClones = append(t.baseofTextClones, tt)
+ } else {
+ tt := htmltemplate.Must(t.parseHTML.CloneShallow()).New(overlay.PathInfo.PathNoLeadingSlash())
+ var err error
+ tt, err = tt.Parse(base.Info.Content)
+ if err != nil {
+ return err
+ }
+ tt, err = tt.Parse(overlay.Content)
+ if err != nil {
+ return err
+ }
+ templ = tt
+
+ t.baseofHtmlClones = append(t.baseofHtmlClones, tt)
+
+ }
+
+ tb.Template = &TemplInfo{
+ Template: templ,
+ Base: base.Info,
+ PathInfo: overlay.PathInfo,
+ Fi: overlay.Fi,
+ D: overlay.D,
+ NoBaseOf: true,
+ }
+
+ variants := overlay.BaseVariants.Get(base.Key)
+ if variants == nil {
+ variants = make(map[TemplateDescriptor]*TemplWithBaseApplied)
+ overlay.BaseVariants.Insert(base.Key, variants)
+ }
+ variants[base.Info.D] = tb
+ return nil
+}
+
+func (t *templateNamespace) templatesIn(in tpl.Template) []tpl.Template {
+ var templs []tpl.Template
+ if textt, ok := in.(*texttemplate.Template); ok {
+ for _, t := range textt.Templates() {
+ templs = append(templs, t)
+ }
+ }
+ if htmlt, ok := in.(*htmltemplate.Template); ok {
+ for _, t := range htmlt.Templates() {
+ templs = append(templs, t)
+ }
+ }
+ return templs
+}
+
+/*
+
+
+func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) {
+ if overlay.isText {
+ var (
+ templ = t.main.getPrototypeText(prototypeCloneIDBaseof).New(overlay.name)
+ err error
+ )
+
+ if !base.IsZero() {
+ templ, err = templ.Parse(base.template)
+ if err != nil {
+ return nil, base.errWithFileContext("text: base: parse failed", err)
+ }
+ }
+
+ templ, err = texttemplate.Must(templ.Clone()).Parse(overlay.template)
+ if err != nil {
+ return nil, overlay.errWithFileContext("text: overlay: parse failed", err)
+ }
+
+ // The extra lookup is a workaround, see
+ // * https://github.com/golang/go/issues/16101
+ // * https://github.com/gohugoio/hugo/issues/2549
+ // templ = templ.Lookup(templ.Name())
+
+ return templ, nil
+ }
+
+ var (
+ templ = t.main.getPrototypeHTML(prototypeCloneIDBaseof).New(overlay.name)
+ err error
+ )
+
+ if !base.IsZero() {
+ templ, err = templ.Parse(base.template)
+ if err != nil {
+ return nil, base.errWithFileContext("html: base: parse failed", err)
+ }
+ }
+
+ templ, err = htmltemplate.Must(templ.Clone()).Parse(overlay.template)
+ if err != nil {
+ return nil, overlay.errWithFileContext("html: overlay: parse failed", err)
+ }
+
+ // The extra lookup is a workaround, see
+ // * https://github.com/golang/go/issues/16101
+ // * https://github.com/gohugoio/hugo/issues/2549
+ templ = templ.Lookup(templ.Name())
+
+ return templ, err
+}
+
+*/
+
+var baseTemplateDefineRe = regexp.MustCompile(`^{{-?\s*define`)
+
+// needsBaseTemplate returns true if the first non-comment template block is a
+// define block.
+func needsBaseTemplate(templ string) bool {
+ idx := -1
+ inComment := false
+ for i := 0; i < len(templ); {
+ if !inComment && strings.HasPrefix(templ[i:], "{{/*") {
+ inComment = true
+ i += 4
+ } else if !inComment && strings.HasPrefix(templ[i:], "{{- /*") {
+ inComment = true
+ i += 6
+ } else if inComment && strings.HasPrefix(templ[i:], "*/}}") {
+ inComment = false
+ i += 4
+ } else if inComment && strings.HasPrefix(templ[i:], "*/ -}}") {
+ inComment = false
+ i += 6
+ } else {
+ r, size := utf8.DecodeRuneInString(templ[i:])
+ if !inComment {
+ if strings.HasPrefix(templ[i:], "{{") {
+ idx = i
+ break
+ } else if !unicode.IsSpace(r) {
+ break
+ }
+ }
+ i += size
+ }
+ }
+
+ if idx == -1 {
+ return false
+ }
+
+ return baseTemplateDefineRe.MatchString(templ[idx:])
+}
+
+func removeLeadingBOM(s string) string {
+ const bom = '\ufeff'
+
+ for i, r := range s {
+ if i == 0 && r != bom {
+ return s
+ }
+ if i > 0 {
+ return s[i:]
+ }
+ }
+
+ return s
+}
+
+type templateNamespace struct {
+ parseText *texttemplate.Template
+ parseHTML *htmltemplate.Template
+ prototypeText *texttemplate.Template
+ prototypeHTML *htmltemplate.Template
+
+ standaloneText *texttemplate.Template
+
+ baseofTextClones []*texttemplate.Template
+ baseofHtmlClones []*htmltemplate.Template
+}
+
+func (t *templateNamespace) createPrototypesParse() error {
+ if t.prototypeHTML == nil {
+ panic("prototypeHTML not set")
+ }
+ t.parseHTML = htmltemplate.Must(t.prototypeHTML.Clone())
+ t.parseText = texttemplate.Must(t.prototypeText.Clone())
+ return nil
+}
+
+func (t *templateNamespace) createPrototypes(init bool) error {
+ if init {
+ t.prototypeHTML = htmltemplate.Must(t.parseHTML.Clone())
+ t.prototypeText = texttemplate.Must(t.parseText.Clone())
+ }
+ // t.execHTML = htmltemplate.Must(t.parseHTML.Clone())
+ // t.execText = texttemplate.Must(t.parseText.Clone())
+
+ return nil
+}
+
+func newTemplateNamespace(funcs map[string]any) *templateNamespace {
+ return &templateNamespace{
+ parseHTML: htmltemplate.New("").Funcs(funcs),
+ parseText: texttemplate.New("").Funcs(funcs),
+ standaloneText: texttemplate.New("").Funcs(funcs),
+ }
+}
(DIR) diff --git a/tpl/tplimpl/templatestore.go b/tpl/tplimpl/templatestore.go
@@ -0,0 +1,1854 @@
+package tplimpl
+
+import (
+ "bytes"
+ "context"
+ "embed"
+ "fmt"
+ "io"
+ "io/fs"
+ "iter"
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+ "regexp"
+ "sort"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/paths"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/hugofs/files"
+ "github.com/gohugoio/hugo/hugolib/doctree"
+ "github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/metrics"
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/resources/kinds"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/tpl"
+ htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
+ texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
+ "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
+ "github.com/spf13/afero"
+)
+
+const (
+ CategoryLayout Category = iota + 1
+ CategoryBaseof
+ CategoryMarkup
+ CategoryShortcode
+ CategoryPartial
+ // Internal categories
+ CategoryServer
+ CategoryHugo
+)
+
+const (
+ SubCategoryMain SubCategory = iota
+ SubCategoryEmbedded // Internal Hugo templates
+ SubCategoryInline // Inline partials
+)
+
+const (
+ containerMarkup = "_markup"
+ containerShortcodes = "_shortcodes"
+ shortcodesPathIdentifier = "/_shortcodes/"
+ containerPartials = "_partials"
+)
+
+const (
+ layoutAll = "all"
+ layoutList = "list"
+ layoutSingle = "single"
+)
+
+var (
+ _ identity.IdentityProvider = (*TemplInfo)(nil)
+ _ identity.IsProbablyDependentProvider = (*TemplInfo)(nil)
+ _ identity.IsProbablyDependencyProvider = (*TemplInfo)(nil)
+)
+
+const (
+ processingStateInitial processingState = iota
+ processingStateTransformed
+)
+
+// The identifiers may be truncated in the log, e.g.
+// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image"
+// We need this to identify position in templates with base templates applied.
+var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`)
+
+var weightNoMatch = weight{w1: -1}
+
+//
+//go:embed all:embedded/templates/*
+var embeddedTemplatesFs embed.FS
+
+func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) {
+ html, ok := opts.OutputFormats.GetByName("html")
+ if !ok {
+ panic("HTML output format not found")
+ }
+ s := &TemplateStore{
+ opts: opts,
+ siteOpts: siteOpts,
+ optsOrig: opts,
+ siteOptsOrig: siteOpts,
+ htmlFormat: html,
+ storeSite: configureSiteStorage(siteOpts, opts.Watching),
+ treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
+ treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
+ templatesByPath: maps.NewCache[string, *TemplInfo](),
+
+ // Note that the funcs passed below is just for name validation.
+ tns: newTemplateNamespace(siteOpts.TemplateFuncs),
+
+ dh: descriptorHandler{
+ opts: opts,
+ },
+ }
+
+ if err := s.init(); err != nil {
+ return nil, err
+ }
+ if err := s.insertTemplates(nil, false); err != nil {
+ return nil, err
+ }
+ if err := s.insertEmbedded(); err != nil {
+ return nil, err
+ }
+ if err := s.parseTemplates(); err != nil {
+ return nil, err
+ }
+ if err := s.extractInlinePartials(); err != nil {
+ return nil, err
+ }
+ if err := s.transformTemplates(); err != nil {
+ return nil, err
+ }
+ if err := s.tns.createPrototypes(true); err != nil {
+ return nil, err
+ }
+ if err := s.prepareTemplates(); err != nil {
+ return nil, err
+ }
+ return s, nil
+}
+
+//go:generate stringer -type Category
+
+type Category int
+
+type SiteOptions struct {
+ Site page.Site
+ TemplateFuncs map[string]any
+}
+
+type StoreOptions struct {
+ // The filesystem to use.
+ Fs afero.Fs
+
+ // The path parser to use.
+ PathParser *paths.PathParser
+
+ // Set when --enableTemplateMetrics is set.
+ Metrics metrics.Provider
+
+ // All configured output formats.
+ OutputFormats output.Formats
+
+ // All configured media types.
+ MediaTypes media.Types
+
+ // The default content language.
+ DefaultContentLanguage string
+
+ // The default output format.
+ DefaultOutputFormat string
+
+ // Taxonomy config.
+ TaxonomySingularPlural map[string]string
+
+ // Whether we are in watch or server mode.
+ Watching bool
+
+ // compiled.
+ legacyMappingTaxonomy map[string]legacyOrdinalMapping
+ legacyMappingTerm map[string]legacyOrdinalMapping
+ legacyMappingSection map[string]legacyOrdinalMapping
+}
+
+//go:generate stringer -type SubCategory
+
+type SubCategory int
+
+type TemplInfo struct {
+ // The category of this template.
+ Category Category
+
+ SubCategory SubCategory
+
+ // PathInfo info.
+ PathInfo *paths.Path
+
+ // Set when backed by a file.
+ Fi hugofs.FileMetaInfo
+
+ // The template content with any leading BOM removed.
+ Content string
+
+ // The parsed template.
+ // Note that any baseof template will be applied later.
+ Template tpl.Template
+
+ // If no baseof is needed, this will be set to true.
+ // E.g. shortcode templates do not need a baseof.
+ NoBaseOf bool
+
+ // If NoBaseOf is false, we will look for the final template in this tree.
+ BaseVariants *doctree.SimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]
+
+ // The template variants that are based on this template.
+ Overlays []*TemplInfo
+
+ // The base template used, if any.
+ Base *TemplInfo
+
+ // The descriptior that this template represents.
+ D TemplateDescriptor
+
+ // Parser state.
+ ParseInfo ParseInfo
+
+ // The execution counter for this template.
+ ExecutionCounter atomic.Uint64
+
+ // processing state.
+ state processingState
+ isLegacyMapped bool
+}
+
+func (ti *TemplInfo) BaseVariantsSeq() iter.Seq[*TemplWithBaseApplied] {
+ return func(yield func(*TemplWithBaseApplied) bool) {
+ ti.BaseVariants.Walk(func(key string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) {
+ for _, vv := range v {
+ if !yield(vv) {
+ return true, nil
+ }
+ }
+ return false, nil
+ })
+ }
+}
+
+func (t *TemplInfo) IdentifierBase() string {
+ if t.PathInfo == nil {
+ return t.Name()
+ }
+ return t.PathInfo.IdentifierBase()
+}
+
+func (t *TemplInfo) GetIdentity() identity.Identity {
+ return t
+}
+
+func (ti *TemplInfo) Name() string {
+ return ti.Template.Name()
+}
+
+func (ti *TemplInfo) Prepare() (*texttemplate.Template, error) {
+ return ti.Template.Prepare()
+}
+
+func (t *TemplInfo) IsProbablyDependency(other identity.Identity) bool {
+ return t.isProbablyTheSameIDAs(other)
+}
+
+func (t *TemplInfo) IsProbablyDependent(other identity.Identity) bool {
+ for _, overlay := range t.Overlays {
+ if overlay.isProbablyTheSameIDAs(other) {
+ return true
+ }
+ }
+ return t.isProbablyTheSameIDAs(other)
+}
+
+func (ti *TemplInfo) String() string {
+ if ti == nil {
+ return "<nil>"
+ }
+ return ti.PathInfo.String()
+}
+
+func (ti *TemplInfo) findBestMatchBaseof(s *TemplateStore, k1 string, slashCountK1 int, best *bestMatch) {
+ if ti.BaseVariants == nil {
+ return
+ }
+
+ ti.BaseVariants.WalkPath(k1, func(k2 string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) {
+ slashCountK2 := strings.Count(k2, "/")
+ distance := slashCountK1 - slashCountK2
+
+ for d, vv := range v {
+ weight := s.dh.compareDescriptors(CategoryBaseof, ti.D, d)
+ weight.distance = distance
+ if best.isBetter(weight, vv.Template) {
+ best.updateValues(weight, k2, d, vv.Template)
+ }
+ }
+ return false, nil
+ })
+}
+
+func (t *TemplInfo) isProbablyTheSameIDAs(other identity.Identity) bool {
+ if t.IdentifierBase() == other.IdentifierBase() {
+ return true
+ }
+
+ if t.Fi != nil && t.Fi.Meta().PathInfo != t.PathInfo {
+ return other.IdentifierBase() == t.Fi.Meta().PathInfo.IdentifierBase()
+ }
+
+ return false
+}
+
+type TemplWithBaseApplied struct {
+ // The template that's overlaid on top of the base template.
+ Overlay *TemplInfo
+ // The base template.
+ Base *TemplInfo
+ // This is the final template that can be used to render a page.
+ Template *TemplInfo
+}
+
+// TemplateQuery is used in LookupPagesLayout to find the best matching template.
+type TemplateQuery struct {
+ // The path to walk down to.
+ Path string
+
+ // The name to look for. Used for shortcode queries.
+ Name string
+
+ // The category to look in.
+ Category Category
+
+ // The template descriptor to match against.
+ Desc TemplateDescriptor
+
+ // Whether to even consider this candidate.
+ Consider func(candidate *TemplInfo) bool
+}
+
+func (q *TemplateQuery) init() {
+ if q.Desc.Kind == kinds.KindTemporary {
+ q.Desc.Kind = ""
+ } else if kinds.GetKindMain(q.Desc.Kind) == "" {
+ q.Desc.Kind = ""
+ }
+ if q.Desc.Layout == "" && q.Desc.Kind != "" {
+ if q.Desc.Kind == kinds.KindPage {
+ q.Desc.Layout = layoutSingle
+ } else {
+ q.Desc.Layout = layoutList
+ }
+ }
+
+ if q.Consider == nil {
+ q.Consider = func(match *TemplInfo) bool {
+ return true
+ }
+ }
+
+ q.Name = strings.ToLower(q.Name)
+
+ if q.Category == 0 {
+ panic("category not set")
+ }
+}
+
+type TemplateStore struct {
+ opts StoreOptions
+ siteOpts SiteOptions
+ htmlFormat output.Format
+
+ treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo]
+ treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
+ templatesByPath *maps.Cache[string, *TemplInfo]
+
+ dh descriptorHandler
+
+ // The template namespace.
+ tns *templateNamespace
+
+ // Site specific state.
+ // All above this is reused.
+ storeSite *storeSite
+
+ // For testing benchmarking.
+ optsOrig StoreOptions
+ siteOptsOrig SiteOptions
+}
+
+// NewFromOpts creates a new store with the same configuration as the original.
+// Used for testing/benchmarking.
+func (s *TemplateStore) NewFromOpts() (*TemplateStore, error) {
+ return NewStore(s.optsOrig, s.siteOptsOrig)
+}
+
+// In the previous implementation of base templates in Hugo, we parsed and applied these base templates on
+// request, e.g. in the middle of rendering. The idea was that we coulnd't know upfront which layoyt/base template
+// combination that would be used.
+// This, however, added a lot of complexity involving a careful dance of template cloning and parsing
+// (Go HTML tenplates cannot be parsed after any of the templates in the tree have been executed).
+// FindAllBaseTemplateCandidates finds all base template candidates for the given descriptor so we can apply them upfront.
+// In this setup we may end up with unused base templates, but not having to do the cloning should more than make up for that.
+func (s *TemplateStore) FindAllBaseTemplateCandidates(overlayKey string, desc TemplateDescriptor) []keyTemplateInfo {
+ var result []keyTemplateInfo
+ descBaseof := desc
+ s.treeMain.Walk(func(k string, v map[nodeKey]*TemplInfo) (bool, error) {
+ for _, vv := range v {
+ if vv.Category != CategoryBaseof {
+ continue
+ }
+
+ if vv.D.isKindInLayout(desc.Layout) && s.dh.compareDescriptors(CategoryBaseof, descBaseof, vv.D).w1 > 0 {
+ result = append(result, keyTemplateInfo{Key: k, Info: vv})
+ }
+ }
+ return false, nil
+ })
+
+ return result
+}
+
+func (t *TemplateStore) ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error {
+ defer func() {
+ ti.ExecutionCounter.Add(1)
+ if ti.Base != nil {
+ ti.Base.ExecutionCounter.Add(1)
+ }
+ }()
+
+ templ := ti.Template
+
+ if t.opts.Metrics != nil {
+ defer t.opts.Metrics.MeasureSince(templ.Name(), time.Now())
+ }
+
+ execErr := t.storeSite.executer.ExecuteWithContext(ctx, ti, wr, data)
+ if execErr != nil {
+ return t.addFileContext(ti, execErr)
+ }
+ return nil
+}
+
+func (t *TemplateStore) GetFunc(name string) (reflect.Value, bool) {
+ v, found := t.storeSite.execHelper.funcs[name]
+ return v, found
+}
+
+func (s *TemplateStore) GetIdentity(p string) identity.Identity {
+ p = paths.AddLeadingSlash(p)
+ v, found := s.templatesByPath.Get(p)
+ if !found {
+ return nil
+ }
+ return v.GetIdentity()
+}
+
+func (t *TemplateStore) LookupByPath(templatePath string) *TemplInfo {
+ v, _ := t.templatesByPath.Get(templatePath)
+ return v
+}
+
+var bestPool = sync.Pool{
+ New: func() any {
+ return &bestMatch{}
+ },
+}
+
+func (s *TemplateStore) getBest() *bestMatch {
+ v := bestPool.Get()
+ b := v.(*bestMatch)
+ b.defaultOutputformat = s.opts.DefaultOutputFormat
+ return b
+}
+
+func (s *TemplateStore) putBest(b *bestMatch) {
+ b.reset()
+ bestPool.Put(b)
+}
+
+func (s *TemplateStore) LookupPagesLayout(q TemplateQuery) *TemplInfo {
+ q.init()
+ key := s.key(q.Path)
+
+ slashCountKey := strings.Count(key, "/")
+ best1 := s.getBest()
+ defer s.putBest(best1)
+ s.findBestMatchWalkPath(q, key, slashCountKey, best1)
+ if best1.w.w1 <= 0 {
+ return nil
+ }
+ m := best1.templ
+ if m.NoBaseOf {
+ return m
+ }
+ best1.reset()
+ m.findBestMatchBaseof(s, key, slashCountKey, best1)
+ if best1.w.w1 <= 0 {
+ return nil
+ }
+ return best1.templ
+}
+
+func (s *TemplateStore) LookupPartial(pth string, desc TemplateDescriptor) *TemplInfo {
+ if desc.Layout != "" {
+ panic("shortcode template descriptor must not have a layout")
+ }
+ best := s.getBest()
+ defer s.putBest(best)
+ s.findBestMatchGet(s.key(path.Join(containerPartials, pth)), CategoryPartial, nil, desc, best)
+ return best.templ
+}
+
+func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo {
+ q.init()
+ k1 := s.key(q.Path)
+
+ slashCountK1 := strings.Count(k1, "/")
+
+ best := s.getBest()
+ defer s.putBest(best)
+
+ s.treeShortcodes.WalkPath(k1, func(k2 string, m map[string]map[TemplateDescriptor]*TemplInfo) (bool, error) {
+ slashCountK2 := strings.Count(k2, "/")
+ distance := slashCountK1 - slashCountK2
+
+ v, found := m[q.Name]
+ if !found {
+ return false, nil
+ }
+
+ for k, vv := range v {
+ if !q.Consider(vv) {
+ continue
+ }
+
+ weight := s.dh.compareDescriptors(q.Category, q.Desc, k)
+ weight.distance = distance
+ if best.isBetter(weight, vv) {
+ best.updateValues(weight, k2, k, vv)
+ }
+ }
+
+ return false, nil
+ })
+
+ // Any match will do.
+ return best.templ
+}
+
+// PrintDebug is for testing/debugging only.
+func (s *TemplateStore) PrintDebug(prefix string, category Category, w io.Writer) {
+ if w == nil {
+ w = os.Stdout
+ }
+
+ printOne := func(key string, vv *TemplInfo) {
+ level := strings.Count(key, "/")
+ if category != vv.Category {
+ return
+ }
+ s := strings.ReplaceAll(strings.TrimSpace(vv.Content), "\n", " ")
+ ts := fmt.Sprintf("kind: %q layout: %q content: %.30s", vv.D.Kind, vv.D.Layout, s)
+ fmt.Fprintf(w, "%s%s %s\n", strings.Repeat(" ", level), key, ts)
+ }
+ s.treeMain.WalkPrefix(prefix, func(key string, v map[nodeKey]*TemplInfo) (bool, error) {
+ for _, vv := range v {
+ printOne(key, vv)
+ }
+ return false, nil
+ })
+ s.treeShortcodes.WalkPrefix(prefix, func(key string, v map[string]map[TemplateDescriptor]*TemplInfo) (bool, error) {
+ for _, vv := range v {
+ for _, vv2 := range vv {
+ printOne(key, vv2)
+ }
+ }
+ return false, nil
+ })
+}
+
+// RefreshFiles refreshes this store for the files matching the given predicate.
+func (s *TemplateStore) RefreshFiles(include func(fi hugofs.FileMetaInfo) bool) error {
+ if err := s.tns.createPrototypesParse(); err != nil {
+ return err
+ }
+ if err := s.insertTemplates(include, true); err != nil {
+ return err
+ }
+ if err := s.parseTemplates(); err != nil {
+ return err
+ }
+ if err := s.extractInlinePartials(); err != nil {
+ return err
+ }
+ if err := s.transformTemplates(); err != nil {
+ return err
+ }
+ if err := s.tns.createPrototypes(false); err != nil {
+ return err
+ }
+ if err := s.prepareTemplates(); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (s *TemplateStore) HasTemplate(templatePath string) bool {
+ templatePath = paths.AddLeadingSlash(templatePath)
+ return s.templatesByPath.Contains(templatePath)
+}
+
+func (t *TemplateStore) TextLookup(name string) *TemplInfo {
+ templ := t.tns.standaloneText.Lookup(name)
+ if templ == nil {
+ return nil
+ }
+ return &TemplInfo{
+ Template: templ,
+ }
+}
+
+func (t *TemplateStore) TextParse(name, tpl string) (*TemplInfo, error) {
+ templ, err := t.tns.standaloneText.New(name).Parse(tpl)
+ if err != nil {
+ return nil, err
+ }
+ return &TemplInfo{
+ Template: templ,
+ }, nil
+}
+
+func (t *TemplateStore) UnusedTemplates() []*TemplInfo {
+ var unused []*TemplInfo
+
+ for vv := range t.templates() {
+ if vv.SubCategory != SubCategoryMain {
+ // Skip inline partials and internal templates.
+ continue
+ }
+ if vv.NoBaseOf {
+ if vv.ExecutionCounter.Load() == 0 {
+ unused = append(unused, vv)
+ }
+ } else {
+ for vvv := range vv.BaseVariantsSeq() {
+ if vvv.Template.ExecutionCounter.Load() == 0 {
+ unused = append(unused, vvv.Template)
+ }
+ }
+ }
+ }
+
+ sort.Sort(byPath(unused))
+ return unused
+}
+
+// WithSiteOpts creates a new store with the given site options.
+// This is used to create per site template store, all sharing the same templates,
+// but with a different template function execution context.
+func (s TemplateStore) WithSiteOpts(opts SiteOptions) *TemplateStore {
+ s.siteOpts = opts
+ s.storeSite = configureSiteStorage(opts, s.opts.Watching)
+ return &s
+}
+
+func (s *TemplateStore) findBestMatchGet(key string, category Category, consider func(candidate *TemplInfo) bool, desc TemplateDescriptor, best *bestMatch) {
+ key = strings.ToLower(key)
+
+ v := s.treeMain.Get(key)
+ if v == nil {
+ return
+ }
+
+ for k, vv := range v {
+ if vv.Category != category {
+ continue
+ }
+
+ if consider != nil && !consider(vv) {
+ continue
+ }
+
+ weight := s.dh.compareDescriptors(category, desc, k.d)
+ if best.isBetter(weight, vv) {
+ best.updateValues(weight, key, k.d, vv)
+ }
+ }
+}
+
+func (s *TemplateStore) findBestMatchWalkPath(q TemplateQuery, k1 string, slashCountK1 int, best *bestMatch) {
+ s.treeMain.WalkPath(k1, func(k2 string, v map[nodeKey]*TemplInfo) (bool, error) {
+ slashCountK2 := strings.Count(k2, "/")
+ distance := slashCountK1 - slashCountK2
+
+ for k, vv := range v {
+ if vv.Category != q.Category {
+ continue
+ }
+
+ if !q.Consider(vv) {
+ continue
+ }
+
+ weight := s.dh.compareDescriptors(q.Category, q.Desc, k.d)
+
+ weight.distance = distance
+ isBetter := best.isBetter(weight, vv)
+
+ if isBetter {
+ best.updateValues(weight, k2, k.d, vv)
+ }
+ }
+
+ return false, nil
+ })
+}
+
+func (t *TemplateStore) addDeferredTemplate(owner *TemplInfo, name string, n *parse.ListNode) error {
+ if _, found := t.templatesByPath.Get(name); found {
+ return nil
+ }
+
+ var templ tpl.Template
+
+ if owner.D.IsPlainText {
+ prototype := t.tns.parseText
+ tt, err := prototype.New(name).Parse("")
+ if err != nil {
+ return fmt.Errorf("failed to parse empty text template %q: %w", name, err)
+ }
+ tt.Tree.Root = n
+ templ = tt
+ } else {
+ prototype := t.tns.parseHTML
+ tt, err := prototype.New(name).Parse("")
+ if err != nil {
+ return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err)
+ }
+ tt.Tree.Root = n
+ templ = tt
+ }
+
+ t.templatesByPath.Set(name, &TemplInfo{
+ Fi: owner.Fi,
+ PathInfo: owner.PathInfo,
+ D: owner.D,
+ Template: templ,
+ })
+
+ return nil
+}
+
+func (s *TemplateStore) addFileContext(ti *TemplInfo, inerr error) error {
+ if ti.Fi == nil {
+ return inerr
+ }
+
+ identifiers := s.extractIdentifiers(inerr.Error())
+
+ checkFilename := func(fi hugofs.FileMetaInfo, inErr error) (error, bool) {
+ lineMatcher := func(m herrors.LineMatcher) int {
+ if m.Position.LineNumber != m.LineNumber {
+ return -1
+ }
+
+ for _, id := range identifiers {
+ if strings.Contains(m.Line, id) {
+ // We found the line, but return a 0 to signal to
+ // use the column from the error message.
+ return 0
+ }
+ }
+ return -1
+ }
+
+ f, err := fi.Meta().Open()
+ if err != nil {
+ return inErr, false
+ }
+ defer f.Close()
+
+ fe := herrors.NewFileErrorFromName(inErr, fi.Meta().Filename)
+ fe.UpdateContent(f, lineMatcher)
+
+ if !fe.ErrorContext().Position.IsValid() {
+ return inErr, false
+ }
+ return fe, true
+ }
+
+ inerr = fmt.Errorf("execute of template failed: %w", inerr)
+
+ if err, ok := checkFilename(ti.Fi, inerr); ok {
+ return err
+ }
+
+ if ti.Base != nil {
+ if err, ok := checkFilename(ti.Base.Fi, inerr); ok {
+ return err
+ }
+ }
+
+ return inerr
+}
+
+func (s *TemplateStore) extractIdentifiers(line string) []string {
+ m := identifiersRe.FindAllStringSubmatch(line, -1)
+ identifiers := make([]string, len(m))
+ for i := range m {
+ identifiers[i] = m[i][1]
+ }
+ return identifiers
+}
+
+func (s *TemplateStore) extractInlinePartials() error {
+ isPartialName := func(s string) bool {
+ return strings.HasPrefix(s, "partials/") || strings.HasPrefix(s, "_partials/")
+ }
+
+ p := s.tns
+ // We may find both inline and external partials in the current template namespaces,
+ // so only add the ones we have not seen before.
+ addIfNotSeen := func(isText bool, templs ...tpl.Template) error {
+ for _, templ := range templs {
+ if templ.Name() == "" || !isPartialName(templ.Name()) {
+ continue
+ }
+ name := templ.Name()
+ if !paths.HasExt(name) {
+ // Assume HTML. This in line with how the lookup works.
+ name = name + ".html"
+ }
+ if !strings.HasPrefix(name, "_") {
+ name = "_" + name
+ }
+ pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
+ ti, err := s.insertTemplate(pi, nil, false, s.treeMain)
+ if err != nil {
+ return err
+ }
+
+ if ti != nil {
+ ti.Template = templ
+ ti.NoBaseOf = true
+ ti.SubCategory = SubCategoryInline
+ ti.D.IsPlainText = isText
+ }
+
+ }
+ return nil
+ }
+ addIfNotSeen(false, p.templatesIn(p.parseHTML)...)
+ addIfNotSeen(true, p.templatesIn(p.parseText)...)
+
+ for _, t := range p.baseofHtmlClones {
+ if err := addIfNotSeen(false, p.templatesIn(t)...); err != nil {
+ return err
+ }
+ }
+ for _, t := range p.baseofTextClones {
+ if err := addIfNotSeen(true, p.templatesIn(t)...); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (s *TemplateStore) insertEmbedded() error {
+ return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d == nil || d.IsDir() || strings.HasPrefix(d.Name(), ".") {
+ return nil
+ }
+
+ templb, err := embeddedTemplatesFs.ReadFile(path)
+ if err != nil {
+ return err
+ }
+
+ // Get the newlines on Windows in line with how we had it back when we used Go Generate
+ // to write the templates to Go files.
+ templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
+ name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/")
+
+ insertOne := func(name, content string) error {
+ pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
+ var (
+ ti *TemplInfo
+ err error
+ )
+ if pi.Section() == containerShortcodes {
+ ti, err = s.insertShortcode(pi, nil, false, s.treeShortcodes)
+ if err != nil {
+ return err
+ }
+ } else {
+ ti, err = s.insertTemplate(pi, nil, false, s.treeMain)
+ if err != nil {
+ return err
+ }
+ }
+
+ if ti != nil {
+ // Currently none of the embedded templates need a baseof template.
+ ti.NoBaseOf = true
+ ti.Content = content
+ ti.SubCategory = SubCategoryEmbedded
+ }
+
+ return nil
+ }
+
+ if err := insertOne(name, templ); err != nil {
+ return err
+ }
+
+ if aliases, found := embeddedTemplatesAliases[name]; found {
+ for _, alias := range aliases {
+ if err := insertOne(alias, templ); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+ })
+}
+
+func (s *TemplateStore) setTemplateByPath(p string, ti *TemplInfo) {
+ s.templatesByPath.Set(p, ti)
+}
+
+func (s *TemplateStore) insertShortcode(pi *paths.Path, fi hugofs.FileMetaInfo, replace bool, tree doctree.Tree[map[string]map[TemplateDescriptor]*TemplInfo]) (*TemplInfo, error) {
+ k1, k2, _, d := s.toKeyCategoryAndDescriptor(pi)
+ m := tree.Get(k1)
+ if m == nil {
+ m = make(map[string]map[TemplateDescriptor]*TemplInfo)
+ tree.Insert(k1, m)
+ }
+
+ m1, found := m[k2]
+ if found {
+ if _, found := m1[d]; found {
+ if !replace {
+ return nil, nil
+ }
+ }
+ } else {
+ m1 = make(map[TemplateDescriptor]*TemplInfo)
+ m[k2] = m1
+ }
+
+ ti := &TemplInfo{
+ PathInfo: pi,
+ Fi: fi,
+ D: d,
+ Category: CategoryShortcode,
+ NoBaseOf: true,
+ }
+
+ m1[d] = ti
+
+ s.setTemplateByPath(pi.Path(), ti)
+
+ if fi != nil {
+ if pi2 := fi.Meta().PathInfo; pi2 != pi {
+ s.setTemplateByPath(pi2.Path(), ti)
+ }
+ }
+
+ return ti, nil
+}
+
+func (s *TemplateStore) insertTemplate(pi *paths.Path, fi hugofs.FileMetaInfo, replace bool, tree doctree.Tree[map[nodeKey]*TemplInfo]) (*TemplInfo, error) {
+ key, _, category, d := s.toKeyCategoryAndDescriptor(pi)
+
+ return s.insertTemplate2(pi, fi, key, category, d, replace, false, tree)
+}
+
+func (s *TemplateStore) insertTemplate2(
+ pi *paths.Path,
+ fi hugofs.FileMetaInfo,
+ key string,
+ category Category,
+ d TemplateDescriptor,
+ replace, isLegacyMapped bool,
+ tree doctree.Tree[map[nodeKey]*TemplInfo],
+) (*TemplInfo, error) {
+ if category == 0 {
+ panic("category not set")
+ }
+
+ m := tree.Get(key)
+ nk := nodeKey{c: category, d: d}
+
+ if m == nil {
+ m = make(map[nodeKey]*TemplInfo)
+ tree.Insert(key, m)
+ }
+
+ if !replace {
+ if v, found := m[nk]; found {
+ if len(pi.IdentifiersUnknown()) >= len(v.PathInfo.IdentifiersUnknown()) {
+ // e.g. /pages/home.foo.html and /pages/home.html where foo may be a valid language name in another site.
+ return nil, nil
+ }
+ }
+ }
+
+ ti := &TemplInfo{
+ PathInfo: pi,
+ Fi: fi,
+ D: d,
+ Category: category,
+ NoBaseOf: category > CategoryLayout,
+ isLegacyMapped: isLegacyMapped,
+ }
+
+ m[nk] = ti
+
+ if !isLegacyMapped {
+ s.setTemplateByPath(pi.Path(), ti)
+ if fi != nil {
+ if pi2 := fi.Meta().PathInfo; pi2 != pi {
+ s.setTemplateByPath(pi2.Path(), ti)
+ }
+ }
+ }
+
+ return ti, nil
+}
+
+func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) bool, replace bool) error {
+ if include == nil {
+ include = func(fi hugofs.FileMetaInfo) bool {
+ return true
+ }
+ }
+
+ // Set if we need to reset the base variants.
+ var (
+ resetBaseVariants bool
+ )
+
+ legacyOrdinalMappings := map[legacyTargetPathIdentifiers]legacyOrdinalMappingFi{}
+
+ walker := func(pth string, fi hugofs.FileMetaInfo) error {
+ piOrig := fi.Meta().PathInfo
+ if fi.IsDir() {
+ return nil
+ }
+
+ if !include(fi) {
+ return nil
+ }
+
+ // Convert any legacy value to new format.
+ fromLegacyPath := func(pi *paths.Path) *paths.Path {
+ p := pi.Path()
+ p = strings.TrimPrefix(p, "/_default")
+ if strings.HasPrefix(p, "/shortcodes") || strings.HasPrefix(p, "/partials") {
+ // Insert an underscore so it becomes /_shortcodes or /_partials.
+ p = "/_" + p[1:]
+ }
+
+ if strings.Contains(p, "-"+baseNameBaseof) {
+ // Before Hugo 0.146.0 we prepended one identifier (layout, type or kind) in front of the baseof keyword,
+ // and then separated with a hyphen before the baseof keyword.
+ // This identifier needs to be moved right after the baseof keyword and the hyphen removed, e.g.
+ // /docs/list-baseof.html => /docs/baseof.list.html.
+ dir, name := path.Split(p)
+ hyphenIdx := strings.Index(name, "-")
+ if hyphenIdx > 0 {
+ id := name[:hyphenIdx]
+ name = name[hyphenIdx+1+len(baseNameBaseof):]
+ if !strings.HasPrefix(name, ".") {
+ name = "." + name
+ }
+ p = path.Join(dir, baseNameBaseof+"."+id+name)
+ }
+ }
+ if p == pi.Path() {
+ return pi
+ }
+ return s.opts.PathParser.Parse(files.ComponentFolderLayouts, p)
+ }
+
+ pi := piOrig
+ var applyLegacyMapping bool
+ switch pi.Section() {
+ case containerPartials, containerShortcodes, containerMarkup:
+ // OK.
+ default:
+ applyLegacyMapping = true
+ pi = fromLegacyPath(pi)
+ }
+
+ if applyLegacyMapping {
+ handleMapping := func(m1 legacyOrdinalMapping) {
+ key := legacyTargetPathIdentifiers{
+ targetPath: m1.mapping.targetPath,
+ targetCategory: m1.mapping.targetCategory,
+ kind: m1.mapping.targetDesc.Kind,
+ lang: pi.Lang(),
+ ext: pi.Ext(),
+ outputFormat: pi.OutputFormat(),
+ }
+ if m2, ok := legacyOrdinalMappings[key]; ok {
+ if m1.ordinal < m2.m.ordinal {
+ // Higher up == better match.
+ legacyOrdinalMappings[key] = legacyOrdinalMappingFi{m1, fi}
+ }
+ } else {
+ legacyOrdinalMappings[key] = legacyOrdinalMappingFi{m1, fi}
+ }
+ }
+
+ if m1, ok := s.opts.legacyMappingTaxonomy[piOrig.PathBeforeLangAndOutputFormatAndExt()]; ok {
+ handleMapping(m1)
+ }
+
+ if m1, ok := s.opts.legacyMappingTerm[piOrig.PathBeforeLangAndOutputFormatAndExt()]; ok {
+ handleMapping(m1)
+ }
+
+ const (
+ sectionKindToken = "SECTIONKIND"
+ sectionToken = "THESECTION"
+ )
+
+ base := piOrig.PathBeforeLangAndOutputFormatAndExt()
+ identifiers := pi.IdentifiersUnknown()
+
+ // Tokens on e.g. form /SECTIONKIND/THESECTION
+ insertSectionTokens := func(section string, kindOnly bool) string {
+ s := base
+ if !kindOnly {
+ s = strings.Replace(s, section, sectionToken, 1)
+ }
+ s = strings.Replace(s, kinds.KindSection, sectionKindToken, 1)
+ return s
+ }
+
+ for _, section := range identifiers {
+ if section == baseNameBaseof {
+ continue
+ }
+ kindOnly := isLayoutStandard(section)
+ p := insertSectionTokens(section, kindOnly)
+ if m1, ok := s.opts.legacyMappingSection[p]; ok {
+ m1.mapping.targetPath = strings.Replace(m1.mapping.targetPath, sectionToken, section, 1)
+ handleMapping(m1)
+ }
+ }
+
+ }
+
+ if replace && pi.NameNoIdentifier() == baseNameBaseof {
+ // A baseof file has changed.
+ resetBaseVariants = true
+ }
+
+ var ti *TemplInfo
+ var err error
+ if pi.Type() == paths.TypeShortcode {
+ ti, err = s.insertShortcode(pi, fi, replace, s.treeShortcodes)
+ if err != nil || ti == nil {
+ return err
+ }
+ } else {
+ ti, err = s.insertTemplate(pi, fi, replace, s.treeMain)
+ if err != nil || ti == nil {
+ return err
+ }
+ }
+
+ if err := s.tns.readTemplateInto(ti); err != nil {
+ return err
+ }
+
+ return nil
+ }
+
+ if err := helpers.Walk(s.opts.Fs, "", walker); err != nil {
+ if !herrors.IsNotExist(err) {
+ return err
+ }
+ return nil
+ }
+
+ for k, v := range legacyOrdinalMappings {
+ targetPath := k.targetPath
+ m := v.m.mapping
+ fi := v.fi
+ pi := fi.Meta().PathInfo
+ outputFormat, mediaType := s.resolveOutputFormatAndOrMediaType(k.outputFormat, k.ext)
+ category := m.targetCategory
+ desc := m.targetDesc
+ desc.Kind = k.kind
+ desc.Lang = k.lang
+ desc.OutputFormat = outputFormat.Name
+ desc.IsPlainText = outputFormat.IsPlainText
+ desc.MediaType = mediaType.Type
+
+ ti, err := s.insertTemplate2(pi, fi, targetPath, category, desc, true, true, s.treeMain)
+ if err != nil {
+ return err
+ }
+ if ti == nil {
+ continue
+ }
+ ti.isLegacyMapped = true
+ if err := s.tns.readTemplateInto(ti); err != nil {
+ return err
+ }
+ }
+
+ if resetBaseVariants {
+ s.tns.baseofHtmlClones = nil
+ s.tns.baseofTextClones = nil
+ s.treeMain.Walk(func(key string, v map[nodeKey]*TemplInfo) (bool, error) {
+ for _, vv := range v {
+ if !vv.NoBaseOf {
+ vv.state = processingStateInitial
+ }
+ }
+ return false, nil
+ })
+ }
+
+ return nil
+}
+
+func (s *TemplateStore) key(dir string) string {
+ dir = paths.AddLeadingSlash(dir)
+ if dir == "/" {
+ return ""
+ }
+ return paths.TrimTrailing(dir)
+}
+
+func (s *TemplateStore) parseTemplates() error {
+ if err := func() error {
+ // Read and parse all templates.
+ for _, v := range s.treeMain.All() {
+ for _, vv := range v {
+ if vv.state == processingStateTransformed {
+ continue
+ }
+ if err := s.tns.parseTemplate(vv); err != nil {
+ return err
+ }
+ }
+ }
+
+ // Lookup and apply base templates where needed.
+ for key, v := range s.treeMain.All() {
+ for _, vv := range v {
+ if vv.state == processingStateTransformed {
+ continue
+ }
+ if !vv.NoBaseOf {
+ d := vv.D
+ // Find all compatible base templates.
+ baseTemplates := s.FindAllBaseTemplateCandidates(key, d)
+ if len(baseTemplates) == 0 {
+ // The regular expression used to detect if a template needs a base template has some
+ // rare false positives. Assume we don't need one.
+ vv.NoBaseOf = true
+ if err := s.tns.parseTemplate(vv); err != nil {
+ return err
+ }
+ continue
+ }
+ vv.BaseVariants = doctree.NewSimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]()
+
+ for _, base := range baseTemplates {
+ if err := s.tns.applyBaseTemplate(vv, base); err != nil {
+ return err
+ }
+ }
+
+ }
+ }
+ }
+
+ return nil
+ }(); err != nil {
+ return err
+ }
+
+ // Prese shortcodes.
+ for _, v := range s.treeShortcodes.All() {
+ for _, vv := range v {
+ for _, vvv := range vv {
+ if vvv.state == processingStateTransformed {
+ continue
+ }
+ if err := s.tns.parseTemplate(vvv); err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+// prepareTemplates prepares all templates for execution.
+func (s *TemplateStore) prepareTemplates() error {
+ for t := range s.templates() {
+ if t.Category == CategoryBaseof {
+ continue
+ }
+ if _, err := t.Prepare(); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// TemplateDescriptorFromPath returns a template descriptor from the given path.
+// This is currently used in partial lookups only.
+func (s *TemplateStore) TemplateDescriptorFromPath(pth string) (string, TemplateDescriptor) {
+ var (
+ mt media.Type
+ of output.Format
+ )
+
+ // Common cases.
+ dotCount := strings.Count(pth, ".")
+ if dotCount <= 1 {
+ if dotCount == 0 {
+ // Asume HTML.
+ of, mt = s.resolveOutputFormatAndOrMediaType("html", "")
+ } else {
+ pth = strings.TrimPrefix(pth, "/")
+ ext := path.Ext(pth)
+ pth = strings.TrimSuffix(pth, ext)
+ ext = ext[1:]
+ of, mt = s.resolveOutputFormatAndOrMediaType("", ext)
+ }
+ } else {
+ path := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth)
+ pth = path.PathNoIdentifier()
+ of, mt = s.resolveOutputFormatAndOrMediaType(path.OutputFormat(), path.Ext())
+ }
+
+ return pth, TemplateDescriptor{
+ OutputFormat: of.Name,
+ MediaType: mt.Type,
+ IsPlainText: of.IsPlainText,
+ }
+}
+
+// resolveOutputFormatAndOrMediaType resolves the output format and/or media type
+// based on the given output format suffix and media type suffix.
+// Either of the suffixes can be empty, and the function will try to find a match
+// based on the other suffix. If both are empty, the function will return zero values.
+func (s *TemplateStore) resolveOutputFormatAndOrMediaType(ofs, mns string) (output.Format, media.Type) {
+ var outputFormat output.Format
+ var mediaType media.Type
+
+ if ofs != "" {
+ if of, found := s.opts.OutputFormats.GetByName(ofs); found {
+ outputFormat = of
+ mediaType = of.MediaType
+ }
+ }
+
+ if mns != "" && mediaType.IsZero() {
+ if of, found := s.opts.OutputFormats.GetBySuffix(mns); found {
+ outputFormat = of
+ mediaType = of.MediaType
+ } else {
+ if mt, _, found := s.opts.MediaTypes.GetFirstBySuffix(mns); found {
+ mediaType = mt
+ if outputFormat.IsZero() {
+ // For e.g. index.xml we will in the default confg now have the application/rss+xml media type.
+ // Try a last time to find the output format using the SubType as the name.
+ // As to template resolution, this value is currently only used to
+ // decide if this is a text or HTML template.
+ outputFormat, _ = s.opts.OutputFormats.GetByName(mt.SubType)
+ }
+ }
+ }
+ }
+
+ return outputFormat, mediaType
+}
+
+func (s *TemplateStore) templates() iter.Seq[*TemplInfo] {
+ return func(yield func(*TemplInfo) bool) {
+ for _, v := range s.treeMain.All() {
+ for _, vv := range v {
+ if !vv.NoBaseOf {
+ for vvv := range vv.BaseVariantsSeq() {
+ if !yield(vvv.Template) {
+ return
+ }
+ }
+ } else {
+ if !yield(vv) {
+ return
+ }
+ }
+ }
+ }
+ for _, v := range s.treeShortcodes.All() {
+ for _, vv := range v {
+ for _, vvv := range vv {
+ if !yield(vvv) {
+ return
+ }
+ }
+ }
+ }
+ }
+}
+
+func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, string, Category, TemplateDescriptor) {
+ k1 := p.Dir()
+ k2 := ""
+
+ outputFormat, mediaType := s.resolveOutputFormatAndOrMediaType(p.OutputFormat(), p.Ext())
+ nameNoIdentifier := p.NameNoIdentifier()
+
+ var layout string
+ unknownids := p.IdentifiersUnknown()
+ if p.Type() == paths.TypeShortcode {
+ if len(unknownids) > 1 {
+ // The name is the last identifier.
+ layout = unknownids[len(unknownids)-2]
+ }
+ } else if len(unknownids) > 0 {
+ // Pick the last, closest to the base name.
+ layout = unknownids[len(unknownids)-1]
+ }
+
+ d := TemplateDescriptor{
+ Lang: p.Lang(),
+ OutputFormat: p.OutputFormat(),
+ MediaType: mediaType.Type,
+ Kind: p.Kind(),
+ Layout: layout,
+ IsPlainText: outputFormat.IsPlainText,
+ }
+
+ d.normalizeFromFile()
+
+ section := p.Section()
+
+ var category Category
+ switch p.Type() {
+ case paths.TypeShortcode:
+ category = CategoryShortcode
+ case paths.TypePartial:
+ category = CategoryPartial
+ case paths.TypeMarkup:
+ category = CategoryMarkup
+ }
+
+ if category == 0 {
+ if nameNoIdentifier == baseNameBaseof {
+ category = CategoryBaseof
+ } else {
+ switch section {
+ case "_hugo":
+ category = CategoryHugo
+ case "_server":
+ category = CategoryServer
+ default:
+ category = CategoryLayout
+ }
+ }
+ }
+
+ if category == CategoryPartial {
+ d.Layout = ""
+ k1 = p.PathNoIdentifier()
+ }
+
+ if category == CategoryShortcode {
+ k1 = p.PathNoIdentifier()
+ parts := strings.Split(k1, "/"+containerShortcodes+"/")
+ k1 = parts[0]
+ if len(parts) > 1 {
+ k2 = parts[1]
+ }
+ k1 = s.key(k1)
+ }
+
+ // Legacy layout for home page.
+ if d.Layout == "index" {
+ if d.Kind == "" {
+ d.Kind = kinds.KindHome
+ }
+ d.Layout = ""
+ }
+
+ if d.Layout == d.Kind {
+ d.Layout = ""
+ }
+
+ k1 = strings.TrimPrefix(k1, "/_default")
+ if k1 == "/" {
+ k1 = ""
+ }
+
+ if category == CategoryMarkup {
+ // We store all template nodes for a given directory on the same level.
+ k1 = strings.TrimSuffix(k1, "/_markup")
+ parts := strings.Split(d.Layout, "-")
+ if len(parts) < 2 {
+ panic("markup template must have at least 2 parts")
+ }
+ // Either 2 or 3 parts, e.g. render-codeblock-go.
+ d.Variant1 = parts[1]
+ if len(parts) > 2 {
+ d.Variant2 = parts[2]
+ }
+ d.Layout = "" // This allows using page layout as part of the key for lookups.
+ }
+
+ return k1, k2, category, d
+}
+
+func (s *TemplateStore) transformTemplates() error {
+ lookup := func(name string, in *TemplInfo) *TemplInfo {
+ if in.D.IsPlainText {
+ templ := in.Template.(*texttemplate.Template).Lookup(name)
+ if templ != nil {
+ return &TemplInfo{
+ Template: templ,
+ }
+ }
+ } else {
+ templ := in.Template.(*htmltemplate.Template).Lookup(name)
+ if templ != nil {
+ return &TemplInfo{
+ Template: templ,
+ }
+ }
+ }
+
+ return nil
+ }
+
+ for vv := range s.templates() {
+ if vv.state == processingStateTransformed {
+ continue
+ }
+ vv.state = processingStateTransformed
+ if vv.Category == CategoryBaseof {
+ continue
+ }
+ if !vv.NoBaseOf {
+ for vvv := range vv.BaseVariantsSeq() {
+ tctx, err := applyTemplateTransformers(vvv.Template, lookup)
+ if err != nil {
+ return err
+ }
+
+ for name, node := range tctx.deferNodes {
+ if err := s.addDeferredTemplate(vvv.Overlay, name, node); err != nil {
+ return err
+ }
+ }
+ }
+ } else {
+ tctx, err := applyTemplateTransformers(vv, lookup)
+ if err != nil {
+ return err
+ }
+
+ for name, node := range tctx.deferNodes {
+ if err := s.addDeferredTemplate(vv, name, node); err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+func (s *TemplateStore) init() error {
+ // Before Hugo 0.146 we had a very elaborate template lookup system, especially for
+ // terms and taxonomies. This is a way of preserving backwards compatibility
+ // by mapping old paths into the new tree.
+ s.opts.legacyMappingTaxonomy = make(map[string]legacyOrdinalMapping)
+ s.opts.legacyMappingTerm = make(map[string]legacyOrdinalMapping)
+ s.opts.legacyMappingSection = make(map[string]legacyOrdinalMapping)
+
+ // Placeholders.
+ const singular = "SINGULAR"
+ const plural = "PLURAL"
+
+ replaceTokens := func(s, singularv, pluralv string) string {
+ s = strings.Replace(s, singular, singularv, -1)
+ s = strings.Replace(s, plural, pluralv, -1)
+ return s
+ }
+
+ hasSingularOrPlural := func(s string) bool {
+ return strings.Contains(s, singular) || strings.Contains(s, plural)
+ }
+
+ expand := func(v layoutLegacyMapping) []layoutLegacyMapping {
+ var result []layoutLegacyMapping
+
+ if hasSingularOrPlural(v.sourcePath) || hasSingularOrPlural(v.target.targetPath) {
+ for s, p := range s.opts.TaxonomySingularPlural {
+ target := v.target
+ target.targetPath = replaceTokens(target.targetPath, s, p)
+ vv := replaceTokens(v.sourcePath, s, p)
+ result = append(result, layoutLegacyMapping{sourcePath: vv, target: target})
+ }
+ } else {
+ result = append(result, v)
+ }
+ return result
+ }
+
+ expandSections := func(v layoutLegacyMapping) []layoutLegacyMapping {
+ var result []layoutLegacyMapping
+ result = append(result, v)
+ baseofVariant := v
+ baseofVariant.sourcePath += "-" + baseNameBaseof
+ baseofVariant.target.targetCategory = CategoryBaseof
+ result = append(result, baseofVariant)
+ return result
+ }
+
+ var terms []layoutLegacyMapping
+ for _, v := range legacyTermMappings {
+ terms = append(terms, expand(v)...)
+ }
+ var taxonomies []layoutLegacyMapping
+ for _, v := range legacyTaxonomyMappings {
+ taxonomies = append(taxonomies, expand(v)...)
+ }
+ var sections []layoutLegacyMapping
+ for _, v := range legacySectionMappings {
+ sections = append(sections, expandSections(v)...)
+ }
+
+ for i, m := range terms {
+ s.opts.legacyMappingTerm[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
+ }
+ for i, m := range taxonomies {
+ s.opts.legacyMappingTaxonomy[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
+ }
+ for i, m := range sections {
+ s.opts.legacyMappingSection[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
+ }
+
+ return nil
+}
+
+type TemplateStoreProvider interface {
+ GetTemplateStore() *TemplateStore
+}
+
+type TextTemplatHandler interface {
+ ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error
+ TextLookup(name string) *TemplInfo
+ TextParse(name, tpl string) (*TemplInfo, error)
+}
+
+type bestMatch struct {
+ templ *TemplInfo
+ desc TemplateDescriptor
+ w weight
+ key string
+
+ // settings.
+ defaultOutputformat string
+}
+
+func (best *bestMatch) reset() {
+ best.templ = nil
+ best.w = weight{}
+ best.desc = TemplateDescriptor{}
+ best.key = ""
+}
+
+func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool {
+ if best.templ == nil {
+ // Anything is better than nothing.
+ return true
+ }
+ if w.w1 <= 0 {
+ if best.w.w1 <= 0 {
+ return ti.PathInfo.Path() < best.templ.PathInfo.Path()
+ }
+ return false
+ }
+
+ if best.w.w1 > 0 {
+ currentBestIsEmbedded := best.templ.SubCategory == SubCategoryEmbedded
+ if currentBestIsEmbedded {
+ if ti.SubCategory != SubCategoryEmbedded {
+ return true
+ }
+ } else {
+ if ti.SubCategory == SubCategoryEmbedded {
+ // Prefer user provided template.
+ return false
+ }
+ }
+ }
+
+ if w.distance < best.w.distance {
+ if w.w2 < best.w.w2 {
+ return false
+ }
+ if w.w3 < best.w.w3 {
+ return false
+ }
+ } else {
+ if w.w1 < best.w.w1 {
+ return false
+ }
+ }
+
+ if w.isEqualWeights(best.w) {
+ // Tie breakers.
+ if w.distance < best.w.distance {
+ return true
+ }
+
+ if ti.D.Layout != "" && best.desc.Layout != "" {
+ return ti.D.Layout != layoutAll
+ }
+
+ return w.distance < best.w.distance || ti.PathInfo.Path() < best.templ.PathInfo.Path()
+ }
+
+ return true
+}
+
+func (best *bestMatch) updateValues(w weight, key string, k TemplateDescriptor, vv *TemplInfo) {
+ best.w = w
+ best.templ = vv
+ best.desc = k
+ best.key = key
+}
+
+type byPath []*TemplInfo
+
+func (a byPath) Len() int { return len(a) }
+func (a byPath) Less(i, j int) bool {
+ return a[i].PathInfo.Path() < a[j].PathInfo.Path()
+}
+
+func (a byPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+
+type keyTemplateInfo struct {
+ Key string
+ Info *TemplInfo
+}
+
+type nodeKey struct {
+ c Category
+ d TemplateDescriptor
+}
+
+type processingState int
+
+// the parts of a template store that's set per site.
+type storeSite struct {
+ opts SiteOptions
+ execHelper *templateExecHelper
+ executer texttemplate.Executer
+}
+
+type weight struct {
+ w1 int
+ w2 int
+ w3 int
+ distance int
+}
+
+func (w weight) isEqualWeights(other weight) bool {
+ return w.w1 == other.w1 && w.w2 == other.w2 && w.w3 == other.w3
+}
+
+func isLayoutCustom(s string) bool {
+ if s == "" || isLayoutStandard(s) {
+ return false
+ }
+ return true
+}
+
+func isLayoutStandard(s string) bool {
+ switch s {
+ case layoutAll, layoutList, layoutSingle:
+ return true
+ default:
+ return false
+ }
+}
+
+func configureSiteStorage(opts SiteOptions, watching bool) *storeSite {
+ funcsv := make(map[string]reflect.Value)
+
+ for k, v := range opts.TemplateFuncs {
+ vv := reflect.ValueOf(v)
+ funcsv[k] = vv
+ }
+
+ // Duplicate Go's internal funcs here for faster lookups.
+ for k, v := range htmltemplate.GoFuncs {
+ if _, exists := funcsv[k]; !exists {
+ vv, ok := v.(reflect.Value)
+ if !ok {
+ vv = reflect.ValueOf(v)
+ }
+ funcsv[k] = vv
+ }
+ }
+
+ for k, v := range texttemplate.GoFuncs {
+ if _, exists := funcsv[k]; !exists {
+ funcsv[k] = v
+ }
+ }
+
+ s := &storeSite{
+ opts: opts,
+ execHelper: &templateExecHelper{
+ watching: watching,
+ funcs: funcsv,
+ site: reflect.ValueOf(opts.Site),
+ siteParams: reflect.ValueOf(opts.Site.Params()),
+ },
+ }
+
+ s.executer = texttemplate.NewExecuter(s.execHelper)
+
+ return s
+}
(DIR) diff --git a/tpl/tplimpl/templatestore_integration_test.go b/tpl/tplimpl/templatestore_integration_test.go
@@ -0,0 +1,842 @@
+package tplimpl_test
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/resources/kinds"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
+)
+
+// Old as in before Hugo v0.146.0.
+func TestLayoutsOldSetup(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[languages]
+[languages.en]
+title = "Title in English"
+weight = 1
+[languages.nn]
+title = "Tittel på nynorsk"
+weight = 2
+-- layouts/index.html --
+Home.
+{{ template "_internal/twitter_cards.html" . }}
+-- layouts/_default/single.html --
+Single.
+-- layouts/_default/single.nn.html --
+Single NN.
+-- layouts/_default/list.html --
+List HTML.
+-- layouts/docs/list-baseof.html --
+Docs Baseof List HTML.
+{{ block "main" . }}Docs Baseof List HTML main block.{{ end }}
+-- layouts/docs/list.section.html --
+{{ define "main" }}
+Docs List HTML.
+{{ end }}
+-- layouts/_default/list.json --
+List JSON.
+-- layouts/_default/list.rss.xml --
+List RSS.
+-- layouts/_default/list.nn.rss.xml --
+List NN RSS.
+-- layouts/_default/baseof.html --
+Base.
+-- layouts/partials/mypartial.html --
+Partial.
+-- layouts/shortcodes/myshortcode.html --
+Shortcode.
+-- content/docs/p1.md --
+---
+title: "P1"
+---
+
+ `
+
+ b := hugolib.Test(t, files)
+
+ // b.DebugPrint("", tplimpl.CategoryBaseof)
+
+ b.AssertFileContent("public/en/docs/index.html", "Docs Baseof List HTML.\n\nDocs List HTML.")
+}
+
+func TestLayoutsOldSetupBaseofPrefix(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/_default/layout1-baseof.html --
+Baseof layout1. {{ block "main" . }}{{ end }}
+-- layouts/_default/layout2-baseof.html --
+Baseof layout2. {{ block "main" . }}{{ end }}
+-- layouts/_default/layout1.html --
+{{ define "main" }}Layout1. {{ .Title }}{{ end }}
+-- layouts/_default/layout2.html --
+{{ define "main" }}Layout2. {{ .Title }}{{ end }}
+-- content/p1.md --
+---
+title: "P1"
+layout: "layout1"
+---
+-- content/p2.md --
+---
+title: "P2"
+layout: "layout2"
+---
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "Baseof layout1. Layout1. P1")
+ b.AssertFileContent("public/p2/index.html", "Baseof layout2. Layout2. P2")
+}
+
+func TestLayoutsOldSetupTaxonomyAndTerm(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+[taxonomies]
+cat = 'cats'
+dog = 'dogs'
+# Templates for term taxonomy, old setup.
+-- layouts/dogs/terms.html --
+Dogs Terms. Most specific taxonomy template.
+-- layouts/taxonomy/terms.html --
+Taxonomy Terms. Down the list.
+# Templates for term term, old setup.
+-- layouts/dogs/term.html --
+Dogs Term. Most specific term template.
+-- layouts/term/term.html --
+Term Term. Down the list.
+-- layouts/dogs/max/list.html --
+max: {{ .Title }}
+-- layouts/_default/list.html --
+Default list.
+-- layouts/_default/single.html --
+Default single.
+-- content/p1.md --
+---
+title: "P1"
+dogs: ["luna", "daisy", "max"]
+---
+
+`
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ b.AssertLogContains("! WARN")
+
+ b.AssertFileContent("public/dogs/index.html", "Dogs Terms. Most specific taxonomy template.")
+ b.AssertFileContent("public/dogs/luna/index.html", "Dogs Term. Most specific term template.")
+ b.AssertFileContent("public/dogs/max/index.html", "max: Max") // layouts/dogs/max/list.html wins over layouts/term/term.html because of distance.
+}
+
+func TestLayoutsOldSetupCustomRSS(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "page"]
+[outputs]
+home = ["rss"]
+-- layouts/_default/list.rss.xml --
+List RSS.
+`
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.xml", "List RSS.")
+}
+
+var newSetupTestSites = `
+-- hugo.toml --
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[languages]
+[languages.en]
+title = "Title in English"
+weight = 1
+[languages.nn]
+title = "Tittel på nynorsk"
+weight = 2
+[languages.fr]
+title = "Titre en français"
+weight = 3
+
+[outputs]
+home = ["html", "rss", "redir"]
+
+[outputFormats]
+[outputFormats.redir]
+mediatype = "text/plain"
+baseName = "_redirects"
+isPlainText = true
+-- layouts/404.html --
+{{ define "main" }}
+404.
+{{ end }}
+-- layouts/home.html --
+{{ define "main" }}
+Home: {{ .Title }}|{{ .Content }}|
+Inline Partial: {{ partial "my-inline-partial.html" . }}
+{{ end }}
+{{ define "hero" }}
+Home hero.
+{{ end }}
+{{ define "partials/my-inline-partial.html" }}
+{{ $value := 32 }}
+{{ return $value }}
+{{ end }}
+-- layouts/index.redir --
+Redir.
+-- layouts/single.html --
+{{ define "main" }}
+Single needs base.
+{{ end }}
+-- layouts/foo/bar/single.html --
+{{ define "main" }}
+Single sub path.
+{{ end }}
+-- layouts/_markup/render-codeblock.html --
+Render codeblock.
+-- layouts/_markup/render-blockquote.html --
+Render blockquote.
+-- layouts/_markup/render-codeblock-go.html --
+ Render codeblock go.
+-- layouts/_markup/render-link.html --
+Link: {{ .Destination | safeURL }}
+-- layouts/foo/baseof.html --
+Base sub path.{{ block "main" . }}{{ end }}
+-- layouts/foo/bar/baseof.page.html --
+Base sub path.{{ block "main" . }}{{ end }}
+-- layouts/list.html --
+{{ define "main" }}
+List needs base.
+{{ end }}
+-- layouts/section.html --
+Section.
+-- layouts/mysectionlayout.section.fr.amp.html --
+Section with layout.
+-- layouts/baseof.html --
+Base.{{ block "main" . }}{{ end }}
+Hero:{{ block "hero" . }}{{ end }}:
+{{ with (templates.Defer (dict "key" "global")) }}
+Defer Block.
+{{ end }}
+-- layouts/baseof.fr.html --
+Base fr.{{ block "main" . }}{{ end }}
+-- layouts/baseof.term.html --
+Base term.
+-- layouts/baseof.section.fr.amp.html --
+Base with identifiers.{{ block "main" . }}{{ end }}
+-- layouts/partials/mypartial.html --
+Partial. {{ partial "_inline/my-inline-partial-in-partial-with-no-ext" . }}
+{{ define "partials/_inline/my-inline-partial-in-partial-with-no-ext" }}
+Partial in partial.
+{{ end }}
+-- layouts/partials/returnfoo.html --
+{{ $v := "foo" }}
+{{ return $v }}
+-- layouts/shortcodes/myshortcode.html --
+Shortcode. {{ partial "mypartial.html" . }}|return:{{ partial "returnfoo.html" . }}|
+-- content/_index.md --
+---
+title: Home sweet home!
+---
+
+{{< myshortcode >}}
+
+> My blockquote.
+
+
+Markdown link: [Foo](/foo)
+-- content/p1.md --
+---
+title: "P1"
+---
+-- content/foo/bar/index.md --
+---
+title: "Foo Bar"
+---
+
+{{< myshortcode >}}
+
+-- content/single-list.md --
+---
+title: "Single List"
+layout: "list"
+---
+
+`
+
+func TestLayoutsType(t *testing.T) {
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+-- layouts/list.html --
+List.
+-- layouts/mysection/single.html --
+mysection/single|{{ .Title }}
+-- layouts/mytype/single.html --
+mytype/single|{{ .Title }}
+-- content/mysection/_index.md --
+-- content/mysection/mysubsection/_index.md --
+-- content/mysection/mysubsection/p1.md --
+---
+title: "P1"
+---
+-- content/mysection/mysubsection/p2.md --
+---
+title: "P2"
+type: "mytype"
+---
+
+`
+
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ b.AssertLogContains("! WARN")
+
+ b.AssertFileContent("public/mysection/mysubsection/p1/index.html", "mysection/single|P1")
+ b.AssertFileContent("public/mysection/mysubsection/p2/index.html", "mytype/single|P2")
+}
+
+// New, as in from Hugo v0.146.0.
+func TestLayoutsNewSetup(t *testing.T) {
+ const numIterations = 1
+ for range numIterations {
+
+ b := hugolib.Test(t, newSetupTestSites, hugolib.TestOptWarn())
+
+ b.AssertLogContains("! WARN")
+
+ b.AssertFileContent("public/en/index.html",
+ "Base.\nHome: Home sweet home!|",
+ "|Shortcode.\n|",
+ "<p>Markdown link: Link: /foo</p>",
+ "|return:foo|",
+ "Defer Block.",
+ "Home hero.",
+ "Render blockquote.",
+ )
+
+ b.AssertFileContent("public/en/p1/index.html", "Base.\nSingle needs base.\n\nHero::\n\nDefer Block.")
+ b.AssertFileContent("public/en/404.html", "404.")
+ b.AssertFileContent("public/nn/404.html", "404.")
+ b.AssertFileContent("public/fr/404.html", "404.")
+
+ }
+}
+
+func TestHomeRSSAndHTMLWithHTMLOnlyShortcode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+[outputs]
+home = ["html", "rss"]
+-- layouts/home.html --
+Home: {{ .Title }}|{{ .Content }}|
+-- layouts/single.html --
+Single: {{ .Title }}|{{ .Content }}|
+-- layouts/shortcodes/myshortcode.html --
+Myshortcode: Count: {{ math.Counter }}|
+-- content/p1.md --
+---
+title: "P1"
+---
+
+{{< myshortcode >}}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "Single: P1|Myshortcode: Count: 1|")
+ b.AssertFileContent("public/index.xml", "Myshortcode: Count: 1")
+}
+
+func TestHomeRSSAndHTMLWithHTMLOnlyRenderHook(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+[outputs]
+home = ["html", "rss"]
+-- layouts/home.html --
+Home: {{ .Title }}|{{ .Content }}|
+-- layouts/single.html --
+Single: {{ .Title }}|{{ .Content }}|
+-- layouts/_markup/render-link.html --
+Render Link: {{ math.Counter }}|
+-- content/p1.md --
+---
+title: "P1"
+---
+
+Link: [Foo](/foo)
+`
+
+ for range 2 {
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.xml", "Link: Render Link: 1|")
+ b.AssertFileContent("public/p1/index.html", "Single: P1|<p>Link: Render Link: 1|<")
+ }
+}
+
+func TestRenderCodeblockSpecificity(t *testing.T) {
+ files := `
+-- hugo.toml --
+-- layouts/_markup/render-codeblock.html --
+Render codeblock.|{{ .Inner }}|
+-- layouts/_markup/render-codeblock-go.html --
+Render codeblock go.|{{ .Inner }}|
+-- layouts/single.html --
+{{ .Title }}|{{ .Content }}|
+-- content/p1.md --
+---
+title: "P1"
+---
+
+§§§
+Basic
+§§§
+
+§§§ go
+Go
+§§§
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "P1|Render codeblock.|Basic|Render codeblock go.|Go|")
+}
+
+func TestPrintUnusedTemplates(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- config.toml --
+baseURL = 'http://example.com/'
+printUnusedTemplates=true
+-- content/p1.md --
+---
+title: "P1"
+---
+{{< usedshortcode >}}
+-- layouts/baseof.html --
+{{ block "main" . }}{{ end }}
+-- layouts/baseof.json --
+{{ block "main" . }}{{ end }}
+-- layouts/index.html --
+{{ define "main" }}FOO{{ end }}
+-- layouts/_default/single.json --
+-- layouts/_default/single.html --
+{{ define "main" }}MAIN{{ end }}
+-- layouts/post/single.html --
+{{ define "main" }}MAIN{{ end }}
+-- layouts/_partials/usedpartial.html --
+-- layouts/_partials/unusedpartial.html --
+-- layouts/_shortcodes/usedshortcode.html --
+{{ partial "usedpartial.html" }}
+-- layouts/shortcodes/unusedshortcode.html --
+
+ `
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ NeedsOsFS: true,
+ },
+ )
+ b.Build()
+
+ unused := b.H.GetTemplateStore().UnusedTemplates()
+ var names []string
+ for _, tmpl := range unused {
+ if fi := tmpl.Fi; fi != nil {
+ names = append(names, fi.Meta().PathInfo.PathNoLeadingSlash())
+ }
+ }
+ b.Assert(len(unused), qt.Equals, 5, qt.Commentf("%#v", names))
+ b.Assert(names, qt.DeepEquals, []string{"_partials/unusedpartial.html", "shortcodes/unusedshortcode.html", "baseof.json", "post/single.html", "_default/single.json"})
+}
+
+func TestCreateManyTemplateStores(t *testing.T) {
+ t.Parallel()
+ b := hugolib.Test(t, newSetupTestSites)
+ store := b.H.TemplateStore
+
+ for range 70 {
+ newStore, err := store.NewFromOpts()
+ b.Assert(err, qt.IsNil)
+ b.Assert(newStore, qt.Not(qt.IsNil))
+ }
+}
+
+func BenchmarkLookupPagesLayout(b *testing.B) {
+ files := `
+-- hugo.toml --
+-- layouts/single.html --
+{{ define "main" }}
+ Main.
+{{ end }}
+-- layouts/baseof.html --
+baseof: {{ block "main" . }}{{ end }}
+-- layouts/foo/bar/single.html --
+{{ define "main" }}
+ Main.
+{{ end }}
+
+`
+ bb := hugolib.Test(b, files)
+ store := bb.H.TemplateStore
+
+ b.ResetTimer()
+ b.Run("Single root", func(b *testing.B) {
+ q := tplimpl.TemplateQuery{
+ Path: "/baz",
+ Category: tplimpl.CategoryLayout,
+ Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, Layout: "single", OutputFormat: "html"},
+ }
+ for i := 0; i < b.N; i++ {
+ store.LookupPagesLayout(q)
+ }
+ })
+
+ b.Run("Single sub folder", func(b *testing.B) {
+ q := tplimpl.TemplateQuery{
+ Path: "/foo/bar",
+ Category: tplimpl.CategoryLayout,
+ Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, Layout: "single", OutputFormat: "html"},
+ }
+ for i := 0; i < b.N; i++ {
+ store.LookupPagesLayout(q)
+ }
+ })
+}
+
+func BenchmarkNewTemplateStore(b *testing.B) {
+ bb := hugolib.Test(b, newSetupTestSites)
+ store := bb.H.TemplateStore
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ newStore, err := store.NewFromOpts()
+ if err != nil {
+ b.Fatal(err)
+ }
+ if newStore == nil {
+ b.Fatal("newStore is nil")
+ }
+ }
+}
+
+func TestLayoutsLookupVariants(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[outputs]
+home = ["html", "rss"]
+page = ["html", "rss", "amp"]
+section = ["html", "rss"]
+
+[languages]
+[languages.en]
+title = "Title in English"
+weight = 1
+[languages.nn]
+title = "Tittel på nynorsk"
+weight = 2
+-- layouts/list.xml --
+layouts/list.xml
+-- layouts/_shortcodes/myshortcode.html --
+layouts/shortcodes/myshortcode.html
+-- layouts/foo/bar/_shortcodes/myshortcode.html --
+layouts/foo/bar/_shortcodes/myshortcode.html
+-- layouts/_markup/render-codeblock.html --
+layouts/_markup/render-codeblock.html|{{ .Type }}|
+-- layouts/_markup/render-codeblock-go.html --
+layouts/_markup/render-codeblock-go.html|{{ .Type }}|
+-- layouts/single.xml --
+layouts/single.xml
+-- layouts/single.rss.xml --
+layouts/single.rss.xml
+-- layouts/single.nn.rss.xml --
+layouts/single.nn.rss.xml
+-- layouts/list.html --
+layouts/list.html
+-- layouts/single.html --
+layouts/single.html
+{{ .Content }}
+-- layouts/mylayout.html --
+layouts/mylayout.html
+-- layouts/mylayout.nn.html --
+layouts/mylayout.nn.html
+-- layouts/foo/single.rss.xml --
+layouts/foo/single.rss.xml
+-- layouts/foo/single.amp.html --
+layouts/foo/single.amp.html
+-- layouts/foo/bar/page.html --
+layouts/foo/bar/page.html
+-- layouts/foo/bar/baz/single.html --
+layouts/foo/bar/baz/single.html
+{{ .Content }}
+-- layouts/qux/mylayout.html --
+layouts/qux/mylayout.html
+-- layouts/qux/single.xml --
+layouts/qux/single.xml
+-- layouts/qux/mylayout.section.html --
+layouts/qux/mylayout.section.html
+-- content/p.md --
+---
+---
+§§§
+code
+§§§
+
+§§§ go
+code
+§§§
+
+{{< myshortcode >}}
+-- content/foo/p.md --
+-- content/foo/p.nn.md --
+-- content/foo/bar/p.md --
+-- content/foo/bar/withmylayout.md --
+---
+layout: mylayout
+---
+-- content/foo/bar/_index.md --
+-- content/foo/bar/baz/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/qux/p.md --
+-- content/qux/_index.md --
+---
+layout: mylayout
+---
+-- content/qux/quux/p.md --
+-- content/qux/quux/withmylayout.md --
+---
+layout: mylayout
+---
+-- content/qux/quux/withmylayout.nn.md --
+---
+layout: mylayout
+---
+
+
+`
+
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ // s := b.H.Sites[0].TemplateStore
+ // s.PrintDebug("", tplimpl.CategoryLayout, os.Stdout)
+
+ b.AssertLogContains("! WARN")
+
+ // Single pages.
+ // output format: html.
+ b.AssertFileContent("public/en/p/index.html", "layouts/single.html",
+ "layouts/_markup/render-codeblock.html|",
+ "layouts/_markup/render-codeblock-go.html|go|",
+ "layouts/shortcodes/myshortcode.html",
+ )
+ b.AssertFileContent("public/en/foo/p/index.html", "layouts/single.html")
+ b.AssertFileContent("public/en/foo/bar/p/index.html", "layouts/foo/bar/page.html")
+ b.AssertFileContent("public/en/foo/bar/withmylayout/index.html", "layouts/mylayout.html")
+ b.AssertFileContent("public/en/foo/bar/baz/p/index.html", "layouts/foo/bar/baz/single.html", "layouts/foo/bar/_shortcodes/myshortcode.html")
+ b.AssertFileContent("public/en/qux/quux/withmylayout/index.html", "layouts/qux/mylayout.html")
+ // output format: amp.
+ b.AssertFileContent("public/en/amp/p/index.html", "layouts/single.html")
+ b.AssertFileContent("public/en/amp/foo/p/index.html", "layouts/foo/single.amp.html")
+ // output format: rss.
+ b.AssertFileContent("public/en/p/index.xml", "layouts/single.rss.xml")
+ b.AssertFileContent("public/en/foo/p/index.xml", "layouts/foo/single.rss.xml")
+ b.AssertFileContent("public/nn/foo/p/index.xml", "layouts/single.nn.rss.xml")
+
+ // Note: There is qux/single.xml that's closer, but the one in the root is used becaulse of the output format match.
+ b.AssertFileContent("public/en/qux/p/index.xml", "layouts/single.rss.xml")
+
+ // Note.
+ b.AssertFileContent("public/nn/qux/quux/withmylayout/index.html", "layouts/mylayout.nn.html")
+
+ // Section pages.
+ // output format: html.
+ b.AssertFileContent("public/en/foo/index.html", "layouts/list.html")
+ b.AssertFileContent("public/en/qux/index.html", "layouts/qux/mylayout.section.html")
+ // output format: rss.
+ b.AssertFileContent("public/en/foo/index.xml", "layouts/list.xml")
+}
+
+func TestLookupShortcodeDepth(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/_shortcodes/myshortcode.html --
+layouts/_shortcodes/myshortcode.html
+-- layouts/foo/_shortcodes/myshortcode.html --
+layouts/foo/_shortcodes/myshortcode.html
+-- layouts/single.html --
+{{ .Content }}|
+-- content/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/bar/p.md --
+---
+---
+{{< myshortcode >}}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p/index.html", "layouts/_shortcodes/myshortcode.html")
+ b.AssertFileContent("public/foo/p/index.html", "layouts/foo/_shortcodes/myshortcode.html")
+ b.AssertFileContent("public/foo/bar/p/index.html", "layouts/foo/_shortcodes/myshortcode.html")
+}
+
+func TestLookupShortcodeLayout(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/_shortcodes/myshortcode.single.html --
+layouts/_shortcodes/myshortcode.single.html
+-- layouts/_shortcodes/myshortcode.list.html --
+layouts/_shortcodes/myshortcode.list.html
+-- layouts/single.html --
+{{ .Content }}|
+-- layouts/list.html --
+{{ .Content }}|
+-- content/_index.md --
+---
+---
+{{< myshortcode >}}
+-- content/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/bar/p.md --
+---
+---
+{{< myshortcode >}}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p/index.html", "layouts/_shortcodes/myshortcode.single.html")
+ b.AssertFileContent("public/index.html", "layouts/_shortcodes/myshortcode.list.html")
+}
+
+func TestLayoutAll(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/single.html --
+Single.
+-- layouts/all.html --
+All.
+-- content/p1.md --
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "Single.")
+ b.AssertFileContent("public/index.html", "All.")
+}
+
+func TestLayoutAllNested(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['rss','sitemap','taxonomy','term']
+-- content/s1/p1.md --
+---
+title: p1
+---
+-- content/s2/p2.md --
+---
+title: p2
+---
+-- layouts/single.html --
+layouts/single.html
+-- layouts/list.html --
+layouts/list.html
+-- layouts/s1/all.html --
+layouts/s1/all.html
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "layouts/list.html")
+ b.AssertFileContent("public/s1/index.html", "layouts/s1/all.html")
+ b.AssertFileContent("public/s1/p1/index.html", "layouts/s1/all.html")
+ b.AssertFileContent("public/s2/index.html", "layouts/list.html")
+ b.AssertFileContent("public/s2/p2/index.html", "layouts/single.html")
+}
+
+func TestPartialHTML(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/all.html --
+<html>
+<head>
+{{ partial "css.html" .}}
+</head>
+</html>
+-- layouts/partials/css.html --
+<link rel="stylesheet" href="/css/style.css">
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "<link rel=\"stylesheet\" href=\"/css/style.css\">")
+}
+
+// Issue #13515
+func TestPrintPathWarningOnDotRemoval(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+printPathWarnings = true
+-- content/v0.124.0.md --
+-- content/v0.123.0.md --
+-- layouts/all.html --
+All.
+-- layouts/_default/single.html --
+{{ .Title }}|
+`
+
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ b.AssertLogContains("Duplicate content path")
+}
(DIR) diff --git a/tpl/tplimpl/templatetransform.go b/tpl/tplimpl/templatetransform.go
@@ -0,0 +1,349 @@
+package tplimpl
+
+import (
+ "errors"
+ "fmt"
+ "slices"
+ "strings"
+
+ "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
+
+ htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
+ texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
+
+ "github.com/gohugoio/hugo/common/hashing"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/mitchellh/mapstructure"
+)
+
+type templateTransformContext struct {
+ visited map[string]bool
+ templateNotFound map[string]bool
+ deferNodes map[string]*parse.ListNode
+ lookupFn func(name string, in *TemplInfo) *TemplInfo
+
+ // The last error encountered.
+ err error
+
+ // Set when we're done checking for config header.
+ configChecked bool
+
+ t *TemplInfo
+
+ // Store away the return node in partials.
+ returnNode *parse.CommandNode
+}
+
+func (c templateTransformContext) getIfNotVisited(name string) *TemplInfo {
+ if c.visited[name] {
+ return nil
+ }
+ c.visited[name] = true
+ templ := c.lookupFn(name, c.t)
+ if templ == nil {
+ // This may be a inline template defined outside of this file
+ // and not yet parsed. Unusual, but it happens.
+ // Store the name to try again later.
+ c.templateNotFound[name] = true
+ }
+
+ return templ
+}
+
+func newTemplateTransformContext(
+ t *TemplInfo,
+ lookupFn func(name string, in *TemplInfo) *TemplInfo,
+) *templateTransformContext {
+ return &templateTransformContext{
+ t: t,
+ lookupFn: lookupFn,
+ visited: make(map[string]bool),
+ templateNotFound: make(map[string]bool),
+ deferNodes: make(map[string]*parse.ListNode),
+ }
+}
+
+func applyTemplateTransformers(
+ t *TemplInfo,
+ lookupFn func(name string, in *TemplInfo) *TemplInfo,
+) (*templateTransformContext, error) {
+ if t == nil {
+ return nil, errors.New("expected template, but none provided")
+ }
+
+ c := newTemplateTransformContext(t, lookupFn)
+ c.t.ParseInfo = defaultParseInfo
+ tree := getParseTree(t.Template)
+ if tree == nil {
+ panic(fmt.Errorf("template %s not parsed", t))
+ }
+
+ _, err := c.applyTransformations(tree.Root)
+
+ if err == nil && c.returnNode != nil {
+ // This is a partial with a return statement.
+ c.t.ParseInfo.HasReturn = true
+ tree.Root = c.wrapInPartialReturnWrapper(tree.Root)
+ }
+
+ return c, err
+}
+
+func getParseTree(templ tpl.Template) *parse.Tree {
+ if text, ok := templ.(*texttemplate.Template); ok {
+ return text.Tree
+ }
+ return templ.(*htmltemplate.Template).Tree
+}
+
+const (
+ // We parse this template and modify the nodes in order to assign
+ // the return value of a partial to a contextWrapper via Set. We use
+ // "range" over a one-element slice so we can shift dot to the
+ // partial's argument, Arg, while allowing Arg to be falsy.
+ partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ range (slice .Arg) }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
+
+ doDeferTempl = `{{ doDefer ("PLACEHOLDER1") ("PLACEHOLDER2") }}`
+)
+
+var (
+ partialReturnWrapper *parse.ListNode
+ doDefer *parse.ListNode
+)
+
+func init() {
+ templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
+ if err != nil {
+ panic(err)
+ }
+ partialReturnWrapper = templ.Tree.Root
+
+ templ, err = texttemplate.New("").Funcs(texttemplate.FuncMap{"doDefer": func(string, string) string { return "" }}).Parse(doDeferTempl)
+ if err != nil {
+ panic(err)
+ }
+ doDefer = templ.Tree.Root
+}
+
+// wrapInPartialReturnWrapper copies and modifies the parsed nodes of a
+// predefined partial return wrapper to insert those of a user-defined partial.
+func (c *templateTransformContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
+ wrapper := partialReturnWrapper.CopyList()
+ rangeNode := wrapper.Nodes[2].(*parse.RangeNode)
+ retn := rangeNode.List.Nodes[0]
+ setCmd := retn.(*parse.ActionNode).Pipe.Cmds[0]
+ setPipe := setCmd.Args[1].(*parse.PipeNode)
+ // Replace PLACEHOLDER with the real return value.
+ // Note that this is a PipeNode, so it will be wrapped in parens.
+ setPipe.Cmds = []*parse.CommandNode{c.returnNode}
+ rangeNode.List.Nodes = append(n.Nodes, retn)
+
+ return wrapper
+}
+
+// applyTransformations do 2 things:
+// 1) Parses partial return statement.
+// 2) Tracks template (partial) dependencies and some other info.
+func (c *templateTransformContext) applyTransformations(n parse.Node) (bool, error) {
+ switch x := n.(type) {
+ case *parse.ListNode:
+ if x != nil {
+ c.applyTransformationsToNodes(x.Nodes...)
+ }
+ case *parse.ActionNode:
+ c.applyTransformationsToNodes(x.Pipe)
+ case *parse.IfNode:
+ c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
+ case *parse.WithNode:
+ c.handleDefer(x)
+ c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
+ case *parse.RangeNode:
+ c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
+ case *parse.TemplateNode:
+ subTempl := c.getIfNotVisited(x.Name)
+ if subTempl != nil {
+ c.applyTransformationsToNodes(getParseTree(subTempl.Template).Root)
+ }
+ case *parse.PipeNode:
+ c.collectConfig(x)
+ for i, cmd := range x.Cmds {
+ keep, _ := c.applyTransformations(cmd)
+ if !keep {
+ x.Cmds = slices.Delete(x.Cmds, i, i+1)
+ }
+ }
+
+ case *parse.CommandNode:
+ c.collectInner(x)
+ keep := c.collectReturnNode(x)
+
+ for _, elem := range x.Args {
+ switch an := elem.(type) {
+ case *parse.PipeNode:
+ c.applyTransformations(an)
+ }
+ }
+ return keep, c.err
+ }
+
+ return true, c.err
+}
+
+func (c *templateTransformContext) handleDefer(withNode *parse.WithNode) {
+ if len(withNode.Pipe.Cmds) != 1 {
+ return
+ }
+ cmd := withNode.Pipe.Cmds[0]
+ if len(cmd.Args) != 1 {
+ return
+ }
+ idArg := cmd.Args[0]
+
+ p, ok := idArg.(*parse.PipeNode)
+ if !ok {
+ return
+ }
+
+ if len(p.Cmds) != 1 {
+ return
+ }
+
+ cmd = p.Cmds[0]
+
+ if len(cmd.Args) != 2 {
+ return
+ }
+
+ idArg = cmd.Args[0]
+
+ id, ok := idArg.(*parse.ChainNode)
+ if !ok || len(id.Field) != 1 || id.Field[0] != "Defer" {
+ return
+ }
+ if id2, ok := id.Node.(*parse.IdentifierNode); !ok || id2.Ident != "templates" {
+ return
+ }
+
+ deferArg := cmd.Args[1]
+ cmd.Args = []parse.Node{idArg}
+
+ l := doDefer.CopyList()
+ n := l.Nodes[0].(*parse.ActionNode)
+
+ inner := withNode.List.CopyList()
+ s := inner.String()
+ if strings.Contains(s, "resources.PostProcess") {
+ c.err = errors.New("resources.PostProcess cannot be used in a deferred template")
+ return
+ }
+ innerHash := hashing.XxHashFromStringHexEncoded(s)
+ deferredID := tpl.HugoDeferredTemplatePrefix + innerHash
+
+ c.deferNodes[deferredID] = inner
+ withNode.List = l
+
+ n.Pipe.Cmds[0].Args[1].(*parse.PipeNode).Cmds[0].Args[0].(*parse.StringNode).Text = deferredID
+ n.Pipe.Cmds[0].Args[2] = deferArg
+}
+
+func (c *templateTransformContext) applyTransformationsToNodes(nodes ...parse.Node) {
+ for _, node := range nodes {
+ c.applyTransformations(node)
+ }
+}
+
+func (c *templateTransformContext) hasIdent(idents []string, ident string) bool {
+ return slices.Contains(idents, ident)
+}
+
+// collectConfig collects and parses any leading template config variable declaration.
+// This will be the first PipeNode in the template, and will be a variable declaration
+// on the form:
+//
+// {{ $_hugo_config:= `{ "version": 1 }` }}
+func (c *templateTransformContext) collectConfig(n *parse.PipeNode) {
+ if c.t.Category != CategoryShortcode {
+ return
+ }
+ if c.configChecked {
+ return
+ }
+ c.configChecked = true
+
+ if len(n.Decl) != 1 || len(n.Cmds) != 1 {
+ // This cannot be a config declaration
+ return
+ }
+
+ v := n.Decl[0]
+
+ if len(v.Ident) == 0 || v.Ident[0] != "$_hugo_config" {
+ return
+ }
+
+ cmd := n.Cmds[0]
+
+ if len(cmd.Args) == 0 {
+ return
+ }
+
+ if s, ok := cmd.Args[0].(*parse.StringNode); ok {
+ errMsg := "failed to decode $_hugo_config in template: %w"
+ m, err := maps.ToStringMapE(s.Text)
+ if err != nil {
+ c.err = fmt.Errorf(errMsg, err)
+ return
+ }
+ if err := mapstructure.WeakDecode(m, &c.t.ParseInfo.Config); err != nil {
+ c.err = fmt.Errorf(errMsg, err)
+ }
+ }
+}
+
+// collectInner determines if the given CommandNode represents a
+// shortcode call to its .Inner.
+func (c *templateTransformContext) collectInner(n *parse.CommandNode) {
+ if c.t.Category != CategoryShortcode {
+ return
+ }
+ if c.t.ParseInfo.IsInner || len(n.Args) == 0 {
+ return
+ }
+
+ for _, arg := range n.Args {
+ var idents []string
+ switch nt := arg.(type) {
+ case *parse.FieldNode:
+ idents = nt.Ident
+ case *parse.VariableNode:
+ idents = nt.Ident
+ }
+
+ if c.hasIdent(idents, "Inner") || c.hasIdent(idents, "InnerDeindent") {
+ c.t.ParseInfo.IsInner = true
+ break
+ }
+ }
+}
+
+func (c *templateTransformContext) collectReturnNode(n *parse.CommandNode) bool {
+ if c.t.Category != CategoryPartial || c.returnNode != nil {
+ return true
+ }
+
+ if len(n.Args) < 2 {
+ return true
+ }
+
+ ident, ok := n.Args[0].(*parse.IdentifierNode)
+ if !ok || ident.Ident != "return" {
+ return true
+ }
+
+ c.returnNode = n
+ // Remove the "return" identifiers
+ c.returnNode.Args = c.returnNode.Args[1:]
+
+ return false
+}
(DIR) diff --git a/tpl/tplimpl/tplimpl_integration_test.go b/tpl/tplimpl/tplimpl_integration_test.go
@@ -1,66 +1,13 @@
package tplimpl_test
import (
- "path/filepath"
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib"
- "github.com/gohugoio/hugo/tpl"
)
-func TestPrintUnusedTemplates(t *testing.T) {
- t.Parallel()
-
- files := `
--- config.toml --
-baseURL = 'http://example.com/'
-printUnusedTemplates=true
--- content/p1.md --
----
-title: "P1"
----
-{{< usedshortcode >}}
--- layouts/baseof.html --
-{{ block "main" . }}{{ end }}
--- layouts/baseof.json --
-{{ block "main" . }}{{ end }}
--- layouts/index.html --
-{{ define "main" }}FOO{{ end }}
--- layouts/_default/single.json --
--- layouts/_default/single.html --
-{{ define "main" }}MAIN{{ end }}
--- layouts/post/single.html --
-{{ define "main" }}MAIN{{ end }}
--- layouts/partials/usedpartial.html --
--- layouts/partials/unusedpartial.html --
--- layouts/shortcodes/usedshortcode.html --
-{{ partial "usedpartial.html" }}
--- layouts/shortcodes/unusedshortcode.html --
-
- `
-
- b := hugolib.NewIntegrationTestBuilder(
- hugolib.IntegrationTestConfig{
- T: t,
- TxtarString: files,
- NeedsOsFS: true,
- },
- )
- b.Build()
-
- unused := b.H.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
-
- var names []string
- for _, tmpl := range unused {
- names = append(names, tmpl.Name())
- }
-
- b.Assert(names, qt.DeepEquals, []string{"_default/single.json", "baseof.json", "partials/unusedpartial.html", "post/single.html", "shortcodes/unusedshortcode.html"})
- b.Assert(unused[0].Filename(), qt.Equals, filepath.Join(b.Cfg.WorkingDir, "layouts/_default/single.json"))
-}
-
// Verify that the new keywords in Go 1.18 is available.
func TestGo18Constructs(t *testing.T) {
t.Parallel()
@@ -627,9 +574,9 @@ Home!
b := hugolib.TestRunning(t, files)
b.AssertFileContent("public/index.html", "Home!")
- b.EditFileReplaceAll("layouts/_default/baseof.html", "Baseof", "Baseof!").Build()
+ b.EditFileReplaceAll("layouts/_default/baseof.html", "baseof", "Baseof!").Build()
b.BuildPartial("/")
- b.AssertFileContent("public/index.html", "Baseof!!")
+ b.AssertFileContent("public/index.html", "Baseof!")
b.BuildPartial("/mybundle1/")
- b.AssertFileContent("public/mybundle1/index.html", "Baseof!!")
+ b.AssertFileContent("public/mybundle1/index.html", "Baseof!")
}
(DIR) diff --git a/tpl/tplimplinit/tplimplinit.go b/tpl/tplimplinit/tplimplinit.go
@@ -0,0 +1,96 @@
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Portions Copyright The Go Authors.
+
+// 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 tplimplinit
+
+import (
+ // Init the template funcs namespaces
+ "context"
+ "html/template"
+
+ "github.com/gohugoio/hugo/deps"
+ _ "github.com/gohugoio/hugo/tpl/cast"
+ _ "github.com/gohugoio/hugo/tpl/collections"
+ _ "github.com/gohugoio/hugo/tpl/compare"
+ _ "github.com/gohugoio/hugo/tpl/crypto"
+ _ "github.com/gohugoio/hugo/tpl/css"
+ _ "github.com/gohugoio/hugo/tpl/data"
+ _ "github.com/gohugoio/hugo/tpl/debug"
+ _ "github.com/gohugoio/hugo/tpl/diagrams"
+ _ "github.com/gohugoio/hugo/tpl/encoding"
+ _ "github.com/gohugoio/hugo/tpl/fmt"
+ _ "github.com/gohugoio/hugo/tpl/hash"
+ _ "github.com/gohugoio/hugo/tpl/hugo"
+ _ "github.com/gohugoio/hugo/tpl/images"
+ _ "github.com/gohugoio/hugo/tpl/inflect"
+ "github.com/gohugoio/hugo/tpl/internal"
+ _ "github.com/gohugoio/hugo/tpl/js"
+ _ "github.com/gohugoio/hugo/tpl/lang"
+ _ "github.com/gohugoio/hugo/tpl/math"
+ _ "github.com/gohugoio/hugo/tpl/openapi/openapi3"
+ _ "github.com/gohugoio/hugo/tpl/os"
+ _ "github.com/gohugoio/hugo/tpl/page"
+ _ "github.com/gohugoio/hugo/tpl/partials"
+ _ "github.com/gohugoio/hugo/tpl/path"
+ _ "github.com/gohugoio/hugo/tpl/reflect"
+ _ "github.com/gohugoio/hugo/tpl/resources"
+ _ "github.com/gohugoio/hugo/tpl/safe"
+ _ "github.com/gohugoio/hugo/tpl/site"
+ _ "github.com/gohugoio/hugo/tpl/strings"
+ _ "github.com/gohugoio/hugo/tpl/templates"
+ _ "github.com/gohugoio/hugo/tpl/time"
+ _ "github.com/gohugoio/hugo/tpl/transform"
+ _ "github.com/gohugoio/hugo/tpl/urls"
+)
+
+// CreateFuncMap creates a template.FuncMap with all of Hugo's template funcs,
+// excluding the Go built-ins.
+func CreateFuncMap(d *deps.Deps) map[string]any {
+ funcMap := template.FuncMap{}
+ nsMap := make(map[string]any)
+ var onCreated []func(namespaces map[string]any)
+
+ // Merge the namespace funcs
+ for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
+ ns := nsf(d)
+ if _, exists := funcMap[ns.Name]; exists {
+ panic(ns.Name + " is a duplicate template func")
+ }
+ funcMap[ns.Name] = ns.Context
+ contextV, err := ns.Context(context.Background())
+ if err != nil {
+ panic(err)
+ }
+ nsMap[ns.Name] = contextV
+ for _, mm := range ns.MethodMappings {
+ for _, alias := range mm.Aliases {
+ if _, exists := funcMap[alias]; exists {
+ panic(alias + " is a duplicate template func")
+ }
+ funcMap[alias] = mm.Method
+ }
+ }
+
+ if ns.OnCreated != nil {
+ onCreated = append(onCreated, ns.OnCreated)
+ }
+ }
+
+ for _, f := range onCreated {
+ f(nsMap)
+ }
+
+ return funcMap
+}