Reimplement and simplify Hugo's template system

See #13541 for details.

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,46 @@
// 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
// Increments on breaking changes.
const TemplateVersion = 2
// ParseInfo holds information about a parsed ntemplate.
type ParseInfo struct {
// Set for shortcode templates with any {{ .Inner }}
IsInner bool
// Set for partials with a return statement.
HasReturn bool
// Config extracted from template.
Config ParseConfig
}
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{
Version: TemplateVersion,
}
var defaultParseInfo = ParseInfo{
Config: defaultParseConfig,
}

View File

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

View File

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

View File

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

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

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

1854
tpl/tplimpl/templatestore.go Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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