all: Rework page store, add a dynacache, improve partial rebuilds, and some general spring cleaning

There are some breaking changes in this commit, see #11455.

Closes #11455
Closes #11549

This fixes a set of bugs (see issue list) and it is also paying some technical debt accumulated over the years. We now build with Staticcheck enabled in the CI build.

The performance should be about the same as before for regular sized Hugo sites, but it should perform and scale much better to larger data sets, as objects that uses lots of memory (e.g. rendered Markdown, big JSON files read into maps with transform.Unmarshal etc.) will now get automatically garbage collected if needed. Performance on partial rebuilds when running the server in fast render mode should be the same, but the change detection should be much more accurate.

A list of the notable new features:

* A new dependency tracker that covers (almost) all of Hugo's API and is used to do fine grained partial rebuilds when running the server.
* A new and simpler tree document store which allows fast lookups and prefix-walking in all dimensions (e.g. language) concurrently.
* You can now configure an upper memory limit allowing for much larger data sets and/or running on lower specced PCs.
We have lifted the "no resources in sub folders" restriction for branch bundles (e.g. sections).
Memory Limit
* Hugos will, by default, set aside a quarter of the total system memory, but you can set this via the OS environment variable HUGO_MEMORYLIMIT (in gigabytes). This is backed by a partitioned LRU cache used throughout Hugo. A cache that gets dynamically resized in low memory situations, allowing Go's Garbage Collector to free the memory.

New Dependency Tracker: Hugo has had a rule based coarse grained approach to server rebuilds that has worked mostly pretty well, but there have been some surprises (e.g. stale content). This is now revamped with a new dependency tracker that can quickly calculate the delta given a changed resource (e.g. a content file, template, JS file etc.). This handles transitive relations, e.g. $page -> js.Build -> JS import, or $page1.Content -> render hook -> site.GetPage -> $page2.Title, or $page1.Content -> shortcode -> partial -> site.RegularPages -> $page2.Content -> shortcode ..., and should also handle changes to aggregated values (e.g. site.Lastmod) effectively.

This covers all of Hugo's API with 2 known exceptions (a list that may not be fully exhaustive):

Changes to files loaded with template func os.ReadFile may not be handled correctly. We recommend loading resources with resources.Get
Changes to Hugo objects (e.g. Page) passed in the template context to lang.Translate may not be detected correctly. We recommend having simple i18n templates without too much data context passed in other than simple types such as strings and numbers.
Note that the cachebuster configuration (when A changes then rebuild B) works well with the above, but we recommend that you revise that configuration, as it in most situations should not be needed. One example where it is still needed is with TailwindCSS and using changes to hugo_stats.json to trigger new CSS rebuilds.

Document Store: Previously, a little simplified, we split the document store (where we store pages and resources) in a tree per language. This worked pretty well, but the structure made some operations harder than they needed to be. We have now restructured it into one Radix tree for all languages. Internally the language is considered to be a dimension of that tree, and the tree can be viewed in all dimensions concurrently. This makes some operations re. language simpler (e.g. finding translations is just a slice range), but the idea is that it should also be relatively inexpensive to add more dimensions if needed (e.g. role).

Fixes #10169
Fixes #10364
Fixes #10482
Fixes #10630
Fixes #10656
Fixes #10694
Fixes #10918
Fixes #11262
Fixes #11439
Fixes #11453
Fixes #11457
Fixes #11466
Fixes #11540
Fixes #11551
Fixes #11556
Fixes #11654
Fixes #11661
Fixes #11663
Fixes #11664
Fixes #11669
Fixes #11671
Fixes #11807
Fixes #11808
Fixes #11809
Fixes #11815
Fixes #11840
Fixes #11853
Fixes #11860
Fixes #11883
Fixes #11904
Fixes #7388
Fixes #7425
Fixes #7436
Fixes #7544
Fixes #7882
Fixes #7960
Fixes #8255
Fixes #8307
Fixes #8863
Fixes #8927
Fixes #9192
Fixes #9324
This commit is contained in:
Bjørn Erik Pedersen
2023-12-24 19:11:05 +01:00
parent 5fd1e74903
commit 7285e74090
437 changed files with 19304 additions and 18384 deletions

View File

@@ -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...)
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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"

View File

@@ -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")
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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$`)
}

View File

@@ -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.

View File

@@ -0,0 +1 @@
checks = ["none"]

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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")
})
}

View File

@@ -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))
}

View File

@@ -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`)

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -21,8 +21,6 @@ import (
var ns = New()
type tstNoStringer struct{}
func TestIsMap(t *testing.T) {
c := qt.New(t)
for _, test := range []struct {

View File

@@ -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
`)
}

View File

@@ -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")
}

View File

@@ -70,11 +70,6 @@ func init() {
},
)
ns.AddMethodMapping(ctx.SanitizeURL,
[]string{"sanitizeURL", "sanitizeurl"},
[][2]string{},
)
return ns
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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>"

View File

@@ -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.

View File

@@ -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`")
}
},
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -52,6 +52,7 @@ func newTestTemplate(templ tpl.Template) *templateState {
templateInfo{
name: templ.Name(),
},
nil,
)
}

View File

@@ -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
meta *hugofs.FileMeta
}
// The filename relative to the fs above.
filename string
// 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)
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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")

View File

@@ -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) {

View File

@@ -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)
}