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
No known key found for this signature in database
138 changed files with 5342 additions and 4396 deletions

View file

@ -23,6 +23,7 @@ import (
"errors"
"fmt"
"io"
"maps"
"net"
"net/http"
_ "net/http/pprof"
@ -48,6 +49,7 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls"
@ -57,7 +59,6 @@ import (
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/livereload"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/transform"
"github.com/gohugoio/hugo/transform/livereloadinject"
"github.com/spf13/afero"
@ -65,7 +66,6 @@ import (
"github.com/spf13/fsync"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
"maps"
)
var (
@ -897,16 +897,16 @@ func (c *serverCommand) serve() error {
// To allow the en user to change the error template while the server is running, we use
// the freshest template we can provide.
var (
errTempl tpl.Template
templHandler tpl.TemplateHandler
errTempl *tplimpl.TemplInfo
templHandler *tplimpl.TemplateStore
)
getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (tpl.Template, tpl.TemplateHandler) {
getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (*tplimpl.TemplInfo, *tplimpl.TemplateStore) {
if h == nil {
return errTempl, templHandler
}
templHandler := h.Tmpl()
errTempl, found := templHandler.Lookup("_server/error.html")
if !found {
templHandler := h.GetTemplateStore()
errTempl := templHandler.LookupByPath("/_server/error.html")
if errTempl == nil {
panic("template server/error.html not found")
}
return errTempl, templHandler

View file

@ -23,6 +23,7 @@ const (
WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html"
WarnGoldmarkRawHTML = "warning-goldmark-raw-html"
WarnPartialSuperfluousPrefix = "warning-partial-superfluous-prefix"
)
// Field/method names with special meaning.

View file

@ -16,11 +16,11 @@ package hstrings
import (
"fmt"
"regexp"
"slices"
"strings"
"sync"
"github.com/gohugoio/hugo/compare"
"slices"
)
var _ compare.Eqer = StringEqualFold("")
@ -128,7 +128,7 @@ func ToString(v any) (string, bool) {
return "", false
}
type Tuple struct {
First string
Second string
}
type (
Strings2 [2]string
Strings3 [3]string
)

View file

@ -69,6 +69,14 @@ func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
return v, nil
}
// Contains returns whether the given key exists in the cache.
func (c *Cache[K, T]) Contains(key K) bool {
c.RLock()
_, found := c.m[key]
c.RUnlock()
return found
}
// InitAndGet initializes the cache if not already done and returns the value for the given key.
// The init state will be reset on Reset or Drain.
func (c *Cache[K, T]) InitAndGet(key K, init func(get func(key K) (T, bool), set func(key K, value T)) error) (T, error) {
@ -108,6 +116,17 @@ func (c *Cache[K, T]) Set(key K, value T) {
c.Unlock()
}
// SetIfAbsent sets the given key to the given value if the key does not already exist in the cache.
func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
c.RLock()
if _, found := c.get(key); !found {
c.RUnlock()
c.Set(key, value)
} else {
c.RUnlock()
}
}
func (c *Cache[K, T]) set(key K, value T) {
c.m[key] = value
}

View file

@ -14,8 +14,9 @@
package maps
import (
"github.com/gohugoio/hugo/common/hashing"
"slices"
"github.com/gohugoio/hugo/common/hashing"
)
// Ordered is a map that can be iterated in the order of insertion.
@ -57,6 +58,15 @@ func (m *Ordered[K, T]) Get(key K) (T, bool) {
return value, found
}
// Has returns whether the given key exists in the map.
func (m *Ordered[K, T]) Has(key K) bool {
if m == nil {
return false
}
_, found := m.values[key]
return found
}
// Delete deletes the value for the given key.
func (m *Ordered[K, T]) Delete(key K) {
if m == nil {

View file

@ -23,6 +23,11 @@ import (
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/kinds"
)
const (
identifierBaseof = "baseof"
)
// PathParser parses a path into a Path.
@ -33,6 +38,10 @@ type PathParser struct {
// Reports whether the given language is disabled.
IsLangDisabled func(string) bool
// IsOutputFormat reports whether the given name is a valid output format.
// The second argument is optional.
IsOutputFormat func(name, ext string) bool
// Reports whether the given ext is a content file.
IsContentExt func(string) bool
}
@ -83,13 +92,10 @@ func (pp *PathParser) Parse(c, s string) *Path {
}
func (pp *PathParser) newPath(component string) *Path {
return &Path{
component: component,
posContainerLow: -1,
posContainerHigh: -1,
posSectionHigh: -1,
posIdentifierLanguage: -1,
}
p := &Path{}
p.reset()
p.component = component
return p
}
func (pp *PathParser) parse(component, s string) (*Path, error) {
@ -114,10 +120,91 @@ func (pp *PathParser) parse(component, s string) (*Path, error) {
return p, nil
}
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
hasLang := pp.LanguageIndex != nil
hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot int) {
if p.posContainerHigh != -1 {
return
}
mayHaveLang := pp.LanguageIndex != nil
mayHaveLang = mayHaveLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
mayHaveOutputFormat := component == files.ComponentFolderLayouts
mayHaveKind := mayHaveOutputFormat
var found bool
var high int
if len(p.identifiers) > 0 {
high = lastDot
} else {
high = len(p.s)
}
id := types.LowHigh[string]{Low: i + 1, High: high}
sid := p.s[id.Low:id.High]
if len(p.identifiers) == 0 {
// The first is always the extension.
p.identifiers = append(p.identifiers, id)
found = true
// May also be the output format.
if mayHaveOutputFormat && pp.IsOutputFormat(sid, "") {
p.posIdentifierOutputFormat = 0
}
} else {
var langFound bool
if mayHaveLang {
var disabled bool
_, langFound = pp.LanguageIndex[sid]
if !langFound {
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(sid)
if disabled {
p.disabled = true
langFound = true
}
}
found = langFound
if langFound {
p.identifiers = append(p.identifiers, id)
p.posIdentifierLanguage = len(p.identifiers) - 1
}
}
if !found && mayHaveOutputFormat {
// At this point we may already have resolved an output format,
// but we need to keep looking for a more specific one, e.g. amp before html.
// Use both name and extension to prevent
// false positives on the form css.html.
if pp.IsOutputFormat(sid, p.Ext()) {
found = true
p.identifiers = append(p.identifiers, id)
p.posIdentifierOutputFormat = len(p.identifiers) - 1
}
}
if !found && mayHaveKind {
if kinds.GetKindMain(sid) != "" {
found = true
p.identifiers = append(p.identifiers, id)
p.posIdentifierKind = len(p.identifiers) - 1
}
}
if !found && sid == identifierBaseof {
found = true
p.identifiers = append(p.identifiers, id)
p.posIdentifierBaseof = len(p.identifiers) - 1
}
if !found {
p.identifiers = append(p.identifiers, id)
p.identifiersUnknown = append(p.identifiersUnknown, len(p.identifiers)-1)
}
}
}
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
if runtime.GOOS == "windows" {
s = path.Clean(filepath.ToSlash(s))
if s == "." {
@ -140,46 +227,21 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
p.s = s
slashCount := 0
lastDot := 0
for i := len(s) - 1; i >= 0; i-- {
c := s[i]
switch c {
case '.':
if p.posContainerHigh == -1 {
var high int
if len(p.identifiers) > 0 {
high = p.identifiers[len(p.identifiers)-1].Low - 1
} else {
high = len(p.s)
}
id := types.LowHigh[string]{Low: i + 1, High: high}
if len(p.identifiers) == 0 {
p.identifiers = append(p.identifiers, id)
} else if len(p.identifiers) == 1 {
// Check for a valid language.
s := p.s[id.Low:id.High]
if hasLang {
var disabled bool
_, langFound := pp.LanguageIndex[s]
if !langFound {
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(s)
if disabled {
p.disabled = true
langFound = true
}
}
if langFound {
p.posIdentifierLanguage = 1
p.identifiers = append(p.identifiers, id)
}
}
}
}
pp.parseIdentifier(component, s, p, i, lastDot)
lastDot = i
case '/':
slashCount++
if p.posContainerHigh == -1 {
if lastDot > 0 {
pp.parseIdentifier(component, s, p, i, lastDot)
}
p.posContainerHigh = i + 1
} else if p.posContainerLow == -1 {
p.posContainerLow = i + 1
@ -194,22 +256,41 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
isContent := isContentComponent && pp.IsContentExt(p.Ext())
id := p.identifiers[len(p.identifiers)-1]
b := p.s[p.posContainerHigh : id.Low-1]
if isContent {
switch b {
case "index":
p.bundleType = PathTypeLeaf
case "_index":
p.bundleType = PathTypeBranch
default:
p.bundleType = PathTypeContentSingle
}
if slashCount == 2 && p.IsLeafBundle() {
p.posSectionHigh = 0
if id.High > p.posContainerHigh {
b := p.s[p.posContainerHigh:id.High]
if isContent {
switch b {
case "index":
p.pathType = TypeLeaf
case "_index":
p.pathType = TypeBranch
default:
p.pathType = TypeContentSingle
}
if slashCount == 2 && p.IsLeafBundle() {
p.posSectionHigh = 0
}
} else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
p.pathType = TypeContentData
}
}
}
if component == files.ComponentFolderLayouts {
if p.posIdentifierBaseof != -1 {
p.pathType = TypeBaseof
} else {
pth := p.Path()
if strings.Contains(pth, "/_shortcodes/") {
p.pathType = TypeShortcode
} else if strings.Contains(pth, "/_markup/") {
p.pathType = TypeMarkup
} else if strings.HasPrefix(pth, "/_partials/") {
p.pathType = TypePartial
}
} else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
p.bundleType = PathTypeContentData
}
}
@ -218,35 +299,44 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
func ModifyPathBundleTypeResource(p *Path) {
if p.IsContent() {
p.bundleType = PathTypeContentResource
p.pathType = TypeContentResource
} else {
p.bundleType = PathTypeFile
p.pathType = TypeFile
}
}
type PathType int
//go:generate stringer -type Type
type Type int
const (
// A generic resource, e.g. a JSON file.
PathTypeFile PathType = iota
TypeFile Type = iota
// All below are content files.
// A resource of a content type with front matter.
PathTypeContentResource
TypeContentResource
// E.g. /blog/my-post.md
PathTypeContentSingle
TypeContentSingle
// All below are bundled content files.
// Leaf bundles, e.g. /blog/my-post/index.md
PathTypeLeaf
TypeLeaf
// Branch bundles, e.g. /blog/_index.md
PathTypeBranch
TypeBranch
// Content data file, _content.gotmpl.
PathTypeContentData
TypeContentData
// Layout types.
TypeMarkup
TypeShortcode
TypePartial
TypeBaseof
)
type Path struct {
@ -257,13 +347,17 @@ type Path struct {
posContainerHigh int
posSectionHigh int
component string
bundleType PathType
component string
pathType Type
identifiers []types.LowHigh[string]
posIdentifierLanguage int
disabled bool
posIdentifierLanguage int
posIdentifierOutputFormat int
posIdentifierKind int
posIdentifierBaseof int
identifiersUnknown []int
disabled bool
trimLeadingSlash bool
@ -293,9 +387,12 @@ func (p *Path) reset() {
p.posContainerHigh = -1
p.posSectionHigh = -1
p.component = ""
p.bundleType = 0
p.pathType = 0
p.identifiers = p.identifiers[:0]
p.posIdentifierLanguage = -1
p.posIdentifierOutputFormat = -1
p.posIdentifierKind = -1
p.posIdentifierBaseof = -1
p.disabled = false
p.trimLeadingSlash = false
p.unnormalized = nil
@ -316,6 +413,9 @@ func (p *Path) norm(s string) string {
// IdentifierBase satisfies identity.Identity.
func (p *Path) IdentifierBase() string {
if p.Component() == files.ComponentFolderLayouts {
return p.Path()
}
return p.Base()
}
@ -332,6 +432,13 @@ func (p *Path) Container() string {
return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1])
}
func (p *Path) String() string {
if p == nil {
return "<nil>"
}
return p.Path()
}
// ContainerDir returns the container directory for this path.
// For content bundles this will be the parent directory.
func (p *Path) ContainerDir() string {
@ -352,13 +459,13 @@ func (p *Path) Section() string {
// IsContent returns true if the path is a content file (e.g. mypost.md).
// Note that this will also return true for content files in a bundle.
func (p *Path) IsContent() bool {
return p.BundleType() >= PathTypeContentResource
return p.Type() >= TypeContentResource && p.Type() <= TypeContentData
}
// isContentPage returns true if the path is a content file (e.g. mypost.md),
// but nof if inside a leaf bundle.
func (p *Path) isContentPage() bool {
return p.BundleType() >= PathTypeContentSingle
return p.Type() >= TypeContentSingle && p.Type() <= TypeContentData
}
// Name returns the last element of path.
@ -398,10 +505,26 @@ func (p *Path) BaseNameNoIdentifier() string {
// NameNoIdentifier returns the last element of path without any identifier (e.g. no extension).
func (p *Path) NameNoIdentifier() string {
lowHigh := p.nameLowHigh()
return p.s[lowHigh.Low:lowHigh.High]
}
func (p *Path) nameLowHigh() types.LowHigh[string] {
if len(p.identifiers) > 0 {
return p.s[p.posContainerHigh : p.identifiers[len(p.identifiers)-1].Low-1]
lastID := p.identifiers[len(p.identifiers)-1]
if p.posContainerHigh == lastID.Low {
// The last identifier is the name.
return lastID
}
return types.LowHigh[string]{
Low: p.posContainerHigh,
High: p.identifiers[len(p.identifiers)-1].Low - 1,
}
}
return types.LowHigh[string]{
Low: p.posContainerHigh,
High: len(p.s),
}
return p.s[p.posContainerHigh:]
}
// Dir returns all but the last element of path, typically the path's directory.
@ -421,6 +544,11 @@ func (p *Path) Path() (d string) {
return p.norm(p.s)
}
// PathNoLeadingSlash returns the full path without the leading slash.
func (p *Path) PathNoLeadingSlash() string {
return p.Path()[1:]
}
// Unnormalized returns the Path with the original case preserved.
func (p *Path) Unnormalized() *Path {
return p.unnormalized
@ -436,6 +564,28 @@ func (p *Path) PathNoIdentifier() string {
return p.base(false, false)
}
// PathBeforeLangAndOutputFormatAndExt returns the path up to the first identifier that is not a language or output format.
func (p *Path) PathBeforeLangAndOutputFormatAndExt() string {
if len(p.identifiers) == 0 {
return p.norm(p.s)
}
i := p.identifierIndex(0)
if j := p.posIdentifierOutputFormat; i == -1 || (j != -1 && j < i) {
i = j
}
if j := p.posIdentifierLanguage; i == -1 || (j != -1 && j < i) {
i = j
}
if i == -1 {
return p.norm(p.s)
}
id := p.identifiers[i]
return p.norm(p.s[:id.Low-1])
}
// PathRel returns the path relative to the given owner.
func (p *Path) PathRel(owner *Path) string {
ob := owner.Base()
@ -462,6 +612,21 @@ func (p *Path) Base() string {
return p.base(!p.isContentPage(), p.IsBundle())
}
// Used in template lookups.
// For pages with Type set, we treat that as the section.
func (p *Path) BaseReTyped(typ string) (d string) {
base := p.Base()
if typ == "" || p.Section() == typ {
return base
}
d = "/" + typ
if p.posSectionHigh != -1 {
d += base[p.posSectionHigh:]
}
d = p.norm(d)
return
}
// BaseNoLeadingSlash returns the base path without the leading slash.
func (p *Path) BaseNoLeadingSlash() string {
return p.Base()[1:]
@ -477,11 +642,12 @@ func (p *Path) base(preserveExt, isBundle bool) string {
return p.norm(p.s)
}
id := p.identifiers[len(p.identifiers)-1]
high := id.Low - 1
var high int
if isBundle {
high = p.posContainerHigh - 1
} else {
high = p.nameLowHigh().High
}
if high == 0 {
@ -493,7 +659,7 @@ func (p *Path) base(preserveExt, isBundle bool) string {
}
// For txt files etc. we want to preserve the extension.
id = p.identifiers[0]
id := p.identifiers[0]
return p.norm(p.s[:high] + p.s[id.Low-1:id.High])
}
@ -502,8 +668,16 @@ func (p *Path) Ext() string {
return p.identifierAsString(0)
}
func (p *Path) OutputFormat() string {
return p.identifierAsString(p.posIdentifierOutputFormat)
}
func (p *Path) Kind() string {
return p.identifierAsString(p.posIdentifierKind)
}
func (p *Path) Lang() string {
return p.identifierAsString(1)
return p.identifierAsString(p.posIdentifierLanguage)
}
func (p *Path) Identifier(i int) string {
@ -522,28 +696,36 @@ func (p *Path) Identifiers() []string {
return ids
}
func (p *Path) BundleType() PathType {
return p.bundleType
func (p *Path) IdentifiersUnknown() []string {
ids := make([]string, len(p.identifiersUnknown))
for i, id := range p.identifiersUnknown {
ids[i] = p.s[p.identifiers[id].Low:p.identifiers[id].High]
}
return ids
}
func (p *Path) Type() Type {
return p.pathType
}
func (p *Path) IsBundle() bool {
return p.bundleType >= PathTypeLeaf
return p.pathType >= TypeLeaf && p.pathType <= TypeContentData
}
func (p *Path) IsBranchBundle() bool {
return p.bundleType == PathTypeBranch
return p.pathType == TypeBranch
}
func (p *Path) IsLeafBundle() bool {
return p.bundleType == PathTypeLeaf
return p.pathType == TypeLeaf
}
func (p *Path) IsContentData() bool {
return p.bundleType == PathTypeContentData
return p.pathType == TypeContentData
}
func (p Path) ForBundleType(t PathType) *Path {
p.bundleType = t
func (p Path) ForBundleType(t Type) *Path {
p.pathType = t
return &p
}

View file

@ -18,6 +18,7 @@ import (
"testing"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/resources/kinds"
qt "github.com/frankban/quicktest"
)
@ -26,10 +27,18 @@ var testParser = &PathParser{
LanguageIndex: map[string]int{
"no": 0,
"en": 1,
"fr": 2,
},
IsContentExt: func(ext string) bool {
return ext == "md"
},
IsOutputFormat: func(name, ext string) bool {
switch name {
case "html", "amp", "csv", "rss":
return true
}
return false
},
}
func TestParse(t *testing.T) {
@ -105,17 +114,19 @@ func TestParse(t *testing.T) {
"Basic Markdown file",
"/a/b/c.md",
func(c *qt.C, p *Path) {
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Type(), qt.Equals, TypeContentSingle)
c.Assert(p.IsContent(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsFalse)
c.Assert(p.Name(), qt.Equals, "c.md")
c.Assert(p.Base(), qt.Equals, "/a/b/c")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b/c")
c.Assert(p.Section(), qt.Equals, "a")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c")
c.Assert(p.Path(), qt.Equals, "/a/b/c.md")
c.Assert(p.Dir(), qt.Equals, "/a/b")
c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a/b")
c.Assert(p.Ext(), qt.Equals, "md")
},
},
{
@ -130,7 +141,7 @@ func TestParse(t *testing.T) {
// Reclassify it as a content resource.
ModifyPathBundleTypeResource(p)
c.Assert(p.BundleType(), qt.Equals, PathTypeContentResource)
c.Assert(p.Type(), qt.Equals, TypeContentResource)
c.Assert(p.IsContent(), qt.IsTrue)
c.Assert(p.Name(), qt.Equals, "b.md")
c.Assert(p.Base(), qt.Equals, "/a/b.md")
@ -160,15 +171,16 @@ func TestParse(t *testing.T) {
"/a/b.a.b.no.txt",
func(c *qt.C, p *Path) {
c.Assert(p.Name(), qt.Equals, "b.a.b.no.txt")
c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b")
c.Assert(p.NameNoIdentifier(), qt.Equals, "b")
c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt")
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no", "b", "a", "b"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"b", "a", "b"})
c.Assert(p.Base(), qt.Equals, "/a/b.txt")
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.txt")
c.Assert(p.Path(), qt.Equals, "/a/b.a.b.no.txt")
c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt")
c.Assert(p.PathNoLang(), qt.Equals, "/a/b.txt")
c.Assert(p.Ext(), qt.Equals, "txt")
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b")
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b")
},
},
{
@ -176,6 +188,7 @@ func TestParse(t *testing.T) {
"/_index.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo")
c.Assert(p.Path(), qt.Equals, "/_index.md")
c.Assert(p.Container(), qt.Equals, "")
c.Assert(p.ContainerDir(), qt.Equals, "/")
@ -186,13 +199,14 @@ func TestParse(t *testing.T) {
"/a/index.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/a")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "a")
c.Assert(p.Container(), qt.Equals, "a")
c.Assert(p.Container(), qt.Equals, "a")
c.Assert(p.ContainerDir(), qt.Equals, "")
c.Assert(p.Dir(), qt.Equals, "/a")
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "index"})
c.Assert(p.IsBranchBundle(), qt.IsFalse)
c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsTrue)
@ -209,11 +223,12 @@ func TestParse(t *testing.T) {
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b")
c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a")
c.Assert(p.Dir(), qt.Equals, "/a/b")
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no", "index"})
c.Assert(p.IsBranchBundle(), qt.IsFalse)
c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsTrue)
@ -235,7 +250,7 @@ func TestParse(t *testing.T) {
c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a")
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no", "_index"})
c.Assert(p.IsBranchBundle(), qt.IsTrue)
c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsFalse)
@ -274,7 +289,7 @@ func TestParse(t *testing.T) {
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/index.txt")
c.Assert(p.Ext(), qt.Equals, "txt")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no", "index"})
c.Assert(p.IsLeafBundle(), qt.IsFalse)
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b/index")
},
@ -357,11 +372,140 @@ func TestParse(t *testing.T) {
}
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
if test.name != "Basic Markdown file" {
// return
}
test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
})
}
}
func TestParseLayouts(t *testing.T) {
c := qt.New(t)
tests := []struct {
name string
path string
assert func(c *qt.C, p *Path)
}{
{
"Basic",
"/list.html",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/list.html")
c.Assert(p.OutputFormat(), qt.Equals, "html")
},
},
{
"Lang",
"/list.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "list"})
c.Assert(p.Base(), qt.Equals, "/list.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
},
{
"Lang and output format",
"/list.no.amp.not.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "not", "amp", "no", "list"})
c.Assert(p.OutputFormat(), qt.Equals, "amp")
c.Assert(p.Ext(), qt.Equals, "html")
c.Assert(p.Lang(), qt.Equals, "no")
c.Assert(p.Base(), qt.Equals, "/list.html")
},
},
{
"Term",
"/term.html",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/term.html")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "term"})
c.Assert(p.PathNoIdentifier(), qt.Equals, "/term")
c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/term")
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.Kind(), qt.Equals, "term")
c.Assert(p.OutputFormat(), qt.Equals, "html")
},
},
{
"Sub dir",
"/pages/home.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "home"})
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.Kind(), qt.Equals, "home")
c.Assert(p.OutputFormat(), qt.Equals, "html")
c.Assert(p.Dir(), qt.Equals, "/pages")
},
},
{
"Baseof",
"/pages/baseof.list.section.fr.amp.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "amp", "fr", "section", "list", "baseof"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"list"})
c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
c.Assert(p.Lang(), qt.Equals, "fr")
c.Assert(p.OutputFormat(), qt.Equals, "amp")
c.Assert(p.Dir(), qt.Equals, "/pages")
c.Assert(p.NameNoIdentifier(), qt.Equals, "baseof")
c.Assert(p.Type(), qt.Equals, TypeBaseof)
c.Assert(p.IdentifierBase(), qt.Equals, "/pages/baseof.list.section.fr.amp.html")
},
},
{
"Markup",
"/_markup/render-link.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeMarkup)
},
},
{
"Markup nested",
"/foo/_markup/render-link.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeMarkup)
},
},
{
"Shortcode",
"/_shortcodes/myshortcode.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
},
},
{
"Shortcode nested",
"/foo/_shortcodes/myshortcode.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
},
},
{
"Shortcode nested sub",
"/foo/_shortcodes/foo/myshortcode.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
},
},
{
"Partials",
"/_partials/foo.bar",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypePartial)
},
},
}
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
test.assert(c, testParser.Parse(files.ComponentFolderLayouts, test.path))
})
}
}
func TestHasExt(t *testing.T) {
c := qt.New(t)

View file

@ -1,27 +0,0 @@
// Code generated by "stringer -type=PathType"; DO NOT EDIT.
package paths
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[PathTypeFile-0]
_ = x[PathTypeContentResource-1]
_ = x[PathTypeContentSingle-2]
_ = x[PathTypeLeaf-3]
_ = x[PathTypeBranch-4]
}
const _PathType_name = "PathTypeFilePathTypeContentResourcePathTypeContentSinglePathTypeLeafPathTypeBranch"
var _PathType_index = [...]uint8{0, 12, 35, 56, 68, 82}
func (i PathType) String() string {
if i < 0 || i >= PathType(len(_PathType_index)-1) {
return "PathType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _PathType_name[_PathType_index[i]:_PathType_index[i+1]]
}

View file

@ -0,0 +1,32 @@
// Code generated by "stringer -type Type"; DO NOT EDIT.
package paths
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[TypeFile-0]
_ = x[TypeContentResource-1]
_ = x[TypeContentSingle-2]
_ = x[TypeLeaf-3]
_ = x[TypeBranch-4]
_ = x[TypeContentData-5]
_ = x[TypeMarkup-6]
_ = x[TypeShortcode-7]
_ = x[TypePartial-8]
_ = x[TypeBaseof-9]
}
const _Type_name = "TypeFileTypeContentResourceTypeContentSingleTypeLeafTypeBranchTypeContentDataTypeMarkupTypeShortcodeTypePartialTypeBaseof"
var _Type_index = [...]uint8{0, 8, 27, 44, 52, 62, 77, 87, 100, 111, 121}
func (i Type) String() string {
if i < 0 || i >= Type(len(_Type_index)-1) {
return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Type_name[_Type_index[i]:_Type_index[i+1]]
}

View file

@ -28,6 +28,16 @@ type RLocker interface {
RUnlock()
}
type Locker interface {
Lock()
Unlock()
}
type RWLocker interface {
RLocker
Locker
}
// KeyValue is a interface{} tuple.
type KeyValue struct {
Key any

View file

@ -849,7 +849,24 @@ func (c *Configs) Init() error {
c.Languages = languages
c.LanguagesDefaultFirst = languagesDefaultFirst
c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix}
c.ContentPathParser = &paths.PathParser{
LanguageIndex: languagesDefaultFirst.AsIndexSet(),
IsLangDisabled: c.Base.IsLangDisabled,
IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix,
IsOutputFormat: func(name, ext string) bool {
if name == "" {
return false
}
if of, ok := c.Base.OutputFormats.Config.GetByName(name); ok {
if ext != "" && !of.MediaType.HasSuffix(ext) {
return false
}
return true
}
return false
},
}
c.configLangs = make([]config.AllProvider, len(c.Languages))
for i, l := range c.LanguagesDefaultFirst {

View file

@ -291,7 +291,7 @@ func (b *contentBuilder) applyArcheType(contentFilename string, archetypeFi hugo
func (b *contentBuilder) mapArcheTypeDir() error {
var m archetypeMap
seen := map[hstrings.Tuple]bool{}
seen := map[hstrings.Strings2]bool{}
walkFn := func(path string, fim hugofs.FileMetaInfo) error {
if fim.IsDir() {
@ -301,7 +301,7 @@ func (b *contentBuilder) mapArcheTypeDir() error {
pi := fim.Meta().PathInfo
if pi.IsContent() {
pathLang := hstrings.Tuple{First: pi.PathNoIdentifier(), Second: fim.Meta().Lang}
pathLang := hstrings.Strings2{pi.PathBeforeLangAndOutputFormatAndExt(), fim.Meta().Lang}
if seen[pathLang] {
// Duplicate content file, e.g. page.md and page.html.
// In the regular build, we will filter out the duplicates, but

47
deps/deps.go vendored
View file

@ -29,6 +29,7 @@ import (
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/postpub"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/metrics"
"github.com/gohugoio/hugo/resources"
@ -46,12 +47,6 @@ type Deps struct {
ExecHelper *hexec.Exec
// The templates to use. This will usually implement the full tpl.TemplateManager.
tmplHandlers *tpl.TemplateHandlers
// The template funcs.
TmplFuncMap map[string]any
// The file systems to use.
Fs *hugofs.Fs `json:"-"`
@ -79,7 +74,8 @@ type Deps struct {
// The site building.
Site page.Site
TemplateProvider ResourceProvider
TemplateStore *tplimpl.TemplateStore
// Used in tests
OverloadedTemplateFuncs map[string]any
@ -102,6 +98,9 @@ type Deps struct {
// This is common/global for all sites.
BuildState *BuildState
// Misc counters.
Counters *Counters
// Holds RPC dispatchers for Katex etc.
// TODO(bep) rethink this re. a plugin setup, but this will have to do for now.
WasmDispatchers *warpc.Dispatchers
@ -109,9 +108,6 @@ type Deps struct {
// The JS batcher client.
JSBatcherClient js.BatcherClient
// The JS batcher client.
// JSBatcherClient *esbuild.BatcherClient
isClosed bool
*globalErrHandler
@ -130,8 +126,8 @@ func (d Deps) Clone(s page.Site, conf config.AllProvider) (*Deps, error) {
return &d, nil
}
func (d *Deps) SetTempl(t *tpl.TemplateHandlers) {
d.tmplHandlers = t
func (d *Deps) GetTemplateStore() *tplimpl.TemplateStore {
return d.TemplateStore
}
func (d *Deps) Init() error {
@ -153,10 +149,12 @@ func (d *Deps) Init() error {
logger: d.Log,
}
}
if d.BuildState == nil {
d.BuildState = &BuildState{}
}
if d.Counters == nil {
d.Counters = &Counters{}
}
if d.BuildState.DeferredExecutions == nil {
if d.BuildState.DeferredExecutionsGroupedByRenderingContext == nil {
d.BuildState.DeferredExecutionsGroupedByRenderingContext = make(map[tpl.RenderingContext]*DeferredExecutions)
@ -263,22 +261,17 @@ func (d *Deps) Init() error {
return nil
}
// TODO(bep) rework this to get it in line with how we manage templates.
func (d *Deps) Compile(prototype *Deps) error {
var err error
if prototype == nil {
if err = d.TemplateProvider.NewResource(d); err != nil {
return err
}
if err = d.TranslationProvider.NewResource(d); err != nil {
return err
}
return nil
}
if err = d.TemplateProvider.CloneResource(d, prototype); err != nil {
return err
}
if err = d.TranslationProvider.CloneResource(d, prototype); err != nil {
return err
}
@ -378,14 +371,6 @@ type ResourceProvider interface {
CloneResource(dst, src *Deps) error
}
func (d *Deps) Tmpl() tpl.TemplateHandler {
return d.tmplHandlers.Tmpl
}
func (d *Deps) TextTmpl() tpl.TemplateParseFinder {
return d.tmplHandlers.TxtTmpl
}
func (d *Deps) Close() error {
if d.isClosed {
return nil
@ -454,6 +439,12 @@ type BuildState struct {
DeferredExecutionsGroupedByRenderingContext map[tpl.RenderingContext]*DeferredExecutions
}
// Misc counters.
type Counters struct {
// Counter for the math.Counter function.
MathCounter atomic.Uint64
}
type DeferredExecutions struct {
// A set of filenames in /public that
// contains a post-processing prefix.

View file

@ -29,16 +29,17 @@ import (
"github.com/gohugoio/hugo/publisher"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/tpl/tplimpl"
)
type aliasHandler struct {
t tpl.TemplateHandler
ts *tplimpl.TemplateStore
log loggers.Logger
allowRoot bool
}
func newAliasHandler(t tpl.TemplateHandler, l loggers.Logger, allowRoot bool) aliasHandler {
return aliasHandler{t, l, allowRoot}
func newAliasHandler(ts *tplimpl.TemplateStore, l loggers.Logger, allowRoot bool) aliasHandler {
return aliasHandler{ts, l, allowRoot}
}
type aliasPage struct {
@ -47,16 +48,24 @@ type aliasPage struct {
}
func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, error) {
var templ tpl.Template
var found bool
var templateDesc tplimpl.TemplateDescriptor
var base string = ""
if ps, ok := p.(*pageState); ok {
base, templateDesc = ps.getTemplateBasePathAndDescriptor()
}
templateDesc.Layout = ""
templateDesc.Kind = ""
templateDesc.OutputFormat = output.AliasHTMLFormat.Name
templ, found = a.t.Lookup("alias.html")
if !found {
// TODO(bep) consolidate
templ, found = a.t.Lookup("_internal/alias.html")
if !found {
return nil, errors.New("no alias template found")
}
q := tplimpl.TemplateQuery{
Path: base,
Category: tplimpl.CategoryLayout,
Desc: templateDesc,
}
t := a.ts.LookupPagesLayout(q)
if t == nil {
return nil, errors.New("no alias template found")
}
data := aliasPage{
@ -67,7 +76,7 @@ func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, err
ctx := tpl.Context.Page.Set(context.Background(), p)
buffer := new(bytes.Buffer)
err := a.t.ExecuteWithContext(ctx, templ, buffer, data)
err := a.ts.ExecuteWithContext(ctx, t, buffer, data)
if err != nil {
return nil, err
}
@ -79,7 +88,7 @@ func (s *Site) writeDestAlias(path, permalink string, outputFormat output.Format
}
func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) {
handler := newAliasHandler(s.Tmpl(), s.Log, allowRoot)
handler := newAliasHandler(s.GetTemplateStore(), s.Log, allowRoot)
targetPath, err := handler.targetPathAlias(path)
if err != nil {

View file

@ -107,13 +107,26 @@ func TestAliasMultipleOutputFormats(t *testing.T) {
func TestAliasTemplate(t *testing.T) {
t.Parallel()
b := newTestSitesBuilder(t)
b.WithSimpleConfigFile().WithContent("page.md", pageWithAlias).WithTemplatesAdded("alias.html", aliasTemplate)
files := `
-- hugo.toml --
baseURL = "http://example.com"
-- layouts/single.html --
Single.
-- layouts/home.html --
Home.
-- layouts/alias.html --
ALIASTEMPLATE
-- content/page.md --
---
title: "Page"
aliases: ["/foo/bar/"]
---
`
b.CreateSites().Build(BuildCfg{})
b := Test(t, files)
// the real page
b.AssertFileContent("public/page/index.html", "For some moments the old man")
b.AssertFileContent("public/page/index.html", "Single.")
// the alias redirector
b.AssertFileContent("public/foo/bar/index.html", "ALIASTEMPLATE")
}

View file

@ -72,12 +72,12 @@ func (f ContentFactory) ApplyArchetypeTemplate(w io.Writer, p page.Page, archety
templateSource = f.shortcodeReplacerPre.Replace(templateSource)
templ, err := ps.s.TextTmpl().Parse("archetype.md", string(templateSource))
templ, err := ps.s.TemplateStore.TextParse("archetype.md", templateSource)
if err != nil {
return fmt.Errorf("failed to parse archetype template: %s: %w", err, err)
}
result, err := executeToString(context.Background(), ps.s.Tmpl(), templ, d)
result, err := executeToString(context.Background(), ps.s.GetTemplateStore(), templ, d)
if err != nil {
return fmt.Errorf("failed to execute archetype template: %s: %w", err, err)
}

View file

@ -264,8 +264,8 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCoun
meta := fi.Meta()
pi := meta.PathInfo
switch pi.BundleType() {
case paths.PathTypeFile, paths.PathTypeContentResource:
switch pi.Type() {
case paths.TypeFile, paths.TypeContentResource:
m.s.Log.Trace(logg.StringFunc(
func() string {
return fmt.Sprintf("insert resource: %q", fi.Meta().Filename)
@ -275,7 +275,7 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCoun
addErr = err
return
}
case paths.PathTypeContentData:
case paths.TypeContentData:
pc, rc, err := m.addPagesFromGoTmplFi(fi, buildConfig)
pageCount += pc
resourceCount += rc
@ -349,8 +349,7 @@ func (m *pageMap) addPagesFromGoTmplFi(fi hugofs.FileMetaInfo, buildConfig *Buil
DepsFromSite: func(s page.Site) pagesfromdata.PagesFromTemplateDeps {
ss := s.(*Site)
return pagesfromdata.PagesFromTemplateDeps{
TmplFinder: ss.TextTmpl(),
TmplExec: ss.Tmpl(),
TemplateStore: ss.GetTemplateStore(),
}
},
DependencyManager: s.Conf.NewIdentityManager("pagesfromdata"),

View file

@ -180,7 +180,7 @@ func (t *pageTrees) collectAndMarkStaleIdentities(p *paths.Path) []identity.Iden
if p.Component() == files.ComponentFolderContent {
// It may also be a bundled content resource.
key := p.ForBundleType(paths.PathTypeContentResource).Base()
key := p.ForBundleType(paths.TypeContentResource).Base()
tree = t.treeResources
nCount = 0
tree.ForEeachInDimension(key, doctree.DimensionLanguage.Index(),
@ -1304,14 +1304,14 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
checkedCounter atomic.Int64
)
resetPo := func(po *pageOutput, r identity.FinderResult) {
if po.pco != nil {
resetPo := func(po *pageOutput, rebuildContent bool, r identity.FinderResult) {
if rebuildContent && po.pco != nil {
po.pco.Reset() // Will invalidate content cache.
}
po.renderState = 0
po.p.resourcesPublishInit = &sync.Once{}
if r == identity.FinderFoundOneOfMany || po.f.Name == output.HTTPStatusHTMLFormat.Name {
if r == identity.FinderFoundOneOfMany || po.f.Name == output.HTTPStatus404HTMLFormat.Name {
// Will force a re-render even in fast render mode.
po.renderOnce = false
}
@ -1323,7 +1323,7 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
}
// This can be a relativeley expensive operations, so we do it in parallel.
g := rungroup.Run[*pageState](ctx, rungroup.Config[*pageState]{
g := rungroup.Run(ctx, rungroup.Config[*pageState]{
NumWorkers: h.numWorkers,
Handle: func(ctx context.Context, p *pageState) error {
if !p.isRenderedAny() {
@ -1335,7 +1335,8 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
checkedCounter.Add(1)
if r := depsFinder.Contains(id, p.dependencyManager, 2); r > identity.FinderFoundOneOfManyRepetition {
for _, po := range p.pageOutputs {
resetPo(po, r)
// Note that p.dependencyManager is used when rendering content, so reset that.
resetPo(po, true, r)
}
// Done.
return nil
@ -1351,7 +1352,8 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
for _, id := range changes {
checkedCounter.Add(1)
if r := depsFinder.Contains(id, po.dependencyManagerOutput, 50); r > identity.FinderFoundOneOfManyRepetition {
resetPo(po, r)
// Note that dependencyManagerOutput is not used when rendering content, so don't reset that.
resetPo(po, false, r)
continue OUTPUTS
}
}
@ -1954,7 +1956,7 @@ func (sa *sitePagesAssembler) addStandalonePages() error {
tree.InsertIntoValuesDimension(key, p)
}
addStandalone("/404", kinds.KindStatus404, output.HTTPStatusHTMLFormat)
addStandalone("/404", kinds.KindStatus404, output.HTTPStatus404HTMLFormat)
if s.conf.EnableRobotsTXT {
if m.i == 0 || s.Conf.IsMultihost() {

View file

@ -242,8 +242,13 @@ Data en
}
func TestBundleMultipleContentPageWithSamePath(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
printPathWarnings = true
-- layouts/all.html --
All.
-- content/bundle/index.md --
---
title: "Bundle md"
@ -273,14 +278,18 @@ Bundle: {{ $bundle.Title }}|{{ $bundle.Params.foo }}|{{ $bundle.File.Filename }}
P1: {{ $p1.Title }}|{{ $p1.Params.foo }}|{{ $p1.File.Filename }}|
`
b := Test(t, files)
for range 3 {
b := Test(t, files, TestOptWarn())
// There's multiple content files sharing the same logical path and language.
// This is a little arbitrary, but we have to pick one and prefer the Markdown version.
b.AssertFileContent("public/index.html",
filepath.FromSlash("Bundle: Bundle md|md|/content/bundle/index.md|"),
filepath.FromSlash("P1: P1 md|md|/content/p1.md|"),
)
b.AssertLogContains("WARN Duplicate content path: \"/p1\"")
// There's multiple content files sharing the same logical path and language.
// This is a little arbitrary, but we have to pick one and prefer the Markdown version.
b.AssertFileContent("public/index.html",
filepath.FromSlash("Bundle: Bundle md|md|/content/bundle/index.md|"),
filepath.FromSlash("P1: P1 md|md|/content/p1.md|"),
)
}
}
// Issue #11944

View file

@ -17,6 +17,8 @@ import (
"fmt"
"strings"
"testing"
qt "github.com/frankban/quicktest"
)
func TestRenderHooksRSS(t *testing.T) {
@ -129,6 +131,7 @@ P1: <p>P1. <a href="https://www.gohugo.io">I&rsquo;m an inline-style link</a></p
<h1 id="heading-in-p1">Heading in p1</h1>
<h1 id="heading-in-p2">Heading in p2</h1>
`)
b.AssertFileContent("public/index.xml", `
P2: <p>P2. xml-link: https://www.bep.is|</p>
P3: <p>P3. xml-link: https://www.example.org|</p>
@ -378,3 +381,93 @@ Content: {{ .Content}}|
"|Text: First line.\nSecond line.||\n",
)
}
func TestContentOutputReuseRenderHooksAndShortcodesHTMLOnly(t *testing.T) {
files := `
-- hugo.toml --
-- layouts/index.html --
HTML: {{ .Title }}|{{ .Content }}|
-- layouts/index.xml --
XML: {{ .Title }}|{{ .Content }}|
-- layouts/_markup/render-heading.html --
Render heading.
-- layouts/shortcodes/myshortcode.html --
My shortcode.
-- content/_index.md --
---
title: "Home"
---
## Heading
{{< myshortcode >}}
`
b := Test(t, files)
s := b.H.Sites[0]
b.Assert(s.home.pageOutputTemplateVariationsState.Load(), qt.Equals, uint32(1))
b.AssertFileContent("public/index.html", "HTML: Home|Render heading.\nMy shortcode.\n|")
b.AssertFileContent("public/index.xml", "XML: Home|Render heading.\nMy shortcode.\n|")
}
func TestContentOutputNoReuseRenderHooksInBothHTMLAnXML(t *testing.T) {
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term"]
-- layouts/index.html --
HTML: {{ .Title }}|{{ .Content }}|
-- layouts/index.xml --
XML: {{ .Title }}|{{ .Content }}|
-- layouts/_markup/render-heading.html --
Render heading.
-- layouts/_markup/render-heading.xml --
Render heading XML.
-- content/_index.md --
---
title: "Home"
---
## Heading
`
b := Test(t, files)
s := b.H.Sites[0]
b.Assert(s.home.pageOutputTemplateVariationsState.Load() > 1, qt.IsTrue)
b.AssertFileContentExact("public/index.xml", "XML: Home|Render heading XML.|")
b.AssertFileContentExact("public/index.html", "HTML: Home|Render heading.|")
}
func TestContentOutputNoReuseShortcodesInBothHTMLAnXML(t *testing.T) {
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term"]
-- layouts/index.html --
HTML: {{ .Title }}|{{ .Content }}|
-- layouts/index.xml --
XML: {{ .Title }}|{{ .Content }}|
-- layouts/_markup/render-heading.html --
Render heading.
-- layouts/shortcodes/myshortcode.html --
My shortcode HTML.
-- layouts/shortcodes/myshortcode.xml --
My shortcode XML.
-- content/_index.md --
---
title: "Home"
---
## Heading
{{< myshortcode >}}
`
b := Test(t, files)
// b.DebugPrint("", tplimpl.CategoryShortcode)
b.AssertFileContentExact("public/index.xml", "My shortcode XML.")
b.AssertFileContentExact("public/index.html", "My shortcode HTML.")
s := b.H.Sites[0]
b.Assert(s.home.pageOutputTemplateVariationsState.Load() > 1, qt.IsTrue)
}

View file

@ -14,35 +14,46 @@
package doctree
import (
"iter"
"sync"
radix "github.com/armon/go-radix"
)
// Tree is a radix tree that holds T.
// Tree is a non thread safe radix tree that holds T.
type Tree[T any] interface {
TreeCommon[T]
WalkPrefix(s string, f func(s string, v T) (bool, error)) error
WalkPath(s string, f func(s string, v T) (bool, error)) error
All() iter.Seq2[string, T]
}
// TreeThreadSafe is a thread safe radix tree that holds T.
type TreeThreadSafe[T any] interface {
TreeCommon[T]
WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error
WalkPath(lockType LockType, s string, f func(s string, v T) (bool, error)) error
All(lockType LockType) iter.Seq2[string, T]
}
type TreeCommon[T any] interface {
Get(s string) T
LongestPrefix(s string) (string, T)
Insert(s string, v T) T
WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error
}
// NewSimpleTree creates a new SimpleTree.
func NewSimpleTree[T comparable]() *SimpleTree[T] {
func NewSimpleTree[T any]() *SimpleTree[T] {
return &SimpleTree[T]{tree: radix.New()}
}
// SimpleTree is a thread safe radix tree that holds T.
type SimpleTree[T comparable] struct {
mu sync.RWMutex
// SimpleTree is a radix tree that holds T.
// This tree is not thread safe.
type SimpleTree[T any] struct {
tree *radix.Tree
zero T
}
func (tree *SimpleTree[T]) Get(s string) T {
tree.mu.RLock()
defer tree.mu.RUnlock()
if v, ok := tree.tree.Get(s); ok {
return v.(T)
}
@ -50,9 +61,6 @@ func (tree *SimpleTree[T]) Get(s string) T {
}
func (tree *SimpleTree[T]) LongestPrefix(s string) (string, T) {
tree.mu.RLock()
defer tree.mu.RUnlock()
if s, v, ok := tree.tree.LongestPrefix(s); ok {
return s, v.(T)
}
@ -60,17 +68,121 @@ func (tree *SimpleTree[T]) LongestPrefix(s string) (string, T) {
}
func (tree *SimpleTree[T]) Insert(s string, v T) T {
tree.tree.Insert(s, v)
return v
}
func (tree *SimpleTree[T]) Walk(f func(s string, v T) (bool, error)) error {
var err error
tree.tree.Walk(func(s string, v any) bool {
var b bool
b, err = f(s, v.(T))
if err != nil {
return true
}
return b
})
return err
}
func (tree *SimpleTree[T]) WalkPrefix(s string, f func(s string, v T) (bool, error)) error {
var err error
tree.tree.WalkPrefix(s, func(s string, v any) bool {
var b bool
b, err = f(s, v.(T))
if err != nil {
return true
}
return b
})
return err
}
func (tree *SimpleTree[T]) WalkPath(s string, f func(s string, v T) (bool, error)) error {
var err error
tree.tree.WalkPath(s, func(s string, v any) bool {
var b bool
b, err = f(s, v.(T))
if err != nil {
return true
}
return b
})
return err
}
func (tree *SimpleTree[T]) All() iter.Seq2[string, T] {
return func(yield func(s string, v T) bool) {
tree.tree.Walk(func(s string, v any) bool {
return !yield(s, v.(T))
})
}
}
// NewSimpleThreadSafeTree creates a new SimpleTree.
func NewSimpleThreadSafeTree[T any]() *SimpleThreadSafeTree[T] {
return &SimpleThreadSafeTree[T]{tree: radix.New(), mu: new(sync.RWMutex)}
}
// SimpleThreadSafeTree is a thread safe radix tree that holds T.
type SimpleThreadSafeTree[T any] struct {
mu *sync.RWMutex
noLock bool
tree *radix.Tree
zero T
}
var noopFunc = func() {}
func (tree *SimpleThreadSafeTree[T]) readLock() func() {
if tree.noLock {
return noopFunc
}
tree.mu.RLock()
return tree.mu.RUnlock
}
func (tree *SimpleThreadSafeTree[T]) writeLock() func() {
if tree.noLock {
return noopFunc
}
tree.mu.Lock()
defer tree.mu.Unlock()
return tree.mu.Unlock
}
func (tree *SimpleThreadSafeTree[T]) Get(s string) T {
unlock := tree.readLock()
defer unlock()
if v, ok := tree.tree.Get(s); ok {
return v.(T)
}
return tree.zero
}
func (tree *SimpleThreadSafeTree[T]) LongestPrefix(s string) (string, T) {
unlock := tree.readLock()
defer unlock()
if s, v, ok := tree.tree.LongestPrefix(s); ok {
return s, v.(T)
}
return "", tree.zero
}
func (tree *SimpleThreadSafeTree[T]) Insert(s string, v T) T {
unlock := tree.writeLock()
defer unlock()
tree.tree.Insert(s, v)
return v
}
func (tree *SimpleTree[T]) Lock(lockType LockType) func() {
func (tree *SimpleThreadSafeTree[T]) Lock(lockType LockType) func() {
switch lockType {
case LockTypeNone:
return func() {}
return noopFunc
case LockTypeRead:
tree.mu.RLock()
return tree.mu.RUnlock
@ -78,10 +190,16 @@ func (tree *SimpleTree[T]) Lock(lockType LockType) func() {
tree.mu.Lock()
return tree.mu.Unlock
}
return func() {}
return noopFunc
}
func (tree *SimpleTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
func (tree SimpleThreadSafeTree[T]) LockTree(lockType LockType) (TreeThreadSafe[T], func()) {
unlock := tree.Lock(lockType)
tree.noLock = true
return &tree, unlock // create a copy of tree with the noLock flag set to true.
}
func (tree *SimpleThreadSafeTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
commit := tree.Lock(lockType)
defer commit()
var err error
@ -96,3 +214,31 @@ func (tree *SimpleTree[T]) WalkPrefix(lockType LockType, s string, f func(s stri
return err
}
func (tree *SimpleThreadSafeTree[T]) WalkPath(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
commit := tree.Lock(lockType)
defer commit()
var err error
tree.tree.WalkPath(s, func(s string, v any) bool {
var b bool
b, err = f(s, v.(T))
if err != nil {
return true
}
return b
})
return err
}
func (tree *SimpleThreadSafeTree[T]) All(lockType LockType) iter.Seq2[string, T] {
commit := tree.Lock(lockType)
defer commit()
return func(yield func(s string, v T) bool) {
tree.tree.Walk(func(s string, v any) bool {
return !yield(s, v.(T))
})
}
}
// iter.Seq[*TemplWithBaseApplied]

View file

@ -17,8 +17,6 @@ import (
"fmt"
"strings"
"sync"
radix "github.com/armon/go-radix"
)
var _ MutableTrees = MutableTrees{}
@ -60,11 +58,9 @@ func (ctx *WalkContext[T]) AddPostHook(handler func() error) {
ctx.HooksPost = append(ctx.HooksPost, handler)
}
func (ctx *WalkContext[T]) Data() *SimpleTree[any] {
func (ctx *WalkContext[T]) Data() *SimpleThreadSafeTree[any] {
ctx.dataInit.Do(func() {
ctx.data = &SimpleTree[any]{
tree: radix.New(),
}
ctx.data = NewSimpleThreadSafeTree[any]()
})
return ctx.data
}
@ -191,7 +187,7 @@ func (t MutableTrees) CanLock() bool {
// WalkContext is passed to the Walk callback.
type WalkContext[T any] struct {
data *SimpleTree[any]
data *SimpleThreadSafeTree[any]
dataInit sync.Once
eventHandlers eventHandlers[T]
events []*Event[T]

View file

@ -13,7 +13,9 @@
package doctree
var _ Tree[string] = (*TreeShiftTree[string])(nil)
import "iter"
var _ TreeThreadSafe[string] = (*TreeShiftTree[string])(nil)
type TreeShiftTree[T comparable] struct {
// This tree is shiftable in one dimension.
@ -26,16 +28,16 @@ type TreeShiftTree[T comparable] struct {
zero T
// Will be of length equal to the length of the dimension.
trees []*SimpleTree[T]
trees []*SimpleThreadSafeTree[T]
}
func NewTreeShiftTree[T comparable](d, length int) *TreeShiftTree[T] {
if length <= 0 {
panic("length must be > 0")
}
trees := make([]*SimpleTree[T], length)
trees := make([]*SimpleThreadSafeTree[T], length)
for i := range length {
trees[i] = NewSimpleTree[T]()
trees[i] = NewSimpleThreadSafeTree[T]()
}
return &TreeShiftTree[T]{d: d, trees: trees}
}
@ -91,6 +93,14 @@ func (t *TreeShiftTree[T]) WalkPrefixRaw(lockType LockType, s string, f func(s s
return nil
}
func (t *TreeShiftTree[T]) WalkPath(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
return t.trees[t.v].WalkPath(lockType, s, f)
}
func (t *TreeShiftTree[T]) All(lockType LockType) iter.Seq2[string, T] {
return t.trees[t.v].All(lockType)
}
func (t *TreeShiftTree[T]) LenRaw() int {
var count int
for _, tt := range t.trees {

View file

@ -134,7 +134,6 @@ func (h *HugoSites) resolveSite(lang string) *Site {
return nil
}
// Only used in tests.
type buildCounters struct {
contentRenderCounter atomic.Uint64
pageRenderCounter atomic.Uint64
@ -557,7 +556,6 @@ func (h *HugoSites) handleDataFile(r *source.File) error {
higherPrecedentData := current[r.BaseFileName()]
switch data.(type) {
case nil:
case map[string]any:
switch higherPrecedentData.(type) {

View file

@ -494,17 +494,17 @@ func (s *Site) executeDeferredTemplates(de *deps.DeferredExecutions) error {
defer deferred.Mu.Unlock()
if !deferred.Executed {
tmpl := s.Deps.Tmpl()
templ, found := tmpl.Lookup(deferred.TemplateName)
if !found {
panic(fmt.Sprintf("template %q not found", deferred.TemplateName))
tmpl := s.Deps.GetTemplateStore()
ti := s.TemplateStore.LookupByPath(deferred.TemplatePath)
if ti == nil {
panic(fmt.Sprintf("template %q not found", deferred.TemplatePath))
}
if err := func() error {
buf := bufferpool.GetBuffer()
defer bufferpool.PutBuffer(buf)
err = tmpl.ExecuteWithContext(deferred.Ctx, templ, buf, deferred.Data)
err = tmpl.ExecuteWithContext(deferred.Ctx, ti, buf, deferred.Data)
if err != nil {
return err
}
@ -577,9 +577,13 @@ func (h *HugoSites) printUnusedTemplatesOnce() error {
h.printUnusedTemplatesInit.Do(func() {
conf := h.Configs.Base
if conf.PrintUnusedTemplates {
unusedTemplates := h.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
unusedTemplates := h.GetTemplateStore().UnusedTemplates()
for _, unusedTemplate := range unusedTemplates {
h.Log.Warnf("Template %s is unused, source file %s", unusedTemplate.Name(), unusedTemplate.Filename())
if unusedTemplate.Fi != nil {
h.Log.Warnf("Template %s is unused, source %q", unusedTemplate.PathInfo.Path(), unusedTemplate.Fi.Meta().Filename)
} else {
h.Log.Warnf("Template %s is unused", unusedTemplate.PathInfo.Path())
}
}
}
})
@ -954,7 +958,7 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
case files.ComponentFolderLayouts:
tmplChanged = true
templatePath := pathInfo.Unnormalized().TrimLeadingSlash().PathNoLang()
if !h.Tmpl().HasTemplate(templatePath) {
if !h.GetTemplateStore().HasTemplate(templatePath) {
tmplAdded = true
}
@ -974,8 +978,9 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
}
} else {
logger.Println("Template changed", pathInfo.Path())
if templ, found := h.Tmpl().GetIdentity(templatePath); found {
changes = append(changes, templ)
id := h.GetTemplateStore().GetIdentity(pathInfo.Path())
if id != nil {
changes = append(changes, id)
} else {
changes = append(changes, pathInfo)
}
@ -1084,7 +1089,6 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
changed := &WhatChanged{
needsPagesAssembly: needsPagesAssemble,
identitySet: make(identity.Identities),
}
changed.Add(changes...)
@ -1106,17 +1110,39 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
}
}
h.Deps.OnChangeListeners.Notify(changed.Changes()...)
changes2 := changed.Changes()
h.Deps.OnChangeListeners.Notify(changes2...)
if err := h.resolveAndClearStateForIdentities(ctx, l, cacheBusterOr, changed.Drain()); err != nil {
return err
}
if tmplChanged || i18nChanged {
if tmplChanged {
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
// TODO(bep) this could probably be optimized to somehow
// only load the changed templates and its dependencies, but that is non-trivial.
depsFinder := identity.NewFinder(identity.FinderConfig{})
ll := l.WithField("substep", "rebuild templates")
s := h.Sites[0]
if err := s.Deps.TemplateStore.RefreshFiles(func(fi hugofs.FileMetaInfo) bool {
pi := fi.Meta().PathInfo
for _, id := range changes2 {
if depsFinder.Contains(pi, id, -1) > 0 {
return true
}
}
return false
}); err != nil {
return ll, err
}
return ll, nil
}); err != nil {
return err
}
}
if i18nChanged {
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
ll := l.WithField("substep", "rebuild i18n")
var prototype *deps.Deps
for i, s := range h.Sites {
if err := s.Deps.Compile(prototype); err != nil {

View file

@ -76,12 +76,13 @@ Single: {{ .Title }}|{{ .RelPermalink}}|{{ range .OutputFormats }}{{ .Name }}: {
`
b := Test(t, files)
b.AssertFileContent("public/index.html", `List: |/|html: /|rss: /index.xml|$`)
b.AssertFileContent("public/index.xml", `List xml: |/|html: /|rss: /index.xml|$`)
b.AssertFileContent("public/p1/index.html", `Single: Page|/p1/|html: /p1/|$`)
b.AssertFileExists("public/p1/index.xml", false)
for i := 0; i < 2; i++ {
b := Test(t, files)
b.AssertFileContent("public/index.html", `List: |/|html: /|rss: /index.xml|$`)
b.AssertFileContent("public/index.xml", `List xml: |/|html: /|rss: /index.xml|$`)
b.AssertFileContent("public/p1/index.html", `Single: Page|/p1/|html: /p1/|$`)
b.AssertFileExists("public/p1/index.xml", false)
}
}
func TestSmoke(t *testing.T) {

View file

@ -219,19 +219,31 @@ type IntegrationTestBuilder struct {
type lockingBuffer struct {
sync.Mutex
bytes.Buffer
buf bytes.Buffer
}
func (b *lockingBuffer) String() string {
b.Lock()
defer b.Unlock()
return b.buf.String()
}
func (b *lockingBuffer) Reset() {
b.Lock()
defer b.Unlock()
b.buf.Reset()
}
func (b *lockingBuffer) ReadFrom(r io.Reader) (n int64, err error) {
b.Lock()
n, err = b.Buffer.ReadFrom(r)
n, err = b.buf.ReadFrom(r)
b.Unlock()
return
}
func (b *lockingBuffer) Write(p []byte) (n int, err error) {
b.Lock()
n, err = b.Buffer.Write(p)
n, err = b.buf.Write(p)
b.Unlock()
return
}

View file

@ -28,15 +28,13 @@ import (
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/output/layouts"
"github.com/gohugoio/hugo/related"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/types"
@ -116,6 +114,14 @@ type pageState struct {
resourcesPublishInit *sync.Once
}
func (p *pageState) incrPageOutputTemplateVariation() {
p.pageOutputTemplateVariationsState.Add(1)
}
func (p *pageState) canReusePageOutputContent() bool {
return p.pageOutputTemplateVariationsState.Load() == 1
}
func (p *pageState) IdentifierBase() string {
return p.Path()
}
@ -169,10 +175,6 @@ func (p *pageState) resetBuildState() {
// Nothing to do for now.
}
func (p *pageState) reusePageOutputContent() bool {
return p.pageOutputTemplateVariationsState.Load() == 1
}
func (p *pageState) skipRender() bool {
b := p.s.conf.C.SegmentFilter.ShouldExcludeFine(
segments.SegmentMatcherFields{
@ -474,49 +476,40 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
return nil
}
func (p *pageState) getLayoutDescriptor() layouts.LayoutDescriptor {
p.layoutDescriptorInit.Do(func() {
var section string
sections := p.SectionsEntries()
switch p.Kind() {
case kinds.KindSection:
if len(sections) > 0 {
section = sections[0]
}
case kinds.KindTaxonomy, kinds.KindTerm:
if p.m.singular != "" {
section = p.m.singular
} else if len(sections) > 0 {
section = sections[0]
}
default:
}
p.layoutDescriptor = layouts.LayoutDescriptor{
Kind: p.Kind(),
Type: p.Type(),
Lang: p.Language().Lang,
Layout: p.Layout(),
Section: section,
}
})
return p.layoutDescriptor
func (po *pageOutput) getTemplateBasePathAndDescriptor() (string, tplimpl.TemplateDescriptor) {
p := po.p
f := po.f
base := p.PathInfo().BaseReTyped(p.m.pageConfig.Type)
return base, tplimpl.TemplateDescriptor{
Kind: p.Kind(),
Lang: p.Language().Lang,
Layout: p.Layout(),
OutputFormat: f.Name,
MediaType: f.MediaType.Type,
IsPlainText: f.IsPlainText,
}
}
func (p *pageState) resolveTemplate(layouts ...string) (tpl.Template, bool, error) {
f := p.outputFormat()
d := p.getLayoutDescriptor()
func (p *pageState) resolveTemplate(layouts ...string) (*tplimpl.TemplInfo, bool, error) {
dir, d := p.getTemplateBasePathAndDescriptor()
if len(layouts) > 0 {
d.Layout = layouts[0]
d.LayoutOverride = true
d.LayoutMustMatch = true
}
return p.s.Tmpl().LookupLayout(d, f)
q := tplimpl.TemplateQuery{
Path: dir,
Category: tplimpl.CategoryLayout,
Desc: d,
}
tinfo := p.s.TemplateStore.LookupPagesLayout(q)
if tinfo == nil {
return nil, false, nil
}
return tinfo, true, nil
}
// Must be run after the site section tree etc. is built and ready.
@ -705,7 +698,7 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
if isRenderingSite {
cp := p.pageOutput.pco
if cp == nil && p.reusePageOutputContent() {
if cp == nil && p.canReusePageOutputContent() {
// Look for content to reuse.
for i := range p.pageOutputs {
if i == idx {

View file

@ -21,7 +21,6 @@ import (
"github.com/gohugoio/hugo/lazy"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/navigation"
"github.com/gohugoio/hugo/output/layouts"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/source"
@ -86,9 +85,6 @@ type pageCommon struct {
// should look like.
targetPathDescriptor page.TargetPathDescriptor
layoutDescriptor layouts.LayoutDescriptor
layoutDescriptorInit sync.Once
// Set if feature enabled and this is in a Git repo.
gitInfo source.GitInfo
codeowners []string

View file

@ -24,6 +24,8 @@ import (
"strings"
"unicode/utf8"
maps0 "maps"
"github.com/bep/logg"
"github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/common/herrors"
@ -32,7 +34,6 @@ import (
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types/hstring"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
@ -45,7 +46,6 @@ import (
"github.com/gohugoio/hugo/tpl"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
maps0 "maps"
)
const (
@ -600,7 +600,7 @@ func (c *cachedContentScope) contentRendered(ctx context.Context) (contentSummar
return nil, err
}
if hasShortcodeVariants {
cp.po.p.pageOutputTemplateVariationsState.Add(1)
cp.po.p.incrPageOutputTemplateVariation()
}
var result contentSummary
@ -684,10 +684,9 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
if err := cp.initRenderHooks(); err != nil {
return nil, err
}
f := cp.po.f
po := cp.po
p := po.p
ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, p, f, false)
ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, po, false)
if err != nil {
return nil, err
}
@ -701,16 +700,14 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
if p.s.conf.Internal.Watch {
for _, s := range cp2.po.p.m.content.shortcodeState.shortcodes {
for _, templ := range s.templs {
cp.trackDependency(templ.(identity.IdentityProvider))
}
cp.trackDependency(s.templ)
}
}
// Transfer shortcode names so HasShortcode works for shortcodes from included pages.
cp.po.p.m.content.shortcodeState.transferNames(cp2.po.p.m.content.shortcodeState)
if cp2.po.p.pageOutputTemplateVariationsState.Load() > 0 {
cp.po.p.pageOutputTemplateVariationsState.Add(1)
cp.po.p.incrPageOutputTemplateVariation()
}
}
@ -723,7 +720,7 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
}
if hasVariants {
p.pageOutputTemplateVariationsState.Add(1)
p.incrPageOutputTemplateVariation()
}
isHTML := cp.po.p.m.pageConfig.ContentMediaType.IsHTML()
@ -980,7 +977,7 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
return "", err
}
placeholders, err := s.prepareShortcodesForPage(ctx, pco.po.p, pco.po.f, true)
placeholders, err := s.prepareShortcodesForPage(ctx, pco.po, true)
if err != nil {
return "", err
}
@ -990,7 +987,7 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
return "", err
}
if hasVariants {
pco.po.p.pageOutputTemplateVariationsState.Add(1)
pco.po.p.incrPageOutputTemplateVariation()
}
b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false)
if err != nil {
@ -1028,7 +1025,7 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
return "", err
}
if hasShortcodeVariants {
pco.po.p.pageOutputTemplateVariationsState.Add(1)
pco.po.p.incrPageOutputTemplateVariation()
}
}
@ -1110,7 +1107,7 @@ func (c *cachedContentScope) RenderShortcodes(ctx context.Context) (template.HTM
}
if hasVariants {
pco.po.p.pageOutputTemplateVariationsState.Add(1)
pco.po.p.incrPageOutputTemplateVariation()
}
if cb != nil {

View file

@ -72,8 +72,11 @@ type pageMeta struct {
// Prepare for a rebuild of the data passed in from front matter.
func (m *pageMeta) setMetaPostPrepareRebuild() {
params := xmaps.Clone[map[string]any](m.paramsOriginal)
params := xmaps.Clone(m.paramsOriginal)
m.pageMetaParams.pageConfig = &pagemeta.PageConfig{
Kind: m.pageConfig.Kind,
Lang: m.pageConfig.Lang,
Path: m.pageConfig.Path,
Params: params,
}
m.pageMetaFrontMatter = pageMetaFrontMatter{}
@ -108,10 +111,10 @@ func (p *pageMeta) Aliases() []string {
}
func (p *pageMeta) BundleType() string {
switch p.pathInfo.BundleType() {
case paths.PathTypeLeaf:
switch p.pathInfo.Type() {
case paths.TypeLeaf:
return "leaf"
case paths.PathTypeBranch:
case paths.TypeBranch:
return "branch"
default:
return ""

View file

@ -19,23 +19,21 @@ import (
"errors"
"fmt"
"html/template"
"strings"
"sync"
"sync/atomic"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/highlight/chromalexers"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/gohugoio/hugo/markup/converter"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources/page"
@ -120,9 +118,9 @@ func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (tem
}
// Make sure to send the *pageState and not the *pageContentOutput to the template.
res, err := executeToString(ctx, pco.po.p.s.Tmpl(), templ, pco.po.p)
res, err := executeToString(ctx, pco.po.p.s.GetTemplateStore(), templ, pco.po.p)
if err != nil {
return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err))
return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Template.Name(), err))
}
return template.HTML(res), nil
}
@ -274,103 +272,100 @@ func (pco *pageContentOutput) initRenderHooks() error {
return r
}
layoutDescriptor := pco.po.p.getLayoutDescriptor()
layoutDescriptor.RenderingHook = true
layoutDescriptor.LayoutOverride = false
layoutDescriptor.Layout = ""
// Inherit the descriptor from the page/current output format.
// This allows for fine-grained control of the template used for
// rendering of e.g. links.
base, layoutDescriptor := pco.po.p.getTemplateBasePathAndDescriptor()
switch tp {
case hooks.LinkRendererType:
layoutDescriptor.Kind = "render-link"
layoutDescriptor.Variant1 = "link"
case hooks.ImageRendererType:
layoutDescriptor.Kind = "render-image"
layoutDescriptor.Variant1 = "image"
case hooks.HeadingRendererType:
layoutDescriptor.Kind = "render-heading"
layoutDescriptor.Variant1 = "heading"
case hooks.PassthroughRendererType:
layoutDescriptor.Kind = "render-passthrough"
layoutDescriptor.Variant1 = "passthrough"
if id != nil {
layoutDescriptor.KindVariants = id.(string)
layoutDescriptor.Variant2 = id.(string)
}
case hooks.BlockquoteRendererType:
layoutDescriptor.Kind = "render-blockquote"
layoutDescriptor.Variant1 = "blockquote"
if id != nil {
layoutDescriptor.KindVariants = id.(string)
layoutDescriptor.Variant2 = id.(string)
}
case hooks.TableRendererType:
layoutDescriptor.Kind = "render-table"
layoutDescriptor.Variant1 = "table"
case hooks.CodeBlockRendererType:
layoutDescriptor.Kind = "render-codeblock"
layoutDescriptor.Variant1 = "codeblock"
if id != nil {
lang := id.(string)
lexer := chromalexers.Get(lang)
if lexer != nil {
layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",")
} else {
layoutDescriptor.KindVariants = lang
}
layoutDescriptor.Variant2 = id.(string)
}
}
getHookTemplate := func(f output.Format) (tpl.Template, bool) {
templ, found, err := pco.po.p.s.Tmpl().LookupLayout(layoutDescriptor, f)
if err != nil {
panic(err)
}
if found {
if isitp, ok := templ.(tpl.IsInternalTemplateProvider); ok && isitp.IsInternalTemplate() {
renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
switch templ.Name() {
case "_default/_markup/render-link.html":
if !renderHookConfig.Link.IsEnableDefault() {
return nil, false
}
case "_default/_markup/render-image.html":
if !renderHookConfig.Image.IsEnableDefault() {
return nil, false
}
}
}
}
return templ, found
renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
var ignoreInternal bool
switch layoutDescriptor.Variant1 {
case "link":
ignoreInternal = !renderHookConfig.Link.IsEnableDefault()
case "image":
ignoreInternal = !renderHookConfig.Image.IsEnableDefault()
}
templ, found1 := getHookTemplate(pco.po.f)
if !found1 || pco.po.p.reusePageOutputContent() {
defaultOutputFormat := pco.po.p.s.conf.C.DefaultOutputFormat
candidates := pco.po.p.s.renderFormats
// Some hooks may only be available in HTML, and if
// this site is configured to not have HTML output, we need to
// make sure we have a fallback. This should be very rare.
if pco.po.f.MediaType.FirstSuffix.Suffix != "html" {
if _, found := candidates.GetBySuffix("html"); !found {
candidates = append(candidates, output.HTMLFormat)
}
candidates := pco.po.p.s.renderFormats
var numCandidatesFound int
consider := func(candidate *tplimpl.TemplInfo) bool {
if layoutDescriptor.Variant1 != candidate.D.Variant1 {
return false
}
// Check if some of the other output formats would give a different template.
for _, f := range candidates {
if f.Name == pco.po.f.Name {
continue
}
templ2, found2 := getHookTemplate(f)
if found2 {
if !found1 && f.Name == defaultOutputFormat.Name {
templ = templ2
found1 = true
break
}
if templ != templ2 {
pco.po.p.pageOutputTemplateVariationsState.Add(1)
break
}
}
if layoutDescriptor.Variant2 != "" && candidate.D.Variant2 != "" && layoutDescriptor.Variant2 != candidate.D.Variant2 {
return false
}
if ignoreInternal && candidate.SubCategory == tplimpl.SubCategoryEmbedded {
// Don't consider the internal hook templates.
return false
}
if pco.po.p.pageOutputTemplateVariationsState.Load() > 1 {
return true
}
if candidate.D.OutputFormat == "" {
numCandidatesFound++
} else if _, found := candidates.GetByName(candidate.D.OutputFormat); found {
numCandidatesFound++
}
return true
}
getHookTemplate := func() (*tplimpl.TemplInfo, bool) {
q := tplimpl.TemplateQuery{
Path: base,
Category: tplimpl.CategoryMarkup,
Desc: layoutDescriptor,
Consider: consider,
}
v := pco.po.p.s.TemplateStore.LookupPagesLayout(q)
return v, v != nil
}
templ, found1 := getHookTemplate()
if found1 && templ == nil {
panic("found1 is true, but templ is nil")
}
if !found1 && layoutDescriptor.OutputFormat == pco.po.p.s.conf.DefaultOutputFormat {
numCandidatesFound++
}
if numCandidatesFound > 1 {
// More than one output format candidate found for this hook temoplate,
// so we cannot reuse the same rendered content.
pco.po.p.incrPageOutputTemplateVariation()
}
if !found1 {
@ -384,7 +379,7 @@ func (pco *pageContentOutput) initRenderHooks() error {
}
r := hookRendererTemplate{
templateHandler: pco.po.p.s.Tmpl(),
templateHandler: pco.po.p.s.GetTemplateStore(),
templ: templ,
resolvePosition: resolvePosition,
}
@ -488,7 +483,7 @@ func (t targetPathsHolder) targetPaths() page.TargetPaths {
return t.paths
}
func executeToString(ctx context.Context, h tpl.TemplateHandler, templ tpl.Template, data any) (string, error) {
func executeToString(ctx context.Context, h *tplimpl.TemplateStore, templ *tplimpl.TemplInfo, data any) (string, error) {
b := bp.GetBuffer()
defer bp.PutBuffer(b)
if err := h.ExecuteWithContext(ctx, templ, b, data); err != nil {

View file

@ -195,7 +195,7 @@ func (c *pagesCollector) Collect() (collectErr error) {
return id.p.Dir() == fim.Meta().PathInfo.Dir()
}
if fim.Meta().PathInfo.IsLeafBundle() && id.p.BundleType() == paths.PathTypeContentSingle {
if fim.Meta().PathInfo.IsLeafBundle() && id.p.Type() == paths.TypeContentSingle {
return id.p.Dir() == fim.Meta().PathInfo.Dir()
}
@ -314,7 +314,7 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
return nil, filepath.SkipDir
}
seen := map[hstrings.Tuple]bool{}
seen := map[hstrings.Strings2]hugofs.FileMetaInfo{}
for _, fi := range readdir {
if fi.IsDir() {
continue
@ -327,11 +327,14 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
// These would eventually have been filtered out as duplicates when
// inserting them into the document store,
// but doing it here will preserve a consistent ordering.
baseLang := hstrings.Tuple{First: pi.Base(), Second: meta.Lang}
if seen[baseLang] {
baseLang := hstrings.Strings2{pi.Base(), meta.Lang}
if fi2, ok := seen[baseLang]; ok {
if c.h.Configs.Base.PrintPathWarnings && !c.h.isRebuild() {
c.logger.Warnf("Duplicate content path: %q file: %q file: %q", pi.Base(), fi2.Meta().Filename, meta.Filename)
}
continue
}
seen[baseLang] = true
seen[baseLang] = fi
if pi == nil {
panic(fmt.Sprintf("no path info for %q", meta.Filename))
@ -374,7 +377,7 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
func (c *pagesCollector) handleBundleLeaf(dir, bundle hugofs.FileMetaInfo, inPath string, readdir []hugofs.FileMetaInfo) error {
bundlePi := bundle.Meta().PathInfo
seen := map[hstrings.Tuple]bool{}
seen := map[hstrings.Strings2]bool{}
walk := func(path string, info hugofs.FileMetaInfo) error {
if info.IsDir() {
@ -396,7 +399,7 @@ func (c *pagesCollector) handleBundleLeaf(dir, bundle hugofs.FileMetaInfo, inPat
// These would eventually have been filtered out as duplicates when
// inserting them into the document store,
// but doing it here will preserve a consistent ordering.
baseLang := hstrings.Tuple{First: pi.Base(), Second: info.Meta().Lang}
baseLang := hstrings.Strings2{pi.Base(), info.Meta().Lang}
if seen[baseLang] {
return nil
}

View file

@ -29,6 +29,7 @@ import (
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
)
@ -167,8 +168,7 @@ type PagesFromTemplateOptions struct {
}
type PagesFromTemplateDeps struct {
TmplFinder tpl.TemplateParseFinder
TmplExec tpl.TemplateExecutor
TemplateStore *tplimpl.TemplateStore
}
var _ resource.Staler = (*PagesFromTemplate)(nil)
@ -303,7 +303,7 @@ func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
}
defer f.Close()
tmpl, err := p.TmplFinder.Parse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
tmpl, err := p.TemplateStore.TextParse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
if err != nil {
return BuildInfo{}, err
}
@ -314,7 +314,7 @@ func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p)
if err := p.TmplExec.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil {
if err := p.TemplateStore.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil {
return BuildInfo{}, err
}

View file

@ -98,7 +98,8 @@ ADD_MORE_PLACEHOLDER
func TestPagesFromGoTmplMisc(t *testing.T) {
t.Parallel()
b := hugolib.Test(t, filesPagesFromDataTempleBasic)
b := hugolib.Test(t, filesPagesFromDataTempleBasic, hugolib.TestOptWarn())
b.AssertLogContains("! WARN")
b.AssertPublishDir(`
docs/p1/mytext.txt
docs/p1/sub/mytex2.tx

View file

@ -15,7 +15,6 @@ package hugolib
import (
"fmt"
"path/filepath"
"testing"
qt "github.com/frankban/quicktest"
@ -102,10 +101,18 @@ URL: {{ $pag.URL }}
// Issue 6023
func TestPaginateWithSort(t *testing.T) {
b := newTestSitesBuilder(t).WithSimpleConfigFile()
b.WithTemplatesAdded("index.html", `{{ range (.Paginate (sort .Site.RegularPages ".File.Filename" "desc")).Pages }}|{{ .File.Filename }}{{ end }}`)
b.Build(BuildCfg{}).AssertFileContent("public/index.html",
filepath.FromSlash("|content/sect/doc1.nn.md|content/sect/doc1.nb.md|content/sect/doc1.fr.md|content/sect/doc1.en.md"))
files := `
-- hugo.toml --
-- content/a/a.md --
-- content/z/b.md --
-- content/x/b.md --
-- content/x/a.md --
-- layouts/home.html --
Paginate: {{ range (.Paginate (sort .Site.RegularPages ".File.Filename" "desc")).Pages }}|{{ .Path }}{{ end }}
`
b := Test(t, files)
b.AssertFileContent("public/index.html", "Paginate: |/z/b|/x/b|/x/a|/a/a")
}
// https://github.com/gohugoio/hugo/issues/6797
@ -176,12 +183,12 @@ Paginator: {{ .Paginator }}
func TestNilPointerErrorMessage(t *testing.T) {
files := `
-- hugo.toml --
-- hugo.toml --
-- content/p1.md --
-- layouts/_default/single.html --
Home Filename: {{ site.Home.File.Filename }}
`
b, err := TestE(t, files)
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, `_default/single.html:1:22: executing "_default/single.html" File is nil; wrap it in if or with: {{ with site.Home.File }}{{ .Filename }}{{ end }}`)
b.Assert(err.Error(), qt.Contains, `single.html:1:22: executing "single.html" File is nil; wrap it in if or with: {{ with site.Home.File }}{{ .Filename }}{{ end }}`)
}

View file

@ -51,6 +51,7 @@ My Section Bundle Content Content.
title: "My Section"
---
-- content/mysection/mysectiontext.txt --
Content.
-- content/_index.md --
---
title: "Home"
@ -99,15 +100,17 @@ My Other Text: {{ $r.Content }}|{{ $r.Permalink }}|
`
func TestRebuildEditLeafBundleHeaderOnly(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/mysection/mysectionbundle/index.html",
"My Section Bundle Content Content.")
b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "My Section Bundle Content.", "My Section Bundle Content Edited.").Build()
b.AssertFileContent("public/mysection/mysectionbundle/index.html",
"My Section Bundle Content Edited.")
b.AssertRenderCountPage(2) // home (rss) + bundle.
b.AssertRenderCountContent(1)
t.Parallel()
for i := 0; i < 3; i++ {
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/mysection/mysectionbundle/index.html",
"My Section Bundle Content Content.")
b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "My Section Bundle Content.", "My Section Bundle Content Edited.").Build()
b.AssertFileContent("public/mysection/mysectionbundle/index.html",
"My Section Bundle Content Edited.")
b.AssertRenderCountPage(2) // home (rss) + bundle.
b.AssertRenderCountContent(1)
}
}
func TestRebuildEditTextFileInLeafBundle(t *testing.T) {
@ -119,7 +122,7 @@ func TestRebuildEditTextFileInLeafBundle(t *testing.T) {
b.AssertFileContent("public/mysection/mysectionbundle/index.html",
"Text 2 Content Edited")
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(1)
b.AssertRenderCountContent(0)
}
func TestRebuildEditTextFileInShortcode(t *testing.T) {
@ -180,17 +183,17 @@ func TestRebuildEditTextFileInHomeBundle(t *testing.T) {
b.AssertFileContent("public/index.html", "Home Content.")
b.AssertFileContent("public/index.html", "Home Text Content Edited.")
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(1)
b.AssertRenderCountContent(0)
}
func TestRebuildEditTextFileInBranchBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/mysection/index.html", "My Section")
b.AssertFileContent("public/mysection/index.html", "My Section", "0:/mysection/mysectiontext.txt|Content.|")
b.EditFileReplaceAll("content/mysection/mysectiontext.txt", "Content.", "Content Edited.").Build()
b.AssertFileContent("public/mysection/index.html", "My Section")
b.AssertFileContent("public/mysection/index.html", "My Section", "0:/mysection/mysectiontext.txt|Content Edited.|")
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(1)
b.AssertRenderCountContent(0)
}
func testRebuildBothWatchingAndRunning(t *testing.T, files string, withB func(b *IntegrationTestBuilder)) {
@ -484,7 +487,43 @@ Home: {{ .Title }}|{{ .Content }}|
})
}
func TestRebuildSingleWithBaseof(t *testing.T) {
func TestRebuildSingle(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
title = "Hugo Site"
baseURL = "https://example.com"
disableKinds = ["term", "taxonomy", "sitemap", "robotstxt", "404"]
disableLiveReload = true
-- content/p1.md --
---
title: "P1"
---
P1 Content.
-- layouts/index.html --
Home.
-- layouts/single.html --
Single: {{ .Title }}|{{ .Content }}|
{{ with (templates.Defer (dict "key" "global")) }}
Defer.
{{ end }}
`
b := Test(t, files, TestOptRunning())
b.AssertFileContent("public/p1/index.html", "Single: P1|", "Defer.")
b.AssertRenderCountPage(3)
b.AssertRenderCountContent(1)
b.EditFileReplaceFunc("layouts/single.html", func(s string) string {
s = strings.Replace(s, "Single", "Single Edited", 1)
s = strings.Replace(s, "Defer.", "Defer Edited.", 1)
return s
}).Build()
b.AssertFileContent("public/p1/index.html", "Single Edited: P1|", "Defer Edited.")
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(0)
}
func TestRebuildSingleWithBaseofEditSingle(t *testing.T) {
t.Parallel()
files := `
@ -498,9 +537,13 @@ disableLiveReload = true
title: "P1"
---
P1 Content.
[foo](/foo)
-- layouts/_default/baseof.html --
Baseof: {{ .Title }}|
{{ block "main" . }}default{{ end }}
{{ with (templates.Defer (dict "foo" "bar")) }}
Defer.
{{ end }}
-- layouts/index.html --
Home.
-- layouts/_default/single.html --
@ -509,11 +552,81 @@ Single: {{ .Title }}|{{ .Content }}|
{{ end }}
`
b := Test(t, files, TestOptRunning())
b.AssertFileContent("public/p1/index.html", "Baseof: P1|\n\nSingle: P1|<p>P1 Content.</p>\n|")
b.AssertFileContent("public/p1/index.html", "Single: P1|")
b.EditFileReplaceFunc("layouts/_default/single.html", func(s string) string {
return strings.Replace(s, "Single", "Single Edited", 1)
}).Build()
b.AssertFileContent("public/p1/index.html", "Baseof: P1|\n\nSingle Edited: P1|<p>P1 Content.</p>\n|")
b.AssertFileContent("public/p1/index.html", "Single Edited")
}
func TestRebuildSingleWithBaseofEditBaseof(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
title = "Hugo Site"
baseURL = "https://example.com"
disableKinds = ["term", "taxonomy"]
disableLiveReload = true
-- content/p1.md --
---
title: "P1"
---
P1 Content.
[foo](/foo)
-- layouts/_default/baseof.html --
Baseof: {{ .Title }}|
{{ block "main" . }}default{{ end }}
{{ with (templates.Defer (dict "foo" "bar")) }}
Defer.
{{ end }}
-- layouts/index.html --
Home.
-- layouts/_default/single.html --
{{ define "main" }}
Single: {{ .Title }}|{{ .Content }}|
{{ end }}
`
b := Test(t, files, TestOptRunning())
b.AssertFileContent("public/p1/index.html", "Single: P1|")
fmt.Println("===============")
b.EditFileReplaceAll("layouts/_default/baseof.html", "Baseof", "Baseof Edited").Build()
b.AssertFileContent("public/p1/index.html", "Baseof Edited")
}
func TestRebuildWithDeferEditRenderHook(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
title = "Hugo Site"
baseURL = "https://example.com"
disableKinds = ["term", "taxonomy"]
disableLiveReload = true
-- content/p1.md --
---
title: "P1"
---
P1 Content.
[foo](/foo)
-- layouts/_default/baseof.html --
Baseof: {{ .Title }}|
{{ block "main" . }}default{{ end }}
{{ with (templates.Defer (dict "foo" "bar")) }}
Defer.
{{ end }}
-- layouts/single.html --
{{ define "main" }}
Single: {{ .Title }}|{{ .Content }}|
{{ end }}
-- layouts/_default/_markup/render-link.html --
Render Link.
`
b := Test(t, files, TestOptRunning())
// Edit render hook.
b.EditFileReplaceAll("layouts/_default/_markup/render-link.html", "Render Link", "Render Link Edited").Build()
b.AssertFileContent("public/p1/index.html", "Render Link Edited")
}
func TestRebuildFromString(t *testing.T) {

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.
@ -29,6 +29,7 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/gohugoio/hugo/resources/page"
@ -36,7 +37,6 @@ import (
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/output"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/tpl"
@ -205,8 +205,7 @@ type shortcode struct {
indentation string // indentation from source.
info tpl.Info // One of the output formats (arbitrary)
templs []tpl.Template // All output formats
templ *tplimpl.TemplInfo
// If set, the rendered shortcode is sent as part of the surrounding content
// to Goldmark and similar.
@ -230,16 +229,15 @@ func (s shortcode) insertPlaceholder() bool {
}
func (s shortcode) needsInner() bool {
return s.info != nil && s.info.ParseInfo().IsInner
return s.templ != nil && s.templ.ParseInfo.IsInner
}
func (s shortcode) configVersion() int {
if s.info == nil {
if s.templ == nil {
// Not set for inline shortcodes.
return 2
}
return s.info.ParseInfo().Config.Version
return s.templ.ParseInfo.Config.Version
}
func (s shortcode) innerString() string {
@ -315,12 +313,12 @@ func prepareShortcode(
ctx context.Context,
level int,
s *Site,
tplVariants tpl.TemplateVariants,
sc *shortcode,
parent *ShortcodeWithPage,
p *pageState,
po *pageOutput,
isRenderString bool,
) (shortcodeRenderer, error) {
p := po.p
toParseErr := func(err error) error {
source := p.m.content.mustSource()
return p.parseError(fmt.Errorf("failed to render shortcode %q: %w", sc.name, err), source, sc.pos)
@ -333,7 +331,7 @@ func prepareShortcode(
// parsed and rendered by Goldmark.
ctx = tpl.Context.IsInGoldmark.Set(ctx, true)
}
r, err := doRenderShortcode(ctx, level, s, tplVariants, sc, parent, p, isRenderString)
r, err := doRenderShortcode(ctx, level, s, sc, parent, po, isRenderString)
if err != nil {
return nil, false, toParseErr(err)
}
@ -352,30 +350,29 @@ func doRenderShortcode(
ctx context.Context,
level int,
s *Site,
tplVariants tpl.TemplateVariants,
sc *shortcode,
parent *ShortcodeWithPage,
p *pageState,
po *pageOutput,
isRenderString bool,
) (shortcodeRenderer, error) {
var tmpl tpl.Template
var tmpl *tplimpl.TemplInfo
p := po.p
// Tracks whether this shortcode or any of its children has template variations
// in other languages or output formats. We are currently only interested in
// the output formats, so we may get some false positives -- we
// should improve on that.
// the output formats.
var hasVariants bool
if sc.isInline {
if !p.s.ExecHelper.Sec().EnableInlineShortcodes {
return zeroShortcode, nil
}
templName := path.Join("_inline_shortcode", p.Path(), sc.name)
templatePath := path.Join("_inline_shortcode", p.Path(), sc.name)
if sc.isClosing {
templStr := sc.innerString()
var err error
tmpl, err = s.TextTmpl().Parse(templName, templStr)
tmpl, err = s.TemplateStore.TextParse(templatePath, templStr)
if err != nil {
if isRenderString {
return zeroShortcode, p.wrapError(err)
@ -389,21 +386,32 @@ func doRenderShortcode(
} else {
// Re-use of shortcode defined earlier in the same page.
var found bool
tmpl, found = s.TextTmpl().Lookup(templName)
if !found {
tmpl = s.TemplateStore.TextLookup(templatePath)
if tmpl == nil {
return zeroShortcode, fmt.Errorf("no earlier definition of shortcode %q found", sc.name)
}
}
tmpl = tpl.AddIdentity(tmpl)
} else {
var found, more bool
tmpl, found, more = s.Tmpl().LookupVariant(sc.name, tplVariants)
if !found {
ofCount := map[string]int{}
include := func(match *tplimpl.TemplInfo) bool {
ofCount[match.D.OutputFormat]++
return true
}
base, layoutDescriptor := po.getTemplateBasePathAndDescriptor()
q := tplimpl.TemplateQuery{
Path: base,
Name: sc.name,
Category: tplimpl.CategoryShortcode,
Desc: layoutDescriptor,
Consider: include,
}
v := s.TemplateStore.LookupShortcode(q)
if v == nil {
s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path())
return zeroShortcode, nil
}
hasVariants = hasVariants || more
tmpl = v
hasVariants = hasVariants || len(ofCount) > 1
}
data := &ShortcodeWithPage{
@ -427,7 +435,7 @@ func doRenderShortcode(
case string:
inner += innerData
case *shortcode:
s, err := prepareShortcode(ctx, level+1, s, tplVariants, innerData, data, p, isRenderString)
s, err := prepareShortcode(ctx, level+1, s, innerData, data, po, isRenderString)
if err != nil {
return zeroShortcode, err
}
@ -484,7 +492,7 @@ func doRenderShortcode(
}
result, err := renderShortcodeWithPage(ctx, s.Tmpl(), tmpl, data)
result, err := renderShortcodeWithPage(ctx, s.GetTemplateStore(), tmpl, data)
if err != nil && sc.isInline {
fe := herrors.NewFileErrorFromName(err, p.File().Filename())
@ -534,16 +542,11 @@ func (s *shortcodeHandler) hasName(name string) bool {
return ok
}
func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, p *pageState, f output.Format, isRenderString bool) (map[string]shortcodeRenderer, error) {
func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, po *pageOutput, isRenderString bool) (map[string]shortcodeRenderer, error) {
rendered := make(map[string]shortcodeRenderer)
tplVariants := tpl.TemplateVariants{
Language: p.Language().Lang,
OutputFormat: f,
}
for _, v := range s.shortcodes {
s, err := prepareShortcode(ctx, 0, s.s, tplVariants, v, nil, p, isRenderString)
s, err := prepareShortcode(ctx, 0, s.s, v, nil, po, isRenderString)
if err != nil {
return nil, err
}
@ -636,7 +639,7 @@ Loop:
// we trust the template on this:
// if there's no inner, we're done
if !sc.isInline {
if !sc.info.ParseInfo().IsInner {
if !sc.templ.ParseInfo.IsInner {
return sc, nil
}
}
@ -672,14 +675,19 @@ Loop:
sc.name = currItem.ValStr(source)
// Used to check if the template expects inner content.
templs := s.s.Tmpl().LookupVariants(sc.name)
if templs == nil {
// Used to check if the template expects inner content,
// so just pick one arbitrarily with the same name.
q := tplimpl.TemplateQuery{
Path: "",
Name: sc.name,
Category: tplimpl.CategoryShortcode,
Consider: nil,
}
templ := s.s.TemplateStore.LookupShortcode(q)
if templ == nil {
return nil, fmt.Errorf("%s: template for shortcode %q not found", errorPrefix, sc.name)
}
sc.info = templs[0].(tpl.Info)
sc.templs = templs
sc.templ = templ
case currItem.IsInlineShortcodeName():
sc.name = currItem.ValStr(source)
sc.isInline = true
@ -778,7 +786,7 @@ func expandShortcodeTokens(
return source, nil
}
func renderShortcodeWithPage(ctx context.Context, h tpl.TemplateHandler, tmpl tpl.Template, data *ShortcodeWithPage) (string, error) {
func renderShortcodeWithPage(ctx context.Context, h *tplimpl.TemplateStore, tmpl *tplimpl.TemplInfo, data *ShortcodeWithPage) (string, error) {
buffer := bp.GetBuffer()
defer bp.PutBuffer(buffer)

View file

@ -33,14 +33,14 @@ func TestExtractShortcodes(t *testing.T) {
b := newTestSitesBuilder(t).WithSimpleConfigFile()
b.WithTemplates(
"default/single.html", `EMPTY`,
"_internal/shortcodes/tag.html", `tag`,
"_internal/shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`,
"_internal/shortcodes/sc1.html", `sc1`,
"_internal/shortcodes/sc2.html", `sc2`,
"_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`,
"_internal/shortcodes/inner2.html", `{{.Inner}}`,
"_internal/shortcodes/inner3.html", `{{.Inner}}`,
"pages/single.html", `EMPTY`,
"shortcodes/tag.html", `tag`,
"shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`,
"shortcodes/sc1.html", `sc1`,
"shortcodes/sc2.html", `sc2`,
"shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`,
"shortcodes/inner2.html", `{{.Inner}}`,
"shortcodes/inner3.html", `{{.Inner}}`,
).WithContent("page.md", `---
title: "Shortcodes Galore!"
---
@ -57,10 +57,9 @@ title: "Shortcodes Galore!"
if s == nil {
return "<nil>"
}
var version int
if s.info != nil {
version = s.info.ParseInfo().Config.Version
if s.templ != nil {
version = s.templ.ParseInfo.Config.Version
}
return strReplacer.Replace(fmt.Sprintf("%s;inline:%t;closing:%t;inner:%v;params:%v;ordinal:%d;markup:%t;version:%d;pos:%d",
s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, version, s.pos))
@ -69,7 +68,7 @@ title: "Shortcodes Galore!"
regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) {
return func(c *qt.C, shortcode *shortcode, err error) {
c.Assert(err, qt.IsNil)
c.Assert(str(shortcode), qt.Matches, ".*"+re+".*")
c.Assert(str(shortcode), qt.Matches, ".*"+re+".*", qt.Commentf("%s", shortcode.name))
}
}
@ -888,6 +887,7 @@ outputs: ["html", "css", "csv", "json"]
"_default/single.json", "{{ .Content }}",
"shortcodes/myshort.html", `Short-HTML`,
"shortcodes/myshort.csv", `Short-CSV`,
"shortcodes/myshort.txt", `Short-TXT`,
)
b.Build(BuildCfg{})
@ -897,12 +897,12 @@ outputs: ["html", "css", "csv", "json"]
for i := range numPages {
b.AssertFileContent(fmt.Sprintf("public/page%d/index.html", i), "Short-HTML")
b.AssertFileContent(fmt.Sprintf("public/page%d/index.csv", i), "Short-CSV")
b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-HTML")
b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-CSV")
}
for i := range numPages {
b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-HTML")
b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-CSV")
}
}

View file

@ -47,7 +47,13 @@ import (
"github.com/gohugoio/hugo/langs/i18n"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/tpl/tplimplinit"
xmaps "golang.org/x/exp/maps"
// Loads the template funcs namespaces.
"golang.org/x/text/unicode/norm"
"github.com/gohugoio/hugo/common/paths"
@ -188,8 +194,8 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
BuildState: &deps.BuildState{
OnSignalRebuild: onSignalRebuild,
},
Counters: &deps.Counters{},
MemCache: memCache,
TemplateProvider: tplimpl.DefaultTemplateProvider,
TranslationProvider: i18n.NewTranslationProvider(),
WasmDispatchers: warpc.AllDispatchers(
warpc.Options{
@ -385,6 +391,34 @@ func newHugoSites(cfg deps.DepsCfg, d *deps.Deps, pageTrees *pageTrees, sites []
var prototype *deps.Deps
for i, s := range sites {
s.h = h
// The template store needs to be initialized after the h container is set on s.
if i == 0 {
templateStore, err := tplimpl.NewStore(
tplimpl.StoreOptions{
Fs: s.BaseFs.Layouts.Fs,
DefaultContentLanguage: s.Conf.DefaultContentLanguage(),
Watching: s.Conf.Watching(),
PathParser: s.Conf.PathParser(),
Metrics: d.Metrics,
OutputFormats: s.conf.OutputFormats.Config,
MediaTypes: s.conf.MediaTypes.Config,
DefaultOutputFormat: s.conf.DefaultOutputFormat,
TaxonomySingularPlural: s.conf.Taxonomies,
}, tplimpl.SiteOptions{
Site: s,
TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps),
})
if err != nil {
return nil, err
}
s.Deps.TemplateStore = templateStore
} else {
s.Deps.TemplateStore = prototype.TemplateStore.WithSiteOpts(
tplimpl.SiteOptions{
Site: s,
TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps),
})
}
if err := s.Deps.Compile(prototype); err != nil {
return nil, err
}
@ -464,7 +498,10 @@ func (s *Site) MainSections() []string {
// Returns a struct with some information about the build.
func (s *Site) Hugo() hugo.HugoInfo {
if s.h == nil || s.h.hugoInfo.Environment == "" {
if s.h == nil {
panic("site: hugo: h not initialized")
}
if s.h.hugoInfo.Environment == "" {
panic("site: hugo: hugoInfo not initialized")
}
return s.h.hugoInfo
@ -797,7 +834,7 @@ func (s *Site) initRenderFormats() {
s.renderFormats = formats
}
func (s *Site) GetRelatedDocsHandler() *page.RelatedDocsHandler {
func (s *Site) GetInternalRelatedDocsHandler() *page.RelatedDocsHandler {
return s.relatedDocsHandler
}
@ -923,19 +960,24 @@ type WhatChanged struct {
mu sync.Mutex
needsPagesAssembly bool
identitySet identity.Identities
ids map[identity.Identity]bool
}
func (w *WhatChanged) init() {
if w.ids == nil {
w.ids = make(map[identity.Identity]bool)
}
}
func (w *WhatChanged) Add(ids ...identity.Identity) {
w.mu.Lock()
defer w.mu.Unlock()
if w.identitySet == nil {
w.identitySet = make(identity.Identities)
}
w.init()
for _, id := range ids {
w.identitySet[id] = true
w.ids[id] = true
}
}
@ -946,20 +988,20 @@ func (w *WhatChanged) Clear() {
}
func (w *WhatChanged) clear() {
w.identitySet = identity.Identities{}
w.ids = nil
}
func (w *WhatChanged) Changes() []identity.Identity {
if w == nil || w.identitySet == nil {
if w == nil || w.ids == nil {
return nil
}
return w.identitySet.AsSlice()
return xmaps.Keys(w.ids)
}
func (w *WhatChanged) Drain() []identity.Identity {
w.mu.Lock()
defer w.mu.Unlock()
ids := w.identitySet.AsSlice()
ids := w.Changes()
w.clear()
return ids
}
@ -1394,7 +1436,7 @@ const (
pageDependencyScopeGlobal
)
func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ tpl.Template) error {
func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ *tplimpl.TemplInfo) error {
s.h.buildCounters.pageRenderCounter.Add(1)
renderBuffer := bp.GetBuffer()
defer bp.PutBuffer(renderBuffer)
@ -1453,8 +1495,8 @@ var infoOnMissingLayout = map[string]bool{
// hookRendererTemplate is the canonical implementation of all hooks.ITEMRenderer,
// where ITEM is the thing being hooked.
type hookRendererTemplate struct {
templateHandler tpl.TemplateHandler
templ tpl.Template
templateHandler *tplimpl.TemplateStore
templ *tplimpl.TemplInfo
resolvePosition func(ctx any) text.Position
}
@ -1490,7 +1532,7 @@ func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool {
return false
}
func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ tpl.Template) (err error) {
func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ *tplimpl.TemplInfo) (err error) {
if templ == nil {
s.logMissingLayout(name, "", "", outputFormat)
return nil
@ -1500,7 +1542,7 @@ func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string,
panic("nil context")
}
if err = s.Tmpl().ExecuteWithContext(ctx, templ, w, d); err != nil {
if err = s.GetTemplateStore().ExecuteWithContext(ctx, templ, w, d); err != nil {
filename := name
if p, ok := d.(*pageState); ok {
filename = p.String()

View file

@ -27,6 +27,7 @@ func createDefaultOutputFormats(allFormats output.Formats) map[string]output.For
htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name)
robotsOut, _ := allFormats.GetByName(output.RobotsTxtFormat.Name)
sitemapOut, _ := allFormats.GetByName(output.SitemapFormat.Name)
httpStatus404Out, _ := allFormats.GetByName(output.HTTPStatus404HTMLFormat.Name)
defaultListTypes := output.Formats{htmlOut}
if rssFound {
@ -42,7 +43,7 @@ func createDefaultOutputFormats(allFormats output.Formats) map[string]output.For
// Below are for consistency. They are currently not used during rendering.
kinds.KindSitemap: {sitemapOut},
kinds.KindRobotsTXT: {robotsOut},
kinds.KindStatus404: {htmlOut},
kinds.KindStatus404: {httpStatus404Out},
}
// May be disabled

View file

@ -387,7 +387,7 @@ func TestCreateSiteOutputFormats(t *testing.T) {
c.Assert(outputs[kinds.KindRSS], deepEqualsOutputFormats, output.Formats{output.RSSFormat})
c.Assert(outputs[kinds.KindSitemap], deepEqualsOutputFormats, output.Formats{output.SitemapFormat})
c.Assert(outputs[kinds.KindRobotsTXT], deepEqualsOutputFormats, output.Formats{output.RobotsTxtFormat})
c.Assert(outputs[kinds.KindStatus404], deepEqualsOutputFormats, output.Formats{output.HTMLFormat})
c.Assert(outputs[kinds.KindStatus404], deepEqualsOutputFormats, output.Formats{output.HTTPStatus404HTMLFormat})
})
// Issue #4528
@ -481,6 +481,7 @@ permalinkable = true
[outputFormats.nobase]
mediaType = "application/json"
permalinkable = true
isPlainText = true
`

View file

@ -23,9 +23,9 @@ import (
"github.com/bep/logg"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugolib/doctree"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/resources/kinds"
"github.com/gohugoio/hugo/resources/page"
@ -57,7 +57,7 @@ func (s siteRenderContext) shouldRenderStandalonePage(kind string) bool {
return s.outIdx == 0
}
if kind == kinds.KindStatus404 {
if kind == kinds.KindTemporary || kind == kinds.KindStatus404 {
// 1 for all output formats
return s.outIdx == 0
}
@ -168,7 +168,7 @@ func pageRenderer(
s.Log.Trace(
func() string {
return fmt.Sprintf("rendering outputFormat %q kind %q using layout %q to %q", p.pageOutput.f.Name, p.Kind(), templ.Name(), targetPath)
return fmt.Sprintf("rendering outputFormat %q kind %q using layout %q to %q", p.pageOutput.f.Name, p.Kind(), templ.Template.Name(), targetPath)
},
)
@ -225,7 +225,7 @@ func (s *Site) logMissingLayout(name, layout, kind, outputFormat string) {
}
// renderPaginator must be run after the owning Page has been rendered.
func (s *Site) renderPaginator(p *pageState, templ tpl.Template) error {
func (s *Site) renderPaginator(p *pageState, templ *tplimpl.TemplInfo) error {
paginatePath := s.Conf.Pagination().Path
d := p.targetPathDescriptor

View file

@ -978,8 +978,7 @@ func TestRefLinking(t *testing.T) {
{".", "", true, "/level2/level3/"},
{"./", "", true, "/level2/level3/"},
// try to confuse parsing
{"embedded.dot.md", "", true, "/level2/level3/embedded.dot/"},
{"embedded.dot.md", "", true, "/level2/level3/embedded/"},
// test empty link, as well as fragment only link
{"", "", true, ""},

View file

@ -76,6 +76,8 @@ func TestTaxonomiesWithAndWithoutContentFile(t *testing.T) {
}
func doTestTaxonomiesWithAndWithoutContentFile(t *testing.T, uglyURLs bool) {
t.Helper()
siteConfig := `
baseURL = "http://example.com/blog"
titleCaseStyle = "firstupper"

View file

@ -26,6 +26,8 @@ import (
"github.com/gohugoio/hugo/hugofs"
)
// TODO(bep) keep this until we release v0.146.0 as a security against breaking changes, but it's rather messy and mostly duplicate of
// tests in the tplimpl package, so eventually just remove it.
func TestTemplateLookupOrder(t *testing.T) {
var (
fs *hugofs.Fs
@ -185,6 +187,9 @@ func TestTemplateLookupOrder(t *testing.T) {
} {
this := this
if this.name != "Variant 1" {
continue
}
t.Run(this.name, func(t *testing.T) {
// TODO(bep) there are some function vars need to pull down here to enable => t.Parallel()
cfg, fs = newTestCfg()
@ -200,7 +205,7 @@ Some content
}
buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{})
// helpers.PrintFs(s.BaseFs.Layouts.Fs, "", os.Stdout)
// s.TemplateStore.PrintDebug("", 0, os.Stdout)
this.assert(t)
})
@ -270,11 +275,11 @@ func TestTemplateNoBasePlease(t *testing.T) {
b := newTestSitesBuilder(t).WithSimpleConfigFile()
b.WithTemplates("_default/list.html", `
{{ define "main" }}
Bonjour
{{ end }}
{{ define "main" }}
Bonjour
{{ end }}
{{ printf "list" }}
{{ printf "list" }}
`)
@ -344,33 +349,36 @@ title: %s
b.AssertFileContent("public/p1/index.html", `Single: P1`)
})
t.Run("baseof", func(t *testing.T) {
t.Parallel()
b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
{
}
}
b.WithTemplatesAdded(
"index.html", `{{ define "main" }}Main Home En{{ end }}`,
"index.fr.html", `{{ define "main" }}Main Home Fr{{ end }}`,
"baseof.html", `Baseof en: {{ block "main" . }}main block{{ end }}`,
"baseof.fr.html", `Baseof fr: {{ block "main" . }}main block{{ end }}`,
"mysection/baseof.html", `Baseof mysection: {{ block "main" . }}mysection block{{ end }}`,
"_default/single.html", `{{ define "main" }}Main Default Single{{ end }}`,
"_default/list.html", `{{ define "main" }}Main Default List{{ end }}`,
)
func TestTemplateLookupSitBaseOf(t *testing.T) {
t.Parallel()
b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
b.WithContent("mysection/p1.md", `---
b.WithTemplatesAdded(
"index.html", `{{ define "main" }}Main Home En{{ end }}`,
"index.fr.html", `{{ define "main" }}Main Home Fr{{ end }}`,
"baseof.html", `Baseof en: {{ block "main" . }}main block{{ end }}`,
"baseof.fr.html", `Baseof fr: {{ block "main" . }}main block{{ end }}`,
"mysection/baseof.html", `Baseof mysection: {{ block "main" . }}mysection block{{ end }}`,
"_default/single.html", `{{ define "main" }}Main Default Single{{ end }}`,
"_default/list.html", `{{ define "main" }}Main Default List{{ end }}`,
)
b.WithContent("mysection/p1.md", `---
title: My Page
---
`)
b.CreateSites().Build(BuildCfg{})
b.CreateSites().Build(BuildCfg{})
b.AssertFileContent("public/en/index.html", `Baseof en: Main Home En`)
b.AssertFileContent("public/fr/index.html", `Baseof fr: Main Home Fr`)
b.AssertFileContent("public/en/mysection/index.html", `Baseof mysection: Main Default List`)
b.AssertFileContent("public/en/mysection/p1/index.html", `Baseof mysection: Main Default Single`)
})
b.AssertFileContent("public/en/index.html", `Baseof en: Main Home En`)
b.AssertFileContent("public/fr/index.html", `Baseof fr: Main Home Fr`)
b.AssertFileContent("public/en/mysection/index.html", `Baseof mysection: Main Default List`)
b.AssertFileContent("public/en/mysection/p1/index.html", `Baseof mysection: Main Default Single`)
}
func TestTemplateFuncs(t *testing.T) {
@ -707,6 +715,7 @@ a: {{ $a }}
b.AssertFileContent("public/index.html", `a: [a b c]`)
}
// Legacy behavior for internal templates.
func TestOverrideInternalTemplate(t *testing.T) {
files := `
-- hugo.toml --

View file

@ -509,6 +509,10 @@ func probablyEq(a, b Identity) bool {
return true
}
if a2, ok := a.(compare.ProbablyEqer); ok && a2.ProbablyEq(b) {
return true
}
if a2, ok := a.(IsProbablyDependentProvider); ok {
return a2.IsProbablyDependent(b)
}

View file

@ -43,7 +43,7 @@ import (
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/resources/resource_factories/create"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
)
@ -192,7 +192,7 @@ type BatcherClient struct {
d *deps.Deps
once sync.Once
runnerTemplate tpl.Template
runnerTemplate *tplimpl.TemplInfo
createClient *create.Client
buildClient *BuildClient
@ -208,7 +208,7 @@ func (c *BatcherClient) New(id string) (js.Batcher, error) {
c.once.Do(func() {
// We should fix the initialization order here (or use the Go template package directly), but we need to wait
// for the Hugo templates to be ready.
tmpl, err := c.d.TextTmpl().Parse("batch-esm-runner", runnerTemplateStr)
tmpl, err := c.d.TemplateStore.TextParse("batch-esm-runner", runnerTemplateStr)
if err != nil {
initErr = err
return
@ -287,7 +287,7 @@ func (c *BatcherClient) Store() *maps.Cache[string, js.Batcher] {
func (c *BatcherClient) buildBatchGroup(ctx context.Context, t *batchGroupTemplateContext) (resource.Resource, string, error) {
var buf bytes.Buffer
if err := c.d.Tmpl().ExecuteWithContext(ctx, c.runnerTemplate, &buf, t); err != nil {
if err := c.d.GetTemplateStore().ExecuteWithContext(ctx, c.runnerTemplate, &buf, t); err != nil {
return nil, "", err
}

View file

@ -23,8 +23,6 @@ import (
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config/testconfig"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/resources/page"
"github.com/spf13/afero"
@ -472,7 +470,6 @@ func prepareTranslationProvider(t testing.TB, test i18nTest, cfg config.Provider
func prepareDeps(afs afero.Fs, cfg config.Provider) (*deps.Deps, *TranslationProvider) {
d := testconfig.GetTestDeps(afs, cfg)
translationProvider := NewTranslationProvider()
d.TemplateProvider = tplimpl.DefaultTemplateProvider
d.TranslationProvider = translationProvider
d.Site = page.NewDummyHugoSite(d.Conf)
if err := d.Compile(nil); err != nil {

View file

@ -69,7 +69,7 @@ fmt.Println("Hello, World!");
## Golang Code
§§§golang
§§§go
fmt.Println("Hello, Golang!");
§§§
@ -97,14 +97,14 @@ Go Language: go|
Go Code: fmt.Println("Hello, World!");
Go Code: fmt.Println("Hello, Golang!");
Go Language: golang|
Go Language: go|
`,
"Goat SVG:<svg class='diagram' xmlns='http://www.w3.org/2000/svg' version='1.1' height='25' width='40'",
"Goat Attribute: 600|",
"<h2 id=\"go-code\">Go Code</h2>\nGo Code: fmt.Println(\"Hello, World!\");\n|\nGo Language: go|",
"<h2 id=\"golang-code\">Golang Code</h2>\nGo Code: fmt.Println(\"Hello, Golang!\");\n|\nGo Language: golang|",
"<h2 id=\"golang-code\">Golang Code</h2>\nGo Code: fmt.Println(\"Hello, Golang!\");\n|\nGo Language: go|",
"<h2 id=\"bash-code\">Bash Code</h2>\n<div class=\"highlight blue\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"ln\">32</span><span class=\"cl\"><span class=\"nb\">echo</span> <span class=\"s2\">&#34;l1&#34;</span><span class=\"p\">;</span>\n</span></span><span class=\"line hl\"><span class=\"ln\">33</span>",
)
}

View file

@ -5,6 +5,7 @@ type BuiltinTypes struct {
CSSType Type
SCSSType Type
SASSType Type
GotmplType Type
CSVType Type
HTMLType Type
JavascriptType Type
@ -60,6 +61,7 @@ var Builtin = BuiltinTypes{
CSSType: Type{Type: "text/css"},
SCSSType: Type{Type: "text/x-scss"},
SASSType: Type{Type: "text/x-sass"},
GotmplType: Type{Type: "text/x-gotmpl"},
CSVType: Type{Type: "text/csv"},
HTMLType: Type{Type: "text/html"},
JavascriptType: Type{Type: "text/javascript"},
@ -121,6 +123,7 @@ var defaultMediaTypesConfig = map[string]any{
"text/typescript": map[string]any{"suffixes": []string{"ts"}},
"text/tsx": map[string]any{"suffixes": []string{"tsx"}},
"text/jsx": map[string]any{"suffixes": []string{"jsx"}},
"text/x-gotmpl": map[string]any{"suffixes": []string{"gotmpl"}},
"application/json": map[string]any{"suffixes": []string{"json"}},
"application/manifest+json": map[string]any{"suffixes": []string{"webmanifest"}},

View file

@ -17,6 +17,7 @@ import (
"fmt"
"path/filepath"
"reflect"
"slices"
"sort"
"strings"
@ -26,7 +27,6 @@ import (
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
"slices"
)
// DefaultTypes is the default media types supported by Hugo.
@ -271,4 +271,7 @@ var DefaultPathParser = &paths.PathParser{
IsContentExt: func(ext string) bool {
panic("not supported")
},
IsOutputFormat: func(name, ext string) bool {
panic("DefaultPathParser: not supported")
},
}

View file

@ -151,5 +151,5 @@ func TestDefaultTypes(t *testing.T) {
}
c.Assert(len(DefaultTypes), qt.Equals, 40)
c.Assert(len(DefaultTypes), qt.Equals, 41)
}

View file

@ -282,7 +282,7 @@ func (t Types) BySuffix(suffix string) []Type {
suffix = t.normalizeSuffix(suffix)
var types []Type
for _, tt := range t {
if tt.hasSuffix(suffix) {
if tt.HasSuffix(suffix) {
types = append(types, tt)
}
}
@ -293,7 +293,7 @@ func (t Types) BySuffix(suffix string) []Type {
func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
if tt.hasSuffix(suffix) {
if tt.HasSuffix(suffix) {
return tt, SuffixInfo{
FullSuffix: tt.Delimiter + suffix,
Suffix: suffix,
@ -310,7 +310,7 @@ func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
if tt.hasSuffix(suffix) {
if tt.HasSuffix(suffix) {
if found {
// ambiguous
found = false
@ -330,14 +330,14 @@ func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
func (t Types) IsTextSuffix(suffix string) bool {
suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
if tt.hasSuffix(suffix) {
if tt.HasSuffix(suffix) {
return tt.IsText()
}
}
return false
}
func (m Type) hasSuffix(suffix string) bool {
func (m Type) HasSuffix(suffix string) bool {
return strings.Contains(","+m.SuffixesCSV+",", ","+suffix+",")
}

View file

@ -1,12 +1,10 @@
package output
import (
"strings"
// "fmt"
"github.com/gohugoio/hugo/docshelper"
"github.com/gohugoio/hugo/output/layouts"
)
// This is is just some helpers used to create some JSON used in the Hugo docs.
@ -14,90 +12,12 @@ func init() {
docsProvider := func() docshelper.DocProvider {
return docshelper.DocProvider{
"output": map[string]any{
"layouts": createLayoutExamples(),
// TODO(bep), maybe revisit this later, but I hope this isn't needed.
// "layouts": createLayoutExamples(),
"layouts": map[string]any{},
},
}
}
docshelper.AddDocProviderFunc(docsProvider)
}
func createLayoutExamples() any {
type Example struct {
Example string
Kind string
OutputFormat string
Suffix string
Layouts []string `json:"Template Lookup Order"`
}
var (
basicExamples []Example
demoLayout = "demolayout"
demoType = "demotype"
)
for _, example := range []struct {
name string
d layouts.LayoutDescriptor
}{
// Taxonomy layouts.LayoutDescriptor={categories category taxonomy en false Type Section
{"Single page in \"posts\" section", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
{"Base template for single page in \"posts\" section", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
{"Single page in \"posts\" section with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
{"Base template for single page in \"posts\" section with layout set to \"demolayout\"", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
{"AMP single page in \"posts\" section", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "amp", Suffix: "html"}},
{"AMP single page in \"posts\" section, French language", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}},
// Typeless pages get "page" as type
{"Home page", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
{"Base template for home page", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
{"Home page with type set to \"demotype\"", layouts.LayoutDescriptor{Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}},
{"Base template for home page with type set to \"demotype\"", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}},
{"Home page with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "home", Type: "page", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
{"AMP home, French language", layouts.LayoutDescriptor{Kind: "home", Type: "page", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}},
{"JSON home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "json", Suffix: "json"}},
{"RSS home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "rss", Suffix: "xml"}},
{"Section list for \"posts\"", layouts.LayoutDescriptor{Kind: "section", Type: "posts", Section: "posts", OutputFormatName: "html", Suffix: "html"}},
{"Section list for \"posts\" with type set to \"blog\"", layouts.LayoutDescriptor{Kind: "section", Type: "blog", Section: "posts", OutputFormatName: "html", Suffix: "html"}},
{"Section list for \"posts\" with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "section", Layout: demoLayout, Section: "posts", OutputFormatName: "html", Suffix: "html"}},
{"Section list for \"posts\"", layouts.LayoutDescriptor{Kind: "section", Type: "posts", OutputFormatName: "rss", Suffix: "xml"}},
{"Taxonomy list for \"categories\"", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}},
{"Taxonomy list for \"categories\"", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "xml"}},
{"Term list for \"categories\"", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}},
{"Term list for \"categories\"", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "xml"}},
} {
l := layouts.NewLayoutHandler()
layouts, _ := l.For(example.d)
basicExamples = append(basicExamples, Example{
Example: example.name,
Kind: example.d.Kind,
OutputFormat: example.d.OutputFormatName,
Suffix: example.d.Suffix,
Layouts: makeLayoutsPresentable(layouts),
})
}
return basicExamples
}
func makeLayoutsPresentable(l []string) []string {
var filtered []string
for _, ll := range l {
if strings.Contains(ll, "page/") {
// This is a valid lookup, but it's more confusing than useful.
continue
}
ll = "layouts/" + strings.TrimPrefix(ll, "_text/")
if !strings.Contains(ll, "indexes") {
filtered = append(filtered, ll)
}
}
return filtered
}

View file

@ -1,336 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// 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 layouts
import (
"strings"
"sync"
)
// These may be used as content sections with potential conflicts. Avoid that.
var reservedSections = map[string]bool{
"shortcodes": true,
"partials": true,
}
// LayoutDescriptor describes how a layout should be chosen. This is
// typically built from a Page.
type LayoutDescriptor struct {
Type string
Section string
// E.g. "page", but also used for the _markup render kinds, e.g. "render-image".
Kind string
// Comma-separated list of kind variants, e.g. "go,json" as variants which would find "render-codeblock-go.html"
KindVariants string
Lang string
Layout string
// LayoutOverride indicates what we should only look for the above layout.
LayoutOverride bool
// From OutputFormat and MediaType.
OutputFormatName string
Suffix string
RenderingHook bool
Baseof bool
}
func (d LayoutDescriptor) isList() bool {
return !d.RenderingHook && d.Kind != "page" && d.Kind != "404" && d.Kind != "sitemap" && d.Kind != "sitemapindex"
}
// LayoutHandler calculates the layout template to use to render a given output type.
type LayoutHandler struct {
mu sync.RWMutex
cache map[LayoutDescriptor][]string
}
// NewLayoutHandler creates a new LayoutHandler.
func NewLayoutHandler() *LayoutHandler {
return &LayoutHandler{cache: make(map[LayoutDescriptor][]string)}
}
// For returns a layout for the given LayoutDescriptor and options.
// Layouts are rendered and cached internally.
func (l *LayoutHandler) For(d LayoutDescriptor) ([]string, error) {
// We will get lots of requests for the same layouts, so avoid recalculations.
l.mu.RLock()
if cacheVal, found := l.cache[d]; found {
l.mu.RUnlock()
return cacheVal, nil
}
l.mu.RUnlock()
layouts := resolvePageTemplate(d)
layouts = uniqueStringsReuse(layouts)
l.mu.Lock()
l.cache[d] = layouts
l.mu.Unlock()
return layouts, nil
}
type layoutBuilder struct {
layoutVariations []string
typeVariations []string
d LayoutDescriptor
// f Format
}
func (l *layoutBuilder) addLayoutVariations(vars ...string) {
for _, layoutVar := range vars {
if l.d.Baseof && layoutVar != "baseof" {
l.layoutVariations = append(l.layoutVariations, layoutVar+"-baseof")
continue
}
if !l.d.RenderingHook && !l.d.Baseof && l.d.LayoutOverride && layoutVar != l.d.Layout {
continue
}
l.layoutVariations = append(l.layoutVariations, layoutVar)
}
}
func (l *layoutBuilder) addTypeVariations(vars ...string) {
for _, typeVar := range vars {
if !reservedSections[typeVar] {
if l.d.RenderingHook {
typeVar = typeVar + renderingHookRoot
}
l.typeVariations = append(l.typeVariations, typeVar)
}
}
}
func (l *layoutBuilder) addSectionType() {
if l.d.Section != "" {
l.addTypeVariations(l.d.Section)
}
}
func (l *layoutBuilder) addKind() {
l.addLayoutVariations(l.d.Kind)
l.addTypeVariations(l.d.Kind)
}
const renderingHookRoot = "/_markup"
func resolvePageTemplate(d LayoutDescriptor) []string {
b := &layoutBuilder{d: d}
if !d.RenderingHook && d.Layout != "" {
b.addLayoutVariations(d.Layout)
}
if d.Type != "" {
b.addTypeVariations(d.Type)
}
if d.RenderingHook {
if d.KindVariants != "" {
// Add the more specific variants first.
for _, variant := range strings.Split(d.KindVariants, ",") {
b.addLayoutVariations(d.Kind + "-" + variant)
}
}
b.addLayoutVariations(d.Kind)
b.addSectionType()
}
switch d.Kind {
case "page":
b.addLayoutVariations("single")
b.addSectionType()
case "home":
b.addLayoutVariations("index", "home")
// Also look in the root
b.addTypeVariations("")
case "section":
if d.Section != "" {
b.addLayoutVariations(d.Section)
}
b.addSectionType()
b.addKind()
case "term":
b.addKind()
if d.Section != "" {
b.addLayoutVariations(d.Section)
}
b.addLayoutVariations("taxonomy")
b.addTypeVariations("taxonomy")
b.addSectionType()
case "taxonomy":
if d.Section != "" {
b.addLayoutVariations(d.Section + ".terms")
}
b.addSectionType()
b.addLayoutVariations("terms")
// For legacy reasons this is deliberately put last.
b.addKind()
case "404":
b.addLayoutVariations("404")
b.addTypeVariations("")
case "robotstxt":
b.addLayoutVariations("robots")
b.addTypeVariations("")
case "sitemap":
b.addLayoutVariations("sitemap")
b.addTypeVariations("")
case "sitemapindex":
b.addLayoutVariations("sitemapindex")
b.addTypeVariations("")
}
isRSS := d.OutputFormatName == "rss"
if !d.RenderingHook && !d.Baseof && isRSS {
// The historic and common rss.xml case
b.addLayoutVariations("")
}
if d.Baseof || d.Kind != "404" {
// Most have _default in their lookup path
b.addTypeVariations("_default")
}
if d.isList() {
// Add the common list type
b.addLayoutVariations("list")
}
if d.Baseof {
b.addLayoutVariations("baseof")
}
layouts := b.resolveVariations()
if !d.RenderingHook && !d.Baseof && isRSS {
layouts = append(layouts, "_internal/_default/rss.xml")
}
switch d.Kind {
case "robotstxt":
layouts = append(layouts, "_internal/_default/robots.txt")
case "sitemap":
layouts = append(layouts, "_internal/_default/sitemap.xml")
case "sitemapindex":
layouts = append(layouts, "_internal/_default/sitemapindex.xml")
}
return layouts
}
func (l *layoutBuilder) resolveVariations() []string {
var layouts []string
var variations []string
name := strings.ToLower(l.d.OutputFormatName)
if l.d.Lang != "" {
// We prefer the most specific type before language.
variations = append(variations, []string{l.d.Lang + "." + name, name, l.d.Lang}...)
} else {
variations = append(variations, name)
}
variations = append(variations, "")
for _, typeVar := range l.typeVariations {
for _, variation := range variations {
for _, layoutVar := range l.layoutVariations {
if variation == "" && layoutVar == "" {
continue
}
s := constructLayoutPath(typeVar, layoutVar, variation, l.d.Suffix)
if s != "" {
layouts = append(layouts, s)
}
}
}
}
return layouts
}
// constructLayoutPath constructs a layout path given a type, layout,
// variations, and extension. The path constructed follows the pattern of
// type/layout.variations.extension. If any value is empty, it will be left out
// of the path construction.
//
// Path construction requires at least 2 of 3 out of layout, variations, and extension.
// If more than one of those is empty, an empty string is returned.
func constructLayoutPath(typ, layout, variations, extension string) string {
// we already know that layout and variations are not both empty because of
// checks in resolveVariants().
if extension == "" && (layout == "" || variations == "") {
return ""
}
// Commence valid path construction...
var (
p strings.Builder
needDot bool
)
if typ != "" {
p.WriteString(typ)
p.WriteString("/")
}
if layout != "" {
p.WriteString(layout)
needDot = true
}
if variations != "" {
if needDot {
p.WriteString(".")
}
p.WriteString(variations)
needDot = true
}
if extension != "" {
if needDot {
p.WriteString(".")
}
p.WriteString(extension)
}
return p.String()
}
// Inline this here so we can use tinygo to compile a wasm binary of this package.
func uniqueStringsReuse(s []string) []string {
result := s[:0]
for i, val := range s {
var seen bool
for j := range i {
if s[j] == val {
seen = true
break
}
}
if !seen {
result = append(result, val)
}
}
return result
}

View file

@ -1,982 +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 layouts
import (
"fmt"
"reflect"
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/kylelemons/godebug/diff"
)
func TestLayout(t *testing.T) {
c := qt.New(t)
for _, this := range []struct {
name string
layoutDescriptor LayoutDescriptor
layoutOverride string
expect []string
}{
{
"Home",
LayoutDescriptor{Kind: "home", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"index.amp.html",
"home.amp.html",
"list.amp.html",
"index.html",
"home.html",
"list.html",
"_default/index.amp.html",
"_default/home.amp.html",
"_default/list.amp.html",
"_default/index.html",
"_default/home.html",
"_default/list.html",
},
},
{
"Home baseof",
LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"index-baseof.amp.html",
"home-baseof.amp.html",
"list-baseof.amp.html",
"baseof.amp.html",
"index-baseof.html",
"home-baseof.html",
"list-baseof.html",
"baseof.html",
"_default/index-baseof.amp.html",
"_default/home-baseof.amp.html",
"_default/list-baseof.amp.html",
"_default/baseof.amp.html",
"_default/index-baseof.html",
"_default/home-baseof.html",
"_default/list-baseof.html",
"_default/baseof.html",
},
},
{
"Home, HTML",
LayoutDescriptor{Kind: "home", OutputFormatName: "html", Suffix: "html"},
"",
// We will eventually get to index.html. This looks stuttery, but makes the lookup logic easy to understand.
[]string{
"index.html.html",
"home.html.html",
"list.html.html",
"index.html",
"home.html",
"list.html",
"_default/index.html.html",
"_default/home.html.html",
"_default/list.html.html",
"_default/index.html",
"_default/home.html",
"_default/list.html",
},
},
{
"Home, HTML, baseof",
LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "html", Suffix: "html"},
"",
[]string{
"index-baseof.html.html",
"home-baseof.html.html",
"list-baseof.html.html",
"baseof.html.html",
"index-baseof.html",
"home-baseof.html",
"list-baseof.html",
"baseof.html",
"_default/index-baseof.html.html",
"_default/home-baseof.html.html",
"_default/list-baseof.html.html",
"_default/baseof.html.html",
"_default/index-baseof.html",
"_default/home-baseof.html",
"_default/list-baseof.html",
"_default/baseof.html",
},
},
{
"Home, french language",
LayoutDescriptor{Kind: "home", Lang: "fr", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"index.fr.amp.html",
"home.fr.amp.html",
"list.fr.amp.html",
"index.amp.html",
"home.amp.html",
"list.amp.html",
"index.fr.html",
"home.fr.html",
"list.fr.html",
"index.html",
"home.html",
"list.html",
"_default/index.fr.amp.html",
"_default/home.fr.amp.html",
"_default/list.fr.amp.html",
"_default/index.amp.html",
"_default/home.amp.html",
"_default/list.amp.html",
"_default/index.fr.html",
"_default/home.fr.html",
"_default/list.fr.html",
"_default/index.html",
"_default/home.html",
"_default/list.html",
},
},
{
"Home, no ext or delim",
LayoutDescriptor{Kind: "home", OutputFormatName: "nem", Suffix: ""},
"",
[]string{
"index.nem",
"home.nem",
"list.nem",
"_default/index.nem",
"_default/home.nem",
"_default/list.nem",
},
},
{
"Home, no ext",
LayoutDescriptor{Kind: "home", OutputFormatName: "nex", Suffix: ""},
"",
[]string{
"index.nex",
"home.nex",
"list.nex",
"_default/index.nex",
"_default/home.nex",
"_default/list.nex",
},
},
{
"Page, no ext or delim",
LayoutDescriptor{Kind: "page", OutputFormatName: "nem", Suffix: ""},
"",
[]string{"_default/single.nem"},
},
{
"Section",
LayoutDescriptor{Kind: "section", Section: "sect1", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"sect1/sect1.amp.html",
"sect1/section.amp.html",
"sect1/list.amp.html",
"sect1/sect1.html",
"sect1/section.html",
"sect1/list.html",
"section/sect1.amp.html",
"section/section.amp.html",
"section/list.amp.html",
"section/sect1.html",
"section/section.html",
"section/list.html",
"_default/sect1.amp.html",
"_default/section.amp.html",
"_default/list.amp.html",
"_default/sect1.html",
"_default/section.html",
"_default/list.html",
},
},
{
"Section, baseof",
LayoutDescriptor{Kind: "section", Section: "sect1", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"sect1/sect1-baseof.amp.html",
"sect1/section-baseof.amp.html",
"sect1/list-baseof.amp.html",
"sect1/baseof.amp.html",
"sect1/sect1-baseof.html",
"sect1/section-baseof.html",
"sect1/list-baseof.html",
"sect1/baseof.html",
"section/sect1-baseof.amp.html",
"section/section-baseof.amp.html",
"section/list-baseof.amp.html",
"section/baseof.amp.html",
"section/sect1-baseof.html",
"section/section-baseof.html",
"section/list-baseof.html",
"section/baseof.html",
"_default/sect1-baseof.amp.html",
"_default/section-baseof.amp.html",
"_default/list-baseof.amp.html",
"_default/baseof.amp.html",
"_default/sect1-baseof.html",
"_default/section-baseof.html",
"_default/list-baseof.html",
"_default/baseof.html",
},
},
{
"Section, baseof, French, AMP",
LayoutDescriptor{Kind: "section", Section: "sect1", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"sect1/sect1-baseof.fr.amp.html",
"sect1/section-baseof.fr.amp.html",
"sect1/list-baseof.fr.amp.html",
"sect1/baseof.fr.amp.html",
"sect1/sect1-baseof.amp.html",
"sect1/section-baseof.amp.html",
"sect1/list-baseof.amp.html",
"sect1/baseof.amp.html",
"sect1/sect1-baseof.fr.html",
"sect1/section-baseof.fr.html",
"sect1/list-baseof.fr.html",
"sect1/baseof.fr.html",
"sect1/sect1-baseof.html",
"sect1/section-baseof.html",
"sect1/list-baseof.html",
"sect1/baseof.html",
"section/sect1-baseof.fr.amp.html",
"section/section-baseof.fr.amp.html",
"section/list-baseof.fr.amp.html",
"section/baseof.fr.amp.html",
"section/sect1-baseof.amp.html",
"section/section-baseof.amp.html",
"section/list-baseof.amp.html",
"section/baseof.amp.html",
"section/sect1-baseof.fr.html",
"section/section-baseof.fr.html",
"section/list-baseof.fr.html",
"section/baseof.fr.html",
"section/sect1-baseof.html",
"section/section-baseof.html",
"section/list-baseof.html",
"section/baseof.html",
"_default/sect1-baseof.fr.amp.html",
"_default/section-baseof.fr.amp.html",
"_default/list-baseof.fr.amp.html",
"_default/baseof.fr.amp.html",
"_default/sect1-baseof.amp.html",
"_default/section-baseof.amp.html",
"_default/list-baseof.amp.html",
"_default/baseof.amp.html",
"_default/sect1-baseof.fr.html",
"_default/section-baseof.fr.html",
"_default/list-baseof.fr.html",
"_default/baseof.fr.html",
"_default/sect1-baseof.html",
"_default/section-baseof.html",
"_default/list-baseof.html",
"_default/baseof.html",
},
},
{
"Section with layout",
LayoutDescriptor{Kind: "section", Section: "sect1", Layout: "mylayout", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"sect1/mylayout.amp.html",
"sect1/sect1.amp.html",
"sect1/section.amp.html",
"sect1/list.amp.html",
"sect1/mylayout.html",
"sect1/sect1.html",
"sect1/section.html",
"sect1/list.html",
"section/mylayout.amp.html",
"section/sect1.amp.html",
"section/section.amp.html",
"section/list.amp.html",
"section/mylayout.html",
"section/sect1.html",
"section/section.html",
"section/list.html",
"_default/mylayout.amp.html",
"_default/sect1.amp.html",
"_default/section.amp.html",
"_default/list.amp.html",
"_default/mylayout.html",
"_default/sect1.html",
"_default/section.html",
"_default/list.html",
},
},
{
"Term, French, AMP",
LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"term/term.fr.amp.html",
"term/tags.fr.amp.html",
"term/taxonomy.fr.amp.html",
"term/list.fr.amp.html",
"term/term.amp.html",
"term/tags.amp.html",
"term/taxonomy.amp.html",
"term/list.amp.html",
"term/term.fr.html",
"term/tags.fr.html",
"term/taxonomy.fr.html",
"term/list.fr.html",
"term/term.html",
"term/tags.html",
"term/taxonomy.html",
"term/list.html",
"taxonomy/term.fr.amp.html",
"taxonomy/tags.fr.amp.html",
"taxonomy/taxonomy.fr.amp.html",
"taxonomy/list.fr.amp.html",
"taxonomy/term.amp.html",
"taxonomy/tags.amp.html",
"taxonomy/taxonomy.amp.html",
"taxonomy/list.amp.html",
"taxonomy/term.fr.html",
"taxonomy/tags.fr.html",
"taxonomy/taxonomy.fr.html",
"taxonomy/list.fr.html",
"taxonomy/term.html",
"taxonomy/tags.html",
"taxonomy/taxonomy.html",
"taxonomy/list.html",
"tags/term.fr.amp.html",
"tags/tags.fr.amp.html",
"tags/taxonomy.fr.amp.html",
"tags/list.fr.amp.html",
"tags/term.amp.html",
"tags/tags.amp.html",
"tags/taxonomy.amp.html",
"tags/list.amp.html",
"tags/term.fr.html",
"tags/tags.fr.html",
"tags/taxonomy.fr.html",
"tags/list.fr.html",
"tags/term.html",
"tags/tags.html",
"tags/taxonomy.html",
"tags/list.html",
"_default/term.fr.amp.html",
"_default/tags.fr.amp.html",
"_default/taxonomy.fr.amp.html",
"_default/list.fr.amp.html",
"_default/term.amp.html",
"_default/tags.amp.html",
"_default/taxonomy.amp.html",
"_default/list.amp.html",
"_default/term.fr.html",
"_default/tags.fr.html",
"_default/taxonomy.fr.html",
"_default/list.fr.html",
"_default/term.html",
"_default/tags.html",
"_default/taxonomy.html",
"_default/list.html",
},
},
{
"Term, baseof, French, AMP",
LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"term/term-baseof.fr.amp.html",
"term/tags-baseof.fr.amp.html",
"term/taxonomy-baseof.fr.amp.html",
"term/list-baseof.fr.amp.html",
"term/baseof.fr.amp.html",
"term/term-baseof.amp.html",
"term/tags-baseof.amp.html",
"term/taxonomy-baseof.amp.html",
"term/list-baseof.amp.html",
"term/baseof.amp.html",
"term/term-baseof.fr.html",
"term/tags-baseof.fr.html",
"term/taxonomy-baseof.fr.html",
"term/list-baseof.fr.html",
"term/baseof.fr.html",
"term/term-baseof.html",
"term/tags-baseof.html",
"term/taxonomy-baseof.html",
"term/list-baseof.html",
"term/baseof.html",
"taxonomy/term-baseof.fr.amp.html",
"taxonomy/tags-baseof.fr.amp.html",
"taxonomy/taxonomy-baseof.fr.amp.html",
"taxonomy/list-baseof.fr.amp.html",
"taxonomy/baseof.fr.amp.html",
"taxonomy/term-baseof.amp.html",
"taxonomy/tags-baseof.amp.html",
"taxonomy/taxonomy-baseof.amp.html",
"taxonomy/list-baseof.amp.html",
"taxonomy/baseof.amp.html",
"taxonomy/term-baseof.fr.html",
"taxonomy/tags-baseof.fr.html",
"taxonomy/taxonomy-baseof.fr.html",
"taxonomy/list-baseof.fr.html",
"taxonomy/baseof.fr.html",
"taxonomy/term-baseof.html",
"taxonomy/tags-baseof.html",
"taxonomy/taxonomy-baseof.html",
"taxonomy/list-baseof.html",
"taxonomy/baseof.html",
"tags/term-baseof.fr.amp.html",
"tags/tags-baseof.fr.amp.html",
"tags/taxonomy-baseof.fr.amp.html",
"tags/list-baseof.fr.amp.html",
"tags/baseof.fr.amp.html",
"tags/term-baseof.amp.html",
"tags/tags-baseof.amp.html",
"tags/taxonomy-baseof.amp.html",
"tags/list-baseof.amp.html",
"tags/baseof.amp.html",
"tags/term-baseof.fr.html",
"tags/tags-baseof.fr.html",
"tags/taxonomy-baseof.fr.html",
"tags/list-baseof.fr.html",
"tags/baseof.fr.html",
"tags/term-baseof.html",
"tags/tags-baseof.html",
"tags/taxonomy-baseof.html",
"tags/list-baseof.html",
"tags/baseof.html",
"_default/term-baseof.fr.amp.html",
"_default/tags-baseof.fr.amp.html",
"_default/taxonomy-baseof.fr.amp.html",
"_default/list-baseof.fr.amp.html",
"_default/baseof.fr.amp.html",
"_default/term-baseof.amp.html",
"_default/tags-baseof.amp.html",
"_default/taxonomy-baseof.amp.html",
"_default/list-baseof.amp.html",
"_default/baseof.amp.html",
"_default/term-baseof.fr.html",
"_default/tags-baseof.fr.html",
"_default/taxonomy-baseof.fr.html",
"_default/list-baseof.fr.html",
"_default/baseof.fr.html",
"_default/term-baseof.html",
"_default/tags-baseof.html",
"_default/taxonomy-baseof.html",
"_default/list-baseof.html",
"_default/baseof.html",
},
},
{
"Term",
LayoutDescriptor{Kind: "term", Section: "tags", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"term/term.amp.html",
"term/tags.amp.html",
"term/taxonomy.amp.html",
"term/list.amp.html",
"term/term.html",
"term/tags.html",
"term/taxonomy.html",
"term/list.html",
"taxonomy/term.amp.html",
"taxonomy/tags.amp.html",
"taxonomy/taxonomy.amp.html",
"taxonomy/list.amp.html",
"taxonomy/term.html",
"taxonomy/tags.html",
"taxonomy/taxonomy.html",
"taxonomy/list.html",
"tags/term.amp.html",
"tags/tags.amp.html",
"tags/taxonomy.amp.html",
"tags/list.amp.html",
"tags/term.html",
"tags/tags.html",
"tags/taxonomy.html",
"tags/list.html",
"_default/term.amp.html",
"_default/tags.amp.html",
"_default/taxonomy.amp.html",
"_default/list.amp.html",
"_default/term.html",
"_default/tags.html",
"_default/taxonomy.html",
"_default/list.html",
},
},
{
"Taxonomy",
LayoutDescriptor{Kind: "taxonomy", Section: "categories", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"categories/categories.terms.amp.html",
"categories/terms.amp.html",
"categories/taxonomy.amp.html",
"categories/list.amp.html",
"categories/categories.terms.html",
"categories/terms.html",
"categories/taxonomy.html",
"categories/list.html",
"taxonomy/categories.terms.amp.html",
"taxonomy/terms.amp.html",
"taxonomy/taxonomy.amp.html",
"taxonomy/list.amp.html",
"taxonomy/categories.terms.html",
"taxonomy/terms.html",
"taxonomy/taxonomy.html",
"taxonomy/list.html",
"_default/categories.terms.amp.html",
"_default/terms.amp.html",
"_default/taxonomy.amp.html",
"_default/list.amp.html",
"_default/categories.terms.html",
"_default/terms.html",
"_default/taxonomy.html",
"_default/list.html",
},
},
{
"Page",
LayoutDescriptor{Kind: "page", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"_default/single.amp.html",
"_default/single.html",
},
},
{
"Page, baseof",
LayoutDescriptor{Kind: "page", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"_default/single-baseof.amp.html",
"_default/baseof.amp.html",
"_default/single-baseof.html",
"_default/baseof.html",
},
},
{
"Page with layout",
LayoutDescriptor{Kind: "page", Layout: "mylayout", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"_default/mylayout.amp.html",
"_default/single.amp.html",
"_default/mylayout.html",
"_default/single.html",
},
},
{
"Page with layout, baseof",
LayoutDescriptor{Kind: "page", Layout: "mylayout", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"_default/mylayout-baseof.amp.html",
"_default/single-baseof.amp.html",
"_default/baseof.amp.html",
"_default/mylayout-baseof.html",
"_default/single-baseof.html",
"_default/baseof.html",
},
},
{
"Page with layout and type",
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"myttype/mylayout.amp.html",
"myttype/single.amp.html",
"myttype/mylayout.html",
"myttype/single.html",
"_default/mylayout.amp.html",
"_default/single.amp.html",
"_default/mylayout.html",
"_default/single.html",
},
},
{
"Page baseof with layout and type",
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"myttype/mylayout-baseof.amp.html",
"myttype/single-baseof.amp.html",
"myttype/baseof.amp.html",
"myttype/mylayout-baseof.html",
"myttype/single-baseof.html",
"myttype/baseof.html",
"_default/mylayout-baseof.amp.html",
"_default/single-baseof.amp.html",
"_default/baseof.amp.html",
"_default/mylayout-baseof.html",
"_default/single-baseof.html",
"_default/baseof.html",
},
},
{
"Page baseof with layout and type in French",
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"myttype/mylayout-baseof.fr.amp.html",
"myttype/single-baseof.fr.amp.html",
"myttype/baseof.fr.amp.html",
"myttype/mylayout-baseof.amp.html",
"myttype/single-baseof.amp.html",
"myttype/baseof.amp.html",
"myttype/mylayout-baseof.fr.html",
"myttype/single-baseof.fr.html",
"myttype/baseof.fr.html",
"myttype/mylayout-baseof.html",
"myttype/single-baseof.html",
"myttype/baseof.html",
"_default/mylayout-baseof.fr.amp.html",
"_default/single-baseof.fr.amp.html",
"_default/baseof.fr.amp.html",
"_default/mylayout-baseof.amp.html",
"_default/single-baseof.amp.html",
"_default/baseof.amp.html",
"_default/mylayout-baseof.fr.html",
"_default/single-baseof.fr.html",
"_default/baseof.fr.html",
"_default/mylayout-baseof.html",
"_default/single-baseof.html",
"_default/baseof.html",
},
},
{
"Page with layout and type with subtype",
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype/mysubtype", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"myttype/mysubtype/mylayout.amp.html",
"myttype/mysubtype/single.amp.html",
"myttype/mysubtype/mylayout.html",
"myttype/mysubtype/single.html",
"_default/mylayout.amp.html",
"_default/single.amp.html",
"_default/mylayout.html",
"_default/single.html",
},
},
// RSS
{
"RSS Home",
LayoutDescriptor{Kind: "home", OutputFormatName: "rss", Suffix: "xml"},
"",
[]string{
"index.rss.xml",
"home.rss.xml",
"rss.xml",
"list.rss.xml",
"index.xml",
"home.xml",
"list.xml",
"_default/index.rss.xml",
"_default/home.rss.xml",
"_default/rss.xml",
"_default/list.rss.xml",
"_default/index.xml",
"_default/home.xml",
"_default/list.xml",
"_internal/_default/rss.xml",
},
},
{
"RSS Home, baseof",
LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "rss", Suffix: "xml"},
"",
[]string{
"index-baseof.rss.xml",
"home-baseof.rss.xml",
"list-baseof.rss.xml",
"baseof.rss.xml",
"index-baseof.xml",
"home-baseof.xml",
"list-baseof.xml",
"baseof.xml",
"_default/index-baseof.rss.xml",
"_default/home-baseof.rss.xml",
"_default/list-baseof.rss.xml",
"_default/baseof.rss.xml",
"_default/index-baseof.xml",
"_default/home-baseof.xml",
"_default/list-baseof.xml",
"_default/baseof.xml",
},
},
{
"RSS Section",
LayoutDescriptor{Kind: "section", Section: "sect1", OutputFormatName: "rss", Suffix: "xml"},
"",
[]string{
"sect1/sect1.rss.xml",
"sect1/section.rss.xml",
"sect1/rss.xml",
"sect1/list.rss.xml",
"sect1/sect1.xml",
"sect1/section.xml",
"sect1/list.xml",
"section/sect1.rss.xml",
"section/section.rss.xml",
"section/rss.xml",
"section/list.rss.xml",
"section/sect1.xml",
"section/section.xml",
"section/list.xml",
"_default/sect1.rss.xml",
"_default/section.rss.xml",
"_default/rss.xml",
"_default/list.rss.xml",
"_default/sect1.xml",
"_default/section.xml",
"_default/list.xml",
"_internal/_default/rss.xml",
},
},
{
"RSS Term",
LayoutDescriptor{Kind: "term", Section: "tag", OutputFormatName: "rss", Suffix: "xml"},
"",
[]string{
"term/term.rss.xml",
"term/tag.rss.xml",
"term/taxonomy.rss.xml",
"term/rss.xml",
"term/list.rss.xml",
"term/term.xml",
"term/tag.xml",
"term/taxonomy.xml",
"term/list.xml",
"taxonomy/term.rss.xml",
"taxonomy/tag.rss.xml",
"taxonomy/taxonomy.rss.xml",
"taxonomy/rss.xml",
"taxonomy/list.rss.xml",
"taxonomy/term.xml",
"taxonomy/tag.xml",
"taxonomy/taxonomy.xml",
"taxonomy/list.xml",
"tag/term.rss.xml",
"tag/tag.rss.xml",
"tag/taxonomy.rss.xml",
"tag/rss.xml",
"tag/list.rss.xml",
"tag/term.xml",
"tag/tag.xml",
"tag/taxonomy.xml",
"tag/list.xml",
"_default/term.rss.xml",
"_default/tag.rss.xml",
"_default/taxonomy.rss.xml",
"_default/rss.xml",
"_default/list.rss.xml",
"_default/term.xml",
"_default/tag.xml",
"_default/taxonomy.xml",
"_default/list.xml",
"_internal/_default/rss.xml",
},
},
{
"RSS Taxonomy",
LayoutDescriptor{Kind: "taxonomy", Section: "tag", OutputFormatName: "rss", Suffix: "xml"},
"",
[]string{
"tag/tag.terms.rss.xml",
"tag/terms.rss.xml",
"tag/taxonomy.rss.xml",
"tag/rss.xml",
"tag/list.rss.xml",
"tag/tag.terms.xml",
"tag/terms.xml",
"tag/taxonomy.xml",
"tag/list.xml",
"taxonomy/tag.terms.rss.xml",
"taxonomy/terms.rss.xml",
"taxonomy/taxonomy.rss.xml",
"taxonomy/rss.xml",
"taxonomy/list.rss.xml",
"taxonomy/tag.terms.xml",
"taxonomy/terms.xml",
"taxonomy/taxonomy.xml",
"taxonomy/list.xml",
"_default/tag.terms.rss.xml",
"_default/terms.rss.xml",
"_default/taxonomy.rss.xml",
"_default/rss.xml",
"_default/list.rss.xml",
"_default/tag.terms.xml",
"_default/terms.xml",
"_default/taxonomy.xml",
"_default/list.xml",
"_internal/_default/rss.xml",
},
},
{
"Home plain text",
LayoutDescriptor{Kind: "home", OutputFormatName: "json", Suffix: "json"},
"",
[]string{
"index.json.json",
"home.json.json",
"list.json.json",
"index.json",
"home.json",
"list.json",
"_default/index.json.json",
"_default/home.json.json",
"_default/list.json.json",
"_default/index.json",
"_default/home.json",
"_default/list.json",
},
},
{
"Page plain text",
LayoutDescriptor{Kind: "page", OutputFormatName: "json", Suffix: "json"},
"",
[]string{
"_default/single.json.json",
"_default/single.json",
},
},
{
"Reserved section, shortcodes",
LayoutDescriptor{Kind: "section", Section: "shortcodes", Type: "shortcodes", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"section/shortcodes.amp.html",
"section/section.amp.html",
"section/list.amp.html",
"section/shortcodes.html",
"section/section.html",
"section/list.html",
"_default/shortcodes.amp.html",
"_default/section.amp.html",
"_default/list.amp.html",
"_default/shortcodes.html",
"_default/section.html",
"_default/list.html",
},
},
{
"Reserved section, partials",
LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"section/partials.amp.html",
"section/section.amp.html",
"section/list.amp.html",
"section/partials.html",
"section/section.html",
"section/list.html",
"_default/partials.amp.html",
"_default/section.amp.html",
"_default/list.amp.html",
"_default/partials.html",
"_default/section.html",
"_default/list.html",
},
},
// This is currently always HTML only
{
"404, HTML",
LayoutDescriptor{Kind: "404", OutputFormatName: "html", Suffix: "html"},
"",
[]string{
"404.html.html",
"404.html",
},
},
{
"404, HTML baseof",
LayoutDescriptor{Kind: "404", Baseof: true, OutputFormatName: "html", Suffix: "html"},
"",
[]string{
"404-baseof.html.html",
"baseof.html.html",
"404-baseof.html",
"baseof.html",
"_default/404-baseof.html.html",
"_default/baseof.html.html",
"_default/404-baseof.html",
"_default/baseof.html",
},
},
{
"Content hook",
LayoutDescriptor{Kind: "render-link", RenderingHook: true, Layout: "mylayout", Section: "blog", OutputFormatName: "amp", Suffix: "html"},
"",
[]string{
"blog/_markup/render-link.amp.html",
"blog/_markup/render-link.html",
"_default/_markup/render-link.amp.html",
"_default/_markup/render-link.html",
},
},
} {
c.Run(this.name, func(c *qt.C) {
l := NewLayoutHandler()
layouts, err := l.For(this.layoutDescriptor)
c.Assert(err, qt.IsNil)
c.Assert(layouts, qt.Not(qt.IsNil), qt.Commentf(this.layoutDescriptor.Kind))
if !reflect.DeepEqual(layouts, this.expect) {
r := strings.NewReplacer(
"[", "\t\"",
"]", "\",",
" ", "\",\n\t\"",
)
fmtGot := r.Replace(fmt.Sprintf("%v", layouts))
fmtExp := r.Replace(fmt.Sprintf("%v", this.expect))
c.Fatalf("got %d items, expected %d:\nGot:\n\t%v\nExpected:\n\t%v\nDiff:\n%s", len(layouts), len(this.expect), layouts, this.expect, diff.Diff(fmtExp, fmtGot))
}
})
}
}
/*
func BenchmarkLayout(b *testing.B) {
descriptor := LayoutDescriptor{Kind: "taxonomy", Section: "categories"}
l := NewLayoutHandler()
for i := 0; i < b.N; i++ {
_, err := l.For(descriptor, HTMLFormat)
if err != nil {
panic(err)
}
}
}
func BenchmarkLayoutUncached(b *testing.B) {
for i := 0; i < b.N; i++ {
descriptor := LayoutDescriptor{Kind: "taxonomy", Section: "categories"}
l := NewLayoutHandler()
_, err := l.For(descriptor, HTMLFormat)
if err != nil {
panic(err)
}
}
}
*/

View file

@ -133,6 +133,15 @@ var (
Weight: 10,
}
// Alias is the output format used for alias redirects.
AliasHTMLFormat = Format{
Name: "alias",
MediaType: media.Builtin.HTMLType,
IsHTML: true,
Ugly: true,
Permalinkable: false,
}
MarkdownFormat = Format{
Name: "markdown",
MediaType: media.Builtin.MarkdownType,
@ -192,8 +201,17 @@ var (
Rel: "sitemap",
}
HTTPStatusHTMLFormat = Format{
Name: "httpstatus",
GotmplFormat = Format{
Name: "gotmpl",
MediaType: media.Builtin.GotmplType,
IsPlainText: true,
NotAlternative: true,
}
// I'm not sure having a 404 format is a good idea,
// for one, we would want to have multiple formats for this.
HTTPStatus404HTMLFormat = Format{
Name: "404",
MediaType: media.Builtin.HTMLType,
NotAlternative: true,
Ugly: true,
@ -209,12 +227,16 @@ var DefaultFormats = Formats{
CSSFormat,
CSVFormat,
HTMLFormat,
GotmplFormat,
HTTPStatus404HTMLFormat,
AliasHTMLFormat,
JSONFormat,
MarkdownFormat,
WebAppManifestFormat,
RobotsTxtFormat,
RSSFormat,
SitemapFormat,
SitemapIndexFormat,
}
func init() {

View file

@ -68,7 +68,7 @@ func TestDefaultTypes(t *testing.T) {
c.Assert(RSSFormat.NoUgly, qt.Equals, true)
c.Assert(CalendarFormat.IsHTML, qt.Equals, false)
c.Assert(len(DefaultFormats), qt.Equals, 11)
c.Assert(len(DefaultFormats), qt.Equals, 15)
}
func TestGetFormatByName(t *testing.T) {
@ -140,7 +140,7 @@ func TestGetFormatByFilename(t *testing.T) {
func TestSort(t *testing.T) {
c := qt.New(t)
c.Assert(DefaultFormats[0].Name, qt.Equals, "html")
c.Assert(DefaultFormats[1].Name, qt.Equals, "amp")
c.Assert(DefaultFormats[1].Name, qt.Equals, "404")
json := JSONFormat
json.Weight = 1

View file

@ -34,6 +34,7 @@ const (
// The following are (currently) temporary nodes,
// i.e. nodes we create just to render in isolation.
KindTemporary = "temporary"
KindRSS = "rss"
KindSitemap = "sitemap"
KindSitemapIndex = "sitemapindex"

View file

@ -150,8 +150,8 @@ type InSectionPositioner interface {
// InternalDependencies is considered an internal interface.
type InternalDependencies interface {
// GetRelatedDocsHandler is for internal use only.
GetRelatedDocsHandler() *RelatedDocsHandler
// GetInternalRelatedDocsHandler is for internal use only.
GetInternalRelatedDocsHandler() *RelatedDocsHandler
}
// OutputFormatsProvider provides the OutputFormats of a Page.

View file

@ -145,7 +145,7 @@ func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) {
pb.isUgly = true
}
if d.Type == output.HTTPStatusHTMLFormat || d.Type == output.SitemapFormat || d.Type == output.RobotsTxtFormat {
if d.Type == output.HTTPStatus404HTMLFormat || d.Type == output.SitemapFormat || d.Type == output.RobotsTxtFormat {
pb.noSubResources = true
} else if d.Kind != kinds.KindPage && d.URL == "" && d.Section.Base() != "/" {
if d.ExpandedPermalink != "" {

View file

@ -129,7 +129,7 @@ func (p Pages) withInvertedIndex(ctx context.Context, search func(idx *related.I
return nil, fmt.Errorf("invalid type %T in related search", p[0])
}
cache := d.GetRelatedDocsHandler()
cache := d.GetInternalRelatedDocsHandler()
searchIndex, err := cache.getOrCreateIndex(ctx, p)
if err != nil {

View file

@ -221,7 +221,7 @@ func (p *testPage) GetTerms(taxonomy string) Pages {
panic("testpage: not implemented")
}
func (p *testPage) GetRelatedDocsHandler() *RelatedDocsHandler {
func (p *testPage) GetInternalRelatedDocsHandler() *RelatedDocsHandler {
return relatedDocsHandler
}

View file

@ -42,7 +42,6 @@ import (
"github.com/gohugoio/hugo/resources/images"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/tpl"
)
func NewSpec(
@ -123,8 +122,6 @@ type Spec struct {
BuildClosers types.CloseAdder
Rebuilder identity.SignalRebuilder
TextTemplates tpl.TemplateParseFinder
Permalinks page.PermalinkExpander
ImageCache *ImageCache

View file

@ -23,17 +23,17 @@ import (
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/internal"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/tpl/tplimpl"
)
// Client contains methods to perform template processing of Resource objects.
type Client struct {
rs *resources.Spec
t tpl.TemplatesProvider
t tplimpl.TemplateStoreProvider
}
// New creates a new Client with the given specification.
func New(rs *resources.Spec, t tpl.TemplatesProvider) *Client {
func New(rs *resources.Spec, t tplimpl.TemplateStoreProvider) *Client {
if rs == nil {
panic("must provide a resource Spec")
}
@ -45,7 +45,7 @@ func New(rs *resources.Spec, t tpl.TemplatesProvider) *Client {
type executeAsTemplateTransform struct {
rs *resources.Spec
t tpl.TemplatesProvider
t tplimpl.TemplateStoreProvider
targetPath string
data any
}
@ -56,14 +56,13 @@ func (t *executeAsTemplateTransform) Key() internal.ResourceTransformationKey {
func (t *executeAsTemplateTransform) Transform(ctx *resources.ResourceTransformationCtx) error {
tplStr := helpers.ReaderToString(ctx.From)
templ, err := t.t.TextTmpl().Parse(ctx.InPath, tplStr)
th := t.t.GetTemplateStore()
ti, err := th.TextParse(ctx.InPath, tplStr)
if err != nil {
return fmt.Errorf("failed to parse Resource %q as Template:: %w", ctx.InPath, err)
}
ctx.OutPath = t.targetPath
return t.t.Tmpl().ExecuteWithContext(ctx.Ctx, templ, ctx.To, t.data)
return th.ExecuteWithContext(ctx.Ctx, ti, ctx.To, t.data)
}
func (c *Client) ExecuteAsTemplate(ctx context.Context, res resources.ResourceTransformer, targetPath string, data any) (resource.Resource, error) {

View file

@ -1,6 +1,6 @@
hugo --printUnusedTemplates
stderr 'Template _default/list.html is unused'
stderr 'Template /list.html is unused'
-- hugo.toml --
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "404", "section", "page"]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more