mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-30 22:39:58 +02:00
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:
@@ -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...)
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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)
|
||||
|
@@ -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"
|
||||
|
@@ -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")
|
||||
}
|
||||
|
@@ -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.
|
||||
|
@@ -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.
|
||||
|
@@ -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.
|
||||
|
@@ -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$`)
|
||||
|
||||
}
|
||||
|
@@ -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.
|
||||
|
1
tpl/internal/go_templates/staticcheck.conf
Normal file
1
tpl/internal/go_templates/staticcheck.conf
Normal file
@@ -0,0 +1 @@
|
||||
checks = ["none"]
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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")
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
@@ -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))
|
||||
|
||||
}
|
||||
|
@@ -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`)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
|
@@ -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")
|
||||
}
|
||||
|
@@ -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")
|
||||
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -21,8 +21,6 @@ import (
|
||||
|
||||
var ns = New()
|
||||
|
||||
type tstNoStringer struct{}
|
||||
|
||||
func TestIsMap(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
for _, test := range []struct {
|
||||
|
@@ -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
|
||||
|
||||
`)
|
||||
|
||||
}
|
||||
|
@@ -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")
|
||||
}
|
||||
|
@@ -70,11 +70,6 @@ func init() {
|
||||
},
|
||||
)
|
||||
|
||||
ns.AddMethodMapping(ctx.SanitizeURL,
|
||||
[]string{"sanitizeURL", "sanitizeurl"},
|
||||
[][2]string{},
|
||||
)
|
||||
|
||||
return ns
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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>"
|
||||
|
@@ -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.
|
||||
|
@@ -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`")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -52,6 +52,7 @@ func newTestTemplate(templ tpl.Template) *templateState {
|
||||
templateInfo{
|
||||
name: templ.Name(),
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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")
|
||||
|
@@ -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) {
|
||||
|
@@ -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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user