summaryrefslogtreecommitdiffstats
path: root/tpl
diff options
context:
space:
mode:
Diffstat (limited to 'tpl')
-rw-r--r--tpl/collections/apply.go2
-rw-r--r--tpl/collections/apply_test.go5
-rw-r--r--tpl/collections/collections.go7
-rw-r--r--tpl/collections/collections_test.go4
-rw-r--r--tpl/collections/integration_test.go7
-rw-r--r--tpl/collections/where.go4
-rw-r--r--tpl/data/data.go4
-rw-r--r--tpl/data/resources.go3
-rw-r--r--tpl/data/resources_test.go7
-rw-r--r--tpl/debug/integration_test.go4
-rw-r--r--tpl/diagrams/diagrams.go2
-rw-r--r--tpl/diagrams/goat.go2
-rw-r--r--tpl/diagrams/init.go2
-rw-r--r--tpl/fmt/integration_test.go3
-rw-r--r--tpl/images/integration_test.go2
-rw-r--r--tpl/internal/go_templates/staticcheck.conf1
-rw-r--r--tpl/internal/go_templates/texttemplate/hugo_template.go19
-rw-r--r--tpl/internal/go_templates/texttemplate/hugo_template_test.go4
-rw-r--r--tpl/internal/templatefuncsRegistry.go2
-rw-r--r--tpl/js/js.go1
-rw-r--r--tpl/lang/lang_test.go9
-rw-r--r--tpl/math/math_test.go4
-rw-r--r--tpl/openapi/openapi3/integration_test.go2
-rw-r--r--tpl/openapi/openapi3/openapi3.go28
-rw-r--r--tpl/os/integration_test.go2
-rw-r--r--tpl/page/init.go4
-rw-r--r--tpl/page/integration_test.go29
-rw-r--r--tpl/partials/integration_test.go5
-rw-r--r--tpl/partials/partials.go31
-rw-r--r--tpl/reflect/reflect_test.go2
-rw-r--r--tpl/resources/integration_test.go7
-rw-r--r--tpl/resources/resources.go16
-rw-r--r--tpl/safe/init.go5
-rw-r--r--tpl/safe/safe.go7
-rw-r--r--tpl/safe/safe_test.go27
-rw-r--r--tpl/site/init.go2
-rw-r--r--tpl/strings/strings.go12
-rw-r--r--tpl/template.go76
-rw-r--r--tpl/template_info.go14
-rw-r--r--tpl/template_test.go2
-rw-r--r--tpl/templates/integration_test.go2
-rw-r--r--tpl/time/init.go2
-rw-r--r--tpl/time/time.go42
-rw-r--r--tpl/tplimpl/template.go136
-rw-r--r--tpl/tplimpl/template_ast_transformers.go47
-rw-r--r--tpl/tplimpl/template_ast_transformers_test.go1
-rw-r--r--tpl/tplimpl/template_errors.go23
-rw-r--r--tpl/tplimpl/template_funcs.go70
-rw-r--r--tpl/transform/integration_test.go4
-rw-r--r--tpl/transform/transform.go22
-rw-r--r--tpl/transform/unmarshal.go43
-rw-r--r--tpl/transform/unmarshal_test.go9
52 files changed, 362 insertions, 408 deletions
diff --git a/tpl/collections/apply.go b/tpl/collections/apply.go
index 397ba0fdb..3d50395b9 100644
--- a/tpl/collections/apply.go
+++ b/tpl/collections/apply.go
@@ -67,7 +67,7 @@ func (ns *Namespace) Apply(ctx context.Context, c any, fname string, args ...any
func applyFnToThis(ctx context.Context, fn, this reflect.Value, args ...any) (reflect.Value, error) {
num := fn.Type().NumIn()
- if num > 0 && fn.Type().In(0).Implements(hreflect.ContextInterface) {
+ if num > 0 && hreflect.IsContextType(fn.Type().In(0)) {
args = append([]any{ctx}, args...)
}
diff --git a/tpl/collections/apply_test.go b/tpl/collections/apply_test.go
index aa39923b7..0a5764264 100644
--- a/tpl/collections/apply_test.go
+++ b/tpl/collections/apply_test.go
@@ -22,6 +22,7 @@ import (
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"
@@ -29,6 +30,10 @@ import (
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
}
diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go
index e34753f17..61fd138e9 100644
--- a/tpl/collections/collections.go
+++ b/tpl/collections/collections.go
@@ -35,11 +35,6 @@ import (
"github.com/spf13/cast"
)
-func init() {
- // htime.Now cannot be used here
- rand.Seed(time.Now().UTC().UnixNano())
-}
-
// New returns a new instance of the collections-namespaced template functions.
func New(deps *deps.Deps) *Namespace {
language := deps.Conf.Language()
@@ -149,7 +144,7 @@ func (ns *Namespace) Delimit(ctx context.Context, l, sep any, last ...any) (stri
}
default:
- return "", fmt.Errorf("can't iterate over %v", l)
+ return "", fmt.Errorf("can't iterate over %T", l)
}
return str, nil
diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go
index dcdd3bd5c..7dd518759 100644
--- a/tpl/collections/collections_test.go
+++ b/tpl/collections/collections_test.go
@@ -699,7 +699,6 @@ func TestShuffleRandomising(t *testing.T) {
// of the sequence happens to be the same as the original sequence. However
// the probability of the event is 10^-158 which is negligible.
seqLen := 100
- rand.Seed(time.Now().UTC().UnixNano())
for _, test := range []struct {
seq []int
@@ -895,6 +894,7 @@ func (x TstX) TstRv2() string {
return "r" + x.B
}
+//lint:ignore U1000 reflect test
func (x TstX) unexportedMethod() string {
return x.unexported
}
@@ -923,7 +923,7 @@ func (x TstX) String() string {
type TstX struct {
A, B string
- unexported string
+ unexported string //lint:ignore U1000 reflect test
}
type TstParams struct {
diff --git a/tpl/collections/integration_test.go b/tpl/collections/integration_test.go
index a443755f8..24727a12c 100644
--- a/tpl/collections/integration_test.go
+++ b/tpl/collections/integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Hugo Authors. All rights reserved.
+// 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.
@@ -97,11 +97,9 @@ func TestAppendSliceToASliceOfSlices(t *testing.T) {
).Build()
b.AssertFileContent("public/index.html", "[[a] [b] [c]]")
-
}
func TestAppendNilToSlice(t *testing.T) {
-
t.Parallel()
files := `
@@ -123,11 +121,9 @@ func TestAppendNilToSlice(t *testing.T) {
).Build()
b.AssertFileContent("public/index.html", "[a <nil>]")
-
}
func TestAppendNilsToSliceWithNils(t *testing.T) {
-
t.Parallel()
files := `
@@ -153,7 +149,6 @@ func TestAppendNilsToSliceWithNils(t *testing.T) {
b.AssertFileContent("public/index.html", "[a <nil> c <nil>]")
}
-
}
// Issue 11234.
diff --git a/tpl/collections/where.go b/tpl/collections/where.go
index 07c2d3deb..bf3f75044 100644
--- a/tpl/collections/where.go
+++ b/tpl/collections/where.go
@@ -51,7 +51,7 @@ func (ns *Namespace) Where(ctx context.Context, c, key any, args ...any) (any, e
case reflect.Map:
return ns.checkWhereMap(ctxv, seqv, kv, mv, path, op)
default:
- return nil, fmt.Errorf("can't iterate over %v", c)
+ return nil, fmt.Errorf("can't iterate over %T", c)
}
}
@@ -320,7 +320,7 @@ func evaluateSubElem(ctx, obj reflect.Value, elemName string) (reflect.Value, er
mt := objPtr.Type().Method(index)
num := mt.Type.NumIn()
maxNumIn := 1
- if num > 1 && mt.Type.In(1).Implements(hreflect.ContextInterface) {
+ if num > 1 && hreflect.IsContextType(mt.Type.In(1)) {
args = []reflect.Value{ctx}
maxNumIn = 2
}
diff --git a/tpl/data/data.go b/tpl/data/data.go
index 380c25685..b6b0515e8 100644
--- a/tpl/data/data.go
+++ b/tpl/data/data.go
@@ -24,6 +24,7 @@ import (
"net/http"
"strings"
+ "github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config/security"
@@ -33,7 +34,6 @@ import (
"github.com/spf13/cast"
- "github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/deps"
)
@@ -108,7 +108,7 @@ func (ns *Namespace) GetJSON(args ...any) (any, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
- return nil, fmt.Errorf("Failed to create request for getJSON resource %s: %w", url, err)
+ return nil, fmt.Errorf("failed to create request for getJSON resource %s: %w", url, err)
}
unmarshal := func(b []byte) (bool, error) {
diff --git a/tpl/data/resources.go b/tpl/data/resources.go
index 45764dae7..3a3701d60 100644
--- a/tpl/data/resources.go
+++ b/tpl/data/resources.go
@@ -23,7 +23,6 @@ import (
"time"
"github.com/gohugoio/hugo/cache/filecache"
-
"github.com/gohugoio/hugo/helpers"
"github.com/spf13/afero"
)
@@ -68,7 +67,7 @@ func (ns *Namespace) getRemote(cache *filecache.Cache, unmarshal func([]byte) (b
res.Body.Close()
if isHTTPError(res) {
- return nil, fmt.Errorf("Failed to retrieve remote file: %s, body: %q", http.StatusText(res.StatusCode), b)
+ return nil, fmt.Errorf("failed to retrieve remote file: %s, body: %q", http.StatusText(res.StatusCode), b)
}
retry, err = unmarshal(b)
diff --git a/tpl/data/resources_test.go b/tpl/data/resources_test.go
index d452a2a43..b8003bf43 100644
--- a/tpl/data/resources_test.go
+++ b/tpl/data/resources_test.go
@@ -15,9 +15,6 @@ package data
import (
"bytes"
-
- "github.com/gohugoio/hugo/common/loggers"
-
"net/http"
"net/http/httptest"
"net/url"
@@ -26,12 +23,14 @@ import (
"testing"
"time"
+ "github.com/gohugoio/hugo/cache/filecache"
+ "github.com/gohugoio/hugo/common/loggers"
+
"github.com/gohugoio/hugo/config/testconfig"
"github.com/gohugoio/hugo/helpers"
qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs"
diff --git a/tpl/debug/integration_test.go b/tpl/debug/integration_test.go
index 3d120580d..9a36e2d12 100644
--- a/tpl/debug/integration_test.go
+++ b/tpl/debug/integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Hugo Authors. All rights reserved.
+// 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.
@@ -41,5 +41,5 @@ disableKinds = ["taxonomy", "term"]
},
).Build()
- b.AssertLogContains("imer: name \"foo\" count '\\x05' duration")
+ b.AssertLogContains("timer: name foo count 5 duration")
}
diff --git a/tpl/diagrams/diagrams.go b/tpl/diagrams/diagrams.go
index dfa29a978..6a58bcfe4 100644
--- a/tpl/diagrams/diagrams.go
+++ b/tpl/diagrams/diagrams.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Hugo Authors. All rights reserved.
+// 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.
diff --git a/tpl/diagrams/goat.go b/tpl/diagrams/goat.go
index f3d4f4bfb..fe156f1e8 100644
--- a/tpl/diagrams/goat.go
+++ b/tpl/diagrams/goat.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Hugo Authors. All rights reserved.
+// 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.
diff --git a/tpl/diagrams/init.go b/tpl/diagrams/init.go
index e6356ce9c..0cbec7e1b 100644
--- a/tpl/diagrams/init.go
+++ b/tpl/diagrams/init.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Hugo Authors. All rights reserved.
+// 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.
diff --git a/tpl/fmt/integration_test.go b/tpl/fmt/integration_test.go
index 5010fa90e..40bfefcdc 100644
--- a/tpl/fmt/integration_test.go
+++ b/tpl/fmt/integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Hugo Authors. All rights reserved.
+// 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.
@@ -41,5 +41,4 @@ ignoreErrors = ['error-b']
b.BuildE()
b.AssertLogMatches(`^ERROR a\nYou can suppress this error by adding the following to your site configuration:\nignoreErrors = \['error-a'\]\n$`)
-
}
diff --git a/tpl/images/integration_test.go b/tpl/images/integration_test.go
index ad810ad92..81f35e39c 100644
--- a/tpl/images/integration_test.go
+++ b/tpl/images/integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Hugo Authors. All rights reserved.
+// 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.
diff --git a/tpl/internal/go_templates/staticcheck.conf b/tpl/internal/go_templates/staticcheck.conf
new file mode 100644
index 000000000..9cf5a78a4
--- /dev/null
+++ b/tpl/internal/go_templates/staticcheck.conf
@@ -0,0 +1 @@
+checks = ["none"] \ No newline at end of file
diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go
index 78be55e18..4db40ce82 100644
--- a/tpl/internal/go_templates/texttemplate/hugo_template.go
+++ b/tpl/internal/go_templates/texttemplate/hugo_template.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Hugo Authors. All rights reserved.
+// 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.
@@ -59,23 +59,6 @@ func NewExecuter(helper ExecHelper) Executer {
return &executer{helper: helper}
}
-type (
- pageContextKeyType string
- hasLockContextKeyType string
- stackContextKeyType string
- callbackContextKeyType string
-)
-
-const (
- // The data page passed to ExecuteWithContext gets stored with this key.
- PageContextKey = pageContextKeyType("page")
- // Used in partialCached to signal to nested templates that a lock is already taken.
- HasLockContextKey = hasLockContextKeyType("hasLock")
-
- // Used to pass down a callback function to nested templates.
- CallbackContextKey = callbackContextKeyType("callback")
-)
-
// Note: The context is currently not fully implemented in Hugo. This is a work in progress.
func (t *executer) ExecuteWithContext(ctx context.Context, p Preparer, wr io.Writer, data any) error {
if ctx == nil {
diff --git a/tpl/internal/go_templates/texttemplate/hugo_template_test.go b/tpl/internal/go_templates/texttemplate/hugo_template_test.go
index cc88151e3..c68b747dd 100644
--- a/tpl/internal/go_templates/texttemplate/hugo_template_test.go
+++ b/tpl/internal/go_templates/texttemplate/hugo_template_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Hugo Authors. All rights reserved.
+// 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.
@@ -56,7 +56,7 @@ func (e *execHelper) GetMapValue(ctx context.Context, tmpl Preparer, m, key refl
return m.MapIndex(key), true
}
-func (e *execHelper) GetMethod(ctx context.Context, tmpl Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value) {
+func (e *execHelper) GetMethod(ctx context.Context, tmpl Preparer, receiver reflect.Value, name string) (reflect.Value, reflect.Value) {
if name != "Hello1" {
return zero, zero
}
diff --git a/tpl/internal/templatefuncsRegistry.go b/tpl/internal/templatefuncsRegistry.go
index c1b01f5a5..fc02a6ef9 100644
--- a/tpl/internal/templatefuncsRegistry.go
+++ b/tpl/internal/templatefuncsRegistry.go
@@ -170,7 +170,7 @@ func (namespaces TemplateFuncsNamespaces) MarshalJSON() ([]byte, error) {
for i, ns := range namespaces {
- b, err := ns.toJSON(context.TODO())
+ b, err := ns.toJSON(context.Background())
if err != nil {
return nil, err
}
diff --git a/tpl/js/js.go b/tpl/js/js.go
index bb8d20966..63a676532 100644
--- a/tpl/js/js.go
+++ b/tpl/js/js.go
@@ -34,7 +34,6 @@ func New(deps *deps.Deps) *Namespace {
// Namespace provides template functions for the "js" namespace.
type Namespace struct {
- deps *deps.Deps
client *js.Client
}
diff --git a/tpl/lang/lang_test.go b/tpl/lang/lang_test.go
index 8d5430f6f..6ec40cab3 100644
--- a/tpl/lang/lang_test.go
+++ b/tpl/lang/lang_test.go
@@ -41,8 +41,8 @@ func TestNumFmt(t *testing.T) {
{6, -12345.6789, "-|,| ", "|", "-12 345,678900"},
// Arabic, ar_AE
- {6, -12345.6789, "‏- ٫ ٬", "", "‏-12٬345٫678900"},
- {6, -12345.6789, "‏-|٫| ", "|", "‏-12 345٫678900"},
+ {6, -12345.6789, "\u200f- ٫ ٬", "", "\u200f-12٬345٫678900"},
+ {6, -12345.6789, "\u200f-|٫| ", "|", "\u200f-12 345٫678900"},
}
for _, cas := range cases {
@@ -65,7 +65,6 @@ func TestNumFmt(t *testing.T) {
}
func TestFormatNumbers(t *testing.T) {
-
c := qt.New(t)
nsNn := New(&deps.Deps{}, translators.GetTranslator("nn"))
@@ -103,12 +102,10 @@ func TestFormatNumbers(t *testing.T) {
c.Assert(err, qt.IsNil)
c.Assert(got, qt.Equals, "$20,000.00")
})
-
}
// Issue 9446
func TestLanguageKeyFormat(t *testing.T) {
-
c := qt.New(t)
nsUnderscoreUpper := New(&deps.Deps{}, translators.GetTranslator("es_ES"))
@@ -134,7 +131,5 @@ func TestLanguageKeyFormat(t *testing.T) {
got, err = nsHyphenLower.FormatNumber(3, pi)
c.Assert(err, qt.IsNil)
c.Assert(got, qt.Equals, "3,142")
-
})
-
}
diff --git a/tpl/math/math_test.go b/tpl/math/math_test.go
index 5b54b6ac8..4cde3fb85 100644
--- a/tpl/math/math_test.go
+++ b/tpl/math/math_test.go
@@ -335,7 +335,7 @@ func TestRound(t *testing.T) {
{0.5, 1.0},
{1.1, 1.0},
{1.5, 2.0},
- {-0.1, -0.0},
+ {-0.1, 0.0},
{-0.5, -1.0},
{-1.1, -1.0},
{-1.5, -2.0},
@@ -524,7 +524,6 @@ func TestSum(t *testing.T) {
_, err := ns.Sum()
c.Assert(err, qt.Not(qt.IsNil))
-
}
func TestProduct(t *testing.T) {
@@ -547,5 +546,4 @@ func TestProduct(t *testing.T) {
_, err := ns.Product()
c.Assert(err, qt.Not(qt.IsNil))
-
}
diff --git a/tpl/openapi/openapi3/integration_test.go b/tpl/openapi/openapi3/integration_test.go
index d3be0eda9..6914a60b3 100644
--- a/tpl/openapi/openapi3/integration_test.go
+++ b/tpl/openapi/openapi3/integration_test.go
@@ -67,7 +67,7 @@ API: {{ $api.Info.Title | safeHTML }}
b.AssertFileContent("public/index.html", `API: Sample API`)
b.
- EditFileReplace("assets/api/myapi.yaml", func(s string) string { return strings.ReplaceAll(s, "Sample API", "Hugo API") }).
+ EditFileReplaceFunc("assets/api/myapi.yaml", func(s string) string { return strings.ReplaceAll(s, "Sample API", "Hugo API") }).
Build()
b.AssertFileContent("public/index.html", `API: Hugo API`)
diff --git a/tpl/openapi/openapi3/openapi3.go b/tpl/openapi/openapi3/openapi3.go
index 38857dd98..f929c7f62 100644
--- a/tpl/openapi/openapi3/openapi3.go
+++ b/tpl/openapi/openapi3/openapi3.go
@@ -15,44 +15,42 @@
package openapi3
import (
+ "errors"
"fmt"
"io"
gyaml "github.com/ghodss/yaml"
- "errors"
-
kopenapi3 "github.com/getkin/kin-openapi/openapi3"
- "github.com/gohugoio/hugo/cache/namedmemcache"
+ "github.com/gohugoio/hugo/cache/dynacache"
"github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/resources/resource"
)
// New returns a new instance of the openapi3-namespaced template functions.
func New(deps *deps.Deps) *Namespace {
- // TODO(bep) consolidate when merging that "other branch" -- but be aware of the keys.
- cache := namedmemcache.New()
- deps.BuildStartListeners.Add(
- func() {
- cache.Clear()
- })
-
return &Namespace{
- cache: cache,
+ cache: dynacache.GetOrCreatePartition[string, *OpenAPIDocument](deps.MemCache, "/tmpl/openapi3", dynacache.OptionsPartition{Weight: 30, ClearWhen: dynacache.ClearOnChange}),
deps: deps,
}
}
// Namespace provides template functions for the "openapi3".
type Namespace struct {
- cache *namedmemcache.Cache
+ cache *dynacache.Partition[string, *OpenAPIDocument]
deps *deps.Deps
}
// OpenAPIDocument represents an OpenAPI 3 document.
type OpenAPIDocument struct {
*kopenapi3.T
+ identityGroup identity.Identity
+}
+
+func (o *OpenAPIDocument) GetIdentityGroup() identity.Identity {
+ return o.identityGroup
}
// Unmarshal unmarshals the given resource into an OpenAPI 3 document.
@@ -62,7 +60,7 @@ func (ns *Namespace) Unmarshal(r resource.UnmarshableResource) (*OpenAPIDocument
return nil, errors.New("no Key set in Resource")
}
- v, err := ns.cache.GetOrCreate(key, func() (any, error) {
+ v, err := ns.cache.GetOrCreate(key, func(string) (*OpenAPIDocument, error) {
f := metadecoders.FormatFromStrings(r.MediaType().Suffixes()...)
if f == "" {
return nil, fmt.Errorf("MIME %q not supported", r.MediaType())
@@ -92,11 +90,11 @@ func (ns *Namespace) Unmarshal(r resource.UnmarshableResource) (*OpenAPIDocument
err = kopenapi3.NewLoader().ResolveRefsIn(s, nil)
- return &OpenAPIDocument{T: s}, err
+ return &OpenAPIDocument{T: s, identityGroup: identity.FirstIdentity(r)}, err
})
if err != nil {
return nil, err
}
- return v.(*OpenAPIDocument), nil
+ return v, nil
}
diff --git a/tpl/os/integration_test.go b/tpl/os/integration_test.go
index d08374f8f..58e0ef70a 100644
--- a/tpl/os/integration_test.go
+++ b/tpl/os/integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Hugo Authors. All rights reserved.
+// 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.
diff --git a/tpl/page/init.go b/tpl/page/init.go
index 52aeaafd6..826aa45d3 100644
--- a/tpl/page/init.go
+++ b/tpl/page/init.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Hugo Authors. All rights reserved.
+// 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.
@@ -32,7 +32,7 @@ func init() {
ns := &internal.TemplateFuncsNamespace{
Name: name,
Context: func(ctx context.Context, args ...interface{}) (interface{}, error) {
- v := tpl.GetPageFromContext(ctx)
+ v := tpl.Context.Page.Get(ctx)
if v == nil {
// The multilingual sitemap does not have a page as its context.
return nil, nil
diff --git a/tpl/page/integration_test.go b/tpl/page/integration_test.go
index 74788377d..632c3b64e 100644
--- a/tpl/page/integration_test.go
+++ b/tpl/page/integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Hugo Authors. All rights reserved.
+// 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.
@@ -112,11 +112,11 @@ Bundled page: {{ $p2_1.Content }}
-- layouts/shortcodes/shortcode.html --
{{ if page.IsHome }}Shortcode {{ .Get 0 }} OK.{{ else }}Failed.{{ end }}
-- layouts/sitemap.xml --
-HRE?{{ if eq page . }}Sitemap OK.{{ else }}Failed.{{ end }}
+{{ if eq page . }}Sitemap OK.{{ else }}Failed.{{ end }}
-- layouts/robots.txt --
{{ if eq page . }}Robots OK.{{ else }}Failed.{{ end }}
-- layouts/sitemapindex.xml --
-{{ if not page }}SitemapIndex OK.{{ else }}Failed.{{ end }}
+{{ with page }}SitemapIndex OK: {{ .Kind }}{{ else }}Failed.{{ end }}
`
@@ -167,15 +167,12 @@ Shortcode in bundled page OK.
b.AssertFileContent("public/page/1/index.html", `Alias OK.`)
b.AssertFileContent("public/page/2/index.html", `Page OK.`)
if multilingual {
- b.AssertFileContent("public/sitemap.xml", `SitemapIndex OK.`)
+ b.AssertFileContent("public/sitemap.xml", `SitemapIndex OK: sitemapindex`)
} else {
b.AssertFileContent("public/sitemap.xml", `Sitemap OK.`)
}
-
})
-
}
-
}
// Issue 10791.
@@ -207,5 +204,23 @@ title: "P1"
).Build()
b.AssertFileContent("public/p1/index.html", "<nav id=\"TableOfContents\"></nav> \n<h1 id=\"heading-1\">Heading 1</h1>")
+}
+
+func TestFromStringRunning(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableLiveReload = true
+-- layouts/index.html --
+{{ with resources.FromString "foo" "{{ seq 3 }}" }}
+{{ with resources.ExecuteAsTemplate "bar" $ . }}
+ {{ .Content | safeHTML }}
+{{ end }}
+{{ end }}
+ `
+
+ b := hugolib.TestRunning(t, files)
+ b.AssertFileContent("public/index.html", "1\n2\n3")
}
diff --git a/tpl/partials/integration_test.go b/tpl/partials/integration_test.go
index 3dbaf2ce4..e48f3bb20 100644
--- a/tpl/partials/integration_test.go
+++ b/tpl/partials/integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Hugo Authors. All rights reserved.
+// 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.
@@ -297,7 +297,6 @@ timeout = '200ms'
b.Assert(err, qt.Not(qt.IsNil))
b.Assert(err.Error(), qt.Contains, "timed out")
-
}
func TestIncludeCachedTimeout(t *testing.T) {
@@ -322,7 +321,6 @@ timeout = '200ms'
b.Assert(err, qt.Not(qt.IsNil))
b.Assert(err.Error(), qt.Contains, "timed out")
-
}
// See Issue #10789
@@ -350,5 +348,4 @@ BAR
).Build()
b.AssertFileContent("public/index.html", "OO:BAR")
-
}
diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go
index 3834529ce..8e36e21b9 100644
--- a/tpl/partials/partials.go
+++ b/tpl/partials/partials.go
@@ -40,9 +40,10 @@ type partialCacheKey struct {
Variants []any
}
type includeResult struct {
- name string
- result any
- err error
+ name string
+ result any
+ mangager identity.Manager
+ err error
}
func (k partialCacheKey) Key() string {
@@ -65,7 +66,7 @@ type partialCache struct {
}
func (p *partialCache) clear() {
- p.cache.DeleteFunc(func(string, includeResult) bool {
+ p.cache.DeleteFunc(func(s string, r includeResult) bool {
return true
})
}
@@ -75,7 +76,7 @@ func New(deps *deps.Deps) *Namespace {
// This lazycache was introduced in Hugo 0.111.0.
// We're going to expand and consolidate all memory caches in Hugo using this,
// so just set a high limit for now.
- lru := lazycache.New[string, includeResult](lazycache.Options{MaxEntries: 1000})
+ lru := lazycache.New(lazycache.Options[string, includeResult]{MaxEntries: 1000})
cache := &partialCache{cache: lru}
deps.BuildStartListeners.Add(
@@ -142,11 +143,11 @@ func (ns *Namespace) includWithTimeout(ctx context.Context, name string, dataLis
case <-timeoutCtx.Done():
err := timeoutCtx.Err()
if err == context.DeadlineExceeded {
+ //lint:ignore ST1005 end user message.
err = fmt.Errorf("partial %q timed out after %s. This is most likely due to infinite recursion. If this is just a slow template, you can try to increase the 'timeout' config setting.", name, ns.deps.Conf.Timeout())
}
return includeResult{err: err}
}
-
}
// include is a helper function that lookups and executes the named partial.
@@ -215,7 +216,6 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
name: templ.Name(),
result: result,
}
-
}
// IncludeCached executes and caches partial templates. The cache is created with name+variants as the key.
@@ -226,12 +226,22 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
Name: name,
Variants: variants,
}
+ depsManagerIn := tpl.Context.GetDependencyManagerInCurrentScope(ctx)
r, found, err := ns.cachedPartials.cache.GetOrCreate(key.Key(), func(string) (includeResult, error) {
+ var depsManagerShared identity.Manager
+ if ns.deps.Conf.Watching() {
+ // We need to create a shared dependency manager to pass downwards
+ // and add those same dependencies to any cached invocation of this partial.
+ depsManagerShared = identity.NewManager("partials")
+ ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, depsManagerShared.(identity.DependencyManagerScopedProvider))
+ }
r := ns.includWithTimeout(ctx, key.Name, context)
+ if ns.deps.Conf.Watching() {
+ r.mangager = depsManagerShared
+ }
return r, r.err
})
-
if err != nil {
return nil, err
}
@@ -242,10 +252,13 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
// 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.TrackValue(key.templateName(), r.result, found)
}
+ if r.mangager != nil && depsManagerIn != nil {
+ depsManagerIn.AddIdentity(r.mangager)
+ }
+
return r.result, nil
}
diff --git a/tpl/reflect/reflect_test.go b/tpl/reflect/reflect_test.go
index f85af87dd..84ffe813b 100644
--- a/tpl/reflect/reflect_test.go
+++ b/tpl/reflect/reflect_test.go
@@ -21,8 +21,6 @@ import (
var ns = New()
-type tstNoStringer struct{}
-
func TestIsMap(t *testing.T) {
c := qt.New(t)
for _, test := range []struct {
diff --git a/tpl/resources/integration_test.go b/tpl/resources/integration_test.go
index 0e0a29a98..02aa5d29d 100644
--- a/tpl/resources/integration_test.go
+++ b/tpl/resources/integration_test.go
@@ -72,10 +72,9 @@ Copy3: /blog/js/copies/moo.a677329fc6c4ad947e0c7116d91f37a2.min.js|text/javascri
`)
- b.AssertDestinationExists("images/copy2.png", true)
+ b.AssertFileExists("public/images/copy2.png", true)
// No permalink used.
- b.AssertDestinationExists("images/copy3.png", false)
-
+ b.AssertFileExists("public/images/copy3.png", false)
}
func TestCopyPageShouldFail(t *testing.T) {
@@ -96,7 +95,6 @@ func TestCopyPageShouldFail(t *testing.T) {
}).BuildE()
b.Assert(err, qt.IsNotNil)
-
}
func TestGet(t *testing.T) {
@@ -125,5 +123,4 @@ Image OK
Empty string not found
`)
-
}
diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go
index d18797ebc..04af756ef 100644
--- a/tpl/resources/resources.go
+++ b/tpl/resources/resources.go
@@ -16,16 +16,15 @@ package resources
import (
"context"
+ "errors"
"fmt"
"sync"
- "errors"
-
"github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/tpl/internal/resourcehelpers"
- "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/resources/postpub"
"github.com/gohugoio/hugo/deps"
@@ -104,7 +103,6 @@ func (ns *Namespace) getscssClientDartSass() (*dartsass.Client, error) {
return
}
ns.deps.BuildClosers.Add(ns.scssClientDartSass)
-
})
return ns.scssClientDartSass, err
@@ -122,7 +120,6 @@ func (ns *Namespace) Copy(s any, r resource.Resource) (resource.Resource, error)
// Get locates the filename given in Hugo's assets filesystem
// and creates a Resource object that can be used for further transformations.
func (ns *Namespace) Get(filename any) resource.Resource {
-
filenamestr, err := cast.ToStringE(filename)
if err != nil {
panic(err)
@@ -172,7 +169,6 @@ func (ns *Namespace) GetRemote(args ...any) resource.Resource {
}
return ns.createClient.FromRemote(urlstr, options)
-
}
r, err := get(args...)
@@ -183,10 +179,8 @@ func (ns *Namespace) GetRemote(args ...any) resource.Resource {
default:
return resources.NewErrorResource(resource.NewResourceError(fmt.Errorf("error calling resources.GetRemote: %w", err), make(map[string]any)))
}
-
}
return r
-
}
// GetMatch finds the first Resource matching the given pattern, or nil if none found.
@@ -344,7 +338,6 @@ func (ns *Namespace) Minify(r resources.ResourceTransformer) (resource.Resource,
// as second argument. As an option, you can e.g. specify e.g. the target path (string)
// for the converted CSS resource.
func (ns *Namespace) ToCSS(args ...any) (resource.Resource, error) {
-
if len(args) > 2 {
return nil, errors.New("must not provide more arguments than resource object and options")
}
@@ -389,7 +382,7 @@ func (ns *Namespace) ToCSS(args ...any) (resource.Resource, error) {
if transpiler == transpilerLibSass {
var options scss.Options
if targetPath != "" {
- options.TargetPath = helpers.ToSlashTrimLeading(targetPath)
+ options.TargetPath = paths.ToSlashTrimLeading(targetPath)
} else if m != nil {
options, err = scss.DecodeOptions(m)
if err != nil {
@@ -413,12 +406,10 @@ func (ns *Namespace) ToCSS(args ...any) (resource.Resource, error) {
}
return client.ToCSS(r, m)
-
}
// PostCSS processes the given Resource with PostCSS
func (ns *Namespace) PostCSS(args ...any) (resource.Resource, error) {
-
if len(args) > 2 {
return nil, errors.New("must not provide more arguments than resource object and options")
}
@@ -438,7 +429,6 @@ func (ns *Namespace) PostProcess(r resource.Resource) (postpub.PostPublishedReso
// Babel processes the given Resource with Babel.
func (ns *Namespace) Babel(args ...any) (resource.Resource, error) {
-
if len(args) > 2 {
return nil, errors.New("must not provide more arguments than resource object and options")
}
diff --git a/tpl/safe/init.go b/tpl/safe/init.go
index 8fc0e82ea..3b498e6df 100644
--- a/tpl/safe/init.go
+++ b/tpl/safe/init.go
@@ -70,11 +70,6 @@ func init() {
},
)
- ns.AddMethodMapping(ctx.SanitizeURL,
- []string{"sanitizeURL", "sanitizeurl"},
- [][2]string{},
- )
-
return ns
}
diff --git a/tpl/safe/safe.go b/tpl/safe/safe.go
index d1a2e8d4e..81b4e0480 100644
--- a/tpl/safe/safe.go
+++ b/tpl/safe/safe.go
@@ -18,7 +18,6 @@ package safe
import (
"html/template"
- "github.com/gohugoio/hugo/helpers"
"github.com/spf13/cast"
)
@@ -65,9 +64,3 @@ func (ns *Namespace) URL(s any) (template.URL, error) {
ss, err := cast.ToStringE(s)
return template.URL(ss), err
}
-
-// SanitizeURL returns the string s as html/template URL content.
-func (ns *Namespace) SanitizeURL(s any) (string, error) {
- ss, err := cast.ToStringE(s)
- return helpers.SanitizeURL(ss), err
-}
diff --git a/tpl/safe/safe_test.go b/tpl/safe/safe_test.go
index 81fa40fd8..f2a54755d 100644
--- a/tpl/safe/safe_test.go
+++ b/tpl/safe/safe_test.go
@@ -182,30 +182,3 @@ func TestURL(t *testing.T) {
c.Assert(result, qt.Equals, test.expect)
}
}
-
-func TestSanitizeURL(t *testing.T) {
- t.Parallel()
- c := qt.New(t)
-
- ns := New()
-
- for _, test := range []struct {
- a any
- expect any
- }{
- {"http://foo/../../bar", "http://foo/bar"},
- // errors
- {tstNoStringer{}, false},
- } {
-
- result, err := ns.SanitizeURL(test.a)
-
- if b, ok := test.expect.(bool); ok && !b {
- c.Assert(err, qt.Not(qt.IsNil))
- continue
- }
-
- c.Assert(err, qt.IsNil)
- c.Assert(result, qt.Equals, test.expect)
- }
-}
diff --git a/tpl/site/init.go b/tpl/site/init.go
index 1c018e14e..1fcb309a0 100644
--- a/tpl/site/init.go
+++ b/tpl/site/init.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Hugo Authors. All rights reserved.
+// 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.
diff --git a/tpl/strings/strings.go b/tpl/strings/strings.go
index 9f16f1581..cd233b0a4 100644
--- a/tpl/strings/strings.go
+++ b/tpl/strings/strings.go
@@ -47,7 +47,7 @@ type Namespace struct {
func (ns *Namespace) CountRunes(s any) (int, error) {
ss, err := cast.ToStringE(s)
if err != nil {
- return 0, fmt.Errorf("Failed to convert content to string: %w", err)
+ return 0, fmt.Errorf("failed to convert content to string: %w", err)
}
counter := 0
@@ -64,7 +64,7 @@ func (ns *Namespace) CountRunes(s any) (int, error) {
func (ns *Namespace) RuneCount(s any) (int, error) {
ss, err := cast.ToStringE(s)
if err != nil {
- return 0, fmt.Errorf("Failed to convert content to string: %w", err)
+ return 0, fmt.Errorf("failed to convert content to string: %w", err)
}
return utf8.RuneCountInString(ss), nil
}
@@ -73,12 +73,12 @@ func (ns *Namespace) RuneCount(s any) (int, error) {
func (ns *Namespace) CountWords(s any) (int, error) {
ss, err := cast.ToStringE(s)
if err != nil {
- return 0, fmt.Errorf("Failed to convert content to string: %w", err)
+ return 0, fmt.Errorf("failed to convert content to string: %w", err)
}
isCJKLanguage, err := regexp.MatchString(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`, ss)
if err != nil {
- return 0, fmt.Errorf("Failed to match regex pattern against string: %w", err)
+ return 0, fmt.Errorf("failed to match regex pattern against string: %w", err)
}
if !isCJKLanguage {
@@ -103,11 +103,11 @@ func (ns *Namespace) CountWords(s any) (int, error) {
func (ns *Namespace) Count(substr, s any) (int, error) {
substrs, err := cast.ToStringE(substr)
if err != nil {
- return 0, fmt.Errorf("Failed to convert substr to string: %w", err)
+ return 0, fmt.Errorf("failed to convert substr to string: %w", err)
}
ss, err := cast.ToStringE(s)
if err != nil {
- return 0, fmt.Errorf("Failed to convert s to string: %w", err)
+ return 0, fmt.Errorf("failed to convert s to string: %w", err)
}
return strings.Count(ss, substrs), nil
}
diff --git a/tpl/template.go b/tpl/template.go
index 1f0127c66..e9725bd74 100644
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -23,6 +23,8 @@ import (
"unicode"
bp "github.com/gohugoio/hugo/bufferpool"
+ "github.com/gohugoio/hugo/common/hcontext"
+ "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/output/layouts"
"github.com/gohugoio/hugo/output"
@@ -69,6 +71,7 @@ type TemplateHandler interface {
ExecuteWithContext(ctx context.Context, t Template, wr io.Writer, data any) error
LookupLayout(d layouts.LayoutDescriptor, f output.Format) (Template, bool, error)
HasTemplate(name string) bool
+ GetIdentity(name string) (identity.Identity, bool)
}
type TemplateLookup interface {
@@ -95,6 +98,27 @@ type Template interface {
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)
@@ -111,18 +135,6 @@ type TemplateDebugger interface {
Debug()
}
-// templateInfo wraps a Template with some additional information.
-type templateInfo struct {
- Template
- Info
-}
-
-// templateInfo wraps a Template with some additional information.
-type templateInfoManager struct {
- Template
- InfoManager
-}
-
// TemplatesProvider as implemented by deps.Deps.
type TemplatesProvider interface {
Tmpl() TemplateHandler
@@ -144,34 +156,38 @@ type TemplateFuncGetter interface {
GetFunc(name string) (reflect.Value, bool)
}
-// GetPageFromContext returns the top level Page.
-func GetPageFromContext(ctx context.Context) any {
- return ctx.Value(texttemplate.PageContextKey)
+type contextKey string
+
+// Context manages values passed in the context to templates.
+var Context = struct {
+ DependencyManagerScopedProvider hcontext.ContextDispatcher[identity.DependencyManagerScopedProvider]
+ GetDependencyManagerInCurrentScope func(context.Context) identity.Manager
+ SetDependencyManagerInCurrentScope func(context.Context, identity.Manager) context.Context
+ DependencyScope hcontext.ContextDispatcher[int]
+ Page hcontext.ContextDispatcher[page]
+}{
+ DependencyManagerScopedProvider: hcontext.NewContextDispatcher[identity.DependencyManagerScopedProvider](contextKey("DependencyManagerScopedProvider")),
+ DependencyScope: hcontext.NewContextDispatcher[int](contextKey("DependencyScope")),
+ Page: hcontext.NewContextDispatcher[page](contextKey("Page")),
}
-// SetPageInContext sets the top level Page.
-func SetPageInContext(ctx context.Context, p page) context.Context {
- return context.WithValue(ctx, texttemplate.PageContextKey, p)
+func init() {
+ Context.GetDependencyManagerInCurrentScope = func(ctx context.Context) identity.Manager {
+ idmsp := Context.DependencyManagerScopedProvider.Get(ctx)
+ if idmsp != nil {
+ return idmsp.GetDependencyManagerForScope(Context.DependencyScope.Get(ctx))
+ }
+ return nil
+ }
}
type page interface {
IsNode() bool
}
-func GetCallbackFunctionFromContext(ctx context.Context) any {
- return ctx.Value(texttemplate.CallbackContextKey)
-}
-
-func SetCallbackFunctionInContext(ctx context.Context, fn any) context.Context {
- return context.WithValue(ctx, texttemplate.CallbackContextKey, fn)
-}
-
const hugoNewLinePlaceholder = "___hugonl_"
-var (
- stripHTMLReplacerPre = strings.NewReplacer("\n", " ", "</p>", hugoNewLinePlaceholder, "<br>", hugoNewLinePlaceholder, "<br />", hugoNewLinePlaceholder)
- whitespaceRe = regexp.MustCompile(`\s+`)
-)
+var stripHTMLReplacerPre = strings.NewReplacer("\n", " ", "</p>", hugoNewLinePlaceholder, "<br>", hugoNewLinePlaceholder, "<br />", hugoNewLinePlaceholder)
// StripHTML strips out all HTML tags in s.
func StripHTML(s string) string {
diff --git a/tpl/template_info.go b/tpl/template_info.go
index 5f748d682..b27debf1f 100644
--- a/tpl/template_info.go
+++ b/tpl/template_info.go
@@ -13,18 +13,11 @@
package tpl
-import (
- "github.com/gohugoio/hugo/identity"
-)
-
// Increments on breaking changes.
const TemplateVersion = 2
type Info interface {
ParseInfo() ParseInfo
-
- // Identifies this template and its dependencies.
- identity.Provider
}
type FileInfo interface {
@@ -32,13 +25,6 @@ type FileInfo interface {
Filename() string
}
-type InfoManager interface {
- ParseInfo() ParseInfo
-
- // Identifies and manages this template and its dependencies.
- identity.Manager
-}
-
type ParseInfo struct {
// Set for shortcode templates with any {{ .Inner }}
IsInner bool
diff --git a/tpl/template_test.go b/tpl/template_test.go
index d989b7158..333513a3d 100644
--- a/tpl/template_test.go
+++ b/tpl/template_test.go
@@ -67,5 +67,3 @@ More text here.</p>
}
}
}
-
-const tstHTMLContent = "<!DOCTYPE html><html><head><script src=\"http://two/foobar.js\"></script></head><body><nav><ul><li hugo-nav=\"section_0\"></li><li hugo-nav=\"section_1\"></li></ul></nav><article>content <a href=\"http://two/foobar\">foobar</a>. Follow up</article><p>This is some text.<br>And some more.</p></body></html>"
diff --git a/tpl/templates/integration_test.go b/tpl/templates/integration_test.go
index 7935fa5e3..7e0bcc824 100644
--- a/tpl/templates/integration_test.go
+++ b/tpl/templates/integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Hugo Authors. All rights reserved.
+// 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.
diff --git a/tpl/time/init.go b/tpl/time/init.go
index 01783270f..5f9dd77bf 100644
--- a/tpl/time/init.go
+++ b/tpl/time/init.go
@@ -51,7 +51,7 @@ func init() {
// 3 or more arguments. Currently not supported.
default:
- return nil, errors.New("Invalid arguments supplied to `time`. Refer to time documentation: https://gohugo.io/functions/time/")
+ return nil, errors.New("invalid arguments supplied to `time`")
}
},
}
diff --git a/tpl/time/time.go b/tpl/time/time.go
index cd78b83aa..57b115f35 100644
--- a/tpl/time/time.go
+++ b/tpl/time/time.go
@@ -17,7 +17,6 @@ package time
import (
"fmt"
"time"
- _time "time"
"github.com/gohugoio/hugo/common/htime"
@@ -47,14 +46,13 @@ func (ns *Namespace) AsTime(v any, args ...any) (any, error) {
if err != nil {
return nil, err
}
- loc, err = _time.LoadLocation(locStr)
+ loc, err = time.LoadLocation(locStr)
if err != nil {
return nil, err
}
}
return htime.ToTimeInDefaultLocationE(v, loc)
-
}
// Format converts the textual representation of the datetime string in v into
@@ -69,7 +67,7 @@ func (ns *Namespace) Format(layout string, v any) (string, error) {
}
// Now returns the current local time or `clock` time
-func (ns *Namespace) Now() _time.Time {
+func (ns *Namespace) Now() time.Time {
return htime.Now()
}
@@ -79,34 +77,34 @@ func (ns *Namespace) Now() _time.Time {
// such as "300ms", "-1.5h" or "2h45m".
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
// See https://golang.org/pkg/time/#ParseDuration
-func (ns *Namespace) ParseDuration(s any) (_time.Duration, error) {
+func (ns *Namespace) ParseDuration(s any) (time.Duration, error) {
ss, err := cast.ToStringE(s)
if err != nil {
return 0, err
}
- return _time.ParseDuration(ss)
+ return time.ParseDuration(ss)
}
-var durationUnits = map[string]_time.Duration{
- "nanosecond": _time.Nanosecond,
- "ns": _time.Nanosecond,
- "microsecond": _time.Microsecond,
- "us": _time.Microsecond,
- "µs": _time.Microsecond,
- "millisecond": _time.Millisecond,
- "ms": _time.Millisecond,
- "second": _time.Second,
- "s": _time.Second,
- "minute": _time.Minute,
- "m": _time.Minute,
- "hour": _time.Hour,
- "h": _time.Hour,
+var durationUnits = map[string]time.Duration{
+ "nanosecond": time.Nanosecond,
+ "ns": time.Nanosecond,
+ "microsecond": time.Microsecond,
+ "us": time.Microsecond,
+ "µs": time.Microsecond,
+ "millisecond": time.Millisecond,
+ "ms": time.Millisecond,
+ "second": time.Second,
+ "s": time.Second,
+ "minute": time.Minute,
+ "m": time.Minute,
+ "hour": time.Hour,
+ "h": time.Hour,
}
// Duration converts the given number to a time.Duration.
// Unit is one of nanosecond/ns, microsecond/us/µs, millisecond/ms, second/s, minute/m or hour/h.
-func (ns *Namespace) Duration(unit any, number any) (_time.Duration, error) {
+func (ns *Namespace) Duration(unit any, number any) (time.Duration, error) {
unitStr, err := cast.ToStringE(unit)
if err != nil {
return 0, err
@@ -119,5 +117,5 @@ func (ns *Namespace) Duration(unit any, number any) (_time.Duration, error) {
if err != nil {
return 0, err
}
- return _time.Duration(n) * unitDuration, nil
+ return time.Duration(n) * unitDuration, nil
}
diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go
index 053b53b53..a8ba6815d 100644
--- a/tpl/tplimpl/template.go
+++ b/tpl/tplimpl/template.go
@@ -42,7 +42,6 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/hugofs/files"
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
@@ -121,10 +120,6 @@ func needsBaseTemplate(templ string) bool {
return baseTemplateDefineRe.MatchString(templ[idx:])
}
-func newIdentity(name string) identity.Manager {
- return identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name))
-}
-
func newStandaloneTextTemplate(funcs map[string]any) tpl.TemplateParseFinder {
return &textTemplateWrapperWithLock{
RWMutex: &sync.RWMutex{},
@@ -147,7 +142,6 @@ func newTemplateHandlers(d *deps.Deps) (*tpl.TemplateHandlers, error) {
h := &templateHandler{
nameBaseTemplateName: make(map[string]string),
transformNotFound: make(map[string]*templateState),
- identityNotFound: make(map[string][]identity.Manager),
shortcodes: make(map[string]*shortcodeTemplates),
templateInfo: make(map[string]tpl.Info),
@@ -187,7 +181,6 @@ func newTemplateHandlers(d *deps.Deps) (*tpl.TemplateHandlers, error) {
Tmpl: e,
TxtTmpl: newStandaloneTextTemplate(funcMap),
}, nil
-
}
func newTemplateNamespace(funcs map[string]any) *templateNamespace {
@@ -200,13 +193,16 @@ func newTemplateNamespace(funcs map[string]any) *templateNamespace {
}
}
-func newTemplateState(templ tpl.Template, info templateInfo) *templateState {
+func newTemplateState(templ tpl.Template, info templateInfo, id identity.Identity) *templateState {
+ if id == nil {
+ id = info
+ }
return &templateState{
info: info,
typ: info.resolveType(),
Template: templ,
- Manager: newIdentity(info.name),
parseInfo: tpl.DefaultParseInfo,
+ id: id,
}
}
@@ -288,7 +284,7 @@ func (t *templateExec) UnusedTemplates() []tpl.FileInfo {
for _, ts := range t.main.templates {
ti := ts.info
- if strings.HasPrefix(ti.name, "_internal/") || ti.realFilename == "" {
+ if strings.HasPrefix(ti.name, "_internal/") || ti.meta == nil {
continue
}
@@ -346,9 +342,6 @@ type templateHandler struct {
// AST transformation pass.
transformNotFound map[string]*templateState
- // Holds identities of templates not found during first pass.
- identityNotFound map[string][]identity.Manager
-
// shortcodes maps shortcode name to template variants
// (language, output format etc.) of that shortcode.
shortcodes map[string]*shortcodeTemplates
@@ -405,7 +398,6 @@ func (t *templateHandler) LookupLayout(d layouts.LayoutDescriptor, f output.Form
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
@@ -456,6 +448,22 @@ func (t *templateHandler) HasTemplate(name string) bool {
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
@@ -488,13 +496,10 @@ func (t *templateHandler) findLayout(d layouts.LayoutDescriptor, f output.Format
return nil, false, err
}
- ts := newTemplateState(templ, overlay)
+ ts := newTemplateState(templ, overlay, identity.Or(base, overlay))
if found {
ts.baseInfo = base
-
- // Add the base identity to detect changes
- ts.Add(identity.NewPathIdentity(files.ComponentFolderLayouts, base.name))
}
t.applyTemplateTransformers(t.main, ts)
@@ -510,13 +515,6 @@ func (t *templateHandler) findLayout(d layouts.LayoutDescriptor, f output.Format
return nil, false, nil
}
-func (t *templateHandler) findTemplate(name string) *templateState {
- if templ, found := t.Lookup(name); found {
- return templ.(*templateState)
- }
- return nil
-}
-
func (t *templateHandler) newTemplateInfo(name, tpl string) templateInfo {
var isText bool
name, isText = t.nameIsText(name)
@@ -539,9 +537,8 @@ func (t *templateHandler) addFileContext(templ tpl.Template, inerr error) error
identifiers := t.extractIdentifiers(inerr.Error())
- //lint:ignore ST1008 the error is the main result
checkFilename := func(info templateInfo, inErr error) (error, bool) {
- if info.filename == "" {
+ if info.meta == nil {
return inErr, false
}
@@ -560,13 +557,13 @@ func (t *templateHandler) addFileContext(templ tpl.Template, inerr error) error
return -1
}
- f, err := t.layoutsFs.Open(info.filename)
+ f, err := info.meta.Open()
if err != nil {
return inErr, false
}
defer f.Close()
- fe := herrors.NewFileErrorFromName(inErr, info.realFilename)
+ fe := herrors.NewFileErrorFromName(inErr, info.meta.Filename)
fe.UpdateContent(f, lineMatcher)
if !fe.ErrorContext().Position.IsValid() {
@@ -621,37 +618,33 @@ func (t *templateHandler) addShortcodeVariant(ts *templateState) {
}
}
-func (t *templateHandler) addTemplateFile(name, path string) error {
- getTemplate := func(filename string) (templateInfo, error) {
- fs := t.Layouts.Fs
- b, err := afero.ReadFile(fs, filename)
+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{filename: filename, fs: fs}, err
+ 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))
- realFilename := filename
- if fi, err := fs.Stat(filename); err == nil {
- if fim, ok := fi.(hugofs.FileMetaInfo); ok {
- realFilename = fim.Meta().Filename
- }
- }
-
var isText bool
name, isText = t.nameIsText(name)
return templateInfo{
- name: name,
- isText: isText,
- template: s,
- filename: filename,
- realFilename: realFilename,
- fs: fs,
+ name: name,
+ isText: isText,
+ template: s,
+ meta: meta,
}, nil
}
- tinfo, err := getTemplate(path)
+ tinfo, err := getTemplate(fim)
if err != nil {
return err
}
@@ -741,11 +734,6 @@ func (t *templateHandler) applyTemplateTransformers(ns *templateNamespace, ts *t
for k := range c.templateNotFound {
t.transformNotFound[k] = ts
- t.identityNotFound[k] = append(t.identityNotFound[k], c.t)
- }
-
- for k := range c.identityNotFound {
- t.identityNotFound[k] = append(t.identityNotFound[k], c.t)
}
return c, err
@@ -804,9 +792,9 @@ func (t *templateHandler) loadEmbedded() error {
}
func (t *templateHandler) loadTemplates() error {
- walker := func(path string, fi hugofs.FileMetaInfo, err error) error {
- if err != nil || fi.IsDir() {
- return err
+ walker := func(path string, fi hugofs.FileMetaInfo) error {
+ if fi.IsDir() {
+ return nil
}
if isDotFile(path) || isBackupFile(path) {
@@ -822,14 +810,14 @@ func (t *templateHandler) loadTemplates() error {
name = textTmplNamePrefix + name
}
- if err := t.addTemplateFile(name, path); err != nil {
+ if err := t.addTemplateFile(name, fi); err != nil {
return err
}
return nil
}
- if err := helpers.SymbolicWalk(t.Layouts.Fs, "", walker); err != nil {
+ if err := helpers.Walk(t.Layouts.Fs, "", walker); err != nil {
if !herrors.IsNotExist(err) {
return err
}
@@ -861,7 +849,7 @@ func (t *templateHandler) extractPartials(templ tpl.Template) error {
continue
}
- ts := newTemplateState(templ, templateInfo{name: templ.Name()})
+ ts := newTemplateState(templ, templateInfo{name: templ.Name()}, nil)
ts.typ = templatePartial
t.main.mu.RLock()
@@ -927,15 +915,6 @@ func (t *templateHandler) postTransform() error {
}
}
- for k, v := range t.identityNotFound {
- ts := t.findTemplate(k)
- if ts != nil {
- for _, im := range v {
- im.Add(ts)
- }
- }
- }
-
for _, v := range t.shortcodes {
sort.Slice(v.variants, func(i, j int) bool {
v1, v2 := v.variants[i], v.variants[j]
@@ -1008,7 +987,7 @@ func (t *templateNamespace) newTemplateLookup(in *templateState) func(name strin
return templ
}
if templ, found := findTemplateIn(name, in); found {
- return newTemplateState(templ, templateInfo{name: templ.Name()})
+ return newTemplateState(templ, templateInfo{name: templ.Name()}, nil)
}
return nil
}
@@ -1026,7 +1005,7 @@ func (t *templateNamespace) parse(info templateInfo) (*templateState, error) {
return nil, err
}
- ts := newTemplateState(templ, info)
+ ts := newTemplateState(templ, info, nil)
t.templates[info.name] = ts
@@ -1040,7 +1019,7 @@ func (t *templateNamespace) parse(info templateInfo) (*templateState, error) {
return nil, err
}
- ts := newTemplateState(templ, info)
+ ts := newTemplateState(templ, info, nil)
t.templates[info.name] = ts
@@ -1052,12 +1031,16 @@ type templateState struct {
typ templateType
parseInfo tpl.ParseInfo
- identity.Manager
+ id identity.Identity
info templateInfo
baseInfo templateInfo // Set when a base template is used.
}
+func (t *templateState) GetIdentity() identity.Identity {
+ return t.id
+}
+
func (t *templateState) ParseInfo() tpl.ParseInfo {
return t.parseInfo
}
@@ -1066,6 +1049,10 @@ 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
@@ -1076,11 +1063,6 @@ type templateStateMap struct {
templates map[string]*templateState
}
-type templateWrapperWithLock struct {
- *sync.RWMutex
- tpl.Template
-}
-
type textTemplateWrapperWithLock struct {
*sync.RWMutex
*texttemplate.Template
diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go
index 8d5d8d1b3..92558a903 100644
--- a/tpl/tplimpl/template_ast_transformers.go
+++ b/tpl/tplimpl/template_ast_transformers.go
@@ -14,17 +14,14 @@
package tplimpl
import (
+ "errors"
"fmt"
- "regexp"
- "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"
- "errors"
-
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/tpl"
"github.com/mitchellh/mapstructure"
@@ -41,7 +38,6 @@ const (
type templateContext struct {
visited map[string]bool
templateNotFound map[string]bool
- identityNotFound map[string]bool
lookupFn func(name string) *templateState
// The last error encountered.
@@ -74,19 +70,20 @@ func (c templateContext) getIfNotVisited(name string) *templateState {
func newTemplateContext(
t *templateState,
- lookupFn func(name string) *templateState) *templateContext {
+ lookupFn func(name string) *templateState,
+) *templateContext {
return &templateContext{
t: t,
lookupFn: lookupFn,
visited: make(map[string]bool),
templateNotFound: make(map[string]bool),
- identityNotFound: make(map[string]bool),
}
}
func applyTemplateTransformers(
t *templateState,
- lookupFn func(name string) *templateState) (*templateContext, error) {
+ lookupFn func(name string) *templateState,
+) (*templateContext, error) {
if t == nil {
return nil, errors.New("expected template, but none provided")
}
@@ -179,7 +176,6 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
}
case *parse.CommandNode:
- c.collectPartialInfo(x)
c.collectInner(x)
keep := c.collectReturnNode(x)
@@ -280,39 +276,6 @@ func (c *templateContext) collectInner(n *parse.CommandNode) {
}
}
-var partialRe = regexp.MustCompile(`^partial(Cached)?$|^partials\.Include(Cached)?$`)
-
-func (c *templateContext) collectPartialInfo(x *parse.CommandNode) {
- if len(x.Args) < 2 {
- return
- }
-
- first := x.Args[0]
- var id string
- switch v := first.(type) {
- case *parse.IdentifierNode:
- id = v.Ident
- case *parse.ChainNode:
- id = v.String()
- }
-
- if partialRe.MatchString(id) {
- partialName := strings.Trim(x.Args[1].String(), "\"")
- if !strings.Contains(partialName, ".") {
- partialName += ".html"
- }
- partialName = "partials/" + partialName
- info := c.lookupFn(partialName)
-
- if info != nil {
- c.t.Add(info)
- } else {
- // Delay for later
- c.identityNotFound[partialName] = true
- }
- }
-}
-
func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
if c.t.typ != templatePartial || c.returnNode != nil {
return true
diff --git a/tpl/tplimpl/template_ast_transformers_test.go b/tpl/tplimpl/template_ast_transformers_test.go
index 90ca325ab..bd889b832 100644
--- a/tpl/tplimpl/template_ast_transformers_test.go
+++ b/tpl/tplimpl/template_ast_transformers_test.go
@@ -52,6 +52,7 @@ func newTestTemplate(templ tpl.Template) *templateState {
templateInfo{
name: templ.Name(),
},
+ nil,
)
}
diff --git a/tpl/tplimpl/template_errors.go b/tpl/tplimpl/template_errors.go
index ac8a72df5..34e73a07a 100644
--- a/tpl/tplimpl/template_errors.go
+++ b/tpl/tplimpl/template_errors.go
@@ -17,22 +17,22 @@ import (
"fmt"
"github.com/gohugoio/hugo/common/herrors"
- "github.com/spf13/afero"
+ "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.
- // Used to create some error context in error situations
- fs afero.Fs
-
- // The filename relative to the fs above.
- filename string
+ meta *hugofs.FileMeta
+}
- // The real filename (if possible). Used for logging.
- realFilename string
+func (t templateInfo) IdentifierBase() string {
+ return t.name
}
func (t templateInfo) Name() string {
@@ -40,7 +40,7 @@ func (t templateInfo) Name() string {
}
func (t templateInfo) Filename() string {
- return t.realFilename
+ return t.meta.Filename
}
func (t templateInfo) IsZero() bool {
@@ -53,12 +53,11 @@ func (t templateInfo) resolveType() templateType {
func (info templateInfo) errWithFileContext(what string, err error) error {
err = fmt.Errorf(what+": %w", err)
- fe := herrors.NewFileErrorFromName(err, info.realFilename)
- f, err := info.fs.Open(info.filename)
+ 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)
-
}
diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go
index 97d1b40dd..8997c83d6 100644
--- a/tpl/tplimpl/template_funcs.go
+++ b/tpl/tplimpl/template_funcs.go
@@ -22,6 +22,7 @@ import (
"github.com/gohugoio/hugo/common/hreflect"
"github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/tpl"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
@@ -65,9 +66,8 @@ import (
)
var (
- _ texttemplate.ExecHelper = (*templateExecHelper)(nil)
- zero reflect.Value
- contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
+ _ texttemplate.ExecHelper = (*templateExecHelper)(nil)
+ zero reflect.Value
)
type templateExecHelper struct {
@@ -81,7 +81,7 @@ func (t *templateExecHelper) GetFunc(ctx context.Context, tmpl texttemplate.Prep
if fn, found := t.funcs[name]; found {
if fn.Type().NumIn() > 0 {
first := fn.Type().In(0)
- if first.Implements(contextInterface) {
+ if hreflect.IsContextType(first) {
// TODO(bep) check if we can void this conversion every time -- and if that matters.
// The first argument may be context.Context. This is never provided by the end user, but it's used to pass down
// contextual information, e.g. the top level data context (e.g. Page).
@@ -95,6 +95,13 @@ func (t *templateExecHelper) GetFunc(ctx context.Context, tmpl texttemplate.Prep
}
func (t *templateExecHelper) Init(ctx context.Context, tmpl texttemplate.Preparer) {
+ if t.running {
+ _, ok := tmpl.(identity.IdentityProvider)
+ if ok {
+ t.trackDependencies(ctx, tmpl, "", reflect.Value{})
+ }
+
+ }
}
func (t *templateExecHelper) GetMapValue(ctx context.Context, tmpl texttemplate.Preparer, receiver, key reflect.Value) (reflect.Value, bool) {
@@ -116,22 +123,14 @@ func (t *templateExecHelper) GetMapValue(ctx context.Context, tmpl texttemplate.
var typeParams = reflect.TypeOf(maps.Params{})
func (t *templateExecHelper) GetMethod(ctx context.Context, tmpl texttemplate.Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value) {
- if t.running {
- switch name {
- case "GetPage", "Render":
- if info, ok := tmpl.(tpl.Info); ok {
- if m := receiver.MethodByName(name + "WithTemplateInfo"); m.IsValid() {
- return m, reflect.ValueOf(info)
- }
- }
- }
- }
-
if strings.EqualFold(name, "mainsections") && receiver.Type() == typeParams && receiver.Pointer() == t.siteParams.Pointer() {
- // MOved to site.MainSections in Hugo 0.112.0.
+ // Moved to site.MainSections in Hugo 0.112.0.
receiver = t.site
name = "MainSections"
+ }
+ if t.running {
+ ctx = t.trackDependencies(ctx, tmpl, name, receiver)
}
fn := hreflect.GetMethodByName(receiver, name)
@@ -141,7 +140,7 @@ func (t *templateExecHelper) GetMethod(ctx context.Context, tmpl texttemplate.Pr
if fn.Type().NumIn() > 0 {
first := fn.Type().In(0)
- if first.Implements(contextInterface) {
+ if hreflect.IsContextType(first) {
// The first argument may be context.Context. This is never provided by the end user, but it's used to pass down
// contextual information, e.g. the top level data context (e.g. Page).
return fn, reflect.ValueOf(ctx)
@@ -151,6 +150,43 @@ func (t *templateExecHelper) GetMethod(ctx context.Context, tmpl texttemplate.Pr
return fn, zero
}
+func (t *templateExecHelper) trackDependencies(ctx context.Context, tmpl texttemplate.Preparer, name string, receiver reflect.Value) context.Context {
+ if tmpl == nil {
+ panic("must provide a template")
+ }
+
+ idm := tpl.Context.GetDependencyManagerInCurrentScope(ctx)
+ if idm == nil {
+ return ctx
+ }
+
+ if info, ok := tmpl.(identity.IdentityProvider); ok {
+ idm.AddIdentity(info.GetIdentity())
+ }
+
+ // The receive is the "." in the method execution or map lookup, e.g. the Page in .Resources.
+ if hreflect.IsValid(receiver) {
+ in := receiver.Interface()
+
+ if idlp, ok := in.(identity.ForEeachIdentityByNameProvider); ok {
+ // This will skip repeated .RelPermalink usage on transformed resources
+ // which is not fingerprinted, e.g. to
+ // prevent all HTML pages to be re-rendered on a small CSS change.
+ idlp.ForEeachIdentityByName(name, func(id identity.Identity) bool {
+ idm.AddIdentity(id)
+ return false
+ })
+ } else {
+ identity.WalkIdentitiesShallow(in, func(level int, id identity.Identity) bool {
+ idm.AddIdentity(id)
+ return false
+ })
+ }
+ }
+
+ return ctx
+}
+
func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflect.Value) {
funcs := createFuncMap(d)
funcsv := make(map[string]reflect.Value)
diff --git a/tpl/transform/integration_test.go b/tpl/transform/integration_test.go
index f035ec719..351420a67 100644
--- a/tpl/transform/integration_test.go
+++ b/tpl/transform/integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Hugo Authors. All rights reserved.
+// 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.
@@ -77,7 +77,7 @@ disableKinds = ['section','sitemap','taxonomy','term']
---
title: p1
---
-a **b** c
+a **b** ` + "\v" + ` c
<!--more-->
`
b := hugolib.Test(t, files)
diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go
index 8078bc0ce..7054c6988 100644
--- a/tpl/transform/transform.go
+++ b/tpl/transform/transform.go
@@ -22,10 +22,11 @@ import (
"html/template"
"strings"
- "github.com/gohugoio/hugo/cache/namedmemcache"
+ "github.com/gohugoio/hugo/cache/dynacache"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/highlight"
"github.com/gohugoio/hugo/markup/highlight/chromalexers"
+ "github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/deps"
@@ -35,21 +36,23 @@ import (
// New returns a new instance of the transform-namespaced template functions.
func New(deps *deps.Deps) *Namespace {
- cache := namedmemcache.New()
- deps.BuildStartListeners.Add(
- func() {
- cache.Clear()
- })
+ if deps.MemCache == nil {
+ panic("must provide MemCache")
+ }
return &Namespace{
- cache: cache,
- deps: deps,
+ deps: deps,
+ cache: dynacache.GetOrCreatePartition[string, *resources.StaleValue[any]](
+ deps.MemCache,
+ "/tmpl/transform",
+ dynacache.OptionsPartition{Weight: 30, ClearWhen: dynacache.ClearOnChange},
+ ),
}
}
// Namespace provides template functions for the "transform" namespace.
type Namespace struct {
- cache *namedmemcache.Cache
+ cache *dynacache.Partition[string, *resources.StaleValue[any]]
deps *deps.Deps
}
@@ -154,7 +157,6 @@ func (ns *Namespace) XMLEscape(s any) (string, error) {
// Markdownify renders s from Markdown to HTML.
func (ns *Namespace) Markdownify(ctx context.Context, s any) (template.HTML, error) {
-
home := ns.deps.Site.Home()
if home == nil {
panic("home must not be nil")
diff --git a/tpl/transform/unmarshal.go b/tpl/transform/unmarshal.go
index 3936126ca..d876c88d7 100644
--- a/tpl/transform/unmarshal.go
+++ b/tpl/transform/unmarshal.go
@@ -14,18 +14,18 @@
package transform
import (
+ "errors"
"fmt"
"io"
"strings"
+ "github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/common/types"
"github.com/mitchellh/mapstructure"
- "errors"
-
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/parser/metadecoders"
@@ -71,7 +71,7 @@ func (ns *Namespace) Unmarshal(args ...any) (any, error) {
key += decoder.OptionsKey()
}
- return ns.cache.GetOrCreate(key, func() (any, error) {
+ v, err := ns.cache.GetOrCreate(key, func(string) (*resources.StaleValue[any], error) {
f := metadecoders.FormatFromStrings(r.MediaType().Suffixes()...)
if f == "" {
return nil, fmt.Errorf("MIME %q not supported", r.MediaType())
@@ -88,8 +88,24 @@ func (ns *Namespace) Unmarshal(args ...any) (any, error) {
return nil, err
}
- return decoder.Unmarshal(b, f)
+ v, err := decoder.Unmarshal(b, f)
+ if err != nil {
+ return nil, err
+ }
+
+ return &resources.StaleValue[any]{
+ Value: v,
+ IsStaleFunc: func() bool {
+ return resource.IsStaleAny(r)
+ },
+ }, nil
})
+ if err != nil {
+ return nil, err
+ }
+
+ return v.Value, nil
+
}
dataStr, err := types.ToStringE(data)
@@ -103,14 +119,29 @@ func (ns *Namespace) Unmarshal(args ...any) (any, error) {
key := helpers.MD5String(dataStr)
- return ns.cache.GetOrCreate(key, func() (any, error) {
+ v, err := ns.cache.GetOrCreate(key, func(string) (*resources.StaleValue[any], error) {
f := decoder.FormatFromContentString(dataStr)
if f == "" {
return nil, errors.New("unknown format")
}
- return decoder.Unmarshal([]byte(dataStr), f)
+ v, err := decoder.Unmarshal([]byte(dataStr), f)
+ if err != nil {
+ return nil, err
+ }
+
+ return &resources.StaleValue[any]{
+ Value: v,
+ IsStaleFunc: func() bool {
+ return false
+ },
+ }, nil
})
+ if err != nil {
+ return nil, err
+ }
+
+ return v.Value, nil
}
func decodeDecoder(m map[string]any) (metadecoders.Decoder, error) {
diff --git a/tpl/transform/unmarshal_test.go b/tpl/transform/unmarshal_test.go
index 12774298a..1b976c449 100644
--- a/tpl/transform/unmarshal_test.go
+++ b/tpl/transform/unmarshal_test.go
@@ -14,6 +14,7 @@
package transform_test
import (
+ "context"
"fmt"
"math/rand"
"strings"
@@ -193,9 +194,11 @@ func BenchmarkUnmarshalString(b *testing.B) {
jsons[i] = strings.Replace(testJSON, "ROOT_KEY", fmt.Sprintf("root%d", i), 1)
}
+ ctx := context.Background()
+
b.ResetTimer()
for i := 0; i < b.N; i++ {
- result, err := ns.Unmarshal(jsons[rand.Intn(numJsons)])
+ result, err := ns.Unmarshal(ctx, jsons[rand.Intn(numJsons)])
if err != nil {
b.Fatal(err)
}
@@ -220,9 +223,11 @@ func BenchmarkUnmarshalResource(b *testing.B) {
jsons[i] = testContentResource{key: key, content: strings.Replace(testJSON, "ROOT_KEY", key, 1), mime: media.Builtin.JSONType}
}
+ ctx := context.Background()
+
b.ResetTimer()
for i := 0; i < b.N; i++ {
- result, err := ns.Unmarshal(jsons[rand.Intn(numJsons)])
+ result, err := ns.Unmarshal(ctx, jsons[rand.Intn(numJsons)])
if err != nil {
b.Fatal(err)
}