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

View file

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

View file

@ -16,11 +16,11 @@ package hstrings
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"slices"
"strings" "strings"
"sync" "sync"
"github.com/gohugoio/hugo/compare" "github.com/gohugoio/hugo/compare"
"slices"
) )
var _ compare.Eqer = StringEqualFold("") var _ compare.Eqer = StringEqualFold("")
@ -128,7 +128,7 @@ func ToString(v any) (string, bool) {
return "", false return "", false
} }
type Tuple struct { type (
First string Strings2 [2]string
Second 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 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. // 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. // 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) { 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() 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) { func (c *Cache[K, T]) set(key K, value T) {
c.m[key] = value c.m[key] = value
} }

View file

@ -14,8 +14,9 @@
package maps package maps
import ( import (
"github.com/gohugoio/hugo/common/hashing"
"slices" "slices"
"github.com/gohugoio/hugo/common/hashing"
) )
// Ordered is a map that can be iterated in the order of insertion. // 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 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. // Delete deletes the value for the given key.
func (m *Ordered[K, T]) Delete(key K) { func (m *Ordered[K, T]) Delete(key K) {
if m == nil { if m == nil {

View file

@ -23,6 +23,11 @@ import (
"github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/kinds"
)
const (
identifierBaseof = "baseof"
) )
// PathParser parses a path into a Path. // PathParser parses a path into a Path.
@ -33,6 +38,10 @@ type PathParser struct {
// Reports whether the given language is disabled. // Reports whether the given language is disabled.
IsLangDisabled func(string) bool 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. // Reports whether the given ext is a content file.
IsContentExt func(string) bool IsContentExt func(string) bool
} }
@ -83,13 +92,10 @@ func (pp *PathParser) Parse(c, s string) *Path {
} }
func (pp *PathParser) newPath(component string) *Path { func (pp *PathParser) newPath(component string) *Path {
return &Path{ p := &Path{}
component: component, p.reset()
posContainerLow: -1, p.component = component
posContainerHigh: -1, return p
posSectionHigh: -1,
posIdentifierLanguage: -1,
}
} }
func (pp *PathParser) parse(component, s string) (*Path, error) { 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 return p, nil
} }
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) { func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot int) {
hasLang := pp.LanguageIndex != nil if p.posContainerHigh != -1 {
hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts) 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" { if runtime.GOOS == "windows" {
s = path.Clean(filepath.ToSlash(s)) s = path.Clean(filepath.ToSlash(s))
if s == "." { if s == "." {
@ -140,46 +227,21 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
p.s = s p.s = s
slashCount := 0 slashCount := 0
lastDot := 0
for i := len(s) - 1; i >= 0; i-- { for i := len(s) - 1; i >= 0; i-- {
c := s[i] c := s[i]
switch c { switch c {
case '.': case '.':
if p.posContainerHigh == -1 { pp.parseIdentifier(component, s, p, i, lastDot)
var high int lastDot = i
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)
}
}
}
}
case '/': case '/':
slashCount++ slashCount++
if p.posContainerHigh == -1 { if p.posContainerHigh == -1 {
if lastDot > 0 {
pp.parseIdentifier(component, s, p, i, lastDot)
}
p.posContainerHigh = i + 1 p.posContainerHigh = i + 1
} else if p.posContainerLow == -1 { } else if p.posContainerLow == -1 {
p.posContainerLow = i + 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 isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
isContent := isContentComponent && pp.IsContentExt(p.Ext()) isContent := isContentComponent && pp.IsContentExt(p.Ext())
id := p.identifiers[len(p.identifiers)-1] 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() { if id.High > p.posContainerHigh {
p.posSectionHigh = 0 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) { func ModifyPathBundleTypeResource(p *Path) {
if p.IsContent() { if p.IsContent() {
p.bundleType = PathTypeContentResource p.pathType = TypeContentResource
} else { } else {
p.bundleType = PathTypeFile p.pathType = TypeFile
} }
} }
type PathType int //go:generate stringer -type Type
type Type int
const ( const (
// A generic resource, e.g. a JSON file. // A generic resource, e.g. a JSON file.
PathTypeFile PathType = iota TypeFile Type = iota
// All below are content files. // All below are content files.
// A resource of a content type with front matter. // A resource of a content type with front matter.
PathTypeContentResource TypeContentResource
// E.g. /blog/my-post.md // E.g. /blog/my-post.md
PathTypeContentSingle TypeContentSingle
// All below are bundled content files. // All below are bundled content files.
// Leaf bundles, e.g. /blog/my-post/index.md // Leaf bundles, e.g. /blog/my-post/index.md
PathTypeLeaf TypeLeaf
// Branch bundles, e.g. /blog/_index.md // Branch bundles, e.g. /blog/_index.md
PathTypeBranch TypeBranch
// Content data file, _content.gotmpl. // Content data file, _content.gotmpl.
PathTypeContentData TypeContentData
// Layout types.
TypeMarkup
TypeShortcode
TypePartial
TypeBaseof
) )
type Path struct { type Path struct {
@ -257,13 +347,17 @@ type Path struct {
posContainerHigh int posContainerHigh int
posSectionHigh int posSectionHigh int
component string component string
bundleType PathType pathType Type
identifiers []types.LowHigh[string] identifiers []types.LowHigh[string]
posIdentifierLanguage int posIdentifierLanguage int
disabled bool posIdentifierOutputFormat int
posIdentifierKind int
posIdentifierBaseof int
identifiersUnknown []int
disabled bool
trimLeadingSlash bool trimLeadingSlash bool
@ -293,9 +387,12 @@ func (p *Path) reset() {
p.posContainerHigh = -1 p.posContainerHigh = -1
p.posSectionHigh = -1 p.posSectionHigh = -1
p.component = "" p.component = ""
p.bundleType = 0 p.pathType = 0
p.identifiers = p.identifiers[:0] p.identifiers = p.identifiers[:0]
p.posIdentifierLanguage = -1 p.posIdentifierLanguage = -1
p.posIdentifierOutputFormat = -1
p.posIdentifierKind = -1
p.posIdentifierBaseof = -1
p.disabled = false p.disabled = false
p.trimLeadingSlash = false p.trimLeadingSlash = false
p.unnormalized = nil p.unnormalized = nil
@ -316,6 +413,9 @@ func (p *Path) norm(s string) string {
// IdentifierBase satisfies identity.Identity. // IdentifierBase satisfies identity.Identity.
func (p *Path) IdentifierBase() string { func (p *Path) IdentifierBase() string {
if p.Component() == files.ComponentFolderLayouts {
return p.Path()
}
return p.Base() return p.Base()
} }
@ -332,6 +432,13 @@ func (p *Path) Container() string {
return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1]) 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. // ContainerDir returns the container directory for this path.
// For content bundles this will be the parent directory. // For content bundles this will be the parent directory.
func (p *Path) ContainerDir() string { 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). // 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. // Note that this will also return true for content files in a bundle.
func (p *Path) IsContent() bool { 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), // isContentPage returns true if the path is a content file (e.g. mypost.md),
// but nof if inside a leaf bundle. // but nof if inside a leaf bundle.
func (p *Path) isContentPage() bool { func (p *Path) isContentPage() bool {
return p.BundleType() >= PathTypeContentSingle return p.Type() >= TypeContentSingle && p.Type() <= TypeContentData
} }
// Name returns the last element of path. // 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). // NameNoIdentifier returns the last element of path without any identifier (e.g. no extension).
func (p *Path) NameNoIdentifier() string { 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 { 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. // 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) 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. // Unnormalized returns the Path with the original case preserved.
func (p *Path) Unnormalized() *Path { func (p *Path) Unnormalized() *Path {
return p.unnormalized return p.unnormalized
@ -436,6 +564,28 @@ func (p *Path) PathNoIdentifier() string {
return p.base(false, false) 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. // PathRel returns the path relative to the given owner.
func (p *Path) PathRel(owner *Path) string { func (p *Path) PathRel(owner *Path) string {
ob := owner.Base() ob := owner.Base()
@ -462,6 +612,21 @@ func (p *Path) Base() string {
return p.base(!p.isContentPage(), p.IsBundle()) 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. // BaseNoLeadingSlash returns the base path without the leading slash.
func (p *Path) BaseNoLeadingSlash() string { func (p *Path) BaseNoLeadingSlash() string {
return p.Base()[1:] return p.Base()[1:]
@ -477,11 +642,12 @@ func (p *Path) base(preserveExt, isBundle bool) string {
return p.norm(p.s) return p.norm(p.s)
} }
id := p.identifiers[len(p.identifiers)-1] var high int
high := id.Low - 1
if isBundle { if isBundle {
high = p.posContainerHigh - 1 high = p.posContainerHigh - 1
} else {
high = p.nameLowHigh().High
} }
if high == 0 { 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. // 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]) 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) 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 { func (p *Path) Lang() string {
return p.identifierAsString(1) return p.identifierAsString(p.posIdentifierLanguage)
} }
func (p *Path) Identifier(i int) string { func (p *Path) Identifier(i int) string {
@ -522,28 +696,36 @@ func (p *Path) Identifiers() []string {
return ids return ids
} }
func (p *Path) BundleType() PathType { func (p *Path) IdentifiersUnknown() []string {
return p.bundleType 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 { func (p *Path) IsBundle() bool {
return p.bundleType >= PathTypeLeaf return p.pathType >= TypeLeaf && p.pathType <= TypeContentData
} }
func (p *Path) IsBranchBundle() bool { func (p *Path) IsBranchBundle() bool {
return p.bundleType == PathTypeBranch return p.pathType == TypeBranch
} }
func (p *Path) IsLeafBundle() bool { func (p *Path) IsLeafBundle() bool {
return p.bundleType == PathTypeLeaf return p.pathType == TypeLeaf
} }
func (p *Path) IsContentData() bool { func (p *Path) IsContentData() bool {
return p.bundleType == PathTypeContentData return p.pathType == TypeContentData
} }
func (p Path) ForBundleType(t PathType) *Path { func (p Path) ForBundleType(t Type) *Path {
p.bundleType = t p.pathType = t
return &p return &p
} }

View file

@ -18,6 +18,7 @@ import (
"testing" "testing"
"github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/resources/kinds"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
) )
@ -26,10 +27,18 @@ var testParser = &PathParser{
LanguageIndex: map[string]int{ LanguageIndex: map[string]int{
"no": 0, "no": 0,
"en": 1, "en": 1,
"fr": 2,
}, },
IsContentExt: func(ext string) bool { IsContentExt: func(ext string) bool {
return ext == "md" 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) { func TestParse(t *testing.T) {
@ -105,17 +114,19 @@ func TestParse(t *testing.T) {
"Basic Markdown file", "Basic Markdown file",
"/a/b/c.md", "/a/b/c.md",
func(c *qt.C, p *Path) { 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.IsContent(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsFalse) c.Assert(p.IsLeafBundle(), qt.IsFalse)
c.Assert(p.Name(), qt.Equals, "c.md") c.Assert(p.Name(), qt.Equals, "c.md")
c.Assert(p.Base(), qt.Equals, "/a/b/c") 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.Section(), qt.Equals, "a")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c") c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c")
c.Assert(p.Path(), qt.Equals, "/a/b/c.md") c.Assert(p.Path(), qt.Equals, "/a/b/c.md")
c.Assert(p.Dir(), qt.Equals, "/a/b") c.Assert(p.Dir(), qt.Equals, "/a/b")
c.Assert(p.Container(), qt.Equals, "b") c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a/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. // Reclassify it as a content resource.
ModifyPathBundleTypeResource(p) 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.IsContent(), qt.IsTrue)
c.Assert(p.Name(), qt.Equals, "b.md") c.Assert(p.Name(), qt.Equals, "b.md")
c.Assert(p.Base(), qt.Equals, "/a/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", "/a/b.a.b.no.txt",
func(c *qt.C, p *Path) { func(c *qt.C, p *Path) {
c.Assert(p.Name(), qt.Equals, "b.a.b.no.txt") 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.NameNoLang(), qt.Equals, "b.a.b.txt")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"}) c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no", "b", "a", "b"})
c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt") c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"b", "a", "b"})
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt") 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.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.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", "/_index.md",
func(c *qt.C, p *Path) { func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/") 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.Path(), qt.Equals, "/_index.md")
c.Assert(p.Container(), qt.Equals, "") c.Assert(p.Container(), qt.Equals, "")
c.Assert(p.ContainerDir(), qt.Equals, "/") c.Assert(p.ContainerDir(), qt.Equals, "/")
@ -186,13 +199,14 @@ func TestParse(t *testing.T) {
"/a/index.md", "/a/index.md",
func(c *qt.C, p *Path) { func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a") 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.BaseNameNoIdentifier(), qt.Equals, "a")
c.Assert(p.Container(), qt.Equals, "a") c.Assert(p.Container(), 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.ContainerDir(), qt.Equals, "")
c.Assert(p.Dir(), qt.Equals, "/a") c.Assert(p.Dir(), qt.Equals, "/a")
c.Assert(p.Ext(), qt.Equals, "md") 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.IsBranchBundle(), qt.IsFalse)
c.Assert(p.IsBundle(), qt.IsTrue) c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsTrue) c.Assert(p.IsLeafBundle(), qt.IsTrue)
@ -209,11 +223,12 @@ func TestParse(t *testing.T) {
func(c *qt.C, p *Path) { func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b") c.Assert(p.Base(), qt.Equals, "/a/b")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "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.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a") c.Assert(p.ContainerDir(), qt.Equals, "/a")
c.Assert(p.Dir(), qt.Equals, "/a/b") c.Assert(p.Dir(), qt.Equals, "/a/b")
c.Assert(p.Ext(), qt.Equals, "md") 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.IsBranchBundle(), qt.IsFalse)
c.Assert(p.IsBundle(), qt.IsTrue) c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), 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.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a") c.Assert(p.ContainerDir(), qt.Equals, "/a")
c.Assert(p.Ext(), qt.Equals, "md") 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.IsBranchBundle(), qt.IsTrue)
c.Assert(p.IsBundle(), qt.IsTrue) c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsFalse) c.Assert(p.IsLeafBundle(), qt.IsFalse)
@ -274,7 +289,7 @@ func TestParse(t *testing.T) {
func(c *qt.C, p *Path) { func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/index.txt") c.Assert(p.Base(), qt.Equals, "/a/b/index.txt")
c.Assert(p.Ext(), qt.Equals, "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.IsLeafBundle(), qt.IsFalse)
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b/index") c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b/index")
}, },
@ -357,11 +372,140 @@ func TestParse(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
c.Run(test.name, func(c *qt.C) { c.Run(test.name, func(c *qt.C) {
if test.name != "Basic Markdown file" {
// return
}
test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path)) 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) { func TestHasExt(t *testing.T) {
c := qt.New(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() RUnlock()
} }
type Locker interface {
Lock()
Unlock()
}
type RWLocker interface {
RLocker
Locker
}
// KeyValue is a interface{} tuple. // KeyValue is a interface{} tuple.
type KeyValue struct { type KeyValue struct {
Key any Key any

View file

@ -849,7 +849,24 @@ func (c *Configs) Init() error {
c.Languages = languages c.Languages = languages
c.LanguagesDefaultFirst = languagesDefaultFirst 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)) c.configLangs = make([]config.AllProvider, len(c.Languages))
for i, l := range c.LanguagesDefaultFirst { 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 { func (b *contentBuilder) mapArcheTypeDir() error {
var m archetypeMap var m archetypeMap
seen := map[hstrings.Tuple]bool{} seen := map[hstrings.Strings2]bool{}
walkFn := func(path string, fim hugofs.FileMetaInfo) error { walkFn := func(path string, fim hugofs.FileMetaInfo) error {
if fim.IsDir() { if fim.IsDir() {
@ -301,7 +301,7 @@ func (b *contentBuilder) mapArcheTypeDir() error {
pi := fim.Meta().PathInfo pi := fim.Meta().PathInfo
if pi.IsContent() { if pi.IsContent() {
pathLang := hstrings.Tuple{First: pi.PathNoIdentifier(), Second: fim.Meta().Lang} pathLang := hstrings.Strings2{pi.PathBeforeLangAndOutputFormatAndExt(), fim.Meta().Lang}
if seen[pathLang] { if seen[pathLang] {
// Duplicate content file, e.g. page.md and page.html. // Duplicate content file, e.g. page.md and page.html.
// In the regular build, we will filter out the duplicates, but // 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/media"
"github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/postpub" "github.com/gohugoio/hugo/resources/postpub"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/metrics" "github.com/gohugoio/hugo/metrics"
"github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources"
@ -46,12 +47,6 @@ type Deps struct {
ExecHelper *hexec.Exec 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. // The file systems to use.
Fs *hugofs.Fs `json:"-"` Fs *hugofs.Fs `json:"-"`
@ -79,7 +74,8 @@ type Deps struct {
// The site building. // The site building.
Site page.Site Site page.Site
TemplateProvider ResourceProvider TemplateStore *tplimpl.TemplateStore
// Used in tests // Used in tests
OverloadedTemplateFuncs map[string]any OverloadedTemplateFuncs map[string]any
@ -102,6 +98,9 @@ type Deps struct {
// This is common/global for all sites. // This is common/global for all sites.
BuildState *BuildState BuildState *BuildState
// Misc counters.
Counters *Counters
// Holds RPC dispatchers for Katex etc. // Holds RPC dispatchers for Katex etc.
// TODO(bep) rethink this re. a plugin setup, but this will have to do for now. // TODO(bep) rethink this re. a plugin setup, but this will have to do for now.
WasmDispatchers *warpc.Dispatchers WasmDispatchers *warpc.Dispatchers
@ -109,9 +108,6 @@ type Deps struct {
// The JS batcher client. // The JS batcher client.
JSBatcherClient js.BatcherClient JSBatcherClient js.BatcherClient
// The JS batcher client.
// JSBatcherClient *esbuild.BatcherClient
isClosed bool isClosed bool
*globalErrHandler *globalErrHandler
@ -130,8 +126,8 @@ func (d Deps) Clone(s page.Site, conf config.AllProvider) (*Deps, error) {
return &d, nil return &d, nil
} }
func (d *Deps) SetTempl(t *tpl.TemplateHandlers) { func (d *Deps) GetTemplateStore() *tplimpl.TemplateStore {
d.tmplHandlers = t return d.TemplateStore
} }
func (d *Deps) Init() error { func (d *Deps) Init() error {
@ -153,10 +149,12 @@ func (d *Deps) Init() error {
logger: d.Log, logger: d.Log,
} }
} }
if d.BuildState == nil { if d.BuildState == nil {
d.BuildState = &BuildState{} d.BuildState = &BuildState{}
} }
if d.Counters == nil {
d.Counters = &Counters{}
}
if d.BuildState.DeferredExecutions == nil { if d.BuildState.DeferredExecutions == nil {
if d.BuildState.DeferredExecutionsGroupedByRenderingContext == nil { if d.BuildState.DeferredExecutionsGroupedByRenderingContext == nil {
d.BuildState.DeferredExecutionsGroupedByRenderingContext = make(map[tpl.RenderingContext]*DeferredExecutions) d.BuildState.DeferredExecutionsGroupedByRenderingContext = make(map[tpl.RenderingContext]*DeferredExecutions)
@ -263,22 +261,17 @@ func (d *Deps) Init() error {
return nil return nil
} }
// TODO(bep) rework this to get it in line with how we manage templates.
func (d *Deps) Compile(prototype *Deps) error { func (d *Deps) Compile(prototype *Deps) error {
var err error var err error
if prototype == nil { if prototype == nil {
if err = d.TemplateProvider.NewResource(d); err != nil {
return err
}
if err = d.TranslationProvider.NewResource(d); err != nil { if err = d.TranslationProvider.NewResource(d); err != nil {
return err return err
} }
return nil return nil
} }
if err = d.TemplateProvider.CloneResource(d, prototype); err != nil {
return err
}
if err = d.TranslationProvider.CloneResource(d, prototype); err != nil { if err = d.TranslationProvider.CloneResource(d, prototype); err != nil {
return err return err
} }
@ -378,14 +371,6 @@ type ResourceProvider interface {
CloneResource(dst, src *Deps) error 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 { func (d *Deps) Close() error {
if d.isClosed { if d.isClosed {
return nil return nil
@ -454,6 +439,12 @@ type BuildState struct {
DeferredExecutionsGroupedByRenderingContext map[tpl.RenderingContext]*DeferredExecutions DeferredExecutionsGroupedByRenderingContext map[tpl.RenderingContext]*DeferredExecutions
} }
// Misc counters.
type Counters struct {
// Counter for the math.Counter function.
MathCounter atomic.Uint64
}
type DeferredExecutions struct { type DeferredExecutions struct {
// A set of filenames in /public that // A set of filenames in /public that
// contains a post-processing prefix. // contains a post-processing prefix.

View file

@ -29,16 +29,17 @@ import (
"github.com/gohugoio/hugo/publisher" "github.com/gohugoio/hugo/publisher"
"github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/tpl/tplimpl"
) )
type aliasHandler struct { type aliasHandler struct {
t tpl.TemplateHandler ts *tplimpl.TemplateStore
log loggers.Logger log loggers.Logger
allowRoot bool allowRoot bool
} }
func newAliasHandler(t tpl.TemplateHandler, l loggers.Logger, allowRoot bool) aliasHandler { func newAliasHandler(ts *tplimpl.TemplateStore, l loggers.Logger, allowRoot bool) aliasHandler {
return aliasHandler{t, l, allowRoot} return aliasHandler{ts, l, allowRoot}
} }
type aliasPage struct { type aliasPage struct {
@ -47,16 +48,24 @@ type aliasPage struct {
} }
func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, error) { func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, error) {
var templ tpl.Template var templateDesc tplimpl.TemplateDescriptor
var found bool 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") q := tplimpl.TemplateQuery{
if !found { Path: base,
// TODO(bep) consolidate Category: tplimpl.CategoryLayout,
templ, found = a.t.Lookup("_internal/alias.html") Desc: templateDesc,
if !found { }
return nil, errors.New("no alias template found")
} t := a.ts.LookupPagesLayout(q)
if t == nil {
return nil, errors.New("no alias template found")
} }
data := aliasPage{ 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) ctx := tpl.Context.Page.Set(context.Background(), p)
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
err := a.t.ExecuteWithContext(ctx, templ, buffer, data) err := a.ts.ExecuteWithContext(ctx, t, buffer, data)
if err != nil { if err != nil {
return nil, err 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) { 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) targetPath, err := handler.targetPathAlias(path)
if err != nil { if err != nil {

View file

@ -107,13 +107,26 @@ func TestAliasMultipleOutputFormats(t *testing.T) {
func TestAliasTemplate(t *testing.T) { func TestAliasTemplate(t *testing.T) {
t.Parallel() t.Parallel()
b := newTestSitesBuilder(t) files := `
b.WithSimpleConfigFile().WithContent("page.md", pageWithAlias).WithTemplatesAdded("alias.html", aliasTemplate) -- 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 // 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 // the alias redirector
b.AssertFileContent("public/foo/bar/index.html", "ALIASTEMPLATE") 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) 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 { if err != nil {
return fmt.Errorf("failed to parse archetype template: %s: %w", err, err) 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 { if err != nil {
return fmt.Errorf("failed to execute archetype template: %s: %w", err, err) 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() meta := fi.Meta()
pi := meta.PathInfo pi := meta.PathInfo
switch pi.BundleType() { switch pi.Type() {
case paths.PathTypeFile, paths.PathTypeContentResource: case paths.TypeFile, paths.TypeContentResource:
m.s.Log.Trace(logg.StringFunc( m.s.Log.Trace(logg.StringFunc(
func() string { func() string {
return fmt.Sprintf("insert resource: %q", fi.Meta().Filename) 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 addErr = err
return return
} }
case paths.PathTypeContentData: case paths.TypeContentData:
pc, rc, err := m.addPagesFromGoTmplFi(fi, buildConfig) pc, rc, err := m.addPagesFromGoTmplFi(fi, buildConfig)
pageCount += pc pageCount += pc
resourceCount += rc resourceCount += rc
@ -349,8 +349,7 @@ func (m *pageMap) addPagesFromGoTmplFi(fi hugofs.FileMetaInfo, buildConfig *Buil
DepsFromSite: func(s page.Site) pagesfromdata.PagesFromTemplateDeps { DepsFromSite: func(s page.Site) pagesfromdata.PagesFromTemplateDeps {
ss := s.(*Site) ss := s.(*Site)
return pagesfromdata.PagesFromTemplateDeps{ return pagesfromdata.PagesFromTemplateDeps{
TmplFinder: ss.TextTmpl(), TemplateStore: ss.GetTemplateStore(),
TmplExec: ss.Tmpl(),
} }
}, },
DependencyManager: s.Conf.NewIdentityManager("pagesfromdata"), 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 { if p.Component() == files.ComponentFolderContent {
// It may also be a bundled content resource. // It may also be a bundled content resource.
key := p.ForBundleType(paths.PathTypeContentResource).Base() key := p.ForBundleType(paths.TypeContentResource).Base()
tree = t.treeResources tree = t.treeResources
nCount = 0 nCount = 0
tree.ForEeachInDimension(key, doctree.DimensionLanguage.Index(), tree.ForEeachInDimension(key, doctree.DimensionLanguage.Index(),
@ -1304,14 +1304,14 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
checkedCounter atomic.Int64 checkedCounter atomic.Int64
) )
resetPo := func(po *pageOutput, r identity.FinderResult) { resetPo := func(po *pageOutput, rebuildContent bool, r identity.FinderResult) {
if po.pco != nil { if rebuildContent && po.pco != nil {
po.pco.Reset() // Will invalidate content cache. po.pco.Reset() // Will invalidate content cache.
} }
po.renderState = 0 po.renderState = 0
po.p.resourcesPublishInit = &sync.Once{} 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. // Will force a re-render even in fast render mode.
po.renderOnce = false 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. // 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, NumWorkers: h.numWorkers,
Handle: func(ctx context.Context, p *pageState) error { Handle: func(ctx context.Context, p *pageState) error {
if !p.isRenderedAny() { if !p.isRenderedAny() {
@ -1335,7 +1335,8 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
checkedCounter.Add(1) checkedCounter.Add(1)
if r := depsFinder.Contains(id, p.dependencyManager, 2); r > identity.FinderFoundOneOfManyRepetition { if r := depsFinder.Contains(id, p.dependencyManager, 2); r > identity.FinderFoundOneOfManyRepetition {
for _, po := range p.pageOutputs { 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. // Done.
return nil return nil
@ -1351,7 +1352,8 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
for _, id := range changes { for _, id := range changes {
checkedCounter.Add(1) checkedCounter.Add(1)
if r := depsFinder.Contains(id, po.dependencyManagerOutput, 50); r > identity.FinderFoundOneOfManyRepetition { 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 continue OUTPUTS
} }
} }
@ -1954,7 +1956,7 @@ func (sa *sitePagesAssembler) addStandalonePages() error {
tree.InsertIntoValuesDimension(key, p) tree.InsertIntoValuesDimension(key, p)
} }
addStandalone("/404", kinds.KindStatus404, output.HTTPStatusHTMLFormat) addStandalone("/404", kinds.KindStatus404, output.HTTPStatus404HTMLFormat)
if s.conf.EnableRobotsTXT { if s.conf.EnableRobotsTXT {
if m.i == 0 || s.Conf.IsMultihost() { if m.i == 0 || s.Conf.IsMultihost() {

View file

@ -242,8 +242,13 @@ Data en
} }
func TestBundleMultipleContentPageWithSamePath(t *testing.T) { func TestBundleMultipleContentPageWithSamePath(t *testing.T) {
t.Parallel()
files := ` files := `
-- hugo.toml -- -- hugo.toml --
printPathWarnings = true
-- layouts/all.html --
All.
-- content/bundle/index.md -- -- content/bundle/index.md --
--- ---
title: "Bundle 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 }}| 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. b.AssertLogContains("WARN Duplicate content path: \"/p1\"")
// This is a little arbitrary, but we have to pick one and prefer the Markdown version.
b.AssertFileContent("public/index.html", // There's multiple content files sharing the same logical path and language.
filepath.FromSlash("Bundle: Bundle md|md|/content/bundle/index.md|"), // This is a little arbitrary, but we have to pick one and prefer the Markdown version.
filepath.FromSlash("P1: P1 md|md|/content/p1.md|"), 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 // Issue #11944

View file

@ -17,6 +17,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
qt "github.com/frankban/quicktest"
) )
func TestRenderHooksRSS(t *testing.T) { 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-p1">Heading in p1</h1>
<h1 id="heading-in-p2">Heading in p2</h1> <h1 id="heading-in-p2">Heading in p2</h1>
`) `)
b.AssertFileContent("public/index.xml", ` b.AssertFileContent("public/index.xml", `
P2: <p>P2. xml-link: https://www.bep.is|</p> P2: <p>P2. xml-link: https://www.bep.is|</p>
P3: <p>P3. xml-link: https://www.example.org|</p> P3: <p>P3. xml-link: https://www.example.org|</p>
@ -378,3 +381,93 @@ Content: {{ .Content}}|
"|Text: First line.\nSecond line.||\n", "|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 package doctree
import ( import (
"iter"
"sync" "sync"
radix "github.com/armon/go-radix" 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 { 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 Get(s string) T
LongestPrefix(s string) (string, T) LongestPrefix(s string) (string, T)
Insert(s string, v T) 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 any]() *SimpleTree[T] {
func NewSimpleTree[T comparable]() *SimpleTree[T] {
return &SimpleTree[T]{tree: radix.New()} return &SimpleTree[T]{tree: radix.New()}
} }
// SimpleTree is a thread safe radix tree that holds T. // SimpleTree is a radix tree that holds T.
type SimpleTree[T comparable] struct { // This tree is not thread safe.
mu sync.RWMutex type SimpleTree[T any] struct {
tree *radix.Tree tree *radix.Tree
zero T zero T
} }
func (tree *SimpleTree[T]) Get(s string) T { func (tree *SimpleTree[T]) Get(s string) T {
tree.mu.RLock()
defer tree.mu.RUnlock()
if v, ok := tree.tree.Get(s); ok { if v, ok := tree.tree.Get(s); ok {
return v.(T) return v.(T)
} }
@ -50,9 +61,6 @@ func (tree *SimpleTree[T]) Get(s string) T {
} }
func (tree *SimpleTree[T]) LongestPrefix(s string) (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 { if s, v, ok := tree.tree.LongestPrefix(s); ok {
return s, v.(T) 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 { 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() 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) tree.tree.Insert(s, v)
return v return v
} }
func (tree *SimpleTree[T]) Lock(lockType LockType) func() { func (tree *SimpleThreadSafeTree[T]) Lock(lockType LockType) func() {
switch lockType { switch lockType {
case LockTypeNone: case LockTypeNone:
return func() {} return noopFunc
case LockTypeRead: case LockTypeRead:
tree.mu.RLock() tree.mu.RLock()
return tree.mu.RUnlock return tree.mu.RUnlock
@ -78,10 +190,16 @@ func (tree *SimpleTree[T]) Lock(lockType LockType) func() {
tree.mu.Lock() tree.mu.Lock()
return tree.mu.Unlock 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) commit := tree.Lock(lockType)
defer commit() defer commit()
var err error var err error
@ -96,3 +214,31 @@ func (tree *SimpleTree[T]) WalkPrefix(lockType LockType, s string, f func(s stri
return err 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" "fmt"
"strings" "strings"
"sync" "sync"
radix "github.com/armon/go-radix"
) )
var _ MutableTrees = MutableTrees{} var _ MutableTrees = MutableTrees{}
@ -60,11 +58,9 @@ func (ctx *WalkContext[T]) AddPostHook(handler func() error) {
ctx.HooksPost = append(ctx.HooksPost, handler) 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.dataInit.Do(func() {
ctx.data = &SimpleTree[any]{ ctx.data = NewSimpleThreadSafeTree[any]()
tree: radix.New(),
}
}) })
return ctx.data return ctx.data
} }
@ -191,7 +187,7 @@ func (t MutableTrees) CanLock() bool {
// WalkContext is passed to the Walk callback. // WalkContext is passed to the Walk callback.
type WalkContext[T any] struct { type WalkContext[T any] struct {
data *SimpleTree[any] data *SimpleThreadSafeTree[any]
dataInit sync.Once dataInit sync.Once
eventHandlers eventHandlers[T] eventHandlers eventHandlers[T]
events []*Event[T] events []*Event[T]

View file

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

View file

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

View file

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

View file

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

View file

@ -219,19 +219,31 @@ type IntegrationTestBuilder struct {
type lockingBuffer struct { type lockingBuffer struct {
sync.Mutex 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) { func (b *lockingBuffer) ReadFrom(r io.Reader) (n int64, err error) {
b.Lock() b.Lock()
n, err = b.Buffer.ReadFrom(r) n, err = b.buf.ReadFrom(r)
b.Unlock() b.Unlock()
return return
} }
func (b *lockingBuffer) Write(p []byte) (n int, err error) { func (b *lockingBuffer) Write(p []byte) (n int, err error) {
b.Lock() b.Lock()
n, err = b.Buffer.Write(p) n, err = b.buf.Write(p)
b.Unlock() b.Unlock()
return return
} }

View file

@ -28,15 +28,13 @@ import (
"github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/output/layouts"
"github.com/gohugoio/hugo/related" "github.com/gohugoio/hugo/related"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/types"
@ -116,6 +114,14 @@ type pageState struct {
resourcesPublishInit *sync.Once 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 { func (p *pageState) IdentifierBase() string {
return p.Path() return p.Path()
} }
@ -169,10 +175,6 @@ func (p *pageState) resetBuildState() {
// Nothing to do for now. // Nothing to do for now.
} }
func (p *pageState) reusePageOutputContent() bool {
return p.pageOutputTemplateVariationsState.Load() == 1
}
func (p *pageState) skipRender() bool { func (p *pageState) skipRender() bool {
b := p.s.conf.C.SegmentFilter.ShouldExcludeFine( b := p.s.conf.C.SegmentFilter.ShouldExcludeFine(
segments.SegmentMatcherFields{ segments.SegmentMatcherFields{
@ -474,49 +476,40 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
return nil return nil
} }
func (p *pageState) getLayoutDescriptor() layouts.LayoutDescriptor { func (po *pageOutput) getTemplateBasePathAndDescriptor() (string, tplimpl.TemplateDescriptor) {
p.layoutDescriptorInit.Do(func() { p := po.p
var section string f := po.f
sections := p.SectionsEntries() base := p.PathInfo().BaseReTyped(p.m.pageConfig.Type)
return base, tplimpl.TemplateDescriptor{
switch p.Kind() { Kind: p.Kind(),
case kinds.KindSection: Lang: p.Language().Lang,
if len(sections) > 0 { Layout: p.Layout(),
section = sections[0] OutputFormat: f.Name,
} MediaType: f.MediaType.Type,
case kinds.KindTaxonomy, kinds.KindTerm: IsPlainText: f.IsPlainText,
}
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 (p *pageState) resolveTemplate(layouts ...string) (tpl.Template, bool, error) { func (p *pageState) resolveTemplate(layouts ...string) (*tplimpl.TemplInfo, bool, error) {
f := p.outputFormat() dir, d := p.getTemplateBasePathAndDescriptor()
d := p.getLayoutDescriptor()
if len(layouts) > 0 { if len(layouts) > 0 {
d.Layout = 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. // 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 { if isRenderingSite {
cp := p.pageOutput.pco cp := p.pageOutput.pco
if cp == nil && p.reusePageOutputContent() { if cp == nil && p.canReusePageOutputContent() {
// Look for content to reuse. // Look for content to reuse.
for i := range p.pageOutputs { for i := range p.pageOutputs {
if i == idx { if i == idx {

View file

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

View file

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

View file

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

View file

@ -19,23 +19,21 @@ import (
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/text" "github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/spf13/cast" "github.com/spf13/cast"
"github.com/gohugoio/hugo/markup/converter/hooks" "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/tableofcontents"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
bp "github.com/gohugoio/hugo/bufferpool" bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources/page" "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. // 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 { 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 return template.HTML(res), nil
} }
@ -274,103 +272,100 @@ func (pco *pageContentOutput) initRenderHooks() error {
return r return r
} }
layoutDescriptor := pco.po.p.getLayoutDescriptor() // Inherit the descriptor from the page/current output format.
layoutDescriptor.RenderingHook = true // This allows for fine-grained control of the template used for
layoutDescriptor.LayoutOverride = false // rendering of e.g. links.
layoutDescriptor.Layout = "" base, layoutDescriptor := pco.po.p.getTemplateBasePathAndDescriptor()
switch tp { switch tp {
case hooks.LinkRendererType: case hooks.LinkRendererType:
layoutDescriptor.Kind = "render-link" layoutDescriptor.Variant1 = "link"
case hooks.ImageRendererType: case hooks.ImageRendererType:
layoutDescriptor.Kind = "render-image" layoutDescriptor.Variant1 = "image"
case hooks.HeadingRendererType: case hooks.HeadingRendererType:
layoutDescriptor.Kind = "render-heading" layoutDescriptor.Variant1 = "heading"
case hooks.PassthroughRendererType: case hooks.PassthroughRendererType:
layoutDescriptor.Kind = "render-passthrough" layoutDescriptor.Variant1 = "passthrough"
if id != nil { if id != nil {
layoutDescriptor.KindVariants = id.(string) layoutDescriptor.Variant2 = id.(string)
} }
case hooks.BlockquoteRendererType: case hooks.BlockquoteRendererType:
layoutDescriptor.Kind = "render-blockquote" layoutDescriptor.Variant1 = "blockquote"
if id != nil { if id != nil {
layoutDescriptor.KindVariants = id.(string) layoutDescriptor.Variant2 = id.(string)
} }
case hooks.TableRendererType: case hooks.TableRendererType:
layoutDescriptor.Kind = "render-table" layoutDescriptor.Variant1 = "table"
case hooks.CodeBlockRendererType: case hooks.CodeBlockRendererType:
layoutDescriptor.Kind = "render-codeblock" layoutDescriptor.Variant1 = "codeblock"
if id != nil { if id != nil {
lang := id.(string) layoutDescriptor.Variant2 = id.(string)
lexer := chromalexers.Get(lang)
if lexer != nil {
layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",")
} else {
layoutDescriptor.KindVariants = lang
}
} }
} }
getHookTemplate := func(f output.Format) (tpl.Template, bool) { renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
templ, found, err := pco.po.p.s.Tmpl().LookupLayout(layoutDescriptor, f) var ignoreInternal bool
if err != nil { switch layoutDescriptor.Variant1 {
panic(err) case "link":
} ignoreInternal = !renderHookConfig.Link.IsEnableDefault()
if found { case "image":
if isitp, ok := templ.(tpl.IsInternalTemplateProvider); ok && isitp.IsInternalTemplate() { ignoreInternal = !renderHookConfig.Image.IsEnableDefault()
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
} }
templ, found1 := getHookTemplate(pco.po.f) candidates := pco.po.p.s.renderFormats
if !found1 || pco.po.p.reusePageOutputContent() { var numCandidatesFound int
defaultOutputFormat := pco.po.p.s.conf.C.DefaultOutputFormat consider := func(candidate *tplimpl.TemplInfo) bool {
if layoutDescriptor.Variant1 != candidate.D.Variant1 {
candidates := pco.po.p.s.renderFormats return false
// 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)
}
} }
// Check if some of the other output formats would give a different template. if layoutDescriptor.Variant2 != "" && candidate.D.Variant2 != "" && layoutDescriptor.Variant2 != candidate.D.Variant2 {
for _, f := range candidates { return false
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 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 { if !found1 {
@ -384,7 +379,7 @@ func (pco *pageContentOutput) initRenderHooks() error {
} }
r := hookRendererTemplate{ r := hookRendererTemplate{
templateHandler: pco.po.p.s.Tmpl(), templateHandler: pco.po.p.s.GetTemplateStore(),
templ: templ, templ: templ,
resolvePosition: resolvePosition, resolvePosition: resolvePosition,
} }
@ -488,7 +483,7 @@ func (t targetPathsHolder) targetPaths() page.TargetPaths {
return t.paths 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() b := bp.GetBuffer()
defer bp.PutBuffer(b) defer bp.PutBuffer(b)
if err := h.ExecuteWithContext(ctx, templ, b, data); err != nil { 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() 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() 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 return nil, filepath.SkipDir
} }
seen := map[hstrings.Tuple]bool{} seen := map[hstrings.Strings2]hugofs.FileMetaInfo{}
for _, fi := range readdir { for _, fi := range readdir {
if fi.IsDir() { if fi.IsDir() {
continue 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 // These would eventually have been filtered out as duplicates when
// inserting them into the document store, // inserting them into the document store,
// but doing it here will preserve a consistent ordering. // but doing it here will preserve a consistent ordering.
baseLang := hstrings.Tuple{First: pi.Base(), Second: meta.Lang} baseLang := hstrings.Strings2{pi.Base(), meta.Lang}
if seen[baseLang] { 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 continue
} }
seen[baseLang] = true seen[baseLang] = fi
if pi == nil { if pi == nil {
panic(fmt.Sprintf("no path info for %q", meta.Filename)) 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 { func (c *pagesCollector) handleBundleLeaf(dir, bundle hugofs.FileMetaInfo, inPath string, readdir []hugofs.FileMetaInfo) error {
bundlePi := bundle.Meta().PathInfo bundlePi := bundle.Meta().PathInfo
seen := map[hstrings.Tuple]bool{} seen := map[hstrings.Strings2]bool{}
walk := func(path string, info hugofs.FileMetaInfo) error { walk := func(path string, info hugofs.FileMetaInfo) error {
if info.IsDir() { 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 // These would eventually have been filtered out as duplicates when
// inserting them into the document store, // inserting them into the document store,
// but doing it here will preserve a consistent ordering. // 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] { if seen[baseLang] {
return nil return nil
} }

View file

@ -29,6 +29,7 @@ import (
"github.com/gohugoio/hugo/resources/page/pagemeta" "github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/spf13/cast" "github.com/spf13/cast"
) )
@ -167,8 +168,7 @@ type PagesFromTemplateOptions struct {
} }
type PagesFromTemplateDeps struct { type PagesFromTemplateDeps struct {
TmplFinder tpl.TemplateParseFinder TemplateStore *tplimpl.TemplateStore
TmplExec tpl.TemplateExecutor
} }
var _ resource.Staler = (*PagesFromTemplate)(nil) var _ resource.Staler = (*PagesFromTemplate)(nil)
@ -303,7 +303,7 @@ func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
} }
defer f.Close() 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 { if err != nil {
return BuildInfo{}, err return BuildInfo{}, err
} }
@ -314,7 +314,7 @@ func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p) 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 return BuildInfo{}, err
} }

View file

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

View file

@ -15,7 +15,6 @@ package hugolib
import ( import (
"fmt" "fmt"
"path/filepath"
"testing" "testing"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
@ -102,10 +101,18 @@ URL: {{ $pag.URL }}
// Issue 6023 // Issue 6023
func TestPaginateWithSort(t *testing.T) { func TestPaginateWithSort(t *testing.T) {
b := newTestSitesBuilder(t).WithSimpleConfigFile() files := `
b.WithTemplatesAdded("index.html", `{{ range (.Paginate (sort .Site.RegularPages ".File.Filename" "desc")).Pages }}|{{ .File.Filename }}{{ end }}`) -- hugo.toml --
b.Build(BuildCfg{}).AssertFileContent("public/index.html", -- content/a/a.md --
filepath.FromSlash("|content/sect/doc1.nn.md|content/sect/doc1.nb.md|content/sect/doc1.fr.md|content/sect/doc1.en.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 // https://github.com/gohugoio/hugo/issues/6797
@ -176,12 +183,12 @@ Paginator: {{ .Paginator }}
func TestNilPointerErrorMessage(t *testing.T) { func TestNilPointerErrorMessage(t *testing.T) {
files := ` files := `
-- hugo.toml -- -- hugo.toml --
-- content/p1.md -- -- content/p1.md --
-- layouts/_default/single.html -- -- layouts/_default/single.html --
Home Filename: {{ site.Home.File.Filename }} Home Filename: {{ site.Home.File.Filename }}
` `
b, err := TestE(t, files) b, err := TestE(t, files)
b.Assert(err, qt.IsNotNil) 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" title: "My Section"
--- ---
-- content/mysection/mysectiontext.txt -- -- content/mysection/mysectiontext.txt --
Content.
-- content/_index.md -- -- content/_index.md --
--- ---
title: "Home" title: "Home"
@ -99,15 +100,17 @@ My Other Text: {{ $r.Content }}|{{ $r.Permalink }}|
` `
func TestRebuildEditLeafBundleHeaderOnly(t *testing.T) { func TestRebuildEditLeafBundleHeaderOnly(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple) t.Parallel()
b.AssertFileContent("public/mysection/mysectionbundle/index.html", for i := 0; i < 3; i++ {
"My Section Bundle Content Content.") b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/mysection/mysectionbundle/index.html",
b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "My Section Bundle Content.", "My Section Bundle Content Edited.").Build() "My Section Bundle Content Content.")
b.AssertFileContent("public/mysection/mysectionbundle/index.html", b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "My Section Bundle Content.", "My Section Bundle Content Edited.").Build()
"My Section Bundle Content Edited.") b.AssertFileContent("public/mysection/mysectionbundle/index.html",
b.AssertRenderCountPage(2) // home (rss) + bundle. "My Section Bundle Content Edited.")
b.AssertRenderCountContent(1) b.AssertRenderCountPage(2) // home (rss) + bundle.
b.AssertRenderCountContent(1)
}
} }
func TestRebuildEditTextFileInLeafBundle(t *testing.T) { func TestRebuildEditTextFileInLeafBundle(t *testing.T) {
@ -119,7 +122,7 @@ func TestRebuildEditTextFileInLeafBundle(t *testing.T) {
b.AssertFileContent("public/mysection/mysectionbundle/index.html", b.AssertFileContent("public/mysection/mysectionbundle/index.html",
"Text 2 Content Edited") "Text 2 Content Edited")
b.AssertRenderCountPage(1) b.AssertRenderCountPage(1)
b.AssertRenderCountContent(1) b.AssertRenderCountContent(0)
} }
func TestRebuildEditTextFileInShortcode(t *testing.T) { 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 Content.")
b.AssertFileContent("public/index.html", "Home Text Content Edited.") b.AssertFileContent("public/index.html", "Home Text Content Edited.")
b.AssertRenderCountPage(1) b.AssertRenderCountPage(1)
b.AssertRenderCountContent(1) b.AssertRenderCountContent(0)
} }
func TestRebuildEditTextFileInBranchBundle(t *testing.T) { func TestRebuildEditTextFileInBranchBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple) 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.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.AssertRenderCountPage(1)
b.AssertRenderCountContent(1) b.AssertRenderCountContent(0)
} }
func testRebuildBothWatchingAndRunning(t *testing.T, files string, withB func(b *IntegrationTestBuilder)) { 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() t.Parallel()
files := ` files := `
@ -498,9 +537,13 @@ disableLiveReload = true
title: "P1" title: "P1"
--- ---
P1 Content. P1 Content.
[foo](/foo)
-- layouts/_default/baseof.html -- -- layouts/_default/baseof.html --
Baseof: {{ .Title }}| Baseof: {{ .Title }}|
{{ block "main" . }}default{{ end }} {{ block "main" . }}default{{ end }}
{{ with (templates.Defer (dict "foo" "bar")) }}
Defer.
{{ end }}
-- layouts/index.html -- -- layouts/index.html --
Home. Home.
-- layouts/_default/single.html -- -- layouts/_default/single.html --
@ -509,11 +552,81 @@ Single: {{ .Title }}|{{ .Content }}|
{{ end }} {{ end }}
` `
b := Test(t, files, TestOptRunning()) 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 { b.EditFileReplaceFunc("layouts/_default/single.html", func(s string) string {
return strings.Replace(s, "Single", "Single Edited", 1) return strings.Replace(s, "Single", "Single Edited", 1)
}).Build() }).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) { 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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with 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/herrors"
"github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/parser/pageparser" "github.com/gohugoio/hugo/parser/pageparser"
"github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page"
@ -36,7 +37,6 @@ import (
"github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/text" "github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/common/urls" "github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/output"
bp "github.com/gohugoio/hugo/bufferpool" bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/tpl"
@ -205,8 +205,7 @@ type shortcode struct {
indentation string // indentation from source. indentation string // indentation from source.
info tpl.Info // One of the output formats (arbitrary) templ *tplimpl.TemplInfo
templs []tpl.Template // All output formats
// If set, the rendered shortcode is sent as part of the surrounding content // If set, the rendered shortcode is sent as part of the surrounding content
// to Goldmark and similar. // to Goldmark and similar.
@ -230,16 +229,15 @@ func (s shortcode) insertPlaceholder() bool {
} }
func (s shortcode) needsInner() 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 { func (s shortcode) configVersion() int {
if s.info == nil { if s.templ == nil {
// Not set for inline shortcodes. // Not set for inline shortcodes.
return 2 return 2
} }
return s.templ.ParseInfo.Config.Version
return s.info.ParseInfo().Config.Version
} }
func (s shortcode) innerString() string { func (s shortcode) innerString() string {
@ -315,12 +313,12 @@ func prepareShortcode(
ctx context.Context, ctx context.Context,
level int, level int,
s *Site, s *Site,
tplVariants tpl.TemplateVariants,
sc *shortcode, sc *shortcode,
parent *ShortcodeWithPage, parent *ShortcodeWithPage,
p *pageState, po *pageOutput,
isRenderString bool, isRenderString bool,
) (shortcodeRenderer, error) { ) (shortcodeRenderer, error) {
p := po.p
toParseErr := func(err error) error { toParseErr := func(err error) error {
source := p.m.content.mustSource() source := p.m.content.mustSource()
return p.parseError(fmt.Errorf("failed to render shortcode %q: %w", sc.name, err), source, sc.pos) 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. // parsed and rendered by Goldmark.
ctx = tpl.Context.IsInGoldmark.Set(ctx, true) 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 { if err != nil {
return nil, false, toParseErr(err) return nil, false, toParseErr(err)
} }
@ -352,30 +350,29 @@ func doRenderShortcode(
ctx context.Context, ctx context.Context,
level int, level int,
s *Site, s *Site,
tplVariants tpl.TemplateVariants,
sc *shortcode, sc *shortcode,
parent *ShortcodeWithPage, parent *ShortcodeWithPage,
p *pageState, po *pageOutput,
isRenderString bool, isRenderString bool,
) (shortcodeRenderer, error) { ) (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 // Tracks whether this shortcode or any of its children has template variations
// in other languages or output formats. We are currently only interested in // in other languages or output formats. We are currently only interested in
// the output formats, so we may get some false positives -- we // the output formats.
// should improve on that.
var hasVariants bool var hasVariants bool
if sc.isInline { if sc.isInline {
if !p.s.ExecHelper.Sec().EnableInlineShortcodes { if !p.s.ExecHelper.Sec().EnableInlineShortcodes {
return zeroShortcode, nil return zeroShortcode, nil
} }
templName := path.Join("_inline_shortcode", p.Path(), sc.name) templatePath := path.Join("_inline_shortcode", p.Path(), sc.name)
if sc.isClosing { if sc.isClosing {
templStr := sc.innerString() templStr := sc.innerString()
var err error var err error
tmpl, err = s.TextTmpl().Parse(templName, templStr) tmpl, err = s.TemplateStore.TextParse(templatePath, templStr)
if err != nil { if err != nil {
if isRenderString { if isRenderString {
return zeroShortcode, p.wrapError(err) return zeroShortcode, p.wrapError(err)
@ -389,21 +386,32 @@ func doRenderShortcode(
} else { } else {
// Re-use of shortcode defined earlier in the same page. // Re-use of shortcode defined earlier in the same page.
var found bool tmpl = s.TemplateStore.TextLookup(templatePath)
tmpl, found = s.TextTmpl().Lookup(templName) if tmpl == nil {
if !found {
return zeroShortcode, fmt.Errorf("no earlier definition of shortcode %q found", sc.name) return zeroShortcode, fmt.Errorf("no earlier definition of shortcode %q found", sc.name)
} }
} }
tmpl = tpl.AddIdentity(tmpl)
} else { } else {
var found, more bool ofCount := map[string]int{}
tmpl, found, more = s.Tmpl().LookupVariant(sc.name, tplVariants) include := func(match *tplimpl.TemplInfo) bool {
if !found { 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()) s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path())
return zeroShortcode, nil return zeroShortcode, nil
} }
hasVariants = hasVariants || more tmpl = v
hasVariants = hasVariants || len(ofCount) > 1
} }
data := &ShortcodeWithPage{ data := &ShortcodeWithPage{
@ -427,7 +435,7 @@ func doRenderShortcode(
case string: case string:
inner += innerData inner += innerData
case *shortcode: 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 { if err != nil {
return zeroShortcode, err 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 { if err != nil && sc.isInline {
fe := herrors.NewFileErrorFromName(err, p.File().Filename()) fe := herrors.NewFileErrorFromName(err, p.File().Filename())
@ -534,16 +542,11 @@ func (s *shortcodeHandler) hasName(name string) bool {
return ok 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) rendered := make(map[string]shortcodeRenderer)
tplVariants := tpl.TemplateVariants{
Language: p.Language().Lang,
OutputFormat: f,
}
for _, v := range s.shortcodes { 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 { if err != nil {
return nil, err return nil, err
} }
@ -636,7 +639,7 @@ Loop:
// we trust the template on this: // we trust the template on this:
// if there's no inner, we're done // if there's no inner, we're done
if !sc.isInline { if !sc.isInline {
if !sc.info.ParseInfo().IsInner { if !sc.templ.ParseInfo.IsInner {
return sc, nil return sc, nil
} }
} }
@ -672,14 +675,19 @@ Loop:
sc.name = currItem.ValStr(source) sc.name = currItem.ValStr(source)
// Used to check if the template expects inner content. // Used to check if the template expects inner content,
templs := s.s.Tmpl().LookupVariants(sc.name) // so just pick one arbitrarily with the same name.
if templs == nil { 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) return nil, fmt.Errorf("%s: template for shortcode %q not found", errorPrefix, sc.name)
} }
sc.templ = templ
sc.info = templs[0].(tpl.Info)
sc.templs = templs
case currItem.IsInlineShortcodeName(): case currItem.IsInlineShortcodeName():
sc.name = currItem.ValStr(source) sc.name = currItem.ValStr(source)
sc.isInline = true sc.isInline = true
@ -778,7 +786,7 @@ func expandShortcodeTokens(
return source, nil 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() buffer := bp.GetBuffer()
defer bp.PutBuffer(buffer) defer bp.PutBuffer(buffer)

View file

@ -33,14 +33,14 @@ func TestExtractShortcodes(t *testing.T) {
b := newTestSitesBuilder(t).WithSimpleConfigFile() b := newTestSitesBuilder(t).WithSimpleConfigFile()
b.WithTemplates( b.WithTemplates(
"default/single.html", `EMPTY`, "pages/single.html", `EMPTY`,
"_internal/shortcodes/tag.html", `tag`, "shortcodes/tag.html", `tag`,
"_internal/shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`, "shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`,
"_internal/shortcodes/sc1.html", `sc1`, "shortcodes/sc1.html", `sc1`,
"_internal/shortcodes/sc2.html", `sc2`, "shortcodes/sc2.html", `sc2`,
"_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`, "shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`,
"_internal/shortcodes/inner2.html", `{{.Inner}}`, "shortcodes/inner2.html", `{{.Inner}}`,
"_internal/shortcodes/inner3.html", `{{.Inner}}`, "shortcodes/inner3.html", `{{.Inner}}`,
).WithContent("page.md", `--- ).WithContent("page.md", `---
title: "Shortcodes Galore!" title: "Shortcodes Galore!"
--- ---
@ -57,10 +57,9 @@ title: "Shortcodes Galore!"
if s == nil { if s == nil {
return "<nil>" return "<nil>"
} }
var version int var version int
if s.info != nil { if s.templ != nil {
version = s.info.ParseInfo().Config.Version 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", 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)) 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) { regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) {
return func(c *qt.C, shortcode *shortcode, err error) { return func(c *qt.C, shortcode *shortcode, err error) {
c.Assert(err, qt.IsNil) 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 }}", "_default/single.json", "{{ .Content }}",
"shortcodes/myshort.html", `Short-HTML`, "shortcodes/myshort.html", `Short-HTML`,
"shortcodes/myshort.csv", `Short-CSV`, "shortcodes/myshort.csv", `Short-CSV`,
"shortcodes/myshort.txt", `Short-TXT`,
) )
b.Build(BuildCfg{}) b.Build(BuildCfg{})
@ -897,12 +897,12 @@ outputs: ["html", "css", "csv", "json"]
for i := range numPages { for i := range numPages {
b.AssertFileContent(fmt.Sprintf("public/page%d/index.html", i), "Short-HTML") 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.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 { 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/langs/i18n"
"github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/tpl/tplimpl" "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" "golang.org/x/text/unicode/norm"
"github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/paths"
@ -188,8 +194,8 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
BuildState: &deps.BuildState{ BuildState: &deps.BuildState{
OnSignalRebuild: onSignalRebuild, OnSignalRebuild: onSignalRebuild,
}, },
Counters: &deps.Counters{},
MemCache: memCache, MemCache: memCache,
TemplateProvider: tplimpl.DefaultTemplateProvider,
TranslationProvider: i18n.NewTranslationProvider(), TranslationProvider: i18n.NewTranslationProvider(),
WasmDispatchers: warpc.AllDispatchers( WasmDispatchers: warpc.AllDispatchers(
warpc.Options{ warpc.Options{
@ -385,6 +391,34 @@ func newHugoSites(cfg deps.DepsCfg, d *deps.Deps, pageTrees *pageTrees, sites []
var prototype *deps.Deps var prototype *deps.Deps
for i, s := range sites { for i, s := range sites {
s.h = h 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 { if err := s.Deps.Compile(prototype); err != nil {
return nil, err return nil, err
} }
@ -464,7 +498,10 @@ func (s *Site) MainSections() []string {
// Returns a struct with some information about the build. // Returns a struct with some information about the build.
func (s *Site) Hugo() hugo.HugoInfo { 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") panic("site: hugo: hugoInfo not initialized")
} }
return s.h.hugoInfo return s.h.hugoInfo
@ -797,7 +834,7 @@ func (s *Site) initRenderFormats() {
s.renderFormats = formats s.renderFormats = formats
} }
func (s *Site) GetRelatedDocsHandler() *page.RelatedDocsHandler { func (s *Site) GetInternalRelatedDocsHandler() *page.RelatedDocsHandler {
return s.relatedDocsHandler return s.relatedDocsHandler
} }
@ -923,19 +960,24 @@ type WhatChanged struct {
mu sync.Mutex mu sync.Mutex
needsPagesAssembly bool 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) { func (w *WhatChanged) Add(ids ...identity.Identity) {
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
if w.identitySet == nil { w.init()
w.identitySet = make(identity.Identities)
}
for _, id := range ids { for _, id := range ids {
w.identitySet[id] = true w.ids[id] = true
} }
} }
@ -946,20 +988,20 @@ func (w *WhatChanged) Clear() {
} }
func (w *WhatChanged) clear() { func (w *WhatChanged) clear() {
w.identitySet = identity.Identities{} w.ids = nil
} }
func (w *WhatChanged) Changes() []identity.Identity { func (w *WhatChanged) Changes() []identity.Identity {
if w == nil || w.identitySet == nil { if w == nil || w.ids == nil {
return nil return nil
} }
return w.identitySet.AsSlice() return xmaps.Keys(w.ids)
} }
func (w *WhatChanged) Drain() []identity.Identity { func (w *WhatChanged) Drain() []identity.Identity {
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
ids := w.identitySet.AsSlice() ids := w.Changes()
w.clear() w.clear()
return ids return ids
} }
@ -1394,7 +1436,7 @@ const (
pageDependencyScopeGlobal 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) s.h.buildCounters.pageRenderCounter.Add(1)
renderBuffer := bp.GetBuffer() renderBuffer := bp.GetBuffer()
defer bp.PutBuffer(renderBuffer) defer bp.PutBuffer(renderBuffer)
@ -1453,8 +1495,8 @@ var infoOnMissingLayout = map[string]bool{
// hookRendererTemplate is the canonical implementation of all hooks.ITEMRenderer, // hookRendererTemplate is the canonical implementation of all hooks.ITEMRenderer,
// where ITEM is the thing being hooked. // where ITEM is the thing being hooked.
type hookRendererTemplate struct { type hookRendererTemplate struct {
templateHandler tpl.TemplateHandler templateHandler *tplimpl.TemplateStore
templ tpl.Template templ *tplimpl.TemplInfo
resolvePosition func(ctx any) text.Position resolvePosition func(ctx any) text.Position
} }
@ -1490,7 +1532,7 @@ func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool {
return false 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 { if templ == nil {
s.logMissingLayout(name, "", "", outputFormat) s.logMissingLayout(name, "", "", outputFormat)
return nil return nil
@ -1500,7 +1542,7 @@ func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string,
panic("nil context") 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 filename := name
if p, ok := d.(*pageState); ok { if p, ok := d.(*pageState); ok {
filename = p.String() filename = p.String()

View file

@ -27,6 +27,7 @@ func createDefaultOutputFormats(allFormats output.Formats) map[string]output.For
htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name) htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name)
robotsOut, _ := allFormats.GetByName(output.RobotsTxtFormat.Name) robotsOut, _ := allFormats.GetByName(output.RobotsTxtFormat.Name)
sitemapOut, _ := allFormats.GetByName(output.SitemapFormat.Name) sitemapOut, _ := allFormats.GetByName(output.SitemapFormat.Name)
httpStatus404Out, _ := allFormats.GetByName(output.HTTPStatus404HTMLFormat.Name)
defaultListTypes := output.Formats{htmlOut} defaultListTypes := output.Formats{htmlOut}
if rssFound { 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. // Below are for consistency. They are currently not used during rendering.
kinds.KindSitemap: {sitemapOut}, kinds.KindSitemap: {sitemapOut},
kinds.KindRobotsTXT: {robotsOut}, kinds.KindRobotsTXT: {robotsOut},
kinds.KindStatus404: {htmlOut}, kinds.KindStatus404: {httpStatus404Out},
} }
// May be disabled // 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.KindRSS], deepEqualsOutputFormats, output.Formats{output.RSSFormat})
c.Assert(outputs[kinds.KindSitemap], deepEqualsOutputFormats, output.Formats{output.SitemapFormat}) c.Assert(outputs[kinds.KindSitemap], deepEqualsOutputFormats, output.Formats{output.SitemapFormat})
c.Assert(outputs[kinds.KindRobotsTXT], deepEqualsOutputFormats, output.Formats{output.RobotsTxtFormat}) 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 // Issue #4528
@ -481,6 +481,7 @@ permalinkable = true
[outputFormats.nobase] [outputFormats.nobase]
mediaType = "application/json" mediaType = "application/json"
permalinkable = true permalinkable = true
isPlainText = true
` `

View file

@ -23,9 +23,9 @@ import (
"github.com/bep/logg" "github.com/bep/logg"
"github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugolib/doctree" "github.com/gohugoio/hugo/hugolib/doctree"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/resources/kinds" "github.com/gohugoio/hugo/resources/kinds"
"github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page"
@ -57,7 +57,7 @@ func (s siteRenderContext) shouldRenderStandalonePage(kind string) bool {
return s.outIdx == 0 return s.outIdx == 0
} }
if kind == kinds.KindStatus404 { if kind == kinds.KindTemporary || kind == kinds.KindStatus404 {
// 1 for all output formats // 1 for all output formats
return s.outIdx == 0 return s.outIdx == 0
} }
@ -168,7 +168,7 @@ func pageRenderer(
s.Log.Trace( s.Log.Trace(
func() string { 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. // 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 paginatePath := s.Conf.Pagination().Path
d := p.targetPathDescriptor d := p.targetPathDescriptor

View file

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

View file

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

View file

@ -26,6 +26,8 @@ import (
"github.com/gohugoio/hugo/hugofs" "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) { func TestTemplateLookupOrder(t *testing.T) {
var ( var (
fs *hugofs.Fs fs *hugofs.Fs
@ -185,6 +187,9 @@ func TestTemplateLookupOrder(t *testing.T) {
} { } {
this := this this := this
if this.name != "Variant 1" {
continue
}
t.Run(this.name, func(t *testing.T) { t.Run(this.name, func(t *testing.T) {
// TODO(bep) there are some function vars need to pull down here to enable => t.Parallel() // TODO(bep) there are some function vars need to pull down here to enable => t.Parallel()
cfg, fs = newTestCfg() cfg, fs = newTestCfg()
@ -200,7 +205,7 @@ Some content
} }
buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) 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) this.assert(t)
}) })
@ -270,11 +275,11 @@ func TestTemplateNoBasePlease(t *testing.T) {
b := newTestSitesBuilder(t).WithSimpleConfigFile() b := newTestSitesBuilder(t).WithSimpleConfigFile()
b.WithTemplates("_default/list.html", ` b.WithTemplates("_default/list.html", `
{{ define "main" }} {{ define "main" }}
Bonjour Bonjour
{{ end }} {{ end }}
{{ printf "list" }} {{ printf "list" }}
`) `)
@ -344,33 +349,36 @@ title: %s
b.AssertFileContent("public/p1/index.html", `Single: P1`) b.AssertFileContent("public/p1/index.html", `Single: P1`)
}) })
t.Run("baseof", func(t *testing.T) { {
t.Parallel() }
b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig() }
b.WithTemplatesAdded( func TestTemplateLookupSitBaseOf(t *testing.T) {
"index.html", `{{ define "main" }}Main Home En{{ end }}`, t.Parallel()
"index.fr.html", `{{ define "main" }}Main Home Fr{{ end }}`, b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
"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", `--- 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 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/en/index.html", `Baseof en: Main Home En`)
b.AssertFileContent("public/fr/index.html", `Baseof fr: Main Home Fr`) 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/index.html", `Baseof mysection: Main Default List`)
b.AssertFileContent("public/en/mysection/p1/index.html", `Baseof mysection: Main Default Single`) b.AssertFileContent("public/en/mysection/p1/index.html", `Baseof mysection: Main Default Single`)
})
} }
func TestTemplateFuncs(t *testing.T) { func TestTemplateFuncs(t *testing.T) {
@ -707,6 +715,7 @@ a: {{ $a }}
b.AssertFileContent("public/index.html", `a: [a b c]`) b.AssertFileContent("public/index.html", `a: [a b c]`)
} }
// Legacy behavior for internal templates.
func TestOverrideInternalTemplate(t *testing.T) { func TestOverrideInternalTemplate(t *testing.T) {
files := ` files := `
-- hugo.toml -- -- hugo.toml --

View file

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

View file

@ -43,7 +43,7 @@ import (
"github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/resources/resource_factories/create" "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/mitchellh/mapstructure"
"github.com/spf13/cast" "github.com/spf13/cast"
) )
@ -192,7 +192,7 @@ type BatcherClient struct {
d *deps.Deps d *deps.Deps
once sync.Once once sync.Once
runnerTemplate tpl.Template runnerTemplate *tplimpl.TemplInfo
createClient *create.Client createClient *create.Client
buildClient *BuildClient buildClient *BuildClient
@ -208,7 +208,7 @@ func (c *BatcherClient) New(id string) (js.Batcher, error) {
c.once.Do(func() { c.once.Do(func() {
// We should fix the initialization order here (or use the Go template package directly), but we need to wait // 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. // 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 { if err != nil {
initErr = err initErr = err
return 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) { func (c *BatcherClient) buildBatchGroup(ctx context.Context, t *batchGroupTemplateContext) (resource.Resource, string, error) {
var buf bytes.Buffer 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 return nil, "", err
} }

View file

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

View file

@ -69,7 +69,7 @@ fmt.Println("Hello, World!");
## Golang Code ## Golang Code
§§§golang §§§go
fmt.Println("Hello, Golang!"); fmt.Println("Hello, Golang!");
§§§ §§§
@ -97,14 +97,14 @@ Go Language: go|
Go Code: fmt.Println("Hello, World!"); Go Code: fmt.Println("Hello, World!");
Go Code: fmt.Println("Hello, Golang!"); 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 SVG:<svg class='diagram' xmlns='http://www.w3.org/2000/svg' version='1.1' height='25' width='40'",
"Goat Attribute: 600|", "Goat Attribute: 600|",
"<h2 id=\"go-code\">Go Code</h2>\nGo Code: fmt.Println(\"Hello, World!\");\n|\nGo Language: go|", "<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>", "<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 CSSType Type
SCSSType Type SCSSType Type
SASSType Type SASSType Type
GotmplType Type
CSVType Type CSVType Type
HTMLType Type HTMLType Type
JavascriptType Type JavascriptType Type
@ -60,6 +61,7 @@ var Builtin = BuiltinTypes{
CSSType: Type{Type: "text/css"}, CSSType: Type{Type: "text/css"},
SCSSType: Type{Type: "text/x-scss"}, SCSSType: Type{Type: "text/x-scss"},
SASSType: Type{Type: "text/x-sass"}, SASSType: Type{Type: "text/x-sass"},
GotmplType: Type{Type: "text/x-gotmpl"},
CSVType: Type{Type: "text/csv"}, CSVType: Type{Type: "text/csv"},
HTMLType: Type{Type: "text/html"}, HTMLType: Type{Type: "text/html"},
JavascriptType: Type{Type: "text/javascript"}, JavascriptType: Type{Type: "text/javascript"},
@ -121,6 +123,7 @@ var defaultMediaTypesConfig = map[string]any{
"text/typescript": map[string]any{"suffixes": []string{"ts"}}, "text/typescript": map[string]any{"suffixes": []string{"ts"}},
"text/tsx": map[string]any{"suffixes": []string{"tsx"}}, "text/tsx": map[string]any{"suffixes": []string{"tsx"}},
"text/jsx": map[string]any{"suffixes": []string{"jsx"}}, "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/json": map[string]any{"suffixes": []string{"json"}},
"application/manifest+json": map[string]any{"suffixes": []string{"webmanifest"}}, "application/manifest+json": map[string]any{"suffixes": []string{"webmanifest"}},

View file

@ -17,6 +17,7 @@ import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"reflect" "reflect"
"slices"
"sort" "sort"
"strings" "strings"
@ -26,7 +27,6 @@ import (
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/spf13/cast" "github.com/spf13/cast"
"slices"
) )
// DefaultTypes is the default media types supported by Hugo. // DefaultTypes is the default media types supported by Hugo.
@ -271,4 +271,7 @@ var DefaultPathParser = &paths.PathParser{
IsContentExt: func(ext string) bool { IsContentExt: func(ext string) bool {
panic("not supported") 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) suffix = t.normalizeSuffix(suffix)
var types []Type var types []Type
for _, tt := range t { for _, tt := range t {
if tt.hasSuffix(suffix) { if tt.HasSuffix(suffix) {
types = append(types, tt) types = append(types, tt)
} }
} }
@ -293,7 +293,7 @@ func (t Types) BySuffix(suffix string) []Type {
func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) { func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
suffix = t.normalizeSuffix(suffix) suffix = t.normalizeSuffix(suffix)
for _, tt := range t { for _, tt := range t {
if tt.hasSuffix(suffix) { if tt.HasSuffix(suffix) {
return tt, SuffixInfo{ return tt, SuffixInfo{
FullSuffix: tt.Delimiter + suffix, FullSuffix: tt.Delimiter + suffix,
Suffix: 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) { func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
suffix = t.normalizeSuffix(suffix) suffix = t.normalizeSuffix(suffix)
for _, tt := range t { for _, tt := range t {
if tt.hasSuffix(suffix) { if tt.HasSuffix(suffix) {
if found { if found {
// ambiguous // ambiguous
found = false 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 { func (t Types) IsTextSuffix(suffix string) bool {
suffix = t.normalizeSuffix(suffix) suffix = t.normalizeSuffix(suffix)
for _, tt := range t { for _, tt := range t {
if tt.hasSuffix(suffix) { if tt.HasSuffix(suffix) {
return tt.IsText() return tt.IsText()
} }
} }
return false return false
} }
func (m Type) hasSuffix(suffix string) bool { func (m Type) HasSuffix(suffix string) bool {
return strings.Contains(","+m.SuffixesCSV+",", ","+suffix+",") return strings.Contains(","+m.SuffixesCSV+",", ","+suffix+",")
} }

View file

@ -1,12 +1,10 @@
package output package output
import ( import (
"strings"
// "fmt" // "fmt"
"github.com/gohugoio/hugo/docshelper" "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. // 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 { docsProvider := func() docshelper.DocProvider {
return docshelper.DocProvider{ return docshelper.DocProvider{
"output": map[string]any{ "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) 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, 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{ MarkdownFormat = Format{
Name: "markdown", Name: "markdown",
MediaType: media.Builtin.MarkdownType, MediaType: media.Builtin.MarkdownType,
@ -192,8 +201,17 @@ var (
Rel: "sitemap", Rel: "sitemap",
} }
HTTPStatusHTMLFormat = Format{ GotmplFormat = Format{
Name: "httpstatus", 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, MediaType: media.Builtin.HTMLType,
NotAlternative: true, NotAlternative: true,
Ugly: true, Ugly: true,
@ -209,12 +227,16 @@ var DefaultFormats = Formats{
CSSFormat, CSSFormat,
CSVFormat, CSVFormat,
HTMLFormat, HTMLFormat,
GotmplFormat,
HTTPStatus404HTMLFormat,
AliasHTMLFormat,
JSONFormat, JSONFormat,
MarkdownFormat, MarkdownFormat,
WebAppManifestFormat, WebAppManifestFormat,
RobotsTxtFormat, RobotsTxtFormat,
RSSFormat, RSSFormat,
SitemapFormat, SitemapFormat,
SitemapIndexFormat,
} }
func init() { func init() {

View file

@ -68,7 +68,7 @@ func TestDefaultTypes(t *testing.T) {
c.Assert(RSSFormat.NoUgly, qt.Equals, true) c.Assert(RSSFormat.NoUgly, qt.Equals, true)
c.Assert(CalendarFormat.IsHTML, qt.Equals, false) 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) { func TestGetFormatByName(t *testing.T) {
@ -140,7 +140,7 @@ func TestGetFormatByFilename(t *testing.T) {
func TestSort(t *testing.T) { func TestSort(t *testing.T) {
c := qt.New(t) c := qt.New(t)
c.Assert(DefaultFormats[0].Name, qt.Equals, "html") 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 := JSONFormat
json.Weight = 1 json.Weight = 1

View file

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

View file

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

View file

@ -145,7 +145,7 @@ func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) {
pb.isUgly = true 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 pb.noSubResources = true
} else if d.Kind != kinds.KindPage && d.URL == "" && d.Section.Base() != "/" { } else if d.Kind != kinds.KindPage && d.URL == "" && d.Section.Base() != "/" {
if d.ExpandedPermalink != "" { 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]) return nil, fmt.Errorf("invalid type %T in related search", p[0])
} }
cache := d.GetRelatedDocsHandler() cache := d.GetInternalRelatedDocsHandler()
searchIndex, err := cache.getOrCreateIndex(ctx, p) searchIndex, err := cache.getOrCreateIndex(ctx, p)
if err != nil { if err != nil {

View file

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

View file

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

View file

@ -23,17 +23,17 @@ import (
"github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/internal" "github.com/gohugoio/hugo/resources/internal"
"github.com/gohugoio/hugo/resources/resource" "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. // Client contains methods to perform template processing of Resource objects.
type Client struct { type Client struct {
rs *resources.Spec rs *resources.Spec
t tpl.TemplatesProvider t tplimpl.TemplateStoreProvider
} }
// New creates a new Client with the given specification. // 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 { if rs == nil {
panic("must provide a resource Spec") panic("must provide a resource Spec")
} }
@ -45,7 +45,7 @@ func New(rs *resources.Spec, t tpl.TemplatesProvider) *Client {
type executeAsTemplateTransform struct { type executeAsTemplateTransform struct {
rs *resources.Spec rs *resources.Spec
t tpl.TemplatesProvider t tplimpl.TemplateStoreProvider
targetPath string targetPath string
data any data any
} }
@ -56,14 +56,13 @@ func (t *executeAsTemplateTransform) Key() internal.ResourceTransformationKey {
func (t *executeAsTemplateTransform) Transform(ctx *resources.ResourceTransformationCtx) error { func (t *executeAsTemplateTransform) Transform(ctx *resources.ResourceTransformationCtx) error {
tplStr := helpers.ReaderToString(ctx.From) 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 { if err != nil {
return fmt.Errorf("failed to parse Resource %q as Template:: %w", ctx.InPath, err) return fmt.Errorf("failed to parse Resource %q as Template:: %w", ctx.InPath, err)
} }
ctx.OutPath = t.targetPath ctx.OutPath = t.targetPath
return th.ExecuteWithContext(ctx.Ctx, ti, ctx.To, t.data)
return t.t.Tmpl().ExecuteWithContext(ctx.Ctx, templ, ctx.To, t.data)
} }
func (c *Client) ExecuteAsTemplate(ctx context.Context, res resources.ResourceTransformer, targetPath string, data any) (resource.Resource, error) { 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 hugo --printUnusedTemplates
stderr 'Template _default/list.html is unused' stderr 'Template /list.html is unused'
-- hugo.toml -- -- hugo.toml --
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "404", "section", "page"] disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "404", "section", "page"]

View file

@ -21,7 +21,6 @@ import (
"strings" "strings"
"github.com/gohugoio/hugo/common/hreflect" "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. // 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) { func (ns *Namespace) lookupFunc(ctx context.Context, fname string) (reflect.Value, bool) {
namespace, methodName, ok := strings.Cut(fname, ".") namespace, methodName, ok := strings.Cut(fname, ".")
if !ok { if !ok {
templ := ns.deps.Tmpl().(tpl.TemplateFuncGetter) return ns.deps.GetTemplateStore().GetFunc(fname)
return templ.GetFunc(fname)
} }
// Namespace // 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 package template
import ( import (
"fmt"
"github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/types"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
) )
@ -51,3 +53,28 @@ func indirect(a any) any {
return in 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() name := x.Name()
src := t.set[name] src := t.set[name]
if src == nil || src.escapeErr != nil { 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() x.Tree = x.Tree.Copy()
ret.set[name] = &Template{ ret.set[name] = &Template{

View file

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

View file

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

View file

@ -20,9 +20,9 @@ import (
"math" "math"
"math/rand" "math/rand"
"reflect" "reflect"
"sync/atomic"
_math "github.com/gohugoio/hugo/common/math" _math "github.com/gohugoio/hugo/common/math"
"github.com/gohugoio/hugo/deps"
"github.com/spf13/cast" "github.com/spf13/cast"
) )
@ -32,12 +32,16 @@ var (
) )
// New returns a new instance of the math-namespaced template functions. // New returns a new instance of the math-namespaced template functions.
func New() *Namespace { func New(d *deps.Deps) *Namespace {
return &Namespace{} return &Namespace{
d: d,
}
} }
// Namespace provides template functions for the "math" namespace. // Namespace provides template functions for the "math" namespace.
type Namespace struct{} type Namespace struct {
d *deps.Deps
}
// Abs returns the absolute value of n. // Abs returns the absolute value of n.
func (ns *Namespace) Abs(n any) (float64, error) { func (ns *Namespace) Abs(n any) (float64, error) {
@ -345,8 +349,6 @@ func (ns *Namespace) doArithmetic(inputs []any, operation rune) (value any, err
return return
} }
var counter uint64
// Counter increments and returns a global counter. // Counter increments and returns a global counter.
// This was originally added to be used in tests where now.UnixNano did not // This was originally added to be used in tests where now.UnixNano did not
// have the needed precision (especially on Windows). // have the needed precision (especially on Windows).
@ -354,5 +356,5 @@ var counter uint64
// and the counter will reset on new builds. // and the counter will reset on new builds.
// <docsmeta>{"identifiers": ["now.UnixNano"] }</docsmeta> // <docsmeta>{"identifiers": ["now.UnixNano"] }</docsmeta>
func (ns *Namespace) Counter() uint64 { 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() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
type TestCase struct { type TestCase struct {
fn func(inputs ...any) (any, error) fn func(inputs ...any) (any, error)
@ -66,7 +66,7 @@ func TestBasicNSArithmetic(t *testing.T) {
func TestAbs(t *testing.T) { func TestAbs(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
x any x any
@ -93,7 +93,7 @@ func TestAbs(t *testing.T) {
func TestCeil(t *testing.T) { func TestCeil(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
x any x any
@ -126,7 +126,7 @@ func TestFloor(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
x any x any
@ -159,7 +159,7 @@ func TestLog(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
a any a any
@ -200,7 +200,7 @@ func TestSqrt(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
a any a any
@ -239,7 +239,7 @@ func TestMod(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
a any a any
@ -279,7 +279,7 @@ func TestModBool(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
a any a any
@ -325,7 +325,7 @@ func TestRound(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
x any x any
@ -358,7 +358,7 @@ func TestPow(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
a any a any
@ -398,7 +398,7 @@ func TestMax(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
type TestCase struct { type TestCase struct {
values []any values []any
@ -452,7 +452,7 @@ func TestMin(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
type TestCase struct { type TestCase struct {
values []any values []any
@ -507,7 +507,7 @@ func TestSum(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
mustSum := func(values ...any) any { mustSum := func(values ...any) any {
result, err := ns.Sum(values...) result, err := ns.Sum(values...)
@ -530,7 +530,7 @@ func TestProduct(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
mustProduct := func(values ...any) any { mustProduct := func(values ...any) any {
result, err := ns.Product(values...) result, err := ns.Product(values...)
@ -554,7 +554,7 @@ func TestPi(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
expect := 3.1415 expect := 3.1415
result := ns.Pi() result := ns.Pi()
@ -570,7 +570,7 @@ func TestSin(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
a any a any
@ -604,7 +604,7 @@ func TestCos(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
a any a any
@ -638,7 +638,7 @@ func TestTan(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
a any a any
@ -680,7 +680,7 @@ func TestTan(t *testing.T) {
func TestAsin(t *testing.T) { func TestAsin(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
x any x any
@ -715,7 +715,7 @@ func TestAsin(t *testing.T) {
func TestAcos(t *testing.T) { func TestAcos(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
x any x any
@ -751,7 +751,7 @@ func TestAcos(t *testing.T) {
func TestAtan(t *testing.T) { func TestAtan(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
x any x any
@ -782,7 +782,7 @@ func TestAtan(t *testing.T) {
func TestAtan2(t *testing.T) { func TestAtan2(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
x any x any
@ -821,7 +821,7 @@ func TestAtan2(t *testing.T) {
func TestToDegrees(t *testing.T) { func TestToDegrees(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
x any x any
@ -852,7 +852,7 @@ func TestToDegrees(t *testing.T) {
func TestToRadians(t *testing.T) { func TestToRadians(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
ns := New() ns := New(nil)
for _, test := range []struct { for _, test := range []struct {
x any x any

View file

@ -25,12 +25,12 @@ import (
"github.com/bep/lazycache" "github.com/bep/lazycache"
"github.com/gohugoio/hugo/common/constants"
"github.com/gohugoio/hugo/common/hashing" "github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/identity"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
"github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/tpl"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
bp "github.com/gohugoio/hugo/bufferpool" bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
@ -54,13 +54,6 @@ func (k partialCacheKey) Key() string {
return hashing.HashString(append([]any{k.Name}, k.Variants...)...) 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. // partialCache represents a LRU cache of partials.
type partialCache struct { type partialCache struct {
cache *lazycache.Cache[string, includeResult] 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 { 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. // Create a new context with a timeout not connected to the incoming context.
timeoutCtx, cancel := context.WithTimeout(context.Background(), ns.deps.Conf.Timeout()) timeoutCtx, cancel := context.WithTimeout(context.Background(), ns.deps.Conf.Timeout())
defer cancel() defer cancel()
@ -159,28 +157,14 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
if len(dataList) > 0 { if len(dataList) > 0 {
data = dataList[0] data = dataList[0]
} }
name, desc := ns.deps.TemplateStore.TemplateDescriptorFromPath(name)
var n string v := ns.deps.TemplateStore.LookupPartial(name, desc)
if strings.HasPrefix(name, "partials/") { if v == nil {
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 {
return includeResult{err: fmt.Errorf("partial %q not found", name)} return includeResult{err: fmt.Errorf("partial %q not found", name)}
} }
var info tpl.ParseInfo templ := v
if ip, ok := templ.(tpl.Info); ok { info := v.ParseInfo
info = ip.ParseInfo()
}
var w io.Writer var w io.Writer
@ -200,7 +184,7 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
w = b 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} 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 { if ctx, ok := data.(*contextWrapper); ok {
result = ctx.Result result = ctx.Result
} else if _, ok := templ.(*texttemplate.Template); ok { } else if _, ok := templ.Template.(*texttemplate.Template); ok {
result = w.(fmt.Stringer).String() result = w.(fmt.Stringer).String()
} else { } else {
result = template.HTML(w.(fmt.Stringer).String()) result = template.HTML(w.(fmt.Stringer).String())
} }
return includeResult{ return includeResult{
name: templ.Name(), name: templ.Template.Name(),
result: result, 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. // The templates that gets executed is measured in Execute.
// We need to track the time spent in the cache to // We need to track the time spent in the cache to
// get the totals correct. // 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 { if r.mangager != nil && depsManagerIn != nil {

View file

@ -170,7 +170,7 @@ D1
got := buf.String() got := buf.String()
// Get rid of all the durations, they are never the same. // 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 { normalize := func(s string) string {
s = durationRe.ReplaceAllString(s, "") s = durationRe.ReplaceAllString(s, "")
@ -193,10 +193,10 @@ D1
expect := ` expect := `
0 0 0 1 index.html 0 0 0 1 index.html
100 0 0 1 partials/static2.html 100 0 0 1 _partials/static2.html
100 50 1 2 partials/static1.html 100 50 1 2 _partials/static1.html
25 50 2 4 partials/dynamic1.html 25 50 2 4 _partials/dynamic1.html
66 33 1 3 partials/halfdynamic1.html 66 33 1 3 _partials/halfdynamic1.html
` `
b.Assert(got, hqt.IsSameString, expect) 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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -16,9 +16,6 @@ package tpl
import ( import (
"context" "context"
"io"
"reflect"
"regexp"
"strings" "strings"
"sync" "sync"
"unicode" "unicode"
@ -27,140 +24,18 @@ import (
"github.com/gohugoio/hugo/common/hcontext" "github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/langs" "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" htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" 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. // Template is the common interface between text/template and html/template.
type Template interface { type Template interface {
Name() string Name() string
Prepare() (*texttemplate.Template, error) Prepare() (*texttemplate.Template, error)
} }
// AddIdentity checks if t is an identity.Identity and returns it if so. // RenderingContext represents the currently rendered site/language.
// 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)
}
type RenderingContext struct { type RenderingContext struct {
Site site Site site
SiteOutIdx int SiteOutIdx int
@ -201,7 +76,9 @@ type site interface {
} }
const ( const (
// HugoDeferredTemplatePrefix is the prefix for placeholders for deferred templates.
HugoDeferredTemplatePrefix = "__hdeferred/" HugoDeferredTemplatePrefix = "__hdeferred/"
// HugoDeferredTemplateSuffix is the suffix for placeholders for deferred templates.
HugoDeferredTemplateSuffix = "__d=" HugoDeferredTemplateSuffix = "__d="
) )
@ -243,10 +120,11 @@ func StripHTML(s string) string {
return s return s
} }
// DeferredExecution holds the template and data for a deferred execution.
type DeferredExecution struct { type DeferredExecution struct {
Mu sync.Mutex Mu sync.Mutex
Ctx context.Context Ctx context.Context
TemplateName string TemplatePath string
Data any Data any
Executed bool 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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -15,20 +15,8 @@ package tpl
import ( import (
"testing" "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) { func TestStripHTML(t *testing.T) {
type test struct { type test struct {
input, expected string 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) { func TestDeferBasic(t *testing.T) {
t.Parallel() t.Parallel()

View file

@ -44,7 +44,7 @@ type Namespace struct {
// Note that this is the Unix-styled relative path including filename suffix, // Note that this is the Unix-styled relative path including filename suffix,
// e.g. partials/header.html // e.g. partials/header.html
func (ns *Namespace) Exists(name string) bool { 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. // 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, _, _ = ns.deps.BuildState.DeferredExecutions.Executions.GetOrCreate(id,
func() (*tpl.DeferredExecution, error) { func() (*tpl.DeferredExecution, error) {
return &tpl.DeferredExecution{ return &tpl.DeferredExecution{
TemplateName: templateName, TemplatePath: templateName,
Ctx: ctx, Ctx: ctx,
Data: opts.Data, Data: opts.Data,
Executed: false, 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 () { window.disqus_config = function () {
{{with .Params.disqus_identifier }}this.page.identifier = '{{ . }}';{{end}} {{with .Params.disqus_identifier }}this.page.identifier = '{{ . }}';{{end}}
{{with .Params.disqus_title }}this.page.title = '{{ . }}';{{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() { (function() {
if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) { if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) {

View file

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

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