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