Reimplement and simplify Hugo's template system

See #13541 for details.

Fixes #13545
Fixes #13515
Closes #7964
Closes #13365
Closes #12988
Closes #4891
This commit is contained in:
Bjørn Erik Pedersen
2025-04-06 19:55:35 +02:00
parent 812ea0b325
commit 83cfdd78ca
138 changed files with 5342 additions and 4396 deletions

View File

@@ -21,7 +21,6 @@ import (
"strings"
"github.com/gohugoio/hugo/common/hreflect"
"github.com/gohugoio/hugo/tpl"
)
// Apply takes an array or slice c and returns a new slice with the function fname applied over it.
@@ -109,8 +108,7 @@ func applyFnToThis(ctx context.Context, fn, this reflect.Value, args ...any) (re
func (ns *Namespace) lookupFunc(ctx context.Context, fname string) (reflect.Value, bool) {
namespace, methodName, ok := strings.Cut(fname, ".")
if !ok {
templ := ns.deps.Tmpl().(tpl.TemplateFuncGetter)
return templ.GetFunc(fname)
return ns.deps.GetTemplateStore().GetFunc(fname)
}
// Namespace

View File

@@ -1,104 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collections
import (
"context"
"fmt"
"io"
"reflect"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config/testconfig"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/output/layouts"
"github.com/gohugoio/hugo/tpl"
)
type templateFinder int
func (templateFinder) GetIdentity(string) (identity.Identity, bool) {
return identity.StringIdentity("test"), true
}
func (templateFinder) Lookup(name string) (tpl.Template, bool) {
return nil, false
}
func (templateFinder) HasTemplate(name string) bool {
return false
}
func (templateFinder) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
return nil, false, false
}
func (templateFinder) LookupVariants(name string) []tpl.Template {
return nil
}
func (templateFinder) LookupLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
return nil, false, nil
}
func (templateFinder) Execute(t tpl.Template, wr io.Writer, data any) error {
return nil
}
func (templateFinder) ExecuteWithContext(ctx context.Context, t tpl.Template, wr io.Writer, data any) error {
return nil
}
func (templateFinder) GetFunc(name string) (reflect.Value, bool) {
if name == "dobedobedo" {
return reflect.Value{}, false
}
return reflect.ValueOf(fmt.Sprint), true
}
func TestApply(t *testing.T) {
t.Parallel()
c := qt.New(t)
d := testconfig.GetTestDeps(nil, nil)
d.SetTempl(&tpl.TemplateHandlers{
Tmpl: new(templateFinder),
})
ns := New(d)
strings := []any{"a\n", "b\n"}
ctx := context.Background()
result, err := ns.Apply(ctx, strings, "print", "a", "b", "c")
c.Assert(err, qt.IsNil)
c.Assert(result, qt.DeepEquals, []any{"abc", "abc"})
_, err = ns.Apply(ctx, strings, "apply", ".")
c.Assert(err, qt.Not(qt.IsNil))
var nilErr *error
_, err = ns.Apply(ctx, nilErr, "chomp", ".")
c.Assert(err, qt.Not(qt.IsNil))
_, err = ns.Apply(ctx, strings, "dobedobedo", ".")
c.Assert(err, qt.Not(qt.IsNil))
_, err = ns.Apply(ctx, strings, "foo.Chomp", "c\n")
if err == nil {
t.Errorf("apply with unknown func should fail")
}
}

View File

@@ -14,6 +14,8 @@
package template
import (
"fmt"
"github.com/gohugoio/hugo/common/types"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
)
@@ -51,3 +53,28 @@ func indirect(a any) any {
return in
}
// CloneShallow creates a shallow copy of the template. It does not clone or copy the nested templates.
func (t *Template) CloneShallow() (*Template, error) {
t.nameSpace.mu.Lock()
defer t.nameSpace.mu.Unlock()
if t.escapeErr != nil {
return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
}
textClone, err := t.text.Clone()
if err != nil {
return nil, err
}
ns := &nameSpace{set: make(map[string]*Template)}
ns.esc = makeEscaper(ns)
ret := &Template{
nil,
textClone,
textClone.Tree,
ns,
}
ret.set[ret.Name()] = ret
// Return the template associated with the name of this template.
return ret.set[ret.Name()], nil
}

View File

@@ -267,7 +267,7 @@ func (t *Template) Clone() (*Template, error) {
name := x.Name()
src := t.set[name]
if src == nil || src.escapeErr != nil {
return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed, %q not found", t.Name(), name)
}
x.Tree = x.Tree.Copy()
ret.set[name] = &Template{

View File

@@ -35,7 +35,7 @@ Josie
Name, Gift string
Attended bool
}
var recipients = []Recipient{
recipients := []Recipient{
{"Aunt Mildred", "bone china tea set", true},
{"Uncle John", "moleskin pants", false},
{"Cousin Rodney", "", false},

View File

@@ -24,7 +24,7 @@ const name = "math"
func init() {
f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
ctx := New()
ctx := New(d)
ns := &internal.TemplateFuncsNamespace{
Name: name,

View File

@@ -20,9 +20,9 @@ import (
"math"
"math/rand"
"reflect"
"sync/atomic"
_math "github.com/gohugoio/hugo/common/math"
"github.com/gohugoio/hugo/deps"
"github.com/spf13/cast"
)
@@ -32,12 +32,16 @@ var (
)
// New returns a new instance of the math-namespaced template functions.
func New() *Namespace {
return &Namespace{}
func New(d *deps.Deps) *Namespace {
return &Namespace{
d: d,
}
}
// Namespace provides template functions for the "math" namespace.
type Namespace struct{}
type Namespace struct {
d *deps.Deps
}
// Abs returns the absolute value of n.
func (ns *Namespace) Abs(n any) (float64, error) {
@@ -345,8 +349,6 @@ func (ns *Namespace) doArithmetic(inputs []any, operation rune) (value any, err
return
}
var counter uint64
// Counter increments and returns a global counter.
// This was originally added to be used in tests where now.UnixNano did not
// have the needed precision (especially on Windows).
@@ -354,5 +356,5 @@ var counter uint64
// and the counter will reset on new builds.
// <docsmeta>{"identifiers": ["now.UnixNano"] }</docsmeta>
func (ns *Namespace) Counter() uint64 {
return atomic.AddUint64(&counter, uint64(1))
return ns.d.Counters.MathCounter.Add(1)
}

View File

@@ -24,7 +24,7 @@ func TestBasicNSArithmetic(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
type TestCase struct {
fn func(inputs ...any) (any, error)
@@ -66,7 +66,7 @@ func TestBasicNSArithmetic(t *testing.T) {
func TestAbs(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
x any
@@ -93,7 +93,7 @@ func TestAbs(t *testing.T) {
func TestCeil(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
x any
@@ -126,7 +126,7 @@ func TestFloor(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
x any
@@ -159,7 +159,7 @@ func TestLog(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
a any
@@ -200,7 +200,7 @@ func TestSqrt(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
a any
@@ -239,7 +239,7 @@ func TestMod(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
a any
@@ -279,7 +279,7 @@ func TestModBool(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
a any
@@ -325,7 +325,7 @@ func TestRound(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
x any
@@ -358,7 +358,7 @@ func TestPow(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
a any
@@ -398,7 +398,7 @@ func TestMax(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
type TestCase struct {
values []any
@@ -452,7 +452,7 @@ func TestMin(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
type TestCase struct {
values []any
@@ -507,7 +507,7 @@ func TestSum(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
mustSum := func(values ...any) any {
result, err := ns.Sum(values...)
@@ -530,7 +530,7 @@ func TestProduct(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
mustProduct := func(values ...any) any {
result, err := ns.Product(values...)
@@ -554,7 +554,7 @@ func TestPi(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
expect := 3.1415
result := ns.Pi()
@@ -570,7 +570,7 @@ func TestSin(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
a any
@@ -604,7 +604,7 @@ func TestCos(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
a any
@@ -638,7 +638,7 @@ func TestTan(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
a any
@@ -680,7 +680,7 @@ func TestTan(t *testing.T) {
func TestAsin(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
x any
@@ -715,7 +715,7 @@ func TestAsin(t *testing.T) {
func TestAcos(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
x any
@@ -751,7 +751,7 @@ func TestAcos(t *testing.T) {
func TestAtan(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
x any
@@ -782,7 +782,7 @@ func TestAtan(t *testing.T) {
func TestAtan2(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
x any
@@ -821,7 +821,7 @@ func TestAtan2(t *testing.T) {
func TestToDegrees(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
x any
@@ -852,7 +852,7 @@ func TestToDegrees(t *testing.T) {
func TestToRadians(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
ns := New(nil)
for _, test := range []struct {
x any

View File

@@ -25,12 +25,12 @@ import (
"github.com/bep/lazycache"
"github.com/gohugoio/hugo/common/constants"
"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/identity"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
"github.com/gohugoio/hugo/tpl"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/deps"
@@ -54,13 +54,6 @@ func (k partialCacheKey) Key() string {
return hashing.HashString(append([]any{k.Name}, k.Variants...)...)
}
func (k partialCacheKey) templateName() string {
if !strings.HasPrefix(k.Name, "partials/") {
return "partials/" + k.Name
}
return k.Name
}
// partialCache represents a LRU cache of partials.
type partialCache struct {
cache *lazycache.Cache[string, includeResult]
@@ -129,6 +122,11 @@ func (ns *Namespace) Include(ctx context.Context, name string, contextList ...an
}
func (ns *Namespace) includWithTimeout(ctx context.Context, name string, dataList ...any) includeResult {
if strings.HasPrefix(name, "partials/") {
// This is most likely not what the user intended.
// This worked before Hugo 0.146.0.
ns.deps.Log.Warnidf(constants.WarnPartialSuperfluousPrefix, "Partial name %q starting with 'partials/' (as in {{ partial \"%s\"}}) is most likely not what you want. Before 0.146.0 we did a double lookup in this situation.", name, name)
}
// Create a new context with a timeout not connected to the incoming context.
timeoutCtx, cancel := context.WithTimeout(context.Background(), ns.deps.Conf.Timeout())
defer cancel()
@@ -159,28 +157,14 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
if len(dataList) > 0 {
data = dataList[0]
}
var n string
if strings.HasPrefix(name, "partials/") {
n = name
} else {
n = "partials/" + name
}
templ, found := ns.deps.Tmpl().Lookup(n)
if !found {
// For legacy reasons.
templ, found = ns.deps.Tmpl().Lookup(n + ".html")
}
if !found {
name, desc := ns.deps.TemplateStore.TemplateDescriptorFromPath(name)
v := ns.deps.TemplateStore.LookupPartial(name, desc)
if v == nil {
return includeResult{err: fmt.Errorf("partial %q not found", name)}
}
var info tpl.ParseInfo
if ip, ok := templ.(tpl.Info); ok {
info = ip.ParseInfo()
}
templ := v
info := v.ParseInfo
var w io.Writer
@@ -200,7 +184,7 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
w = b
}
if err := ns.deps.Tmpl().ExecuteWithContext(ctx, templ, w, data); err != nil {
if err := ns.deps.GetTemplateStore().ExecuteWithContext(ctx, templ, w, data); err != nil {
return includeResult{err: err}
}
@@ -208,14 +192,14 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
if ctx, ok := data.(*contextWrapper); ok {
result = ctx.Result
} else if _, ok := templ.(*texttemplate.Template); ok {
} else if _, ok := templ.Template.(*texttemplate.Template); ok {
result = w.(fmt.Stringer).String()
} else {
result = template.HTML(w.(fmt.Stringer).String())
}
return includeResult{
name: templ.Name(),
name: templ.Template.Name(),
result: result,
}
}
@@ -253,9 +237,9 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
// The templates that gets executed is measured in Execute.
// We need to track the time spent in the cache to
// get the totals correct.
ns.deps.Metrics.MeasureSince(key.templateName(), start)
ns.deps.Metrics.MeasureSince(r.name, start)
}
ns.deps.Metrics.TrackValue(key.templateName(), r.result, found)
ns.deps.Metrics.TrackValue(r.name, r.result, found)
}
if r.mangager != nil && depsManagerIn != nil {

View File

@@ -170,7 +170,7 @@ D1
got := buf.String()
// Get rid of all the durations, they are never the same.
durationRe := regexp.MustCompile(`\b[\.\d]*(ms|µs|s)\b`)
durationRe := regexp.MustCompile(`\b[\.\d]*(ms|ns|µs|s)\b`)
normalize := func(s string) string {
s = durationRe.ReplaceAllString(s, "")
@@ -193,10 +193,10 @@ D1
expect := `
0 0 0 1 index.html
100 0 0 1 partials/static2.html
100 50 1 2 partials/static1.html
25 50 2 4 partials/dynamic1.html
66 33 1 3 partials/halfdynamic1.html
100 0 0 1 _partials/static2.html
100 50 1 2 _partials/static1.html
25 50 2 4 _partials/dynamic1.html
66 33 1 3 _partials/halfdynamic1.html
`
b.Assert(got, hqt.IsSameString, expect)

View File

@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -16,9 +16,6 @@ package tpl
import (
"context"
"io"
"reflect"
"regexp"
"strings"
"sync"
"unicode"
@@ -27,140 +24,18 @@ import (
"github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/output/layouts"
"github.com/gohugoio/hugo/output"
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
)
// TemplateManager manages the collection of templates.
type TemplateManager interface {
TemplateHandler
TemplateFuncGetter
AddTemplate(name, tpl string) error
}
// TemplateVariants describes the possible variants of a template.
// All of these may be empty.
type TemplateVariants struct {
Language string
OutputFormat output.Format
}
// TemplateFinder finds templates.
type TemplateFinder interface {
TemplateLookup
TemplateLookupVariant
}
// UnusedTemplatesProvider lists unused templates if the build is configured to track those.
type UnusedTemplatesProvider interface {
UnusedTemplates() []FileInfo
}
// TemplateHandlers holds the templates needed by Hugo.
type TemplateHandlers struct {
Tmpl TemplateHandler
TxtTmpl TemplateParseFinder
}
type TemplateExecutor interface {
ExecuteWithContext(ctx context.Context, t Template, wr io.Writer, data any) error
}
// TemplateHandler finds and executes templates.
type TemplateHandler interface {
TemplateFinder
TemplateExecutor
LookupLayout(d layouts.LayoutDescriptor, f output.Format) (Template, bool, error)
HasTemplate(name string) bool
GetIdentity(name string) (identity.Identity, bool)
}
type TemplateLookup interface {
Lookup(name string) (Template, bool)
}
type TemplateLookupVariant interface {
// TODO(bep) this currently only works for shortcodes.
// We may unify and expand this variant pattern to the
// other templates, but we need this now for the shortcodes to
// quickly determine if a shortcode has a template for a given
// output format.
// It returns the template, if it was found or not and if there are
// alternative representations (output format, language).
// We are currently only interested in output formats, so we should improve
// this for speed.
LookupVariant(name string, variants TemplateVariants) (Template, bool, bool)
LookupVariants(name string) []Template
}
// Template is the common interface between text/template and html/template.
type Template interface {
Name() string
Prepare() (*texttemplate.Template, error)
}
// AddIdentity checks if t is an identity.Identity and returns it if so.
// Else it wraps it in a templateIdentity using its name as the base.
func AddIdentity(t Template) Template {
if _, ok := t.(identity.IdentityProvider); ok {
return t
}
return templateIdentityProvider{
Template: t,
id: identity.StringIdentity(t.Name()),
}
}
type templateIdentityProvider struct {
Template
id identity.Identity
}
func (t templateIdentityProvider) GetIdentity() identity.Identity {
return t.id
}
// TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain.
type TemplateParser interface {
Parse(name, tpl string) (Template, error)
}
// TemplateParseFinder provides both parsing and finding.
type TemplateParseFinder interface {
TemplateParser
TemplateFinder
}
// TemplateDebugger prints some debug info to stdout.
type TemplateDebugger interface {
Debug()
}
// TemplatesProvider as implemented by deps.Deps.
type TemplatesProvider interface {
Tmpl() TemplateHandler
TextTmpl() TemplateParseFinder
}
var baseOfRe = regexp.MustCompile("template: (.*?):")
func extractBaseOf(err string) string {
m := baseOfRe.FindStringSubmatch(err)
if len(m) == 2 {
return m[1]
}
return ""
}
// TemplateFuncGetter allows to find a template func by name.
type TemplateFuncGetter interface {
GetFunc(name string) (reflect.Value, bool)
}
// RenderingContext represents the currently rendered site/language.
type RenderingContext struct {
Site site
SiteOutIdx int
@@ -201,7 +76,9 @@ type site interface {
}
const (
// HugoDeferredTemplatePrefix is the prefix for placeholders for deferred templates.
HugoDeferredTemplatePrefix = "__hdeferred/"
// HugoDeferredTemplateSuffix is the suffix for placeholders for deferred templates.
HugoDeferredTemplateSuffix = "__d="
)
@@ -243,10 +120,11 @@ func StripHTML(s string) string {
return s
}
// DeferredExecution holds the template and data for a deferred execution.
type DeferredExecution struct {
Mu sync.Mutex
Ctx context.Context
TemplateName string
TemplatePath string
Data any
Executed bool

View File

@@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -15,20 +15,8 @@ package tpl
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestExtractBaseof(t *testing.T) {
c := qt.New(t)
replaced := extractBaseOf(`failed: template: _default/baseof.html:37:11: executing "_default/baseof.html" at <.Parents>: can't evaluate field Parents in type *hugolib.PageOutput`)
c.Assert(replaced, qt.Equals, "_default/baseof.html")
c.Assert(extractBaseOf("not baseof for you"), qt.Equals, "")
c.Assert(extractBaseOf("template: blog/baseof.html:23:11:"), qt.Equals, "blog/baseof.html")
}
func TestStripHTML(t *testing.T) {
type test struct {
input, expected string

View File

@@ -71,6 +71,81 @@ AMP.
`
func TestDeferNoBaseof(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- layouts/index.html --
Home.
{{ with (templates.Defer (dict "key" "foo")) }}
Defer
{{ end }}
-- content/_index.md --
---
title: "Home"
---
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html", "Home.\n\n Defer")
}
func TestDeferBaseof(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- layouts/baseof.html --
{{ with (templates.Defer (dict "key" "foo")) }}
Defer
{{ end }}
Block:{{ block "main" . }}{{ end }}$
-- layouts/index.html --
{{ define "main" }}
Home.
{{ end }}
-- content/_index.md --
---
title: "Home"
---
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html", "Home.\n\n Defer")
}
func TestDeferMain(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- layouts/baseof.html --
Block:{{ block "main" . }}{{ end }}$
-- layouts/index.html --
{{ define "main" }}
Home.
{{ with (templates.Defer (dict "key" "foo")) }}
Defer
{{ end }}
{{ end }}
-- content/_index.md --
---
title: "Home"
---
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html", "Home.\n\n Defer")
}
func TestDeferBasic(t *testing.T) {
t.Parallel()

View File

@@ -44,7 +44,7 @@ type Namespace struct {
// Note that this is the Unix-styled relative path including filename suffix,
// e.g. partials/header.html
func (ns *Namespace) Exists(name string) bool {
return ns.deps.Tmpl().HasTemplate(name)
return ns.deps.GetTemplateStore().HasTemplate(name)
}
// Defer defers the execution of a template block.
@@ -93,7 +93,7 @@ func (ns *Namespace) DoDefer(ctx context.Context, id string, optsv any) string {
_, _ = ns.deps.BuildState.DeferredExecutions.Executions.GetOrCreate(id,
func() (*tpl.DeferredExecution, error) {
return &tpl.DeferredExecution{
TemplateName: templateName,
TemplatePath: templateName,
Ctx: ctx,
Data: opts.Data,
Executed: false,

View File

@@ -0,0 +1,30 @@
// Code generated by "stringer -type Category"; DO NOT EDIT.
package tplimpl
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[CategoryLayout-1]
_ = x[CategoryBaseof-2]
_ = x[CategoryMarkup-3]
_ = x[CategoryShortcode-4]
_ = x[CategoryPartial-5]
_ = x[CategoryServer-6]
_ = x[CategoryHugo-7]
}
const _Category_name = "CategoryLayoutCategoryBaseofCategoryMarkupCategoryShortcodeCategoryPartialCategoryServerCategoryHugo"
var _Category_index = [...]uint8{0, 14, 28, 42, 59, 74, 88, 100}
func (i Category) String() string {
i -= 1
if i < 0 || i >= Category(len(_Category_index)-1) {
return "Category(" + strconv.FormatInt(int64(i+1), 10) + ")"
}
return _Category_name[_Category_index[i]:_Category_index[i+1]]
}

View File

@@ -5,7 +5,7 @@
window.disqus_config = function () {
{{with .Params.disqus_identifier }}this.page.identifier = '{{ . }}';{{end}}
{{with .Params.disqus_title }}this.page.title = '{{ . }}';{{end}}
{{with .Params.disqus_url }}this.page.url = '{{ . | html }}';{{end}}
{{with .Params.disqus_url }}this.page.url = '{{ . | transform.HTMLEscape | safeURL }}';{{end}}
};
(function() {
if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) {

View File

@@ -20,7 +20,7 @@
{{- if in $validFormats $format }}
{{- if gt $page.Paginator.TotalPages 1 }}
<ul class="pagination pagination-{{ $format }}">
{{- partial (printf "partials/inline/pagination/%s" $format) $page }}
{{- partial (printf "inline/pagination/%s" $format) $page }}
</ul>
{{- end }}
{{- else }}

130
tpl/tplimpl/legacy.go Normal file
View File

@@ -0,0 +1,130 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tplimpl
import (
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/resources/kinds"
)
type layoutLegacyMapping struct {
sourcePath string
target layoutLegacyMappingTarget
}
type layoutLegacyMappingTarget struct {
targetPath string
targetDesc TemplateDescriptor
targetCategory Category
}
var (
ltermPlural = layoutLegacyMappingTarget{
targetPath: "/PLURAL",
targetDesc: TemplateDescriptor{Kind: kinds.KindTerm},
targetCategory: CategoryLayout,
}
ltermBase = layoutLegacyMappingTarget{
targetPath: "",
targetDesc: TemplateDescriptor{Kind: kinds.KindTerm},
targetCategory: CategoryLayout,
}
ltaxPlural = layoutLegacyMappingTarget{
targetPath: "/PLURAL",
targetDesc: TemplateDescriptor{Kind: kinds.KindTaxonomy},
targetCategory: CategoryLayout,
}
ltaxBase = layoutLegacyMappingTarget{
targetPath: "",
targetDesc: TemplateDescriptor{Kind: kinds.KindTaxonomy},
targetCategory: CategoryLayout,
}
lsectBase = layoutLegacyMappingTarget{
targetPath: "",
targetDesc: TemplateDescriptor{Kind: kinds.KindSection},
targetCategory: CategoryLayout,
}
lsectTheSection = layoutLegacyMappingTarget{
targetPath: "/THESECTION",
targetDesc: TemplateDescriptor{Kind: kinds.KindSection},
targetCategory: CategoryLayout,
}
)
type legacyTargetPathIdentifiers struct {
targetPath string
targetCategory Category
kind string
lang string
outputFormat string
ext string
}
type legacyOrdinalMapping struct {
ordinal int
mapping layoutLegacyMappingTarget
}
type legacyOrdinalMappingFi struct {
m legacyOrdinalMapping
fi hugofs.FileMetaInfo
}
var legacyTermMappings = []layoutLegacyMapping{
{sourcePath: "/PLURAL/term", target: ltermPlural},
{sourcePath: "/PLURAL/SINGULAR", target: ltermPlural},
{sourcePath: "/term/term", target: ltermBase},
{sourcePath: "/term/SINGULAR", target: ltermPlural},
{sourcePath: "/term/taxonomy", target: ltermPlural},
{sourcePath: "/term/list", target: ltermBase},
{sourcePath: "/taxonomy/term", target: ltermBase},
{sourcePath: "/taxonomy/SINGULAR", target: ltermPlural},
{sourcePath: "/SINGULAR/term", target: ltermPlural},
{sourcePath: "/SINGULAR/SINGULAR", target: ltermPlural},
{sourcePath: "/_default/SINGULAR", target: ltermPlural},
{sourcePath: "/_default/taxonomy", target: ltermBase},
}
var legacyTaxonomyMappings = []layoutLegacyMapping{
{sourcePath: "/PLURAL/SINGULAR.terms", target: ltaxPlural},
{sourcePath: "/PLURAL/terms", target: ltaxPlural},
{sourcePath: "/PLURAL/taxonomy", target: ltaxPlural},
{sourcePath: "/PLURAL/list", target: ltaxPlural},
{sourcePath: "/SINGULAR/SINGULAR.terms", target: ltaxPlural},
{sourcePath: "/SINGULAR/terms", target: ltaxPlural},
{sourcePath: "/SINGULAR/taxonomy", target: ltaxPlural},
{sourcePath: "/SINGULAR/list", target: ltaxPlural},
{sourcePath: "/taxonomy/SINGULAR.terms", target: ltaxPlural},
{sourcePath: "/taxonomy/terms", target: ltaxBase},
{sourcePath: "/taxonomy/taxonomy", target: ltaxBase},
{sourcePath: "/taxonomy/list", target: ltaxBase},
{sourcePath: "/_default/SINGULAR.terms", target: ltaxBase},
{sourcePath: "/_default/terms", target: ltaxBase},
{sourcePath: "/_default/taxonomy", target: ltaxBase},
}
var legacySectionMappings = []layoutLegacyMapping{
// E.g. /mysection/mysection.html
{sourcePath: "/THESECTION/THESECTION", target: lsectTheSection},
// E.g. /section/mysection.html
{sourcePath: "/SECTIONKIND/THESECTION", target: lsectTheSection},
// E.g. /section/section.html
{sourcePath: "/SECTIONKIND/SECTIONKIND", target: lsectBase},
// E.g. /section/list.html
{sourcePath: "/SECTIONKIND/list", target: lsectBase},
// E.g. /_default/mysection.html
{sourcePath: "/_default/THESECTION", target: lsectTheSection},
}

View File

@@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.

View File

@@ -1,153 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tplimpl
import (
"strings"
"github.com/gohugoio/hugo/tpl"
)
// Currently lang, outFormat, suffix
const numTemplateVariants = 3
type shortcodeVariant struct {
// The possible variants: lang, outFormat, suffix
// gtag
// gtag.html
// gtag.no.html
// gtag.no.amp.html
// A slice of length numTemplateVariants.
variants []string
ts *templateState
}
type shortcodeTemplates struct {
variants []shortcodeVariant
}
func (s *shortcodeTemplates) indexOf(variants []string) int {
L:
for i, v1 := range s.variants {
for i, v2 := range v1.variants {
if v2 != variants[i] {
continue L
}
}
return i
}
return -1
}
func (s *shortcodeTemplates) fromVariants(variants tpl.TemplateVariants) (shortcodeVariant, bool) {
return s.fromVariantsSlice([]string{
variants.Language,
strings.ToLower(variants.OutputFormat.Name),
variants.OutputFormat.MediaType.FirstSuffix.Suffix,
})
}
func (s *shortcodeTemplates) fromVariantsSlice(variants []string) (shortcodeVariant, bool) {
var (
bestMatch shortcodeVariant
bestMatchWeight int
)
for _, variant := range s.variants {
w := s.compareVariants(variants, variant.variants)
if bestMatchWeight == 0 || w > bestMatchWeight {
bestMatch = variant
bestMatchWeight = w
}
}
return bestMatch, true
}
// calculate a weight for two string slices of same length.
// higher value means "better match".
func (s *shortcodeTemplates) compareVariants(a, b []string) int {
weight := 0
k := len(a)
for i, av := range a {
bv := b[i]
if av == bv {
// Add more weight to the left side (language...).
weight = weight + k - i
} else {
weight--
}
}
return weight
}
func templateVariants(name string) []string {
_, variants := templateNameAndVariants(name)
return variants
}
func templateNameAndVariants(name string) (string, []string) {
variants := make([]string, numTemplateVariants)
parts := strings.Split(name, ".")
if len(parts) <= 1 {
// No variants.
return name, variants
}
name = parts[0]
parts = parts[1:]
lp := len(parts)
start := len(variants) - lp
for i, j := start, 0; i < len(variants); i, j = i+1, j+1 {
variants[i] = parts[j]
}
if lp > 1 && lp < len(variants) {
for i := lp - 1; i > 0; i-- {
variants[i-1] = variants[i]
}
}
if lp == 1 {
// Suffix only. Duplicate it into the output format field to
// make HTML win over AMP.
variants[len(variants)-2] = variants[len(variants)-1]
}
return name, variants
}
func resolveTemplateType(name string) templateType {
if isShortcode(name) {
return templateShortcode
}
if strings.Contains(name, "partials/") {
return templatePartial
}
return templateUndefined
}
func isShortcode(name string) bool {
return strings.Contains(name, shortcodesPathPrefix)
}
func isInternal(name string) bool {
return strings.HasPrefix(name, internalPathPrefix)
}

View File

@@ -1,91 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tplimpl
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestShortcodesTemplate(t *testing.T) {
t.Run("isShortcode", func(t *testing.T) {
c := qt.New(t)
c.Assert(isShortcode("shortcodes/figures.html"), qt.Equals, true)
c.Assert(isShortcode("_internal/shortcodes/figures.html"), qt.Equals, true)
c.Assert(isShortcode("shortcodes\\figures.html"), qt.Equals, false)
c.Assert(isShortcode("myshortcodes"), qt.Equals, false)
})
t.Run("variantsFromName", func(t *testing.T) {
c := qt.New(t)
c.Assert(templateVariants("figure.html"), qt.DeepEquals, []string{"", "html", "html"})
c.Assert(templateVariants("figure.no.html"), qt.DeepEquals, []string{"no", "no", "html"})
c.Assert(templateVariants("figure.no.amp.html"), qt.DeepEquals, []string{"no", "amp", "html"})
c.Assert(templateVariants("figure.amp.html"), qt.DeepEquals, []string{"amp", "amp", "html"})
name, variants := templateNameAndVariants("figure.html")
c.Assert(name, qt.Equals, "figure")
c.Assert(variants, qt.DeepEquals, []string{"", "html", "html"})
})
t.Run("compareVariants", func(t *testing.T) {
c := qt.New(t)
var s *shortcodeTemplates
tests := []struct {
name string
name1 string
name2 string
expected int
}{
{"Same suffix", "figure.html", "figure.html", 6},
{"Same suffix and output format", "figure.html.html", "figure.html.html", 6},
{"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 6},
{"No suffix", "figure", "figure", 6},
{"Different output format", "figure.amp.html", "figure.html.html", -1},
{"One with output format, one without", "figure.amp.html", "figure.html", -1},
}
for _, test := range tests {
w := s.compareVariants(templateVariants(test.name1), templateVariants(test.name2))
c.Assert(w, qt.Equals, test.expected)
}
})
t.Run("indexOf", func(t *testing.T) {
c := qt.New(t)
s := &shortcodeTemplates{
variants: []shortcodeVariant{
{variants: []string{"a", "b", "c"}},
{variants: []string{"a", "b", "d"}},
},
}
c.Assert(s.indexOf([]string{"a", "b", "c"}), qt.Equals, 0)
c.Assert(s.indexOf([]string{"a", "b", "d"}), qt.Equals, 1)
c.Assert(s.indexOf([]string{"a", "b", "x"}), qt.Equals, -1)
})
t.Run("Name", func(t *testing.T) {
c := qt.New(t)
c.Assert(templateBaseName(templateShortcode, "shortcodes/foo.html"), qt.Equals, "foo.html")
c.Assert(templateBaseName(templateShortcode, "_internal/shortcodes/foo.html"), qt.Equals, "foo.html")
c.Assert(templateBaseName(templateShortcode, "shortcodes/test/foo.html"), qt.Equals, "test/foo.html")
c.Assert(true, qt.Equals, true)
})
}

View File

@@ -0,0 +1,25 @@
// Code generated by "stringer -type SubCategory"; DO NOT EDIT.
package tplimpl
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[SubCategoryMain-0]
_ = x[SubCategoryEmbedded-1]
_ = x[SubCategoryInline-2]
}
const _SubCategory_name = "SubCategoryMainSubCategoryEmbeddedSubCategoryInline"
var _SubCategory_index = [...]uint8{0, 15, 34, 51}
func (i SubCategory) String() string {
if i < 0 || i >= SubCategory(len(_SubCategory_index)-1) {
return "SubCategory(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _SubCategory_name[_SubCategory_index[i]:_SubCategory_index[i+1]]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tplimpl

View File

@@ -1,51 +0,0 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tplimpl
import (
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/tpl"
)
// TemplateProvider manages templates.
type TemplateProvider struct{}
// DefaultTemplateProvider is a globally available TemplateProvider.
var DefaultTemplateProvider *TemplateProvider
// Update updates the Hugo Template System in the provided Deps
// with all the additional features, templates & functions.
func (*TemplateProvider) NewResource(dst *deps.Deps) error {
handlers, err := newTemplateHandlers(dst)
if err != nil {
return err
}
dst.SetTempl(handlers)
return nil
}
// Clone clones.
func (*TemplateProvider) CloneResource(dst, src *deps.Deps) error {
t := src.Tmpl().(*templateExec)
c := t.Clone(dst)
funcMap := make(map[string]any)
for k, v := range c.funcs {
funcMap[k] = v.Interface()
}
dst.SetTempl(&tpl.TemplateHandlers{
Tmpl: c,
TxtTmpl: newStandaloneTextTemplate(funcMap),
})
return nil
}

View File

@@ -1,161 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tplimpl
import (
"testing"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/tpl"
)
// Issue #2927
func TestTransformRecursiveTemplate(t *testing.T) {
c := qt.New(t)
recursive := `
{{ define "menu-nodes" }}
{{ template "menu-node" }}
{{ end }}
{{ define "menu-node" }}
{{ template "menu-node" }}
{{ end }}
{{ template "menu-nodes" }}
`
templ, err := template.New("foo").Parse(recursive)
c.Assert(err, qt.IsNil)
ts := newTestTemplate(templ)
ctx := newTemplateContext(
ts,
newTestTemplateLookup(ts),
)
ctx.applyTransformations(templ.Tree.Root)
}
func newTestTemplate(templ tpl.Template) *templateState {
return newTemplateState(nil,
templ,
templateInfo{
name: templ.Name(),
},
nil,
)
}
func newTestTemplateLookup(in *templateState) func(name string) *templateState {
m := make(map[string]*templateState)
return func(name string) *templateState {
if in.Name() == name {
return in
}
if ts, found := m[name]; found {
return ts
}
if templ, found := findTemplateIn(name, in); found {
ts := newTestTemplate(templ)
m[name] = ts
return ts
}
return nil
}
}
func TestCollectInfo(t *testing.T) {
configStr := `{ "version": 42 }`
tests := []struct {
name string
tplString string
expected tpl.ParseInfo
}{
{"Basic Inner", `{{ .Inner }}`, tpl.ParseInfo{IsInner: true, Config: tpl.DefaultParseConfig}},
{"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.ParseInfo{Config: tpl.ParseConfig{Version: 42}}},
}
echo := func(in any) any {
return in
}
funcs := template.FuncMap{
"highlight": echo,
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := qt.New(t)
templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
c.Assert(err, qt.IsNil)
ts := newTestTemplate(templ)
ts.typ = templateShortcode
ctx := newTemplateContext(
ts,
newTestTemplateLookup(ts),
)
ctx.applyTransformations(templ.Tree.Root)
c.Assert(ctx.t.parseInfo, qt.DeepEquals, test.expected)
})
}
}
func TestPartialReturn(t *testing.T) {
tests := []struct {
name string
tplString string
expected bool
}{
{"Basic", `
{{ $a := "Hugo Rocks!" }}
{{ return $a }}
`, true},
{"Expression", `
{{ return add 32 }}
`, true},
}
echo := func(in any) any {
return in
}
funcs := template.FuncMap{
"return": echo,
"add": echo,
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := qt.New(t)
templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
c.Assert(err, qt.IsNil)
ts := newTestTemplate(templ)
ctx := newTemplateContext(
ts,
newTestTemplateLookup(ts),
)
_, err = ctx.applyTransformations(templ.Tree.Root)
// Just check that it doesn't fail in this test. We have functional tests
// in hugoblib.
c.Assert(err, qt.IsNil)
})
}
}

View File

@@ -1,64 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tplimpl
import (
"fmt"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/identity"
)
var _ identity.Identity = (*templateInfo)(nil)
type templateInfo struct {
name string
template string
isText bool // HTML or plain text template.
isEmbedded bool
meta *hugofs.FileMeta
}
func (t templateInfo) IdentifierBase() string {
return t.name
}
func (t templateInfo) Name() string {
return t.name
}
func (t templateInfo) Filename() string {
return t.meta.Filename
}
func (t templateInfo) IsZero() bool {
return t.name == ""
}
func (t templateInfo) resolveType() templateType {
return resolveTemplateType(t.name)
}
func (info templateInfo) errWithFileContext(what string, err error) error {
err = fmt.Errorf(what+": %w", err)
fe := herrors.NewFileErrorFromName(err, info.meta.Filename)
f, err := info.meta.Open()
if err != nil {
return err
}
defer f.Close()
return fe.UpdateContent(f, nil)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Portions Copyright The Go Authors.
@@ -25,46 +25,7 @@ import (
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/tpl"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/tpl/internal"
// Init the namespaces
_ "github.com/gohugoio/hugo/tpl/cast"
_ "github.com/gohugoio/hugo/tpl/collections"
_ "github.com/gohugoio/hugo/tpl/compare"
_ "github.com/gohugoio/hugo/tpl/crypto"
_ "github.com/gohugoio/hugo/tpl/css"
_ "github.com/gohugoio/hugo/tpl/data"
_ "github.com/gohugoio/hugo/tpl/debug"
_ "github.com/gohugoio/hugo/tpl/diagrams"
_ "github.com/gohugoio/hugo/tpl/encoding"
_ "github.com/gohugoio/hugo/tpl/fmt"
_ "github.com/gohugoio/hugo/tpl/hash"
_ "github.com/gohugoio/hugo/tpl/hugo"
_ "github.com/gohugoio/hugo/tpl/images"
_ "github.com/gohugoio/hugo/tpl/inflect"
_ "github.com/gohugoio/hugo/tpl/js"
_ "github.com/gohugoio/hugo/tpl/lang"
_ "github.com/gohugoio/hugo/tpl/math"
_ "github.com/gohugoio/hugo/tpl/openapi/openapi3"
_ "github.com/gohugoio/hugo/tpl/os"
_ "github.com/gohugoio/hugo/tpl/page"
_ "github.com/gohugoio/hugo/tpl/partials"
_ "github.com/gohugoio/hugo/tpl/path"
_ "github.com/gohugoio/hugo/tpl/reflect"
_ "github.com/gohugoio/hugo/tpl/resources"
_ "github.com/gohugoio/hugo/tpl/safe"
_ "github.com/gohugoio/hugo/tpl/site"
_ "github.com/gohugoio/hugo/tpl/strings"
_ "github.com/gohugoio/hugo/tpl/templates"
_ "github.com/gohugoio/hugo/tpl/time"
_ "github.com/gohugoio/hugo/tpl/transform"
_ "github.com/gohugoio/hugo/tpl/urls"
maps0 "maps"
)
var (
@@ -212,89 +173,3 @@ func (t *templateExecHelper) trackDependencies(ctx context.Context, tmpl texttem
return ctx
}
func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflect.Value) {
funcs := createFuncMap(d)
funcsv := make(map[string]reflect.Value)
for k, v := range funcs {
vv := reflect.ValueOf(v)
funcsv[k] = vv
}
// Duplicate Go's internal funcs here for faster lookups.
for k, v := range template.GoFuncs {
if _, exists := funcsv[k]; !exists {
vv, ok := v.(reflect.Value)
if !ok {
vv = reflect.ValueOf(v)
}
funcsv[k] = vv
}
}
for k, v := range texttemplate.GoFuncs {
if _, exists := funcsv[k]; !exists {
funcsv[k] = v
}
}
exeHelper := &templateExecHelper{
watching: d.Conf.Watching(),
funcs: funcsv,
site: reflect.ValueOf(d.Site),
siteParams: reflect.ValueOf(d.Site.Params()),
}
return texttemplate.NewExecuter(
exeHelper,
), funcsv
}
func createFuncMap(d *deps.Deps) map[string]any {
if d.TmplFuncMap != nil {
return d.TmplFuncMap
}
funcMap := template.FuncMap{}
nsMap := make(map[string]any)
var onCreated []func(namespaces map[string]any)
// Merge the namespace funcs
for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
ns := nsf(d)
if _, exists := funcMap[ns.Name]; exists {
panic(ns.Name + " is a duplicate template func")
}
funcMap[ns.Name] = ns.Context
contextV, err := ns.Context(context.Background())
if err != nil {
panic(err)
}
nsMap[ns.Name] = contextV
for _, mm := range ns.MethodMappings {
for _, alias := range mm.Aliases {
if _, exists := funcMap[alias]; exists {
panic(alias + " is a duplicate template func")
}
funcMap[alias] = mm.Method
}
}
if ns.OnCreated != nil {
onCreated = append(onCreated, ns.OnCreated)
}
}
for _, f := range onCreated {
f(nsMap)
}
if d.OverloadedTemplateFuncs != nil {
maps0.Copy(funcMap, d.OverloadedTemplateFuncs)
}
d.TmplFuncMap = funcMap
return d.TmplFuncMap
}

View File

@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.

View File

@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,24 +11,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package tpl
package tplimpl
// Increments on breaking changes.
const TemplateVersion = 2
type Info interface {
ParseInfo() ParseInfo
}
type FileInfo interface {
Name() string
Filename() string
}
type IsInternalTemplateProvider interface {
IsInternalTemplate() bool
}
// ParseInfo holds information about a parsed ntemplate.
type ParseInfo struct {
// Set for shortcode templates with any {{ .Inner }}
IsInner bool
@@ -44,14 +32,15 @@ func (info ParseInfo) IsZero() bool {
return info.Config.Version == 0
}
// ParseConfig holds configuration extracted from the template.
type ParseConfig struct {
Version int
}
var DefaultParseConfig = ParseConfig{
var defaultParseConfig = ParseConfig{
Version: TemplateVersion,
}
var DefaultParseInfo = ParseInfo{
Config: DefaultParseConfig,
var defaultParseInfo = ParseInfo{
Config: defaultParseConfig,
}

View File

@@ -1,40 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tplimpl
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestNeedsBaseTemplate(t *testing.T) {
c := qt.New(t)
c.Assert(needsBaseTemplate(`{{ define "main" }}`), qt.Equals, true)
c.Assert(needsBaseTemplate(`{{define "main" }}`), qt.Equals, true)
c.Assert(needsBaseTemplate(`{{- define "main" }}`), qt.Equals, true)
c.Assert(needsBaseTemplate(`{{-define "main" }}`), qt.Equals, true)
c.Assert(needsBaseTemplate(`
{{-define "main" }}
`), qt.Equals, true)
c.Assert(needsBaseTemplate(` {{ define "main" }}`), qt.Equals, true)
c.Assert(needsBaseTemplate(`
{{ define "main" }}`), qt.Equals, true)
c.Assert(needsBaseTemplate(` A {{ define "main" }}`), qt.Equals, false)
c.Assert(needsBaseTemplate(` {{ printf "foo" }}`), qt.Equals, false)
c.Assert(needsBaseTemplate(`{{/* comment */}} {{ define "main" }}`), qt.Equals, true)
c.Assert(needsBaseTemplate(` {{/* comment */}} A {{ define "main" }}`), qt.Equals, false)
}

View File

@@ -0,0 +1,225 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tplimpl
import (
"github.com/gohugoio/hugo/resources/kinds"
)
const baseNameBaseof = "baseof"
// This is used both as a key and in lookups.
type TemplateDescriptor struct {
// Group 1.
Kind string // page, home, section, taxonomy, term (and only those)
Layout string // list, single, baseof, mycustomlayout.
// Group 2.
OutputFormat string // rss, csv ...
MediaType string // text/html, text/plain, ...
Lang string // en, nn, fr, ...
Variant1 string // contextual variant, e.g. "link" in render hooks."
Variant2 string // contextual variant, e.g. "id" in render.
// Misc.
LayoutMustMatch bool // If set, we only look for the exact layout.
IsPlainText bool // Whether this is a plain text template.
}
func (d *TemplateDescriptor) normalizeFromFile() {
// fmt.Println("normalizeFromFile", "kind:", d.Kind, "layout:", d.Layout, "of:", d.OutputFormat)
if d.Layout == d.OutputFormat {
d.Layout = ""
}
if d.Kind == kinds.KindTemporary {
d.Kind = ""
}
if d.Layout == d.Kind {
d.Layout = ""
}
}
type descriptorHandler struct {
opts StoreOptions
}
// Note that this in this setup is usually a descriptor constructed from a page,
// so we want to find the best match for that page.
func (s descriptorHandler) compareDescriptors(category Category, this, other TemplateDescriptor) weight {
if this.LayoutMustMatch && this.Layout != other.Layout {
return weightNoMatch
}
w := this.doCompare(category, other)
if w.w1 <= 0 {
if category == CategoryMarkup && (this.Variant1 == other.Variant1) && (this.Variant2 == other.Variant2 || this.Variant2 != "" && other.Variant2 == "") {
// See issue 13242.
if this.OutputFormat != other.OutputFormat && this.OutputFormat == s.opts.DefaultOutputFormat {
return w
}
w.w1 = 1
return w
}
}
return w
}
//lint:ignore ST1006 this vs other makes it easier to reason about.
func (this TemplateDescriptor) doCompare(category Category, other TemplateDescriptor) weight {
w := weightNoMatch
// HTML in plain text is OK, but not the other way around.
if other.IsPlainText && !this.IsPlainText {
return w
}
if other.Kind != "" && other.Kind != this.Kind {
return w
}
if other.Layout != "" && other.Layout != layoutAll && other.Layout != this.Layout {
if isLayoutCustom(this.Layout) {
if this.Kind == "" {
this.Layout = ""
} else if this.Kind == kinds.KindPage {
this.Layout = layoutSingle
} else {
this.Layout = layoutList
}
}
// Test again.
if other.Layout != this.Layout {
return w
}
}
if other.Lang != "" && other.Lang != this.Lang {
return w
}
if other.OutputFormat != "" && other.OutputFormat != this.OutputFormat {
if this.MediaType != other.MediaType {
return w
}
// We want e.g. home page in amp output format (media type text/html) to
// find a template even if one isn't specified for that output format,
// when one exist for the html output format (same media type).
if category != CategoryBaseof && (this.Kind == "" || (this.Kind != other.Kind && this.Layout != other.Layout)) {
return w
}
// Continue.
}
// One example of variant1 and 2 is for render codeblocks:
// variant1=codeblock, variant2=go (language).
if other.Variant1 != "" && other.Variant1 != this.Variant1 {
return w
}
// If both are set and different, no match.
if other.Variant2 != "" && this.Variant2 != "" && other.Variant2 != this.Variant2 {
return w
}
const (
weightKind = 3 // page, home, section, taxonomy, term (and only those)
weightcustomLayout = 4 // custom layout (mylayout, set in e.g. front matter)
weightLayout = 2 // standard layouts (single,list,all)
weightOutputFormat = 2 // a configured output format (e.g. rss, html, json)
weightMediaType = 1 // a configured media type (e.g. text/html, text/plain)
weightLang = 1 // a configured language (e.g. en, nn, fr, ...)
weightVariant1 = 4 // currently used for render hooks, e.g. "link", "image"
weightVariant2 = 2 // currently used for render hooks, e.g. the language "go" in code blocks.
// We will use the values for group 2 and 3
// if the distance up to the template is shorter than
// the one we're comparing with.
// E.g for a page in /posts/mypage.md with the
// two templates /layouts/posts/single.html and /layouts/page.html,
// the first one is the best match even if the second one
// has a higher w1 value.
weight2Group1 = 1 // kind, standardl layout (single,list,all)
weight2Group2 = 2 // custom layout (mylayout)
weight3 = 1 // for media type, lang, output format.
)
// Now we now know that the other descriptor is a subset of this.
// Now calculate the weights.
w.w1++
if other.Kind != "" && other.Kind == this.Kind {
w.w1 += weightKind
w.w2 = weight2Group1
}
if other.Layout != "" && other.Layout == this.Layout || other.Layout == layoutAll {
if isLayoutCustom(this.Layout) {
w.w1 += weightcustomLayout
w.w2 = weight2Group2
} else {
w.w1 += weightLayout
w.w2 = weight2Group1
}
}
if other.Lang != "" && other.Lang == this.Lang {
w.w1 += weightLang
w.w3 += weight3
}
if other.OutputFormat != "" && other.OutputFormat == this.OutputFormat {
w.w1 += weightOutputFormat
w.w3 += weight3
}
if other.MediaType != "" && other.MediaType == this.MediaType {
w.w1 += weightMediaType
w.w3 += weight3
}
if other.Variant1 != "" && other.Variant1 == this.Variant1 {
w.w1 += weightVariant1
}
if other.Variant2 != "" && other.Variant2 == this.Variant2 {
w.w1 += weightVariant2
}
if other.Variant2 != "" && this.Variant2 == "" {
w.w1--
}
return w
}
func (d TemplateDescriptor) IsZero() bool {
return d == TemplateDescriptor{}
}
//lint:ignore ST1006 this vs other makes it easier to reason about.
func (this TemplateDescriptor) isKindInLayout(layout string) bool {
if this.Kind == "" {
return true
}
if this.Kind != kinds.KindPage {
return layout != layoutSingle
}
return layout != layoutList
}

View File

@@ -0,0 +1,104 @@
package tplimpl
import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources/kinds"
)
func TestTemplateDescriptorCompare(t *testing.T) {
c := qt.New(t)
dh := descriptorHandler{
opts: StoreOptions{
OutputFormats: output.DefaultFormats,
DefaultOutputFormat: "html",
},
}
less := func(category Category, this, other1, other2 TemplateDescriptor) {
c.Helper()
result1 := dh.compareDescriptors(category, this, other1)
result2 := dh.compareDescriptors(category, this, other2)
c.Assert(result1.w1 < result2.w1, qt.IsTrue, qt.Commentf("%d < %d", result1, result2))
}
check := func(category Category, this, other TemplateDescriptor, less bool) {
c.Helper()
result := dh.compareDescriptors(category, this, other)
if less {
c.Assert(result.w1 < 0, qt.IsTrue, qt.Commentf("%d", result))
} else {
c.Assert(result.w1 >= 0, qt.IsTrue, qt.Commentf("%d", result))
}
}
check(
CategoryBaseof,
TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "404", MediaType: "text/html"},
TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "html", MediaType: "text/html"},
false,
)
check(
CategoryLayout,
TemplateDescriptor{Kind: "", Lang: "en", OutputFormat: "404", MediaType: "text/html"},
TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "alias", MediaType: "text/html"},
true,
)
less(
CategoryLayout,
TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "html"},
TemplateDescriptor{Layout: "list", OutputFormat: "html"},
TemplateDescriptor{Kind: kinds.KindHome, OutputFormat: "html"},
)
check(
CategoryLayout,
TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "html", MediaType: "text/html"},
TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "myformat", MediaType: "text/html"},
false,
)
}
// INFO timer: name resolveTemplate count 779 duration 5.482274ms average 7.037µs median 4µs
func BenchmarkCompareDescriptors(b *testing.B) {
dh := descriptorHandler{
opts: StoreOptions{
OutputFormats: output.DefaultFormats,
DefaultOutputFormat: "html",
},
}
pairs := []struct {
d1, d2 TemplateDescriptor
}{
{
TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "404", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
},
{
TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
TemplateDescriptor{Kind: "", Layout: "list", OutputFormat: "", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
},
{
TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "alias", MediaType: "text/html", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
},
{
TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
TemplateDescriptor{Kind: "", Layout: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "nn", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, pair := range pairs {
_ = dh.compareDescriptors(CategoryLayout, pair.d1, pair.d2)
}
}
}

331
tpl/tplimpl/templates.go Normal file
View File

@@ -0,0 +1,331 @@
package tplimpl
import (
"io"
"regexp"
"strings"
"unicode"
"unicode/utf8"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/tpl"
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
)
func (t *templateNamespace) readTemplateInto(templ *TemplInfo) error {
if err := func() error {
meta := templ.Fi.Meta()
f, err := meta.Open()
if err != nil {
return err
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return err
}
templ.Content = removeLeadingBOM(string(b))
if !templ.NoBaseOf {
templ.NoBaseOf = !needsBaseTemplate(templ.Content)
}
return nil
}(); err != nil {
return err
}
return nil
}
// The tweet and twitter shortcodes were deprecated in favor of the x shortcode
// in v0.141.0. We can remove these aliases in v0.155.0 or later.
var embeddedTemplatesAliases = map[string][]string{
"_shortcodes/twitter.html": {"_shortcodes/tweet.html"},
}
func (t *templateNamespace) parseTemplate(ti *TemplInfo) error {
if !ti.NoBaseOf || ti.Category == CategoryBaseof {
// Delay parsing until we have the base template.
return nil
}
pi := ti.PathInfo
name := pi.PathNoLeadingSlash()
if ti.isLegacyMapped {
// When mapping the old taxonomy structure to the new one, we may map the same path to multiple templates per kind.
// Append the kind here to make the name unique.
name += ("-" + ti.D.Kind)
}
var (
templ tpl.Template
err error
)
if ti.D.IsPlainText {
prototype := t.parseText
templ, err = prototype.New(name).Parse(ti.Content)
if err != nil {
return err
}
} else {
prototype := t.parseHTML
templ, err = prototype.New(name).Parse(ti.Content)
if err != nil {
return err
}
if ti.SubCategory == SubCategoryEmbedded {
// In Hugo 0.146.0 we moved the internal templates around.
// For the "_internal/twitter_cards.html" style templates, they
// were moved to the _partials directory.
// But we need to make them accessible from the old path for a while.
if pi.Type() == paths.TypePartial {
aliasName := strings.TrimPrefix(name, "_partials/")
aliasName = "_internal/" + aliasName
_, err = prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree)
if err != nil {
return err
}
}
// This was also possible before Hugo 0.146.0, but this should be deprecated.
if pi.Type() == paths.TypeShortcode {
aliasName := strings.TrimPrefix(name, "_shortcodes/")
aliasName = "_internal/shortcodes/" + aliasName
_, err = prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree)
if err != nil {
return err
}
}
}
}
ti.Template = templ
return nil
}
func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTemplateInfo) error {
tb := &TemplWithBaseApplied{
Overlay: overlay,
Base: base.Info,
}
base.Info.Overlays = append(base.Info.Overlays, overlay)
var templ tpl.Template
if overlay.D.IsPlainText {
tt := texttemplate.Must(t.parseText.Clone()).New(overlay.PathInfo.PathNoLeadingSlash())
var err error
tt, err = tt.Parse(base.Info.Content)
if err != nil {
return err
}
tt, err = tt.Parse(overlay.Content)
if err != nil {
return err
}
templ = tt
t.baseofTextClones = append(t.baseofTextClones, tt)
} else {
tt := htmltemplate.Must(t.parseHTML.CloneShallow()).New(overlay.PathInfo.PathNoLeadingSlash())
var err error
tt, err = tt.Parse(base.Info.Content)
if err != nil {
return err
}
tt, err = tt.Parse(overlay.Content)
if err != nil {
return err
}
templ = tt
t.baseofHtmlClones = append(t.baseofHtmlClones, tt)
}
tb.Template = &TemplInfo{
Template: templ,
Base: base.Info,
PathInfo: overlay.PathInfo,
Fi: overlay.Fi,
D: overlay.D,
NoBaseOf: true,
}
variants := overlay.BaseVariants.Get(base.Key)
if variants == nil {
variants = make(map[TemplateDescriptor]*TemplWithBaseApplied)
overlay.BaseVariants.Insert(base.Key, variants)
}
variants[base.Info.D] = tb
return nil
}
func (t *templateNamespace) templatesIn(in tpl.Template) []tpl.Template {
var templs []tpl.Template
if textt, ok := in.(*texttemplate.Template); ok {
for _, t := range textt.Templates() {
templs = append(templs, t)
}
}
if htmlt, ok := in.(*htmltemplate.Template); ok {
for _, t := range htmlt.Templates() {
templs = append(templs, t)
}
}
return templs
}
/*
func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) {
if overlay.isText {
var (
templ = t.main.getPrototypeText(prototypeCloneIDBaseof).New(overlay.name)
err error
)
if !base.IsZero() {
templ, err = templ.Parse(base.template)
if err != nil {
return nil, base.errWithFileContext("text: base: parse failed", err)
}
}
templ, err = texttemplate.Must(templ.Clone()).Parse(overlay.template)
if err != nil {
return nil, overlay.errWithFileContext("text: overlay: parse failed", err)
}
// The extra lookup is a workaround, see
// * https://github.com/golang/go/issues/16101
// * https://github.com/gohugoio/hugo/issues/2549
// templ = templ.Lookup(templ.Name())
return templ, nil
}
var (
templ = t.main.getPrototypeHTML(prototypeCloneIDBaseof).New(overlay.name)
err error
)
if !base.IsZero() {
templ, err = templ.Parse(base.template)
if err != nil {
return nil, base.errWithFileContext("html: base: parse failed", err)
}
}
templ, err = htmltemplate.Must(templ.Clone()).Parse(overlay.template)
if err != nil {
return nil, overlay.errWithFileContext("html: overlay: parse failed", err)
}
// The extra lookup is a workaround, see
// * https://github.com/golang/go/issues/16101
// * https://github.com/gohugoio/hugo/issues/2549
templ = templ.Lookup(templ.Name())
return templ, err
}
*/
var baseTemplateDefineRe = regexp.MustCompile(`^{{-?\s*define`)
// needsBaseTemplate returns true if the first non-comment template block is a
// define block.
func needsBaseTemplate(templ string) bool {
idx := -1
inComment := false
for i := 0; i < len(templ); {
if !inComment && strings.HasPrefix(templ[i:], "{{/*") {
inComment = true
i += 4
} else if !inComment && strings.HasPrefix(templ[i:], "{{- /*") {
inComment = true
i += 6
} else if inComment && strings.HasPrefix(templ[i:], "*/}}") {
inComment = false
i += 4
} else if inComment && strings.HasPrefix(templ[i:], "*/ -}}") {
inComment = false
i += 6
} else {
r, size := utf8.DecodeRuneInString(templ[i:])
if !inComment {
if strings.HasPrefix(templ[i:], "{{") {
idx = i
break
} else if !unicode.IsSpace(r) {
break
}
}
i += size
}
}
if idx == -1 {
return false
}
return baseTemplateDefineRe.MatchString(templ[idx:])
}
func removeLeadingBOM(s string) string {
const bom = '\ufeff'
for i, r := range s {
if i == 0 && r != bom {
return s
}
if i > 0 {
return s[i:]
}
}
return s
}
type templateNamespace struct {
parseText *texttemplate.Template
parseHTML *htmltemplate.Template
prototypeText *texttemplate.Template
prototypeHTML *htmltemplate.Template
standaloneText *texttemplate.Template
baseofTextClones []*texttemplate.Template
baseofHtmlClones []*htmltemplate.Template
}
func (t *templateNamespace) createPrototypesParse() error {
if t.prototypeHTML == nil {
panic("prototypeHTML not set")
}
t.parseHTML = htmltemplate.Must(t.prototypeHTML.Clone())
t.parseText = texttemplate.Must(t.prototypeText.Clone())
return nil
}
func (t *templateNamespace) createPrototypes(init bool) error {
if init {
t.prototypeHTML = htmltemplate.Must(t.parseHTML.Clone())
t.prototypeText = texttemplate.Must(t.parseText.Clone())
}
// t.execHTML = htmltemplate.Must(t.parseHTML.Clone())
// t.execText = texttemplate.Must(t.parseText.Clone())
return nil
}
func newTemplateNamespace(funcs map[string]any) *templateNamespace {
return &templateNamespace{
parseHTML: htmltemplate.New("").Funcs(funcs),
parseText: texttemplate.New("").Funcs(funcs),
standaloneText: texttemplate.New("").Funcs(funcs),
}
}

1854
tpl/tplimpl/templatestore.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,842 @@
package tplimpl_test
import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/resources/kinds"
"github.com/gohugoio/hugo/tpl/tplimpl"
)
// Old as in before Hugo v0.146.0.
func TestLayoutsOldSetup(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
title = "Title in English"
weight = 1
[languages.nn]
title = "Tittel på nynorsk"
weight = 2
-- layouts/index.html --
Home.
{{ template "_internal/twitter_cards.html" . }}
-- layouts/_default/single.html --
Single.
-- layouts/_default/single.nn.html --
Single NN.
-- layouts/_default/list.html --
List HTML.
-- layouts/docs/list-baseof.html --
Docs Baseof List HTML.
{{ block "main" . }}Docs Baseof List HTML main block.{{ end }}
-- layouts/docs/list.section.html --
{{ define "main" }}
Docs List HTML.
{{ end }}
-- layouts/_default/list.json --
List JSON.
-- layouts/_default/list.rss.xml --
List RSS.
-- layouts/_default/list.nn.rss.xml --
List NN RSS.
-- layouts/_default/baseof.html --
Base.
-- layouts/partials/mypartial.html --
Partial.
-- layouts/shortcodes/myshortcode.html --
Shortcode.
-- content/docs/p1.md --
---
title: "P1"
---
`
b := hugolib.Test(t, files)
// b.DebugPrint("", tplimpl.CategoryBaseof)
b.AssertFileContent("public/en/docs/index.html", "Docs Baseof List HTML.\n\nDocs List HTML.")
}
func TestLayoutsOldSetupBaseofPrefix(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- layouts/_default/layout1-baseof.html --
Baseof layout1. {{ block "main" . }}{{ end }}
-- layouts/_default/layout2-baseof.html --
Baseof layout2. {{ block "main" . }}{{ end }}
-- layouts/_default/layout1.html --
{{ define "main" }}Layout1. {{ .Title }}{{ end }}
-- layouts/_default/layout2.html --
{{ define "main" }}Layout2. {{ .Title }}{{ end }}
-- content/p1.md --
---
title: "P1"
layout: "layout1"
---
-- content/p2.md --
---
title: "P2"
layout: "layout2"
---
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "Baseof layout1. Layout1. P1")
b.AssertFileContent("public/p2/index.html", "Baseof layout2. Layout2. P2")
}
func TestLayoutsOldSetupTaxonomyAndTerm(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
[taxonomies]
cat = 'cats'
dog = 'dogs'
# Templates for term taxonomy, old setup.
-- layouts/dogs/terms.html --
Dogs Terms. Most specific taxonomy template.
-- layouts/taxonomy/terms.html --
Taxonomy Terms. Down the list.
# Templates for term term, old setup.
-- layouts/dogs/term.html --
Dogs Term. Most specific term template.
-- layouts/term/term.html --
Term Term. Down the list.
-- layouts/dogs/max/list.html --
max: {{ .Title }}
-- layouts/_default/list.html --
Default list.
-- layouts/_default/single.html --
Default single.
-- content/p1.md --
---
title: "P1"
dogs: ["luna", "daisy", "max"]
---
`
b := hugolib.Test(t, files, hugolib.TestOptWarn())
b.AssertLogContains("! WARN")
b.AssertFileContent("public/dogs/index.html", "Dogs Terms. Most specific taxonomy template.")
b.AssertFileContent("public/dogs/luna/index.html", "Dogs Term. Most specific term template.")
b.AssertFileContent("public/dogs/max/index.html", "max: Max") // layouts/dogs/max/list.html wins over layouts/term/term.html because of distance.
}
func TestLayoutsOldSetupCustomRSS(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "page"]
[outputs]
home = ["rss"]
-- layouts/_default/list.rss.xml --
List RSS.
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.xml", "List RSS.")
}
var newSetupTestSites = `
-- hugo.toml --
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
title = "Title in English"
weight = 1
[languages.nn]
title = "Tittel på nynorsk"
weight = 2
[languages.fr]
title = "Titre en français"
weight = 3
[outputs]
home = ["html", "rss", "redir"]
[outputFormats]
[outputFormats.redir]
mediatype = "text/plain"
baseName = "_redirects"
isPlainText = true
-- layouts/404.html --
{{ define "main" }}
404.
{{ end }}
-- layouts/home.html --
{{ define "main" }}
Home: {{ .Title }}|{{ .Content }}|
Inline Partial: {{ partial "my-inline-partial.html" . }}
{{ end }}
{{ define "hero" }}
Home hero.
{{ end }}
{{ define "partials/my-inline-partial.html" }}
{{ $value := 32 }}
{{ return $value }}
{{ end }}
-- layouts/index.redir --
Redir.
-- layouts/single.html --
{{ define "main" }}
Single needs base.
{{ end }}
-- layouts/foo/bar/single.html --
{{ define "main" }}
Single sub path.
{{ end }}
-- layouts/_markup/render-codeblock.html --
Render codeblock.
-- layouts/_markup/render-blockquote.html --
Render blockquote.
-- layouts/_markup/render-codeblock-go.html --
Render codeblock go.
-- layouts/_markup/render-link.html --
Link: {{ .Destination | safeURL }}
-- layouts/foo/baseof.html --
Base sub path.{{ block "main" . }}{{ end }}
-- layouts/foo/bar/baseof.page.html --
Base sub path.{{ block "main" . }}{{ end }}
-- layouts/list.html --
{{ define "main" }}
List needs base.
{{ end }}
-- layouts/section.html --
Section.
-- layouts/mysectionlayout.section.fr.amp.html --
Section with layout.
-- layouts/baseof.html --
Base.{{ block "main" . }}{{ end }}
Hero:{{ block "hero" . }}{{ end }}:
{{ with (templates.Defer (dict "key" "global")) }}
Defer Block.
{{ end }}
-- layouts/baseof.fr.html --
Base fr.{{ block "main" . }}{{ end }}
-- layouts/baseof.term.html --
Base term.
-- layouts/baseof.section.fr.amp.html --
Base with identifiers.{{ block "main" . }}{{ end }}
-- layouts/partials/mypartial.html --
Partial. {{ partial "_inline/my-inline-partial-in-partial-with-no-ext" . }}
{{ define "partials/_inline/my-inline-partial-in-partial-with-no-ext" }}
Partial in partial.
{{ end }}
-- layouts/partials/returnfoo.html --
{{ $v := "foo" }}
{{ return $v }}
-- layouts/shortcodes/myshortcode.html --
Shortcode. {{ partial "mypartial.html" . }}|return:{{ partial "returnfoo.html" . }}|
-- content/_index.md --
---
title: Home sweet home!
---
{{< myshortcode >}}
> My blockquote.
Markdown link: [Foo](/foo)
-- content/p1.md --
---
title: "P1"
---
-- content/foo/bar/index.md --
---
title: "Foo Bar"
---
{{< myshortcode >}}
-- content/single-list.md --
---
title: "Single List"
layout: "list"
---
`
func TestLayoutsType(t *testing.T) {
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term"]
-- layouts/list.html --
List.
-- layouts/mysection/single.html --
mysection/single|{{ .Title }}
-- layouts/mytype/single.html --
mytype/single|{{ .Title }}
-- content/mysection/_index.md --
-- content/mysection/mysubsection/_index.md --
-- content/mysection/mysubsection/p1.md --
---
title: "P1"
---
-- content/mysection/mysubsection/p2.md --
---
title: "P2"
type: "mytype"
---
`
b := hugolib.Test(t, files, hugolib.TestOptWarn())
b.AssertLogContains("! WARN")
b.AssertFileContent("public/mysection/mysubsection/p1/index.html", "mysection/single|P1")
b.AssertFileContent("public/mysection/mysubsection/p2/index.html", "mytype/single|P2")
}
// New, as in from Hugo v0.146.0.
func TestLayoutsNewSetup(t *testing.T) {
const numIterations = 1
for range numIterations {
b := hugolib.Test(t, newSetupTestSites, hugolib.TestOptWarn())
b.AssertLogContains("! WARN")
b.AssertFileContent("public/en/index.html",
"Base.\nHome: Home sweet home!|",
"|Shortcode.\n|",
"<p>Markdown link: Link: /foo</p>",
"|return:foo|",
"Defer Block.",
"Home hero.",
"Render blockquote.",
)
b.AssertFileContent("public/en/p1/index.html", "Base.\nSingle needs base.\n\nHero::\n\nDefer Block.")
b.AssertFileContent("public/en/404.html", "404.")
b.AssertFileContent("public/nn/404.html", "404.")
b.AssertFileContent("public/fr/404.html", "404.")
}
}
func TestHomeRSSAndHTMLWithHTMLOnlyShortcode(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term"]
[outputs]
home = ["html", "rss"]
-- layouts/home.html --
Home: {{ .Title }}|{{ .Content }}|
-- layouts/single.html --
Single: {{ .Title }}|{{ .Content }}|
-- layouts/shortcodes/myshortcode.html --
Myshortcode: Count: {{ math.Counter }}|
-- content/p1.md --
---
title: "P1"
---
{{< myshortcode >}}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "Single: P1|Myshortcode: Count: 1|")
b.AssertFileContent("public/index.xml", "Myshortcode: Count: 1")
}
func TestHomeRSSAndHTMLWithHTMLOnlyRenderHook(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term"]
[outputs]
home = ["html", "rss"]
-- layouts/home.html --
Home: {{ .Title }}|{{ .Content }}|
-- layouts/single.html --
Single: {{ .Title }}|{{ .Content }}|
-- layouts/_markup/render-link.html --
Render Link: {{ math.Counter }}|
-- content/p1.md --
---
title: "P1"
---
Link: [Foo](/foo)
`
for range 2 {
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.xml", "Link: Render Link: 1|")
b.AssertFileContent("public/p1/index.html", "Single: P1|<p>Link: Render Link: 1|<")
}
}
func TestRenderCodeblockSpecificity(t *testing.T) {
files := `
-- hugo.toml --
-- layouts/_markup/render-codeblock.html --
Render codeblock.|{{ .Inner }}|
-- layouts/_markup/render-codeblock-go.html --
Render codeblock go.|{{ .Inner }}|
-- layouts/single.html --
{{ .Title }}|{{ .Content }}|
-- content/p1.md --
---
title: "P1"
---
§§§
Basic
§§§
§§§ go
Go
§§§
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "P1|Render codeblock.|Basic|Render codeblock go.|Go|")
}
func TestPrintUnusedTemplates(t *testing.T) {
t.Parallel()
files := `
-- config.toml --
baseURL = 'http://example.com/'
printUnusedTemplates=true
-- content/p1.md --
---
title: "P1"
---
{{< usedshortcode >}}
-- layouts/baseof.html --
{{ block "main" . }}{{ end }}
-- layouts/baseof.json --
{{ block "main" . }}{{ end }}
-- layouts/index.html --
{{ define "main" }}FOO{{ end }}
-- layouts/_default/single.json --
-- layouts/_default/single.html --
{{ define "main" }}MAIN{{ end }}
-- layouts/post/single.html --
{{ define "main" }}MAIN{{ end }}
-- layouts/_partials/usedpartial.html --
-- layouts/_partials/unusedpartial.html --
-- layouts/_shortcodes/usedshortcode.html --
{{ partial "usedpartial.html" }}
-- layouts/shortcodes/unusedshortcode.html --
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
NeedsOsFS: true,
},
)
b.Build()
unused := b.H.GetTemplateStore().UnusedTemplates()
var names []string
for _, tmpl := range unused {
if fi := tmpl.Fi; fi != nil {
names = append(names, fi.Meta().PathInfo.PathNoLeadingSlash())
}
}
b.Assert(len(unused), qt.Equals, 5, qt.Commentf("%#v", names))
b.Assert(names, qt.DeepEquals, []string{"_partials/unusedpartial.html", "shortcodes/unusedshortcode.html", "baseof.json", "post/single.html", "_default/single.json"})
}
func TestCreateManyTemplateStores(t *testing.T) {
t.Parallel()
b := hugolib.Test(t, newSetupTestSites)
store := b.H.TemplateStore
for range 70 {
newStore, err := store.NewFromOpts()
b.Assert(err, qt.IsNil)
b.Assert(newStore, qt.Not(qt.IsNil))
}
}
func BenchmarkLookupPagesLayout(b *testing.B) {
files := `
-- hugo.toml --
-- layouts/single.html --
{{ define "main" }}
Main.
{{ end }}
-- layouts/baseof.html --
baseof: {{ block "main" . }}{{ end }}
-- layouts/foo/bar/single.html --
{{ define "main" }}
Main.
{{ end }}
`
bb := hugolib.Test(b, files)
store := bb.H.TemplateStore
b.ResetTimer()
b.Run("Single root", func(b *testing.B) {
q := tplimpl.TemplateQuery{
Path: "/baz",
Category: tplimpl.CategoryLayout,
Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, Layout: "single", OutputFormat: "html"},
}
for i := 0; i < b.N; i++ {
store.LookupPagesLayout(q)
}
})
b.Run("Single sub folder", func(b *testing.B) {
q := tplimpl.TemplateQuery{
Path: "/foo/bar",
Category: tplimpl.CategoryLayout,
Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, Layout: "single", OutputFormat: "html"},
}
for i := 0; i < b.N; i++ {
store.LookupPagesLayout(q)
}
})
}
func BenchmarkNewTemplateStore(b *testing.B) {
bb := hugolib.Test(b, newSetupTestSites)
store := bb.H.TemplateStore
b.ResetTimer()
for i := 0; i < b.N; i++ {
newStore, err := store.NewFromOpts()
if err != nil {
b.Fatal(err)
}
if newStore == nil {
b.Fatal("newStore is nil")
}
}
}
func TestLayoutsLookupVariants(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true
[outputs]
home = ["html", "rss"]
page = ["html", "rss", "amp"]
section = ["html", "rss"]
[languages]
[languages.en]
title = "Title in English"
weight = 1
[languages.nn]
title = "Tittel på nynorsk"
weight = 2
-- layouts/list.xml --
layouts/list.xml
-- layouts/_shortcodes/myshortcode.html --
layouts/shortcodes/myshortcode.html
-- layouts/foo/bar/_shortcodes/myshortcode.html --
layouts/foo/bar/_shortcodes/myshortcode.html
-- layouts/_markup/render-codeblock.html --
layouts/_markup/render-codeblock.html|{{ .Type }}|
-- layouts/_markup/render-codeblock-go.html --
layouts/_markup/render-codeblock-go.html|{{ .Type }}|
-- layouts/single.xml --
layouts/single.xml
-- layouts/single.rss.xml --
layouts/single.rss.xml
-- layouts/single.nn.rss.xml --
layouts/single.nn.rss.xml
-- layouts/list.html --
layouts/list.html
-- layouts/single.html --
layouts/single.html
{{ .Content }}
-- layouts/mylayout.html --
layouts/mylayout.html
-- layouts/mylayout.nn.html --
layouts/mylayout.nn.html
-- layouts/foo/single.rss.xml --
layouts/foo/single.rss.xml
-- layouts/foo/single.amp.html --
layouts/foo/single.amp.html
-- layouts/foo/bar/page.html --
layouts/foo/bar/page.html
-- layouts/foo/bar/baz/single.html --
layouts/foo/bar/baz/single.html
{{ .Content }}
-- layouts/qux/mylayout.html --
layouts/qux/mylayout.html
-- layouts/qux/single.xml --
layouts/qux/single.xml
-- layouts/qux/mylayout.section.html --
layouts/qux/mylayout.section.html
-- content/p.md --
---
---
§§§
code
§§§
§§§ go
code
§§§
{{< myshortcode >}}
-- content/foo/p.md --
-- content/foo/p.nn.md --
-- content/foo/bar/p.md --
-- content/foo/bar/withmylayout.md --
---
layout: mylayout
---
-- content/foo/bar/_index.md --
-- content/foo/bar/baz/p.md --
---
---
{{< myshortcode >}}
-- content/qux/p.md --
-- content/qux/_index.md --
---
layout: mylayout
---
-- content/qux/quux/p.md --
-- content/qux/quux/withmylayout.md --
---
layout: mylayout
---
-- content/qux/quux/withmylayout.nn.md --
---
layout: mylayout
---
`
b := hugolib.Test(t, files, hugolib.TestOptWarn())
// s := b.H.Sites[0].TemplateStore
// s.PrintDebug("", tplimpl.CategoryLayout, os.Stdout)
b.AssertLogContains("! WARN")
// Single pages.
// output format: html.
b.AssertFileContent("public/en/p/index.html", "layouts/single.html",
"layouts/_markup/render-codeblock.html|",
"layouts/_markup/render-codeblock-go.html|go|",
"layouts/shortcodes/myshortcode.html",
)
b.AssertFileContent("public/en/foo/p/index.html", "layouts/single.html")
b.AssertFileContent("public/en/foo/bar/p/index.html", "layouts/foo/bar/page.html")
b.AssertFileContent("public/en/foo/bar/withmylayout/index.html", "layouts/mylayout.html")
b.AssertFileContent("public/en/foo/bar/baz/p/index.html", "layouts/foo/bar/baz/single.html", "layouts/foo/bar/_shortcodes/myshortcode.html")
b.AssertFileContent("public/en/qux/quux/withmylayout/index.html", "layouts/qux/mylayout.html")
// output format: amp.
b.AssertFileContent("public/en/amp/p/index.html", "layouts/single.html")
b.AssertFileContent("public/en/amp/foo/p/index.html", "layouts/foo/single.amp.html")
// output format: rss.
b.AssertFileContent("public/en/p/index.xml", "layouts/single.rss.xml")
b.AssertFileContent("public/en/foo/p/index.xml", "layouts/foo/single.rss.xml")
b.AssertFileContent("public/nn/foo/p/index.xml", "layouts/single.nn.rss.xml")
// Note: There is qux/single.xml that's closer, but the one in the root is used becaulse of the output format match.
b.AssertFileContent("public/en/qux/p/index.xml", "layouts/single.rss.xml")
// Note.
b.AssertFileContent("public/nn/qux/quux/withmylayout/index.html", "layouts/mylayout.nn.html")
// Section pages.
// output format: html.
b.AssertFileContent("public/en/foo/index.html", "layouts/list.html")
b.AssertFileContent("public/en/qux/index.html", "layouts/qux/mylayout.section.html")
// output format: rss.
b.AssertFileContent("public/en/foo/index.xml", "layouts/list.xml")
}
func TestLookupShortcodeDepth(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- layouts/_shortcodes/myshortcode.html --
layouts/_shortcodes/myshortcode.html
-- layouts/foo/_shortcodes/myshortcode.html --
layouts/foo/_shortcodes/myshortcode.html
-- layouts/single.html --
{{ .Content }}|
-- content/p.md --
---
---
{{< myshortcode >}}
-- content/foo/p.md --
---
---
{{< myshortcode >}}
-- content/foo/bar/p.md --
---
---
{{< myshortcode >}}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p/index.html", "layouts/_shortcodes/myshortcode.html")
b.AssertFileContent("public/foo/p/index.html", "layouts/foo/_shortcodes/myshortcode.html")
b.AssertFileContent("public/foo/bar/p/index.html", "layouts/foo/_shortcodes/myshortcode.html")
}
func TestLookupShortcodeLayout(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- layouts/_shortcodes/myshortcode.single.html --
layouts/_shortcodes/myshortcode.single.html
-- layouts/_shortcodes/myshortcode.list.html --
layouts/_shortcodes/myshortcode.list.html
-- layouts/single.html --
{{ .Content }}|
-- layouts/list.html --
{{ .Content }}|
-- content/_index.md --
---
---
{{< myshortcode >}}
-- content/p.md --
---
---
{{< myshortcode >}}
-- content/foo/p.md --
---
---
{{< myshortcode >}}
-- content/foo/bar/p.md --
---
---
{{< myshortcode >}}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p/index.html", "layouts/_shortcodes/myshortcode.single.html")
b.AssertFileContent("public/index.html", "layouts/_shortcodes/myshortcode.list.html")
}
func TestLayoutAll(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- layouts/single.html --
Single.
-- layouts/all.html --
All.
-- content/p1.md --
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "Single.")
b.AssertFileContent("public/index.html", "All.")
}
func TestLayoutAllNested(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['rss','sitemap','taxonomy','term']
-- content/s1/p1.md --
---
title: p1
---
-- content/s2/p2.md --
---
title: p2
---
-- layouts/single.html --
layouts/single.html
-- layouts/list.html --
layouts/list.html
-- layouts/s1/all.html --
layouts/s1/all.html
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html", "layouts/list.html")
b.AssertFileContent("public/s1/index.html", "layouts/s1/all.html")
b.AssertFileContent("public/s1/p1/index.html", "layouts/s1/all.html")
b.AssertFileContent("public/s2/index.html", "layouts/list.html")
b.AssertFileContent("public/s2/p2/index.html", "layouts/single.html")
}
func TestPartialHTML(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- layouts/all.html --
<html>
<head>
{{ partial "css.html" .}}
</head>
</html>
-- layouts/partials/css.html --
<link rel="stylesheet" href="/css/style.css">
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html", "<link rel=\"stylesheet\" href=\"/css/style.css\">")
}
// Issue #13515
func TestPrintPathWarningOnDotRemoval(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
printPathWarnings = true
-- content/v0.124.0.md --
-- content/v0.123.0.md --
-- layouts/all.html --
All.
-- layouts/_default/single.html --
{{ .Title }}|
`
b := hugolib.Test(t, files, hugolib.TestOptWarn())
b.AssertLogContains("Duplicate content path")
}

View File

@@ -1,48 +1,27 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tplimpl
import (
"errors"
"fmt"
"slices"
"strings"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/tpl"
"github.com/mitchellh/mapstructure"
"slices"
)
type templateType int
const (
templateUndefined templateType = iota
templateShortcode
templatePartial
)
type templateContext struct {
type templateTransformContext struct {
visited map[string]bool
templateNotFound map[string]bool
deferNodes map[string]*parse.ListNode
lookupFn func(name string) *templateState
lookupFn func(name string, in *TemplInfo) *TemplInfo
// The last error encountered.
err error
@@ -50,18 +29,18 @@ type templateContext struct {
// Set when we're done checking for config header.
configChecked bool
t *templateState
t *TemplInfo
// Store away the return node in partials.
returnNode *parse.CommandNode
}
func (c templateContext) getIfNotVisited(name string) *templateState {
func (c templateTransformContext) getIfNotVisited(name string) *TemplInfo {
if c.visited[name] {
return nil
}
c.visited[name] = true
templ := c.lookupFn(name)
templ := c.lookupFn(name, c.t)
if templ == nil {
// This may be a inline template defined outside of this file
// and not yet parsed. Unusual, but it happens.
@@ -72,11 +51,11 @@ func (c templateContext) getIfNotVisited(name string) *templateState {
return templ
}
func newTemplateContext(
t *templateState,
lookupFn func(name string) *templateState,
) *templateContext {
return &templateContext{
func newTemplateTransformContext(
t *TemplInfo,
lookupFn func(name string, in *TemplInfo) *TemplInfo,
) *templateTransformContext {
return &templateTransformContext{
t: t,
lookupFn: lookupFn,
visited: make(map[string]bool),
@@ -86,21 +65,25 @@ func newTemplateContext(
}
func applyTemplateTransformers(
t *templateState,
lookupFn func(name string) *templateState,
) (*templateContext, error) {
t *TemplInfo,
lookupFn func(name string, in *TemplInfo) *TemplInfo,
) (*templateTransformContext, error) {
if t == nil {
return nil, errors.New("expected template, but none provided")
}
c := newTemplateContext(t, lookupFn)
c := newTemplateTransformContext(t, lookupFn)
c.t.ParseInfo = defaultParseInfo
tree := getParseTree(t.Template)
if tree == nil {
panic(fmt.Errorf("template %s not parsed", t))
}
_, err := c.applyTransformations(tree.Root)
if err == nil && c.returnNode != nil {
// This is a partial with a return statement.
c.t.parseInfo.HasReturn = true
c.t.ParseInfo.HasReturn = true
tree.Root = c.wrapInPartialReturnWrapper(tree.Root)
}
@@ -108,7 +91,6 @@ func applyTemplateTransformers(
}
func getParseTree(templ tpl.Template) *parse.Tree {
templ = unwrap(templ)
if text, ok := templ.(*texttemplate.Template); ok {
return text.Tree
}
@@ -146,7 +128,7 @@ func init() {
// wrapInPartialReturnWrapper copies and modifies the parsed nodes of a
// predefined partial return wrapper to insert those of a user-defined partial.
func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
func (c *templateTransformContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
wrapper := partialReturnWrapper.CopyList()
rangeNode := wrapper.Nodes[2].(*parse.RangeNode)
retn := rangeNode.List.Nodes[0]
@@ -163,7 +145,7 @@ func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.L
// applyTransformations do 2 things:
// 1) Parses partial return statement.
// 2) Tracks template (partial) dependencies and some other info.
func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
func (c *templateTransformContext) applyTransformations(n parse.Node) (bool, error) {
switch x := n.(type) {
case *parse.ListNode:
if x != nil {
@@ -208,7 +190,7 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
return true, c.err
}
func (c *templateContext) handleDefer(withNode *parse.WithNode) {
func (c *templateTransformContext) handleDefer(withNode *parse.WithNode) {
if len(withNode.Pipe.Cmds) != 1 {
return
}
@@ -265,13 +247,13 @@ func (c *templateContext) handleDefer(withNode *parse.WithNode) {
n.Pipe.Cmds[0].Args[2] = deferArg
}
func (c *templateContext) applyTransformationsToNodes(nodes ...parse.Node) {
func (c *templateTransformContext) applyTransformationsToNodes(nodes ...parse.Node) {
for _, node := range nodes {
c.applyTransformations(node)
}
}
func (c *templateContext) hasIdent(idents []string, ident string) bool {
func (c *templateTransformContext) hasIdent(idents []string, ident string) bool {
return slices.Contains(idents, ident)
}
@@ -280,8 +262,8 @@ func (c *templateContext) hasIdent(idents []string, ident string) bool {
// on the form:
//
// {{ $_hugo_config:= `{ "version": 1 }` }}
func (c *templateContext) collectConfig(n *parse.PipeNode) {
if c.t.typ != templateShortcode {
func (c *templateTransformContext) collectConfig(n *parse.PipeNode) {
if c.t.Category != CategoryShortcode {
return
}
if c.configChecked {
@@ -313,7 +295,7 @@ func (c *templateContext) collectConfig(n *parse.PipeNode) {
c.err = fmt.Errorf(errMsg, err)
return
}
if err := mapstructure.WeakDecode(m, &c.t.parseInfo.Config); err != nil {
if err := mapstructure.WeakDecode(m, &c.t.ParseInfo.Config); err != nil {
c.err = fmt.Errorf(errMsg, err)
}
}
@@ -321,11 +303,11 @@ func (c *templateContext) collectConfig(n *parse.PipeNode) {
// collectInner determines if the given CommandNode represents a
// shortcode call to its .Inner.
func (c *templateContext) collectInner(n *parse.CommandNode) {
if c.t.typ != templateShortcode {
func (c *templateTransformContext) collectInner(n *parse.CommandNode) {
if c.t.Category != CategoryShortcode {
return
}
if c.t.parseInfo.IsInner || len(n.Args) == 0 {
if c.t.ParseInfo.IsInner || len(n.Args) == 0 {
return
}
@@ -339,14 +321,14 @@ func (c *templateContext) collectInner(n *parse.CommandNode) {
}
if c.hasIdent(idents, "Inner") || c.hasIdent(idents, "InnerDeindent") {
c.t.parseInfo.IsInner = true
c.t.ParseInfo.IsInner = true
break
}
}
}
func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
if c.t.typ != templatePartial || c.returnNode != nil {
func (c *templateTransformContext) collectReturnNode(n *parse.CommandNode) bool {
if c.t.Category != CategoryPartial || c.returnNode != nil {
return true
}
@@ -365,17 +347,3 @@ func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
return false
}
func findTemplateIn(name string, in tpl.Template) (tpl.Template, bool) {
in = unwrap(in)
if text, ok := in.(*texttemplate.Template); ok {
if templ := text.Lookup(name); templ != nil {
return templ, true
}
return nil, false
}
if templ := in.(*htmltemplate.Template).Lookup(name); templ != nil {
return templ, true
}
return nil, false
}

View File

@@ -1,66 +1,13 @@
package tplimpl_test
import (
"path/filepath"
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/tpl"
)
func TestPrintUnusedTemplates(t *testing.T) {
t.Parallel()
files := `
-- config.toml --
baseURL = 'http://example.com/'
printUnusedTemplates=true
-- content/p1.md --
---
title: "P1"
---
{{< usedshortcode >}}
-- layouts/baseof.html --
{{ block "main" . }}{{ end }}
-- layouts/baseof.json --
{{ block "main" . }}{{ end }}
-- layouts/index.html --
{{ define "main" }}FOO{{ end }}
-- layouts/_default/single.json --
-- layouts/_default/single.html --
{{ define "main" }}MAIN{{ end }}
-- layouts/post/single.html --
{{ define "main" }}MAIN{{ end }}
-- layouts/partials/usedpartial.html --
-- layouts/partials/unusedpartial.html --
-- layouts/shortcodes/usedshortcode.html --
{{ partial "usedpartial.html" }}
-- layouts/shortcodes/unusedshortcode.html --
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
NeedsOsFS: true,
},
)
b.Build()
unused := b.H.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
var names []string
for _, tmpl := range unused {
names = append(names, tmpl.Name())
}
b.Assert(names, qt.DeepEquals, []string{"_default/single.json", "baseof.json", "partials/unusedpartial.html", "post/single.html", "shortcodes/unusedshortcode.html"})
b.Assert(unused[0].Filename(), qt.Equals, filepath.Join(b.Cfg.WorkingDir, "layouts/_default/single.json"))
}
// Verify that the new keywords in Go 1.18 is available.
func TestGo18Constructs(t *testing.T) {
t.Parallel()
@@ -627,9 +574,9 @@ Home!
b := hugolib.TestRunning(t, files)
b.AssertFileContent("public/index.html", "Home!")
b.EditFileReplaceAll("layouts/_default/baseof.html", "Baseof", "Baseof!").Build()
b.EditFileReplaceAll("layouts/_default/baseof.html", "baseof", "Baseof!").Build()
b.BuildPartial("/")
b.AssertFileContent("public/index.html", "Baseof!!")
b.AssertFileContent("public/index.html", "Baseof!")
b.BuildPartial("/mybundle1/")
b.AssertFileContent("public/mybundle1/index.html", "Baseof!!")
b.AssertFileContent("public/mybundle1/index.html", "Baseof!")
}

View File

@@ -0,0 +1,96 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Portions Copyright The Go Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tplimplinit
import (
// Init the template funcs namespaces
"context"
"html/template"
"github.com/gohugoio/hugo/deps"
_ "github.com/gohugoio/hugo/tpl/cast"
_ "github.com/gohugoio/hugo/tpl/collections"
_ "github.com/gohugoio/hugo/tpl/compare"
_ "github.com/gohugoio/hugo/tpl/crypto"
_ "github.com/gohugoio/hugo/tpl/css"
_ "github.com/gohugoio/hugo/tpl/data"
_ "github.com/gohugoio/hugo/tpl/debug"
_ "github.com/gohugoio/hugo/tpl/diagrams"
_ "github.com/gohugoio/hugo/tpl/encoding"
_ "github.com/gohugoio/hugo/tpl/fmt"
_ "github.com/gohugoio/hugo/tpl/hash"
_ "github.com/gohugoio/hugo/tpl/hugo"
_ "github.com/gohugoio/hugo/tpl/images"
_ "github.com/gohugoio/hugo/tpl/inflect"
"github.com/gohugoio/hugo/tpl/internal"
_ "github.com/gohugoio/hugo/tpl/js"
_ "github.com/gohugoio/hugo/tpl/lang"
_ "github.com/gohugoio/hugo/tpl/math"
_ "github.com/gohugoio/hugo/tpl/openapi/openapi3"
_ "github.com/gohugoio/hugo/tpl/os"
_ "github.com/gohugoio/hugo/tpl/page"
_ "github.com/gohugoio/hugo/tpl/partials"
_ "github.com/gohugoio/hugo/tpl/path"
_ "github.com/gohugoio/hugo/tpl/reflect"
_ "github.com/gohugoio/hugo/tpl/resources"
_ "github.com/gohugoio/hugo/tpl/safe"
_ "github.com/gohugoio/hugo/tpl/site"
_ "github.com/gohugoio/hugo/tpl/strings"
_ "github.com/gohugoio/hugo/tpl/templates"
_ "github.com/gohugoio/hugo/tpl/time"
_ "github.com/gohugoio/hugo/tpl/transform"
_ "github.com/gohugoio/hugo/tpl/urls"
)
// CreateFuncMap creates a template.FuncMap with all of Hugo's template funcs,
// excluding the Go built-ins.
func CreateFuncMap(d *deps.Deps) map[string]any {
funcMap := template.FuncMap{}
nsMap := make(map[string]any)
var onCreated []func(namespaces map[string]any)
// Merge the namespace funcs
for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
ns := nsf(d)
if _, exists := funcMap[ns.Name]; exists {
panic(ns.Name + " is a duplicate template func")
}
funcMap[ns.Name] = ns.Context
contextV, err := ns.Context(context.Background())
if err != nil {
panic(err)
}
nsMap[ns.Name] = contextV
for _, mm := range ns.MethodMappings {
for _, alias := range mm.Aliases {
if _, exists := funcMap[alias]; exists {
panic(alias + " is a duplicate template func")
}
funcMap[alias] = mm.Method
}
}
if ns.OnCreated != nil {
onCreated = append(onCreated, ns.OnCreated)
}
}
for _, f := range onCreated {
f(nsMap)
}
return funcMap
}