See #13541 for details.
Fixes #13545
Fixes #13515
Closes #7964
Closes #13365
Closes #12988
Closes #4891
"errors"
"fmt"
"io"
+ "maps"
"net"
"net/http"
_ "net/http/pprof"
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/livereload"
- "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/transform"
"github.com/gohugoio/hugo/transform/livereloadinject"
"github.com/spf13/afero"
"github.com/spf13/fsync"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
- "maps"
)
var (
// To allow the en user to change the error template while the server is running, we use
// the freshest template we can provide.
var (
- errTempl tpl.Template
- templHandler tpl.TemplateHandler
+ errTempl *tplimpl.TemplInfo
+ templHandler *tplimpl.TemplateStore
)
- getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (tpl.Template, tpl.TemplateHandler) {
+ getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (*tplimpl.TemplInfo, *tplimpl.TemplateStore) {
if h == nil {
return errTempl, templHandler
}
- templHandler := h.Tmpl()
- errTempl, found := templHandler.Lookup("_server/error.html")
- if !found {
+ templHandler := h.GetTemplateStore()
+ errTempl := templHandler.LookupByPath("/_server/error.html")
+ if errTempl == nil {
panic("template server/error.html not found")
}
return errTempl, templHandler
WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html"
WarnGoldmarkRawHTML = "warning-goldmark-raw-html"
+ WarnPartialSuperfluousPrefix = "warning-partial-superfluous-prefix"
)
// Field/method names with special meaning.
import (
"fmt"
"regexp"
+ "slices"
"strings"
"sync"
"github.com/gohugoio/hugo/compare"
- "slices"
)
var _ compare.Eqer = StringEqualFold("")
return "", false
}
-type Tuple struct {
- First string
- Second string
-}
+type (
+ Strings2 [2]string
+ Strings3 [3]string
+)
return v, nil
}
+// Contains returns whether the given key exists in the cache.
+func (c *Cache[K, T]) Contains(key K) bool {
+ c.RLock()
+ _, found := c.m[key]
+ c.RUnlock()
+ return found
+}
+
// InitAndGet initializes the cache if not already done and returns the value for the given key.
// The init state will be reset on Reset or Drain.
func (c *Cache[K, T]) InitAndGet(key K, init func(get func(key K) (T, bool), set func(key K, value T)) error) (T, error) {
c.Unlock()
}
+// SetIfAbsent sets the given key to the given value if the key does not already exist in the cache.
+func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
+ c.RLock()
+ if _, found := c.get(key); !found {
+ c.RUnlock()
+ c.Set(key, value)
+ } else {
+ c.RUnlock()
+ }
+}
+
func (c *Cache[K, T]) set(key K, value T) {
c.m[key] = value
}
package maps
import (
- "github.com/gohugoio/hugo/common/hashing"
"slices"
+
+ "github.com/gohugoio/hugo/common/hashing"
)
// Ordered is a map that can be iterated in the order of insertion.
return value, found
}
+// Has returns whether the given key exists in the map.
+func (m *Ordered[K, T]) Has(key K) bool {
+ if m == nil {
+ return false
+ }
+ _, found := m.values[key]
+ return found
+}
+
// Delete deletes the value for the given key.
func (m *Ordered[K, T]) Delete(key K) {
if m == nil {
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/resources/kinds"
+)
+
+const (
+ identifierBaseof = "baseof"
)
// PathParser parses a path into a Path.
// Reports whether the given language is disabled.
IsLangDisabled func(string) bool
+ // IsOutputFormat reports whether the given name is a valid output format.
+ // The second argument is optional.
+ IsOutputFormat func(name, ext string) bool
+
// Reports whether the given ext is a content file.
IsContentExt func(string) bool
}
}
func (pp *PathParser) newPath(component string) *Path {
- return &Path{
- component: component,
- posContainerLow: -1,
- posContainerHigh: -1,
- posSectionHigh: -1,
- posIdentifierLanguage: -1,
- }
+ p := &Path{}
+ p.reset()
+ p.component = component
+ return p
}
func (pp *PathParser) parse(component, s string) (*Path, error) {
return p, nil
}
-func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
- hasLang := pp.LanguageIndex != nil
- hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
+func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot int) {
+ if p.posContainerHigh != -1 {
+ return
+ }
+ mayHaveLang := pp.LanguageIndex != nil
+ mayHaveLang = mayHaveLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
+ mayHaveOutputFormat := component == files.ComponentFolderLayouts
+ mayHaveKind := mayHaveOutputFormat
+
+ var found bool
+ var high int
+ if len(p.identifiers) > 0 {
+ high = lastDot
+ } else {
+ high = len(p.s)
+ }
+ id := types.LowHigh[string]{Low: i + 1, High: high}
+ sid := p.s[id.Low:id.High]
+
+ if len(p.identifiers) == 0 {
+ // The first is always the extension.
+ p.identifiers = append(p.identifiers, id)
+ found = true
+
+ // May also be the output format.
+ if mayHaveOutputFormat && pp.IsOutputFormat(sid, "") {
+ p.posIdentifierOutputFormat = 0
+ }
+ } else {
+
+ var langFound bool
+
+ if mayHaveLang {
+ var disabled bool
+ _, langFound = pp.LanguageIndex[sid]
+ if !langFound {
+ disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(sid)
+ if disabled {
+ p.disabled = true
+ langFound = true
+ }
+ }
+ found = langFound
+ if langFound {
+ p.identifiers = append(p.identifiers, id)
+ p.posIdentifierLanguage = len(p.identifiers) - 1
+
+ }
+ }
+
+ if !found && mayHaveOutputFormat {
+ // At this point we may already have resolved an output format,
+ // but we need to keep looking for a more specific one, e.g. amp before html.
+ // Use both name and extension to prevent
+ // false positives on the form css.html.
+ if pp.IsOutputFormat(sid, p.Ext()) {
+ found = true
+ p.identifiers = append(p.identifiers, id)
+ p.posIdentifierOutputFormat = len(p.identifiers) - 1
+ }
+ }
+
+ if !found && mayHaveKind {
+ if kinds.GetKindMain(sid) != "" {
+ found = true
+ p.identifiers = append(p.identifiers, id)
+ p.posIdentifierKind = len(p.identifiers) - 1
+ }
+ }
+
+ if !found && sid == identifierBaseof {
+ found = true
+ p.identifiers = append(p.identifiers, id)
+ p.posIdentifierBaseof = len(p.identifiers) - 1
+ }
+
+ if !found {
+ p.identifiers = append(p.identifiers, id)
+ p.identifiersUnknown = append(p.identifiersUnknown, len(p.identifiers)-1)
+ }
+
+ }
+}
+func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
if runtime.GOOS == "windows" {
s = path.Clean(filepath.ToSlash(s))
if s == "." {
p.s = s
slashCount := 0
+ lastDot := 0
for i := len(s) - 1; i >= 0; i-- {
c := s[i]
switch c {
case '.':
- if p.posContainerHigh == -1 {
- var high int
- if len(p.identifiers) > 0 {
- high = p.identifiers[len(p.identifiers)-1].Low - 1
- } else {
- high = len(p.s)
- }
- id := types.LowHigh[string]{Low: i + 1, High: high}
- if len(p.identifiers) == 0 {
- p.identifiers = append(p.identifiers, id)
- } else if len(p.identifiers) == 1 {
- // Check for a valid language.
- s := p.s[id.Low:id.High]
-
- if hasLang {
- var disabled bool
- _, langFound := pp.LanguageIndex[s]
- if !langFound {
- disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(s)
- if disabled {
- p.disabled = true
- langFound = true
- }
- }
- if langFound {
- p.posIdentifierLanguage = 1
- p.identifiers = append(p.identifiers, id)
- }
- }
- }
- }
+ pp.parseIdentifier(component, s, p, i, lastDot)
+ lastDot = i
case '/':
slashCount++
if p.posContainerHigh == -1 {
+ if lastDot > 0 {
+ pp.parseIdentifier(component, s, p, i, lastDot)
+ }
p.posContainerHigh = i + 1
} else if p.posContainerLow == -1 {
p.posContainerLow = i + 1
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
isContent := isContentComponent && pp.IsContentExt(p.Ext())
id := p.identifiers[len(p.identifiers)-1]
- b := p.s[p.posContainerHigh : id.Low-1]
- if isContent {
- switch b {
- case "index":
- p.bundleType = PathTypeLeaf
- case "_index":
- p.bundleType = PathTypeBranch
- default:
- p.bundleType = PathTypeContentSingle
+
+ if id.High > p.posContainerHigh {
+ b := p.s[p.posContainerHigh:id.High]
+ if isContent {
+ switch b {
+ case "index":
+ p.pathType = TypeLeaf
+ case "_index":
+ p.pathType = TypeBranch
+ default:
+ p.pathType = TypeContentSingle
+ }
+
+ if slashCount == 2 && p.IsLeafBundle() {
+ p.posSectionHigh = 0
+ }
+ } else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
+ p.pathType = TypeContentData
}
+ }
+
+ }
- if slashCount == 2 && p.IsLeafBundle() {
- p.posSectionHigh = 0
+ 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
}
}
func ModifyPathBundleTypeResource(p *Path) {
if p.IsContent() {
- p.bundleType = PathTypeContentResource
+ p.pathType = TypeContentResource
} else {
- p.bundleType = PathTypeFile
+ p.pathType = TypeFile
}
}
-type PathType int
+//go:generate stringer -type Type
+
+type Type int
const (
+
// A generic resource, e.g. a JSON file.
- PathTypeFile PathType = iota
+ TypeFile Type = iota
// All below are content files.
// A resource of a content type with front matter.
- PathTypeContentResource
+ TypeContentResource
// E.g. /blog/my-post.md
- PathTypeContentSingle
+ TypeContentSingle
// All below are bundled content files.
// Leaf bundles, e.g. /blog/my-post/index.md
- PathTypeLeaf
+ TypeLeaf
// Branch bundles, e.g. /blog/_index.md
- PathTypeBranch
+ TypeBranch
// Content data file, _content.gotmpl.
- PathTypeContentData
+ TypeContentData
+
+ // Layout types.
+ TypeMarkup
+ TypeShortcode
+ TypePartial
+ TypeBaseof
)
type Path struct {
posContainerHigh int
posSectionHigh int
- component string
- bundleType PathType
+ component string
+ pathType Type
identifiers []types.LowHigh[string]
- posIdentifierLanguage int
- disabled bool
+ posIdentifierLanguage int
+ posIdentifierOutputFormat int
+ posIdentifierKind int
+ posIdentifierBaseof int
+ identifiersUnknown []int
+ disabled bool
trimLeadingSlash bool
p.posContainerHigh = -1
p.posSectionHigh = -1
p.component = ""
- p.bundleType = 0
+ p.pathType = 0
p.identifiers = p.identifiers[:0]
p.posIdentifierLanguage = -1
+ p.posIdentifierOutputFormat = -1
+ p.posIdentifierKind = -1
+ p.posIdentifierBaseof = -1
p.disabled = false
p.trimLeadingSlash = false
p.unnormalized = nil
// IdentifierBase satisfies identity.Identity.
func (p *Path) IdentifierBase() string {
+ if p.Component() == files.ComponentFolderLayouts {
+ return p.Path()
+ }
return p.Base()
}
return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1])
}
+func (p *Path) String() string {
+ if p == nil {
+ return "<nil>"
+ }
+ return p.Path()
+}
+
// ContainerDir returns the container directory for this path.
// For content bundles this will be the parent directory.
func (p *Path) ContainerDir() string {
// IsContent returns true if the path is a content file (e.g. mypost.md).
// Note that this will also return true for content files in a bundle.
func (p *Path) IsContent() bool {
- return p.BundleType() >= PathTypeContentResource
+ return p.Type() >= TypeContentResource && p.Type() <= TypeContentData
}
// isContentPage returns true if the path is a content file (e.g. mypost.md),
// but nof if inside a leaf bundle.
func (p *Path) isContentPage() bool {
- return p.BundleType() >= PathTypeContentSingle
+ return p.Type() >= TypeContentSingle && p.Type() <= TypeContentData
}
// Name returns the last element of path.
// NameNoIdentifier returns the last element of path without any identifier (e.g. no extension).
func (p *Path) NameNoIdentifier() string {
+ lowHigh := p.nameLowHigh()
+ return p.s[lowHigh.Low:lowHigh.High]
+}
+
+func (p *Path) nameLowHigh() types.LowHigh[string] {
if len(p.identifiers) > 0 {
- return p.s[p.posContainerHigh : p.identifiers[len(p.identifiers)-1].Low-1]
+ lastID := p.identifiers[len(p.identifiers)-1]
+ if p.posContainerHigh == lastID.Low {
+ // The last identifier is the name.
+ return lastID
+ }
+ return types.LowHigh[string]{
+ Low: p.posContainerHigh,
+ High: p.identifiers[len(p.identifiers)-1].Low - 1,
+ }
+ }
+ return types.LowHigh[string]{
+ Low: p.posContainerHigh,
+ High: len(p.s),
}
- return p.s[p.posContainerHigh:]
}
// Dir returns all but the last element of path, typically the path's directory.
return p.norm(p.s)
}
+// PathNoLeadingSlash returns the full path without the leading slash.
+func (p *Path) PathNoLeadingSlash() string {
+ return p.Path()[1:]
+}
+
// Unnormalized returns the Path with the original case preserved.
func (p *Path) Unnormalized() *Path {
return p.unnormalized
return p.base(false, false)
}
+// PathBeforeLangAndOutputFormatAndExt returns the path up to the first identifier that is not a language or output format.
+func (p *Path) PathBeforeLangAndOutputFormatAndExt() string {
+ if len(p.identifiers) == 0 {
+ return p.norm(p.s)
+ }
+ i := p.identifierIndex(0)
+
+ if j := p.posIdentifierOutputFormat; i == -1 || (j != -1 && j < i) {
+ i = j
+ }
+ if j := p.posIdentifierLanguage; i == -1 || (j != -1 && j < i) {
+ i = j
+ }
+
+ if i == -1 {
+ return p.norm(p.s)
+ }
+
+ id := p.identifiers[i]
+ return p.norm(p.s[:id.Low-1])
+}
+
// PathRel returns the path relative to the given owner.
func (p *Path) PathRel(owner *Path) string {
ob := owner.Base()
return p.base(!p.isContentPage(), p.IsBundle())
}
+// Used in template lookups.
+// For pages with Type set, we treat that as the section.
+func (p *Path) BaseReTyped(typ string) (d string) {
+ base := p.Base()
+ if typ == "" || p.Section() == typ {
+ return base
+ }
+ d = "/" + typ
+ if p.posSectionHigh != -1 {
+ d += base[p.posSectionHigh:]
+ }
+ d = p.norm(d)
+ return
+}
+
// BaseNoLeadingSlash returns the base path without the leading slash.
func (p *Path) BaseNoLeadingSlash() string {
return p.Base()[1:]
return p.norm(p.s)
}
- id := p.identifiers[len(p.identifiers)-1]
- high := id.Low - 1
+ var high int
if isBundle {
high = p.posContainerHigh - 1
+ } else {
+ high = p.nameLowHigh().High
}
if high == 0 {
}
// 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.identifierAsString(0)
}
+func (p *Path) OutputFormat() string {
+ return p.identifierAsString(p.posIdentifierOutputFormat)
+}
+
+func (p *Path) Kind() string {
+ return p.identifierAsString(p.posIdentifierKind)
+}
+
func (p *Path) Lang() string {
- return p.identifierAsString(1)
+ return p.identifierAsString(p.posIdentifierLanguage)
}
func (p *Path) Identifier(i int) string {
return ids
}
-func (p *Path) BundleType() PathType {
- return p.bundleType
+func (p *Path) IdentifiersUnknown() []string {
+ ids := make([]string, len(p.identifiersUnknown))
+ for i, id := range p.identifiersUnknown {
+ ids[i] = p.s[p.identifiers[id].Low:p.identifiers[id].High]
+ }
+ return ids
+}
+
+func (p *Path) Type() Type {
+ return p.pathType
}
func (p *Path) IsBundle() bool {
- return p.bundleType >= PathTypeLeaf
+ return p.pathType >= TypeLeaf && p.pathType <= TypeContentData
}
func (p *Path) IsBranchBundle() bool {
- return p.bundleType == PathTypeBranch
+ return p.pathType == TypeBranch
}
func (p *Path) IsLeafBundle() bool {
- return p.bundleType == PathTypeLeaf
+ return p.pathType == TypeLeaf
}
func (p *Path) IsContentData() bool {
- return p.bundleType == PathTypeContentData
+ return p.pathType == TypeContentData
}
-func (p Path) ForBundleType(t PathType) *Path {
- p.bundleType = t
+func (p Path) ForBundleType(t Type) *Path {
+ p.pathType = t
return &p
}
"testing"
"github.com/gohugoio/hugo/hugofs/files"
+ "github.com/gohugoio/hugo/resources/kinds"
qt "github.com/frankban/quicktest"
)
LanguageIndex: map[string]int{
"no": 0,
"en": 1,
+ "fr": 2,
},
IsContentExt: func(ext string) bool {
return ext == "md"
},
+ IsOutputFormat: func(name, ext string) bool {
+ switch name {
+ case "html", "amp", "csv", "rss":
+ return true
+ }
+ return false
+ },
}
func TestParse(t *testing.T) {
"Basic Markdown file",
"/a/b/c.md",
func(c *qt.C, p *Path) {
+ c.Assert(p.Ext(), qt.Equals, "md")
+ c.Assert(p.Type(), qt.Equals, TypeContentSingle)
c.Assert(p.IsContent(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsFalse)
c.Assert(p.Name(), qt.Equals, "c.md")
c.Assert(p.Base(), qt.Equals, "/a/b/c")
+ c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b/c")
c.Assert(p.Section(), qt.Equals, "a")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c")
c.Assert(p.Path(), qt.Equals, "/a/b/c.md")
c.Assert(p.Dir(), qt.Equals, "/a/b")
c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a/b")
- c.Assert(p.Ext(), qt.Equals, "md")
},
},
{
// Reclassify it as a content resource.
ModifyPathBundleTypeResource(p)
- c.Assert(p.BundleType(), qt.Equals, PathTypeContentResource)
+ c.Assert(p.Type(), qt.Equals, TypeContentResource)
c.Assert(p.IsContent(), qt.IsTrue)
c.Assert(p.Name(), qt.Equals, "b.md")
c.Assert(p.Base(), qt.Equals, "/a/b.md")
"/a/b.a.b.no.txt",
func(c *qt.C, p *Path) {
c.Assert(p.Name(), qt.Equals, "b.a.b.no.txt")
- c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b")
+ c.Assert(p.NameNoIdentifier(), qt.Equals, "b")
c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt")
- c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
- c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt")
- c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt")
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no", "b", "a", "b"})
+ c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"b", "a", "b"})
+ c.Assert(p.Base(), qt.Equals, "/a/b.txt")
+ c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.txt")
c.Assert(p.Path(), qt.Equals, "/a/b.a.b.no.txt")
- c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt")
+ c.Assert(p.PathNoLang(), qt.Equals, "/a/b.txt")
c.Assert(p.Ext(), qt.Equals, "txt")
- c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b")
+ c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b")
},
},
{
"/_index.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/")
+ c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo")
c.Assert(p.Path(), qt.Equals, "/_index.md")
c.Assert(p.Container(), qt.Equals, "")
c.Assert(p.ContainerDir(), qt.Equals, "/")
"/a/index.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a")
+ c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/a")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "a")
c.Assert(p.Container(), qt.Equals, "a")
c.Assert(p.Container(), qt.Equals, "a")
c.Assert(p.ContainerDir(), qt.Equals, "")
c.Assert(p.Dir(), qt.Equals, "/a")
c.Assert(p.Ext(), qt.Equals, "md")
- c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "index"})
c.Assert(p.IsBranchBundle(), qt.IsFalse)
c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsTrue)
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
+ c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b")
c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a")
c.Assert(p.Dir(), qt.Equals, "/a/b")
c.Assert(p.Ext(), qt.Equals, "md")
- c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no", "index"})
c.Assert(p.IsBranchBundle(), qt.IsFalse)
c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsTrue)
c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a")
c.Assert(p.Ext(), qt.Equals, "md")
- c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no", "_index"})
c.Assert(p.IsBranchBundle(), qt.IsTrue)
c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsFalse)
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/index.txt")
c.Assert(p.Ext(), qt.Equals, "txt")
- c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no", "index"})
c.Assert(p.IsLeafBundle(), qt.IsFalse)
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b/index")
},
}
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
+ if test.name != "Basic Markdown file" {
+ // return
+ }
test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
})
}
}
+func TestParseLayouts(t *testing.T) {
+ c := qt.New(t)
+
+ tests := []struct {
+ name string
+ path string
+ assert func(c *qt.C, p *Path)
+ }{
+ {
+ "Basic",
+ "/list.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/list.html")
+ c.Assert(p.OutputFormat(), qt.Equals, "html")
+ },
+ },
+ {
+ "Lang",
+ "/list.no.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "list"})
+ c.Assert(p.Base(), qt.Equals, "/list.html")
+ c.Assert(p.Lang(), qt.Equals, "no")
+ },
+ },
+ {
+ "Lang and output format",
+ "/list.no.amp.not.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "not", "amp", "no", "list"})
+ c.Assert(p.OutputFormat(), qt.Equals, "amp")
+ c.Assert(p.Ext(), qt.Equals, "html")
+ c.Assert(p.Lang(), qt.Equals, "no")
+ c.Assert(p.Base(), qt.Equals, "/list.html")
+ },
+ },
+ {
+ "Term",
+ "/term.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Base(), qt.Equals, "/term.html")
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "term"})
+ c.Assert(p.PathNoIdentifier(), qt.Equals, "/term")
+ c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/term")
+ c.Assert(p.Lang(), qt.Equals, "")
+ c.Assert(p.Kind(), qt.Equals, "term")
+ c.Assert(p.OutputFormat(), qt.Equals, "html")
+ },
+ },
+ {
+ "Sub dir",
+ "/pages/home.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "home"})
+ c.Assert(p.Lang(), qt.Equals, "")
+ c.Assert(p.Kind(), qt.Equals, "home")
+ c.Assert(p.OutputFormat(), qt.Equals, "html")
+ c.Assert(p.Dir(), qt.Equals, "/pages")
+ },
+ },
+ {
+ "Baseof",
+ "/pages/baseof.list.section.fr.amp.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "amp", "fr", "section", "list", "baseof"})
+ c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"list"})
+ c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
+ c.Assert(p.Lang(), qt.Equals, "fr")
+ c.Assert(p.OutputFormat(), qt.Equals, "amp")
+ c.Assert(p.Dir(), qt.Equals, "/pages")
+ c.Assert(p.NameNoIdentifier(), qt.Equals, "baseof")
+ c.Assert(p.Type(), qt.Equals, TypeBaseof)
+ c.Assert(p.IdentifierBase(), qt.Equals, "/pages/baseof.list.section.fr.amp.html")
+ },
+ },
+ {
+ "Markup",
+ "/_markup/render-link.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Type(), qt.Equals, TypeMarkup)
+ },
+ },
+ {
+ "Markup nested",
+ "/foo/_markup/render-link.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Type(), qt.Equals, TypeMarkup)
+ },
+ },
+ {
+ "Shortcode",
+ "/_shortcodes/myshortcode.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Type(), qt.Equals, TypeShortcode)
+ },
+ },
+ {
+ "Shortcode nested",
+ "/foo/_shortcodes/myshortcode.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Type(), qt.Equals, TypeShortcode)
+ },
+ },
+ {
+ "Shortcode nested sub",
+ "/foo/_shortcodes/foo/myshortcode.html",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Type(), qt.Equals, TypeShortcode)
+ },
+ },
+ {
+ "Partials",
+ "/_partials/foo.bar",
+ func(c *qt.C, p *Path) {
+ c.Assert(p.Type(), qt.Equals, TypePartial)
+ },
+ },
+ }
+
+ for _, test := range tests {
+ c.Run(test.name, func(c *qt.C) {
+ test.assert(c, testParser.Parse(files.ComponentFolderLayouts, test.path))
+ })
+ }
+}
+
func TestHasExt(t *testing.T) {
c := qt.New(t)
+++ /dev/null
-// 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]]
-}
--- /dev/null
+// 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]]
+}
RUnlock()
}
+type Locker interface {
+ Lock()
+ Unlock()
+}
+
+type RWLocker interface {
+ RLocker
+ Locker
+}
+
// KeyValue is a interface{} tuple.
type KeyValue struct {
Key any
c.Languages = languages
c.LanguagesDefaultFirst = languagesDefaultFirst
- c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix}
+ c.ContentPathParser = &paths.PathParser{
+ LanguageIndex: languagesDefaultFirst.AsIndexSet(),
+ IsLangDisabled: c.Base.IsLangDisabled,
+ IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix,
+ IsOutputFormat: func(name, ext string) bool {
+ if name == "" {
+ return false
+ }
+
+ if of, ok := c.Base.OutputFormats.Config.GetByName(name); ok {
+ if ext != "" && !of.MediaType.HasSuffix(ext) {
+ return false
+ }
+ return true
+ }
+ return false
+ },
+ }
c.configLangs = make([]config.AllProvider, len(c.Languages))
for i, l := range c.LanguagesDefaultFirst {
func (b *contentBuilder) mapArcheTypeDir() error {
var m archetypeMap
- seen := map[hstrings.Tuple]bool{}
+ seen := map[hstrings.Strings2]bool{}
walkFn := func(path string, fim hugofs.FileMetaInfo) error {
if fim.IsDir() {
pi := fim.Meta().PathInfo
if pi.IsContent() {
- pathLang := hstrings.Tuple{First: pi.PathNoIdentifier(), Second: fim.Meta().Lang}
+ pathLang := hstrings.Strings2{pi.PathBeforeLangAndOutputFormatAndExt(), fim.Meta().Lang}
if seen[pathLang] {
// Duplicate content file, e.g. page.md and page.html.
// In the regular build, we will filter out the duplicates, but
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/postpub"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/metrics"
"github.com/gohugoio/hugo/resources"
ExecHelper *hexec.Exec
- // The templates to use. This will usually implement the full tpl.TemplateManager.
- tmplHandlers *tpl.TemplateHandlers
-
- // The template funcs.
- TmplFuncMap map[string]any
-
// The file systems to use.
Fs *hugofs.Fs `json:"-"`
// The site building.
Site page.Site
- TemplateProvider ResourceProvider
+ TemplateStore *tplimpl.TemplateStore
+
// Used in tests
OverloadedTemplateFuncs map[string]any
// This is common/global for all sites.
BuildState *BuildState
+ // Misc counters.
+ Counters *Counters
+
// Holds RPC dispatchers for Katex etc.
// TODO(bep) rethink this re. a plugin setup, but this will have to do for now.
WasmDispatchers *warpc.Dispatchers
// The JS batcher client.
JSBatcherClient js.BatcherClient
- // The JS batcher client.
- // JSBatcherClient *esbuild.BatcherClient
-
isClosed bool
*globalErrHandler
return &d, nil
}
-func (d *Deps) SetTempl(t *tpl.TemplateHandlers) {
- d.tmplHandlers = t
+func (d *Deps) GetTemplateStore() *tplimpl.TemplateStore {
+ return d.TemplateStore
}
func (d *Deps) Init() error {
logger: d.Log,
}
}
-
if d.BuildState == nil {
d.BuildState = &BuildState{}
}
+ if d.Counters == nil {
+ d.Counters = &Counters{}
+ }
if d.BuildState.DeferredExecutions == nil {
if d.BuildState.DeferredExecutionsGroupedByRenderingContext == nil {
d.BuildState.DeferredExecutionsGroupedByRenderingContext = make(map[tpl.RenderingContext]*DeferredExecutions)
return nil
}
+// TODO(bep) rework this to get it in line with how we manage templates.
func (d *Deps) Compile(prototype *Deps) error {
var err error
if prototype == nil {
- if err = d.TemplateProvider.NewResource(d); err != nil {
- return err
- }
+
if err = d.TranslationProvider.NewResource(d); err != nil {
return err
}
return nil
}
- if err = d.TemplateProvider.CloneResource(d, prototype); err != nil {
- return err
- }
-
if err = d.TranslationProvider.CloneResource(d, prototype); err != nil {
return err
}
CloneResource(dst, src *Deps) error
}
-func (d *Deps) Tmpl() tpl.TemplateHandler {
- return d.tmplHandlers.Tmpl
-}
-
-func (d *Deps) TextTmpl() tpl.TemplateParseFinder {
- return d.tmplHandlers.TxtTmpl
-}
-
func (d *Deps) Close() error {
if d.isClosed {
return nil
DeferredExecutionsGroupedByRenderingContext map[tpl.RenderingContext]*DeferredExecutions
}
+// Misc counters.
+type Counters struct {
+ // Counter for the math.Counter function.
+ MathCounter atomic.Uint64
+}
+
type DeferredExecutions struct {
// A set of filenames in /public that
// contains a post-processing prefix.
"github.com/gohugoio/hugo/publisher"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/tpl"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
)
type aliasHandler struct {
- t tpl.TemplateHandler
+ ts *tplimpl.TemplateStore
log loggers.Logger
allowRoot bool
}
-func newAliasHandler(t tpl.TemplateHandler, l loggers.Logger, allowRoot bool) aliasHandler {
- return aliasHandler{t, l, allowRoot}
+func newAliasHandler(ts *tplimpl.TemplateStore, l loggers.Logger, allowRoot bool) aliasHandler {
+ return aliasHandler{ts, l, allowRoot}
}
type aliasPage struct {
}
func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, error) {
- var templ tpl.Template
- var found bool
-
- templ, found = a.t.Lookup("alias.html")
- if !found {
- // TODO(bep) consolidate
- templ, found = a.t.Lookup("_internal/alias.html")
- if !found {
- return nil, errors.New("no alias template found")
- }
+ var templateDesc tplimpl.TemplateDescriptor
+ var base string = ""
+ if ps, ok := p.(*pageState); ok {
+ base, templateDesc = ps.getTemplateBasePathAndDescriptor()
+ }
+ templateDesc.Layout = ""
+ templateDesc.Kind = ""
+ templateDesc.OutputFormat = output.AliasHTMLFormat.Name
+
+ q := tplimpl.TemplateQuery{
+ Path: base,
+ Category: tplimpl.CategoryLayout,
+ Desc: templateDesc,
+ }
+
+ t := a.ts.LookupPagesLayout(q)
+ if t == nil {
+ return nil, errors.New("no alias template found")
}
data := aliasPage{
ctx := tpl.Context.Page.Set(context.Background(), p)
buffer := new(bytes.Buffer)
- err := a.t.ExecuteWithContext(ctx, templ, buffer, data)
+ err := a.ts.ExecuteWithContext(ctx, t, buffer, data)
if err != nil {
return nil, err
}
}
func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) {
- handler := newAliasHandler(s.Tmpl(), s.Log, allowRoot)
+ handler := newAliasHandler(s.GetTemplateStore(), s.Log, allowRoot)
targetPath, err := handler.targetPathAlias(path)
if err != nil {
func TestAliasTemplate(t *testing.T) {
t.Parallel()
- b := newTestSitesBuilder(t)
- b.WithSimpleConfigFile().WithContent("page.md", pageWithAlias).WithTemplatesAdded("alias.html", aliasTemplate)
+ files := `
+-- hugo.toml --
+baseURL = "http://example.com"
+-- layouts/single.html --
+Single.
+-- layouts/home.html --
+Home.
+-- layouts/alias.html --
+ALIASTEMPLATE
+-- content/page.md --
+---
+title: "Page"
+aliases: ["/foo/bar/"]
+---
+`
- b.CreateSites().Build(BuildCfg{})
+ b := Test(t, files)
// the real page
- b.AssertFileContent("public/page/index.html", "For some moments the old man")
+ b.AssertFileContent("public/page/index.html", "Single.")
// the alias redirector
b.AssertFileContent("public/foo/bar/index.html", "ALIASTEMPLATE")
}
templateSource = f.shortcodeReplacerPre.Replace(templateSource)
- templ, err := ps.s.TextTmpl().Parse("archetype.md", string(templateSource))
+ templ, err := ps.s.TemplateStore.TextParse("archetype.md", templateSource)
if err != nil {
return fmt.Errorf("failed to parse archetype template: %s: %w", err, err)
}
- result, err := executeToString(context.Background(), ps.s.Tmpl(), templ, d)
+ result, err := executeToString(context.Background(), ps.s.GetTemplateStore(), templ, d)
if err != nil {
return fmt.Errorf("failed to execute archetype template: %s: %w", err, err)
}
meta := fi.Meta()
pi := meta.PathInfo
- switch pi.BundleType() {
- case paths.PathTypeFile, paths.PathTypeContentResource:
+ switch pi.Type() {
+ case paths.TypeFile, paths.TypeContentResource:
m.s.Log.Trace(logg.StringFunc(
func() string {
return fmt.Sprintf("insert resource: %q", fi.Meta().Filename)
addErr = err
return
}
- case paths.PathTypeContentData:
+ case paths.TypeContentData:
pc, rc, err := m.addPagesFromGoTmplFi(fi, buildConfig)
pageCount += pc
resourceCount += rc
DepsFromSite: func(s page.Site) pagesfromdata.PagesFromTemplateDeps {
ss := s.(*Site)
return pagesfromdata.PagesFromTemplateDeps{
- TmplFinder: ss.TextTmpl(),
- TmplExec: ss.Tmpl(),
+ TemplateStore: ss.GetTemplateStore(),
}
},
DependencyManager: s.Conf.NewIdentityManager("pagesfromdata"),
if p.Component() == files.ComponentFolderContent {
// It may also be a bundled content resource.
- key := p.ForBundleType(paths.PathTypeContentResource).Base()
+ key := p.ForBundleType(paths.TypeContentResource).Base()
tree = t.treeResources
nCount = 0
tree.ForEeachInDimension(key, doctree.DimensionLanguage.Index(),
checkedCounter atomic.Int64
)
- resetPo := func(po *pageOutput, r identity.FinderResult) {
- if po.pco != nil {
+ resetPo := func(po *pageOutput, rebuildContent bool, r identity.FinderResult) {
+ if rebuildContent && po.pco != nil {
po.pco.Reset() // Will invalidate content cache.
}
po.renderState = 0
po.p.resourcesPublishInit = &sync.Once{}
- if r == identity.FinderFoundOneOfMany || po.f.Name == output.HTTPStatusHTMLFormat.Name {
+ if r == identity.FinderFoundOneOfMany || po.f.Name == output.HTTPStatus404HTMLFormat.Name {
// Will force a re-render even in fast render mode.
po.renderOnce = false
}
}
// This can be a relativeley expensive operations, so we do it in parallel.
- g := rungroup.Run[*pageState](ctx, rungroup.Config[*pageState]{
+ g := rungroup.Run(ctx, rungroup.Config[*pageState]{
NumWorkers: h.numWorkers,
Handle: func(ctx context.Context, p *pageState) error {
if !p.isRenderedAny() {
checkedCounter.Add(1)
if r := depsFinder.Contains(id, p.dependencyManager, 2); r > identity.FinderFoundOneOfManyRepetition {
for _, po := range p.pageOutputs {
- resetPo(po, r)
+ // Note that p.dependencyManager is used when rendering content, so reset that.
+ resetPo(po, true, r)
}
// Done.
return nil
for _, id := range changes {
checkedCounter.Add(1)
if r := depsFinder.Contains(id, po.dependencyManagerOutput, 50); r > identity.FinderFoundOneOfManyRepetition {
- resetPo(po, r)
+ // Note that dependencyManagerOutput is not used when rendering content, so don't reset that.
+ resetPo(po, false, r)
continue OUTPUTS
}
}
tree.InsertIntoValuesDimension(key, p)
}
- addStandalone("/404", kinds.KindStatus404, output.HTTPStatusHTMLFormat)
+ addStandalone("/404", kinds.KindStatus404, output.HTTPStatus404HTMLFormat)
if s.conf.EnableRobotsTXT {
if m.i == 0 || s.Conf.IsMultihost() {
}
func TestBundleMultipleContentPageWithSamePath(t *testing.T) {
+ t.Parallel()
+
files := `
-- hugo.toml --
+printPathWarnings = true
+-- layouts/all.html --
+All.
-- content/bundle/index.md --
---
title: "Bundle md"
P1: {{ $p1.Title }}|{{ $p1.Params.foo }}|{{ $p1.File.Filename }}|
`
- b := Test(t, files)
+ for range 3 {
+ b := Test(t, files, TestOptWarn())
- // There's multiple content files sharing the same logical path and language.
- // This is a little arbitrary, but we have to pick one and prefer the Markdown version.
- b.AssertFileContent("public/index.html",
- filepath.FromSlash("Bundle: Bundle md|md|/content/bundle/index.md|"),
- filepath.FromSlash("P1: P1 md|md|/content/p1.md|"),
- )
+ b.AssertLogContains("WARN Duplicate content path: \"/p1\"")
+
+ // There's multiple content files sharing the same logical path and language.
+ // This is a little arbitrary, but we have to pick one and prefer the Markdown version.
+ b.AssertFileContent("public/index.html",
+ filepath.FromSlash("Bundle: Bundle md|md|/content/bundle/index.md|"),
+ filepath.FromSlash("P1: P1 md|md|/content/p1.md|"),
+ )
+ }
}
// Issue #11944
"fmt"
"strings"
"testing"
+
+ qt "github.com/frankban/quicktest"
)
func TestRenderHooksRSS(t *testing.T) {
<h1 id="heading-in-p1">Heading in p1</h1>
<h1 id="heading-in-p2">Heading in p2</h1>
`)
+
b.AssertFileContent("public/index.xml", `
P2: <p>P2. xml-link: https://www.bep.is|</p>
P3: <p>P3. xml-link: https://www.example.org|</p>
"|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)
+}
package doctree
import (
+ "iter"
"sync"
radix "github.com/armon/go-radix"
)
-// Tree is a radix tree that holds T.
+// Tree is a non thread safe radix tree that holds T.
type Tree[T any] interface {
+ TreeCommon[T]
+ WalkPrefix(s string, f func(s string, v T) (bool, error)) error
+ WalkPath(s string, f func(s string, v T) (bool, error)) error
+ All() iter.Seq2[string, T]
+}
+
+// TreeThreadSafe is a thread safe radix tree that holds T.
+type TreeThreadSafe[T any] interface {
+ TreeCommon[T]
+ WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error
+ WalkPath(lockType LockType, s string, f func(s string, v T) (bool, error)) error
+ All(lockType LockType) iter.Seq2[string, T]
+}
+
+type TreeCommon[T any] interface {
Get(s string) T
LongestPrefix(s string) (string, T)
Insert(s string, v T) T
- WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error
}
-// NewSimpleTree creates a new SimpleTree.
-func NewSimpleTree[T comparable]() *SimpleTree[T] {
+func NewSimpleTree[T any]() *SimpleTree[T] {
return &SimpleTree[T]{tree: radix.New()}
}
-// SimpleTree is a thread safe radix tree that holds T.
-type SimpleTree[T comparable] struct {
- mu sync.RWMutex
+// SimpleTree is a radix tree that holds T.
+// This tree is not thread safe.
+type SimpleTree[T any] struct {
tree *radix.Tree
zero T
}
func (tree *SimpleTree[T]) Get(s string) T {
- tree.mu.RLock()
- defer tree.mu.RUnlock()
-
if v, ok := tree.tree.Get(s); ok {
return v.(T)
}
}
func (tree *SimpleTree[T]) LongestPrefix(s string) (string, T) {
- tree.mu.RLock()
- defer tree.mu.RUnlock()
-
if s, v, ok := tree.tree.LongestPrefix(s); ok {
return s, v.(T)
}
}
func (tree *SimpleTree[T]) Insert(s string, v T) T {
+ tree.tree.Insert(s, v)
+ return v
+}
+
+func (tree *SimpleTree[T]) Walk(f func(s string, v T) (bool, error)) error {
+ var err error
+ tree.tree.Walk(func(s string, v any) bool {
+ var b bool
+ b, err = f(s, v.(T))
+ if err != nil {
+ return true
+ }
+ return b
+ })
+ return err
+}
+
+func (tree *SimpleTree[T]) WalkPrefix(s string, f func(s string, v T) (bool, error)) error {
+ var err error
+ tree.tree.WalkPrefix(s, func(s string, v any) bool {
+ var b bool
+ b, err = f(s, v.(T))
+ if err != nil {
+ return true
+ }
+ return b
+ })
+
+ return err
+}
+
+func (tree *SimpleTree[T]) WalkPath(s string, f func(s string, v T) (bool, error)) error {
+ var err error
+ tree.tree.WalkPath(s, func(s string, v any) bool {
+ var b bool
+ b, err = f(s, v.(T))
+ if err != nil {
+ return true
+ }
+ return b
+ })
+ return err
+}
+
+func (tree *SimpleTree[T]) All() iter.Seq2[string, T] {
+ return func(yield func(s string, v T) bool) {
+ tree.tree.Walk(func(s string, v any) bool {
+ return !yield(s, v.(T))
+ })
+ }
+}
+
+// NewSimpleThreadSafeTree creates a new SimpleTree.
+func NewSimpleThreadSafeTree[T any]() *SimpleThreadSafeTree[T] {
+ return &SimpleThreadSafeTree[T]{tree: radix.New(), mu: new(sync.RWMutex)}
+}
+
+// SimpleThreadSafeTree is a thread safe radix tree that holds T.
+type SimpleThreadSafeTree[T any] struct {
+ mu *sync.RWMutex
+ noLock bool
+ tree *radix.Tree
+ zero T
+}
+
+var noopFunc = func() {}
+
+func (tree *SimpleThreadSafeTree[T]) readLock() func() {
+ if tree.noLock {
+ return noopFunc
+ }
+ tree.mu.RLock()
+ return tree.mu.RUnlock
+}
+
+func (tree *SimpleThreadSafeTree[T]) writeLock() func() {
+ if tree.noLock {
+ return noopFunc
+ }
tree.mu.Lock()
- defer tree.mu.Unlock()
+ return tree.mu.Unlock
+}
+
+func (tree *SimpleThreadSafeTree[T]) Get(s string) T {
+ unlock := tree.readLock()
+ defer unlock()
+
+ if v, ok := tree.tree.Get(s); ok {
+ return v.(T)
+ }
+ return tree.zero
+}
+
+func (tree *SimpleThreadSafeTree[T]) LongestPrefix(s string) (string, T) {
+ unlock := tree.readLock()
+ defer unlock()
+
+ if s, v, ok := tree.tree.LongestPrefix(s); ok {
+ return s, v.(T)
+ }
+ return "", tree.zero
+}
+
+func (tree *SimpleThreadSafeTree[T]) Insert(s string, v T) T {
+ unlock := tree.writeLock()
+ defer unlock()
tree.tree.Insert(s, v)
return v
}
-func (tree *SimpleTree[T]) Lock(lockType LockType) func() {
+func (tree *SimpleThreadSafeTree[T]) Lock(lockType LockType) func() {
switch lockType {
case LockTypeNone:
- return func() {}
+ return noopFunc
case LockTypeRead:
tree.mu.RLock()
return tree.mu.RUnlock
tree.mu.Lock()
return tree.mu.Unlock
}
- return func() {}
+ return noopFunc
}
-func (tree *SimpleTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
+func (tree SimpleThreadSafeTree[T]) LockTree(lockType LockType) (TreeThreadSafe[T], func()) {
+ unlock := tree.Lock(lockType)
+ tree.noLock = true
+ return &tree, unlock // create a copy of tree with the noLock flag set to true.
+}
+
+func (tree *SimpleThreadSafeTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
commit := tree.Lock(lockType)
defer commit()
var err error
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]
"fmt"
"strings"
"sync"
-
- radix "github.com/armon/go-radix"
)
var _ MutableTrees = MutableTrees{}
ctx.HooksPost = append(ctx.HooksPost, handler)
}
-func (ctx *WalkContext[T]) Data() *SimpleTree[any] {
+func (ctx *WalkContext[T]) Data() *SimpleThreadSafeTree[any] {
ctx.dataInit.Do(func() {
- ctx.data = &SimpleTree[any]{
- tree: radix.New(),
- }
+ ctx.data = NewSimpleThreadSafeTree[any]()
})
return ctx.data
}
// WalkContext is passed to the Walk callback.
type WalkContext[T any] struct {
- data *SimpleTree[any]
+ data *SimpleThreadSafeTree[any]
dataInit sync.Once
eventHandlers eventHandlers[T]
events []*Event[T]
package doctree
-var _ Tree[string] = (*TreeShiftTree[string])(nil)
+import "iter"
+
+var _ TreeThreadSafe[string] = (*TreeShiftTree[string])(nil)
type TreeShiftTree[T comparable] struct {
// This tree is shiftable in one dimension.
zero T
// Will be of length equal to the length of the dimension.
- trees []*SimpleTree[T]
+ trees []*SimpleThreadSafeTree[T]
}
func NewTreeShiftTree[T comparable](d, length int) *TreeShiftTree[T] {
if length <= 0 {
panic("length must be > 0")
}
- trees := make([]*SimpleTree[T], length)
+ trees := make([]*SimpleThreadSafeTree[T], length)
for i := range length {
- trees[i] = NewSimpleTree[T]()
+ trees[i] = NewSimpleThreadSafeTree[T]()
}
return &TreeShiftTree[T]{d: d, trees: trees}
}
return nil
}
+func (t *TreeShiftTree[T]) WalkPath(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
+ return t.trees[t.v].WalkPath(lockType, s, f)
+}
+
+func (t *TreeShiftTree[T]) All(lockType LockType) iter.Seq2[string, T] {
+ return t.trees[t.v].All(lockType)
+}
+
func (t *TreeShiftTree[T]) LenRaw() int {
var count int
for _, tt := range t.trees {
return nil
}
-// Only used in tests.
type buildCounters struct {
contentRenderCounter atomic.Uint64
pageRenderCounter atomic.Uint64
higherPrecedentData := current[r.BaseFileName()]
switch data.(type) {
- case nil:
case map[string]any:
switch higherPrecedentData.(type) {
defer deferred.Mu.Unlock()
if !deferred.Executed {
- tmpl := s.Deps.Tmpl()
- templ, found := tmpl.Lookup(deferred.TemplateName)
- if !found {
- panic(fmt.Sprintf("template %q not found", deferred.TemplateName))
+ tmpl := s.Deps.GetTemplateStore()
+ ti := s.TemplateStore.LookupByPath(deferred.TemplatePath)
+ if ti == nil {
+ panic(fmt.Sprintf("template %q not found", deferred.TemplatePath))
}
if err := func() error {
buf := bufferpool.GetBuffer()
defer bufferpool.PutBuffer(buf)
- err = tmpl.ExecuteWithContext(deferred.Ctx, templ, buf, deferred.Data)
+ err = tmpl.ExecuteWithContext(deferred.Ctx, ti, buf, deferred.Data)
if err != nil {
return err
}
h.printUnusedTemplatesInit.Do(func() {
conf := h.Configs.Base
if conf.PrintUnusedTemplates {
- unusedTemplates := h.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
+ unusedTemplates := h.GetTemplateStore().UnusedTemplates()
for _, unusedTemplate := range unusedTemplates {
- h.Log.Warnf("Template %s is unused, source file %s", unusedTemplate.Name(), unusedTemplate.Filename())
+ if unusedTemplate.Fi != nil {
+ h.Log.Warnf("Template %s is unused, source %q", unusedTemplate.PathInfo.Path(), unusedTemplate.Fi.Meta().Filename)
+ } else {
+ h.Log.Warnf("Template %s is unused", unusedTemplate.PathInfo.Path())
+ }
}
}
})
case files.ComponentFolderLayouts:
tmplChanged = true
templatePath := pathInfo.Unnormalized().TrimLeadingSlash().PathNoLang()
- if !h.Tmpl().HasTemplate(templatePath) {
+ if !h.GetTemplateStore().HasTemplate(templatePath) {
tmplAdded = true
}
}
} else {
logger.Println("Template changed", pathInfo.Path())
- if templ, found := h.Tmpl().GetIdentity(templatePath); found {
- changes = append(changes, templ)
+ id := h.GetTemplateStore().GetIdentity(pathInfo.Path())
+ if id != nil {
+ changes = append(changes, id)
} else {
changes = append(changes, pathInfo)
}
changed := &WhatChanged{
needsPagesAssembly: needsPagesAssemble,
- identitySet: make(identity.Identities),
}
changed.Add(changes...)
}
}
- h.Deps.OnChangeListeners.Notify(changed.Changes()...)
+ changes2 := changed.Changes()
+ h.Deps.OnChangeListeners.Notify(changes2...)
if err := h.resolveAndClearStateForIdentities(ctx, l, cacheBusterOr, changed.Drain()); err != nil {
return err
}
- if tmplChanged || i18nChanged {
+ if tmplChanged {
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
- // TODO(bep) this could probably be optimized to somehow
- // only load the changed templates and its dependencies, but that is non-trivial.
+ depsFinder := identity.NewFinder(identity.FinderConfig{})
ll := l.WithField("substep", "rebuild templates")
+ s := h.Sites[0]
+ if err := s.Deps.TemplateStore.RefreshFiles(func(fi hugofs.FileMetaInfo) bool {
+ pi := fi.Meta().PathInfo
+ for _, id := range changes2 {
+ if depsFinder.Contains(pi, id, -1) > 0 {
+ return true
+ }
+ }
+ return false
+ }); err != nil {
+ return ll, err
+ }
+
+ return ll, nil
+ }); err != nil {
+ return err
+ }
+ }
+
+ if i18nChanged {
+ if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
+ ll := l.WithField("substep", "rebuild i18n")
var prototype *deps.Deps
for i, s := range h.Sites {
if err := s.Deps.Compile(prototype); err != nil {
`
- b := Test(t, files)
-
- b.AssertFileContent("public/index.html", `List: |/|html: /|rss: /index.xml|$`)
- b.AssertFileContent("public/index.xml", `List xml: |/|html: /|rss: /index.xml|$`)
- b.AssertFileContent("public/p1/index.html", `Single: Page|/p1/|html: /p1/|$`)
- b.AssertFileExists("public/p1/index.xml", false)
+ for i := 0; i < 2; i++ {
+ b := Test(t, files)
+ b.AssertFileContent("public/index.html", `List: |/|html: /|rss: /index.xml|$`)
+ b.AssertFileContent("public/index.xml", `List xml: |/|html: /|rss: /index.xml|$`)
+ b.AssertFileContent("public/p1/index.html", `Single: Page|/p1/|html: /p1/|$`)
+ b.AssertFileExists("public/p1/index.xml", false)
+ }
}
func TestSmoke(t *testing.T) {
type lockingBuffer struct {
sync.Mutex
- bytes.Buffer
+ buf bytes.Buffer
+}
+
+func (b *lockingBuffer) String() string {
+ b.Lock()
+ defer b.Unlock()
+ return b.buf.String()
+}
+
+func (b *lockingBuffer) Reset() {
+ b.Lock()
+ defer b.Unlock()
+ b.buf.Reset()
}
func (b *lockingBuffer) ReadFrom(r io.Reader) (n int64, err error) {
b.Lock()
- n, err = b.Buffer.ReadFrom(r)
+ n, err = b.buf.ReadFrom(r)
b.Unlock()
return
}
func (b *lockingBuffer) Write(p []byte) (n int, err error) {
b.Lock()
- n, err = b.Buffer.Write(p)
+ n, err = b.buf.Write(p)
b.Unlock()
return
}
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/output"
- "github.com/gohugoio/hugo/output/layouts"
"github.com/gohugoio/hugo/related"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/tableofcontents"
- "github.com/gohugoio/hugo/tpl"
-
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/types"
resourcesPublishInit *sync.Once
}
+func (p *pageState) incrPageOutputTemplateVariation() {
+ p.pageOutputTemplateVariationsState.Add(1)
+}
+
+func (p *pageState) canReusePageOutputContent() bool {
+ return p.pageOutputTemplateVariationsState.Load() == 1
+}
+
func (p *pageState) IdentifierBase() string {
return p.Path()
}
// Nothing to do for now.
}
-func (p *pageState) reusePageOutputContent() bool {
- return p.pageOutputTemplateVariationsState.Load() == 1
-}
-
func (p *pageState) skipRender() bool {
b := p.s.conf.C.SegmentFilter.ShouldExcludeFine(
segments.SegmentMatcherFields{
return nil
}
-func (p *pageState) getLayoutDescriptor() layouts.LayoutDescriptor {
- p.layoutDescriptorInit.Do(func() {
- var section string
- sections := p.SectionsEntries()
-
- switch p.Kind() {
- case kinds.KindSection:
- if len(sections) > 0 {
- section = sections[0]
- }
- case kinds.KindTaxonomy, kinds.KindTerm:
-
- if p.m.singular != "" {
- section = p.m.singular
- } else if len(sections) > 0 {
- section = sections[0]
- }
- default:
- }
-
- p.layoutDescriptor = layouts.LayoutDescriptor{
- Kind: p.Kind(),
- Type: p.Type(),
- Lang: p.Language().Lang,
- Layout: p.Layout(),
- Section: section,
- }
- })
-
- return p.layoutDescriptor
+func (po *pageOutput) getTemplateBasePathAndDescriptor() (string, tplimpl.TemplateDescriptor) {
+ p := po.p
+ f := po.f
+ base := p.PathInfo().BaseReTyped(p.m.pageConfig.Type)
+ return base, tplimpl.TemplateDescriptor{
+ Kind: p.Kind(),
+ Lang: p.Language().Lang,
+ Layout: p.Layout(),
+ OutputFormat: f.Name,
+ MediaType: f.MediaType.Type,
+ IsPlainText: f.IsPlainText,
+ }
}
-func (p *pageState) resolveTemplate(layouts ...string) (tpl.Template, bool, error) {
- f := p.outputFormat()
-
- d := p.getLayoutDescriptor()
+func (p *pageState) resolveTemplate(layouts ...string) (*tplimpl.TemplInfo, bool, error) {
+ dir, d := p.getTemplateBasePathAndDescriptor()
if len(layouts) > 0 {
d.Layout = layouts[0]
- d.LayoutOverride = true
+ d.LayoutMustMatch = true
+ }
+
+ q := tplimpl.TemplateQuery{
+ Path: dir,
+ Category: tplimpl.CategoryLayout,
+ Desc: d,
+ }
+
+ tinfo := p.s.TemplateStore.LookupPagesLayout(q)
+ if tinfo == nil {
+ return nil, false, nil
}
- return p.s.Tmpl().LookupLayout(d, f)
+ return tinfo, true, nil
}
// Must be run after the site section tree etc. is built and ready.
if isRenderingSite {
cp := p.pageOutput.pco
- if cp == nil && p.reusePageOutputContent() {
+ if cp == nil && p.canReusePageOutputContent() {
// Look for content to reuse.
for i := range p.pageOutputs {
if i == idx {
"github.com/gohugoio/hugo/lazy"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/navigation"
- "github.com/gohugoio/hugo/output/layouts"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/source"
// should look like.
targetPathDescriptor page.TargetPathDescriptor
- layoutDescriptor layouts.LayoutDescriptor
- layoutDescriptorInit sync.Once
-
// Set if feature enabled and this is in a Git repo.
gitInfo source.GitInfo
codeowners []string
"strings"
"unicode/utf8"
+ maps0 "maps"
+
"github.com/bep/logg"
"github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types/hstring"
"github.com/gohugoio/hugo/helpers"
- "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
"github.com/gohugoio/hugo/tpl"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
- maps0 "maps"
)
const (
return nil, err
}
if hasShortcodeVariants {
- cp.po.p.pageOutputTemplateVariationsState.Add(1)
+ cp.po.p.incrPageOutputTemplateVariation()
}
var result contentSummary
if err := cp.initRenderHooks(); err != nil {
return nil, err
}
- f := cp.po.f
po := cp.po
p := po.p
- ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, p, f, false)
+ ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, po, false)
if err != nil {
return nil, err
}
if p.s.conf.Internal.Watch {
for _, s := range cp2.po.p.m.content.shortcodeState.shortcodes {
- for _, templ := range s.templs {
- cp.trackDependency(templ.(identity.IdentityProvider))
- }
+ cp.trackDependency(s.templ)
}
}
// Transfer shortcode names so HasShortcode works for shortcodes from included pages.
cp.po.p.m.content.shortcodeState.transferNames(cp2.po.p.m.content.shortcodeState)
if cp2.po.p.pageOutputTemplateVariationsState.Load() > 0 {
- cp.po.p.pageOutputTemplateVariationsState.Add(1)
+ cp.po.p.incrPageOutputTemplateVariation()
}
}
}
if hasVariants {
- p.pageOutputTemplateVariationsState.Add(1)
+ p.incrPageOutputTemplateVariation()
}
isHTML := cp.po.p.m.pageConfig.ContentMediaType.IsHTML()
return "", err
}
- placeholders, err := s.prepareShortcodesForPage(ctx, pco.po.p, pco.po.f, true)
+ placeholders, err := s.prepareShortcodesForPage(ctx, pco.po, true)
if err != nil {
return "", err
}
return "", err
}
if hasVariants {
- pco.po.p.pageOutputTemplateVariationsState.Add(1)
+ pco.po.p.incrPageOutputTemplateVariation()
}
b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false)
if err != nil {
return "", err
}
if hasShortcodeVariants {
- pco.po.p.pageOutputTemplateVariationsState.Add(1)
+ pco.po.p.incrPageOutputTemplateVariation()
}
}
}
if hasVariants {
- pco.po.p.pageOutputTemplateVariationsState.Add(1)
+ pco.po.p.incrPageOutputTemplateVariation()
}
if cb != nil {
// Prepare for a rebuild of the data passed in from front matter.
func (m *pageMeta) setMetaPostPrepareRebuild() {
- params := xmaps.Clone[map[string]any](m.paramsOriginal)
+ params := xmaps.Clone(m.paramsOriginal)
m.pageMetaParams.pageConfig = &pagemeta.PageConfig{
+ Kind: m.pageConfig.Kind,
+ Lang: m.pageConfig.Lang,
+ Path: m.pageConfig.Path,
Params: params,
}
m.pageMetaFrontMatter = pageMetaFrontMatter{}
}
func (p *pageMeta) BundleType() string {
- switch p.pathInfo.BundleType() {
- case paths.PathTypeLeaf:
+ switch p.pathInfo.Type() {
+ case paths.TypeLeaf:
return "leaf"
- case paths.PathTypeBranch:
+ case paths.TypeBranch:
return "branch"
default:
return ""
"errors"
"fmt"
"html/template"
- "strings"
"sync"
"sync/atomic"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/markup/converter/hooks"
- "github.com/gohugoio/hugo/markup/highlight/chromalexers"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/gohugoio/hugo/markup/converter"
bp "github.com/gohugoio/hugo/bufferpool"
- "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources/page"
}
// Make sure to send the *pageState and not the *pageContentOutput to the template.
- res, err := executeToString(ctx, pco.po.p.s.Tmpl(), templ, pco.po.p)
+ res, err := executeToString(ctx, pco.po.p.s.GetTemplateStore(), templ, pco.po.p)
if err != nil {
- return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err))
+ return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Template.Name(), err))
}
return template.HTML(res), nil
}
return r
}
- layoutDescriptor := pco.po.p.getLayoutDescriptor()
- layoutDescriptor.RenderingHook = true
- layoutDescriptor.LayoutOverride = false
- layoutDescriptor.Layout = ""
+ // Inherit the descriptor from the page/current output format.
+ // This allows for fine-grained control of the template used for
+ // rendering of e.g. links.
+ base, layoutDescriptor := pco.po.p.getTemplateBasePathAndDescriptor()
switch tp {
case hooks.LinkRendererType:
- layoutDescriptor.Kind = "render-link"
+ layoutDescriptor.Variant1 = "link"
case hooks.ImageRendererType:
- layoutDescriptor.Kind = "render-image"
+ layoutDescriptor.Variant1 = "image"
case hooks.HeadingRendererType:
- layoutDescriptor.Kind = "render-heading"
+ layoutDescriptor.Variant1 = "heading"
case hooks.PassthroughRendererType:
- layoutDescriptor.Kind = "render-passthrough"
+ layoutDescriptor.Variant1 = "passthrough"
if id != nil {
- layoutDescriptor.KindVariants = id.(string)
+ layoutDescriptor.Variant2 = id.(string)
}
case hooks.BlockquoteRendererType:
- layoutDescriptor.Kind = "render-blockquote"
+ layoutDescriptor.Variant1 = "blockquote"
if id != nil {
- layoutDescriptor.KindVariants = id.(string)
+ layoutDescriptor.Variant2 = id.(string)
}
case hooks.TableRendererType:
- layoutDescriptor.Kind = "render-table"
+ layoutDescriptor.Variant1 = "table"
case hooks.CodeBlockRendererType:
- layoutDescriptor.Kind = "render-codeblock"
+ layoutDescriptor.Variant1 = "codeblock"
if id != nil {
- lang := id.(string)
- lexer := chromalexers.Get(lang)
- if lexer != nil {
- layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",")
- } else {
- layoutDescriptor.KindVariants = lang
- }
+ layoutDescriptor.Variant2 = id.(string)
}
}
- getHookTemplate := func(f output.Format) (tpl.Template, bool) {
- templ, found, err := pco.po.p.s.Tmpl().LookupLayout(layoutDescriptor, f)
- if err != nil {
- panic(err)
+ renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
+ var ignoreInternal bool
+ switch layoutDescriptor.Variant1 {
+ case "link":
+ ignoreInternal = !renderHookConfig.Link.IsEnableDefault()
+ case "image":
+ ignoreInternal = !renderHookConfig.Image.IsEnableDefault()
+ }
+
+ candidates := pco.po.p.s.renderFormats
+ var numCandidatesFound int
+ consider := func(candidate *tplimpl.TemplInfo) bool {
+ if layoutDescriptor.Variant1 != candidate.D.Variant1 {
+ return false
}
- if found {
- if isitp, ok := templ.(tpl.IsInternalTemplateProvider); ok && isitp.IsInternalTemplate() {
-
- renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
-
- switch templ.Name() {
- case "_default/_markup/render-link.html":
- if !renderHookConfig.Link.IsEnableDefault() {
- return nil, false
- }
- case "_default/_markup/render-image.html":
- if !renderHookConfig.Image.IsEnableDefault() {
- return nil, false
- }
- }
- }
+
+ if layoutDescriptor.Variant2 != "" && candidate.D.Variant2 != "" && layoutDescriptor.Variant2 != candidate.D.Variant2 {
+ return false
}
- return templ, found
- }
- templ, found1 := getHookTemplate(pco.po.f)
- if !found1 || pco.po.p.reusePageOutputContent() {
- defaultOutputFormat := pco.po.p.s.conf.C.DefaultOutputFormat
+ if ignoreInternal && candidate.SubCategory == tplimpl.SubCategoryEmbedded {
+ // Don't consider the internal hook templates.
+ return false
+ }
- candidates := pco.po.p.s.renderFormats
+ if pco.po.p.pageOutputTemplateVariationsState.Load() > 1 {
+ return true
+ }
- // 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)
- }
+ if candidate.D.OutputFormat == "" {
+ numCandidatesFound++
+ } else if _, found := candidates.GetByName(candidate.D.OutputFormat); found {
+ numCandidatesFound++
}
- // Check if some of the other output formats would give a different template.
- for _, f := range candidates {
- if f.Name == pco.po.f.Name {
- continue
- }
- templ2, found2 := getHookTemplate(f)
-
- if found2 {
- if !found1 && f.Name == defaultOutputFormat.Name {
- templ = templ2
- found1 = true
- break
- }
-
- if templ != templ2 {
- pco.po.p.pageOutputTemplateVariationsState.Add(1)
- break
- }
- }
+ 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 {
}
r := hookRendererTemplate{
- templateHandler: pco.po.p.s.Tmpl(),
+ templateHandler: pco.po.p.s.GetTemplateStore(),
templ: templ,
resolvePosition: resolvePosition,
}
return t.paths
}
-func executeToString(ctx context.Context, h tpl.TemplateHandler, templ tpl.Template, data any) (string, error) {
+func executeToString(ctx context.Context, h *tplimpl.TemplateStore, templ *tplimpl.TemplInfo, data any) (string, error) {
b := bp.GetBuffer()
defer bp.PutBuffer(b)
if err := h.ExecuteWithContext(ctx, templ, b, data); err != nil {
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 nil, filepath.SkipDir
}
- seen := map[hstrings.Tuple]bool{}
+ seen := map[hstrings.Strings2]hugofs.FileMetaInfo{}
for _, fi := range readdir {
if fi.IsDir() {
continue
// These would eventually have been filtered out as duplicates when
// inserting them into the document store,
// but doing it here will preserve a consistent ordering.
- baseLang := hstrings.Tuple{First: pi.Base(), Second: meta.Lang}
- if seen[baseLang] {
+ baseLang := hstrings.Strings2{pi.Base(), meta.Lang}
+ if fi2, ok := seen[baseLang]; ok {
+ if c.h.Configs.Base.PrintPathWarnings && !c.h.isRebuild() {
+ c.logger.Warnf("Duplicate content path: %q file: %q file: %q", pi.Base(), fi2.Meta().Filename, meta.Filename)
+ }
continue
}
- seen[baseLang] = true
+ seen[baseLang] = fi
if pi == nil {
panic(fmt.Sprintf("no path info for %q", meta.Filename))
func (c *pagesCollector) handleBundleLeaf(dir, bundle hugofs.FileMetaInfo, inPath string, readdir []hugofs.FileMetaInfo) error {
bundlePi := bundle.Meta().PathInfo
- seen := map[hstrings.Tuple]bool{}
+ seen := map[hstrings.Strings2]bool{}
walk := func(path string, info hugofs.FileMetaInfo) error {
if info.IsDir() {
// These would eventually have been filtered out as duplicates when
// inserting them into the document store,
// but doing it here will preserve a consistent ordering.
- baseLang := hstrings.Tuple{First: pi.Base(), Second: info.Meta().Lang}
+ baseLang := hstrings.Strings2{pi.Base(), info.Meta().Lang}
if seen[baseLang] {
return nil
}
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/tpl"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
)
}
type PagesFromTemplateDeps struct {
- TmplFinder tpl.TemplateParseFinder
- TmplExec tpl.TemplateExecutor
+ TemplateStore *tplimpl.TemplateStore
}
var _ resource.Staler = (*PagesFromTemplate)(nil)
}
defer f.Close()
- tmpl, err := p.TmplFinder.Parse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
+ tmpl, err := p.TemplateStore.TextParse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
if err != nil {
return BuildInfo{}, err
}
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
}
func TestPagesFromGoTmplMisc(t *testing.T) {
t.Parallel()
- b := hugolib.Test(t, filesPagesFromDataTempleBasic)
+ b := hugolib.Test(t, filesPagesFromDataTempleBasic, hugolib.TestOptWarn())
+ b.AssertLogContains("! WARN")
b.AssertPublishDir(`
docs/p1/mytext.txt
docs/p1/sub/mytex2.tx
import (
"fmt"
- "path/filepath"
"testing"
qt "github.com/frankban/quicktest"
// Issue 6023
func TestPaginateWithSort(t *testing.T) {
- b := newTestSitesBuilder(t).WithSimpleConfigFile()
- b.WithTemplatesAdded("index.html", `{{ range (.Paginate (sort .Site.RegularPages ".File.Filename" "desc")).Pages }}|{{ .File.Filename }}{{ end }}`)
- b.Build(BuildCfg{}).AssertFileContent("public/index.html",
- filepath.FromSlash("|content/sect/doc1.nn.md|content/sect/doc1.nb.md|content/sect/doc1.fr.md|content/sect/doc1.en.md"))
+ files := `
+-- hugo.toml --
+-- content/a/a.md --
+-- content/z/b.md --
+-- content/x/b.md --
+-- content/x/a.md --
+-- layouts/home.html --
+Paginate: {{ range (.Paginate (sort .Site.RegularPages ".File.Filename" "desc")).Pages }}|{{ .Path }}{{ end }}
+`
+ b := Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Paginate: |/z/b|/x/b|/x/a|/a/a")
}
// https://github.com/gohugoio/hugo/issues/6797
func TestNilPointerErrorMessage(t *testing.T) {
files := `
--- hugo.toml --
+-- hugo.toml --
-- content/p1.md --
-- layouts/_default/single.html --
Home Filename: {{ site.Home.File.Filename }}
`
b, err := TestE(t, files)
b.Assert(err, qt.IsNotNil)
- b.Assert(err.Error(), qt.Contains, `_default/single.html:1:22: executing "_default/single.html" – File is nil; wrap it in if or with: {{ with site.Home.File }}{{ .Filename }}{{ end }}`)
+ b.Assert(err.Error(), qt.Contains, `single.html:1:22: executing "single.html" – File is nil; wrap it in if or with: {{ with site.Home.File }}{{ .Filename }}{{ end }}`)
}
title: "My Section"
---
-- content/mysection/mysectiontext.txt --
+Content.
-- content/_index.md --
---
title: "Home"
`
func TestRebuildEditLeafBundleHeaderOnly(t *testing.T) {
- b := TestRunning(t, rebuildFilesSimple)
- b.AssertFileContent("public/mysection/mysectionbundle/index.html",
- "My Section Bundle Content Content.")
-
- b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "My Section Bundle Content.", "My Section Bundle Content Edited.").Build()
- b.AssertFileContent("public/mysection/mysectionbundle/index.html",
- "My Section Bundle Content Edited.")
- b.AssertRenderCountPage(2) // home (rss) + bundle.
- b.AssertRenderCountContent(1)
+ t.Parallel()
+ for i := 0; i < 3; i++ {
+ b := TestRunning(t, rebuildFilesSimple)
+ b.AssertFileContent("public/mysection/mysectionbundle/index.html",
+ "My Section Bundle Content Content.")
+ b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "My Section Bundle Content.", "My Section Bundle Content Edited.").Build()
+ b.AssertFileContent("public/mysection/mysectionbundle/index.html",
+ "My Section Bundle Content Edited.")
+ b.AssertRenderCountPage(2) // home (rss) + bundle.
+ b.AssertRenderCountContent(1)
+ }
}
func TestRebuildEditTextFileInLeafBundle(t *testing.T) {
b.AssertFileContent("public/mysection/mysectionbundle/index.html",
"Text 2 Content Edited")
b.AssertRenderCountPage(1)
- b.AssertRenderCountContent(1)
+ b.AssertRenderCountContent(0)
}
func TestRebuildEditTextFileInShortcode(t *testing.T) {
b.AssertFileContent("public/index.html", "Home Content.")
b.AssertFileContent("public/index.html", "Home Text Content Edited.")
b.AssertRenderCountPage(1)
- b.AssertRenderCountContent(1)
+ b.AssertRenderCountContent(0)
}
func TestRebuildEditTextFileInBranchBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
- b.AssertFileContent("public/mysection/index.html", "My Section")
+ b.AssertFileContent("public/mysection/index.html", "My Section", "0:/mysection/mysectiontext.txt|Content.|")
b.EditFileReplaceAll("content/mysection/mysectiontext.txt", "Content.", "Content Edited.").Build()
- b.AssertFileContent("public/mysection/index.html", "My Section")
+ b.AssertFileContent("public/mysection/index.html", "My Section", "0:/mysection/mysectiontext.txt|Content Edited.|")
b.AssertRenderCountPage(1)
- b.AssertRenderCountContent(1)
+ b.AssertRenderCountContent(0)
}
func testRebuildBothWatchingAndRunning(t *testing.T, files string, withB func(b *IntegrationTestBuilder)) {
})
}
-func TestRebuildSingleWithBaseof(t *testing.T) {
+func TestRebuildSingle(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+title = "Hugo Site"
+baseURL = "https://example.com"
+disableKinds = ["term", "taxonomy", "sitemap", "robotstxt", "404"]
+disableLiveReload = true
+-- content/p1.md --
+---
+title: "P1"
+---
+P1 Content.
+-- layouts/index.html --
+Home.
+-- layouts/single.html --
+Single: {{ .Title }}|{{ .Content }}|
+{{ with (templates.Defer (dict "key" "global")) }}
+Defer.
+{{ end }}
+`
+ b := Test(t, files, TestOptRunning())
+ b.AssertFileContent("public/p1/index.html", "Single: P1|", "Defer.")
+ b.AssertRenderCountPage(3)
+ b.AssertRenderCountContent(1)
+ b.EditFileReplaceFunc("layouts/single.html", func(s string) string {
+ s = strings.Replace(s, "Single", "Single Edited", 1)
+ s = strings.Replace(s, "Defer.", "Defer Edited.", 1)
+ return s
+ }).Build()
+ b.AssertFileContent("public/p1/index.html", "Single Edited: P1|", "Defer Edited.")
+ b.AssertRenderCountPage(1)
+ b.AssertRenderCountContent(0)
+}
+
+func TestRebuildSingleWithBaseofEditSingle(t *testing.T) {
t.Parallel()
files := `
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 --
{{ end }}
`
b := Test(t, files, TestOptRunning())
- b.AssertFileContent("public/p1/index.html", "Baseof: P1|\n\nSingle: P1|<p>P1 Content.</p>\n|")
+ b.AssertFileContent("public/p1/index.html", "Single: P1|")
b.EditFileReplaceFunc("layouts/_default/single.html", func(s string) string {
return strings.Replace(s, "Single", "Single Edited", 1)
}).Build()
- b.AssertFileContent("public/p1/index.html", "Baseof: P1|\n\nSingle Edited: P1|<p>P1 Content.</p>\n|")
+ b.AssertFileContent("public/p1/index.html", "Single Edited")
+}
+
+func TestRebuildSingleWithBaseofEditBaseof(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+title = "Hugo Site"
+baseURL = "https://example.com"
+disableKinds = ["term", "taxonomy"]
+disableLiveReload = true
+-- content/p1.md --
+---
+title: "P1"
+---
+P1 Content.
+[foo](/foo)
+-- layouts/_default/baseof.html --
+Baseof: {{ .Title }}|
+{{ block "main" . }}default{{ end }}
+{{ with (templates.Defer (dict "foo" "bar")) }}
+Defer.
+{{ end }}
+-- layouts/index.html --
+Home.
+-- layouts/_default/single.html --
+{{ define "main" }}
+Single: {{ .Title }}|{{ .Content }}|
+{{ end }}
+`
+ b := Test(t, files, TestOptRunning())
+ b.AssertFileContent("public/p1/index.html", "Single: P1|")
+ fmt.Println("===============")
+ b.EditFileReplaceAll("layouts/_default/baseof.html", "Baseof", "Baseof Edited").Build()
+ b.AssertFileContent("public/p1/index.html", "Baseof Edited")
+}
+
+func TestRebuildWithDeferEditRenderHook(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+title = "Hugo Site"
+baseURL = "https://example.com"
+disableKinds = ["term", "taxonomy"]
+disableLiveReload = true
+-- content/p1.md --
+---
+title: "P1"
+---
+P1 Content.
+[foo](/foo)
+-- layouts/_default/baseof.html --
+Baseof: {{ .Title }}|
+{{ block "main" . }}default{{ end }}
+ {{ with (templates.Defer (dict "foo" "bar")) }}
+Defer.
+{{ end }}
+-- layouts/single.html --
+{{ define "main" }}
+Single: {{ .Title }}|{{ .Content }}|
+{{ end }}
+-- layouts/_default/_markup/render-link.html --
+Render Link.
+`
+ b := Test(t, files, TestOptRunning())
+ // Edit render hook.
+ b.EditFileReplaceAll("layouts/_default/_markup/render-link.html", "Render Link", "Render Link Edited").Build()
+
+ b.AssertFileContent("public/p1/index.html", "Render Link Edited")
}
func TestRebuildFromString(t *testing.T) {
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/common/urls"
- "github.com/gohugoio/hugo/output"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/tpl"
indentation string // indentation from source.
- info tpl.Info // One of the output formats (arbitrary)
- templs []tpl.Template // All output formats
+ templ *tplimpl.TemplInfo
// If set, the rendered shortcode is sent as part of the surrounding content
// to Goldmark and similar.
}
func (s shortcode) needsInner() bool {
- return s.info != nil && s.info.ParseInfo().IsInner
+ return s.templ != nil && s.templ.ParseInfo.IsInner
}
func (s shortcode) configVersion() int {
- if s.info == nil {
+ if s.templ == nil {
// Not set for inline shortcodes.
return 2
}
-
- return s.info.ParseInfo().Config.Version
+ return s.templ.ParseInfo.Config.Version
}
func (s shortcode) innerString() string {
ctx context.Context,
level int,
s *Site,
- tplVariants tpl.TemplateVariants,
sc *shortcode,
parent *ShortcodeWithPage,
- p *pageState,
+ po *pageOutput,
isRenderString bool,
) (shortcodeRenderer, error) {
+ p := po.p
toParseErr := func(err error) error {
source := p.m.content.mustSource()
return p.parseError(fmt.Errorf("failed to render shortcode %q: %w", sc.name, err), source, sc.pos)
// parsed and rendered by Goldmark.
ctx = tpl.Context.IsInGoldmark.Set(ctx, true)
}
- r, err := doRenderShortcode(ctx, level, s, tplVariants, sc, parent, p, isRenderString)
+ r, err := doRenderShortcode(ctx, level, s, sc, parent, po, isRenderString)
if err != nil {
return nil, false, toParseErr(err)
}
ctx context.Context,
level int,
s *Site,
- tplVariants tpl.TemplateVariants,
sc *shortcode,
parent *ShortcodeWithPage,
- p *pageState,
+ po *pageOutput,
isRenderString bool,
) (shortcodeRenderer, error) {
- var tmpl tpl.Template
+ var tmpl *tplimpl.TemplInfo
+ p := po.p
// Tracks whether this shortcode or any of its children has template variations
// in other languages or output formats. We are currently only interested in
- // the output formats, so we may get some false positives -- we
- // should improve on that.
+ // the output formats.
var hasVariants bool
if sc.isInline {
if !p.s.ExecHelper.Sec().EnableInlineShortcodes {
return zeroShortcode, nil
}
- templName := path.Join("_inline_shortcode", p.Path(), sc.name)
+ templatePath := path.Join("_inline_shortcode", p.Path(), sc.name)
if sc.isClosing {
templStr := sc.innerString()
var err error
- tmpl, err = s.TextTmpl().Parse(templName, templStr)
+ tmpl, err = s.TemplateStore.TextParse(templatePath, templStr)
if err != nil {
if isRenderString {
return zeroShortcode, p.wrapError(err)
} else {
// Re-use of shortcode defined earlier in the same page.
- var found bool
- tmpl, found = s.TextTmpl().Lookup(templName)
- if !found {
+ tmpl = s.TemplateStore.TextLookup(templatePath)
+ if tmpl == nil {
return zeroShortcode, fmt.Errorf("no earlier definition of shortcode %q found", sc.name)
}
}
- tmpl = tpl.AddIdentity(tmpl)
} else {
- var found, more bool
- tmpl, found, more = s.Tmpl().LookupVariant(sc.name, tplVariants)
- if !found {
+ ofCount := map[string]int{}
+ include := func(match *tplimpl.TemplInfo) bool {
+ ofCount[match.D.OutputFormat]++
+ return true
+ }
+ base, layoutDescriptor := po.getTemplateBasePathAndDescriptor()
+ q := tplimpl.TemplateQuery{
+ Path: base,
+ Name: sc.name,
+ Category: tplimpl.CategoryShortcode,
+ Desc: layoutDescriptor,
+ Consider: include,
+ }
+ v := s.TemplateStore.LookupShortcode(q)
+ if v == nil {
s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path())
return zeroShortcode, nil
}
- hasVariants = hasVariants || more
+ tmpl = v
+ hasVariants = hasVariants || len(ofCount) > 1
}
data := &ShortcodeWithPage{
case string:
inner += innerData
case *shortcode:
- s, err := prepareShortcode(ctx, level+1, s, tplVariants, innerData, data, p, isRenderString)
+ s, err := prepareShortcode(ctx, level+1, s, innerData, data, po, isRenderString)
if err != nil {
return zeroShortcode, err
}
}
- result, err := renderShortcodeWithPage(ctx, s.Tmpl(), tmpl, data)
+ result, err := renderShortcodeWithPage(ctx, s.GetTemplateStore(), tmpl, data)
if err != nil && sc.isInline {
fe := herrors.NewFileErrorFromName(err, p.File().Filename())
return ok
}
-func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, p *pageState, f output.Format, isRenderString bool) (map[string]shortcodeRenderer, error) {
+func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, po *pageOutput, isRenderString bool) (map[string]shortcodeRenderer, error) {
rendered := make(map[string]shortcodeRenderer)
- tplVariants := tpl.TemplateVariants{
- Language: p.Language().Lang,
- OutputFormat: f,
- }
-
for _, v := range s.shortcodes {
- s, err := prepareShortcode(ctx, 0, s.s, tplVariants, v, nil, p, isRenderString)
+ s, err := prepareShortcode(ctx, 0, s.s, v, nil, po, isRenderString)
if err != nil {
return nil, err
}
// we trust the template on this:
// if there's no inner, we're done
if !sc.isInline {
- if !sc.info.ParseInfo().IsInner {
+ if !sc.templ.ParseInfo.IsInner {
return sc, nil
}
}
sc.name = currItem.ValStr(source)
- // Used to check if the template expects inner content.
- templs := s.s.Tmpl().LookupVariants(sc.name)
- if templs == nil {
+ // Used to check if the template expects inner content,
+ // so just pick one arbitrarily with the same name.
+ q := tplimpl.TemplateQuery{
+ Path: "",
+ Name: sc.name,
+ Category: tplimpl.CategoryShortcode,
+ Consider: nil,
+ }
+ templ := s.s.TemplateStore.LookupShortcode(q)
+ if templ == nil {
return nil, fmt.Errorf("%s: template for shortcode %q not found", errorPrefix, sc.name)
}
-
- sc.info = templs[0].(tpl.Info)
- sc.templs = templs
+ sc.templ = templ
case currItem.IsInlineShortcodeName():
sc.name = currItem.ValStr(source)
sc.isInline = true
return source, nil
}
-func renderShortcodeWithPage(ctx context.Context, h tpl.TemplateHandler, tmpl tpl.Template, data *ShortcodeWithPage) (string, error) {
+func renderShortcodeWithPage(ctx context.Context, h *tplimpl.TemplateStore, tmpl *tplimpl.TemplInfo, data *ShortcodeWithPage) (string, error) {
buffer := bp.GetBuffer()
defer bp.PutBuffer(buffer)
b := newTestSitesBuilder(t).WithSimpleConfigFile()
b.WithTemplates(
- "default/single.html", `EMPTY`,
- "_internal/shortcodes/tag.html", `tag`,
- "_internal/shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`,
- "_internal/shortcodes/sc1.html", `sc1`,
- "_internal/shortcodes/sc2.html", `sc2`,
- "_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`,
- "_internal/shortcodes/inner2.html", `{{.Inner}}`,
- "_internal/shortcodes/inner3.html", `{{.Inner}}`,
+ "pages/single.html", `EMPTY`,
+ "shortcodes/tag.html", `tag`,
+ "shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`,
+ "shortcodes/sc1.html", `sc1`,
+ "shortcodes/sc2.html", `sc2`,
+ "shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`,
+ "shortcodes/inner2.html", `{{.Inner}}`,
+ "shortcodes/inner3.html", `{{.Inner}}`,
).WithContent("page.md", `---
title: "Shortcodes Galore!"
---
if s == nil {
return "<nil>"
}
-
var version int
- if s.info != nil {
- version = s.info.ParseInfo().Config.Version
+ if s.templ != nil {
+ version = s.templ.ParseInfo.Config.Version
}
return strReplacer.Replace(fmt.Sprintf("%s;inline:%t;closing:%t;inner:%v;params:%v;ordinal:%d;markup:%t;version:%d;pos:%d",
s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, version, s.pos))
regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) {
return func(c *qt.C, shortcode *shortcode, err error) {
c.Assert(err, qt.IsNil)
- c.Assert(str(shortcode), qt.Matches, ".*"+re+".*")
+ c.Assert(str(shortcode), qt.Matches, ".*"+re+".*", qt.Commentf("%s", shortcode.name))
}
}
"_default/single.json", "{{ .Content }}",
"shortcodes/myshort.html", `Short-HTML`,
"shortcodes/myshort.csv", `Short-CSV`,
+ "shortcodes/myshort.txt", `Short-TXT`,
)
b.Build(BuildCfg{})
for i := range numPages {
b.AssertFileContent(fmt.Sprintf("public/page%d/index.html", i), "Short-HTML")
b.AssertFileContent(fmt.Sprintf("public/page%d/index.csv", i), "Short-CSV")
- b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-HTML")
+ b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-CSV")
}
for i := range numPages {
- b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-HTML")
+ b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-CSV")
}
}
"github.com/gohugoio/hugo/langs/i18n"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/resources"
+
"github.com/gohugoio/hugo/tpl/tplimpl"
+ "github.com/gohugoio/hugo/tpl/tplimplinit"
+ xmaps "golang.org/x/exp/maps"
+
+ // Loads the template funcs namespaces.
+
"golang.org/x/text/unicode/norm"
"github.com/gohugoio/hugo/common/paths"
BuildState: &deps.BuildState{
OnSignalRebuild: onSignalRebuild,
},
+ Counters: &deps.Counters{},
MemCache: memCache,
- TemplateProvider: tplimpl.DefaultTemplateProvider,
TranslationProvider: i18n.NewTranslationProvider(),
WasmDispatchers: warpc.AllDispatchers(
warpc.Options{
var prototype *deps.Deps
for i, s := range sites {
s.h = h
+ // The template store needs to be initialized after the h container is set on s.
+ if i == 0 {
+ templateStore, err := tplimpl.NewStore(
+ tplimpl.StoreOptions{
+ Fs: s.BaseFs.Layouts.Fs,
+ DefaultContentLanguage: s.Conf.DefaultContentLanguage(),
+ Watching: s.Conf.Watching(),
+ PathParser: s.Conf.PathParser(),
+ Metrics: d.Metrics,
+ OutputFormats: s.conf.OutputFormats.Config,
+ MediaTypes: s.conf.MediaTypes.Config,
+ DefaultOutputFormat: s.conf.DefaultOutputFormat,
+ TaxonomySingularPlural: s.conf.Taxonomies,
+ }, tplimpl.SiteOptions{
+ Site: s,
+ TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps),
+ })
+ if err != nil {
+ return nil, err
+ }
+ s.Deps.TemplateStore = templateStore
+ } else {
+ s.Deps.TemplateStore = prototype.TemplateStore.WithSiteOpts(
+ tplimpl.SiteOptions{
+ Site: s,
+ TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps),
+ })
+ }
if err := s.Deps.Compile(prototype); err != nil {
return nil, err
}
// Returns a struct with some information about the build.
func (s *Site) Hugo() hugo.HugoInfo {
- if s.h == nil || s.h.hugoInfo.Environment == "" {
+ if s.h == nil {
+ panic("site: hugo: h not initialized")
+ }
+ if s.h.hugoInfo.Environment == "" {
panic("site: hugo: hugoInfo not initialized")
}
return s.h.hugoInfo
s.renderFormats = formats
}
-func (s *Site) GetRelatedDocsHandler() *page.RelatedDocsHandler {
+func (s *Site) GetInternalRelatedDocsHandler() *page.RelatedDocsHandler {
return s.relatedDocsHandler
}
mu sync.Mutex
needsPagesAssembly bool
- identitySet identity.Identities
+
+ ids map[identity.Identity]bool
+}
+
+func (w *WhatChanged) init() {
+ if w.ids == nil {
+ w.ids = make(map[identity.Identity]bool)
+ }
}
func (w *WhatChanged) Add(ids ...identity.Identity) {
w.mu.Lock()
defer w.mu.Unlock()
- if w.identitySet == nil {
- w.identitySet = make(identity.Identities)
- }
+ w.init()
for _, id := range ids {
- w.identitySet[id] = true
+ w.ids[id] = true
}
}
}
func (w *WhatChanged) clear() {
- w.identitySet = identity.Identities{}
+ w.ids = nil
}
func (w *WhatChanged) Changes() []identity.Identity {
- if w == nil || w.identitySet == nil {
+ if w == nil || w.ids == nil {
return nil
}
- return w.identitySet.AsSlice()
+ return xmaps.Keys(w.ids)
}
func (w *WhatChanged) Drain() []identity.Identity {
w.mu.Lock()
defer w.mu.Unlock()
- ids := w.identitySet.AsSlice()
+ ids := w.Changes()
w.clear()
return ids
}
pageDependencyScopeGlobal
)
-func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ tpl.Template) error {
+func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ *tplimpl.TemplInfo) error {
s.h.buildCounters.pageRenderCounter.Add(1)
renderBuffer := bp.GetBuffer()
defer bp.PutBuffer(renderBuffer)
// hookRendererTemplate is the canonical implementation of all hooks.ITEMRenderer,
// where ITEM is the thing being hooked.
type hookRendererTemplate struct {
- templateHandler tpl.TemplateHandler
- templ tpl.Template
+ templateHandler *tplimpl.TemplateStore
+ templ *tplimpl.TemplInfo
resolvePosition func(ctx any) text.Position
}
return false
}
-func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ tpl.Template) (err error) {
+func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ *tplimpl.TemplInfo) (err error) {
if templ == nil {
s.logMissingLayout(name, "", "", outputFormat)
return nil
panic("nil context")
}
- if err = s.Tmpl().ExecuteWithContext(ctx, templ, w, d); err != nil {
+ if err = s.GetTemplateStore().ExecuteWithContext(ctx, templ, w, d); err != nil {
filename := name
if p, ok := d.(*pageState); ok {
filename = p.String()
htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name)
robotsOut, _ := allFormats.GetByName(output.RobotsTxtFormat.Name)
sitemapOut, _ := allFormats.GetByName(output.SitemapFormat.Name)
+ httpStatus404Out, _ := allFormats.GetByName(output.HTTPStatus404HTMLFormat.Name)
defaultListTypes := output.Formats{htmlOut}
if rssFound {
// Below are for consistency. They are currently not used during rendering.
kinds.KindSitemap: {sitemapOut},
kinds.KindRobotsTXT: {robotsOut},
- kinds.KindStatus404: {htmlOut},
+ kinds.KindStatus404: {httpStatus404Out},
}
// May be disabled
c.Assert(outputs[kinds.KindRSS], deepEqualsOutputFormats, output.Formats{output.RSSFormat})
c.Assert(outputs[kinds.KindSitemap], deepEqualsOutputFormats, output.Formats{output.SitemapFormat})
c.Assert(outputs[kinds.KindRobotsTXT], deepEqualsOutputFormats, output.Formats{output.RobotsTxtFormat})
- c.Assert(outputs[kinds.KindStatus404], deepEqualsOutputFormats, output.Formats{output.HTMLFormat})
+ c.Assert(outputs[kinds.KindStatus404], deepEqualsOutputFormats, output.Formats{output.HTTPStatus404HTMLFormat})
})
// Issue #4528
[outputFormats.nobase]
mediaType = "application/json"
permalinkable = true
+isPlainText = true
`
"github.com/bep/logg"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugolib/doctree"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/config"
- "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/resources/kinds"
"github.com/gohugoio/hugo/resources/page"
return s.outIdx == 0
}
- if kind == kinds.KindStatus404 {
+ if kind == kinds.KindTemporary || kind == kinds.KindStatus404 {
// 1 for all output formats
return s.outIdx == 0
}
s.Log.Trace(
func() string {
- return fmt.Sprintf("rendering outputFormat %q kind %q using layout %q to %q", p.pageOutput.f.Name, p.Kind(), templ.Name(), targetPath)
+ return fmt.Sprintf("rendering outputFormat %q kind %q using layout %q to %q", p.pageOutput.f.Name, p.Kind(), templ.Template.Name(), targetPath)
},
)
}
// renderPaginator must be run after the owning Page has been rendered.
-func (s *Site) renderPaginator(p *pageState, templ tpl.Template) error {
+func (s *Site) renderPaginator(p *pageState, templ *tplimpl.TemplInfo) error {
paginatePath := s.Conf.Pagination().Path
d := p.targetPathDescriptor
{".", "", true, "/level2/level3/"},
{"./", "", true, "/level2/level3/"},
- // try to confuse parsing
- {"embedded.dot.md", "", true, "/level2/level3/embedded.dot/"},
+ {"embedded.dot.md", "", true, "/level2/level3/embedded/"},
// test empty link, as well as fragment only link
{"", "", true, ""},
}
func doTestTaxonomiesWithAndWithoutContentFile(t *testing.T, uglyURLs bool) {
+ t.Helper()
+
siteConfig := `
baseURL = "http://example.com/blog"
titleCaseStyle = "firstupper"
"github.com/gohugoio/hugo/hugofs"
)
+// TODO(bep) keep this until we release v0.146.0 as a security against breaking changes, but it's rather messy and mostly duplicate of
+// tests in the tplimpl package, so eventually just remove it.
func TestTemplateLookupOrder(t *testing.T) {
var (
fs *hugofs.Fs
} {
this := this
+ if this.name != "Variant 1" {
+ continue
+ }
t.Run(this.name, func(t *testing.T) {
// TODO(bep) there are some function vars need to pull down here to enable => t.Parallel()
cfg, fs = newTestCfg()
}
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)
})
b := newTestSitesBuilder(t).WithSimpleConfigFile()
b.WithTemplates("_default/list.html", `
- {{ define "main" }}
- Bonjour
- {{ end }}
+{{ define "main" }}
+ Bonjour
+{{ end }}
- {{ printf "list" }}
+{{ printf "list" }}
`)
b.AssertFileContent("public/p1/index.html", `Single: P1`)
})
- t.Run("baseof", func(t *testing.T) {
- t.Parallel()
- b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
+ {
+ }
+}
- b.WithTemplatesAdded(
- "index.html", `{{ define "main" }}Main Home En{{ end }}`,
- "index.fr.html", `{{ define "main" }}Main Home Fr{{ end }}`,
- "baseof.html", `Baseof en: {{ block "main" . }}main block{{ end }}`,
- "baseof.fr.html", `Baseof fr: {{ block "main" . }}main block{{ end }}`,
- "mysection/baseof.html", `Baseof mysection: {{ block "main" . }}mysection block{{ end }}`,
- "_default/single.html", `{{ define "main" }}Main Default Single{{ end }}`,
- "_default/list.html", `{{ define "main" }}Main Default List{{ end }}`,
- )
+func TestTemplateLookupSitBaseOf(t *testing.T) {
+ t.Parallel()
+ b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
+
+ b.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", `---
+ b.WithContent("mysection/p1.md", `---
title: My Page
---
`)
- b.CreateSites().Build(BuildCfg{})
+ b.CreateSites().Build(BuildCfg{})
- b.AssertFileContent("public/en/index.html", `Baseof en: Main Home En`)
- b.AssertFileContent("public/fr/index.html", `Baseof fr: Main Home Fr`)
- b.AssertFileContent("public/en/mysection/index.html", `Baseof mysection: Main Default List`)
- b.AssertFileContent("public/en/mysection/p1/index.html", `Baseof mysection: Main Default Single`)
- })
+ b.AssertFileContent("public/en/index.html", `Baseof en: Main Home En`)
+ b.AssertFileContent("public/fr/index.html", `Baseof fr: Main Home Fr`)
+ b.AssertFileContent("public/en/mysection/index.html", `Baseof mysection: Main Default List`)
+ b.AssertFileContent("public/en/mysection/p1/index.html", `Baseof mysection: Main Default Single`)
}
func TestTemplateFuncs(t *testing.T) {
b.AssertFileContent("public/index.html", `a: [a b c]`)
}
+// Legacy behavior for internal templates.
func TestOverrideInternalTemplate(t *testing.T) {
files := `
-- hugo.toml --
return true
}
+ if a2, ok := a.(compare.ProbablyEqer); ok && a2.ProbablyEq(b) {
+ return true
+ }
+
if a2, ok := a.(IsProbablyDependentProvider); ok {
return a2.IsProbablyDependent(b)
}
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/resources/resource_factories/create"
- "github.com/gohugoio/hugo/tpl"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
)
d *deps.Deps
once sync.Once
- runnerTemplate tpl.Template
+ runnerTemplate *tplimpl.TemplInfo
createClient *create.Client
buildClient *BuildClient
c.once.Do(func() {
// We should fix the initialization order here (or use the Go template package directly), but we need to wait
// for the Hugo templates to be ready.
- tmpl, err := c.d.TextTmpl().Parse("batch-esm-runner", runnerTemplateStr)
+ tmpl, err := c.d.TemplateStore.TextParse("batch-esm-runner", runnerTemplateStr)
if err != nil {
initErr = err
return
func (c *BatcherClient) buildBatchGroup(ctx context.Context, t *batchGroupTemplateContext) (resource.Resource, string, error) {
var buf bytes.Buffer
- if err := c.d.Tmpl().ExecuteWithContext(ctx, c.runnerTemplate, &buf, t); err != nil {
+ if err := c.d.GetTemplateStore().ExecuteWithContext(ctx, c.runnerTemplate, &buf, t); err != nil {
return nil, "", err
}
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config/testconfig"
- "github.com/gohugoio/hugo/tpl/tplimpl"
-
"github.com/gohugoio/hugo/resources/page"
"github.com/spf13/afero"
func prepareDeps(afs afero.Fs, cfg config.Provider) (*deps.Deps, *TranslationProvider) {
d := testconfig.GetTestDeps(afs, cfg)
translationProvider := NewTranslationProvider()
- d.TemplateProvider = tplimpl.DefaultTemplateProvider
d.TranslationProvider = translationProvider
d.Site = page.NewDummyHugoSite(d.Conf)
if err := d.Compile(nil); err != nil {
## Golang Code
-§§§golang
+§§§go
fmt.Println("Hello, Golang!");
§§§
Go Code: fmt.Println("Hello, World!");
Go Code: fmt.Println("Hello, Golang!");
-Go Language: golang|
+Go Language: go|
`,
"Goat SVG:<svg class='diagram' xmlns='http://www.w3.org/2000/svg' version='1.1' height='25' width='40'",
"Goat Attribute: 600|",
"<h2 id=\"go-code\">Go Code</h2>\nGo Code: fmt.Println(\"Hello, World!\");\n|\nGo Language: go|",
- "<h2 id=\"golang-code\">Golang Code</h2>\nGo Code: fmt.Println(\"Hello, Golang!\");\n|\nGo Language: golang|",
+ "<h2 id=\"golang-code\">Golang Code</h2>\nGo Code: fmt.Println(\"Hello, Golang!\");\n|\nGo Language: go|",
"<h2 id=\"bash-code\">Bash Code</h2>\n<div class=\"highlight blue\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"ln\">32</span><span class=\"cl\"><span class=\"nb\">echo</span> <span class=\"s2\">"l1"</span><span class=\"p\">;</span>\n</span></span><span class=\"line hl\"><span class=\"ln\">33</span>",
)
}
CSSType Type
SCSSType Type
SASSType Type
+ GotmplType Type
CSVType Type
HTMLType Type
JavascriptType Type
CSSType: Type{Type: "text/css"},
SCSSType: Type{Type: "text/x-scss"},
SASSType: Type{Type: "text/x-sass"},
+ GotmplType: Type{Type: "text/x-gotmpl"},
CSVType: Type{Type: "text/csv"},
HTMLType: Type{Type: "text/html"},
JavascriptType: Type{Type: "text/javascript"},
"text/typescript": map[string]any{"suffixes": []string{"ts"}},
"text/tsx": map[string]any{"suffixes": []string{"tsx"}},
"text/jsx": map[string]any{"suffixes": []string{"jsx"}},
+ "text/x-gotmpl": map[string]any{"suffixes": []string{"gotmpl"}},
"application/json": map[string]any{"suffixes": []string{"json"}},
"application/manifest+json": map[string]any{"suffixes": []string{"webmanifest"}},
"fmt"
"path/filepath"
"reflect"
+ "slices"
"sort"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
- "slices"
)
// DefaultTypes is the default media types supported by Hugo.
IsContentExt: func(ext string) bool {
panic("not supported")
},
+ IsOutputFormat: func(name, ext string) bool {
+ panic("DefaultPathParser: not supported")
+ },
}
}
- c.Assert(len(DefaultTypes), qt.Equals, 40)
+ c.Assert(len(DefaultTypes), qt.Equals, 41)
}
suffix = t.normalizeSuffix(suffix)
var types []Type
for _, tt := range t {
- if tt.hasSuffix(suffix) {
+ if tt.HasSuffix(suffix) {
types = append(types, tt)
}
}
func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
- if tt.hasSuffix(suffix) {
+ if tt.HasSuffix(suffix) {
return tt, SuffixInfo{
FullSuffix: tt.Delimiter + suffix,
Suffix: suffix,
func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
- if tt.hasSuffix(suffix) {
+ if tt.HasSuffix(suffix) {
if found {
// ambiguous
found = false
func (t Types) IsTextSuffix(suffix string) bool {
suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
- if tt.hasSuffix(suffix) {
+ if tt.HasSuffix(suffix) {
return tt.IsText()
}
}
return false
}
-func (m Type) hasSuffix(suffix string) bool {
+func (m Type) HasSuffix(suffix string) bool {
return strings.Contains(","+m.SuffixesCSV+",", ","+suffix+",")
}
package output
import (
- "strings"
// "fmt"
"github.com/gohugoio/hugo/docshelper"
- "github.com/gohugoio/hugo/output/layouts"
)
// This is is just some helpers used to create some JSON used in the Hugo docs.
docsProvider := func() docshelper.DocProvider {
return docshelper.DocProvider{
"output": map[string]any{
- "layouts": createLayoutExamples(),
+ // TODO(bep), maybe revisit this later, but I hope this isn't needed.
+ // "layouts": createLayoutExamples(),
+ "layouts": map[string]any{},
},
}
}
docshelper.AddDocProviderFunc(docsProvider)
}
-
-func createLayoutExamples() any {
- type Example struct {
- Example string
- Kind string
- OutputFormat string
- Suffix string
- Layouts []string `json:"Template Lookup Order"`
- }
-
- var (
- basicExamples []Example
- demoLayout = "demolayout"
- demoType = "demotype"
- )
-
- for _, example := range []struct {
- name string
- d layouts.LayoutDescriptor
- }{
- // Taxonomy layouts.LayoutDescriptor={categories category taxonomy en false Type Section
- {"Single page in \"posts\" section", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Base template for single page in \"posts\" section", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Single page in \"posts\" section with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
- {"Base template for single page in \"posts\" section with layout set to \"demolayout\"", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
- {"AMP single page in \"posts\" section", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "amp", Suffix: "html"}},
- {"AMP single page in \"posts\" section, French language", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}},
- // Typeless pages get "page" as type
- {"Home page", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
- {"Base template for home page", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
- {"Home page with type set to \"demotype\"", layouts.LayoutDescriptor{Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}},
- {"Base template for home page with type set to \"demotype\"", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}},
- {"Home page with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "home", Type: "page", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
- {"AMP home, French language", layouts.LayoutDescriptor{Kind: "home", Type: "page", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}},
- {"JSON home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "json", Suffix: "json"}},
- {"RSS home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "rss", Suffix: "xml"}},
-
- {"Section list for \"posts\"", layouts.LayoutDescriptor{Kind: "section", Type: "posts", Section: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Section list for \"posts\" with type set to \"blog\"", layouts.LayoutDescriptor{Kind: "section", Type: "blog", Section: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Section list for \"posts\" with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "section", Layout: demoLayout, Section: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Section list for \"posts\"", layouts.LayoutDescriptor{Kind: "section", Type: "posts", OutputFormatName: "rss", Suffix: "xml"}},
-
- {"Taxonomy list for \"categories\"", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}},
- {"Taxonomy list for \"categories\"", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "xml"}},
-
- {"Term list for \"categories\"", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}},
- {"Term list for \"categories\"", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "xml"}},
- } {
-
- l := layouts.NewLayoutHandler()
- layouts, _ := l.For(example.d)
-
- basicExamples = append(basicExamples, Example{
- Example: example.name,
- Kind: example.d.Kind,
- OutputFormat: example.d.OutputFormatName,
- Suffix: example.d.Suffix,
- Layouts: makeLayoutsPresentable(layouts),
- })
- }
-
- return basicExamples
-}
-
-func makeLayoutsPresentable(l []string) []string {
- var filtered []string
- for _, ll := range l {
- if strings.Contains(ll, "page/") {
- // This is a valid lookup, but it's more confusing than useful.
- continue
- }
- ll = "layouts/" + strings.TrimPrefix(ll, "_text/")
-
- if !strings.Contains(ll, "indexes") {
- filtered = append(filtered, ll)
- }
- }
-
- return filtered
-}
+++ /dev/null
-// 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
-}
+++ /dev/null
-// 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)
- }
- }
-}
-*/
Weight: 10,
}
+ // Alias is the output format used for alias redirects.
+ AliasHTMLFormat = Format{
+ Name: "alias",
+ MediaType: media.Builtin.HTMLType,
+ IsHTML: true,
+ Ugly: true,
+ Permalinkable: false,
+ }
+
MarkdownFormat = Format{
Name: "markdown",
MediaType: media.Builtin.MarkdownType,
Rel: "sitemap",
}
- HTTPStatusHTMLFormat = Format{
- Name: "httpstatus",
+ GotmplFormat = Format{
+ Name: "gotmpl",
+ MediaType: media.Builtin.GotmplType,
+ IsPlainText: true,
+ NotAlternative: true,
+ }
+
+ // I'm not sure having a 404 format is a good idea,
+ // for one, we would want to have multiple formats for this.
+ HTTPStatus404HTMLFormat = Format{
+ Name: "404",
MediaType: media.Builtin.HTMLType,
NotAlternative: true,
Ugly: true,
CSSFormat,
CSVFormat,
HTMLFormat,
+ GotmplFormat,
+ HTTPStatus404HTMLFormat,
+ AliasHTMLFormat,
JSONFormat,
MarkdownFormat,
WebAppManifestFormat,
RobotsTxtFormat,
RSSFormat,
SitemapFormat,
+ SitemapIndexFormat,
}
func init() {
c.Assert(RSSFormat.NoUgly, qt.Equals, true)
c.Assert(CalendarFormat.IsHTML, qt.Equals, false)
- c.Assert(len(DefaultFormats), qt.Equals, 11)
+ c.Assert(len(DefaultFormats), qt.Equals, 15)
}
func TestGetFormatByName(t *testing.T) {
func TestSort(t *testing.T) {
c := qt.New(t)
c.Assert(DefaultFormats[0].Name, qt.Equals, "html")
- c.Assert(DefaultFormats[1].Name, qt.Equals, "amp")
+ c.Assert(DefaultFormats[1].Name, qt.Equals, "404")
json := JSONFormat
json.Weight = 1
// The following are (currently) temporary nodes,
// i.e. nodes we create just to render in isolation.
+ KindTemporary = "temporary"
KindRSS = "rss"
KindSitemap = "sitemap"
KindSitemapIndex = "sitemapindex"
// InternalDependencies is considered an internal interface.
type InternalDependencies interface {
- // GetRelatedDocsHandler is for internal use only.
- GetRelatedDocsHandler() *RelatedDocsHandler
+ // GetInternalRelatedDocsHandler is for internal use only.
+ GetInternalRelatedDocsHandler() *RelatedDocsHandler
}
// OutputFormatsProvider provides the OutputFormats of a Page.
pb.isUgly = true
}
- if d.Type == output.HTTPStatusHTMLFormat || d.Type == output.SitemapFormat || d.Type == output.RobotsTxtFormat {
+ if d.Type == output.HTTPStatus404HTMLFormat || d.Type == output.SitemapFormat || d.Type == output.RobotsTxtFormat {
pb.noSubResources = true
} else if d.Kind != kinds.KindPage && d.URL == "" && d.Section.Base() != "/" {
if d.ExpandedPermalink != "" {
return nil, fmt.Errorf("invalid type %T in related search", p[0])
}
- cache := d.GetRelatedDocsHandler()
+ cache := d.GetInternalRelatedDocsHandler()
searchIndex, err := cache.getOrCreateIndex(ctx, p)
if err != nil {
panic("testpage: not implemented")
}
-func (p *testPage) GetRelatedDocsHandler() *RelatedDocsHandler {
+func (p *testPage) GetInternalRelatedDocsHandler() *RelatedDocsHandler {
return relatedDocsHandler
}
"github.com/gohugoio/hugo/resources/images"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
- "github.com/gohugoio/hugo/tpl"
)
func NewSpec(
BuildClosers types.CloseAdder
Rebuilder identity.SignalRebuilder
- TextTemplates tpl.TemplateParseFinder
-
Permalinks page.PermalinkExpander
ImageCache *ImageCache
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/internal"
"github.com/gohugoio/hugo/resources/resource"
- "github.com/gohugoio/hugo/tpl"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
)
// Client contains methods to perform template processing of Resource objects.
type Client struct {
rs *resources.Spec
- t tpl.TemplatesProvider
+ t tplimpl.TemplateStoreProvider
}
// New creates a new Client with the given specification.
-func New(rs *resources.Spec, t tpl.TemplatesProvider) *Client {
+func New(rs *resources.Spec, t tplimpl.TemplateStoreProvider) *Client {
if rs == nil {
panic("must provide a resource Spec")
}
type executeAsTemplateTransform struct {
rs *resources.Spec
- t tpl.TemplatesProvider
+ t tplimpl.TemplateStoreProvider
targetPath string
data any
}
func (t *executeAsTemplateTransform) Transform(ctx *resources.ResourceTransformationCtx) error {
tplStr := helpers.ReaderToString(ctx.From)
- templ, err := t.t.TextTmpl().Parse(ctx.InPath, tplStr)
+ th := t.t.GetTemplateStore()
+ ti, err := th.TextParse(ctx.InPath, tplStr)
if err != nil {
return fmt.Errorf("failed to parse Resource %q as Template:: %w", ctx.InPath, err)
}
-
ctx.OutPath = t.targetPath
-
- return t.t.Tmpl().ExecuteWithContext(ctx.Ctx, templ, ctx.To, t.data)
+ return th.ExecuteWithContext(ctx.Ctx, ti, ctx.To, t.data)
}
func (c *Client) ExecuteAsTemplate(ctx context.Context, res resources.ResourceTransformer, targetPath string, data any) (resource.Resource, error) {
hugo --printUnusedTemplates
-stderr 'Template _default/list.html is unused'
+stderr 'Template /list.html is unused'
-- hugo.toml --
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "404", "section", "page"]
"strings"
"github.com/gohugoio/hugo/common/hreflect"
- "github.com/gohugoio/hugo/tpl"
)
// Apply takes an array or slice c and returns a new slice with the function fname applied over it.
func (ns *Namespace) lookupFunc(ctx context.Context, fname string) (reflect.Value, bool) {
namespace, methodName, ok := strings.Cut(fname, ".")
if !ok {
- templ := ns.deps.Tmpl().(tpl.TemplateFuncGetter)
- return templ.GetFunc(fname)
+ return ns.deps.GetTemplateStore().GetFunc(fname)
}
// Namespace
+++ /dev/null
-// 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")
- }
-}
package template
import (
+ "fmt"
+
"github.com/gohugoio/hugo/common/types"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
)
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
+}
name := x.Name()
src := t.set[name]
if src == nil || src.escapeErr != nil {
- return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
+ return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed, %q not found", t.Name(), name)
}
x.Tree = x.Tree.Copy()
ret.set[name] = &Template{
Name, Gift string
Attended bool
}
- var recipients = []Recipient{
+ recipients := []Recipient{
{"Aunt Mildred", "bone china tea set", true},
{"Uncle John", "moleskin pants", false},
{"Cousin Rodney", "", false},
func init() {
f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
- ctx := New()
+ ctx := New(d)
ns := &internal.TemplateFuncsNamespace{
Name: name,
"math"
"math/rand"
"reflect"
- "sync/atomic"
_math "github.com/gohugoio/hugo/common/math"
+ "github.com/gohugoio/hugo/deps"
"github.com/spf13/cast"
)
)
// New returns a new instance of the math-namespaced template functions.
-func New() *Namespace {
- return &Namespace{}
+func New(d *deps.Deps) *Namespace {
+ return &Namespace{
+ d: d,
+ }
}
// Namespace provides template functions for the "math" namespace.
-type Namespace struct{}
+type Namespace struct {
+ d *deps.Deps
+}
// Abs returns the absolute value of n.
func (ns *Namespace) Abs(n any) (float64, error) {
return
}
-var counter uint64
-
// Counter increments and returns a global counter.
// This was originally added to be used in tests where now.UnixNano did not
// have the needed precision (especially on Windows).
// and the counter will reset on new builds.
// <docsmeta>{"identifiers": ["now.UnixNano"] }</docsmeta>
func (ns *Namespace) Counter() uint64 {
- return atomic.AddUint64(&counter, uint64(1))
+ return ns.d.Counters.MathCounter.Add(1)
}
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
type TestCase struct {
fn func(inputs ...any) (any, error)
func TestAbs(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
func TestCeil(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
type TestCase struct {
values []any
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
type TestCase struct {
values []any
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
mustSum := func(values ...any) any {
result, err := ns.Sum(values...)
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
mustProduct := func(values ...any) any {
result, err := ns.Product(values...)
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
expect := 3.1415
result := ns.Pi()
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
func TestAsin(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
func TestAcos(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
func TestAtan(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
func TestAtan2(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
func TestToDegrees(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
func TestToRadians(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
"github.com/bep/lazycache"
+ "github.com/gohugoio/hugo/common/constants"
"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/identity"
- texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-
"github.com/gohugoio/hugo/tpl"
+ texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/deps"
return hashing.HashString(append([]any{k.Name}, k.Variants...)...)
}
-func (k partialCacheKey) templateName() string {
- if !strings.HasPrefix(k.Name, "partials/") {
- return "partials/" + k.Name
- }
- return k.Name
-}
-
// partialCache represents a LRU cache of partials.
type partialCache struct {
cache *lazycache.Cache[string, includeResult]
}
func (ns *Namespace) includWithTimeout(ctx context.Context, name string, dataList ...any) includeResult {
+ if strings.HasPrefix(name, "partials/") {
+ // This is most likely not what the user intended.
+ // This worked before Hugo 0.146.0.
+ ns.deps.Log.Warnidf(constants.WarnPartialSuperfluousPrefix, "Partial name %q starting with 'partials/' (as in {{ partial \"%s\"}}) is most likely not what you want. Before 0.146.0 we did a double lookup in this situation.", name, name)
+ }
// Create a new context with a timeout not connected to the incoming context.
timeoutCtx, cancel := context.WithTimeout(context.Background(), ns.deps.Conf.Timeout())
defer cancel()
if len(dataList) > 0 {
data = dataList[0]
}
-
- var n string
- if strings.HasPrefix(name, "partials/") {
- n = name
- } else {
- n = "partials/" + name
- }
-
- templ, found := ns.deps.Tmpl().Lookup(n)
- if !found {
- // For legacy reasons.
- templ, found = ns.deps.Tmpl().Lookup(n + ".html")
- }
-
- if !found {
+ name, desc := ns.deps.TemplateStore.TemplateDescriptorFromPath(name)
+ v := ns.deps.TemplateStore.LookupPartial(name, desc)
+ if v == nil {
return includeResult{err: fmt.Errorf("partial %q not found", name)}
}
- var info tpl.ParseInfo
- if ip, ok := templ.(tpl.Info); ok {
- info = ip.ParseInfo()
- }
+ templ := v
+ info := v.ParseInfo
var w io.Writer
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}
}
if ctx, ok := data.(*contextWrapper); ok {
result = ctx.Result
- } else if _, ok := templ.(*texttemplate.Template); ok {
+ } else if _, ok := templ.Template.(*texttemplate.Template); ok {
result = w.(fmt.Stringer).String()
} else {
result = template.HTML(w.(fmt.Stringer).String())
}
return includeResult{
- name: templ.Name(),
+ name: templ.Template.Name(),
result: result,
}
}
// The templates that gets executed is measured in Execute.
// We need to track the time spent in the cache to
// get the totals correct.
- ns.deps.Metrics.MeasureSince(key.templateName(), start)
+ ns.deps.Metrics.MeasureSince(r.name, start)
}
- ns.deps.Metrics.TrackValue(key.templateName(), r.result, found)
+ ns.deps.Metrics.TrackValue(r.name, r.result, found)
}
if r.mangager != nil && depsManagerIn != nil {
got := buf.String()
// Get rid of all the durations, they are never the same.
- durationRe := regexp.MustCompile(`\b[\.\d]*(ms|µs|s)\b`)
+ durationRe := regexp.MustCompile(`\b[\.\d]*(ms|ns|µs|s)\b`)
normalize := func(s string) string {
s = durationRe.ReplaceAllString(s, "")
expect := `
0 0 0 1 index.html
- 100 0 0 1 partials/static2.html
- 100 50 1 2 partials/static1.html
- 25 50 2 4 partials/dynamic1.html
- 66 33 1 3 partials/halfdynamic1.html
+ 100 0 0 1 _partials/static2.html
+ 100 50 1 2 _partials/static1.html
+ 25 50 2 4 _partials/dynamic1.html
+ 66 33 1 3 _partials/halfdynamic1.html
`
b.Assert(got, hqt.IsSameString, expect)
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
import (
"context"
- "io"
- "reflect"
- "regexp"
"strings"
"sync"
"unicode"
"github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/langs"
- "github.com/gohugoio/hugo/output/layouts"
-
- "github.com/gohugoio/hugo/output"
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
)
-// TemplateManager manages the collection of templates.
-type TemplateManager interface {
- TemplateHandler
- TemplateFuncGetter
- AddTemplate(name, tpl string) error
-}
-
-// TemplateVariants describes the possible variants of a template.
-// All of these may be empty.
-type TemplateVariants struct {
- Language string
- OutputFormat output.Format
-}
-
-// TemplateFinder finds templates.
-type TemplateFinder interface {
- TemplateLookup
- TemplateLookupVariant
-}
-
-// UnusedTemplatesProvider lists unused templates if the build is configured to track those.
-type UnusedTemplatesProvider interface {
- UnusedTemplates() []FileInfo
-}
-
-// TemplateHandlers holds the templates needed by Hugo.
-type TemplateHandlers struct {
- Tmpl TemplateHandler
- TxtTmpl TemplateParseFinder
-}
-
-type TemplateExecutor interface {
- ExecuteWithContext(ctx context.Context, t Template, wr io.Writer, data any) error
-}
-
-// TemplateHandler finds and executes templates.
-type TemplateHandler interface {
- TemplateFinder
- TemplateExecutor
- LookupLayout(d layouts.LayoutDescriptor, f output.Format) (Template, bool, error)
- HasTemplate(name string) bool
- GetIdentity(name string) (identity.Identity, bool)
-}
-
-type TemplateLookup interface {
- Lookup(name string) (Template, bool)
-}
-
-type TemplateLookupVariant interface {
- // TODO(bep) this currently only works for shortcodes.
- // We may unify and expand this variant pattern to the
- // other templates, but we need this now for the shortcodes to
- // quickly determine if a shortcode has a template for a given
- // output format.
- // It returns the template, if it was found or not and if there are
- // alternative representations (output format, language).
- // We are currently only interested in output formats, so we should improve
- // this for speed.
- LookupVariant(name string, variants TemplateVariants) (Template, bool, bool)
- LookupVariants(name string) []Template
-}
-
// Template is the common interface between text/template and html/template.
type Template interface {
Name() string
Prepare() (*texttemplate.Template, error)
}
-// AddIdentity checks if t is an identity.Identity and returns it if so.
-// Else it wraps it in a templateIdentity using its name as the base.
-func AddIdentity(t Template) Template {
- if _, ok := t.(identity.IdentityProvider); ok {
- return t
- }
- return templateIdentityProvider{
- Template: t,
- id: identity.StringIdentity(t.Name()),
- }
-}
-
-type templateIdentityProvider struct {
- Template
- id identity.Identity
-}
-
-func (t templateIdentityProvider) GetIdentity() identity.Identity {
- return t.id
-}
-
-// TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain.
-type TemplateParser interface {
- Parse(name, tpl string) (Template, error)
-}
-
-// TemplateParseFinder provides both parsing and finding.
-type TemplateParseFinder interface {
- TemplateParser
- TemplateFinder
-}
-
-// TemplateDebugger prints some debug info to stdout.
-type TemplateDebugger interface {
- Debug()
-}
-
-// TemplatesProvider as implemented by deps.Deps.
-type TemplatesProvider interface {
- Tmpl() TemplateHandler
- TextTmpl() TemplateParseFinder
-}
-
-var baseOfRe = regexp.MustCompile("template: (.*?):")
-
-func extractBaseOf(err string) string {
- m := baseOfRe.FindStringSubmatch(err)
- if len(m) == 2 {
- return m[1]
- }
- return ""
-}
-
-// TemplateFuncGetter allows to find a template func by name.
-type TemplateFuncGetter interface {
- GetFunc(name string) (reflect.Value, bool)
-}
-
+// RenderingContext represents the currently rendered site/language.
type RenderingContext struct {
Site site
SiteOutIdx int
}
const (
+ // HugoDeferredTemplatePrefix is the prefix for placeholders for deferred templates.
HugoDeferredTemplatePrefix = "__hdeferred/"
+ // HugoDeferredTemplateSuffix is the suffix for placeholders for deferred templates.
HugoDeferredTemplateSuffix = "__d="
)
return s
}
+// DeferredExecution holds the template and data for a deferred execution.
type DeferredExecution struct {
Mu sync.Mutex
Ctx context.Context
- TemplateName string
+ TemplatePath string
Data any
Executed bool
+++ /dev/null
-// 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 tpl
-
-// Increments on breaking changes.
-const TemplateVersion = 2
-
-type Info interface {
- ParseInfo() ParseInfo
-}
-
-type FileInfo interface {
- Name() string
- Filename() string
-}
-
-type IsInternalTemplateProvider interface {
- IsInternalTemplate() bool
-}
-
-type ParseInfo struct {
- // Set for shortcode templates with any {{ .Inner }}
- IsInner bool
-
- // Set for partials with a return statement.
- HasReturn bool
-
- // Config extracted from template.
- Config ParseConfig
-}
-
-func (info ParseInfo) IsZero() bool {
- return info.Config.Version == 0
-}
-
-type ParseConfig struct {
- Version int
-}
-
-var DefaultParseConfig = ParseConfig{
- Version: TemplateVersion,
-}
-
-var DefaultParseInfo = ParseInfo{
- Config: DefaultParseConfig,
-}
-// Copyright 2018 The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
import (
"testing"
-
- qt "github.com/frankban/quicktest"
)
-func TestExtractBaseof(t *testing.T) {
- c := qt.New(t)
-
- replaced := extractBaseOf(`failed: template: _default/baseof.html:37:11: executing "_default/baseof.html" at <.Parents>: can't evaluate field Parents in type *hugolib.PageOutput`)
-
- c.Assert(replaced, qt.Equals, "_default/baseof.html")
- c.Assert(extractBaseOf("not baseof for you"), qt.Equals, "")
- c.Assert(extractBaseOf("template: blog/baseof.html:23:11:"), qt.Equals, "blog/baseof.html")
-}
-
func TestStripHTML(t *testing.T) {
type test struct {
input, expected string
`
+func TestDeferNoBaseof(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/index.html --
+Home.
+{{ with (templates.Defer (dict "key" "foo")) }}
+ Defer
+{{ end }}
+-- content/_index.md --
+---
+title: "Home"
+---
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Home.\n\n Defer")
+}
+
+func TestDeferBaseof(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/baseof.html --
+{{ with (templates.Defer (dict "key" "foo")) }}
+Defer
+{{ end }}
+Block:{{ block "main" . }}{{ end }}$
+-- layouts/index.html --
+{{ define "main" }}
+Home.
+{{ end }}
+-- content/_index.md --
+---
+title: "Home"
+---
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Home.\n\n Defer")
+}
+
+func TestDeferMain(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/baseof.html --
+
+Block:{{ block "main" . }}{{ end }}$
+-- layouts/index.html --
+{{ define "main" }}
+Home.
+{{ with (templates.Defer (dict "key" "foo")) }}
+Defer
+{{ end }}
+{{ end }}
+-- content/_index.md --
+---
+title: "Home"
+---
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Home.\n\n Defer")
+}
+
func TestDeferBasic(t *testing.T) {
t.Parallel()
// Note that this is the Unix-styled relative path including filename suffix,
// e.g. partials/header.html
func (ns *Namespace) Exists(name string) bool {
- return ns.deps.Tmpl().HasTemplate(name)
+ return ns.deps.GetTemplateStore().HasTemplate(name)
}
// Defer defers the execution of a template block.
_, _ = ns.deps.BuildState.DeferredExecutions.Executions.GetOrCreate(id,
func() (*tpl.DeferredExecution, error) {
return &tpl.DeferredExecution{
- TemplateName: templateName,
+ TemplatePath: templateName,
Ctx: ctx,
Data: opts.Data,
Executed: false,
--- /dev/null
+// 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]]
+}
+++ /dev/null
-{{ $width := .Attributes.width }}
-{{ $height := .Attributes.height }}
-{{ $class := .Attributes.class | default "" }}
-<div class="goat svg-container {{ $class }}">
- {{ with diagrams.Goat .Inner }}
- <svg
- xmlns="http://www.w3.org/2000/svg"
- font-family="Menlo,Lucida Console,monospace"
- {{ if or $width $height }}
- {{ with $width }}width="{{ . }}"{{ end }}
- {{ with $height }}height="{{ . }}"{{ end }}
- {{ else }}
- viewBox="0 0 {{ .Width }} {{ .Height }}"
- {{ end }}>
- {{ .Inner }}
- </svg>
- {{ end }}
-</div>
+++ /dev/null
-{{- $u := urls.Parse .Destination -}}
-{{- $src := $u.String -}}
-{{- if not $u.IsAbs -}}
- {{- $path := strings.TrimPrefix "./" $u.Path -}}
- {{- with or (.PageInner.Resources.Get $path) (resources.Get $path) -}}
- {{- $src = .RelPermalink -}}
- {{- with $u.RawQuery -}}
- {{- $src = printf "%s?%s" $src . -}}
- {{- end -}}
- {{- with $u.Fragment -}}
- {{- $src = printf "%s#%s" $src . -}}
- {{- end -}}
- {{- end -}}
-{{- end -}}
-<img src="{{ $src }}" alt="{{ .PlainText }}"
- {{- with .Title }} title="{{ . }}" {{- end -}}
- {{- range $k, $v := .Attributes -}}
- {{- if $v -}}
- {{- printf " %s=%q" $k ($v | transform.HTMLEscape) | safeHTMLAttr -}}
- {{- end -}}
- {{- end -}}
->
-{{- /**/ -}}
+++ /dev/null
-{{- $u := urls.Parse .Destination -}}
-{{- $href := $u.String -}}
-{{- if strings.HasPrefix $u.String "#" -}}
- {{- $href = printf "%s#%s" .PageInner.RelPermalink $u.Fragment -}}
-{{- else if and $href (not $u.IsAbs) -}}
- {{- $path := strings.TrimPrefix "./" $u.Path -}}
- {{- with or
- ($.PageInner.GetPage $path)
- ($.PageInner.Resources.Get $path)
- (resources.Get $path)
- -}}
- {{- $href = .RelPermalink -}}
- {{- with $u.RawQuery -}}
- {{- $href = printf "%s?%s" $href . -}}
- {{- end -}}
- {{- with $u.Fragment -}}
- {{- $href = printf "%s#%s" $href . -}}
- {{- end -}}
- {{- end -}}
-{{- end -}}
-<a href="{{ $href }}" {{- with .Title }} title="{{ . }}" {{- end }}>{{ .Text }}</a>
-{{- /**/ -}}
+++ /dev/null
-<table
- {{- range $k, $v := .Attributes }}
- {{- if $v }}
- {{- printf " %s=%q" $k ($v | transform.HTMLEscape) | safeHTMLAttr }}
- {{- end }}
- {{- end }}>
- <thead>
- {{- range .THead }}
- <tr>
- {{- range . }}
- <th
- {{- with .Alignment }}
- {{- printf " style=%q" (printf "text-align: %s" .) | safeHTMLAttr }}
- {{- end -}}
- >
- {{- .Text -}}
- </th>
- {{- end }}
- </tr>
- {{- end }}
- </thead>
- <tbody>
- {{- range .TBody }}
- <tr>
- {{- range . }}
- <td
- {{- with .Alignment }}
- {{- printf " style=%q" (printf "text-align: %s" .) | safeHTMLAttr }}
- {{- end -}}
- >
- {{- .Text -}}
- </td>
- {{- end }}
- </tr>
- {{- end }}
- </tbody>
-</table>
+++ /dev/null
-User-agent: *
+++ /dev/null
-{{- $authorEmail := "" }}
-{{- with site.Params.author }}
- {{- if reflect.IsMap . }}
- {{- with .email }}
- {{- $authorEmail = . }}
- {{- end }}
- {{- end }}
-{{- end }}
-
-{{- $authorName := "" }}
-{{- with site.Params.author }}
- {{- if reflect.IsMap . }}
- {{- with .name }}
- {{- $authorName = . }}
- {{- end }}
- {{- else }}
- {{- $authorName = . }}
- {{- end }}
-{{- end }}
-
-{{- $pctx := . }}
-{{- if .IsHome }}{{ $pctx = .Site }}{{ end }}
-{{- $pages := slice }}
-{{- if or $.IsHome $.IsSection }}
-{{- $pages = $pctx.RegularPages }}
-{{- else }}
-{{- $pages = $pctx.Pages }}
-{{- end }}
-{{- $limit := .Site.Config.Services.RSS.Limit }}
-{{- if ge $limit 1 }}
-{{- $pages = $pages | first $limit }}
-{{- end }}
-{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
-<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
- <channel>
- <title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{ . }} on {{ end }}{{ .Site.Title }}{{ end }}</title>
- <link>{{ .Permalink }}</link>
- <description>Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{ . }} {{ end }}{{ end }}on {{ .Site.Title }}</description>
- <generator>Hugo</generator>
- <language>{{ site.Language.LanguageCode }}</language>{{ with $authorEmail }}
- <managingEditor>{{.}}{{ with $authorName }} ({{ . }}){{ end }}</managingEditor>{{ end }}{{ with $authorEmail }}
- <webMaster>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</webMaster>{{ end }}{{ with .Site.Copyright }}
- <copyright>{{ . }}</copyright>{{ end }}{{ if not .Date.IsZero }}
- <lastBuildDate>{{ (index $pages.ByLastmod.Reverse 0).Lastmod.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
- {{- with .OutputFormats.Get "RSS" }}
- {{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
- {{- end }}
- {{- range $pages }}
- <item>
- <title>{{ .Title }}</title>
- <link>{{ .Permalink }}</link>
- <pubDate>{{ .PublishDate.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
- {{- with $authorEmail }}<author>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</author>{{ end }}
- <guid>{{ .Permalink }}</guid>
- <description>{{ .Summary | transform.XMLEscape | safeHTML }}</description>
- </item>
- {{- end }}
- </channel>
-</rss>
+++ /dev/null
-{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
-<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
- xmlns:xhtml="http://www.w3.org/1999/xhtml">
- {{ range where .Pages "Sitemap.Disable" "ne" true }}
- {{- if .Permalink -}}
- <url>
- <loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }}
- <lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>{{ end }}{{ with .Sitemap.ChangeFreq }}
- <changefreq>{{ . }}</changefreq>{{ end }}{{ if ge .Sitemap.Priority 0.0 }}
- <priority>{{ .Sitemap.Priority }}</priority>{{ end }}{{ if .IsTranslated }}{{ range .Translations }}
- <xhtml:link
- rel="alternate"
- hreflang="{{ .Language.LanguageCode }}"
- href="{{ .Permalink }}"
- />{{ end }}
- <xhtml:link
- rel="alternate"
- hreflang="{{ .Language.LanguageCode }}"
- href="{{ .Permalink }}"
- />{{ end }}
- </url>
- {{- end -}}
- {{ end }}
-</urlset>
+++ /dev/null
-{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
-<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
- {{ range . }}
- <sitemap>
- <loc>{{ .SitemapAbsURL }}</loc>
- {{ if not .Lastmod.IsZero }}
- <lastmod>{{ .Lastmod.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</lastmod>
- {{ end }}
- </sitemap>
- {{ end }}
-</sitemapindex>
--- /dev/null
+{{ $width := .Attributes.width }}
+{{ $height := .Attributes.height }}
+{{ $class := .Attributes.class | default "" }}
+<div class="goat svg-container {{ $class }}">
+ {{ with diagrams.Goat .Inner }}
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ font-family="Menlo,Lucida Console,monospace"
+ {{ if or $width $height }}
+ {{ with $width }}width="{{ . }}"{{ end }}
+ {{ with $height }}height="{{ . }}"{{ end }}
+ {{ else }}
+ viewBox="0 0 {{ .Width }} {{ .Height }}"
+ {{ end }}>
+ {{ .Inner }}
+ </svg>
+ {{ end }}
+</div>
--- /dev/null
+{{- $u := urls.Parse .Destination -}}
+{{- $src := $u.String -}}
+{{- if not $u.IsAbs -}}
+ {{- $path := strings.TrimPrefix "./" $u.Path -}}
+ {{- with or (.PageInner.Resources.Get $path) (resources.Get $path) -}}
+ {{- $src = .RelPermalink -}}
+ {{- with $u.RawQuery -}}
+ {{- $src = printf "%s?%s" $src . -}}
+ {{- end -}}
+ {{- with $u.Fragment -}}
+ {{- $src = printf "%s#%s" $src . -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+<img src="{{ $src }}" alt="{{ .PlainText }}"
+ {{- with .Title }} title="{{ . }}" {{- end -}}
+ {{- range $k, $v := .Attributes -}}
+ {{- if $v -}}
+ {{- printf " %s=%q" $k ($v | transform.HTMLEscape) | safeHTMLAttr -}}
+ {{- end -}}
+ {{- end -}}
+>
+{{- /**/ -}}
--- /dev/null
+{{- $u := urls.Parse .Destination -}}
+{{- $href := $u.String -}}
+{{- if strings.HasPrefix $u.String "#" -}}
+ {{- $href = printf "%s#%s" .PageInner.RelPermalink $u.Fragment -}}
+{{- else if and $href (not $u.IsAbs) -}}
+ {{- $path := strings.TrimPrefix "./" $u.Path -}}
+ {{- with or
+ ($.PageInner.GetPage $path)
+ ($.PageInner.Resources.Get $path)
+ (resources.Get $path)
+ -}}
+ {{- $href = .RelPermalink -}}
+ {{- with $u.RawQuery -}}
+ {{- $href = printf "%s?%s" $href . -}}
+ {{- end -}}
+ {{- with $u.Fragment -}}
+ {{- $href = printf "%s#%s" $href . -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+<a href="{{ $href }}" {{- with .Title }} title="{{ . }}" {{- end }}>{{ .Text }}</a>
+{{- /**/ -}}
--- /dev/null
+<table
+ {{- range $k, $v := .Attributes }}
+ {{- if $v }}
+ {{- printf " %s=%q" $k ($v | transform.HTMLEscape) | safeHTMLAttr }}
+ {{- end }}
+ {{- end }}>
+ <thead>
+ {{- range .THead }}
+ <tr>
+ {{- range . }}
+ <th
+ {{- with .Alignment }}
+ {{- printf " style=%q" (printf "text-align: %s" .) | safeHTMLAttr }}
+ {{- end -}}
+ >
+ {{- .Text -}}
+ </th>
+ {{- end }}
+ </tr>
+ {{- end }}
+ </thead>
+ <tbody>
+ {{- range .TBody }}
+ <tr>
+ {{- range . }}
+ <td
+ {{- with .Alignment }}
+ {{- printf " style=%q" (printf "text-align: %s" .) | safeHTMLAttr }}
+ {{- end -}}
+ >
+ {{- .Text -}}
+ </td>
+ {{- end }}
+ </tr>
+ {{- end }}
+ </tbody>
+</table>
--- /dev/null
+{{- $imgs := slice }}
+{{- $imgParams := .Params.images }}
+{{- $resources := .Resources.ByType "image" -}}
+{{/* Find featured image resources if the images parameter is empty. */}}
+{{- if not $imgParams }}
+ {{- $featured := $resources.GetMatch "*feature*" -}}
+ {{- if not $featured }}{{ $featured = $resources.GetMatch "{*cover*,*thumbnail*}" }}{{ end -}}
+ {{- with $featured }}
+ {{- $imgs = $imgs | append (dict
+ "Image" .
+ "RelPermalink" .RelPermalink
+ "Permalink" .Permalink) }}
+ {{- end }}
+{{- end }}
+{{/* Use the first one of site images as the fallback. */}}
+{{- if and (not $imgParams) (not $imgs) }}
+ {{- with site.Params.images }}
+ {{- $imgParams = first 1 . }}
+ {{- end }}
+{{- end }}
+{{/* Parse page's images parameter. */}}
+{{- range $imgParams }}
+ {{- $img := . }}
+ {{- $url := urls.Parse $img }}
+ {{- if eq $url.Scheme "" }}
+ {{/* Internal image. */}}
+ {{- with $resources.GetMatch $img -}}
+ {{/* Image resource. */}}
+ {{- $imgs = $imgs | append (dict
+ "Image" .
+ "RelPermalink" .RelPermalink
+ "Permalink" .Permalink) }}
+ {{- else }}
+ {{- $imgs = $imgs | append (dict
+ "RelPermalink" (relURL $img)
+ "Permalink" (absURL $img)
+ ) }}
+ {{- end }}
+ {{- else }}
+ {{/* External image */}}
+ {{- $imgs = $imgs | append (dict
+ "RelPermalink" $img
+ "Permalink" $img
+ ) }}
+ {{- end }}
+{{- end }}
+{{- return $imgs }}
--- /dev/null
+{{- $pc := .Site.Config.Privacy.Disqus -}}
+{{- if not $pc.Disable -}}
+{{ if .Site.Config.Services.Disqus.Shortname }}<div id="disqus_thread"></div>
+<script>
+ window.disqus_config = function () {
+ {{with .Params.disqus_identifier }}this.page.identifier = '{{ . }}';{{end}}
+ {{with .Params.disqus_title }}this.page.title = '{{ . }}';{{end}}
+ {{with .Params.disqus_url }}this.page.url = '{{ . | transform.HTMLEscape | safeURL }}';{{end}}
+ };
+ (function() {
+ if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) {
+ document.getElementById('disqus_thread').innerHTML = 'Disqus comments not available by default when the website is previewed locally.';
+ return;
+ }
+ var d = document, s = d.createElement('script'); s.async = true;
+ s.src = '//' + {{ .Site.Config.Services.Disqus.Shortname }} + '.disqus.com/embed.js';
+ s.setAttribute('data-timestamp', +new Date());
+ (d.head || d.body).appendChild(s);
+ })();
+</script>
+<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
+<a href="https://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>{{end}}
+{{- end -}}
--- /dev/null
+{{ if not site.Config.Privacy.GoogleAnalytics.Disable }}
+ {{- with site.Config.Services.GoogleAnalytics.ID }}
+ {{- if strings.HasPrefix (lower .) "ua-" }}
+ {{- warnf "Google Analytics 4 (GA4) replaced Google Universal Analytics (UA) effective 1 July 2023. See https://support.google.com/analytics/answer/11583528. Create a GA4 property and data stream, then replace the Google Analytics ID in your site configuration with the new value." }}
+ {{- else }}
+ <script async src="https://www.googletagmanager.com/gtag/js?id={{ . }}"></script>
+ <script>
+ var doNotTrack = false;
+ if ({{ site.Config.Privacy.GoogleAnalytics.RespectDoNotTrack }}) {
+ var dnt = (navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack);
+ var doNotTrack = (dnt == "1" || dnt == "yes");
+ }
+ if (!doNotTrack) {
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', '{{ . }}');
+ }
+ </script>
+ {{- end }}
+ {{- end }}
+{{- end -}}
--- /dev/null
+<meta property="og:url" content="{{ .Permalink }}">
+
+{{- with or site.Title site.Params.title | plainify }}
+ <meta property="og:site_name" content="{{ . }}">
+{{- end }}
+
+{{- with or .Title site.Title site.Params.title | plainify }}
+ <meta property="og:title" content="{{ . }}">
+{{- end }}
+
+{{- with or .Description .Summary site.Params.description | plainify | htmlUnescape }}
+ <meta property="og:description" content="{{ trim . "\n\r\t " }}">
+{{- end }}
+
+{{- with or .Params.locale site.Language.LanguageCode }}
+ <meta property="og:locale" content="{{ replace . `-` `_` }}">
+{{- end }}
+
+{{- if .IsPage }}
+ <meta property="og:type" content="article">
+ {{- with .Section }}
+ <meta property="article:section" content="{{ . }}">
+ {{- end }}
+ {{- $ISO8601 := "2006-01-02T15:04:05-07:00" }}
+ {{- with .PublishDate }}
+ <meta property="article:published_time" {{ .Format $ISO8601 | printf "content=%q" | safeHTMLAttr }}>
+ {{- end }}
+ {{- with .Lastmod }}
+ <meta property="article:modified_time" {{ .Format $ISO8601 | printf "content=%q" | safeHTMLAttr }}>
+ {{- end }}
+ {{- range .GetTerms "tags" | first 6 }}
+ <meta property="article:tag" content="{{ .Page.Title | plainify }}">
+ {{- end }}
+{{- else }}
+ <meta property="og:type" content="website">
+{{- end }}
+
+{{- with partial "_funcs/get-page-images" . }}
+ {{- range . | first 6 }}
+ <meta property="og:image" content="{{ .Permalink }}">
+ {{- end }}
+{{- end }}
+
+{{- with .Params.audio }}
+ {{- range . | first 6 }}
+ <meta property="og:audio" content="{{ . | absURL }}">
+ {{- end }}
+{{- end }}
+
+{{- with .Params.videos }}
+ {{- range . | first 6 }}
+ <meta property="og:video" content="{{ . | absURL }}">
+ {{- end }}
+{{- end }}
+
+{{- range .GetTerms "series" }}
+ {{- range .Pages | first 7 }}
+ {{- if ne $ . }}
+ <meta property="og:see_also" content="{{ .Permalink }}">
+ {{- end }}
+ {{- end }}
+{{- end }}
+
+{{- with site.Params.social }}
+ {{- if reflect.IsMap . }}
+ {{- with .facebook_app_id }}
+ <meta property="fb:app_id" content="{{ . }}">
+ {{- else }}
+ {{- with .facebook_admin }}
+ <meta property="fb:admins" content="{{ . }}">
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end }}
--- /dev/null
+{{- $validFormats := slice "default" "terse" }}
+
+{{- $msg1 := "When passing a map to the internal pagination template, one of the elements must be named 'page', and it must be set to the context of the current page." }}
+{{- $msg2 := "The 'format' specified in the map passed to the internal pagination template is invalid. Valid choices are: %s." }}
+
+{{- $page := . }}
+{{- $format := "default" }}
+
+{{- if reflect.IsMap . }}
+ {{- with .page }}
+ {{- $page = . }}
+ {{- else }}
+ {{- errorf $msg1 }}
+ {{- end }}
+ {{- with .format }}
+ {{- $format = lower . }}
+ {{- end }}
+{{- end }}
+
+{{- if in $validFormats $format }}
+ {{- if gt $page.Paginator.TotalPages 1 }}
+ <ul class="pagination pagination-{{ $format }}">
+ {{- partial (printf "inline/pagination/%s" $format) $page }}
+ </ul>
+ {{- end }}
+{{- else }}
+ {{- errorf $msg2 (delimit $validFormats ", ") }}
+{{- end -}}
+
+{{/* Format: default
+{{/* --------------------------------------------------------------------- */}}
+{{- define "partials/inline/pagination/default" }}
+ {{- with .Paginator }}
+ {{- $currentPageNumber := .PageNumber }}
+
+ {{- with .First }}
+ {{- if ne $currentPageNumber .PageNumber }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="First" class="page-link" role="button"><span aria-hidden="true">««</span></a>
+ </li>
+ {{- else }}
+ <li class="page-item disabled">
+ <a aria-disabled="true" aria-label="First" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">««</span></a>
+ </li>
+ {{- end }}
+ {{- end }}
+
+ {{- with .Prev }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="Previous" class="page-link" role="button"><span aria-hidden="true">«</span></a>
+ </li>
+ {{- else }}
+ <li class="page-item disabled">
+ <a aria-disabled="true" aria-label="Previous" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">«</span></a>
+ </li>
+ {{- end }}
+
+ {{- $slots := 5 }}
+ {{- $start := math.Max 1 (sub .PageNumber (math.Floor (div $slots 2))) }}
+ {{- $end := math.Min .TotalPages (sub (add $start $slots) 1) }}
+ {{- if lt (add (sub $end $start) 1) $slots }}
+ {{- $start = math.Max 1 (add (sub $end $slots) 1) }}
+ {{- end }}
+
+ {{- range $k := seq $start $end }}
+ {{- if eq $.Paginator.PageNumber $k }}
+ <li class="page-item active">
+ <a aria-current="page" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
+ </li>
+ {{- else }}
+ <li class="page-item">
+ <a href="{{ (index $.Paginator.Pagers (sub $k 1)).URL }}" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
+ </li>
+ {{- end }}
+ {{- end }}
+
+ {{- with .Next }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="Next" class="page-link" role="button"><span aria-hidden="true">»</span></a>
+ </li>
+ {{- else }}
+ <li class="page-item disabled">
+ <a aria-disabled="true" aria-label="Next" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">»</span></a>
+ </li>
+ {{- end }}
+
+ {{- with .Last }}
+ {{- if ne $currentPageNumber .PageNumber }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="Last" class="page-link" role="button"><span aria-hidden="true">»»</span></a>
+ </li>
+ {{- else }}
+ <li class="page-item disabled">
+ <a aria-disabled="true" aria-label="Last" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">»»</span></a>
+ </li>
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end -}}
+
+{{/* Format: terse
+{{/* --------------------------------------------------------------------- */}}
+{{- define "partials/inline/pagination/terse" }}
+ {{- with .Paginator }}
+ {{- $currentPageNumber := .PageNumber }}
+
+ {{- with .First }}
+ {{- if ne $currentPageNumber .PageNumber }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="First" class="page-link" role="button"><span aria-hidden="true">««</span></a>
+ </li>
+ {{- end }}
+ {{- end }}
+
+ {{- with .Prev }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="Previous" class="page-link" role="button"><span aria-hidden="true">«</span></a>
+ </li>
+ {{- end }}
+
+ {{- $slots := 3 }}
+ {{- $start := math.Max 1 (sub .PageNumber (math.Floor (div $slots 2))) }}
+ {{- $end := math.Min .TotalPages (sub (add $start $slots) 1) }}
+ {{- if lt (add (sub $end $start) 1) $slots }}
+ {{- $start = math.Max 1 (add (sub $end $slots) 1) }}
+ {{- end }}
+
+ {{- range $k := seq $start $end }}
+ {{- if eq $.Paginator.PageNumber $k }}
+ <li class="page-item active">
+ <a aria-current="page" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
+ </li>
+ {{- else }}
+ <li class="page-item">
+ <a href="{{ (index $.Paginator.Pagers (sub $k 1)).URL }}" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
+ </li>
+ {{- end }}
+ {{- end }}
+
+ {{- with .Next }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="Next" class="page-link" role="button"><span aria-hidden="true">»</span></a>
+ </li>
+ {{- end }}
+
+ {{- with .Last }}
+ {{- if ne $currentPageNumber .PageNumber }}
+ <li class="page-item">
+ <a href="{{ .URL }}" aria-label="Last" class="page-link" role="button"><span aria-hidden="true">»»</span></a>
+ </li>
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end -}}
--- /dev/null
+{{- with or .Title site.Title | plainify }}
+ <meta itemprop="name" content="{{ . }}">
+{{- end }}
+
+{{- with or .Description .Summary site.Params.description | plainify | htmlUnescape }}
+ <meta itemprop="description" content="{{ trim . "\n\r\t " }}">
+{{- end }}
+
+{{- $ISO8601 := "2006-01-02T15:04:05-07:00" }}
+{{- with .PublishDate }}
+ <meta itemprop="datePublished" {{ .Format $ISO8601 | printf "content=%q" | safeHTMLAttr }}>
+{{- end }}
+
+{{- with .Lastmod }}
+ <meta itemprop="dateModified" {{ .Format $ISO8601 | printf "content=%q" | safeHTMLAttr }}>
+{{- end }}
+
+{{- with .WordCount }}
+ <meta itemprop="wordCount" content="{{ . }}">
+{{- end }}
+
+{{- $images := partial "_funcs/get-page-images" . }}
+{{- range first 6 $images }}
+ <meta itemprop="image" content="{{ .Permalink }}">
+{{- end }}
+
+{{- /*
+Keywords precedence:
+
+1. Use "keywords" term page titles.
+2. Use "keywords" from front matter if "keywords" is not a taxonomy.
+3. Use "tags" term page titles.
+4. Use term page titles from all taxonomies.
+
+*/}}
+{{- $keywords := slice }}
+{{- range .GetTerms "keywords" }}
+ {{- $keywords = $keywords | append .Title }}
+{{- else }}
+ {{- with .Keywords }}
+ {{- $keywords = . }}
+ {{- else }}
+ {{- range .GetTerms "tags" }}
+ {{- $keywords = $keywords | append .Title }}
+ {{- else }}
+ {{- range $taxonomy, $_ := site.Taxonomies }}
+ {{- range $.GetTerms $taxonomy }}
+ {{- $keywords = $keywords | append .Title }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end }}
+{{- with $keywords }}
+ <meta itemprop="keywords" content="{{ delimit . `,` }}">
+{{- end -}}
--- /dev/null
+{{- $images := partial "_funcs/get-page-images" . }}
+{{- with index $images 0 }}
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:image" content="{{ .Permalink }}">
+{{- else }}
+ <meta name="twitter:card" content="summary">
+{{- end }}
+
+{{- with or .Title site.Title site.Params.title | plainify }}
+ <meta name="twitter:title" content="{{ . }}">
+{{- end }}
+
+{{- with or .Description .Summary site.Params.description | plainify | htmlUnescape }}
+ <meta name="twitter:description" content="{{ trim . "\n\r\t " }}">
+{{- end }}
+
+{{- $twitterSite := "" }}
+{{- with site.Params.social }}
+ {{- if reflect.IsMap . }}
+ {{- with .twitter }}
+ {{- $content := . }}
+ {{- if not (strings.HasPrefix . "@") }}
+ {{- $content = printf "@%v" . }}
+ {{- end }}
+ <meta name="twitter:site" content="{{ $content }}">
+ {{- end }}
+ {{- end }}
+{{- end }}
--- /dev/null
+{{ define "__h_simple_css" }}{{/* These template definitions are global. */}}
+{{- if not (.Page.Scratch.Get "__h_simple_css") -}}
+{{/* Only include once */}}
+{{- .Page.Scratch.Set "__h_simple_css" true -}}
+<style>
+.__h_video {
+ position: relative;
+ padding-bottom: 56.23%;
+ height: 0;
+ overflow: hidden;
+ width: 100%;
+ background: #000;
+}
+.__h_video img {
+ width: 100%;
+ height: auto;
+ color: #000;
+}
+.__h_video .play {
+ height: 72px;
+ width: 72px;
+ left: 50%;
+ top: 50%;
+ margin-left: -36px;
+ margin-top: -36px;
+ position: absolute;
+ cursor: pointer;
+}
+</style>
+{{- end -}}
+{{- end -}}
+{{- define "__h_simple_icon_play" -}}
+<svg version="1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 61 61"><circle cx="30.5" cy="30.5" r="30.5" opacity=".8" fill="#000"></circle><path d="M25.3 19.2c-2.1-1.2-3.8-.2-3.8 2.2v18.1c0 2.4 1.7 3.4 3.8 2.2l16.6-9.1c2.1-1.2 2.1-3.2 0-4.4l-16.6-9z" fill="#fff"></path></svg>
+{{- end -}}
--- /dev/null
+{{- warnf "The %q shortcode was deprecated in v0.143.0 and will be removed in a future release. Please use HTML comments instead. See %s" .Name .Position -}}
+{{- $noop := .Inner -}}
--- /dev/null
+{{- /*
+Renders an HTML details element.
+
+@param {string} [class] The value of the element's class attribute.
+@param {string} [name] The value of the element's name attribute.
+@param {string} [summary] The content of the child summary element.
+@param {string} [title] The value of the element's title attribute.
+@param {bool} [open=false] Whether to initially display the content of the details element.
+
+@reference https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details
+
+@examples
+
+ {{< details >}}
+ A basic collapsible section.
+ {{< /details >}}
+
+ {{< details summary="Custom Summary Text" >}}
+ Showing custom `summary` text.
+ {{< /details >}}
+
+ {{< details summary="Open Details" open=true >}}
+ Contents displayed initially by using `open`.
+ {{< /details >}}
+
+ {{< details summary="Styled Content" class="my-custom-class" >}}
+ Content can be styled with CSS by specifying a `class`.
+
+ Target details element:
+
+ ```css
+ details.my-custom-class { }
+ ```
+
+ Target summary element:
+
+ ```css
+ details.my-custom-class > summary > * { }
+ ```
+
+ Target inner content:
+
+ ```css
+ details.my-custom-class > :not(summary) { }
+ ```
+ {{< /details >}}
+
+ {{< details summary="Grouped Details" name="my-details" >}}
+ Specifying a `name` allows elements to be connected, with only one able to be open at a time.
+ {{< /details >}}
+
+*/}}
+
+{{- /* Get arguments. */}}
+{{- $class := or (.Get "class") "" }}
+{{- $name := or (.Get "name") "" }}
+{{- $summary := or (.Get "summary") "Details" }}
+{{- $title := or (.Get "title") "" }}
+{{- $open := false }}
+{{- if in (slice "false" false 0) (.Get "open") }}
+ {{- $open = false }}
+{{- else if in (slice "true" true 1) (.Get "open") }}
+ {{- $open = true }}
+{{- end }}
+
+{{- /* Render. */}}
+<details
+ {{- with $class }} class="{{ . }}" {{- end }}
+ {{- with $name }} name="{{ . }}" {{- end }}
+ {{- with $open }} open {{- end }}
+ {{- with $title }} title="{{ . }}" {{- end -}}
+>
+ <summary>{{ $summary | .Page.RenderString }}</summary>
+ {{ .Inner | .Page.RenderString (dict "display" "block") -}}
+</details>
--- /dev/null
+<figure{{ with .Get "class" }} class="{{ . }}"{{ end }}>
+ {{- if .Get "link" -}}
+ <a href="{{ .Get "link" }}"{{ with .Get "target" }} target="{{ . }}"{{ end }}{{ with .Get "rel" }} rel="{{ . }}"{{ end }}>
+ {{- end -}}
+
+ {{- $u := urls.Parse (.Get "src") -}}
+ {{- $src := $u.String -}}
+ {{- if not $u.IsAbs -}}
+ {{- with or (.Page.Resources.Get $u.Path) (resources.Get $u.Path) -}}
+ {{- $src = .RelPermalink -}}
+ {{- end -}}
+ {{- end -}}
+
+ <img src="{{ $src }}"
+ {{- if or (.Get "alt") (.Get "caption") }}
+ alt="{{ with .Get "alt" }}{{ . }}{{ else }}{{ .Get "caption" | markdownify| plainify }}{{ end }}"
+ {{- end -}}
+ {{- with .Get "width" }} width="{{ . }}"{{ end -}}
+ {{- with .Get "height" }} height="{{ . }}"{{ end -}}
+ {{- with .Get "loading" }} loading="{{ . }}"{{ end -}}
+ ><!-- Closing img tag -->
+ {{- if .Get "link" }}</a>{{ end -}}
+ {{- if or (or (.Get "title") (.Get "caption")) (.Get "attr") -}}
+ <figcaption>
+ {{ with (.Get "title") -}}
+ <h4>{{ . }}</h4>
+ {{- end -}}
+ {{- if or (.Get "caption") (.Get "attr") -}}<p>
+ {{- .Get "caption" | markdownify -}}
+ {{- with .Get "attrlink" }}
+ <a href="{{ . }}">
+ {{- end -}}
+ {{- .Get "attr" | markdownify -}}
+ {{- if .Get "attrlink" }}</a>{{ end }}</p>
+ {{- end }}
+ </figcaption>
+ {{- end }}
+</figure>
--- /dev/null
+{{- warnf "The %q shortcode was deprecated in v0.143.0 and will be removed in a future release. See https://gohugo.io/shortcodes/gist for instructions to create a replacement." .Name -}}
+<script src="https://gist.github.com/{{ index .Params 0 }}/{{ index .Params 1 }}.js{{if len .Params | eq 3 }}?file={{ index .Params 2 }}{{end}}"></script>
--- /dev/null
+{{ if len .Params | eq 2 }}{{ highlight (trim .InnerDeindent "\n\r") (.Get 0) (.Get 1) }}{{ else }}{{ highlight (trim .InnerDeindent "\n\r") (.Get 0) "" }}{{ end }}
\ No newline at end of file
--- /dev/null
+{{- $pc := site.Config.Privacy.Instagram -}}
+{{- if not $pc.Disable -}}
+ {{- with .Get 0 -}}
+ {{- template "render-instagram" (dict "id" . "pc" $pc) -}}
+ {{- else -}}
+ {{- errorf "The %q shortocde requires a single positional parameter, the ID of the Instagram post. See %s" .Name .Position -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "render-instagram" -}}
+ <blockquote
+ class="instagram-media"
+ data-instgrm-captioned
+ data-instgrm-permalink="https://www.instagram.com/p/{{ .id }}"
+ data-instgrm-version="14"
+ style="
+ background: #fff;
+ border: 0;
+ border-radius: 3px;
+ box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.5), 0 1px 10px 0 rgba(0, 0, 0, 0.15);
+ margin: 1px;
+ max-width: 540px;
+ min-width: 326px;
+ padding: 0;
+ width: 99.375%;
+ width: -webkit-calc(100% - 2px);
+ width: calc(100% - 2px);
+ "
+ >
+ <div style="padding: 16px">
+ <a
+ href="https://www.instagram.com/p/{{ .id }}"
+ style="
+ background: #ffffff;
+ line-height: 0;
+ padding: 0 0;
+ text-align: center;
+ text-decoration: none;
+ width: 100%;
+ "
+ target="_blank"
+ >
+ <div style="display: flex; flex-direction: row; align-items: center">
+ <div
+ style="
+ background-color: #f4f4f4;
+ border-radius: 50%;
+ flex-grow: 0;
+ height: 40px;
+ margin-right: 14px;
+ width: 40px;
+ "
+ ></div>
+ <div
+ style="
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ justify-content: center;
+ "
+ >
+ <div
+ style="
+ background-color: #f4f4f4;
+ border-radius: 4px;
+ flex-grow: 0;
+ height: 14px;
+ margin-bottom: 6px;
+ width: 100px;
+ "
+ ></div>
+ <div
+ style="
+ background-color: #f4f4f4;
+ border-radius: 4px;
+ flex-grow: 0;
+ height: 14px;
+ width: 60px;
+ "
+ ></div>
+ </div>
+ </div>
+ <div style="padding: 19% 0"></div>
+ <div
+ style="display: block; height: 50px; margin: 0 auto 12px; width: 50px"
+ >
+ <svg
+ width="50px"
+ height="50px"
+ viewBox="0 0 60 60"
+ version="1.1"
+ xmlns="https://www.w3.org/2000/svg"
+ xmlns:xlink="https://www.w3.org/1999/xlink"
+ >
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g transform="translate(-511.000000, -20.000000)" fill="#000000">
+ <g>
+ <path
+ d="M556.869,30.41 C554.814,30.41 553.148,32.076 553.148,34.131 C553.148,36.186 554.814,37.852 556.869,37.852 C558.924,37.852 560.59,36.186 560.59,34.131 C560.59,32.076 558.924,30.41 556.869,30.41 M541,60.657 C535.114,60.657 530.342,55.887 530.342,50 C530.342,44.114 535.114,39.342 541,39.342 C546.887,39.342 551.658,44.114 551.658,50 C551.658,55.887 546.887,60.657 541,60.657 M541,33.886 C532.1,33.886 524.886,41.1 524.886,50 C524.886,58.899 532.1,66.113 541,66.113 C549.9,66.113 557.115,58.899 557.115,50 C557.115,41.1 549.9,33.886 541,33.886 M565.378,62.101 C565.244,65.022 564.756,66.606 564.346,67.663 C563.803,69.06 563.154,70.057 562.106,71.106 C561.058,72.155 560.06,72.803 558.662,73.347 C557.607,73.757 556.021,74.244 553.102,74.378 C549.944,74.521 548.997,74.552 541,74.552 C533.003,74.552 532.056,74.521 528.898,74.378 C525.979,74.244 524.393,73.757 523.338,73.347 C521.94,72.803 520.942,72.155 519.894,71.106 C518.846,70.057 518.197,69.06 517.654,67.663 C517.244,66.606 516.755,65.022 516.623,62.101 C516.479,58.943 516.448,57.996 516.448,50 C516.448,42.003 516.479,41.056 516.623,37.899 C516.755,34.978 517.244,33.391 517.654,32.338 C518.197,30.938 518.846,29.942 519.894,28.894 C520.942,27.846 521.94,27.196 523.338,26.654 C524.393,26.244 525.979,25.756 528.898,25.623 C532.057,25.479 533.004,25.448 541,25.448 C548.997,25.448 549.943,25.479 553.102,25.623 C556.021,25.756 557.607,26.244 558.662,26.654 C560.06,27.196 561.058,27.846 562.106,28.894 C563.154,29.942 563.803,30.938 564.346,32.338 C564.756,33.391 565.244,34.978 565.378,37.899 C565.522,41.056 565.552,42.003 565.552,50 C565.552,57.996 565.522,58.943 565.378,62.101 M570.82,37.631 C570.674,34.438 570.167,32.258 569.425,30.349 C568.659,28.377 567.633,26.702 565.965,25.035 C564.297,23.368 562.623,22.342 560.652,21.575 C558.743,20.834 556.562,20.326 553.369,20.18 C550.169,20.033 549.148,20 541,20 C532.853,20 531.831,20.033 528.631,20.18 C525.438,20.326 523.257,20.834 521.349,21.575 C519.376,22.342 517.703,23.368 516.035,25.035 C514.368,26.702 513.342,28.377 512.574,30.349 C511.834,32.258 511.326,34.438 511.181,37.631 C511.035,40.831 511,41.851 511,50 C511,58.147 511.035,59.17 511.181,62.369 C511.326,65.562 511.834,67.743 512.574,69.651 C513.342,71.625 514.368,73.296 516.035,74.965 C517.703,76.634 519.376,77.658 521.349,78.425 C523.257,79.167 525.438,79.673 528.631,79.82 C531.831,79.965 532.853,80.001 541,80.001 C549.148,80.001 550.169,79.965 553.369,79.82 C556.562,79.673 558.743,79.167 560.652,78.425 C562.623,77.658 564.297,76.634 565.965,74.965 C567.633,73.296 568.659,71.625 569.425,69.651 C570.167,67.743 570.674,65.562 570.82,62.369 C570.966,59.17 571,58.147 571,50 C571,41.851 570.966,40.831 570.82,37.631"
+ ></path>
+ </g>
+ </g>
+ </g>
+ </svg>
+ </div>
+ <div style="padding-top: 8px">
+ <div
+ style="
+ color: #3897f0;
+ font-family: Arial, sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 550;
+ line-height: 18px;
+ "
+ >
+ View this post on Instagram
+ </div>
+ </div>
+ <div style="padding: 12.5% 0"></div>
+ <div
+ style="
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 14px;
+ align-items: center;
+ "
+ >
+ <div>
+ <div
+ style="
+ background-color: #f4f4f4;
+ border-radius: 50%;
+ height: 12.5px;
+ width: 12.5px;
+ transform: translateX(0px) translateY(7px);
+ "
+ ></div>
+ <div
+ style="
+ background-color: #f4f4f4;
+ height: 12.5px;
+ transform: rotate(-45deg) translateX(3px) translateY(1px);
+ width: 12.5px;
+ flex-grow: 0;
+ margin-right: 14px;
+ margin-left: 2px;
+ "
+ ></div>
+ <div
+ style="
+ background-color: #f4f4f4;
+ border-radius: 50%;
+ height: 12.5px;
+ width: 12.5px;
+ transform: translateX(9px) translateY(-18px);
+ "
+ ></div>
+ </div>
+ <div style="margin-left: 8px">
+ <div
+ style="
+ background-color: #f4f4f4;
+ border-radius: 50%;
+ flex-grow: 0;
+ height: 20px;
+ width: 20px;
+ "
+ ></div>
+ <div
+ style="
+ width: 0;
+ height: 0;
+ border-top: 2px solid transparent;
+ border-left: 6px solid #f4f4f4;
+ border-bottom: 2px solid transparent;
+ transform: translateX(16px) translateY(-4px) rotate(30deg);
+ "
+ ></div>
+ </div>
+ <div style="margin-left: auto">
+ <div
+ style="
+ width: 0px;
+ border-top: 8px solid #f4f4f4;
+ border-right: 8px solid transparent;
+ transform: translateY(16px);
+ "
+ ></div>
+ <div
+ style="
+ background-color: #f4f4f4;
+ flex-grow: 0;
+ height: 12px;
+ width: 16px;
+ transform: translateY(-4px);
+ "
+ ></div>
+ <div
+ style="
+ width: 0;
+ height: 0;
+ border-top: 8px solid #f4f4f4;
+ border-left: 8px solid transparent;
+ transform: translateY(-4px) translateX(8px);
+ "
+ ></div>
+ </div>
+ </div>
+ <div
+ style="
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ justify-content: center;
+ margin-bottom: 24px;
+ "
+ >
+ <div
+ style="
+ background-color: #f4f4f4;
+ border-radius: 4px;
+ flex-grow: 0;
+ height: 14px;
+ margin-bottom: 6px;
+ width: 224px;
+ "
+ ></div>
+ <div
+ style="
+ background-color: #f4f4f4;
+ border-radius: 4px;
+ flex-grow: 0;
+ height: 14px;
+ width: 144px;
+ "
+ ></div></div
+ ></a>
+ </div>
+ </blockquote>
+ {{- if not .pc.Simple -}}
+ <script async src="https://www.instagram.com/embed.js"></script>
+ {{- end -}}
+{{- end -}}
--- /dev/null
+{{- errorf `The %q shortcode was removed in v0.123.0. Use the "instagram" shortcode instead. See %s` .Name .Position -}}
--- /dev/null
+{{- $name := (.Get 0) -}}
+{{- with $name -}}
+{{- with ($.Page.Param .) }}{{ . }}{{ else }}{{ errorf "Param %q not found: %s" $name $.Position }}{{ end -}}
+{{- else }}{{ errorf "Missing param key: %s" $.Position }}{{ end -}}
\ No newline at end of file
--- /dev/null
+{{- /*
+Encodes the given text into a QR code using the specified options and renders the resulting image.
+
+@param {string} text The text to encode, falling back to the text between the opening and closing shortcode tags.
+@param {string} [level=medium] The error correction level to use when encoding the text, one of low, medium, quartile, or high.
+@param {int} [scale=4] The number of image pixels per QR code module. Must be greater than or equal to 2.
+@param {string} [targetDir] The subdirectory within the publishDir where Hugo will place the generated image.
+@param {string} [alt] The alt attribute of the img element.
+@param {string} [class] The class attribute of the img element.
+@param {string} [id] The id attribute of the img element.
+@param {string} [title] The title attribute of the img element.
+@param {string} [loading] The loading attribute of the img element, one of lazy or eager.
+
+@returns {template.HTML}
+
+@examples
+
+ {{< qr text="https://gohugo.io" />}}
+
+ {{< qr >}}
+ https://gohugo.io"
+ {{< /qr >}}
+
+ {{< qr
+ text="https://gohugo.io"
+ level="high"
+ scale=4
+ targetDir="codes"
+ alt="QR code linking to https://gohugo.io"
+ class="my-class"
+ id="my-id"
+ title="My Title"
+ />}}
+
+*/}}
+
+{{- /* Constants. */}}
+{{- $validLevels := slice "low" "medium" "quartile" "high" }}
+{{- $minimumScale := 2 }}
+
+{{- /* Get arguments. */}}
+{{- $text := or (.Get "text") (strings.TrimSpace .Inner) "" }}
+{{- $level := or (.Get "level") "medium" }}
+{{- $scale := or (.Get "scale") 4 }}
+{{- $targetDir := or (.Get "targetDir") "" }}
+{{- $alt := or (.Get "alt") "" }}
+{{- $class := or (.Get "class") "" }}
+{{- $id := or (.Get "id") "" }}
+{{- $title := or (.Get "title") "" }}
+{{- $loading := or (.Get "loading") "" }}
+
+{{- /* Validate arguments. */}}
+{{- $errors := false}}
+{{- if not $text }}
+ {{- errorf "The %q shortcode requires a %q argument. See %s" .Name "text" .Position }}
+ {{- $errors = true }}
+{{- end }}
+{{- if not (in $validLevels $level) }}
+ {{- errorf "The %q argument passed to the %q shortcode must be one of %s. See %s" "level" .Name (delimit $validLevels ", " ", or ") .Position }}
+ {{- $errors = true }}
+{{- end }}
+{{- if or (lt $scale $minimumScale) (ne $scale (int $scale)) }}
+ {{- errorf "The %q argument passed to the %q shortcode must be an integer greater than or equal to %d. See %s" "scale" .Name $minimumScale .Position }}
+ {{- $errors = true }}
+{{- end }}
+
+{{- /* Render image. */}}
+{{- if not $errors }}
+ {{- $opts := dict "level" $level "scale" $scale "targetDir" $targetDir }}
+ {{- with images.QR $text $opts -}}
+ <img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}"
+ {{- with $alt }} alt="{{ $alt }}" {{- end }}
+ {{- with $class }} class="{{ $class }}" {{- end }}
+ {{- with $id }} id="{{ $id }}" {{- end }}
+ {{- with $title }} title="{{ $title }}" {{- end -}}
+ {{- with $loading }} loading="{{ $loading }}" {{- end -}}
+ >
+ {{- end }}
+{{- end -}}
--- /dev/null
+{{ ref . .Params }}
\ No newline at end of file
--- /dev/null
+{{ relref . .Params }}
\ No newline at end of file
--- /dev/null
+{{- warnf "The \"twitter\", \"tweet\", and \"twitter_simple\" shortcodes were deprecated in v0.142.0 and will be removed in a future release. Please use the \"x\" shortcode instead." }}
+{{- $pc := site.Config.Privacy.Twitter -}}
+{{- if not $pc.Disable -}}
+ {{- if $pc.Simple -}}
+ {{- template "_internal/shortcodes/twitter_simple.html" . -}}
+ {{- else -}}
+ {{- $id := or (.Get "id") "" -}}
+ {{- $user := or (.Get "user") "" -}}
+ {{- if and $id $user -}}
+ {{- template "render-tweet" (dict "id" $id "user" $user "dnt" $pc.EnableDNT "ctx" .) -}}
+ {{- else -}}
+ {{- errorf "The %q shortcode requires two named parameters: user and id. See %s" .Name .Position -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "render-tweet" -}}
+ {{- $url := printf "https://twitter.com/%v/status/%v" .user .id -}}
+ {{- $query := querify "url" $url "dnt" .dnt -}}
+ {{- $request := printf "https://publish.twitter.com/oembed?%s" $query -}}
+ {{- with try (resources.GetRemote $request) -}}
+ {{- with .Err -}}
+ {{- errorf "%s" . -}}
+ {{- else with .Value -}}
+ {{- (. | transform.Unmarshal).html | safeHTML -}}
+ {{- else -}}
+ {{- warnidf "shortcode-twitter-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
--- /dev/null
+{{- warnf "The \"twitter\", \"tweet\", and \"twitter_simple\" shortcodes were deprecated in v0.142.0 and will be removed in a future release. Please use the \"x\" shortcode instead." }}
+{{- if not site.Config.Privacy.Twitter.Disable -}}
+ {{- $id := or (.Get "id") "" -}}
+ {{- $user := or (.Get "user") "" -}}
+ {{- if and $id $user -}}
+ {{- template "render-simple-tweet" (dict "id" $id "user" $user "ctx" .) -}}
+ {{- else -}}
+ {{- errorf "The %q shortcode requires two named parameters: user and id. See %s" .Name .Position -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "render-simple-tweet" -}}
+ {{- $dnt := site.Config.Privacy.Twitter.EnableDNT }}
+ {{- $url := printf "https://twitter.com/%v/status/%v" .user .id -}}
+ {{- $query := querify "url" $url "dnt" $dnt "omit_script" true -}}
+ {{- $request := printf "https://publish.twitter.com/oembed?%s" $query -}}
+ {{- with try (resources.GetRemote $request) -}}
+ {{- with .Err -}}
+ {{- errorf "%s" . -}}
+ {{- else with .Value -}}
+ {{- if not site.Config.Services.Twitter.DisableInlineCSS }}
+ {{- template "__h_simple_twitter_css" (dict "ctx" $.ctx) }}
+ {{- end }}
+ {{- (. | transform.Unmarshal).html | safeHTML -}}
+ {{- else -}}
+ {{- warnidf "shortcode-twitter-simple-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "__h_simple_twitter_css" -}}
+ {{- if not (.ctx.Page.Store.Get "__h_simple_twitter_css") -}}
+ {{/* Only include once */}}
+ {{- .ctx.Page.Store.Set "__h_simple_twitter_css" true -}}
+ <style type="text/css">
+ .twitter-tweet {
+ font:
+ 14px/1.45 -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ Roboto,
+ Oxygen-Sans,
+ Ubuntu,
+ Cantarell,
+ "Helvetica Neue",
+ sans-serif;
+ border-left: 4px solid #2b7bb9;
+ padding-left: 1.5em;
+ color: #555;
+ }
+ .twitter-tweet a {
+ color: #2b7bb9;
+ text-decoration: none;
+ }
+ blockquote.twitter-tweet a:hover,
+ blockquote.twitter-tweet a:focus {
+ text-decoration: underline;
+ }
+ </style>
+ {{- end -}}
+{{- end -}}
--- /dev/null
+{{- /*
+Renders an embedded Vimeo video.
+
+Accepts named or positional arguments. If positional, order is id, class,
+title, then loading.
+
+@param {string} [id] The video id. Optional if the id is provided as first positional argument.
+@param {string} [class] The class attribute of the wrapping div element. When specified, removes the style attributes from the iframe element and its wrapping div element.
+@param {string} [loading=eager] The loading attribute of the iframe element.
+@param {string} [title=Vimeo video] The title attribute of the iframe element.
+@param {bool} [allowFullScreen=true] Whether the iframe element can activate full screen mode.
+
+@returns {template.HTML}
+
+@example {{< vimeo 55073825 >}}
+@example {{< vimeo id=55073825 class="foo bar" loading=lazy title="My Video" >}}
+*/}}
+{{- $pc := site.Config.Privacy.Vimeo }}
+{{- if not $pc.Disable }}
+ {{- if $pc.Simple }}
+ {{- template "_internal/shortcodes/vimeo_simple.html" . }}
+ {{- else }}
+ {{- $dnt := cond $pc.EnableDNT 1 0 }}
+
+ {{- $id := or (.Get "id") (.Get 0) "" }}
+ {{- $class := or (.Get "class") (.Get 1) "" }}
+ {{- $title := or (.Get "title") (.Get 2) "Vimeo video" }}
+ {{- $loading := or (.Get "loading") (.Get 3) "eager" }}
+ {{- $allowFullScreen := or (.Get "allowFullScreen") (.Get 4) true }}
+
+ {{- if in (slice "false" false 0) ($.Get "allowFullScreen") }}
+ {{- $allowFullScreen = false }}
+ {{- else if in (slice "true" true 1) ($.Get "allowFullScreen") }}
+ {{- $allowFullScreen = true }}
+ {{- end }}
+
+ {{- $iframeAllowList := "" }}
+ {{- if $allowFullScreen }}
+ {{- $iframeAllowList = "fullscreen" }}
+ {{- end }}
+
+ {{- $divStyle := "position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;" }}
+ {{- $iframeStyle := "position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" }}
+
+ {{- with $id }}
+ {{- $src := printf "https://player.vimeo.com/video/%v?dnt=%v" . $dnt }}
+ <div
+ {{- with $class }}
+ class="{{ . }}"
+ {{- else }}
+ style="{{ $divStyle | safeCSS }}"
+ {{- end }}>
+ <iframe
+ src="{{- $src }}"
+ {{- if not $class }}
+ style="{{ $iframeStyle | safeCSS }}"
+ {{- end }}
+ {{- with $iframeAllowList }} allow="{{ . }}" {{- end }}
+ {{- with $loading }} loading="{{ . }}" {{- end }}
+ {{- with $title }} title="{{ . }}" {{- end }}>
+ </iframe>
+ </div>
+ {{- else }}
+ {{- errorf "The %q shortcode requires a video id, either as the first positional argument or an argument named id. See %s" .Name .Position }}
+ {{- end }}
+ {{- end }}
+{{- end }}
--- /dev/null
+{{- $pc := site.Config.Privacy.Vimeo -}}
+{{- if not $pc.Disable -}}
+ {{- $ctx := dict "ctx" . }}
+ {{- if .IsNamedParams -}}
+ {{- with .Get "id" -}}
+ {{- $ctx = merge $ctx (dict "id" . "class" ($.Get "class")) -}}
+ {{- template "render-vimeo" $ctx -}}
+ {{- else -}}
+ {{- errorf "The %q shortocde requires a single named parameter, the ID of the Vimeo video. See %s" .Name .Position -}}
+ {{- end -}}
+ {{- else -}}
+ {{- with .Get 0 -}}
+ {{- $ctx = merge $ctx (dict "id" . "class" ($.Get 1)) -}}
+ {{- template "render-vimeo" $ctx -}}
+ {{- else -}}
+ {{- errorf "The %q shortocde requires a single positional parameter, the ID of the Vimeo video. See %s" .Name .Position -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "render-vimeo" -}}
+ {{- $dnt := cond site.Config.Privacy.Vimeo.EnableDNT 1 0 -}}
+ {{- $url := urls.JoinPath "https://vimeo.com" .id -}}
+ {{- $query := querify "url" $url "dnt" $dnt -}}
+ {{- $request := printf "https://vimeo.com/api/oembed.json?%s" $query -}}
+ {{- with try (resources.GetRemote $request) -}}
+ {{- with .Err -}}
+ {{- errorf "%s" . -}}
+ {{- else with .Value -}}
+ {{- with . | transform.Unmarshal -}}
+ {{- $class := printf "%s %s" "s_video_simple" "__h_video" -}}
+ {{- with $.class -}}
+ {{- $class = printf "%s %s" "s_video_simple" . -}}
+ {{- else -}}
+ {{ template "__h_simple_css" $.ctx.Page }}
+ {{- end -}}
+ {{- $thumbnail := .thumbnail_url -}}
+ {{- $original := $thumbnail | replaceRE "(_.*\\.)" "." -}}
+ <div class="{{ $class }}">
+ <a href="{{ .provider_url }}{{ .video_id }}" rel="noopener" target="_blank">
+ <img src="{{ $thumbnail }}" srcset="{{ $thumbnail }} 1x, {{ $original }} 2x" alt="{{ .title }}">
+ <div class="play">
+ {{ template "__h_simple_icon_play" $.ctx.Page }}
+ </div>
+ </a>
+ </div>
+ {{- end -}}
+ {{- else -}}
+ {{- warnidf "shortcode-vimeo-simple" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
--- /dev/null
+{{- $pc := site.Config.Privacy.X -}}
+{{- if not $pc.Disable -}}
+ {{- if $pc.Simple -}}
+ {{- template "_internal/shortcodes/x_simple.html" . -}}
+ {{- else -}}
+ {{- $id := or (.Get "id") "" -}}
+ {{- $user := or (.Get "user") "" -}}
+ {{- if and $id $user -}}
+ {{- template "render-x" (dict "id" $id "user" $user "dnt" $pc.EnableDNT "ctx" .) -}}
+ {{- else -}}
+ {{- errorf "The %q shortcode requires two named parameters: user and id. See %s" .Name .Position -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "render-x" -}}
+ {{- $url := printf "https://x.com/%v/status/%v" .user .id -}}
+ {{- $query := querify "url" $url "dnt" .dnt -}}
+ {{- $request := printf "https://publish.x.com/oembed?%s" $query -}}
+ {{- with try (resources.GetRemote $request) -}}
+ {{- with .Err -}}
+ {{- errorf "%s" . -}}
+ {{- else with .Value -}}
+ {{- (. | transform.Unmarshal).html | safeHTML -}}
+ {{- else -}}
+ {{- warnidf "shortcode-x-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
--- /dev/null
+{{- if not site.Config.Privacy.X.Disable -}}
+ {{- $id := or (.Get "id") "" -}}
+ {{- $user := or (.Get "user") "" -}}
+ {{- if and $id $user -}}
+ {{- template "render-simple-x" (dict "id" $id "user" $user "ctx" .) -}}
+ {{- else -}}
+ {{- errorf "The %q shortcode requires two named parameters: user and id. See %s" .Name .Position -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "render-simple-x" -}}
+ {{- $dnt := site.Config.Privacy.X.EnableDNT }}
+ {{- $url := printf "https://x.com/%v/status/%v" .user .id -}}
+ {{- $query := querify "url" $url "dnt" $dnt "omit_script" true -}}
+ {{- $request := printf "https://publish.x.com/oembed?%s" $query -}}
+ {{- with try (resources.GetRemote $request) -}}
+ {{- with .Err -}}
+ {{- errorf "%s" . -}}
+ {{- else with .Value -}}
+ {{- if not site.Config.Services.X.DisableInlineCSS }}
+ {{- template "__h_simple_x_css" (dict "ctx" $.ctx) }}
+ {{- end }}
+ {{- (. | transform.Unmarshal).html | safeHTML -}}
+ {{- else -}}
+ {{- warnidf "shortcode-x-simple-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "__h_simple_x_css" -}}
+ {{- if not (.ctx.Page.Store.Get "__h_simple_x_css") -}}
+ {{/* Only include once */}}
+ {{- .ctx.Page.Store.Set "__h_simple_x_css" true -}}
+ <style type="text/css">
+ .twitter-tweet {
+ font:
+ 14px/1.45 -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ Roboto,
+ Oxygen-Sans,
+ Ubuntu,
+ Cantarell,
+ "Helvetica Neue",
+ sans-serif;
+ border-left: 4px solid #2b7bb9;
+ padding-left: 1.5em;
+ color: #555;
+ }
+ .twitter-tweet a {
+ color: #2b7bb9;
+ text-decoration: none;
+ }
+ blockquote.twitter-tweet a:hover,
+ blockquote.twitter-tweet a:focus {
+ text-decoration: underline;
+ }
+ </style>
+ {{- end -}}
+{{- end -}}
--- /dev/null
+{{- /*
+Renders an embedded YouTube video.
+
+@param {bool} [allowFullScreen=true] Whether the iframe element can activate full screen mode.
+@param {bool} [autoplay=false] Whether to automatically play the video. Forces mute to be true.
+@param {string} [class] The class attribute of the wrapping div element. When specified, removes the style attributes from the iframe element and its wrapping div element.
+@param {bool} [controls=true] Whether to display the video controls.
+@param {int} [end] The time, measured in seconds from the start of the video, when the player should stop playing the video.
+@param {string} [id] The video id. Optional if the id is provided as first positional argument.
+@param {string} [loading=eager] The loading attribute of the iframe element.
+@param {bool} [loop=false] Whether to indefinitely repeat the video. Ignores the start and end arguments after the first play.
+@param {bool} [mute=false] Whether to mute the video. Always true when autoplay is true.
+@param {int} [start] The time, measured in seconds from the start of the video, when the player should start playing the video.
+@param {string} [title] The title attribute of the iframe element. Defaults to "YouTube video".
+
+@returns {template.HTML}
+
+@reference https://developers.google.com/youtube/player_parameters
+
+@example {{< youtube 0RKpf3rK57I >}}
+@example {{< youtube id=0RKpf3rK57I loading=lazy start=30 >}}
+*/}}
+
+{{- $pc := .Page.Site.Config.Privacy.YouTube }}
+{{- $remoteErrID := "err-youtube-remote" }}
+{{- if not $pc.Disable }}
+ {{- with $id := or (.Get "id") (.Get 0) }}
+
+ {{- /* Set defaults. */}}
+ {{- $allowFullScreen := true }}
+ {{- $autoplay := 0 }}
+ {{- $class := "" }}
+ {{- $controls := 1 }}
+ {{- $end := 0 }}
+ {{- $loading := "eager" }}
+ {{- $loop := 0 }}
+ {{- $mute := 0 }}
+ {{- $start := 0 }}
+ {{- $title := "YouTube video" }}
+ {{- $iframeAllowList := "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" }}
+
+ {{- /* Get arguments. */}}
+ {{- if in (slice "true" true 1) ($.Get "allowFullScreen") }}
+ {{- $iframeAllowList = printf "%s; fullscreen" $iframeAllowList }}
+ {{- end }}
+ {{- if in (slice "false" false 0) ($.Get "autoplay") }}
+ {{- $autoplay = 0 }}
+ {{- else if in (slice "true" true 1) ($.Get "autoplay") }}
+ {{- $autoplay = 1 }}
+ {{- end }}
+ {{- if in (slice "false" false 0) ($.Get "controls") }}
+ {{- $controls = 0 }}
+ {{- else if in (slice "true" true 1) ($.Get "controls") }}
+ {{- $controls = 1 }}
+ {{- end }}
+ {{- if in (slice "false" false 0) ($.Get "loop") }}
+ {{- $loop = 0 }}
+ {{- else if in (slice "true" true 1) ($.Get "loop") }}
+ {{- $loop = 1 }}
+ {{- end }}
+ {{- if in (slice "false" false 0) ($.Get "mute") }}
+ {{- $mute = 0 }}
+ {{- else if or (in (slice "true" true 1) ($.Get "mute")) $autoplay }}
+ {{- $mute = 1 }}
+ {{- end }}
+ {{- $class := or ($.Get "class") $class }}
+ {{- $end := or ($.Get "end") $end }}
+ {{- $loading := or ($.Get "loading") $loading }}
+ {{- $start := or ($.Get "start") $start }}
+ {{- $title := or ($.Get "title") $title }}
+
+ {{- /* Define src attribute. */}}
+ {{- $host := cond $pc.PrivacyEnhanced "www.youtube-nocookie.com" "www.youtube.com" }}
+ {{- $src := printf "https://%s/embed/%s" $host $id }}
+ {{- $params := dict
+ "autoplay" $autoplay
+ "controls" $controls
+ "end" $end
+ "mute" $mute
+ "start" $start
+ "loop" $loop
+ }}
+ {{- if $loop }}
+ {{- $params = merge $params (dict "playlist" $id) }}
+ {{- end }}
+ {{- with querify $params }}
+ {{- $src = printf "%s?%s" $src . }}
+ {{- end }}
+
+ {{- /* Set div attributes. */}}
+ {{- $divStyle := "position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;" }}
+ {{- if $class }}
+ {{- $divStyle = "" }}
+ {{- end }}
+
+ {{- /* Set iframe attributes. */}}
+ {{- $iframeStyle := "position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" }}
+ {{- if $class }}
+ {{- $iframeStyle = "" }}
+ {{- end }}
+ {{- $referrerpolicy := "strict-origin-when-cross-origin" }}
+
+ {{- /* Render. */ -}}
+ <div
+ {{- with $class }} class="{{ . }}" {{- end }}
+ {{- with $divStyle }} style="{{ . | safeCSS }}" {{- end -}}
+ >
+ <iframe
+ {{- with $iframeAllowList }} allow="{{ . }}" {{- end }}
+ {{- with $loading }} loading="{{ . }}" {{- end }}
+ {{- with $referrerpolicy }} referrerpolicy="{{ . }}" {{- end }}
+ {{- with $src }} src="{{ . }}" {{- end }}
+ {{- with $iframeStyle}} style="{{ . | safeCSS }}" {{- end }}
+ {{- with $title }} title="{{ . }}" {{- end -}}
+ ></iframe>
+ </div>
+ {{- else }}
+ {{- errorf "The %q shortcode requires an id argument. See %s" .Name .Position }}
+ {{- end }}
+{{- end }}
+++ /dev/null
-{{- $pc := .Site.Config.Privacy.Disqus -}}
-{{- if not $pc.Disable -}}
-{{ if .Site.Config.Services.Disqus.Shortname }}<div id="disqus_thread"></div>
-<script>
- window.disqus_config = function () {
- {{with .Params.disqus_identifier }}this.page.identifier = '{{ . }}';{{end}}
- {{with .Params.disqus_title }}this.page.title = '{{ . }}';{{end}}
- {{with .Params.disqus_url }}this.page.url = '{{ . | html }}';{{end}}
- };
- (function() {
- if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) {
- document.getElementById('disqus_thread').innerHTML = 'Disqus comments not available by default when the website is previewed locally.';
- return;
- }
- var d = document, s = d.createElement('script'); s.async = true;
- s.src = '//' + {{ .Site.Config.Services.Disqus.Shortname }} + '.disqus.com/embed.js';
- s.setAttribute('data-timestamp', +new Date());
- (d.head || d.body).appendChild(s);
- })();
-</script>
-<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
-<a href="https://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>{{end}}
-{{- end -}}
+++ /dev/null
-{{ if not site.Config.Privacy.GoogleAnalytics.Disable }}
- {{- with site.Config.Services.GoogleAnalytics.ID }}
- {{- if strings.HasPrefix (lower .) "ua-" }}
- {{- warnf "Google Analytics 4 (GA4) replaced Google Universal Analytics (UA) effective 1 July 2023. See https://support.google.com/analytics/answer/11583528. Create a GA4 property and data stream, then replace the Google Analytics ID in your site configuration with the new value." }}
- {{- else }}
- <script async src="https://www.googletagmanager.com/gtag/js?id={{ . }}"></script>
- <script>
- var doNotTrack = false;
- if ({{ site.Config.Privacy.GoogleAnalytics.RespectDoNotTrack }}) {
- var dnt = (navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack);
- var doNotTrack = (dnt == "1" || dnt == "yes");
- }
- if (!doNotTrack) {
- window.dataLayer = window.dataLayer || [];
- function gtag(){dataLayer.push(arguments);}
- gtag('js', new Date());
- gtag('config', '{{ . }}');
- }
- </script>
- {{- end }}
- {{- end }}
-{{- end -}}
+++ /dev/null
-<meta property="og:url" content="{{ .Permalink }}">
-
-{{- with or site.Title site.Params.title | plainify }}
- <meta property="og:site_name" content="{{ . }}">
-{{- end }}
-
-{{- with or .Title site.Title site.Params.title | plainify }}
- <meta property="og:title" content="{{ . }}">
-{{- end }}
-
-{{- with or .Description .Summary site.Params.description | plainify | htmlUnescape }}
- <meta property="og:description" content="{{ trim . "\n\r\t " }}">
-{{- end }}
-
-{{- with or .Params.locale site.Language.LanguageCode }}
- <meta property="og:locale" content="{{ replace . `-` `_` }}">
-{{- end }}
-
-{{- if .IsPage }}
- <meta property="og:type" content="article">
- {{- with .Section }}
- <meta property="article:section" content="{{ . }}">
- {{- end }}
- {{- $ISO8601 := "2006-01-02T15:04:05-07:00" }}
- {{- with .PublishDate }}
- <meta property="article:published_time" {{ .Format $ISO8601 | printf "content=%q" | safeHTMLAttr }}>
- {{- end }}
- {{- with .Lastmod }}
- <meta property="article:modified_time" {{ .Format $ISO8601 | printf "content=%q" | safeHTMLAttr }}>
- {{- end }}
- {{- range .GetTerms "tags" | first 6 }}
- <meta property="article:tag" content="{{ .Page.Title | plainify }}">
- {{- end }}
-{{- else }}
- <meta property="og:type" content="website">
-{{- end }}
-
-{{- with partial "_funcs/get-page-images" . }}
- {{- range . | first 6 }}
- <meta property="og:image" content="{{ .Permalink }}">
- {{- end }}
-{{- end }}
-
-{{- with .Params.audio }}
- {{- range . | first 6 }}
- <meta property="og:audio" content="{{ . | absURL }}">
- {{- end }}
-{{- end }}
-
-{{- with .Params.videos }}
- {{- range . | first 6 }}
- <meta property="og:video" content="{{ . | absURL }}">
- {{- end }}
-{{- end }}
-
-{{- range .GetTerms "series" }}
- {{- range .Pages | first 7 }}
- {{- if ne $ . }}
- <meta property="og:see_also" content="{{ .Permalink }}">
- {{- end }}
- {{- end }}
-{{- end }}
-
-{{- with site.Params.social }}
- {{- if reflect.IsMap . }}
- {{- with .facebook_app_id }}
- <meta property="fb:app_id" content="{{ . }}">
- {{- else }}
- {{- with .facebook_admin }}
- <meta property="fb:admins" content="{{ . }}">
- {{- end }}
- {{- end }}
- {{- end }}
-{{- end }}
+++ /dev/null
-{{- $validFormats := slice "default" "terse" }}
-
-{{- $msg1 := "When passing a map to the internal pagination template, one of the elements must be named 'page', and it must be set to the context of the current page." }}
-{{- $msg2 := "The 'format' specified in the map passed to the internal pagination template is invalid. Valid choices are: %s." }}
-
-{{- $page := . }}
-{{- $format := "default" }}
-
-{{- if reflect.IsMap . }}
- {{- with .page }}
- {{- $page = . }}
- {{- else }}
- {{- errorf $msg1 }}
- {{- end }}
- {{- with .format }}
- {{- $format = lower . }}
- {{- end }}
-{{- end }}
-
-{{- if in $validFormats $format }}
- {{- if gt $page.Paginator.TotalPages 1 }}
- <ul class="pagination pagination-{{ $format }}">
- {{- partial (printf "partials/inline/pagination/%s" $format) $page }}
- </ul>
- {{- end }}
-{{- else }}
- {{- errorf $msg2 (delimit $validFormats ", ") }}
-{{- end -}}
-
-{{/* Format: default
-{{/* --------------------------------------------------------------------- */}}
-{{- define "partials/inline/pagination/default" }}
- {{- with .Paginator }}
- {{- $currentPageNumber := .PageNumber }}
-
- {{- with .First }}
- {{- if ne $currentPageNumber .PageNumber }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="First" class="page-link" role="button"><span aria-hidden="true">««</span></a>
- </li>
- {{- else }}
- <li class="page-item disabled">
- <a aria-disabled="true" aria-label="First" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">««</span></a>
- </li>
- {{- end }}
- {{- end }}
-
- {{- with .Prev }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="Previous" class="page-link" role="button"><span aria-hidden="true">«</span></a>
- </li>
- {{- else }}
- <li class="page-item disabled">
- <a aria-disabled="true" aria-label="Previous" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">«</span></a>
- </li>
- {{- end }}
-
- {{- $slots := 5 }}
- {{- $start := math.Max 1 (sub .PageNumber (math.Floor (div $slots 2))) }}
- {{- $end := math.Min .TotalPages (sub (add $start $slots) 1) }}
- {{- if lt (add (sub $end $start) 1) $slots }}
- {{- $start = math.Max 1 (add (sub $end $slots) 1) }}
- {{- end }}
-
- {{- range $k := seq $start $end }}
- {{- if eq $.Paginator.PageNumber $k }}
- <li class="page-item active">
- <a aria-current="page" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
- </li>
- {{- else }}
- <li class="page-item">
- <a href="{{ (index $.Paginator.Pagers (sub $k 1)).URL }}" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
- </li>
- {{- end }}
- {{- end }}
-
- {{- with .Next }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="Next" class="page-link" role="button"><span aria-hidden="true">»</span></a>
- </li>
- {{- else }}
- <li class="page-item disabled">
- <a aria-disabled="true" aria-label="Next" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">»</span></a>
- </li>
- {{- end }}
-
- {{- with .Last }}
- {{- if ne $currentPageNumber .PageNumber }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="Last" class="page-link" role="button"><span aria-hidden="true">»»</span></a>
- </li>
- {{- else }}
- <li class="page-item disabled">
- <a aria-disabled="true" aria-label="Last" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">»»</span></a>
- </li>
- {{- end }}
- {{- end }}
- {{- end }}
-{{- end -}}
-
-{{/* Format: terse
-{{/* --------------------------------------------------------------------- */}}
-{{- define "partials/inline/pagination/terse" }}
- {{- with .Paginator }}
- {{- $currentPageNumber := .PageNumber }}
-
- {{- with .First }}
- {{- if ne $currentPageNumber .PageNumber }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="First" class="page-link" role="button"><span aria-hidden="true">««</span></a>
- </li>
- {{- end }}
- {{- end }}
-
- {{- with .Prev }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="Previous" class="page-link" role="button"><span aria-hidden="true">«</span></a>
- </li>
- {{- end }}
-
- {{- $slots := 3 }}
- {{- $start := math.Max 1 (sub .PageNumber (math.Floor (div $slots 2))) }}
- {{- $end := math.Min .TotalPages (sub (add $start $slots) 1) }}
- {{- if lt (add (sub $end $start) 1) $slots }}
- {{- $start = math.Max 1 (add (sub $end $slots) 1) }}
- {{- end }}
-
- {{- range $k := seq $start $end }}
- {{- if eq $.Paginator.PageNumber $k }}
- <li class="page-item active">
- <a aria-current="page" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
- </li>
- {{- else }}
- <li class="page-item">
- <a href="{{ (index $.Paginator.Pagers (sub $k 1)).URL }}" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
- </li>
- {{- end }}
- {{- end }}
-
- {{- with .Next }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="Next" class="page-link" role="button"><span aria-hidden="true">»</span></a>
- </li>
- {{- end }}
-
- {{- with .Last }}
- {{- if ne $currentPageNumber .PageNumber }}
- <li class="page-item">
- <a href="{{ .URL }}" aria-label="Last" class="page-link" role="button"><span aria-hidden="true">»»</span></a>
- </li>
- {{- end }}
- {{- end }}
- {{- end }}
-{{- end -}}
+++ /dev/null
-{{- $imgs := slice }}
-{{- $imgParams := .Params.images }}
-{{- $resources := .Resources.ByType "image" -}}
-{{/* Find featured image resources if the images parameter is empty. */}}
-{{- if not $imgParams }}
- {{- $featured := $resources.GetMatch "*feature*" -}}
- {{- if not $featured }}{{ $featured = $resources.GetMatch "{*cover*,*thumbnail*}" }}{{ end -}}
- {{- with $featured }}
- {{- $imgs = $imgs | append (dict
- "Image" .
- "RelPermalink" .RelPermalink
- "Permalink" .Permalink) }}
- {{- end }}
-{{- end }}
-{{/* Use the first one of site images as the fallback. */}}
-{{- if and (not $imgParams) (not $imgs) }}
- {{- with site.Params.images }}
- {{- $imgParams = first 1 . }}
- {{- end }}
-{{- end }}
-{{/* Parse page's images parameter. */}}
-{{- range $imgParams }}
- {{- $img := . }}
- {{- $url := urls.Parse $img }}
- {{- if eq $url.Scheme "" }}
- {{/* Internal image. */}}
- {{- with $resources.GetMatch $img -}}
- {{/* Image resource. */}}
- {{- $imgs = $imgs | append (dict
- "Image" .
- "RelPermalink" .RelPermalink
- "Permalink" .Permalink) }}
- {{- else }}
- {{- $imgs = $imgs | append (dict
- "RelPermalink" (relURL $img)
- "Permalink" (absURL $img)
- ) }}
- {{- end }}
- {{- else }}
- {{/* External image */}}
- {{- $imgs = $imgs | append (dict
- "RelPermalink" $img
- "Permalink" $img
- ) }}
- {{- end }}
-{{- end }}
-{{- return $imgs }}
--- /dev/null
+User-agent: *
--- /dev/null
+{{- $authorEmail := "" }}
+{{- with site.Params.author }}
+ {{- if reflect.IsMap . }}
+ {{- with .email }}
+ {{- $authorEmail = . }}
+ {{- end }}
+ {{- end }}
+{{- end }}
+
+{{- $authorName := "" }}
+{{- with site.Params.author }}
+ {{- if reflect.IsMap . }}
+ {{- with .name }}
+ {{- $authorName = . }}
+ {{- end }}
+ {{- else }}
+ {{- $authorName = . }}
+ {{- end }}
+{{- end }}
+
+{{- $pctx := . }}
+{{- if .IsHome }}{{ $pctx = .Site }}{{ end }}
+{{- $pages := slice }}
+{{- if or $.IsHome $.IsSection }}
+{{- $pages = $pctx.RegularPages }}
+{{- else }}
+{{- $pages = $pctx.Pages }}
+{{- end }}
+{{- $limit := .Site.Config.Services.RSS.Limit }}
+{{- if ge $limit 1 }}
+{{- $pages = $pages | first $limit }}
+{{- end }}
+{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
+<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
+ <channel>
+ <title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{ . }} on {{ end }}{{ .Site.Title }}{{ end }}</title>
+ <link>{{ .Permalink }}</link>
+ <description>Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{ . }} {{ end }}{{ end }}on {{ .Site.Title }}</description>
+ <generator>Hugo</generator>
+ <language>{{ site.Language.LanguageCode }}</language>{{ with $authorEmail }}
+ <managingEditor>{{.}}{{ with $authorName }} ({{ . }}){{ end }}</managingEditor>{{ end }}{{ with $authorEmail }}
+ <webMaster>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</webMaster>{{ end }}{{ with .Site.Copyright }}
+ <copyright>{{ . }}</copyright>{{ end }}{{ if not .Date.IsZero }}
+ <lastBuildDate>{{ (index $pages.ByLastmod.Reverse 0).Lastmod.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
+ {{- with .OutputFormats.Get "RSS" }}
+ {{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
+ {{- end }}
+ {{- range $pages }}
+ <item>
+ <title>{{ .Title }}</title>
+ <link>{{ .Permalink }}</link>
+ <pubDate>{{ .PublishDate.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
+ {{- with $authorEmail }}<author>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</author>{{ end }}
+ <guid>{{ .Permalink }}</guid>
+ <description>{{ .Summary | transform.XMLEscape | safeHTML }}</description>
+ </item>
+ {{- end }}
+ </channel>
+</rss>
+++ /dev/null
-{{- with or .Title site.Title | plainify }}
- <meta itemprop="name" content="{{ . }}">
-{{- end }}
-
-{{- with or .Description .Summary site.Params.description | plainify | htmlUnescape }}
- <meta itemprop="description" content="{{ trim . "\n\r\t " }}">
-{{- end }}
-
-{{- $ISO8601 := "2006-01-02T15:04:05-07:00" }}
-{{- with .PublishDate }}
- <meta itemprop="datePublished" {{ .Format $ISO8601 | printf "content=%q" | safeHTMLAttr }}>
-{{- end }}
-
-{{- with .Lastmod }}
- <meta itemprop="dateModified" {{ .Format $ISO8601 | printf "content=%q" | safeHTMLAttr }}>
-{{- end }}
-
-{{- with .WordCount }}
- <meta itemprop="wordCount" content="{{ . }}">
-{{- end }}
-
-{{- $images := partial "_funcs/get-page-images" . }}
-{{- range first 6 $images }}
- <meta itemprop="image" content="{{ .Permalink }}">
-{{- end }}
-
-{{- /*
-Keywords precedence:
-
-1. Use "keywords" term page titles.
-2. Use "keywords" from front matter if "keywords" is not a taxonomy.
-3. Use "tags" term page titles.
-4. Use term page titles from all taxonomies.
-
-*/}}
-{{- $keywords := slice }}
-{{- range .GetTerms "keywords" }}
- {{- $keywords = $keywords | append .Title }}
-{{- else }}
- {{- with .Keywords }}
- {{- $keywords = . }}
- {{- else }}
- {{- range .GetTerms "tags" }}
- {{- $keywords = $keywords | append .Title }}
- {{- else }}
- {{- range $taxonomy, $_ := site.Taxonomies }}
- {{- range $.GetTerms $taxonomy }}
- {{- $keywords = $keywords | append .Title }}
- {{- end }}
- {{- end }}
- {{- end }}
- {{- end }}
-{{- end }}
-{{- with $keywords }}
- <meta itemprop="keywords" content="{{ delimit . `,` }}">
-{{- end -}}
+++ /dev/null
-{{ define "__h_simple_css" }}{{/* These template definitions are global. */}}
-{{- if not (.Page.Scratch.Get "__h_simple_css") -}}
-{{/* Only include once */}}
-{{- .Page.Scratch.Set "__h_simple_css" true -}}
-<style>
-.__h_video {
- position: relative;
- padding-bottom: 56.23%;
- height: 0;
- overflow: hidden;
- width: 100%;
- background: #000;
-}
-.__h_video img {
- width: 100%;
- height: auto;
- color: #000;
-}
-.__h_video .play {
- height: 72px;
- width: 72px;
- left: 50%;
- top: 50%;
- margin-left: -36px;
- margin-top: -36px;
- position: absolute;
- cursor: pointer;
-}
-</style>
-{{- end -}}
-{{- end -}}
-{{- define "__h_simple_icon_play" -}}
-<svg version="1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 61 61"><circle cx="30.5" cy="30.5" r="30.5" opacity=".8" fill="#000"></circle><path d="M25.3 19.2c-2.1-1.2-3.8-.2-3.8 2.2v18.1c0 2.4 1.7 3.4 3.8 2.2l16.6-9.1c2.1-1.2 2.1-3.2 0-4.4l-16.6-9z" fill="#fff"></path></svg>
-{{- end -}}
+++ /dev/null
-{{- warnf "The %q shortcode was deprecated in v0.143.0 and will be removed in a future release. Please use HTML comments instead. See %s" .Name .Position -}}
-{{- $noop := .Inner -}}
+++ /dev/null
-{{- /*
-Renders an HTML details element.
-
-@param {string} [class] The value of the element's class attribute.
-@param {string} [name] The value of the element's name attribute.
-@param {string} [summary] The content of the child summary element.
-@param {string} [title] The value of the element's title attribute.
-@param {bool} [open=false] Whether to initially display the content of the details element.
-
-@reference https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details
-
-@examples
-
- {{< details >}}
- A basic collapsible section.
- {{< /details >}}
-
- {{< details summary="Custom Summary Text" >}}
- Showing custom `summary` text.
- {{< /details >}}
-
- {{< details summary="Open Details" open=true >}}
- Contents displayed initially by using `open`.
- {{< /details >}}
-
- {{< details summary="Styled Content" class="my-custom-class" >}}
- Content can be styled with CSS by specifying a `class`.
-
- Target details element:
-
- ```css
- details.my-custom-class { }
- ```
-
- Target summary element:
-
- ```css
- details.my-custom-class > summary > * { }
- ```
-
- Target inner content:
-
- ```css
- details.my-custom-class > :not(summary) { }
- ```
- {{< /details >}}
-
- {{< details summary="Grouped Details" name="my-details" >}}
- Specifying a `name` allows elements to be connected, with only one able to be open at a time.
- {{< /details >}}
-
-*/}}
-
-{{- /* Get arguments. */}}
-{{- $class := or (.Get "class") "" }}
-{{- $name := or (.Get "name") "" }}
-{{- $summary := or (.Get "summary") "Details" }}
-{{- $title := or (.Get "title") "" }}
-{{- $open := false }}
-{{- if in (slice "false" false 0) (.Get "open") }}
- {{- $open = false }}
-{{- else if in (slice "true" true 1) (.Get "open") }}
- {{- $open = true }}
-{{- end }}
-
-{{- /* Render. */}}
-<details
- {{- with $class }} class="{{ . }}" {{- end }}
- {{- with $name }} name="{{ . }}" {{- end }}
- {{- with $open }} open {{- end }}
- {{- with $title }} title="{{ . }}" {{- end -}}
->
- <summary>{{ $summary | .Page.RenderString }}</summary>
- {{ .Inner | .Page.RenderString (dict "display" "block") -}}
-</details>
+++ /dev/null
-<figure{{ with .Get "class" }} class="{{ . }}"{{ end }}>
- {{- if .Get "link" -}}
- <a href="{{ .Get "link" }}"{{ with .Get "target" }} target="{{ . }}"{{ end }}{{ with .Get "rel" }} rel="{{ . }}"{{ end }}>
- {{- end -}}
-
- {{- $u := urls.Parse (.Get "src") -}}
- {{- $src := $u.String -}}
- {{- if not $u.IsAbs -}}
- {{- with or (.Page.Resources.Get $u.Path) (resources.Get $u.Path) -}}
- {{- $src = .RelPermalink -}}
- {{- end -}}
- {{- end -}}
-
- <img src="{{ $src }}"
- {{- if or (.Get "alt") (.Get "caption") }}
- alt="{{ with .Get "alt" }}{{ . }}{{ else }}{{ .Get "caption" | markdownify| plainify }}{{ end }}"
- {{- end -}}
- {{- with .Get "width" }} width="{{ . }}"{{ end -}}
- {{- with .Get "height" }} height="{{ . }}"{{ end -}}
- {{- with .Get "loading" }} loading="{{ . }}"{{ end -}}
- ><!-- Closing img tag -->
- {{- if .Get "link" }}</a>{{ end -}}
- {{- if or (or (.Get "title") (.Get "caption")) (.Get "attr") -}}
- <figcaption>
- {{ with (.Get "title") -}}
- <h4>{{ . }}</h4>
- {{- end -}}
- {{- if or (.Get "caption") (.Get "attr") -}}<p>
- {{- .Get "caption" | markdownify -}}
- {{- with .Get "attrlink" }}
- <a href="{{ . }}">
- {{- end -}}
- {{- .Get "attr" | markdownify -}}
- {{- if .Get "attrlink" }}</a>{{ end }}</p>
- {{- end }}
- </figcaption>
- {{- end }}
-</figure>
+++ /dev/null
-{{- warnf "The %q shortcode was deprecated in v0.143.0 and will be removed in a future release. See https://gohugo.io/shortcodes/gist for instructions to create a replacement." .Name -}}
-<script src="https://gist.github.com/{{ index .Params 0 }}/{{ index .Params 1 }}.js{{if len .Params | eq 3 }}?file={{ index .Params 2 }}{{end}}"></script>
+++ /dev/null
-{{ if len .Params | eq 2 }}{{ highlight (trim .InnerDeindent "\n\r") (.Get 0) (.Get 1) }}{{ else }}{{ highlight (trim .InnerDeindent "\n\r") (.Get 0) "" }}{{ end }}
\ No newline at end of file
+++ /dev/null
-{{- $pc := site.Config.Privacy.Instagram -}}
-{{- if not $pc.Disable -}}
- {{- with .Get 0 -}}
- {{- template "render-instagram" (dict "id" . "pc" $pc) -}}
- {{- else -}}
- {{- errorf "The %q shortocde requires a single positional parameter, the ID of the Instagram post. See %s" .Name .Position -}}
- {{- end -}}
-{{- end -}}
-
-{{- define "render-instagram" -}}
- <blockquote
- class="instagram-media"
- data-instgrm-captioned
- data-instgrm-permalink="https://www.instagram.com/p/{{ .id }}"
- data-instgrm-version="14"
- style="
- background: #fff;
- border: 0;
- border-radius: 3px;
- box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.5), 0 1px 10px 0 rgba(0, 0, 0, 0.15);
- margin: 1px;
- max-width: 540px;
- min-width: 326px;
- padding: 0;
- width: 99.375%;
- width: -webkit-calc(100% - 2px);
- width: calc(100% - 2px);
- "
- >
- <div style="padding: 16px">
- <a
- href="https://www.instagram.com/p/{{ .id }}"
- style="
- background: #ffffff;
- line-height: 0;
- padding: 0 0;
- text-align: center;
- text-decoration: none;
- width: 100%;
- "
- target="_blank"
- >
- <div style="display: flex; flex-direction: row; align-items: center">
- <div
- style="
- background-color: #f4f4f4;
- border-radius: 50%;
- flex-grow: 0;
- height: 40px;
- margin-right: 14px;
- width: 40px;
- "
- ></div>
- <div
- style="
- display: flex;
- flex-direction: column;
- flex-grow: 1;
- justify-content: center;
- "
- >
- <div
- style="
- background-color: #f4f4f4;
- border-radius: 4px;
- flex-grow: 0;
- height: 14px;
- margin-bottom: 6px;
- width: 100px;
- "
- ></div>
- <div
- style="
- background-color: #f4f4f4;
- border-radius: 4px;
- flex-grow: 0;
- height: 14px;
- width: 60px;
- "
- ></div>
- </div>
- </div>
- <div style="padding: 19% 0"></div>
- <div
- style="display: block; height: 50px; margin: 0 auto 12px; width: 50px"
- >
- <svg
- width="50px"
- height="50px"
- viewBox="0 0 60 60"
- version="1.1"
- xmlns="https://www.w3.org/2000/svg"
- xmlns:xlink="https://www.w3.org/1999/xlink"
- >
- <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <g transform="translate(-511.000000, -20.000000)" fill="#000000">
- <g>
- <path
- d="M556.869,30.41 C554.814,30.41 553.148,32.076 553.148,34.131 C553.148,36.186 554.814,37.852 556.869,37.852 C558.924,37.852 560.59,36.186 560.59,34.131 C560.59,32.076 558.924,30.41 556.869,30.41 M541,60.657 C535.114,60.657 530.342,55.887 530.342,50 C530.342,44.114 535.114,39.342 541,39.342 C546.887,39.342 551.658,44.114 551.658,50 C551.658,55.887 546.887,60.657 541,60.657 M541,33.886 C532.1,33.886 524.886,41.1 524.886,50 C524.886,58.899 532.1,66.113 541,66.113 C549.9,66.113 557.115,58.899 557.115,50 C557.115,41.1 549.9,33.886 541,33.886 M565.378,62.101 C565.244,65.022 564.756,66.606 564.346,67.663 C563.803,69.06 563.154,70.057 562.106,71.106 C561.058,72.155 560.06,72.803 558.662,73.347 C557.607,73.757 556.021,74.244 553.102,74.378 C549.944,74.521 548.997,74.552 541,74.552 C533.003,74.552 532.056,74.521 528.898,74.378 C525.979,74.244 524.393,73.757 523.338,73.347 C521.94,72.803 520.942,72.155 519.894,71.106 C518.846,70.057 518.197,69.06 517.654,67.663 C517.244,66.606 516.755,65.022 516.623,62.101 C516.479,58.943 516.448,57.996 516.448,50 C516.448,42.003 516.479,41.056 516.623,37.899 C516.755,34.978 517.244,33.391 517.654,32.338 C518.197,30.938 518.846,29.942 519.894,28.894 C520.942,27.846 521.94,27.196 523.338,26.654 C524.393,26.244 525.979,25.756 528.898,25.623 C532.057,25.479 533.004,25.448 541,25.448 C548.997,25.448 549.943,25.479 553.102,25.623 C556.021,25.756 557.607,26.244 558.662,26.654 C560.06,27.196 561.058,27.846 562.106,28.894 C563.154,29.942 563.803,30.938 564.346,32.338 C564.756,33.391 565.244,34.978 565.378,37.899 C565.522,41.056 565.552,42.003 565.552,50 C565.552,57.996 565.522,58.943 565.378,62.101 M570.82,37.631 C570.674,34.438 570.167,32.258 569.425,30.349 C568.659,28.377 567.633,26.702 565.965,25.035 C564.297,23.368 562.623,22.342 560.652,21.575 C558.743,20.834 556.562,20.326 553.369,20.18 C550.169,20.033 549.148,20 541,20 C532.853,20 531.831,20.033 528.631,20.18 C525.438,20.326 523.257,20.834 521.349,21.575 C519.376,22.342 517.703,23.368 516.035,25.035 C514.368,26.702 513.342,28.377 512.574,30.349 C511.834,32.258 511.326,34.438 511.181,37.631 C511.035,40.831 511,41.851 511,50 C511,58.147 511.035,59.17 511.181,62.369 C511.326,65.562 511.834,67.743 512.574,69.651 C513.342,71.625 514.368,73.296 516.035,74.965 C517.703,76.634 519.376,77.658 521.349,78.425 C523.257,79.167 525.438,79.673 528.631,79.82 C531.831,79.965 532.853,80.001 541,80.001 C549.148,80.001 550.169,79.965 553.369,79.82 C556.562,79.673 558.743,79.167 560.652,78.425 C562.623,77.658 564.297,76.634 565.965,74.965 C567.633,73.296 568.659,71.625 569.425,69.651 C570.167,67.743 570.674,65.562 570.82,62.369 C570.966,59.17 571,58.147 571,50 C571,41.851 570.966,40.831 570.82,37.631"
- ></path>
- </g>
- </g>
- </g>
- </svg>
- </div>
- <div style="padding-top: 8px">
- <div
- style="
- color: #3897f0;
- font-family: Arial, sans-serif;
- font-size: 14px;
- font-style: normal;
- font-weight: 550;
- line-height: 18px;
- "
- >
- View this post on Instagram
- </div>
- </div>
- <div style="padding: 12.5% 0"></div>
- <div
- style="
- display: flex;
- flex-direction: row;
- margin-bottom: 14px;
- align-items: center;
- "
- >
- <div>
- <div
- style="
- background-color: #f4f4f4;
- border-radius: 50%;
- height: 12.5px;
- width: 12.5px;
- transform: translateX(0px) translateY(7px);
- "
- ></div>
- <div
- style="
- background-color: #f4f4f4;
- height: 12.5px;
- transform: rotate(-45deg) translateX(3px) translateY(1px);
- width: 12.5px;
- flex-grow: 0;
- margin-right: 14px;
- margin-left: 2px;
- "
- ></div>
- <div
- style="
- background-color: #f4f4f4;
- border-radius: 50%;
- height: 12.5px;
- width: 12.5px;
- transform: translateX(9px) translateY(-18px);
- "
- ></div>
- </div>
- <div style="margin-left: 8px">
- <div
- style="
- background-color: #f4f4f4;
- border-radius: 50%;
- flex-grow: 0;
- height: 20px;
- width: 20px;
- "
- ></div>
- <div
- style="
- width: 0;
- height: 0;
- border-top: 2px solid transparent;
- border-left: 6px solid #f4f4f4;
- border-bottom: 2px solid transparent;
- transform: translateX(16px) translateY(-4px) rotate(30deg);
- "
- ></div>
- </div>
- <div style="margin-left: auto">
- <div
- style="
- width: 0px;
- border-top: 8px solid #f4f4f4;
- border-right: 8px solid transparent;
- transform: translateY(16px);
- "
- ></div>
- <div
- style="
- background-color: #f4f4f4;
- flex-grow: 0;
- height: 12px;
- width: 16px;
- transform: translateY(-4px);
- "
- ></div>
- <div
- style="
- width: 0;
- height: 0;
- border-top: 8px solid #f4f4f4;
- border-left: 8px solid transparent;
- transform: translateY(-4px) translateX(8px);
- "
- ></div>
- </div>
- </div>
- <div
- style="
- display: flex;
- flex-direction: column;
- flex-grow: 1;
- justify-content: center;
- margin-bottom: 24px;
- "
- >
- <div
- style="
- background-color: #f4f4f4;
- border-radius: 4px;
- flex-grow: 0;
- height: 14px;
- margin-bottom: 6px;
- width: 224px;
- "
- ></div>
- <div
- style="
- background-color: #f4f4f4;
- border-radius: 4px;
- flex-grow: 0;
- height: 14px;
- width: 144px;
- "
- ></div></div
- ></a>
- </div>
- </blockquote>
- {{- if not .pc.Simple -}}
- <script async src="https://www.instagram.com/embed.js"></script>
- {{- end -}}
-{{- end -}}
+++ /dev/null
-{{- errorf `The %q shortcode was removed in v0.123.0. Use the "instagram" shortcode instead. See %s` .Name .Position -}}
+++ /dev/null
-{{- $name := (.Get 0) -}}
-{{- with $name -}}
-{{- with ($.Page.Param .) }}{{ . }}{{ else }}{{ errorf "Param %q not found: %s" $name $.Position }}{{ end -}}
-{{- else }}{{ errorf "Missing param key: %s" $.Position }}{{ end -}}
\ No newline at end of file
+++ /dev/null
-{{- /*
-Encodes the given text into a QR code using the specified options and renders the resulting image.
-
-@param {string} text The text to encode, falling back to the text between the opening and closing shortcode tags.
-@param {string} [level=medium] The error correction level to use when encoding the text, one of low, medium, quartile, or high.
-@param {int} [scale=4] The number of image pixels per QR code module. Must be greater than or equal to 2.
-@param {string} [targetDir] The subdirectory within the publishDir where Hugo will place the generated image.
-@param {string} [alt] The alt attribute of the img element.
-@param {string} [class] The class attribute of the img element.
-@param {string} [id] The id attribute of the img element.
-@param {string} [title] The title attribute of the img element.
-@param {string} [loading] The loading attribute of the img element, one of lazy or eager.
-
-@returns {template.HTML}
-
-@examples
-
- {{< qr text="https://gohugo.io" />}}
-
- {{< qr >}}
- https://gohugo.io"
- {{< /qr >}}
-
- {{< qr
- text="https://gohugo.io"
- level="high"
- scale=4
- targetDir="codes"
- alt="QR code linking to https://gohugo.io"
- class="my-class"
- id="my-id"
- title="My Title"
- />}}
-
-*/}}
-
-{{- /* Constants. */}}
-{{- $validLevels := slice "low" "medium" "quartile" "high" }}
-{{- $minimumScale := 2 }}
-
-{{- /* Get arguments. */}}
-{{- $text := or (.Get "text") (strings.TrimSpace .Inner) "" }}
-{{- $level := or (.Get "level") "medium" }}
-{{- $scale := or (.Get "scale") 4 }}
-{{- $targetDir := or (.Get "targetDir") "" }}
-{{- $alt := or (.Get "alt") "" }}
-{{- $class := or (.Get "class") "" }}
-{{- $id := or (.Get "id") "" }}
-{{- $title := or (.Get "title") "" }}
-{{- $loading := or (.Get "loading") "" }}
-
-{{- /* Validate arguments. */}}
-{{- $errors := false}}
-{{- if not $text }}
- {{- errorf "The %q shortcode requires a %q argument. See %s" .Name "text" .Position }}
- {{- $errors = true }}
-{{- end }}
-{{- if not (in $validLevels $level) }}
- {{- errorf "The %q argument passed to the %q shortcode must be one of %s. See %s" "level" .Name (delimit $validLevels ", " ", or ") .Position }}
- {{- $errors = true }}
-{{- end }}
-{{- if or (lt $scale $minimumScale) (ne $scale (int $scale)) }}
- {{- errorf "The %q argument passed to the %q shortcode must be an integer greater than or equal to %d. See %s" "scale" .Name $minimumScale .Position }}
- {{- $errors = true }}
-{{- end }}
-
-{{- /* Render image. */}}
-{{- if not $errors }}
- {{- $opts := dict "level" $level "scale" $scale "targetDir" $targetDir }}
- {{- with images.QR $text $opts -}}
- <img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}"
- {{- with $alt }} alt="{{ $alt }}" {{- end }}
- {{- with $class }} class="{{ $class }}" {{- end }}
- {{- with $id }} id="{{ $id }}" {{- end }}
- {{- with $title }} title="{{ $title }}" {{- end -}}
- {{- with $loading }} loading="{{ $loading }}" {{- end -}}
- >
- {{- end }}
-{{- end -}}
+++ /dev/null
-{{ ref . .Params }}
\ No newline at end of file
+++ /dev/null
-{{ relref . .Params }}
\ No newline at end of file
+++ /dev/null
-{{- warnf "The \"twitter\", \"tweet\", and \"twitter_simple\" shortcodes were deprecated in v0.142.0 and will be removed in a future release. Please use the \"x\" shortcode instead." }}
-{{- $pc := site.Config.Privacy.Twitter -}}
-{{- if not $pc.Disable -}}
- {{- if $pc.Simple -}}
- {{- template "_internal/shortcodes/twitter_simple.html" . -}}
- {{- else -}}
- {{- $id := or (.Get "id") "" -}}
- {{- $user := or (.Get "user") "" -}}
- {{- if and $id $user -}}
- {{- template "render-tweet" (dict "id" $id "user" $user "dnt" $pc.EnableDNT "ctx" .) -}}
- {{- else -}}
- {{- errorf "The %q shortcode requires two named parameters: user and id. See %s" .Name .Position -}}
- {{- end -}}
- {{- end -}}
-{{- end -}}
-
-{{- define "render-tweet" -}}
- {{- $url := printf "https://twitter.com/%v/status/%v" .user .id -}}
- {{- $query := querify "url" $url "dnt" .dnt -}}
- {{- $request := printf "https://publish.twitter.com/oembed?%s" $query -}}
- {{- with try (resources.GetRemote $request) -}}
- {{- with .Err -}}
- {{- errorf "%s" . -}}
- {{- else with .Value -}}
- {{- (. | transform.Unmarshal).html | safeHTML -}}
- {{- else -}}
- {{- warnidf "shortcode-twitter-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
- {{- end -}}
- {{- end -}}
-{{- end -}}
+++ /dev/null
-{{- warnf "The \"twitter\", \"tweet\", and \"twitter_simple\" shortcodes were deprecated in v0.142.0 and will be removed in a future release. Please use the \"x\" shortcode instead." }}
-{{- if not site.Config.Privacy.Twitter.Disable -}}
- {{- $id := or (.Get "id") "" -}}
- {{- $user := or (.Get "user") "" -}}
- {{- if and $id $user -}}
- {{- template "render-simple-tweet" (dict "id" $id "user" $user "ctx" .) -}}
- {{- else -}}
- {{- errorf "The %q shortcode requires two named parameters: user and id. See %s" .Name .Position -}}
- {{- end -}}
-{{- end -}}
-
-{{- define "render-simple-tweet" -}}
- {{- $dnt := site.Config.Privacy.Twitter.EnableDNT }}
- {{- $url := printf "https://twitter.com/%v/status/%v" .user .id -}}
- {{- $query := querify "url" $url "dnt" $dnt "omit_script" true -}}
- {{- $request := printf "https://publish.twitter.com/oembed?%s" $query -}}
- {{- with try (resources.GetRemote $request) -}}
- {{- with .Err -}}
- {{- errorf "%s" . -}}
- {{- else with .Value -}}
- {{- if not site.Config.Services.Twitter.DisableInlineCSS }}
- {{- template "__h_simple_twitter_css" (dict "ctx" $.ctx) }}
- {{- end }}
- {{- (. | transform.Unmarshal).html | safeHTML -}}
- {{- else -}}
- {{- warnidf "shortcode-twitter-simple-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
- {{- end -}}
- {{- end -}}
-{{- end -}}
-
-{{- define "__h_simple_twitter_css" -}}
- {{- if not (.ctx.Page.Store.Get "__h_simple_twitter_css") -}}
- {{/* Only include once */}}
- {{- .ctx.Page.Store.Set "__h_simple_twitter_css" true -}}
- <style type="text/css">
- .twitter-tweet {
- font:
- 14px/1.45 -apple-system,
- BlinkMacSystemFont,
- "Segoe UI",
- Roboto,
- Oxygen-Sans,
- Ubuntu,
- Cantarell,
- "Helvetica Neue",
- sans-serif;
- border-left: 4px solid #2b7bb9;
- padding-left: 1.5em;
- color: #555;
- }
- .twitter-tweet a {
- color: #2b7bb9;
- text-decoration: none;
- }
- blockquote.twitter-tweet a:hover,
- blockquote.twitter-tweet a:focus {
- text-decoration: underline;
- }
- </style>
- {{- end -}}
-{{- end -}}
+++ /dev/null
-{{- /*
-Renders an embedded Vimeo video.
-
-Accepts named or positional arguments. If positional, order is id, class,
-title, then loading.
-
-@param {string} [id] The video id. Optional if the id is provided as first positional argument.
-@param {string} [class] The class attribute of the wrapping div element. When specified, removes the style attributes from the iframe element and its wrapping div element.
-@param {string} [loading=eager] The loading attribute of the iframe element.
-@param {string} [title=Vimeo video] The title attribute of the iframe element.
-@param {bool} [allowFullScreen=true] Whether the iframe element can activate full screen mode.
-
-@returns {template.HTML}
-
-@example {{< vimeo 55073825 >}}
-@example {{< vimeo id=55073825 class="foo bar" loading=lazy title="My Video" >}}
-*/}}
-{{- $pc := site.Config.Privacy.Vimeo }}
-{{- if not $pc.Disable }}
- {{- if $pc.Simple }}
- {{- template "_internal/shortcodes/vimeo_simple.html" . }}
- {{- else }}
- {{- $dnt := cond $pc.EnableDNT 1 0 }}
-
- {{- $id := or (.Get "id") (.Get 0) "" }}
- {{- $class := or (.Get "class") (.Get 1) "" }}
- {{- $title := or (.Get "title") (.Get 2) "Vimeo video" }}
- {{- $loading := or (.Get "loading") (.Get 3) "eager" }}
- {{- $allowFullScreen := or (.Get "allowFullScreen") (.Get 4) true }}
-
- {{- if in (slice "false" false 0) ($.Get "allowFullScreen") }}
- {{- $allowFullScreen = false }}
- {{- else if in (slice "true" true 1) ($.Get "allowFullScreen") }}
- {{- $allowFullScreen = true }}
- {{- end }}
-
- {{- $iframeAllowList := "" }}
- {{- if $allowFullScreen }}
- {{- $iframeAllowList = "fullscreen" }}
- {{- end }}
-
- {{- $divStyle := "position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;" }}
- {{- $iframeStyle := "position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" }}
-
- {{- with $id }}
- {{- $src := printf "https://player.vimeo.com/video/%v?dnt=%v" . $dnt }}
- <div
- {{- with $class }}
- class="{{ . }}"
- {{- else }}
- style="{{ $divStyle | safeCSS }}"
- {{- end }}>
- <iframe
- src="{{- $src }}"
- {{- if not $class }}
- style="{{ $iframeStyle | safeCSS }}"
- {{- end }}
- {{- with $iframeAllowList }} allow="{{ . }}" {{- end }}
- {{- with $loading }} loading="{{ . }}" {{- end }}
- {{- with $title }} title="{{ . }}" {{- end }}>
- </iframe>
- </div>
- {{- else }}
- {{- errorf "The %q shortcode requires a video id, either as the first positional argument or an argument named id. See %s" .Name .Position }}
- {{- end }}
- {{- end }}
-{{- end }}
+++ /dev/null
-{{- $pc := site.Config.Privacy.Vimeo -}}
-{{- if not $pc.Disable -}}
- {{- $ctx := dict "ctx" . }}
- {{- if .IsNamedParams -}}
- {{- with .Get "id" -}}
- {{- $ctx = merge $ctx (dict "id" . "class" ($.Get "class")) -}}
- {{- template "render-vimeo" $ctx -}}
- {{- else -}}
- {{- errorf "The %q shortocde requires a single named parameter, the ID of the Vimeo video. See %s" .Name .Position -}}
- {{- end -}}
- {{- else -}}
- {{- with .Get 0 -}}
- {{- $ctx = merge $ctx (dict "id" . "class" ($.Get 1)) -}}
- {{- template "render-vimeo" $ctx -}}
- {{- else -}}
- {{- errorf "The %q shortocde requires a single positional parameter, the ID of the Vimeo video. See %s" .Name .Position -}}
- {{- end -}}
- {{- end -}}
-{{- end -}}
-
-{{- define "render-vimeo" -}}
- {{- $dnt := cond site.Config.Privacy.Vimeo.EnableDNT 1 0 -}}
- {{- $url := urls.JoinPath "https://vimeo.com" .id -}}
- {{- $query := querify "url" $url "dnt" $dnt -}}
- {{- $request := printf "https://vimeo.com/api/oembed.json?%s" $query -}}
- {{- with try (resources.GetRemote $request) -}}
- {{- with .Err -}}
- {{- errorf "%s" . -}}
- {{- else with .Value -}}
- {{- with . | transform.Unmarshal -}}
- {{- $class := printf "%s %s" "s_video_simple" "__h_video" -}}
- {{- with $.class -}}
- {{- $class = printf "%s %s" "s_video_simple" . -}}
- {{- else -}}
- {{ template "__h_simple_css" $.ctx.Page }}
- {{- end -}}
- {{- $thumbnail := .thumbnail_url -}}
- {{- $original := $thumbnail | replaceRE "(_.*\\.)" "." -}}
- <div class="{{ $class }}">
- <a href="{{ .provider_url }}{{ .video_id }}" rel="noopener" target="_blank">
- <img src="{{ $thumbnail }}" srcset="{{ $thumbnail }} 1x, {{ $original }} 2x" alt="{{ .title }}">
- <div class="play">
- {{ template "__h_simple_icon_play" $.ctx.Page }}
- </div>
- </a>
- </div>
- {{- end -}}
- {{- else -}}
- {{- warnidf "shortcode-vimeo-simple" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
- {{- end -}}
- {{- end -}}
-{{- end -}}
+++ /dev/null
-{{- $pc := site.Config.Privacy.X -}}
-{{- if not $pc.Disable -}}
- {{- if $pc.Simple -}}
- {{- template "_internal/shortcodes/x_simple.html" . -}}
- {{- else -}}
- {{- $id := or (.Get "id") "" -}}
- {{- $user := or (.Get "user") "" -}}
- {{- if and $id $user -}}
- {{- template "render-x" (dict "id" $id "user" $user "dnt" $pc.EnableDNT "ctx" .) -}}
- {{- else -}}
- {{- errorf "The %q shortcode requires two named parameters: user and id. See %s" .Name .Position -}}
- {{- end -}}
- {{- end -}}
-{{- end -}}
-
-{{- define "render-x" -}}
- {{- $url := printf "https://x.com/%v/status/%v" .user .id -}}
- {{- $query := querify "url" $url "dnt" .dnt -}}
- {{- $request := printf "https://publish.x.com/oembed?%s" $query -}}
- {{- with try (resources.GetRemote $request) -}}
- {{- with .Err -}}
- {{- errorf "%s" . -}}
- {{- else with .Value -}}
- {{- (. | transform.Unmarshal).html | safeHTML -}}
- {{- else -}}
- {{- warnidf "shortcode-x-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
- {{- end -}}
- {{- end -}}
-{{- end -}}
+++ /dev/null
-{{- if not site.Config.Privacy.X.Disable -}}
- {{- $id := or (.Get "id") "" -}}
- {{- $user := or (.Get "user") "" -}}
- {{- if and $id $user -}}
- {{- template "render-simple-x" (dict "id" $id "user" $user "ctx" .) -}}
- {{- else -}}
- {{- errorf "The %q shortcode requires two named parameters: user and id. See %s" .Name .Position -}}
- {{- end -}}
-{{- end -}}
-
-{{- define "render-simple-x" -}}
- {{- $dnt := site.Config.Privacy.X.EnableDNT }}
- {{- $url := printf "https://x.com/%v/status/%v" .user .id -}}
- {{- $query := querify "url" $url "dnt" $dnt "omit_script" true -}}
- {{- $request := printf "https://publish.x.com/oembed?%s" $query -}}
- {{- with try (resources.GetRemote $request) -}}
- {{- with .Err -}}
- {{- errorf "%s" . -}}
- {{- else with .Value -}}
- {{- if not site.Config.Services.X.DisableInlineCSS }}
- {{- template "__h_simple_x_css" (dict "ctx" $.ctx) }}
- {{- end }}
- {{- (. | transform.Unmarshal).html | safeHTML -}}
- {{- else -}}
- {{- warnidf "shortcode-x-simple-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
- {{- end -}}
- {{- end -}}
-{{- end -}}
-
-{{- define "__h_simple_x_css" -}}
- {{- if not (.ctx.Page.Store.Get "__h_simple_x_css") -}}
- {{/* Only include once */}}
- {{- .ctx.Page.Store.Set "__h_simple_x_css" true -}}
- <style type="text/css">
- .twitter-tweet {
- font:
- 14px/1.45 -apple-system,
- BlinkMacSystemFont,
- "Segoe UI",
- Roboto,
- Oxygen-Sans,
- Ubuntu,
- Cantarell,
- "Helvetica Neue",
- sans-serif;
- border-left: 4px solid #2b7bb9;
- padding-left: 1.5em;
- color: #555;
- }
- .twitter-tweet a {
- color: #2b7bb9;
- text-decoration: none;
- }
- blockquote.twitter-tweet a:hover,
- blockquote.twitter-tweet a:focus {
- text-decoration: underline;
- }
- </style>
- {{- end -}}
-{{- end -}}
+++ /dev/null
-{{- /*
-Renders an embedded YouTube video.
-
-@param {bool} [allowFullScreen=true] Whether the iframe element can activate full screen mode.
-@param {bool} [autoplay=false] Whether to automatically play the video. Forces mute to be true.
-@param {string} [class] The class attribute of the wrapping div element. When specified, removes the style attributes from the iframe element and its wrapping div element.
-@param {bool} [controls=true] Whether to display the video controls.
-@param {int} [end] The time, measured in seconds from the start of the video, when the player should stop playing the video.
-@param {string} [id] The video id. Optional if the id is provided as first positional argument.
-@param {string} [loading=eager] The loading attribute of the iframe element.
-@param {bool} [loop=false] Whether to indefinitely repeat the video. Ignores the start and end arguments after the first play.
-@param {bool} [mute=false] Whether to mute the video. Always true when autoplay is true.
-@param {int} [start] The time, measured in seconds from the start of the video, when the player should start playing the video.
-@param {string} [title] The title attribute of the iframe element. Defaults to "YouTube video".
-
-@returns {template.HTML}
-
-@reference https://developers.google.com/youtube/player_parameters
-
-@example {{< youtube 0RKpf3rK57I >}}
-@example {{< youtube id=0RKpf3rK57I loading=lazy start=30 >}}
-*/}}
-
-{{- $pc := .Page.Site.Config.Privacy.YouTube }}
-{{- $remoteErrID := "err-youtube-remote" }}
-{{- if not $pc.Disable }}
- {{- with $id := or (.Get "id") (.Get 0) }}
-
- {{- /* Set defaults. */}}
- {{- $allowFullScreen := true }}
- {{- $autoplay := 0 }}
- {{- $class := "" }}
- {{- $controls := 1 }}
- {{- $end := 0 }}
- {{- $loading := "eager" }}
- {{- $loop := 0 }}
- {{- $mute := 0 }}
- {{- $start := 0 }}
- {{- $title := "YouTube video" }}
- {{- $iframeAllowList := "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" }}
-
- {{- /* Get arguments. */}}
- {{- if in (slice "true" true 1) ($.Get "allowFullScreen") }}
- {{- $iframeAllowList = printf "%s; fullscreen" $iframeAllowList }}
- {{- end }}
- {{- if in (slice "false" false 0) ($.Get "autoplay") }}
- {{- $autoplay = 0 }}
- {{- else if in (slice "true" true 1) ($.Get "autoplay") }}
- {{- $autoplay = 1 }}
- {{- end }}
- {{- if in (slice "false" false 0) ($.Get "controls") }}
- {{- $controls = 0 }}
- {{- else if in (slice "true" true 1) ($.Get "controls") }}
- {{- $controls = 1 }}
- {{- end }}
- {{- if in (slice "false" false 0) ($.Get "loop") }}
- {{- $loop = 0 }}
- {{- else if in (slice "true" true 1) ($.Get "loop") }}
- {{- $loop = 1 }}
- {{- end }}
- {{- if in (slice "false" false 0) ($.Get "mute") }}
- {{- $mute = 0 }}
- {{- else if or (in (slice "true" true 1) ($.Get "mute")) $autoplay }}
- {{- $mute = 1 }}
- {{- end }}
- {{- $class := or ($.Get "class") $class }}
- {{- $end := or ($.Get "end") $end }}
- {{- $loading := or ($.Get "loading") $loading }}
- {{- $start := or ($.Get "start") $start }}
- {{- $title := or ($.Get "title") $title }}
-
- {{- /* Define src attribute. */}}
- {{- $host := cond $pc.PrivacyEnhanced "www.youtube-nocookie.com" "www.youtube.com" }}
- {{- $src := printf "https://%s/embed/%s" $host $id }}
- {{- $params := dict
- "autoplay" $autoplay
- "controls" $controls
- "end" $end
- "mute" $mute
- "start" $start
- "loop" $loop
- }}
- {{- if $loop }}
- {{- $params = merge $params (dict "playlist" $id) }}
- {{- end }}
- {{- with querify $params }}
- {{- $src = printf "%s?%s" $src . }}
- {{- end }}
-
- {{- /* Set div attributes. */}}
- {{- $divStyle := "position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;" }}
- {{- if $class }}
- {{- $divStyle = "" }}
- {{- end }}
-
- {{- /* Set iframe attributes. */}}
- {{- $iframeStyle := "position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" }}
- {{- if $class }}
- {{- $iframeStyle = "" }}
- {{- end }}
- {{- $referrerpolicy := "strict-origin-when-cross-origin" }}
-
- {{- /* Render. */ -}}
- <div
- {{- with $class }} class="{{ . }}" {{- end }}
- {{- with $divStyle }} style="{{ . | safeCSS }}" {{- end -}}
- >
- <iframe
- {{- with $iframeAllowList }} allow="{{ . }}" {{- end }}
- {{- with $loading }} loading="{{ . }}" {{- end }}
- {{- with $referrerpolicy }} referrerpolicy="{{ . }}" {{- end }}
- {{- with $src }} src="{{ . }}" {{- end }}
- {{- with $iframeStyle}} style="{{ . | safeCSS }}" {{- end }}
- {{- with $title }} title="{{ . }}" {{- end -}}
- ></iframe>
- </div>
- {{- else }}
- {{- errorf "The %q shortcode requires an id argument. See %s" .Name .Position }}
- {{- end }}
-{{- end }}
--- /dev/null
+{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+ xmlns:xhtml="http://www.w3.org/1999/xhtml">
+ {{ range where .Pages "Sitemap.Disable" "ne" true }}
+ {{- if .Permalink -}}
+ <url>
+ <loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }}
+ <lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>{{ end }}{{ with .Sitemap.ChangeFreq }}
+ <changefreq>{{ . }}</changefreq>{{ end }}{{ if ge .Sitemap.Priority 0.0 }}
+ <priority>{{ .Sitemap.Priority }}</priority>{{ end }}{{ if .IsTranslated }}{{ range .Translations }}
+ <xhtml:link
+ rel="alternate"
+ hreflang="{{ .Language.LanguageCode }}"
+ href="{{ .Permalink }}"
+ />{{ end }}
+ <xhtml:link
+ rel="alternate"
+ hreflang="{{ .Language.LanguageCode }}"
+ href="{{ .Permalink }}"
+ />{{ end }}
+ </url>
+ {{- end -}}
+ {{ end }}
+</urlset>
--- /dev/null
+{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
+<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+ {{ range . }}
+ <sitemap>
+ <loc>{{ .SitemapAbsURL }}</loc>
+ {{ if not .Lastmod.IsZero }}
+ <lastmod>{{ .Lastmod.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</lastmod>
+ {{ end }}
+ </sitemap>
+ {{ end }}
+</sitemapindex>
+++ /dev/null
-{{- $images := partial "_funcs/get-page-images" . }}
-{{- with index $images 0 }}
- <meta name="twitter:card" content="summary_large_image">
- <meta name="twitter:image" content="{{ .Permalink }}">
-{{- else }}
- <meta name="twitter:card" content="summary">
-{{- end }}
-
-{{- with or .Title site.Title site.Params.title | plainify }}
- <meta name="twitter:title" content="{{ . }}">
-{{- end }}
-
-{{- with or .Description .Summary site.Params.description | plainify | htmlUnescape }}
- <meta name="twitter:description" content="{{ trim . "\n\r\t " }}">
-{{- end }}
-
-{{- $twitterSite := "" }}
-{{- with site.Params.social }}
- {{- if reflect.IsMap . }}
- {{- with .twitter }}
- {{- $content := . }}
- {{- if not (strings.HasPrefix . "@") }}
- {{- $content = printf "@%v" . }}
- {{- end }}
- <meta name="twitter:site" content="{{ $content }}">
- {{- end }}
- {{- end }}
-{{- end }}
--- /dev/null
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tplimpl
+
+import (
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/resources/kinds"
+)
+
+type layoutLegacyMapping struct {
+ sourcePath string
+ target layoutLegacyMappingTarget
+}
+
+type layoutLegacyMappingTarget struct {
+ targetPath string
+ targetDesc TemplateDescriptor
+ targetCategory Category
+}
+
+var (
+ ltermPlural = layoutLegacyMappingTarget{
+ targetPath: "/PLURAL",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindTerm},
+ targetCategory: CategoryLayout,
+ }
+ ltermBase = layoutLegacyMappingTarget{
+ targetPath: "",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindTerm},
+ targetCategory: CategoryLayout,
+ }
+
+ ltaxPlural = layoutLegacyMappingTarget{
+ targetPath: "/PLURAL",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindTaxonomy},
+ targetCategory: CategoryLayout,
+ }
+ ltaxBase = layoutLegacyMappingTarget{
+ targetPath: "",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindTaxonomy},
+ targetCategory: CategoryLayout,
+ }
+
+ lsectBase = layoutLegacyMappingTarget{
+ targetPath: "",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindSection},
+ targetCategory: CategoryLayout,
+ }
+ lsectTheSection = layoutLegacyMappingTarget{
+ targetPath: "/THESECTION",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindSection},
+ targetCategory: CategoryLayout,
+ }
+)
+
+type legacyTargetPathIdentifiers struct {
+ targetPath string
+ targetCategory Category
+ kind string
+ lang string
+ outputFormat string
+ ext string
+}
+
+type legacyOrdinalMapping struct {
+ ordinal int
+ mapping layoutLegacyMappingTarget
+}
+
+type legacyOrdinalMappingFi struct {
+ m legacyOrdinalMapping
+ fi hugofs.FileMetaInfo
+}
+
+var legacyTermMappings = []layoutLegacyMapping{
+ {sourcePath: "/PLURAL/term", target: ltermPlural},
+ {sourcePath: "/PLURAL/SINGULAR", target: ltermPlural},
+ {sourcePath: "/term/term", target: ltermBase},
+ {sourcePath: "/term/SINGULAR", target: ltermPlural},
+ {sourcePath: "/term/taxonomy", target: ltermPlural},
+ {sourcePath: "/term/list", target: ltermBase},
+ {sourcePath: "/taxonomy/term", target: ltermBase},
+ {sourcePath: "/taxonomy/SINGULAR", target: ltermPlural},
+ {sourcePath: "/SINGULAR/term", target: ltermPlural},
+ {sourcePath: "/SINGULAR/SINGULAR", target: ltermPlural},
+ {sourcePath: "/_default/SINGULAR", target: ltermPlural},
+ {sourcePath: "/_default/taxonomy", target: ltermBase},
+}
+
+var legacyTaxonomyMappings = []layoutLegacyMapping{
+ {sourcePath: "/PLURAL/SINGULAR.terms", target: ltaxPlural},
+ {sourcePath: "/PLURAL/terms", target: ltaxPlural},
+ {sourcePath: "/PLURAL/taxonomy", target: ltaxPlural},
+ {sourcePath: "/PLURAL/list", target: ltaxPlural},
+ {sourcePath: "/SINGULAR/SINGULAR.terms", target: ltaxPlural},
+ {sourcePath: "/SINGULAR/terms", target: ltaxPlural},
+ {sourcePath: "/SINGULAR/taxonomy", target: ltaxPlural},
+ {sourcePath: "/SINGULAR/list", target: ltaxPlural},
+ {sourcePath: "/taxonomy/SINGULAR.terms", target: ltaxPlural},
+ {sourcePath: "/taxonomy/terms", target: ltaxBase},
+ {sourcePath: "/taxonomy/taxonomy", target: ltaxBase},
+ {sourcePath: "/taxonomy/list", target: ltaxBase},
+ {sourcePath: "/_default/SINGULAR.terms", target: ltaxBase},
+ {sourcePath: "/_default/terms", target: ltaxBase},
+ {sourcePath: "/_default/taxonomy", target: ltaxBase},
+}
+
+var legacySectionMappings = []layoutLegacyMapping{
+ // E.g. /mysection/mysection.html
+ {sourcePath: "/THESECTION/THESECTION", target: lsectTheSection},
+ // E.g. /section/mysection.html
+ {sourcePath: "/SECTIONKIND/THESECTION", target: lsectTheSection},
+ // E.g. /section/section.html
+ {sourcePath: "/SECTIONKIND/SECTIONKIND", target: lsectBase},
+ // E.g. /section/list.html
+ {sourcePath: "/SECTIONKIND/list", target: lsectBase},
+ // E.g. /_default/mysection.html
+ {sourcePath: "/_default/THESECTION", target: lsectTheSection},
+}
-// Copyright 2024 The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
+++ /dev/null
-// Copyright 2019 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package tplimpl
-
-import (
- "strings"
-
- "github.com/gohugoio/hugo/tpl"
-)
-
-// Currently lang, outFormat, suffix
-const numTemplateVariants = 3
-
-type shortcodeVariant struct {
- // The possible variants: lang, outFormat, suffix
- // gtag
- // gtag.html
- // gtag.no.html
- // gtag.no.amp.html
- // A slice of length numTemplateVariants.
- variants []string
-
- ts *templateState
-}
-
-type shortcodeTemplates struct {
- variants []shortcodeVariant
-}
-
-func (s *shortcodeTemplates) indexOf(variants []string) int {
-L:
- for i, v1 := range s.variants {
- for i, v2 := range v1.variants {
- if v2 != variants[i] {
- continue L
- }
- }
- return i
- }
- return -1
-}
-
-func (s *shortcodeTemplates) fromVariants(variants tpl.TemplateVariants) (shortcodeVariant, bool) {
- return s.fromVariantsSlice([]string{
- variants.Language,
- strings.ToLower(variants.OutputFormat.Name),
- variants.OutputFormat.MediaType.FirstSuffix.Suffix,
- })
-}
-
-func (s *shortcodeTemplates) fromVariantsSlice(variants []string) (shortcodeVariant, bool) {
- var (
- bestMatch shortcodeVariant
- bestMatchWeight int
- )
-
- for _, variant := range s.variants {
- w := s.compareVariants(variants, variant.variants)
- if bestMatchWeight == 0 || w > bestMatchWeight {
- bestMatch = variant
- bestMatchWeight = w
- }
- }
-
- return bestMatch, true
-}
-
-// calculate a weight for two string slices of same length.
-// higher value means "better match".
-func (s *shortcodeTemplates) compareVariants(a, b []string) int {
- weight := 0
- k := len(a)
- for i, av := range a {
- bv := b[i]
- if av == bv {
- // Add more weight to the left side (language...).
- weight = weight + k - i
- } else {
- weight--
- }
- }
- return weight
-}
-
-func templateVariants(name string) []string {
- _, variants := templateNameAndVariants(name)
- return variants
-}
-
-func templateNameAndVariants(name string) (string, []string) {
- variants := make([]string, numTemplateVariants)
-
- parts := strings.Split(name, ".")
-
- if len(parts) <= 1 {
- // No variants.
- return name, variants
- }
-
- name = parts[0]
- parts = parts[1:]
- lp := len(parts)
- start := len(variants) - lp
-
- for i, j := start, 0; i < len(variants); i, j = i+1, j+1 {
- variants[i] = parts[j]
- }
-
- if lp > 1 && lp < len(variants) {
- for i := lp - 1; i > 0; i-- {
- variants[i-1] = variants[i]
- }
- }
-
- if lp == 1 {
- // Suffix only. Duplicate it into the output format field to
- // make HTML win over AMP.
- variants[len(variants)-2] = variants[len(variants)-1]
- }
-
- return name, variants
-}
-
-func resolveTemplateType(name string) templateType {
- if isShortcode(name) {
- return templateShortcode
- }
-
- if strings.Contains(name, "partials/") {
- return templatePartial
- }
-
- return templateUndefined
-}
-
-func isShortcode(name string) bool {
- return strings.Contains(name, shortcodesPathPrefix)
-}
-
-func isInternal(name string) bool {
- return strings.HasPrefix(name, internalPathPrefix)
-}
+++ /dev/null
-// Copyright 2019 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package tplimpl
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
-)
-
-func TestShortcodesTemplate(t *testing.T) {
- t.Run("isShortcode", func(t *testing.T) {
- c := qt.New(t)
- c.Assert(isShortcode("shortcodes/figures.html"), qt.Equals, true)
- c.Assert(isShortcode("_internal/shortcodes/figures.html"), qt.Equals, true)
- c.Assert(isShortcode("shortcodes\\figures.html"), qt.Equals, false)
- c.Assert(isShortcode("myshortcodes"), qt.Equals, false)
- })
-
- t.Run("variantsFromName", func(t *testing.T) {
- c := qt.New(t)
- c.Assert(templateVariants("figure.html"), qt.DeepEquals, []string{"", "html", "html"})
- c.Assert(templateVariants("figure.no.html"), qt.DeepEquals, []string{"no", "no", "html"})
- c.Assert(templateVariants("figure.no.amp.html"), qt.DeepEquals, []string{"no", "amp", "html"})
- c.Assert(templateVariants("figure.amp.html"), qt.DeepEquals, []string{"amp", "amp", "html"})
-
- name, variants := templateNameAndVariants("figure.html")
- c.Assert(name, qt.Equals, "figure")
- c.Assert(variants, qt.DeepEquals, []string{"", "html", "html"})
- })
-
- t.Run("compareVariants", func(t *testing.T) {
- c := qt.New(t)
- var s *shortcodeTemplates
-
- tests := []struct {
- name string
- name1 string
- name2 string
- expected int
- }{
- {"Same suffix", "figure.html", "figure.html", 6},
- {"Same suffix and output format", "figure.html.html", "figure.html.html", 6},
- {"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 6},
- {"No suffix", "figure", "figure", 6},
- {"Different output format", "figure.amp.html", "figure.html.html", -1},
- {"One with output format, one without", "figure.amp.html", "figure.html", -1},
- }
-
- for _, test := range tests {
- w := s.compareVariants(templateVariants(test.name1), templateVariants(test.name2))
- c.Assert(w, qt.Equals, test.expected)
- }
- })
-
- t.Run("indexOf", func(t *testing.T) {
- c := qt.New(t)
-
- s := &shortcodeTemplates{
- variants: []shortcodeVariant{
- {variants: []string{"a", "b", "c"}},
- {variants: []string{"a", "b", "d"}},
- },
- }
-
- c.Assert(s.indexOf([]string{"a", "b", "c"}), qt.Equals, 0)
- c.Assert(s.indexOf([]string{"a", "b", "d"}), qt.Equals, 1)
- c.Assert(s.indexOf([]string{"a", "b", "x"}), qt.Equals, -1)
- })
-
- t.Run("Name", func(t *testing.T) {
- c := qt.New(t)
-
- c.Assert(templateBaseName(templateShortcode, "shortcodes/foo.html"), qt.Equals, "foo.html")
- c.Assert(templateBaseName(templateShortcode, "_internal/shortcodes/foo.html"), qt.Equals, "foo.html")
- c.Assert(templateBaseName(templateShortcode, "shortcodes/test/foo.html"), qt.Equals, "test/foo.html")
-
- c.Assert(true, qt.Equals, true)
- })
-}
--- /dev/null
+// Code generated by "stringer -type SubCategory"; DO NOT EDIT.
+
+package tplimpl
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[SubCategoryMain-0]
+ _ = x[SubCategoryEmbedded-1]
+ _ = x[SubCategoryInline-2]
+}
+
+const _SubCategory_name = "SubCategoryMainSubCategoryEmbeddedSubCategoryInline"
+
+var _SubCategory_index = [...]uint8{0, 15, 34, 51}
+
+func (i SubCategory) String() string {
+ if i < 0 || i >= SubCategory(len(_SubCategory_index)-1) {
+ return "SubCategory(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _SubCategory_name[_SubCategory_index[i]:_SubCategory_index[i+1]]
+}
+++ /dev/null
-// Copyright 2019 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package tplimpl
-
-import (
- "bytes"
- "context"
- "embed"
- "fmt"
- "io"
- "io/fs"
- "path/filepath"
- "reflect"
- "regexp"
- "sort"
- "strings"
- "sync"
- "time"
- "unicode"
- "unicode/utf8"
-
- "github.com/gohugoio/hugo/common/maps"
- "github.com/gohugoio/hugo/common/types"
- "github.com/gohugoio/hugo/output/layouts"
-
- "github.com/gohugoio/hugo/helpers"
-
- "github.com/gohugoio/hugo/output"
-
- "github.com/gohugoio/hugo/deps"
- "github.com/spf13/afero"
-
- "github.com/gohugoio/hugo/common/herrors"
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
-
- htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
- texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-
- "github.com/gohugoio/hugo/identity"
- "github.com/gohugoio/hugo/tpl"
-)
-
-const (
- textTmplNamePrefix = "_text/"
-
- shortcodesPathPrefix = "shortcodes/"
- internalPathPrefix = "_internal/"
- embeddedPathPrefix = "_embedded/"
- baseFileBase = "baseof"
-)
-
-// The identifiers may be truncated in the log, e.g.
-// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image"
-// We need this to identify position in templates with base templates applied.
-var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`)
-
-// The tweet and twitter shortcodes were deprecated in favor of the x shortcode
-// in v0.141.0. We can remove these aliases in v0.155.0 or later.
-var embeddedTemplatesAliases = map[string][]string{
- "shortcodes/twitter.html": {"shortcodes/tweet.html"},
-}
-
-var (
- _ tpl.TemplateManager = (*templateExec)(nil)
- _ tpl.TemplateHandler = (*templateExec)(nil)
- _ tpl.TemplateFuncGetter = (*templateExec)(nil)
- _ tpl.TemplateFinder = (*templateExec)(nil)
- _ tpl.UnusedTemplatesProvider = (*templateExec)(nil)
-
- _ tpl.Template = (*templateState)(nil)
- _ tpl.Info = (*templateState)(nil)
-)
-
-var baseTemplateDefineRe = regexp.MustCompile(`^{{-?\s*define`)
-
-// needsBaseTemplate returns true if the first non-comment template block is a
-// define block.
-// If a base template does not exist, we will handle that when it's used.
-func needsBaseTemplate(templ string) bool {
- idx := -1
- inComment := false
- for i := 0; i < len(templ); {
- if !inComment && strings.HasPrefix(templ[i:], "{{/*") {
- inComment = true
- i += 4
- } else if !inComment && strings.HasPrefix(templ[i:], "{{- /*") {
- inComment = true
- i += 6
- } else if inComment && strings.HasPrefix(templ[i:], "*/}}") {
- inComment = false
- i += 4
- } else if inComment && strings.HasPrefix(templ[i:], "*/ -}}") {
- inComment = false
- i += 6
- } else {
- r, size := utf8.DecodeRuneInString(templ[i:])
- if !inComment {
- if strings.HasPrefix(templ[i:], "{{") {
- idx = i
- break
- } else if !unicode.IsSpace(r) {
- break
- }
- }
- i += size
- }
- }
-
- if idx == -1 {
- return false
- }
-
- return baseTemplateDefineRe.MatchString(templ[idx:])
-}
-
-func newStandaloneTextTemplate(funcs map[string]any) tpl.TemplateParseFinder {
- return &textTemplateWrapperWithLock{
- RWMutex: &sync.RWMutex{},
- Template: texttemplate.New("").Funcs(funcs),
- }
-}
-
-func newTemplateHandlers(d *deps.Deps) (*tpl.TemplateHandlers, error) {
- exec, funcs := newTemplateExecuter(d)
- funcMap := make(map[string]any)
- for k, v := range funcs {
- funcMap[k] = v.Interface()
- }
-
- var templateUsageTracker map[string]templateInfo
- if d.Conf.PrintUnusedTemplates() {
- templateUsageTracker = make(map[string]templateInfo)
- }
-
- h := &templateHandler{
- nameBaseTemplateName: make(map[string]string),
- transformNotFound: make(map[string]*templateState),
-
- shortcodes: make(map[string]*shortcodeTemplates),
- templateInfo: make(map[string]tpl.Info),
- baseof: make(map[string]templateInfo),
- needsBaseof: make(map[string]templateInfo),
-
- main: newTemplateNamespace(funcMap),
-
- Deps: d,
- layoutHandler: layouts.NewLayoutHandler(),
- layoutsFs: d.BaseFs.Layouts.Fs,
- layoutTemplateCache: make(map[layoutCacheKey]layoutCacheEntry),
-
- templateUsageTracker: templateUsageTracker,
- }
-
- if err := h.loadEmbedded(); err != nil {
- return nil, err
- }
-
- if err := h.loadTemplates(); err != nil {
- return nil, err
- }
-
- if err := h.main.createPrototypes(); err != nil {
- return nil, err
- }
-
- e := &templateExec{
- d: d,
- executor: exec,
- funcs: funcs,
- templateHandler: h,
- }
-
- if err := e.postTransform(); err != nil {
- return nil, err
- }
-
- return &tpl.TemplateHandlers{
- Tmpl: e,
- TxtTmpl: newStandaloneTextTemplate(funcMap),
- }, nil
-}
-
-func newTemplateNamespace(funcs map[string]any) *templateNamespace {
- return &templateNamespace{
- prototypeHTML: htmltemplate.New("").Funcs(funcs),
- prototypeText: texttemplate.New("").Funcs(funcs),
- prototypeHTMLCloneCache: maps.NewCache[prototypeCloneID, *htmltemplate.Template](),
- prototypeTextCloneCache: maps.NewCache[prototypeCloneID, *texttemplate.Template](),
- templateStateMap: &templateStateMap{
- templates: make(map[string]*templateState),
- },
- }
-}
-
-func newTemplateState(owner *templateState, templ tpl.Template, info templateInfo, id identity.Identity) *templateState {
- if id == nil {
- id = info
- }
- return &templateState{
- owner: owner,
- info: info,
- typ: info.resolveType(),
- Template: templ,
- parseInfo: tpl.DefaultParseInfo,
- id: id,
- }
-}
-
-type layoutCacheKey struct {
- d layouts.LayoutDescriptor
- f string
-}
-
-type templateExec struct {
- d *deps.Deps
- executor texttemplate.Executer
- funcs map[string]reflect.Value
-
- *templateHandler
-}
-
-func (t templateExec) Clone(d *deps.Deps) *templateExec {
- exec, funcs := newTemplateExecuter(d)
- t.executor = exec
- t.funcs = funcs
- t.d = d
- return &t
-}
-
-func (t *templateExec) Execute(templ tpl.Template, wr io.Writer, data any) error {
- return t.ExecuteWithContext(context.Background(), templ, wr, data)
-}
-
-func (t *templateExec) ExecuteWithContext(ctx context.Context, templ tpl.Template, wr io.Writer, data any) error {
- if rlocker, ok := templ.(types.RLocker); ok {
- rlocker.RLock()
- defer rlocker.RUnlock()
- }
- if t.Metrics != nil {
- defer t.Metrics.MeasureSince(templ.Name(), time.Now())
- }
-
- if t.templateUsageTracker != nil {
- if ts, ok := templ.(*templateState); ok {
-
- t.templateUsageTrackerMu.Lock()
- if _, found := t.templateUsageTracker[ts.Name()]; !found {
- t.templateUsageTracker[ts.Name()] = ts.info
- }
-
- if !ts.baseInfo.IsZero() {
- if _, found := t.templateUsageTracker[ts.baseInfo.name]; !found {
- t.templateUsageTracker[ts.baseInfo.name] = ts.baseInfo
- }
- }
- t.templateUsageTrackerMu.Unlock()
- }
- }
-
- execErr := t.executor.ExecuteWithContext(ctx, templ, wr, data)
- if execErr != nil {
- owner := templ
- if ts, ok := templ.(*templateState); ok && ts.owner != nil {
- owner = ts.owner
- }
- execErr = t.addFileContext(owner, execErr)
- }
- return execErr
-}
-
-func (t *templateExec) UnusedTemplates() []tpl.FileInfo {
- if t.templateUsageTracker == nil {
- return nil
- }
- var unused []tpl.FileInfo
-
- for _, ti := range t.needsBaseof {
- if _, found := t.templateUsageTracker[ti.name]; !found {
- unused = append(unused, ti)
- }
- }
-
- for _, ti := range t.baseof {
- if _, found := t.templateUsageTracker[ti.name]; !found {
- unused = append(unused, ti)
- }
- }
-
- for _, ts := range t.main.templates {
- ti := ts.info
- if strings.HasPrefix(ti.name, "_internal/") || ti.meta == nil {
- continue
- }
-
- if _, found := t.templateUsageTracker[ti.name]; !found {
- unused = append(unused, ti)
- }
- }
-
- sort.Slice(unused, func(i, j int) bool {
- return unused[i].Name() < unused[j].Name()
- })
-
- return unused
-}
-
-func (t *templateExec) GetFunc(name string) (reflect.Value, bool) {
- v, found := t.funcs[name]
- return v, found
-}
-
-type templateHandler struct {
- main *templateNamespace
- needsBaseof map[string]templateInfo
- baseof map[string]templateInfo
-
- // This is the filesystem to load the templates from. All the templates are
- // stored in the root of this filesystem.
- layoutsFs afero.Fs
-
- layoutHandler *layouts.LayoutHandler
-
- layoutTemplateCache map[layoutCacheKey]layoutCacheEntry
- layoutTemplateCacheMu sync.RWMutex
-
- *deps.Deps
-
- // Used to get proper filenames in errors
- nameBaseTemplateName map[string]string
-
- // Holds name and source of template definitions not found during the first
- // AST transformation pass.
- transformNotFound map[string]*templateState
-
- // shortcodes maps shortcode name to template variants
- // (language, output format etc.) of that shortcode.
- shortcodes map[string]*shortcodeTemplates
-
- // templateInfo maps template name to some additional information about that template.
- // Note that for shortcodes that same information is embedded in the
- // shortcodeTemplates type.
- templateInfo map[string]tpl.Info
-
- // May be nil.
- templateUsageTracker map[string]templateInfo
- templateUsageTrackerMu sync.Mutex
-}
-
-type layoutCacheEntry struct {
- found bool
- templ tpl.Template
- err error
-}
-
-// AddTemplate parses and adds a template to the collection.
-// Templates with name prefixed with "_text" will be handled as plain
-// text templates.
-func (t *templateHandler) AddTemplate(name, tpl string) error {
- templ, err := t.addTemplateTo(t.newTemplateInfo(name, tpl), t.main)
- if err == nil {
- _, err = t.applyTemplateTransformers(t.main, templ)
- }
- return err
-}
-
-func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
- templ, found := t.main.Lookup(name)
- if found {
- return templ, true
- }
-
- return nil, false
-}
-
-func (t *templateHandler) LookupLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
- key := layoutCacheKey{d, f.Name}
- t.layoutTemplateCacheMu.RLock()
- if cacheVal, found := t.layoutTemplateCache[key]; found {
- t.layoutTemplateCacheMu.RUnlock()
- return cacheVal.templ, cacheVal.found, cacheVal.err
- }
-
- t.layoutTemplateCacheMu.RUnlock()
-
- t.layoutTemplateCacheMu.Lock()
- defer t.layoutTemplateCacheMu.Unlock()
-
- templ, found, err := t.findLayout(d, f)
- cacheVal := layoutCacheEntry{found: found, templ: templ, err: err}
- t.layoutTemplateCache[key] = cacheVal
- return cacheVal.templ, cacheVal.found, cacheVal.err
-}
-
-// This currently only applies to shortcodes and what we get here is the
-// shortcode name.
-func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
- name = templateBaseName(templateShortcode, name)
- s, found := t.shortcodes[name]
- if !found {
- return nil, false, false
- }
-
- sv, found := s.fromVariants(variants)
- if !found {
- return nil, false, false
- }
-
- more := len(s.variants) > 1
-
- return sv.ts, true, more
-}
-
-// LookupVariants returns all variants of name, nil if none found.
-func (t *templateHandler) LookupVariants(name string) []tpl.Template {
- name = templateBaseName(templateShortcode, name)
- s, found := t.shortcodes[name]
- if !found {
- return nil
- }
-
- variants := make([]tpl.Template, len(s.variants))
- for i := range variants {
- variants[i] = s.variants[i].ts
- }
-
- return variants
-}
-
-func (t *templateHandler) HasTemplate(name string) bool {
- if _, found := t.baseof[name]; found {
- return true
- }
-
- if _, found := t.needsBaseof[name]; found {
- return true
- }
-
- _, found := t.Lookup(name)
- return found
-}
-
-func (t *templateHandler) GetIdentity(name string) (identity.Identity, bool) {
- if _, found := t.needsBaseof[name]; found {
- return identity.StringIdentity(name), true
- }
-
- if _, found := t.baseof[name]; found {
- return identity.StringIdentity(name), true
- }
-
- tt, found := t.Lookup(name)
- if !found {
- return nil, false
- }
- return tt.(identity.IdentityProvider).GetIdentity(), found
-}
-
-func (t *templateHandler) findLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
- d.OutputFormatName = f.Name
- d.Suffix = f.MediaType.FirstSuffix.Suffix
- layouts, _ := t.layoutHandler.For(d)
- for _, name := range layouts {
- templ, found := t.main.Lookup(name)
- if found {
- return templ, true, nil
- }
-
- overlay, found := t.needsBaseof[name]
-
- if !found {
- continue
- }
-
- d.Baseof = true
- baseLayouts, _ := t.layoutHandler.For(d)
- var base templateInfo
- found = false
- for _, l := range baseLayouts {
- base, found = t.baseof[l]
- if found {
- break
- }
- }
-
- templ, err := t.applyBaseTemplate(overlay, base)
- if err != nil {
- return nil, false, err
- }
-
- ts := newTemplateState(nil, templ, overlay, identity.Or(base, overlay))
-
- if found {
- ts.baseInfo = base
- }
-
- if _, err := t.applyTemplateTransformers(t.main, ts); err != nil {
- return nil, false, err
- }
-
- if err := t.extractPartials(ts.Template); err != nil {
- return nil, false, err
- }
-
- return ts, true, nil
-
- }
-
- return nil, false, nil
-}
-
-func (t *templateHandler) newTemplateInfo(name, tpl string) templateInfo {
- var isText bool
- var isEmbedded bool
-
- if strings.HasPrefix(name, embeddedPathPrefix) {
- isEmbedded = true
- name = strings.TrimPrefix(name, embeddedPathPrefix)
- }
-
- name, isText = t.nameIsText(name)
- return templateInfo{
- name: name,
- isText: isText,
- isEmbedded: isEmbedded,
- template: tpl,
- }
-}
-
-func (t *templateHandler) addFileContext(templ tpl.Template, inerr error) error {
- if strings.HasPrefix(templ.Name(), "_internal") {
- return inerr
- }
-
- ts, ok := templ.(*templateState)
- if !ok {
- return inerr
- }
-
- identifiers := t.extractIdentifiers(inerr.Error())
-
- checkFilename := func(info templateInfo, inErr error) (error, bool) {
- if info.meta == nil {
- return inErr, false
- }
-
- lineMatcher := func(m herrors.LineMatcher) int {
- if m.Position.LineNumber != m.LineNumber {
- return -1
- }
-
- for _, id := range identifiers {
- if strings.Contains(m.Line, id) {
- // We found the line, but return a 0 to signal to
- // use the column from the error message.
- return 0
- }
- }
- return -1
- }
-
- f, err := info.meta.Open()
- if err != nil {
- return inErr, false
- }
- defer f.Close()
-
- fe := herrors.NewFileErrorFromName(inErr, info.meta.Filename)
- fe.UpdateContent(f, lineMatcher)
-
- if !fe.ErrorContext().Position.IsValid() {
- return inErr, false
- }
- return fe, true
- }
-
- inerr = fmt.Errorf("execute of template failed: %w", inerr)
-
- if err, ok := checkFilename(ts.info, inerr); ok {
- return err
- }
-
- err, _ := checkFilename(ts.baseInfo, inerr)
-
- return err
-}
-
-func (t *templateHandler) extractIdentifiers(line string) []string {
- m := identifiersRe.FindAllStringSubmatch(line, -1)
- identifiers := make([]string, len(m))
- for i := range m {
- identifiers[i] = m[i][1]
- }
- return identifiers
-}
-
-func (t *templateHandler) addShortcodeVariant(ts *templateState) {
- name := ts.Name()
- base := templateBaseName(templateShortcode, name)
-
- shortcodename, variants := templateNameAndVariants(base)
-
- templs, found := t.shortcodes[shortcodename]
- if !found {
- templs = &shortcodeTemplates{}
- t.shortcodes[shortcodename] = templs
- }
-
- sv := shortcodeVariant{variants: variants, ts: ts}
-
- i := templs.indexOf(variants)
-
- if i != -1 {
- // Only replace if it's an override of an internal template.
- if !isInternal(name) {
- templs.variants[i] = sv
- }
- } else {
- templs.variants = append(templs.variants, sv)
- }
-}
-
-func (t *templateHandler) addTemplateFile(name string, fim hugofs.FileMetaInfo) error {
- getTemplate := func(fim hugofs.FileMetaInfo) (templateInfo, error) {
- meta := fim.Meta()
- f, err := meta.Open()
- if err != nil {
- return templateInfo{meta: meta}, err
- }
- defer f.Close()
- b, err := io.ReadAll(f)
- if err != nil {
- return templateInfo{meta: meta}, err
- }
-
- s := removeLeadingBOM(string(b))
-
- var isText bool
- name, isText = t.nameIsText(name)
-
- return templateInfo{
- name: name,
- isText: isText,
- template: s,
- meta: meta,
- }, nil
- }
-
- tinfo, err := getTemplate(fim)
- if err != nil {
- return err
- }
-
- if isBaseTemplatePath(name) {
- // Store it for later.
- t.baseof[name] = tinfo
- return nil
- }
-
- needsBaseof := !t.noBaseNeeded(name) && needsBaseTemplate(tinfo.template)
- if needsBaseof {
- t.needsBaseof[name] = tinfo
- return nil
- }
-
- templ, err := t.addTemplateTo(tinfo, t.main)
- if err != nil {
- return tinfo.errWithFileContext("parse failed", err)
- }
-
- if _, err = t.applyTemplateTransformers(t.main, templ); err != nil {
- return tinfo.errWithFileContext("transform failed", err)
- }
-
- return nil
-}
-
-func (t *templateHandler) addTemplateTo(info templateInfo, to *templateNamespace) (*templateState, error) {
- return to.parse(info)
-}
-
-func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) {
- if overlay.isText {
- var (
- templ = t.main.getPrototypeText(prototypeCloneIDBaseof).New(overlay.name)
- err error
- )
-
- if !base.IsZero() {
- templ, err = templ.Parse(base.template)
- if err != nil {
- return nil, base.errWithFileContext("text: base: parse failed", err)
- }
- }
-
- templ, err = texttemplate.Must(templ.Clone()).Parse(overlay.template)
- if err != nil {
- return nil, overlay.errWithFileContext("text: overlay: parse failed", err)
- }
-
- // The extra lookup is a workaround, see
- // * https://github.com/golang/go/issues/16101
- // * https://github.com/gohugoio/hugo/issues/2549
- // templ = templ.Lookup(templ.Name())
-
- return templ, nil
- }
-
- var (
- templ = t.main.getPrototypeHTML(prototypeCloneIDBaseof).New(overlay.name)
- err error
- )
-
- if !base.IsZero() {
- templ, err = templ.Parse(base.template)
- if err != nil {
- return nil, base.errWithFileContext("html: base: parse failed", err)
- }
- }
-
- templ, err = htmltemplate.Must(templ.Clone()).Parse(overlay.template)
- if err != nil {
- return nil, overlay.errWithFileContext("html: overlay: parse failed", err)
- }
-
- // The extra lookup is a workaround, see
- // * https://github.com/golang/go/issues/16101
- // * https://github.com/gohugoio/hugo/issues/2549
- templ = templ.Lookup(templ.Name())
-
- return templ, err
-}
-
-func (t *templateHandler) applyTemplateTransformers(ns *templateNamespace, ts *templateState) (*templateContext, error) {
- c, err := applyTemplateTransformers(ts, ns.newTemplateLookup(ts))
- if err != nil {
- return nil, err
- }
-
- for k := range c.templateNotFound {
- t.transformNotFound[k] = ts
- }
-
- for k, v := range c.deferNodes {
- if err = t.main.addDeferredTemplate(ts, k, v); err != nil {
- return nil, err
- }
- }
-
- return c, err
-}
-
-//go:embed all:embedded/templates/*
-//go:embed embedded/templates/_default/*
-//go:embed embedded/templates/_server/*
-var embeddedTemplatesFs embed.FS
-
-func (t *templateHandler) loadEmbedded() error {
- return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error {
- if err != nil {
- return err
- }
- if d == nil || d.IsDir() {
- return nil
- }
-
- templb, err := embeddedTemplatesFs.ReadFile(path)
- if err != nil {
- return err
- }
-
- // Get the newlines on Windows in line with how we had it back when we used Go Generate
- // to write the templates to Go files.
- templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
- name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/")
- templateName := name
-
- // For the render hooks and the server templates it does not make sense to preserve the
- // double _internal double book-keeping,
- // just add it if its now provided by the user.
- if !strings.Contains(path, "_default/_markup") && !strings.HasPrefix(name, "_server/") && !strings.HasPrefix(name, "partials/_funcs/") {
- templateName = internalPathPrefix + name
- }
-
- if _, found := t.Lookup(templateName); !found {
- if err := t.AddTemplate(embeddedPathPrefix+templateName, templ); err != nil {
- return err
- }
- }
-
- if aliases, found := embeddedTemplatesAliases[name]; found {
- // TODO(bep) avoid reparsing these aliases
- for _, alias := range aliases {
- alias = internalPathPrefix + alias
- if err := t.AddTemplate(embeddedPathPrefix+alias, templ); err != nil {
- return err
- }
- }
- }
-
- return nil
- })
-}
-
-func (t *templateHandler) loadTemplates() error {
- walker := func(path string, fi hugofs.FileMetaInfo) error {
- if fi.IsDir() {
- return nil
- }
-
- if isDotFile(path) || isBackupFile(path) {
- return nil
- }
-
- name := strings.TrimPrefix(filepath.ToSlash(path), "/")
- filename := filepath.Base(path)
- outputFormats := t.Conf.GetConfigSection("outputFormats").(output.Formats)
- outputFormat, found := outputFormats.FromFilename(filename)
-
- if found && outputFormat.IsPlainText {
- name = textTmplNamePrefix + name
- }
-
- if err := t.addTemplateFile(name, fi); err != nil {
- return err
- }
-
- return nil
- }
-
- if err := helpers.Walk(t.Layouts.Fs, "", walker); err != nil {
- if !herrors.IsNotExist(err) {
- return err
- }
- return nil
- }
-
- return nil
-}
-
-func (t *templateHandler) nameIsText(name string) (string, bool) {
- isText := strings.HasPrefix(name, textTmplNamePrefix)
- if isText {
- name = strings.TrimPrefix(name, textTmplNamePrefix)
- }
- return name, isText
-}
-
-func (t *templateHandler) noBaseNeeded(name string) bool {
- if strings.HasPrefix(name, "shortcodes/") || strings.HasPrefix(name, "partials/") {
- return true
- }
- return strings.Contains(name, "_markup/")
-}
-
-func (t *templateHandler) extractPartials(templ tpl.Template) error {
- templs := templates(templ)
- for _, templ := range templs {
- if templ.Name() == "" || !strings.HasPrefix(templ.Name(), "partials/") {
- continue
- }
-
- ts := newTemplateState(nil, templ, templateInfo{name: templ.Name()}, nil)
- ts.typ = templatePartial
-
- t.main.mu.RLock()
- _, found := t.main.templates[templ.Name()]
- t.main.mu.RUnlock()
-
- if !found {
- t.main.mu.Lock()
- // This is a template defined inline.
- _, err := applyTemplateTransformers(ts, t.main.newTemplateLookup(ts))
- if err != nil {
- t.main.mu.Unlock()
- return err
- }
- t.main.templates[templ.Name()] = ts
- t.main.mu.Unlock()
-
- }
- }
-
- return nil
-}
-
-func (t *templateHandler) postTransform() error {
- defineCheckedHTML := false
- defineCheckedText := false
-
- for _, v := range t.main.templates {
- if v.typ == templateShortcode {
- t.addShortcodeVariant(v)
- }
-
- if defineCheckedHTML && defineCheckedText {
- continue
- }
-
- isText := isText(v.Template)
- if isText {
- if defineCheckedText {
- continue
- }
- defineCheckedText = true
- } else {
- if defineCheckedHTML {
- continue
- }
- defineCheckedHTML = true
- }
-
- if err := t.extractPartials(v.Template); err != nil {
- return err
- }
- }
-
- for name, source := range t.transformNotFound {
- lookup := t.main.newTemplateLookup(source)
- templ := lookup(name)
- if templ != nil {
- _, err := applyTemplateTransformers(templ, lookup)
- if err != nil {
- return err
- }
- }
- }
-
- for _, v := range t.shortcodes {
- sort.Slice(v.variants, func(i, j int) bool {
- v1, v2 := v.variants[i], v.variants[j]
- name1, name2 := v1.ts.Name(), v2.ts.Name()
- isHTMl1, isHTML2 := strings.HasSuffix(name1, "html"), strings.HasSuffix(name2, "html")
-
- // There will be a weighted selection later, but make
- // sure these are sorted to get a stable selection for
- // output formats missing specific templates.
- // Prefer HTML.
- if isHTMl1 || isHTML2 && !(isHTMl1 && isHTML2) {
- return isHTMl1
- }
-
- return name1 < name2
- })
- }
-
- return nil
-}
-
-type prototypeCloneID uint16
-
-const (
- prototypeCloneIDBaseof prototypeCloneID = iota + 1
- prototypeCloneIDDefer
-)
-
-type templateNamespace struct {
- prototypeText *texttemplate.Template
- prototypeHTML *htmltemplate.Template
-
- prototypeHTMLCloneCache *maps.Cache[prototypeCloneID, *htmltemplate.Template]
- prototypeTextCloneCache *maps.Cache[prototypeCloneID, *texttemplate.Template]
-
- *templateStateMap
-}
-
-func (t *templateNamespace) getPrototypeText(id prototypeCloneID) *texttemplate.Template {
- v, ok := t.prototypeTextCloneCache.Get(id)
- if !ok {
- return t.prototypeText
- }
- return v
-}
-
-func (t *templateNamespace) getPrototypeHTML(id prototypeCloneID) *htmltemplate.Template {
- v, ok := t.prototypeHTMLCloneCache.Get(id)
- if !ok {
- return t.prototypeHTML
- }
- return v
-}
-
-func (t *templateNamespace) Lookup(name string) (tpl.Template, bool) {
- t.mu.RLock()
- defer t.mu.RUnlock()
-
- templ, found := t.templates[name]
- if !found {
- return nil, false
- }
-
- return templ, found
-}
-
-func (t *templateNamespace) createPrototypes() error {
- for _, id := range []prototypeCloneID{prototypeCloneIDBaseof, prototypeCloneIDDefer} {
- t.prototypeHTMLCloneCache.Set(id, htmltemplate.Must(t.prototypeHTML.Clone()))
- t.prototypeTextCloneCache.Set(id, texttemplate.Must(t.prototypeText.Clone()))
- }
- return nil
-}
-
-func (t *templateNamespace) newTemplateLookup(in *templateState) func(name string) *templateState {
- return func(name string) *templateState {
- if templ, found := t.templates[name]; found {
- if templ.isText() != in.isText() {
- return nil
- }
- return templ
- }
- if templ, found := findTemplateIn(name, in); found {
- return newTemplateState(nil, templ, templateInfo{name: templ.Name()}, nil)
- }
- return nil
- }
-}
-
-func (t *templateNamespace) addDeferredTemplate(owner *templateState, name string, n *parse.ListNode) error {
- t.mu.Lock()
- defer t.mu.Unlock()
-
- if _, found := t.templates[name]; found {
- return nil
- }
-
- var templ tpl.Template
-
- if owner.isText() {
- prototype := t.getPrototypeText(prototypeCloneIDDefer)
- tt, err := prototype.New(name).Parse("")
- if err != nil {
- return fmt.Errorf("failed to parse empty text template %q: %w", name, err)
- }
- tt.Tree.Root = n
- templ = tt
- } else {
- prototype := t.getPrototypeHTML(prototypeCloneIDDefer)
- tt, err := prototype.New(name).Parse("")
- if err != nil {
- return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err)
- }
- tt.Tree.Root = n
- templ = tt
- }
-
- dts := newTemplateState(owner, templ, templateInfo{name: name}, nil)
- t.templates[name] = dts
-
- return nil
-}
-
-func (t *templateNamespace) parse(info templateInfo) (*templateState, error) {
- t.mu.Lock()
- defer t.mu.Unlock()
-
- if info.isText {
- prototype := t.prototypeText
-
- templ, err := prototype.New(info.name).Parse(info.template)
- if err != nil {
- return nil, err
- }
-
- ts := newTemplateState(nil, templ, info, nil)
-
- t.templates[info.name] = ts
-
- return ts, nil
- }
-
- prototype := t.prototypeHTML
-
- templ, err := prototype.New(info.name).Parse(info.template)
- if err != nil {
- return nil, err
- }
-
- ts := newTemplateState(nil, templ, info, nil)
-
- t.templates[info.name] = ts
-
- return ts, nil
-}
-
-var _ tpl.IsInternalTemplateProvider = (*templateState)(nil)
-
-type templateState struct {
- tpl.Template
-
- // Set for deferred templates.
- owner *templateState
-
- typ templateType
- parseInfo tpl.ParseInfo
- id identity.Identity
-
- info templateInfo
- baseInfo templateInfo // Set when a base template is used.
-}
-
-func (t *templateState) IsInternalTemplate() bool {
- return t.info.isEmbedded
-}
-
-func (t *templateState) GetIdentity() identity.Identity {
- return t.id
-}
-
-func (t *templateState) ParseInfo() tpl.ParseInfo {
- return t.parseInfo
-}
-
-func (t *templateState) isText() bool {
- return isText(t.Template)
-}
-
-func (t *templateState) String() string {
- return t.Name()
-}
-
-func isText(templ tpl.Template) bool {
- _, isText := templ.(*texttemplate.Template)
- return isText
-}
-
-type templateStateMap struct {
- mu sync.RWMutex
- templates map[string]*templateState
-}
-
-type textTemplateWrapperWithLock struct {
- *sync.RWMutex
- *texttemplate.Template
-}
-
-func (t *textTemplateWrapperWithLock) Lookup(name string) (tpl.Template, bool) {
- t.RLock()
- templ := t.Template.Lookup(name)
- t.RUnlock()
- if templ == nil {
- return nil, false
- }
- return &textTemplateWrapperWithLock{
- RWMutex: t.RWMutex,
- Template: templ,
- }, true
-}
-
-func (t *textTemplateWrapperWithLock) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
- panic("not supported")
-}
-
-func (t *textTemplateWrapperWithLock) LookupVariants(name string) []tpl.Template {
- panic("not supported")
-}
-
-func (t *textTemplateWrapperWithLock) Parse(name, tpl string) (tpl.Template, error) {
- t.Lock()
- defer t.Unlock()
- return t.Template.New(name).Parse(tpl)
-}
-
-func isBackupFile(path string) bool {
- return path[len(path)-1] == '~'
-}
-
-func isBaseTemplatePath(path string) bool {
- return strings.Contains(filepath.Base(path), baseFileBase)
-}
-
-func isDotFile(path string) bool {
- return filepath.Base(path)[0] == '.'
-}
-
-func removeLeadingBOM(s string) string {
- const bom = '\ufeff'
-
- for i, r := range s {
- if i == 0 && r != bom {
- return s
- }
- if i > 0 {
- return s[i:]
- }
- }
-
- return s
-}
-
-// resolves _internal/shortcodes/param.html => param.html etc.
-func templateBaseName(typ templateType, name string) string {
- name = strings.TrimPrefix(name, internalPathPrefix)
- switch typ {
- case templateShortcode:
- return strings.TrimPrefix(name, shortcodesPathPrefix)
- default:
- panic("not implemented")
- }
-}
-
-func unwrap(templ tpl.Template) tpl.Template {
- if ts, ok := templ.(*templateState); ok {
- return ts.Template
- }
- return templ
-}
-
-func templates(in tpl.Template) []tpl.Template {
- var templs []tpl.Template
- in = unwrap(in)
- if textt, ok := in.(*texttemplate.Template); ok {
- for _, t := range textt.Templates() {
- templs = append(templs, t)
- }
- }
-
- if htmlt, ok := in.(*htmltemplate.Template); ok {
- for _, t := range htmlt.Templates() {
- templs = append(templs, t)
- }
- }
-
- return templs
-}
+++ /dev/null
-// Copyright 2019 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package tplimpl
+++ /dev/null
-// Copyright 2017-present The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package tplimpl
-
-import (
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/tpl"
-)
-
-// TemplateProvider manages templates.
-type TemplateProvider struct{}
-
-// DefaultTemplateProvider is a globally available TemplateProvider.
-var DefaultTemplateProvider *TemplateProvider
-
-// Update updates the Hugo Template System in the provided Deps
-// with all the additional features, templates & functions.
-func (*TemplateProvider) NewResource(dst *deps.Deps) error {
- handlers, err := newTemplateHandlers(dst)
- if err != nil {
- return err
- }
- dst.SetTempl(handlers)
- return nil
-}
-
-// Clone clones.
-func (*TemplateProvider) CloneResource(dst, src *deps.Deps) error {
- t := src.Tmpl().(*templateExec)
- c := t.Clone(dst)
- funcMap := make(map[string]any)
- for k, v := range c.funcs {
- funcMap[k] = v.Interface()
- }
- dst.SetTempl(&tpl.TemplateHandlers{
- Tmpl: c,
- TxtTmpl: newStandaloneTextTemplate(funcMap),
- })
- return nil
-}
+++ /dev/null
-// Copyright 2016 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package tplimpl
-
-import (
- "errors"
- "fmt"
- "strings"
-
- htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
- texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-
- "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
-
- "github.com/gohugoio/hugo/common/hashing"
- "github.com/gohugoio/hugo/common/maps"
- "github.com/gohugoio/hugo/tpl"
- "github.com/mitchellh/mapstructure"
- "slices"
-)
-
-type templateType int
-
-const (
- templateUndefined templateType = iota
- templateShortcode
- templatePartial
-)
-
-type templateContext struct {
- visited map[string]bool
- templateNotFound map[string]bool
- deferNodes map[string]*parse.ListNode
- lookupFn func(name string) *templateState
-
- // The last error encountered.
- err error
-
- // Set when we're done checking for config header.
- configChecked bool
-
- t *templateState
-
- // Store away the return node in partials.
- returnNode *parse.CommandNode
-}
-
-func (c templateContext) getIfNotVisited(name string) *templateState {
- if c.visited[name] {
- return nil
- }
- c.visited[name] = true
- templ := c.lookupFn(name)
- if templ == nil {
- // This may be a inline template defined outside of this file
- // and not yet parsed. Unusual, but it happens.
- // Store the name to try again later.
- c.templateNotFound[name] = true
- }
-
- return templ
-}
-
-func newTemplateContext(
- t *templateState,
- lookupFn func(name string) *templateState,
-) *templateContext {
- return &templateContext{
- t: t,
- lookupFn: lookupFn,
- visited: make(map[string]bool),
- templateNotFound: make(map[string]bool),
- deferNodes: make(map[string]*parse.ListNode),
- }
-}
-
-func applyTemplateTransformers(
- t *templateState,
- lookupFn func(name string) *templateState,
-) (*templateContext, error) {
- if t == nil {
- return nil, errors.New("expected template, but none provided")
- }
-
- c := newTemplateContext(t, lookupFn)
- tree := getParseTree(t.Template)
-
- _, err := c.applyTransformations(tree.Root)
-
- if err == nil && c.returnNode != nil {
- // This is a partial with a return statement.
- c.t.parseInfo.HasReturn = true
- tree.Root = c.wrapInPartialReturnWrapper(tree.Root)
- }
-
- return c, err
-}
-
-func getParseTree(templ tpl.Template) *parse.Tree {
- templ = unwrap(templ)
- if text, ok := templ.(*texttemplate.Template); ok {
- return text.Tree
- }
- return templ.(*htmltemplate.Template).Tree
-}
-
-const (
- // We parse this template and modify the nodes in order to assign
- // the return value of a partial to a contextWrapper via Set. We use
- // "range" over a one-element slice so we can shift dot to the
- // partial's argument, Arg, while allowing Arg to be falsy.
- partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ range (slice .Arg) }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
-
- doDeferTempl = `{{ doDefer ("PLACEHOLDER1") ("PLACEHOLDER2") }}`
-)
-
-var (
- partialReturnWrapper *parse.ListNode
- doDefer *parse.ListNode
-)
-
-func init() {
- templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
- if err != nil {
- panic(err)
- }
- partialReturnWrapper = templ.Tree.Root
-
- templ, err = texttemplate.New("").Funcs(texttemplate.FuncMap{"doDefer": func(string, string) string { return "" }}).Parse(doDeferTempl)
- if err != nil {
- panic(err)
- }
- doDefer = templ.Tree.Root
-}
-
-// wrapInPartialReturnWrapper copies and modifies the parsed nodes of a
-// predefined partial return wrapper to insert those of a user-defined partial.
-func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
- wrapper := partialReturnWrapper.CopyList()
- rangeNode := wrapper.Nodes[2].(*parse.RangeNode)
- retn := rangeNode.List.Nodes[0]
- setCmd := retn.(*parse.ActionNode).Pipe.Cmds[0]
- setPipe := setCmd.Args[1].(*parse.PipeNode)
- // Replace PLACEHOLDER with the real return value.
- // Note that this is a PipeNode, so it will be wrapped in parens.
- setPipe.Cmds = []*parse.CommandNode{c.returnNode}
- rangeNode.List.Nodes = append(n.Nodes, retn)
-
- return wrapper
-}
-
-// applyTransformations do 2 things:
-// 1) Parses partial return statement.
-// 2) Tracks template (partial) dependencies and some other info.
-func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
- switch x := n.(type) {
- case *parse.ListNode:
- if x != nil {
- c.applyTransformationsToNodes(x.Nodes...)
- }
- case *parse.ActionNode:
- c.applyTransformationsToNodes(x.Pipe)
- case *parse.IfNode:
- c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
- case *parse.WithNode:
- c.handleDefer(x)
- c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
- case *parse.RangeNode:
- c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
- case *parse.TemplateNode:
- subTempl := c.getIfNotVisited(x.Name)
- if subTempl != nil {
- c.applyTransformationsToNodes(getParseTree(subTempl.Template).Root)
- }
- case *parse.PipeNode:
- c.collectConfig(x)
- for i, cmd := range x.Cmds {
- keep, _ := c.applyTransformations(cmd)
- if !keep {
- x.Cmds = slices.Delete(x.Cmds, i, i+1)
- }
- }
-
- case *parse.CommandNode:
- c.collectInner(x)
- keep := c.collectReturnNode(x)
-
- for _, elem := range x.Args {
- switch an := elem.(type) {
- case *parse.PipeNode:
- c.applyTransformations(an)
- }
- }
- return keep, c.err
- }
-
- return true, c.err
-}
-
-func (c *templateContext) handleDefer(withNode *parse.WithNode) {
- if len(withNode.Pipe.Cmds) != 1 {
- return
- }
- cmd := withNode.Pipe.Cmds[0]
- if len(cmd.Args) != 1 {
- return
- }
- idArg := cmd.Args[0]
-
- p, ok := idArg.(*parse.PipeNode)
- if !ok {
- return
- }
-
- if len(p.Cmds) != 1 {
- return
- }
-
- cmd = p.Cmds[0]
-
- if len(cmd.Args) != 2 {
- return
- }
-
- idArg = cmd.Args[0]
-
- id, ok := idArg.(*parse.ChainNode)
- if !ok || len(id.Field) != 1 || id.Field[0] != "Defer" {
- return
- }
- if id2, ok := id.Node.(*parse.IdentifierNode); !ok || id2.Ident != "templates" {
- return
- }
-
- deferArg := cmd.Args[1]
- cmd.Args = []parse.Node{idArg}
-
- l := doDefer.CopyList()
- n := l.Nodes[0].(*parse.ActionNode)
-
- inner := withNode.List.CopyList()
- s := inner.String()
- if strings.Contains(s, "resources.PostProcess") {
- c.err = errors.New("resources.PostProcess cannot be used in a deferred template")
- return
- }
- innerHash := hashing.XxHashFromStringHexEncoded(s)
- deferredID := tpl.HugoDeferredTemplatePrefix + innerHash
-
- c.deferNodes[deferredID] = inner
- withNode.List = l
-
- n.Pipe.Cmds[0].Args[1].(*parse.PipeNode).Cmds[0].Args[0].(*parse.StringNode).Text = deferredID
- n.Pipe.Cmds[0].Args[2] = deferArg
-}
-
-func (c *templateContext) applyTransformationsToNodes(nodes ...parse.Node) {
- for _, node := range nodes {
- c.applyTransformations(node)
- }
-}
-
-func (c *templateContext) hasIdent(idents []string, ident string) bool {
- return slices.Contains(idents, ident)
-}
-
-// collectConfig collects and parses any leading template config variable declaration.
-// This will be the first PipeNode in the template, and will be a variable declaration
-// on the form:
-//
-// {{ $_hugo_config:= `{ "version": 1 }` }}
-func (c *templateContext) collectConfig(n *parse.PipeNode) {
- if c.t.typ != templateShortcode {
- return
- }
- if c.configChecked {
- return
- }
- c.configChecked = true
-
- if len(n.Decl) != 1 || len(n.Cmds) != 1 {
- // This cannot be a config declaration
- return
- }
-
- v := n.Decl[0]
-
- if len(v.Ident) == 0 || v.Ident[0] != "$_hugo_config" {
- return
- }
-
- cmd := n.Cmds[0]
-
- if len(cmd.Args) == 0 {
- return
- }
-
- if s, ok := cmd.Args[0].(*parse.StringNode); ok {
- errMsg := "failed to decode $_hugo_config in template: %w"
- m, err := maps.ToStringMapE(s.Text)
- if err != nil {
- c.err = fmt.Errorf(errMsg, err)
- return
- }
- if err := mapstructure.WeakDecode(m, &c.t.parseInfo.Config); err != nil {
- c.err = fmt.Errorf(errMsg, err)
- }
- }
-}
-
-// collectInner determines if the given CommandNode represents a
-// shortcode call to its .Inner.
-func (c *templateContext) collectInner(n *parse.CommandNode) {
- if c.t.typ != templateShortcode {
- return
- }
- if c.t.parseInfo.IsInner || len(n.Args) == 0 {
- return
- }
-
- for _, arg := range n.Args {
- var idents []string
- switch nt := arg.(type) {
- case *parse.FieldNode:
- idents = nt.Ident
- case *parse.VariableNode:
- idents = nt.Ident
- }
-
- if c.hasIdent(idents, "Inner") || c.hasIdent(idents, "InnerDeindent") {
- c.t.parseInfo.IsInner = true
- break
- }
- }
-}
-
-func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
- if c.t.typ != templatePartial || c.returnNode != nil {
- return true
- }
-
- if len(n.Args) < 2 {
- return true
- }
-
- ident, ok := n.Args[0].(*parse.IdentifierNode)
- if !ok || ident.Ident != "return" {
- return true
- }
-
- c.returnNode = n
- // Remove the "return" identifiers
- c.returnNode.Args = c.returnNode.Args[1:]
-
- return false
-}
-
-func findTemplateIn(name string, in tpl.Template) (tpl.Template, bool) {
- in = unwrap(in)
- if text, ok := in.(*texttemplate.Template); ok {
- if templ := text.Lookup(name); templ != nil {
- return templ, true
- }
- return nil, false
- }
- if templ := in.(*htmltemplate.Template).Lookup(name); templ != nil {
- return templ, true
- }
- return nil, false
-}
+++ /dev/null
-// Copyright 2019 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package tplimpl
-
-import (
- "testing"
-
- template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/tpl"
-)
-
-// Issue #2927
-func TestTransformRecursiveTemplate(t *testing.T) {
- c := qt.New(t)
-
- recursive := `
-{{ define "menu-nodes" }}
-{{ template "menu-node" }}
-{{ end }}
-{{ define "menu-node" }}
-{{ template "menu-node" }}
-{{ end }}
-{{ template "menu-nodes" }}
-`
-
- templ, err := template.New("foo").Parse(recursive)
- c.Assert(err, qt.IsNil)
- ts := newTestTemplate(templ)
-
- ctx := newTemplateContext(
- ts,
- newTestTemplateLookup(ts),
- )
- ctx.applyTransformations(templ.Tree.Root)
-}
-
-func newTestTemplate(templ tpl.Template) *templateState {
- return newTemplateState(nil,
- templ,
- templateInfo{
- name: templ.Name(),
- },
- nil,
- )
-}
-
-func newTestTemplateLookup(in *templateState) func(name string) *templateState {
- m := make(map[string]*templateState)
- return func(name string) *templateState {
- if in.Name() == name {
- return in
- }
-
- if ts, found := m[name]; found {
- return ts
- }
-
- if templ, found := findTemplateIn(name, in); found {
- ts := newTestTemplate(templ)
- m[name] = ts
- return ts
- }
-
- return nil
- }
-}
-
-func TestCollectInfo(t *testing.T) {
- configStr := `{ "version": 42 }`
-
- tests := []struct {
- name string
- tplString string
- expected tpl.ParseInfo
- }{
- {"Basic Inner", `{{ .Inner }}`, tpl.ParseInfo{IsInner: true, Config: tpl.DefaultParseConfig}},
- {"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.ParseInfo{Config: tpl.ParseConfig{Version: 42}}},
- }
-
- echo := func(in any) any {
- return in
- }
-
- funcs := template.FuncMap{
- "highlight": echo,
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- c := qt.New(t)
-
- templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
- c.Assert(err, qt.IsNil)
- ts := newTestTemplate(templ)
- ts.typ = templateShortcode
- ctx := newTemplateContext(
- ts,
- newTestTemplateLookup(ts),
- )
- ctx.applyTransformations(templ.Tree.Root)
- c.Assert(ctx.t.parseInfo, qt.DeepEquals, test.expected)
- })
- }
-}
-
-func TestPartialReturn(t *testing.T) {
- tests := []struct {
- name string
- tplString string
- expected bool
- }{
- {"Basic", `
-{{ $a := "Hugo Rocks!" }}
-{{ return $a }}
-`, true},
- {"Expression", `
-{{ return add 32 }}
-`, true},
- }
-
- echo := func(in any) any {
- return in
- }
-
- funcs := template.FuncMap{
- "return": echo,
- "add": echo,
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- c := qt.New(t)
-
- templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
- c.Assert(err, qt.IsNil)
- ts := newTestTemplate(templ)
- ctx := newTemplateContext(
- ts,
- newTestTemplateLookup(ts),
- )
-
- _, err = ctx.applyTransformations(templ.Tree.Root)
-
- // Just check that it doesn't fail in this test. We have functional tests
- // in hugoblib.
- c.Assert(err, qt.IsNil)
- })
- }
-}
+++ /dev/null
-// Copyright 2018 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package tplimpl
-
-import (
- "fmt"
-
- "github.com/gohugoio/hugo/common/herrors"
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/identity"
-)
-
-var _ identity.Identity = (*templateInfo)(nil)
-
-type templateInfo struct {
- name string
- template string
- isText bool // HTML or plain text template.
- isEmbedded bool
-
- meta *hugofs.FileMeta
-}
-
-func (t templateInfo) IdentifierBase() string {
- return t.name
-}
-
-func (t templateInfo) Name() string {
- return t.name
-}
-
-func (t templateInfo) Filename() string {
- return t.meta.Filename
-}
-
-func (t templateInfo) IsZero() bool {
- return t.name == ""
-}
-
-func (t templateInfo) resolveType() templateType {
- return resolveTemplateType(t.name)
-}
-
-func (info templateInfo) errWithFileContext(what string, err error) error {
- err = fmt.Errorf(what+": %w", err)
- fe := herrors.NewFileErrorFromName(err, info.meta.Filename)
- f, err := info.meta.Open()
- if err != nil {
- return err
- }
- defer f.Close()
- return fe.UpdateContent(f, nil)
-}
-// Copyright 2017-present The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Portions Copyright The Go Authors.
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/tpl"
- template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-
- "github.com/gohugoio/hugo/deps"
-
- "github.com/gohugoio/hugo/tpl/internal"
-
- // Init the namespaces
- _ "github.com/gohugoio/hugo/tpl/cast"
- _ "github.com/gohugoio/hugo/tpl/collections"
- _ "github.com/gohugoio/hugo/tpl/compare"
- _ "github.com/gohugoio/hugo/tpl/crypto"
- _ "github.com/gohugoio/hugo/tpl/css"
- _ "github.com/gohugoio/hugo/tpl/data"
- _ "github.com/gohugoio/hugo/tpl/debug"
- _ "github.com/gohugoio/hugo/tpl/diagrams"
- _ "github.com/gohugoio/hugo/tpl/encoding"
- _ "github.com/gohugoio/hugo/tpl/fmt"
- _ "github.com/gohugoio/hugo/tpl/hash"
- _ "github.com/gohugoio/hugo/tpl/hugo"
- _ "github.com/gohugoio/hugo/tpl/images"
- _ "github.com/gohugoio/hugo/tpl/inflect"
- _ "github.com/gohugoio/hugo/tpl/js"
- _ "github.com/gohugoio/hugo/tpl/lang"
- _ "github.com/gohugoio/hugo/tpl/math"
- _ "github.com/gohugoio/hugo/tpl/openapi/openapi3"
- _ "github.com/gohugoio/hugo/tpl/os"
- _ "github.com/gohugoio/hugo/tpl/page"
- _ "github.com/gohugoio/hugo/tpl/partials"
- _ "github.com/gohugoio/hugo/tpl/path"
- _ "github.com/gohugoio/hugo/tpl/reflect"
- _ "github.com/gohugoio/hugo/tpl/resources"
- _ "github.com/gohugoio/hugo/tpl/safe"
- _ "github.com/gohugoio/hugo/tpl/site"
- _ "github.com/gohugoio/hugo/tpl/strings"
- _ "github.com/gohugoio/hugo/tpl/templates"
- _ "github.com/gohugoio/hugo/tpl/time"
- _ "github.com/gohugoio/hugo/tpl/transform"
- _ "github.com/gohugoio/hugo/tpl/urls"
- maps0 "maps"
)
var (
return ctx
}
-
-func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflect.Value) {
- funcs := createFuncMap(d)
- funcsv := make(map[string]reflect.Value)
-
- for k, v := range funcs {
- vv := reflect.ValueOf(v)
- funcsv[k] = vv
- }
-
- // Duplicate Go's internal funcs here for faster lookups.
- for k, v := range template.GoFuncs {
- if _, exists := funcsv[k]; !exists {
- vv, ok := v.(reflect.Value)
- if !ok {
- vv = reflect.ValueOf(v)
- }
- funcsv[k] = vv
- }
- }
-
- for k, v := range texttemplate.GoFuncs {
- if _, exists := funcsv[k]; !exists {
- funcsv[k] = v
- }
- }
-
- exeHelper := &templateExecHelper{
- watching: d.Conf.Watching(),
- funcs: funcsv,
- site: reflect.ValueOf(d.Site),
- siteParams: reflect.ValueOf(d.Site.Params()),
- }
-
- return texttemplate.NewExecuter(
- exeHelper,
- ), funcsv
-}
-
-func createFuncMap(d *deps.Deps) map[string]any {
- if d.TmplFuncMap != nil {
- return d.TmplFuncMap
- }
- funcMap := template.FuncMap{}
-
- nsMap := make(map[string]any)
- var onCreated []func(namespaces map[string]any)
-
- // Merge the namespace funcs
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns := nsf(d)
- if _, exists := funcMap[ns.Name]; exists {
- panic(ns.Name + " is a duplicate template func")
- }
- funcMap[ns.Name] = ns.Context
- contextV, err := ns.Context(context.Background())
- if err != nil {
- panic(err)
- }
- nsMap[ns.Name] = contextV
- for _, mm := range ns.MethodMappings {
- for _, alias := range mm.Aliases {
- if _, exists := funcMap[alias]; exists {
- panic(alias + " is a duplicate template func")
- }
- funcMap[alias] = mm.Method
- }
- }
-
- if ns.OnCreated != nil {
- onCreated = append(onCreated, ns.OnCreated)
- }
- }
-
- for _, f := range onCreated {
- f(nsMap)
- }
-
- if d.OverloadedTemplateFuncs != nil {
- maps0.Copy(funcMap, d.OverloadedTemplateFuncs)
- }
-
- d.TmplFuncMap = funcMap
-
- return d.TmplFuncMap
-}
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
--- /dev/null
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tplimpl
+
+// Increments on breaking changes.
+const TemplateVersion = 2
+
+// ParseInfo holds information about a parsed ntemplate.
+type ParseInfo struct {
+ // Set for shortcode templates with any {{ .Inner }}
+ IsInner bool
+
+ // Set for partials with a return statement.
+ HasReturn bool
+
+ // Config extracted from template.
+ Config ParseConfig
+}
+
+func (info ParseInfo) IsZero() bool {
+ return info.Config.Version == 0
+}
+
+// ParseConfig holds configuration extracted from the template.
+type ParseConfig struct {
+ Version int
+}
+
+var defaultParseConfig = ParseConfig{
+ Version: TemplateVersion,
+}
+
+var defaultParseInfo = ParseInfo{
+ Config: defaultParseConfig,
+}
+++ /dev/null
-// Copyright 2019 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package tplimpl
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
-)
-
-func TestNeedsBaseTemplate(t *testing.T) {
- c := qt.New(t)
-
- c.Assert(needsBaseTemplate(`{{ define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`{{define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`{{- define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`{{-define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`
-
- {{-define "main" }}
-
- `), qt.Equals, true)
- c.Assert(needsBaseTemplate(` {{ define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`
- {{ define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(` A {{ define "main" }}`), qt.Equals, false)
- c.Assert(needsBaseTemplate(` {{ printf "foo" }}`), qt.Equals, false)
- c.Assert(needsBaseTemplate(`{{/* comment */}} {{ define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(` {{/* comment */}} A {{ define "main" }}`), qt.Equals, false)
-}
--- /dev/null
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tplimpl
+
+import (
+ "github.com/gohugoio/hugo/resources/kinds"
+)
+
+const baseNameBaseof = "baseof"
+
+// This is used both as a key and in lookups.
+type TemplateDescriptor struct {
+ // Group 1.
+ Kind string // page, home, section, taxonomy, term (and only those)
+ Layout string // list, single, baseof, mycustomlayout.
+
+ // Group 2.
+ OutputFormat string // rss, csv ...
+ MediaType string // text/html, text/plain, ...
+ Lang string // en, nn, fr, ...
+
+ Variant1 string // contextual variant, e.g. "link" in render hooks."
+ Variant2 string // contextual variant, e.g. "id" in render.
+
+ // Misc.
+ LayoutMustMatch bool // If set, we only look for the exact layout.
+ IsPlainText bool // Whether this is a plain text template.
+}
+
+func (d *TemplateDescriptor) normalizeFromFile() {
+ // fmt.Println("normalizeFromFile", "kind:", d.Kind, "layout:", d.Layout, "of:", d.OutputFormat)
+
+ if d.Layout == d.OutputFormat {
+ d.Layout = ""
+ }
+
+ if d.Kind == kinds.KindTemporary {
+ d.Kind = ""
+ }
+
+ if d.Layout == d.Kind {
+ d.Layout = ""
+ }
+}
+
+type descriptorHandler struct {
+ opts StoreOptions
+}
+
+// Note that this in this setup is usually a descriptor constructed from a page,
+// so we want to find the best match for that page.
+func (s descriptorHandler) compareDescriptors(category Category, this, other TemplateDescriptor) weight {
+ if this.LayoutMustMatch && this.Layout != other.Layout {
+ return weightNoMatch
+ }
+
+ w := this.doCompare(category, other)
+ if w.w1 <= 0 {
+ if category == CategoryMarkup && (this.Variant1 == other.Variant1) && (this.Variant2 == other.Variant2 || this.Variant2 != "" && other.Variant2 == "") {
+ // See issue 13242.
+ if this.OutputFormat != other.OutputFormat && this.OutputFormat == s.opts.DefaultOutputFormat {
+ return w
+ }
+
+ w.w1 = 1
+ return w
+ }
+ }
+
+ return w
+}
+
+//lint:ignore ST1006 this vs other makes it easier to reason about.
+func (this TemplateDescriptor) doCompare(category Category, other TemplateDescriptor) weight {
+ w := weightNoMatch
+
+ // HTML in plain text is OK, but not the other way around.
+ if other.IsPlainText && !this.IsPlainText {
+ return w
+ }
+ if other.Kind != "" && other.Kind != this.Kind {
+ return w
+ }
+ if other.Layout != "" && other.Layout != layoutAll && other.Layout != this.Layout {
+ if isLayoutCustom(this.Layout) {
+ if this.Kind == "" {
+ this.Layout = ""
+ } else if this.Kind == kinds.KindPage {
+ this.Layout = layoutSingle
+ } else {
+ this.Layout = layoutList
+ }
+ }
+
+ // Test again.
+ if other.Layout != this.Layout {
+ return w
+ }
+ }
+ if other.Lang != "" && other.Lang != this.Lang {
+ return w
+ }
+
+ if other.OutputFormat != "" && other.OutputFormat != this.OutputFormat {
+ if this.MediaType != other.MediaType {
+ return w
+ }
+
+ // We want e.g. home page in amp output format (media type text/html) to
+ // find a template even if one isn't specified for that output format,
+ // when one exist for the html output format (same media type).
+ if category != CategoryBaseof && (this.Kind == "" || (this.Kind != other.Kind && this.Layout != other.Layout)) {
+ return w
+ }
+
+ // Continue.
+ }
+
+ // One example of variant1 and 2 is for render codeblocks:
+ // variant1=codeblock, variant2=go (language).
+ if other.Variant1 != "" && other.Variant1 != this.Variant1 {
+ return w
+ }
+
+ // If both are set and different, no match.
+ if other.Variant2 != "" && this.Variant2 != "" && other.Variant2 != this.Variant2 {
+ return w
+ }
+
+ const (
+ weightKind = 3 // page, home, section, taxonomy, term (and only those)
+ weightcustomLayout = 4 // custom layout (mylayout, set in e.g. front matter)
+ weightLayout = 2 // standard layouts (single,list,all)
+ weightOutputFormat = 2 // a configured output format (e.g. rss, html, json)
+ weightMediaType = 1 // a configured media type (e.g. text/html, text/plain)
+ weightLang = 1 // a configured language (e.g. en, nn, fr, ...)
+ weightVariant1 = 4 // currently used for render hooks, e.g. "link", "image"
+ weightVariant2 = 2 // currently used for render hooks, e.g. the language "go" in code blocks.
+
+ // We will use the values for group 2 and 3
+ // if the distance up to the template is shorter than
+ // the one we're comparing with.
+ // E.g for a page in /posts/mypage.md with the
+ // two templates /layouts/posts/single.html and /layouts/page.html,
+ // the first one is the best match even if the second one
+ // has a higher w1 value.
+ weight2Group1 = 1 // kind, standardl layout (single,list,all)
+ weight2Group2 = 2 // custom layout (mylayout)
+
+ weight3 = 1 // for media type, lang, output format.
+ )
+
+ // Now we now know that the other descriptor is a subset of this.
+ // Now calculate the weights.
+ w.w1++
+
+ if other.Kind != "" && other.Kind == this.Kind {
+ w.w1 += weightKind
+ w.w2 = weight2Group1
+ }
+
+ if other.Layout != "" && other.Layout == this.Layout || other.Layout == layoutAll {
+ if isLayoutCustom(this.Layout) {
+ w.w1 += weightcustomLayout
+ w.w2 = weight2Group2
+ } else {
+ w.w1 += weightLayout
+ w.w2 = weight2Group1
+ }
+ }
+
+ if other.Lang != "" && other.Lang == this.Lang {
+ w.w1 += weightLang
+ w.w3 += weight3
+ }
+
+ if other.OutputFormat != "" && other.OutputFormat == this.OutputFormat {
+ w.w1 += weightOutputFormat
+ w.w3 += weight3
+ }
+
+ if other.MediaType != "" && other.MediaType == this.MediaType {
+ w.w1 += weightMediaType
+ w.w3 += weight3
+ }
+
+ if other.Variant1 != "" && other.Variant1 == this.Variant1 {
+ w.w1 += weightVariant1
+ }
+
+ if other.Variant2 != "" && other.Variant2 == this.Variant2 {
+ w.w1 += weightVariant2
+ }
+ if other.Variant2 != "" && this.Variant2 == "" {
+ w.w1--
+ }
+
+ return w
+}
+
+func (d TemplateDescriptor) IsZero() bool {
+ return d == TemplateDescriptor{}
+}
+
+//lint:ignore ST1006 this vs other makes it easier to reason about.
+func (this TemplateDescriptor) isKindInLayout(layout string) bool {
+ if this.Kind == "" {
+ return true
+ }
+ if this.Kind != kinds.KindPage {
+ return layout != layoutSingle
+ }
+ return layout != layoutList
+}
--- /dev/null
+package tplimpl
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/resources/kinds"
+)
+
+func TestTemplateDescriptorCompare(t *testing.T) {
+ c := qt.New(t)
+
+ dh := descriptorHandler{
+ opts: StoreOptions{
+ OutputFormats: output.DefaultFormats,
+ DefaultOutputFormat: "html",
+ },
+ }
+
+ less := func(category Category, this, other1, other2 TemplateDescriptor) {
+ c.Helper()
+ result1 := dh.compareDescriptors(category, this, other1)
+ result2 := dh.compareDescriptors(category, this, other2)
+ c.Assert(result1.w1 < result2.w1, qt.IsTrue, qt.Commentf("%d < %d", result1, result2))
+ }
+
+ check := func(category Category, this, other TemplateDescriptor, less bool) {
+ c.Helper()
+ result := dh.compareDescriptors(category, this, other)
+ if less {
+ c.Assert(result.w1 < 0, qt.IsTrue, qt.Commentf("%d", result))
+ } else {
+ c.Assert(result.w1 >= 0, qt.IsTrue, qt.Commentf("%d", result))
+ }
+ }
+
+ check(
+
+ CategoryBaseof,
+ TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "404", MediaType: "text/html"},
+ TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "html", MediaType: "text/html"},
+ false,
+ )
+
+ check(
+ CategoryLayout,
+ TemplateDescriptor{Kind: "", Lang: "en", OutputFormat: "404", MediaType: "text/html"},
+ TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "alias", MediaType: "text/html"},
+ true,
+ )
+
+ less(
+ CategoryLayout,
+ TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "html"},
+ TemplateDescriptor{Layout: "list", OutputFormat: "html"},
+ TemplateDescriptor{Kind: kinds.KindHome, OutputFormat: "html"},
+ )
+
+ check(
+ CategoryLayout,
+ TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "html", MediaType: "text/html"},
+ TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "myformat", MediaType: "text/html"},
+ false,
+ )
+}
+
+// INFO timer: name resolveTemplate count 779 duration 5.482274ms average 7.037µs median 4µs
+func BenchmarkCompareDescriptors(b *testing.B) {
+ dh := descriptorHandler{
+ opts: StoreOptions{
+ OutputFormats: output.DefaultFormats,
+ DefaultOutputFormat: "html",
+ },
+ }
+
+ pairs := []struct {
+ d1, d2 TemplateDescriptor
+ }{
+ {
+ TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "404", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ },
+ {
+ TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ TemplateDescriptor{Kind: "", Layout: "list", OutputFormat: "", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ },
+ {
+ TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "alias", MediaType: "text/html", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ },
+ {
+ TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ TemplateDescriptor{Kind: "", Layout: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "nn", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+ },
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ for _, pair := range pairs {
+ _ = dh.compareDescriptors(CategoryLayout, pair.d1, pair.d2)
+ }
+ }
+}
--- /dev/null
+package tplimpl
+
+import (
+ "io"
+ "regexp"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/gohugoio/hugo/common/paths"
+ "github.com/gohugoio/hugo/tpl"
+ htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
+ texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
+)
+
+func (t *templateNamespace) readTemplateInto(templ *TemplInfo) error {
+ if err := func() error {
+ meta := templ.Fi.Meta()
+ f, err := meta.Open()
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ b, err := io.ReadAll(f)
+ if err != nil {
+ return err
+ }
+ templ.Content = removeLeadingBOM(string(b))
+ if !templ.NoBaseOf {
+ templ.NoBaseOf = !needsBaseTemplate(templ.Content)
+ }
+ return nil
+ }(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// The tweet and twitter shortcodes were deprecated in favor of the x shortcode
+// in v0.141.0. We can remove these aliases in v0.155.0 or later.
+var embeddedTemplatesAliases = map[string][]string{
+ "_shortcodes/twitter.html": {"_shortcodes/tweet.html"},
+}
+
+func (t *templateNamespace) parseTemplate(ti *TemplInfo) error {
+ if !ti.NoBaseOf || ti.Category == CategoryBaseof {
+ // Delay parsing until we have the base template.
+ return nil
+ }
+ pi := ti.PathInfo
+ name := pi.PathNoLeadingSlash()
+ if ti.isLegacyMapped {
+ // When mapping the old taxonomy structure to the new one, we may map the same path to multiple templates per kind.
+ // Append the kind here to make the name unique.
+ name += ("-" + ti.D.Kind)
+ }
+
+ var (
+ templ tpl.Template
+ err error
+ )
+
+ if ti.D.IsPlainText {
+ prototype := t.parseText
+ templ, err = prototype.New(name).Parse(ti.Content)
+ if err != nil {
+ return err
+ }
+ } else {
+ prototype := t.parseHTML
+ templ, err = prototype.New(name).Parse(ti.Content)
+ if err != nil {
+ return err
+ }
+
+ if ti.SubCategory == SubCategoryEmbedded {
+ // In Hugo 0.146.0 we moved the internal templates around.
+ // For the "_internal/twitter_cards.html" style templates, they
+ // were moved to the _partials directory.
+ // But we need to make them accessible from the old path for a while.
+ if pi.Type() == paths.TypePartial {
+ aliasName := strings.TrimPrefix(name, "_partials/")
+ aliasName = "_internal/" + aliasName
+ _, err = prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree)
+ if err != nil {
+ return err
+ }
+ }
+
+ // This was also possible before Hugo 0.146.0, but this should be deprecated.
+ if pi.Type() == paths.TypeShortcode {
+ aliasName := strings.TrimPrefix(name, "_shortcodes/")
+ aliasName = "_internal/shortcodes/" + aliasName
+ _, err = prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree)
+ if err != nil {
+ return err
+ }
+ }
+
+ }
+ }
+
+ ti.Template = templ
+
+ return nil
+}
+
+func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTemplateInfo) error {
+ tb := &TemplWithBaseApplied{
+ Overlay: overlay,
+ Base: base.Info,
+ }
+
+ base.Info.Overlays = append(base.Info.Overlays, overlay)
+
+ var templ tpl.Template
+ if overlay.D.IsPlainText {
+ tt := texttemplate.Must(t.parseText.Clone()).New(overlay.PathInfo.PathNoLeadingSlash())
+ var err error
+ tt, err = tt.Parse(base.Info.Content)
+ if err != nil {
+ return err
+ }
+ tt, err = tt.Parse(overlay.Content)
+ if err != nil {
+ return err
+ }
+ templ = tt
+ t.baseofTextClones = append(t.baseofTextClones, tt)
+ } else {
+ tt := htmltemplate.Must(t.parseHTML.CloneShallow()).New(overlay.PathInfo.PathNoLeadingSlash())
+ var err error
+ tt, err = tt.Parse(base.Info.Content)
+ if err != nil {
+ return err
+ }
+ tt, err = tt.Parse(overlay.Content)
+ if err != nil {
+ return err
+ }
+ templ = tt
+
+ t.baseofHtmlClones = append(t.baseofHtmlClones, tt)
+
+ }
+
+ tb.Template = &TemplInfo{
+ Template: templ,
+ Base: base.Info,
+ PathInfo: overlay.PathInfo,
+ Fi: overlay.Fi,
+ D: overlay.D,
+ NoBaseOf: true,
+ }
+
+ variants := overlay.BaseVariants.Get(base.Key)
+ if variants == nil {
+ variants = make(map[TemplateDescriptor]*TemplWithBaseApplied)
+ overlay.BaseVariants.Insert(base.Key, variants)
+ }
+ variants[base.Info.D] = tb
+ return nil
+}
+
+func (t *templateNamespace) templatesIn(in tpl.Template) []tpl.Template {
+ var templs []tpl.Template
+ if textt, ok := in.(*texttemplate.Template); ok {
+ for _, t := range textt.Templates() {
+ templs = append(templs, t)
+ }
+ }
+ if htmlt, ok := in.(*htmltemplate.Template); ok {
+ for _, t := range htmlt.Templates() {
+ templs = append(templs, t)
+ }
+ }
+ return templs
+}
+
+/*
+
+
+func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) {
+ if overlay.isText {
+ var (
+ templ = t.main.getPrototypeText(prototypeCloneIDBaseof).New(overlay.name)
+ err error
+ )
+
+ if !base.IsZero() {
+ templ, err = templ.Parse(base.template)
+ if err != nil {
+ return nil, base.errWithFileContext("text: base: parse failed", err)
+ }
+ }
+
+ templ, err = texttemplate.Must(templ.Clone()).Parse(overlay.template)
+ if err != nil {
+ return nil, overlay.errWithFileContext("text: overlay: parse failed", err)
+ }
+
+ // The extra lookup is a workaround, see
+ // * https://github.com/golang/go/issues/16101
+ // * https://github.com/gohugoio/hugo/issues/2549
+ // templ = templ.Lookup(templ.Name())
+
+ return templ, nil
+ }
+
+ var (
+ templ = t.main.getPrototypeHTML(prototypeCloneIDBaseof).New(overlay.name)
+ err error
+ )
+
+ if !base.IsZero() {
+ templ, err = templ.Parse(base.template)
+ if err != nil {
+ return nil, base.errWithFileContext("html: base: parse failed", err)
+ }
+ }
+
+ templ, err = htmltemplate.Must(templ.Clone()).Parse(overlay.template)
+ if err != nil {
+ return nil, overlay.errWithFileContext("html: overlay: parse failed", err)
+ }
+
+ // The extra lookup is a workaround, see
+ // * https://github.com/golang/go/issues/16101
+ // * https://github.com/gohugoio/hugo/issues/2549
+ templ = templ.Lookup(templ.Name())
+
+ return templ, err
+}
+
+*/
+
+var baseTemplateDefineRe = regexp.MustCompile(`^{{-?\s*define`)
+
+// needsBaseTemplate returns true if the first non-comment template block is a
+// define block.
+func needsBaseTemplate(templ string) bool {
+ idx := -1
+ inComment := false
+ for i := 0; i < len(templ); {
+ if !inComment && strings.HasPrefix(templ[i:], "{{/*") {
+ inComment = true
+ i += 4
+ } else if !inComment && strings.HasPrefix(templ[i:], "{{- /*") {
+ inComment = true
+ i += 6
+ } else if inComment && strings.HasPrefix(templ[i:], "*/}}") {
+ inComment = false
+ i += 4
+ } else if inComment && strings.HasPrefix(templ[i:], "*/ -}}") {
+ inComment = false
+ i += 6
+ } else {
+ r, size := utf8.DecodeRuneInString(templ[i:])
+ if !inComment {
+ if strings.HasPrefix(templ[i:], "{{") {
+ idx = i
+ break
+ } else if !unicode.IsSpace(r) {
+ break
+ }
+ }
+ i += size
+ }
+ }
+
+ if idx == -1 {
+ return false
+ }
+
+ return baseTemplateDefineRe.MatchString(templ[idx:])
+}
+
+func removeLeadingBOM(s string) string {
+ const bom = '\ufeff'
+
+ for i, r := range s {
+ if i == 0 && r != bom {
+ return s
+ }
+ if i > 0 {
+ return s[i:]
+ }
+ }
+
+ return s
+}
+
+type templateNamespace struct {
+ parseText *texttemplate.Template
+ parseHTML *htmltemplate.Template
+ prototypeText *texttemplate.Template
+ prototypeHTML *htmltemplate.Template
+
+ standaloneText *texttemplate.Template
+
+ baseofTextClones []*texttemplate.Template
+ baseofHtmlClones []*htmltemplate.Template
+}
+
+func (t *templateNamespace) createPrototypesParse() error {
+ if t.prototypeHTML == nil {
+ panic("prototypeHTML not set")
+ }
+ t.parseHTML = htmltemplate.Must(t.prototypeHTML.Clone())
+ t.parseText = texttemplate.Must(t.prototypeText.Clone())
+ return nil
+}
+
+func (t *templateNamespace) createPrototypes(init bool) error {
+ if init {
+ t.prototypeHTML = htmltemplate.Must(t.parseHTML.Clone())
+ t.prototypeText = texttemplate.Must(t.parseText.Clone())
+ }
+ // t.execHTML = htmltemplate.Must(t.parseHTML.Clone())
+ // t.execText = texttemplate.Must(t.parseText.Clone())
+
+ return nil
+}
+
+func newTemplateNamespace(funcs map[string]any) *templateNamespace {
+ return &templateNamespace{
+ parseHTML: htmltemplate.New("").Funcs(funcs),
+ parseText: texttemplate.New("").Funcs(funcs),
+ standaloneText: texttemplate.New("").Funcs(funcs),
+ }
+}
--- /dev/null
+package tplimpl
+
+import (
+ "bytes"
+ "context"
+ "embed"
+ "fmt"
+ "io"
+ "io/fs"
+ "iter"
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+ "regexp"
+ "sort"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/paths"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/hugofs/files"
+ "github.com/gohugoio/hugo/hugolib/doctree"
+ "github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/metrics"
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/resources/kinds"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/tpl"
+ htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
+ texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
+ "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
+ "github.com/spf13/afero"
+)
+
+const (
+ CategoryLayout Category = iota + 1
+ CategoryBaseof
+ CategoryMarkup
+ CategoryShortcode
+ CategoryPartial
+ // Internal categories
+ CategoryServer
+ CategoryHugo
+)
+
+const (
+ SubCategoryMain SubCategory = iota
+ SubCategoryEmbedded // Internal Hugo templates
+ SubCategoryInline // Inline partials
+)
+
+const (
+ containerMarkup = "_markup"
+ containerShortcodes = "_shortcodes"
+ shortcodesPathIdentifier = "/_shortcodes/"
+ containerPartials = "_partials"
+)
+
+const (
+ layoutAll = "all"
+ layoutList = "list"
+ layoutSingle = "single"
+)
+
+var (
+ _ identity.IdentityProvider = (*TemplInfo)(nil)
+ _ identity.IsProbablyDependentProvider = (*TemplInfo)(nil)
+ _ identity.IsProbablyDependencyProvider = (*TemplInfo)(nil)
+)
+
+const (
+ processingStateInitial processingState = iota
+ processingStateTransformed
+)
+
+// The identifiers may be truncated in the log, e.g.
+// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image"
+// We need this to identify position in templates with base templates applied.
+var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`)
+
+var weightNoMatch = weight{w1: -1}
+
+//
+//go:embed all:embedded/templates/*
+var embeddedTemplatesFs embed.FS
+
+func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) {
+ html, ok := opts.OutputFormats.GetByName("html")
+ if !ok {
+ panic("HTML output format not found")
+ }
+ s := &TemplateStore{
+ opts: opts,
+ siteOpts: siteOpts,
+ optsOrig: opts,
+ siteOptsOrig: siteOpts,
+ htmlFormat: html,
+ storeSite: configureSiteStorage(siteOpts, opts.Watching),
+ treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
+ treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
+ templatesByPath: maps.NewCache[string, *TemplInfo](),
+
+ // Note that the funcs passed below is just for name validation.
+ tns: newTemplateNamespace(siteOpts.TemplateFuncs),
+
+ dh: descriptorHandler{
+ opts: opts,
+ },
+ }
+
+ if err := s.init(); err != nil {
+ return nil, err
+ }
+ if err := s.insertTemplates(nil, false); err != nil {
+ return nil, err
+ }
+ if err := s.insertEmbedded(); err != nil {
+ return nil, err
+ }
+ if err := s.parseTemplates(); err != nil {
+ return nil, err
+ }
+ if err := s.extractInlinePartials(); err != nil {
+ return nil, err
+ }
+ if err := s.transformTemplates(); err != nil {
+ return nil, err
+ }
+ if err := s.tns.createPrototypes(true); err != nil {
+ return nil, err
+ }
+ if err := s.prepareTemplates(); err != nil {
+ return nil, err
+ }
+ return s, nil
+}
+
+//go:generate stringer -type Category
+
+type Category int
+
+type SiteOptions struct {
+ Site page.Site
+ TemplateFuncs map[string]any
+}
+
+type StoreOptions struct {
+ // The filesystem to use.
+ Fs afero.Fs
+
+ // The path parser to use.
+ PathParser *paths.PathParser
+
+ // Set when --enableTemplateMetrics is set.
+ Metrics metrics.Provider
+
+ // All configured output formats.
+ OutputFormats output.Formats
+
+ // All configured media types.
+ MediaTypes media.Types
+
+ // The default content language.
+ DefaultContentLanguage string
+
+ // The default output format.
+ DefaultOutputFormat string
+
+ // Taxonomy config.
+ TaxonomySingularPlural map[string]string
+
+ // Whether we are in watch or server mode.
+ Watching bool
+
+ // compiled.
+ legacyMappingTaxonomy map[string]legacyOrdinalMapping
+ legacyMappingTerm map[string]legacyOrdinalMapping
+ legacyMappingSection map[string]legacyOrdinalMapping
+}
+
+//go:generate stringer -type SubCategory
+
+type SubCategory int
+
+type TemplInfo struct {
+ // The category of this template.
+ Category Category
+
+ SubCategory SubCategory
+
+ // PathInfo info.
+ PathInfo *paths.Path
+
+ // Set when backed by a file.
+ Fi hugofs.FileMetaInfo
+
+ // The template content with any leading BOM removed.
+ Content string
+
+ // The parsed template.
+ // Note that any baseof template will be applied later.
+ Template tpl.Template
+
+ // If no baseof is needed, this will be set to true.
+ // E.g. shortcode templates do not need a baseof.
+ NoBaseOf bool
+
+ // If NoBaseOf is false, we will look for the final template in this tree.
+ BaseVariants *doctree.SimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]
+
+ // The template variants that are based on this template.
+ Overlays []*TemplInfo
+
+ // The base template used, if any.
+ Base *TemplInfo
+
+ // The descriptior that this template represents.
+ D TemplateDescriptor
+
+ // Parser state.
+ ParseInfo ParseInfo
+
+ // The execution counter for this template.
+ ExecutionCounter atomic.Uint64
+
+ // processing state.
+ state processingState
+ isLegacyMapped bool
+}
+
+func (ti *TemplInfo) BaseVariantsSeq() iter.Seq[*TemplWithBaseApplied] {
+ return func(yield func(*TemplWithBaseApplied) bool) {
+ ti.BaseVariants.Walk(func(key string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) {
+ for _, vv := range v {
+ if !yield(vv) {
+ return true, nil
+ }
+ }
+ return false, nil
+ })
+ }
+}
+
+func (t *TemplInfo) IdentifierBase() string {
+ if t.PathInfo == nil {
+ return t.Name()
+ }
+ return t.PathInfo.IdentifierBase()
+}
+
+func (t *TemplInfo) GetIdentity() identity.Identity {
+ return t
+}
+
+func (ti *TemplInfo) Name() string {
+ return ti.Template.Name()
+}
+
+func (ti *TemplInfo) Prepare() (*texttemplate.Template, error) {
+ return ti.Template.Prepare()
+}
+
+func (t *TemplInfo) IsProbablyDependency(other identity.Identity) bool {
+ return t.isProbablyTheSameIDAs(other)
+}
+
+func (t *TemplInfo) IsProbablyDependent(other identity.Identity) bool {
+ for _, overlay := range t.Overlays {
+ if overlay.isProbablyTheSameIDAs(other) {
+ return true
+ }
+ }
+ return t.isProbablyTheSameIDAs(other)
+}
+
+func (ti *TemplInfo) String() string {
+ if ti == nil {
+ return "<nil>"
+ }
+ return ti.PathInfo.String()
+}
+
+func (ti *TemplInfo) findBestMatchBaseof(s *TemplateStore, k1 string, slashCountK1 int, best *bestMatch) {
+ if ti.BaseVariants == nil {
+ return
+ }
+
+ ti.BaseVariants.WalkPath(k1, func(k2 string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) {
+ slashCountK2 := strings.Count(k2, "/")
+ distance := slashCountK1 - slashCountK2
+
+ for d, vv := range v {
+ weight := s.dh.compareDescriptors(CategoryBaseof, ti.D, d)
+ weight.distance = distance
+ if best.isBetter(weight, vv.Template) {
+ best.updateValues(weight, k2, d, vv.Template)
+ }
+ }
+ return false, nil
+ })
+}
+
+func (t *TemplInfo) isProbablyTheSameIDAs(other identity.Identity) bool {
+ if t.IdentifierBase() == other.IdentifierBase() {
+ return true
+ }
+
+ if t.Fi != nil && t.Fi.Meta().PathInfo != t.PathInfo {
+ return other.IdentifierBase() == t.Fi.Meta().PathInfo.IdentifierBase()
+ }
+
+ return false
+}
+
+type TemplWithBaseApplied struct {
+ // The template that's overlaid on top of the base template.
+ Overlay *TemplInfo
+ // The base template.
+ Base *TemplInfo
+ // This is the final template that can be used to render a page.
+ Template *TemplInfo
+}
+
+// TemplateQuery is used in LookupPagesLayout to find the best matching template.
+type TemplateQuery struct {
+ // The path to walk down to.
+ Path string
+
+ // The name to look for. Used for shortcode queries.
+ Name string
+
+ // The category to look in.
+ Category Category
+
+ // The template descriptor to match against.
+ Desc TemplateDescriptor
+
+ // Whether to even consider this candidate.
+ Consider func(candidate *TemplInfo) bool
+}
+
+func (q *TemplateQuery) init() {
+ if q.Desc.Kind == kinds.KindTemporary {
+ q.Desc.Kind = ""
+ } else if kinds.GetKindMain(q.Desc.Kind) == "" {
+ q.Desc.Kind = ""
+ }
+ if q.Desc.Layout == "" && q.Desc.Kind != "" {
+ if q.Desc.Kind == kinds.KindPage {
+ q.Desc.Layout = layoutSingle
+ } else {
+ q.Desc.Layout = layoutList
+ }
+ }
+
+ if q.Consider == nil {
+ q.Consider = func(match *TemplInfo) bool {
+ return true
+ }
+ }
+
+ q.Name = strings.ToLower(q.Name)
+
+ if q.Category == 0 {
+ panic("category not set")
+ }
+}
+
+type TemplateStore struct {
+ opts StoreOptions
+ siteOpts SiteOptions
+ htmlFormat output.Format
+
+ treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo]
+ treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
+ templatesByPath *maps.Cache[string, *TemplInfo]
+
+ dh descriptorHandler
+
+ // The template namespace.
+ tns *templateNamespace
+
+ // Site specific state.
+ // All above this is reused.
+ storeSite *storeSite
+
+ // For testing benchmarking.
+ optsOrig StoreOptions
+ siteOptsOrig SiteOptions
+}
+
+// NewFromOpts creates a new store with the same configuration as the original.
+// Used for testing/benchmarking.
+func (s *TemplateStore) NewFromOpts() (*TemplateStore, error) {
+ return NewStore(s.optsOrig, s.siteOptsOrig)
+}
+
+// In the previous implementation of base templates in Hugo, we parsed and applied these base templates on
+// request, e.g. in the middle of rendering. The idea was that we coulnd't know upfront which layoyt/base template
+// combination that would be used.
+// This, however, added a lot of complexity involving a careful dance of template cloning and parsing
+// (Go HTML tenplates cannot be parsed after any of the templates in the tree have been executed).
+// FindAllBaseTemplateCandidates finds all base template candidates for the given descriptor so we can apply them upfront.
+// In this setup we may end up with unused base templates, but not having to do the cloning should more than make up for that.
+func (s *TemplateStore) FindAllBaseTemplateCandidates(overlayKey string, desc TemplateDescriptor) []keyTemplateInfo {
+ var result []keyTemplateInfo
+ descBaseof := desc
+ s.treeMain.Walk(func(k string, v map[nodeKey]*TemplInfo) (bool, error) {
+ for _, vv := range v {
+ if vv.Category != CategoryBaseof {
+ continue
+ }
+
+ if vv.D.isKindInLayout(desc.Layout) && s.dh.compareDescriptors(CategoryBaseof, descBaseof, vv.D).w1 > 0 {
+ result = append(result, keyTemplateInfo{Key: k, Info: vv})
+ }
+ }
+ return false, nil
+ })
+
+ return result
+}
+
+func (t *TemplateStore) ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error {
+ defer func() {
+ ti.ExecutionCounter.Add(1)
+ if ti.Base != nil {
+ ti.Base.ExecutionCounter.Add(1)
+ }
+ }()
+
+ templ := ti.Template
+
+ if t.opts.Metrics != nil {
+ defer t.opts.Metrics.MeasureSince(templ.Name(), time.Now())
+ }
+
+ execErr := t.storeSite.executer.ExecuteWithContext(ctx, ti, wr, data)
+ if execErr != nil {
+ return t.addFileContext(ti, execErr)
+ }
+ return nil
+}
+
+func (t *TemplateStore) GetFunc(name string) (reflect.Value, bool) {
+ v, found := t.storeSite.execHelper.funcs[name]
+ return v, found
+}
+
+func (s *TemplateStore) GetIdentity(p string) identity.Identity {
+ p = paths.AddLeadingSlash(p)
+ v, found := s.templatesByPath.Get(p)
+ if !found {
+ return nil
+ }
+ return v.GetIdentity()
+}
+
+func (t *TemplateStore) LookupByPath(templatePath string) *TemplInfo {
+ v, _ := t.templatesByPath.Get(templatePath)
+ return v
+}
+
+var bestPool = sync.Pool{
+ New: func() any {
+ return &bestMatch{}
+ },
+}
+
+func (s *TemplateStore) getBest() *bestMatch {
+ v := bestPool.Get()
+ b := v.(*bestMatch)
+ b.defaultOutputformat = s.opts.DefaultOutputFormat
+ return b
+}
+
+func (s *TemplateStore) putBest(b *bestMatch) {
+ b.reset()
+ bestPool.Put(b)
+}
+
+func (s *TemplateStore) LookupPagesLayout(q TemplateQuery) *TemplInfo {
+ q.init()
+ key := s.key(q.Path)
+
+ slashCountKey := strings.Count(key, "/")
+ best1 := s.getBest()
+ defer s.putBest(best1)
+ s.findBestMatchWalkPath(q, key, slashCountKey, best1)
+ if best1.w.w1 <= 0 {
+ return nil
+ }
+ m := best1.templ
+ if m.NoBaseOf {
+ return m
+ }
+ best1.reset()
+ m.findBestMatchBaseof(s, key, slashCountKey, best1)
+ if best1.w.w1 <= 0 {
+ return nil
+ }
+ return best1.templ
+}
+
+func (s *TemplateStore) LookupPartial(pth string, desc TemplateDescriptor) *TemplInfo {
+ if desc.Layout != "" {
+ panic("shortcode template descriptor must not have a layout")
+ }
+ best := s.getBest()
+ defer s.putBest(best)
+ s.findBestMatchGet(s.key(path.Join(containerPartials, pth)), CategoryPartial, nil, desc, best)
+ return best.templ
+}
+
+func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo {
+ q.init()
+ k1 := s.key(q.Path)
+
+ slashCountK1 := strings.Count(k1, "/")
+
+ best := s.getBest()
+ defer s.putBest(best)
+
+ s.treeShortcodes.WalkPath(k1, func(k2 string, m map[string]map[TemplateDescriptor]*TemplInfo) (bool, error) {
+ slashCountK2 := strings.Count(k2, "/")
+ distance := slashCountK1 - slashCountK2
+
+ v, found := m[q.Name]
+ if !found {
+ return false, nil
+ }
+
+ for k, vv := range v {
+ if !q.Consider(vv) {
+ continue
+ }
+
+ weight := s.dh.compareDescriptors(q.Category, q.Desc, k)
+ weight.distance = distance
+ if best.isBetter(weight, vv) {
+ best.updateValues(weight, k2, k, vv)
+ }
+ }
+
+ return false, nil
+ })
+
+ // Any match will do.
+ return best.templ
+}
+
+// PrintDebug is for testing/debugging only.
+func (s *TemplateStore) PrintDebug(prefix string, category Category, w io.Writer) {
+ if w == nil {
+ w = os.Stdout
+ }
+
+ printOne := func(key string, vv *TemplInfo) {
+ level := strings.Count(key, "/")
+ if category != vv.Category {
+ return
+ }
+ s := strings.ReplaceAll(strings.TrimSpace(vv.Content), "\n", " ")
+ ts := fmt.Sprintf("kind: %q layout: %q content: %.30s", vv.D.Kind, vv.D.Layout, s)
+ fmt.Fprintf(w, "%s%s %s\n", strings.Repeat(" ", level), key, ts)
+ }
+ s.treeMain.WalkPrefix(prefix, func(key string, v map[nodeKey]*TemplInfo) (bool, error) {
+ for _, vv := range v {
+ printOne(key, vv)
+ }
+ return false, nil
+ })
+ s.treeShortcodes.WalkPrefix(prefix, func(key string, v map[string]map[TemplateDescriptor]*TemplInfo) (bool, error) {
+ for _, vv := range v {
+ for _, vv2 := range vv {
+ printOne(key, vv2)
+ }
+ }
+ return false, nil
+ })
+}
+
+// RefreshFiles refreshes this store for the files matching the given predicate.
+func (s *TemplateStore) RefreshFiles(include func(fi hugofs.FileMetaInfo) bool) error {
+ if err := s.tns.createPrototypesParse(); err != nil {
+ return err
+ }
+ if err := s.insertTemplates(include, true); err != nil {
+ return err
+ }
+ if err := s.parseTemplates(); err != nil {
+ return err
+ }
+ if err := s.extractInlinePartials(); err != nil {
+ return err
+ }
+ if err := s.transformTemplates(); err != nil {
+ return err
+ }
+ if err := s.tns.createPrototypes(false); err != nil {
+ return err
+ }
+ if err := s.prepareTemplates(); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (s *TemplateStore) HasTemplate(templatePath string) bool {
+ templatePath = paths.AddLeadingSlash(templatePath)
+ return s.templatesByPath.Contains(templatePath)
+}
+
+func (t *TemplateStore) TextLookup(name string) *TemplInfo {
+ templ := t.tns.standaloneText.Lookup(name)
+ if templ == nil {
+ return nil
+ }
+ return &TemplInfo{
+ Template: templ,
+ }
+}
+
+func (t *TemplateStore) TextParse(name, tpl string) (*TemplInfo, error) {
+ templ, err := t.tns.standaloneText.New(name).Parse(tpl)
+ if err != nil {
+ return nil, err
+ }
+ return &TemplInfo{
+ Template: templ,
+ }, nil
+}
+
+func (t *TemplateStore) UnusedTemplates() []*TemplInfo {
+ var unused []*TemplInfo
+
+ for vv := range t.templates() {
+ if vv.SubCategory != SubCategoryMain {
+ // Skip inline partials and internal templates.
+ continue
+ }
+ if vv.NoBaseOf {
+ if vv.ExecutionCounter.Load() == 0 {
+ unused = append(unused, vv)
+ }
+ } else {
+ for vvv := range vv.BaseVariantsSeq() {
+ if vvv.Template.ExecutionCounter.Load() == 0 {
+ unused = append(unused, vvv.Template)
+ }
+ }
+ }
+ }
+
+ sort.Sort(byPath(unused))
+ return unused
+}
+
+// WithSiteOpts creates a new store with the given site options.
+// This is used to create per site template store, all sharing the same templates,
+// but with a different template function execution context.
+func (s TemplateStore) WithSiteOpts(opts SiteOptions) *TemplateStore {
+ s.siteOpts = opts
+ s.storeSite = configureSiteStorage(opts, s.opts.Watching)
+ return &s
+}
+
+func (s *TemplateStore) findBestMatchGet(key string, category Category, consider func(candidate *TemplInfo) bool, desc TemplateDescriptor, best *bestMatch) {
+ key = strings.ToLower(key)
+
+ v := s.treeMain.Get(key)
+ if v == nil {
+ return
+ }
+
+ for k, vv := range v {
+ if vv.Category != category {
+ continue
+ }
+
+ if consider != nil && !consider(vv) {
+ continue
+ }
+
+ weight := s.dh.compareDescriptors(category, desc, k.d)
+ if best.isBetter(weight, vv) {
+ best.updateValues(weight, key, k.d, vv)
+ }
+ }
+}
+
+func (s *TemplateStore) findBestMatchWalkPath(q TemplateQuery, k1 string, slashCountK1 int, best *bestMatch) {
+ s.treeMain.WalkPath(k1, func(k2 string, v map[nodeKey]*TemplInfo) (bool, error) {
+ slashCountK2 := strings.Count(k2, "/")
+ distance := slashCountK1 - slashCountK2
+
+ for k, vv := range v {
+ if vv.Category != q.Category {
+ continue
+ }
+
+ if !q.Consider(vv) {
+ continue
+ }
+
+ weight := s.dh.compareDescriptors(q.Category, q.Desc, k.d)
+
+ weight.distance = distance
+ isBetter := best.isBetter(weight, vv)
+
+ if isBetter {
+ best.updateValues(weight, k2, k.d, vv)
+ }
+ }
+
+ return false, nil
+ })
+}
+
+func (t *TemplateStore) addDeferredTemplate(owner *TemplInfo, name string, n *parse.ListNode) error {
+ if _, found := t.templatesByPath.Get(name); found {
+ return nil
+ }
+
+ var templ tpl.Template
+
+ if owner.D.IsPlainText {
+ prototype := t.tns.parseText
+ tt, err := prototype.New(name).Parse("")
+ if err != nil {
+ return fmt.Errorf("failed to parse empty text template %q: %w", name, err)
+ }
+ tt.Tree.Root = n
+ templ = tt
+ } else {
+ prototype := t.tns.parseHTML
+ tt, err := prototype.New(name).Parse("")
+ if err != nil {
+ return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err)
+ }
+ tt.Tree.Root = n
+ templ = tt
+ }
+
+ t.templatesByPath.Set(name, &TemplInfo{
+ Fi: owner.Fi,
+ PathInfo: owner.PathInfo,
+ D: owner.D,
+ Template: templ,
+ })
+
+ return nil
+}
+
+func (s *TemplateStore) addFileContext(ti *TemplInfo, inerr error) error {
+ if ti.Fi == nil {
+ return inerr
+ }
+
+ identifiers := s.extractIdentifiers(inerr.Error())
+
+ checkFilename := func(fi hugofs.FileMetaInfo, inErr error) (error, bool) {
+ lineMatcher := func(m herrors.LineMatcher) int {
+ if m.Position.LineNumber != m.LineNumber {
+ return -1
+ }
+
+ for _, id := range identifiers {
+ if strings.Contains(m.Line, id) {
+ // We found the line, but return a 0 to signal to
+ // use the column from the error message.
+ return 0
+ }
+ }
+ return -1
+ }
+
+ f, err := fi.Meta().Open()
+ if err != nil {
+ return inErr, false
+ }
+ defer f.Close()
+
+ fe := herrors.NewFileErrorFromName(inErr, fi.Meta().Filename)
+ fe.UpdateContent(f, lineMatcher)
+
+ if !fe.ErrorContext().Position.IsValid() {
+ return inErr, false
+ }
+ return fe, true
+ }
+
+ inerr = fmt.Errorf("execute of template failed: %w", inerr)
+
+ if err, ok := checkFilename(ti.Fi, inerr); ok {
+ return err
+ }
+
+ if ti.Base != nil {
+ if err, ok := checkFilename(ti.Base.Fi, inerr); ok {
+ return err
+ }
+ }
+
+ return inerr
+}
+
+func (s *TemplateStore) extractIdentifiers(line string) []string {
+ m := identifiersRe.FindAllStringSubmatch(line, -1)
+ identifiers := make([]string, len(m))
+ for i := range m {
+ identifiers[i] = m[i][1]
+ }
+ return identifiers
+}
+
+func (s *TemplateStore) extractInlinePartials() error {
+ isPartialName := func(s string) bool {
+ return strings.HasPrefix(s, "partials/") || strings.HasPrefix(s, "_partials/")
+ }
+
+ p := s.tns
+ // We may find both inline and external partials in the current template namespaces,
+ // so only add the ones we have not seen before.
+ addIfNotSeen := func(isText bool, templs ...tpl.Template) error {
+ for _, templ := range templs {
+ if templ.Name() == "" || !isPartialName(templ.Name()) {
+ continue
+ }
+ name := templ.Name()
+ if !paths.HasExt(name) {
+ // Assume HTML. This in line with how the lookup works.
+ name = name + ".html"
+ }
+ if !strings.HasPrefix(name, "_") {
+ name = "_" + name
+ }
+ pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
+ ti, err := s.insertTemplate(pi, nil, false, s.treeMain)
+ if err != nil {
+ return err
+ }
+
+ if ti != nil {
+ ti.Template = templ
+ ti.NoBaseOf = true
+ ti.SubCategory = SubCategoryInline
+ ti.D.IsPlainText = isText
+ }
+
+ }
+ return nil
+ }
+ addIfNotSeen(false, p.templatesIn(p.parseHTML)...)
+ addIfNotSeen(true, p.templatesIn(p.parseText)...)
+
+ for _, t := range p.baseofHtmlClones {
+ if err := addIfNotSeen(false, p.templatesIn(t)...); err != nil {
+ return err
+ }
+ }
+ for _, t := range p.baseofTextClones {
+ if err := addIfNotSeen(true, p.templatesIn(t)...); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (s *TemplateStore) insertEmbedded() error {
+ return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d == nil || d.IsDir() || strings.HasPrefix(d.Name(), ".") {
+ return nil
+ }
+
+ templb, err := embeddedTemplatesFs.ReadFile(path)
+ if err != nil {
+ return err
+ }
+
+ // Get the newlines on Windows in line with how we had it back when we used Go Generate
+ // to write the templates to Go files.
+ templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
+ name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/")
+
+ insertOne := func(name, content string) error {
+ pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
+ var (
+ ti *TemplInfo
+ err error
+ )
+ if pi.Section() == containerShortcodes {
+ ti, err = s.insertShortcode(pi, nil, false, s.treeShortcodes)
+ if err != nil {
+ return err
+ }
+ } else {
+ ti, err = s.insertTemplate(pi, nil, false, s.treeMain)
+ if err != nil {
+ return err
+ }
+ }
+
+ if ti != nil {
+ // Currently none of the embedded templates need a baseof template.
+ ti.NoBaseOf = true
+ ti.Content = content
+ ti.SubCategory = SubCategoryEmbedded
+ }
+
+ return nil
+ }
+
+ if err := insertOne(name, templ); err != nil {
+ return err
+ }
+
+ if aliases, found := embeddedTemplatesAliases[name]; found {
+ for _, alias := range aliases {
+ if err := insertOne(alias, templ); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+ })
+}
+
+func (s *TemplateStore) setTemplateByPath(p string, ti *TemplInfo) {
+ s.templatesByPath.Set(p, ti)
+}
+
+func (s *TemplateStore) insertShortcode(pi *paths.Path, fi hugofs.FileMetaInfo, replace bool, tree doctree.Tree[map[string]map[TemplateDescriptor]*TemplInfo]) (*TemplInfo, error) {
+ k1, k2, _, d := s.toKeyCategoryAndDescriptor(pi)
+ m := tree.Get(k1)
+ if m == nil {
+ m = make(map[string]map[TemplateDescriptor]*TemplInfo)
+ tree.Insert(k1, m)
+ }
+
+ m1, found := m[k2]
+ if found {
+ if _, found := m1[d]; found {
+ if !replace {
+ return nil, nil
+ }
+ }
+ } else {
+ m1 = make(map[TemplateDescriptor]*TemplInfo)
+ m[k2] = m1
+ }
+
+ ti := &TemplInfo{
+ PathInfo: pi,
+ Fi: fi,
+ D: d,
+ Category: CategoryShortcode,
+ NoBaseOf: true,
+ }
+
+ m1[d] = ti
+
+ s.setTemplateByPath(pi.Path(), ti)
+
+ if fi != nil {
+ if pi2 := fi.Meta().PathInfo; pi2 != pi {
+ s.setTemplateByPath(pi2.Path(), ti)
+ }
+ }
+
+ return ti, nil
+}
+
+func (s *TemplateStore) insertTemplate(pi *paths.Path, fi hugofs.FileMetaInfo, replace bool, tree doctree.Tree[map[nodeKey]*TemplInfo]) (*TemplInfo, error) {
+ key, _, category, d := s.toKeyCategoryAndDescriptor(pi)
+
+ return s.insertTemplate2(pi, fi, key, category, d, replace, false, tree)
+}
+
+func (s *TemplateStore) insertTemplate2(
+ pi *paths.Path,
+ fi hugofs.FileMetaInfo,
+ key string,
+ category Category,
+ d TemplateDescriptor,
+ replace, isLegacyMapped bool,
+ tree doctree.Tree[map[nodeKey]*TemplInfo],
+) (*TemplInfo, error) {
+ if category == 0 {
+ panic("category not set")
+ }
+
+ m := tree.Get(key)
+ nk := nodeKey{c: category, d: d}
+
+ if m == nil {
+ m = make(map[nodeKey]*TemplInfo)
+ tree.Insert(key, m)
+ }
+
+ if !replace {
+ if v, found := m[nk]; found {
+ if len(pi.IdentifiersUnknown()) >= len(v.PathInfo.IdentifiersUnknown()) {
+ // e.g. /pages/home.foo.html and /pages/home.html where foo may be a valid language name in another site.
+ return nil, nil
+ }
+ }
+ }
+
+ ti := &TemplInfo{
+ PathInfo: pi,
+ Fi: fi,
+ D: d,
+ Category: category,
+ NoBaseOf: category > CategoryLayout,
+ isLegacyMapped: isLegacyMapped,
+ }
+
+ m[nk] = ti
+
+ if !isLegacyMapped {
+ s.setTemplateByPath(pi.Path(), ti)
+ if fi != nil {
+ if pi2 := fi.Meta().PathInfo; pi2 != pi {
+ s.setTemplateByPath(pi2.Path(), ti)
+ }
+ }
+ }
+
+ return ti, nil
+}
+
+func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) bool, replace bool) error {
+ if include == nil {
+ include = func(fi hugofs.FileMetaInfo) bool {
+ return true
+ }
+ }
+
+ // Set if we need to reset the base variants.
+ var (
+ resetBaseVariants bool
+ )
+
+ legacyOrdinalMappings := map[legacyTargetPathIdentifiers]legacyOrdinalMappingFi{}
+
+ walker := func(pth string, fi hugofs.FileMetaInfo) error {
+ piOrig := fi.Meta().PathInfo
+ if fi.IsDir() {
+ return nil
+ }
+
+ if !include(fi) {
+ return nil
+ }
+
+ // Convert any legacy value to new format.
+ fromLegacyPath := func(pi *paths.Path) *paths.Path {
+ p := pi.Path()
+ p = strings.TrimPrefix(p, "/_default")
+ if strings.HasPrefix(p, "/shortcodes") || strings.HasPrefix(p, "/partials") {
+ // Insert an underscore so it becomes /_shortcodes or /_partials.
+ p = "/_" + p[1:]
+ }
+
+ if strings.Contains(p, "-"+baseNameBaseof) {
+ // Before Hugo 0.146.0 we prepended one identifier (layout, type or kind) in front of the baseof keyword,
+ // and then separated with a hyphen before the baseof keyword.
+ // This identifier needs to be moved right after the baseof keyword and the hyphen removed, e.g.
+ // /docs/list-baseof.html => /docs/baseof.list.html.
+ dir, name := path.Split(p)
+ hyphenIdx := strings.Index(name, "-")
+ if hyphenIdx > 0 {
+ id := name[:hyphenIdx]
+ name = name[hyphenIdx+1+len(baseNameBaseof):]
+ if !strings.HasPrefix(name, ".") {
+ name = "." + name
+ }
+ p = path.Join(dir, baseNameBaseof+"."+id+name)
+ }
+ }
+ if p == pi.Path() {
+ return pi
+ }
+ return s.opts.PathParser.Parse(files.ComponentFolderLayouts, p)
+ }
+
+ pi := piOrig
+ var applyLegacyMapping bool
+ switch pi.Section() {
+ case containerPartials, containerShortcodes, containerMarkup:
+ // OK.
+ default:
+ applyLegacyMapping = true
+ pi = fromLegacyPath(pi)
+ }
+
+ if applyLegacyMapping {
+ handleMapping := func(m1 legacyOrdinalMapping) {
+ key := legacyTargetPathIdentifiers{
+ targetPath: m1.mapping.targetPath,
+ targetCategory: m1.mapping.targetCategory,
+ kind: m1.mapping.targetDesc.Kind,
+ lang: pi.Lang(),
+ ext: pi.Ext(),
+ outputFormat: pi.OutputFormat(),
+ }
+ if m2, ok := legacyOrdinalMappings[key]; ok {
+ if m1.ordinal < m2.m.ordinal {
+ // Higher up == better match.
+ legacyOrdinalMappings[key] = legacyOrdinalMappingFi{m1, fi}
+ }
+ } else {
+ legacyOrdinalMappings[key] = legacyOrdinalMappingFi{m1, fi}
+ }
+ }
+
+ if m1, ok := s.opts.legacyMappingTaxonomy[piOrig.PathBeforeLangAndOutputFormatAndExt()]; ok {
+ handleMapping(m1)
+ }
+
+ if m1, ok := s.opts.legacyMappingTerm[piOrig.PathBeforeLangAndOutputFormatAndExt()]; ok {
+ handleMapping(m1)
+ }
+
+ const (
+ sectionKindToken = "SECTIONKIND"
+ sectionToken = "THESECTION"
+ )
+
+ base := piOrig.PathBeforeLangAndOutputFormatAndExt()
+ identifiers := pi.IdentifiersUnknown()
+
+ // Tokens on e.g. form /SECTIONKIND/THESECTION
+ insertSectionTokens := func(section string, kindOnly bool) string {
+ s := base
+ if !kindOnly {
+ s = strings.Replace(s, section, sectionToken, 1)
+ }
+ s = strings.Replace(s, kinds.KindSection, sectionKindToken, 1)
+ return s
+ }
+
+ for _, section := range identifiers {
+ if section == baseNameBaseof {
+ continue
+ }
+ kindOnly := isLayoutStandard(section)
+ p := insertSectionTokens(section, kindOnly)
+ if m1, ok := s.opts.legacyMappingSection[p]; ok {
+ m1.mapping.targetPath = strings.Replace(m1.mapping.targetPath, sectionToken, section, 1)
+ handleMapping(m1)
+ }
+ }
+
+ }
+
+ if replace && pi.NameNoIdentifier() == baseNameBaseof {
+ // A baseof file has changed.
+ resetBaseVariants = true
+ }
+
+ var ti *TemplInfo
+ var err error
+ if pi.Type() == paths.TypeShortcode {
+ ti, err = s.insertShortcode(pi, fi, replace, s.treeShortcodes)
+ if err != nil || ti == nil {
+ return err
+ }
+ } else {
+ ti, err = s.insertTemplate(pi, fi, replace, s.treeMain)
+ if err != nil || ti == nil {
+ return err
+ }
+ }
+
+ if err := s.tns.readTemplateInto(ti); err != nil {
+ return err
+ }
+
+ return nil
+ }
+
+ if err := helpers.Walk(s.opts.Fs, "", walker); err != nil {
+ if !herrors.IsNotExist(err) {
+ return err
+ }
+ return nil
+ }
+
+ for k, v := range legacyOrdinalMappings {
+ targetPath := k.targetPath
+ m := v.m.mapping
+ fi := v.fi
+ pi := fi.Meta().PathInfo
+ outputFormat, mediaType := s.resolveOutputFormatAndOrMediaType(k.outputFormat, k.ext)
+ category := m.targetCategory
+ desc := m.targetDesc
+ desc.Kind = k.kind
+ desc.Lang = k.lang
+ desc.OutputFormat = outputFormat.Name
+ desc.IsPlainText = outputFormat.IsPlainText
+ desc.MediaType = mediaType.Type
+
+ ti, err := s.insertTemplate2(pi, fi, targetPath, category, desc, true, true, s.treeMain)
+ if err != nil {
+ return err
+ }
+ if ti == nil {
+ continue
+ }
+ ti.isLegacyMapped = true
+ if err := s.tns.readTemplateInto(ti); err != nil {
+ return err
+ }
+ }
+
+ if resetBaseVariants {
+ s.tns.baseofHtmlClones = nil
+ s.tns.baseofTextClones = nil
+ s.treeMain.Walk(func(key string, v map[nodeKey]*TemplInfo) (bool, error) {
+ for _, vv := range v {
+ if !vv.NoBaseOf {
+ vv.state = processingStateInitial
+ }
+ }
+ return false, nil
+ })
+ }
+
+ return nil
+}
+
+func (s *TemplateStore) key(dir string) string {
+ dir = paths.AddLeadingSlash(dir)
+ if dir == "/" {
+ return ""
+ }
+ return paths.TrimTrailing(dir)
+}
+
+func (s *TemplateStore) parseTemplates() error {
+ if err := func() error {
+ // Read and parse all templates.
+ for _, v := range s.treeMain.All() {
+ for _, vv := range v {
+ if vv.state == processingStateTransformed {
+ continue
+ }
+ if err := s.tns.parseTemplate(vv); err != nil {
+ return err
+ }
+ }
+ }
+
+ // Lookup and apply base templates where needed.
+ for key, v := range s.treeMain.All() {
+ for _, vv := range v {
+ if vv.state == processingStateTransformed {
+ continue
+ }
+ if !vv.NoBaseOf {
+ d := vv.D
+ // Find all compatible base templates.
+ baseTemplates := s.FindAllBaseTemplateCandidates(key, d)
+ if len(baseTemplates) == 0 {
+ // The regular expression used to detect if a template needs a base template has some
+ // rare false positives. Assume we don't need one.
+ vv.NoBaseOf = true
+ if err := s.tns.parseTemplate(vv); err != nil {
+ return err
+ }
+ continue
+ }
+ vv.BaseVariants = doctree.NewSimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]()
+
+ for _, base := range baseTemplates {
+ if err := s.tns.applyBaseTemplate(vv, base); err != nil {
+ return err
+ }
+ }
+
+ }
+ }
+ }
+
+ return nil
+ }(); err != nil {
+ return err
+ }
+
+ // Prese shortcodes.
+ for _, v := range s.treeShortcodes.All() {
+ for _, vv := range v {
+ for _, vvv := range vv {
+ if vvv.state == processingStateTransformed {
+ continue
+ }
+ if err := s.tns.parseTemplate(vvv); err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+// prepareTemplates prepares all templates for execution.
+func (s *TemplateStore) prepareTemplates() error {
+ for t := range s.templates() {
+ if t.Category == CategoryBaseof {
+ continue
+ }
+ if _, err := t.Prepare(); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// TemplateDescriptorFromPath returns a template descriptor from the given path.
+// This is currently used in partial lookups only.
+func (s *TemplateStore) TemplateDescriptorFromPath(pth string) (string, TemplateDescriptor) {
+ var (
+ mt media.Type
+ of output.Format
+ )
+
+ // Common cases.
+ dotCount := strings.Count(pth, ".")
+ if dotCount <= 1 {
+ if dotCount == 0 {
+ // Asume HTML.
+ of, mt = s.resolveOutputFormatAndOrMediaType("html", "")
+ } else {
+ pth = strings.TrimPrefix(pth, "/")
+ ext := path.Ext(pth)
+ pth = strings.TrimSuffix(pth, ext)
+ ext = ext[1:]
+ of, mt = s.resolveOutputFormatAndOrMediaType("", ext)
+ }
+ } else {
+ path := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth)
+ pth = path.PathNoIdentifier()
+ of, mt = s.resolveOutputFormatAndOrMediaType(path.OutputFormat(), path.Ext())
+ }
+
+ return pth, TemplateDescriptor{
+ OutputFormat: of.Name,
+ MediaType: mt.Type,
+ IsPlainText: of.IsPlainText,
+ }
+}
+
+// resolveOutputFormatAndOrMediaType resolves the output format and/or media type
+// based on the given output format suffix and media type suffix.
+// Either of the suffixes can be empty, and the function will try to find a match
+// based on the other suffix. If both are empty, the function will return zero values.
+func (s *TemplateStore) resolveOutputFormatAndOrMediaType(ofs, mns string) (output.Format, media.Type) {
+ var outputFormat output.Format
+ var mediaType media.Type
+
+ if ofs != "" {
+ if of, found := s.opts.OutputFormats.GetByName(ofs); found {
+ outputFormat = of
+ mediaType = of.MediaType
+ }
+ }
+
+ if mns != "" && mediaType.IsZero() {
+ if of, found := s.opts.OutputFormats.GetBySuffix(mns); found {
+ outputFormat = of
+ mediaType = of.MediaType
+ } else {
+ if mt, _, found := s.opts.MediaTypes.GetFirstBySuffix(mns); found {
+ mediaType = mt
+ if outputFormat.IsZero() {
+ // For e.g. index.xml we will in the default confg now have the application/rss+xml media type.
+ // Try a last time to find the output format using the SubType as the name.
+ // As to template resolution, this value is currently only used to
+ // decide if this is a text or HTML template.
+ outputFormat, _ = s.opts.OutputFormats.GetByName(mt.SubType)
+ }
+ }
+ }
+ }
+
+ return outputFormat, mediaType
+}
+
+func (s *TemplateStore) templates() iter.Seq[*TemplInfo] {
+ return func(yield func(*TemplInfo) bool) {
+ for _, v := range s.treeMain.All() {
+ for _, vv := range v {
+ if !vv.NoBaseOf {
+ for vvv := range vv.BaseVariantsSeq() {
+ if !yield(vvv.Template) {
+ return
+ }
+ }
+ } else {
+ if !yield(vv) {
+ return
+ }
+ }
+ }
+ }
+ for _, v := range s.treeShortcodes.All() {
+ for _, vv := range v {
+ for _, vvv := range vv {
+ if !yield(vvv) {
+ return
+ }
+ }
+ }
+ }
+ }
+}
+
+func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, string, Category, TemplateDescriptor) {
+ k1 := p.Dir()
+ k2 := ""
+
+ outputFormat, mediaType := s.resolveOutputFormatAndOrMediaType(p.OutputFormat(), p.Ext())
+ nameNoIdentifier := p.NameNoIdentifier()
+
+ var layout string
+ unknownids := p.IdentifiersUnknown()
+ if p.Type() == paths.TypeShortcode {
+ if len(unknownids) > 1 {
+ // The name is the last identifier.
+ layout = unknownids[len(unknownids)-2]
+ }
+ } else if len(unknownids) > 0 {
+ // Pick the last, closest to the base name.
+ layout = unknownids[len(unknownids)-1]
+ }
+
+ d := TemplateDescriptor{
+ Lang: p.Lang(),
+ OutputFormat: p.OutputFormat(),
+ MediaType: mediaType.Type,
+ Kind: p.Kind(),
+ Layout: layout,
+ IsPlainText: outputFormat.IsPlainText,
+ }
+
+ d.normalizeFromFile()
+
+ section := p.Section()
+
+ var category Category
+ switch p.Type() {
+ case paths.TypeShortcode:
+ category = CategoryShortcode
+ case paths.TypePartial:
+ category = CategoryPartial
+ case paths.TypeMarkup:
+ category = CategoryMarkup
+ }
+
+ if category == 0 {
+ if nameNoIdentifier == baseNameBaseof {
+ category = CategoryBaseof
+ } else {
+ switch section {
+ case "_hugo":
+ category = CategoryHugo
+ case "_server":
+ category = CategoryServer
+ default:
+ category = CategoryLayout
+ }
+ }
+ }
+
+ if category == CategoryPartial {
+ d.Layout = ""
+ k1 = p.PathNoIdentifier()
+ }
+
+ if category == CategoryShortcode {
+ k1 = p.PathNoIdentifier()
+ parts := strings.Split(k1, "/"+containerShortcodes+"/")
+ k1 = parts[0]
+ if len(parts) > 1 {
+ k2 = parts[1]
+ }
+ k1 = s.key(k1)
+ }
+
+ // Legacy layout for home page.
+ if d.Layout == "index" {
+ if d.Kind == "" {
+ d.Kind = kinds.KindHome
+ }
+ d.Layout = ""
+ }
+
+ if d.Layout == d.Kind {
+ d.Layout = ""
+ }
+
+ k1 = strings.TrimPrefix(k1, "/_default")
+ if k1 == "/" {
+ k1 = ""
+ }
+
+ if category == CategoryMarkup {
+ // We store all template nodes for a given directory on the same level.
+ k1 = strings.TrimSuffix(k1, "/_markup")
+ parts := strings.Split(d.Layout, "-")
+ if len(parts) < 2 {
+ panic("markup template must have at least 2 parts")
+ }
+ // Either 2 or 3 parts, e.g. render-codeblock-go.
+ d.Variant1 = parts[1]
+ if len(parts) > 2 {
+ d.Variant2 = parts[2]
+ }
+ d.Layout = "" // This allows using page layout as part of the key for lookups.
+ }
+
+ return k1, k2, category, d
+}
+
+func (s *TemplateStore) transformTemplates() error {
+ lookup := func(name string, in *TemplInfo) *TemplInfo {
+ if in.D.IsPlainText {
+ templ := in.Template.(*texttemplate.Template).Lookup(name)
+ if templ != nil {
+ return &TemplInfo{
+ Template: templ,
+ }
+ }
+ } else {
+ templ := in.Template.(*htmltemplate.Template).Lookup(name)
+ if templ != nil {
+ return &TemplInfo{
+ Template: templ,
+ }
+ }
+ }
+
+ return nil
+ }
+
+ for vv := range s.templates() {
+ if vv.state == processingStateTransformed {
+ continue
+ }
+ vv.state = processingStateTransformed
+ if vv.Category == CategoryBaseof {
+ continue
+ }
+ if !vv.NoBaseOf {
+ for vvv := range vv.BaseVariantsSeq() {
+ tctx, err := applyTemplateTransformers(vvv.Template, lookup)
+ if err != nil {
+ return err
+ }
+
+ for name, node := range tctx.deferNodes {
+ if err := s.addDeferredTemplate(vvv.Overlay, name, node); err != nil {
+ return err
+ }
+ }
+ }
+ } else {
+ tctx, err := applyTemplateTransformers(vv, lookup)
+ if err != nil {
+ return err
+ }
+
+ for name, node := range tctx.deferNodes {
+ if err := s.addDeferredTemplate(vv, name, node); err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+func (s *TemplateStore) init() error {
+ // Before Hugo 0.146 we had a very elaborate template lookup system, especially for
+ // terms and taxonomies. This is a way of preserving backwards compatibility
+ // by mapping old paths into the new tree.
+ s.opts.legacyMappingTaxonomy = make(map[string]legacyOrdinalMapping)
+ s.opts.legacyMappingTerm = make(map[string]legacyOrdinalMapping)
+ s.opts.legacyMappingSection = make(map[string]legacyOrdinalMapping)
+
+ // Placeholders.
+ const singular = "SINGULAR"
+ const plural = "PLURAL"
+
+ replaceTokens := func(s, singularv, pluralv string) string {
+ s = strings.Replace(s, singular, singularv, -1)
+ s = strings.Replace(s, plural, pluralv, -1)
+ return s
+ }
+
+ hasSingularOrPlural := func(s string) bool {
+ return strings.Contains(s, singular) || strings.Contains(s, plural)
+ }
+
+ expand := func(v layoutLegacyMapping) []layoutLegacyMapping {
+ var result []layoutLegacyMapping
+
+ if hasSingularOrPlural(v.sourcePath) || hasSingularOrPlural(v.target.targetPath) {
+ for s, p := range s.opts.TaxonomySingularPlural {
+ target := v.target
+ target.targetPath = replaceTokens(target.targetPath, s, p)
+ vv := replaceTokens(v.sourcePath, s, p)
+ result = append(result, layoutLegacyMapping{sourcePath: vv, target: target})
+ }
+ } else {
+ result = append(result, v)
+ }
+ return result
+ }
+
+ expandSections := func(v layoutLegacyMapping) []layoutLegacyMapping {
+ var result []layoutLegacyMapping
+ result = append(result, v)
+ baseofVariant := v
+ baseofVariant.sourcePath += "-" + baseNameBaseof
+ baseofVariant.target.targetCategory = CategoryBaseof
+ result = append(result, baseofVariant)
+ return result
+ }
+
+ var terms []layoutLegacyMapping
+ for _, v := range legacyTermMappings {
+ terms = append(terms, expand(v)...)
+ }
+ var taxonomies []layoutLegacyMapping
+ for _, v := range legacyTaxonomyMappings {
+ taxonomies = append(taxonomies, expand(v)...)
+ }
+ var sections []layoutLegacyMapping
+ for _, v := range legacySectionMappings {
+ sections = append(sections, expandSections(v)...)
+ }
+
+ for i, m := range terms {
+ s.opts.legacyMappingTerm[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
+ }
+ for i, m := range taxonomies {
+ s.opts.legacyMappingTaxonomy[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
+ }
+ for i, m := range sections {
+ s.opts.legacyMappingSection[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
+ }
+
+ return nil
+}
+
+type TemplateStoreProvider interface {
+ GetTemplateStore() *TemplateStore
+}
+
+type TextTemplatHandler interface {
+ ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error
+ TextLookup(name string) *TemplInfo
+ TextParse(name, tpl string) (*TemplInfo, error)
+}
+
+type bestMatch struct {
+ templ *TemplInfo
+ desc TemplateDescriptor
+ w weight
+ key string
+
+ // settings.
+ defaultOutputformat string
+}
+
+func (best *bestMatch) reset() {
+ best.templ = nil
+ best.w = weight{}
+ best.desc = TemplateDescriptor{}
+ best.key = ""
+}
+
+func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool {
+ if best.templ == nil {
+ // Anything is better than nothing.
+ return true
+ }
+ if w.w1 <= 0 {
+ if best.w.w1 <= 0 {
+ return ti.PathInfo.Path() < best.templ.PathInfo.Path()
+ }
+ return false
+ }
+
+ if best.w.w1 > 0 {
+ currentBestIsEmbedded := best.templ.SubCategory == SubCategoryEmbedded
+ if currentBestIsEmbedded {
+ if ti.SubCategory != SubCategoryEmbedded {
+ return true
+ }
+ } else {
+ if ti.SubCategory == SubCategoryEmbedded {
+ // Prefer user provided template.
+ return false
+ }
+ }
+ }
+
+ if w.distance < best.w.distance {
+ if w.w2 < best.w.w2 {
+ return false
+ }
+ if w.w3 < best.w.w3 {
+ return false
+ }
+ } else {
+ if w.w1 < best.w.w1 {
+ return false
+ }
+ }
+
+ if w.isEqualWeights(best.w) {
+ // Tie breakers.
+ if w.distance < best.w.distance {
+ return true
+ }
+
+ if ti.D.Layout != "" && best.desc.Layout != "" {
+ return ti.D.Layout != layoutAll
+ }
+
+ return w.distance < best.w.distance || ti.PathInfo.Path() < best.templ.PathInfo.Path()
+ }
+
+ return true
+}
+
+func (best *bestMatch) updateValues(w weight, key string, k TemplateDescriptor, vv *TemplInfo) {
+ best.w = w
+ best.templ = vv
+ best.desc = k
+ best.key = key
+}
+
+type byPath []*TemplInfo
+
+func (a byPath) Len() int { return len(a) }
+func (a byPath) Less(i, j int) bool {
+ return a[i].PathInfo.Path() < a[j].PathInfo.Path()
+}
+
+func (a byPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+
+type keyTemplateInfo struct {
+ Key string
+ Info *TemplInfo
+}
+
+type nodeKey struct {
+ c Category
+ d TemplateDescriptor
+}
+
+type processingState int
+
+// the parts of a template store that's set per site.
+type storeSite struct {
+ opts SiteOptions
+ execHelper *templateExecHelper
+ executer texttemplate.Executer
+}
+
+type weight struct {
+ w1 int
+ w2 int
+ w3 int
+ distance int
+}
+
+func (w weight) isEqualWeights(other weight) bool {
+ return w.w1 == other.w1 && w.w2 == other.w2 && w.w3 == other.w3
+}
+
+func isLayoutCustom(s string) bool {
+ if s == "" || isLayoutStandard(s) {
+ return false
+ }
+ return true
+}
+
+func isLayoutStandard(s string) bool {
+ switch s {
+ case layoutAll, layoutList, layoutSingle:
+ return true
+ default:
+ return false
+ }
+}
+
+func configureSiteStorage(opts SiteOptions, watching bool) *storeSite {
+ funcsv := make(map[string]reflect.Value)
+
+ for k, v := range opts.TemplateFuncs {
+ vv := reflect.ValueOf(v)
+ funcsv[k] = vv
+ }
+
+ // Duplicate Go's internal funcs here for faster lookups.
+ for k, v := range htmltemplate.GoFuncs {
+ if _, exists := funcsv[k]; !exists {
+ vv, ok := v.(reflect.Value)
+ if !ok {
+ vv = reflect.ValueOf(v)
+ }
+ funcsv[k] = vv
+ }
+ }
+
+ for k, v := range texttemplate.GoFuncs {
+ if _, exists := funcsv[k]; !exists {
+ funcsv[k] = v
+ }
+ }
+
+ s := &storeSite{
+ opts: opts,
+ execHelper: &templateExecHelper{
+ watching: watching,
+ funcs: funcsv,
+ site: reflect.ValueOf(opts.Site),
+ siteParams: reflect.ValueOf(opts.Site.Params()),
+ },
+ }
+
+ s.executer = texttemplate.NewExecuter(s.execHelper)
+
+ return s
+}
--- /dev/null
+package tplimpl_test
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/resources/kinds"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
+)
+
+// Old as in before Hugo v0.146.0.
+func TestLayoutsOldSetup(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[languages]
+[languages.en]
+title = "Title in English"
+weight = 1
+[languages.nn]
+title = "Tittel på nynorsk"
+weight = 2
+-- layouts/index.html --
+Home.
+{{ template "_internal/twitter_cards.html" . }}
+-- layouts/_default/single.html --
+Single.
+-- layouts/_default/single.nn.html --
+Single NN.
+-- layouts/_default/list.html --
+List HTML.
+-- layouts/docs/list-baseof.html --
+Docs Baseof List HTML.
+{{ block "main" . }}Docs Baseof List HTML main block.{{ end }}
+-- layouts/docs/list.section.html --
+{{ define "main" }}
+Docs List HTML.
+{{ end }}
+-- layouts/_default/list.json --
+List JSON.
+-- layouts/_default/list.rss.xml --
+List RSS.
+-- layouts/_default/list.nn.rss.xml --
+List NN RSS.
+-- layouts/_default/baseof.html --
+Base.
+-- layouts/partials/mypartial.html --
+Partial.
+-- layouts/shortcodes/myshortcode.html --
+Shortcode.
+-- content/docs/p1.md --
+---
+title: "P1"
+---
+
+ `
+
+ b := hugolib.Test(t, files)
+
+ // b.DebugPrint("", tplimpl.CategoryBaseof)
+
+ b.AssertFileContent("public/en/docs/index.html", "Docs Baseof List HTML.\n\nDocs List HTML.")
+}
+
+func TestLayoutsOldSetupBaseofPrefix(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/_default/layout1-baseof.html --
+Baseof layout1. {{ block "main" . }}{{ end }}
+-- layouts/_default/layout2-baseof.html --
+Baseof layout2. {{ block "main" . }}{{ end }}
+-- layouts/_default/layout1.html --
+{{ define "main" }}Layout1. {{ .Title }}{{ end }}
+-- layouts/_default/layout2.html --
+{{ define "main" }}Layout2. {{ .Title }}{{ end }}
+-- content/p1.md --
+---
+title: "P1"
+layout: "layout1"
+---
+-- content/p2.md --
+---
+title: "P2"
+layout: "layout2"
+---
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "Baseof layout1. Layout1. P1")
+ b.AssertFileContent("public/p2/index.html", "Baseof layout2. Layout2. P2")
+}
+
+func TestLayoutsOldSetupTaxonomyAndTerm(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+[taxonomies]
+cat = 'cats'
+dog = 'dogs'
+# Templates for term taxonomy, old setup.
+-- layouts/dogs/terms.html --
+Dogs Terms. Most specific taxonomy template.
+-- layouts/taxonomy/terms.html --
+Taxonomy Terms. Down the list.
+# Templates for term term, old setup.
+-- layouts/dogs/term.html --
+Dogs Term. Most specific term template.
+-- layouts/term/term.html --
+Term Term. Down the list.
+-- layouts/dogs/max/list.html --
+max: {{ .Title }}
+-- layouts/_default/list.html --
+Default list.
+-- layouts/_default/single.html --
+Default single.
+-- content/p1.md --
+---
+title: "P1"
+dogs: ["luna", "daisy", "max"]
+---
+
+`
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ b.AssertLogContains("! WARN")
+
+ b.AssertFileContent("public/dogs/index.html", "Dogs Terms. Most specific taxonomy template.")
+ b.AssertFileContent("public/dogs/luna/index.html", "Dogs Term. Most specific term template.")
+ b.AssertFileContent("public/dogs/max/index.html", "max: Max") // layouts/dogs/max/list.html wins over layouts/term/term.html because of distance.
+}
+
+func TestLayoutsOldSetupCustomRSS(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "page"]
+[outputs]
+home = ["rss"]
+-- layouts/_default/list.rss.xml --
+List RSS.
+`
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.xml", "List RSS.")
+}
+
+var newSetupTestSites = `
+-- hugo.toml --
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[languages]
+[languages.en]
+title = "Title in English"
+weight = 1
+[languages.nn]
+title = "Tittel på nynorsk"
+weight = 2
+[languages.fr]
+title = "Titre en français"
+weight = 3
+
+[outputs]
+home = ["html", "rss", "redir"]
+
+[outputFormats]
+[outputFormats.redir]
+mediatype = "text/plain"
+baseName = "_redirects"
+isPlainText = true
+-- layouts/404.html --
+{{ define "main" }}
+404.
+{{ end }}
+-- layouts/home.html --
+{{ define "main" }}
+Home: {{ .Title }}|{{ .Content }}|
+Inline Partial: {{ partial "my-inline-partial.html" . }}
+{{ end }}
+{{ define "hero" }}
+Home hero.
+{{ end }}
+{{ define "partials/my-inline-partial.html" }}
+{{ $value := 32 }}
+{{ return $value }}
+{{ end }}
+-- layouts/index.redir --
+Redir.
+-- layouts/single.html --
+{{ define "main" }}
+Single needs base.
+{{ end }}
+-- layouts/foo/bar/single.html --
+{{ define "main" }}
+Single sub path.
+{{ end }}
+-- layouts/_markup/render-codeblock.html --
+Render codeblock.
+-- layouts/_markup/render-blockquote.html --
+Render blockquote.
+-- layouts/_markup/render-codeblock-go.html --
+ Render codeblock go.
+-- layouts/_markup/render-link.html --
+Link: {{ .Destination | safeURL }}
+-- layouts/foo/baseof.html --
+Base sub path.{{ block "main" . }}{{ end }}
+-- layouts/foo/bar/baseof.page.html --
+Base sub path.{{ block "main" . }}{{ end }}
+-- layouts/list.html --
+{{ define "main" }}
+List needs base.
+{{ end }}
+-- layouts/section.html --
+Section.
+-- layouts/mysectionlayout.section.fr.amp.html --
+Section with layout.
+-- layouts/baseof.html --
+Base.{{ block "main" . }}{{ end }}
+Hero:{{ block "hero" . }}{{ end }}:
+{{ with (templates.Defer (dict "key" "global")) }}
+Defer Block.
+{{ end }}
+-- layouts/baseof.fr.html --
+Base fr.{{ block "main" . }}{{ end }}
+-- layouts/baseof.term.html --
+Base term.
+-- layouts/baseof.section.fr.amp.html --
+Base with identifiers.{{ block "main" . }}{{ end }}
+-- layouts/partials/mypartial.html --
+Partial. {{ partial "_inline/my-inline-partial-in-partial-with-no-ext" . }}
+{{ define "partials/_inline/my-inline-partial-in-partial-with-no-ext" }}
+Partial in partial.
+{{ end }}
+-- layouts/partials/returnfoo.html --
+{{ $v := "foo" }}
+{{ return $v }}
+-- layouts/shortcodes/myshortcode.html --
+Shortcode. {{ partial "mypartial.html" . }}|return:{{ partial "returnfoo.html" . }}|
+-- content/_index.md --
+---
+title: Home sweet home!
+---
+
+{{< myshortcode >}}
+
+> My blockquote.
+
+
+Markdown link: [Foo](/foo)
+-- content/p1.md --
+---
+title: "P1"
+---
+-- content/foo/bar/index.md --
+---
+title: "Foo Bar"
+---
+
+{{< myshortcode >}}
+
+-- content/single-list.md --
+---
+title: "Single List"
+layout: "list"
+---
+
+`
+
+func TestLayoutsType(t *testing.T) {
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+-- layouts/list.html --
+List.
+-- layouts/mysection/single.html --
+mysection/single|{{ .Title }}
+-- layouts/mytype/single.html --
+mytype/single|{{ .Title }}
+-- content/mysection/_index.md --
+-- content/mysection/mysubsection/_index.md --
+-- content/mysection/mysubsection/p1.md --
+---
+title: "P1"
+---
+-- content/mysection/mysubsection/p2.md --
+---
+title: "P2"
+type: "mytype"
+---
+
+`
+
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ b.AssertLogContains("! WARN")
+
+ b.AssertFileContent("public/mysection/mysubsection/p1/index.html", "mysection/single|P1")
+ b.AssertFileContent("public/mysection/mysubsection/p2/index.html", "mytype/single|P2")
+}
+
+// New, as in from Hugo v0.146.0.
+func TestLayoutsNewSetup(t *testing.T) {
+ const numIterations = 1
+ for range numIterations {
+
+ b := hugolib.Test(t, newSetupTestSites, hugolib.TestOptWarn())
+
+ b.AssertLogContains("! WARN")
+
+ b.AssertFileContent("public/en/index.html",
+ "Base.\nHome: Home sweet home!|",
+ "|Shortcode.\n|",
+ "<p>Markdown link: Link: /foo</p>",
+ "|return:foo|",
+ "Defer Block.",
+ "Home hero.",
+ "Render blockquote.",
+ )
+
+ b.AssertFileContent("public/en/p1/index.html", "Base.\nSingle needs base.\n\nHero::\n\nDefer Block.")
+ b.AssertFileContent("public/en/404.html", "404.")
+ b.AssertFileContent("public/nn/404.html", "404.")
+ b.AssertFileContent("public/fr/404.html", "404.")
+
+ }
+}
+
+func TestHomeRSSAndHTMLWithHTMLOnlyShortcode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+[outputs]
+home = ["html", "rss"]
+-- layouts/home.html --
+Home: {{ .Title }}|{{ .Content }}|
+-- layouts/single.html --
+Single: {{ .Title }}|{{ .Content }}|
+-- layouts/shortcodes/myshortcode.html --
+Myshortcode: Count: {{ math.Counter }}|
+-- content/p1.md --
+---
+title: "P1"
+---
+
+{{< myshortcode >}}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "Single: P1|Myshortcode: Count: 1|")
+ b.AssertFileContent("public/index.xml", "Myshortcode: Count: 1")
+}
+
+func TestHomeRSSAndHTMLWithHTMLOnlyRenderHook(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+[outputs]
+home = ["html", "rss"]
+-- layouts/home.html --
+Home: {{ .Title }}|{{ .Content }}|
+-- layouts/single.html --
+Single: {{ .Title }}|{{ .Content }}|
+-- layouts/_markup/render-link.html --
+Render Link: {{ math.Counter }}|
+-- content/p1.md --
+---
+title: "P1"
+---
+
+Link: [Foo](/foo)
+`
+
+ for range 2 {
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.xml", "Link: Render Link: 1|")
+ b.AssertFileContent("public/p1/index.html", "Single: P1|<p>Link: Render Link: 1|<")
+ }
+}
+
+func TestRenderCodeblockSpecificity(t *testing.T) {
+ files := `
+-- hugo.toml --
+-- layouts/_markup/render-codeblock.html --
+Render codeblock.|{{ .Inner }}|
+-- layouts/_markup/render-codeblock-go.html --
+Render codeblock go.|{{ .Inner }}|
+-- layouts/single.html --
+{{ .Title }}|{{ .Content }}|
+-- content/p1.md --
+---
+title: "P1"
+---
+
+§§§
+Basic
+§§§
+
+§§§ go
+Go
+§§§
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "P1|Render codeblock.|Basic|Render codeblock go.|Go|")
+}
+
+func TestPrintUnusedTemplates(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- config.toml --
+baseURL = 'http://example.com/'
+printUnusedTemplates=true
+-- content/p1.md --
+---
+title: "P1"
+---
+{{< usedshortcode >}}
+-- layouts/baseof.html --
+{{ block "main" . }}{{ end }}
+-- layouts/baseof.json --
+{{ block "main" . }}{{ end }}
+-- layouts/index.html --
+{{ define "main" }}FOO{{ end }}
+-- layouts/_default/single.json --
+-- layouts/_default/single.html --
+{{ define "main" }}MAIN{{ end }}
+-- layouts/post/single.html --
+{{ define "main" }}MAIN{{ end }}
+-- layouts/_partials/usedpartial.html --
+-- layouts/_partials/unusedpartial.html --
+-- layouts/_shortcodes/usedshortcode.html --
+{{ partial "usedpartial.html" }}
+-- layouts/shortcodes/unusedshortcode.html --
+
+ `
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ NeedsOsFS: true,
+ },
+ )
+ b.Build()
+
+ unused := b.H.GetTemplateStore().UnusedTemplates()
+ var names []string
+ for _, tmpl := range unused {
+ if fi := tmpl.Fi; fi != nil {
+ names = append(names, fi.Meta().PathInfo.PathNoLeadingSlash())
+ }
+ }
+ b.Assert(len(unused), qt.Equals, 5, qt.Commentf("%#v", names))
+ b.Assert(names, qt.DeepEquals, []string{"_partials/unusedpartial.html", "shortcodes/unusedshortcode.html", "baseof.json", "post/single.html", "_default/single.json"})
+}
+
+func TestCreateManyTemplateStores(t *testing.T) {
+ t.Parallel()
+ b := hugolib.Test(t, newSetupTestSites)
+ store := b.H.TemplateStore
+
+ for range 70 {
+ newStore, err := store.NewFromOpts()
+ b.Assert(err, qt.IsNil)
+ b.Assert(newStore, qt.Not(qt.IsNil))
+ }
+}
+
+func BenchmarkLookupPagesLayout(b *testing.B) {
+ files := `
+-- hugo.toml --
+-- layouts/single.html --
+{{ define "main" }}
+ Main.
+{{ end }}
+-- layouts/baseof.html --
+baseof: {{ block "main" . }}{{ end }}
+-- layouts/foo/bar/single.html --
+{{ define "main" }}
+ Main.
+{{ end }}
+
+`
+ bb := hugolib.Test(b, files)
+ store := bb.H.TemplateStore
+
+ b.ResetTimer()
+ b.Run("Single root", func(b *testing.B) {
+ q := tplimpl.TemplateQuery{
+ Path: "/baz",
+ Category: tplimpl.CategoryLayout,
+ Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, Layout: "single", OutputFormat: "html"},
+ }
+ for i := 0; i < b.N; i++ {
+ store.LookupPagesLayout(q)
+ }
+ })
+
+ b.Run("Single sub folder", func(b *testing.B) {
+ q := tplimpl.TemplateQuery{
+ Path: "/foo/bar",
+ Category: tplimpl.CategoryLayout,
+ Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, Layout: "single", OutputFormat: "html"},
+ }
+ for i := 0; i < b.N; i++ {
+ store.LookupPagesLayout(q)
+ }
+ })
+}
+
+func BenchmarkNewTemplateStore(b *testing.B) {
+ bb := hugolib.Test(b, newSetupTestSites)
+ store := bb.H.TemplateStore
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ newStore, err := store.NewFromOpts()
+ if err != nil {
+ b.Fatal(err)
+ }
+ if newStore == nil {
+ b.Fatal("newStore is nil")
+ }
+ }
+}
+
+func TestLayoutsLookupVariants(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[outputs]
+home = ["html", "rss"]
+page = ["html", "rss", "amp"]
+section = ["html", "rss"]
+
+[languages]
+[languages.en]
+title = "Title in English"
+weight = 1
+[languages.nn]
+title = "Tittel på nynorsk"
+weight = 2
+-- layouts/list.xml --
+layouts/list.xml
+-- layouts/_shortcodes/myshortcode.html --
+layouts/shortcodes/myshortcode.html
+-- layouts/foo/bar/_shortcodes/myshortcode.html --
+layouts/foo/bar/_shortcodes/myshortcode.html
+-- layouts/_markup/render-codeblock.html --
+layouts/_markup/render-codeblock.html|{{ .Type }}|
+-- layouts/_markup/render-codeblock-go.html --
+layouts/_markup/render-codeblock-go.html|{{ .Type }}|
+-- layouts/single.xml --
+layouts/single.xml
+-- layouts/single.rss.xml --
+layouts/single.rss.xml
+-- layouts/single.nn.rss.xml --
+layouts/single.nn.rss.xml
+-- layouts/list.html --
+layouts/list.html
+-- layouts/single.html --
+layouts/single.html
+{{ .Content }}
+-- layouts/mylayout.html --
+layouts/mylayout.html
+-- layouts/mylayout.nn.html --
+layouts/mylayout.nn.html
+-- layouts/foo/single.rss.xml --
+layouts/foo/single.rss.xml
+-- layouts/foo/single.amp.html --
+layouts/foo/single.amp.html
+-- layouts/foo/bar/page.html --
+layouts/foo/bar/page.html
+-- layouts/foo/bar/baz/single.html --
+layouts/foo/bar/baz/single.html
+{{ .Content }}
+-- layouts/qux/mylayout.html --
+layouts/qux/mylayout.html
+-- layouts/qux/single.xml --
+layouts/qux/single.xml
+-- layouts/qux/mylayout.section.html --
+layouts/qux/mylayout.section.html
+-- content/p.md --
+---
+---
+§§§
+code
+§§§
+
+§§§ go
+code
+§§§
+
+{{< myshortcode >}}
+-- content/foo/p.md --
+-- content/foo/p.nn.md --
+-- content/foo/bar/p.md --
+-- content/foo/bar/withmylayout.md --
+---
+layout: mylayout
+---
+-- content/foo/bar/_index.md --
+-- content/foo/bar/baz/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/qux/p.md --
+-- content/qux/_index.md --
+---
+layout: mylayout
+---
+-- content/qux/quux/p.md --
+-- content/qux/quux/withmylayout.md --
+---
+layout: mylayout
+---
+-- content/qux/quux/withmylayout.nn.md --
+---
+layout: mylayout
+---
+
+
+`
+
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ // s := b.H.Sites[0].TemplateStore
+ // s.PrintDebug("", tplimpl.CategoryLayout, os.Stdout)
+
+ b.AssertLogContains("! WARN")
+
+ // Single pages.
+ // output format: html.
+ b.AssertFileContent("public/en/p/index.html", "layouts/single.html",
+ "layouts/_markup/render-codeblock.html|",
+ "layouts/_markup/render-codeblock-go.html|go|",
+ "layouts/shortcodes/myshortcode.html",
+ )
+ b.AssertFileContent("public/en/foo/p/index.html", "layouts/single.html")
+ b.AssertFileContent("public/en/foo/bar/p/index.html", "layouts/foo/bar/page.html")
+ b.AssertFileContent("public/en/foo/bar/withmylayout/index.html", "layouts/mylayout.html")
+ b.AssertFileContent("public/en/foo/bar/baz/p/index.html", "layouts/foo/bar/baz/single.html", "layouts/foo/bar/_shortcodes/myshortcode.html")
+ b.AssertFileContent("public/en/qux/quux/withmylayout/index.html", "layouts/qux/mylayout.html")
+ // output format: amp.
+ b.AssertFileContent("public/en/amp/p/index.html", "layouts/single.html")
+ b.AssertFileContent("public/en/amp/foo/p/index.html", "layouts/foo/single.amp.html")
+ // output format: rss.
+ b.AssertFileContent("public/en/p/index.xml", "layouts/single.rss.xml")
+ b.AssertFileContent("public/en/foo/p/index.xml", "layouts/foo/single.rss.xml")
+ b.AssertFileContent("public/nn/foo/p/index.xml", "layouts/single.nn.rss.xml")
+
+ // Note: There is qux/single.xml that's closer, but the one in the root is used becaulse of the output format match.
+ b.AssertFileContent("public/en/qux/p/index.xml", "layouts/single.rss.xml")
+
+ // Note.
+ b.AssertFileContent("public/nn/qux/quux/withmylayout/index.html", "layouts/mylayout.nn.html")
+
+ // Section pages.
+ // output format: html.
+ b.AssertFileContent("public/en/foo/index.html", "layouts/list.html")
+ b.AssertFileContent("public/en/qux/index.html", "layouts/qux/mylayout.section.html")
+ // output format: rss.
+ b.AssertFileContent("public/en/foo/index.xml", "layouts/list.xml")
+}
+
+func TestLookupShortcodeDepth(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/_shortcodes/myshortcode.html --
+layouts/_shortcodes/myshortcode.html
+-- layouts/foo/_shortcodes/myshortcode.html --
+layouts/foo/_shortcodes/myshortcode.html
+-- layouts/single.html --
+{{ .Content }}|
+-- content/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/bar/p.md --
+---
+---
+{{< myshortcode >}}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p/index.html", "layouts/_shortcodes/myshortcode.html")
+ b.AssertFileContent("public/foo/p/index.html", "layouts/foo/_shortcodes/myshortcode.html")
+ b.AssertFileContent("public/foo/bar/p/index.html", "layouts/foo/_shortcodes/myshortcode.html")
+}
+
+func TestLookupShortcodeLayout(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/_shortcodes/myshortcode.single.html --
+layouts/_shortcodes/myshortcode.single.html
+-- layouts/_shortcodes/myshortcode.list.html --
+layouts/_shortcodes/myshortcode.list.html
+-- layouts/single.html --
+{{ .Content }}|
+-- layouts/list.html --
+{{ .Content }}|
+-- content/_index.md --
+---
+---
+{{< myshortcode >}}
+-- content/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/bar/p.md --
+---
+---
+{{< myshortcode >}}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p/index.html", "layouts/_shortcodes/myshortcode.single.html")
+ b.AssertFileContent("public/index.html", "layouts/_shortcodes/myshortcode.list.html")
+}
+
+func TestLayoutAll(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/single.html --
+Single.
+-- layouts/all.html --
+All.
+-- content/p1.md --
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "Single.")
+ b.AssertFileContent("public/index.html", "All.")
+}
+
+func TestLayoutAllNested(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['rss','sitemap','taxonomy','term']
+-- content/s1/p1.md --
+---
+title: p1
+---
+-- content/s2/p2.md --
+---
+title: p2
+---
+-- layouts/single.html --
+layouts/single.html
+-- layouts/list.html --
+layouts/list.html
+-- layouts/s1/all.html --
+layouts/s1/all.html
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "layouts/list.html")
+ b.AssertFileContent("public/s1/index.html", "layouts/s1/all.html")
+ b.AssertFileContent("public/s1/p1/index.html", "layouts/s1/all.html")
+ b.AssertFileContent("public/s2/index.html", "layouts/list.html")
+ b.AssertFileContent("public/s2/p2/index.html", "layouts/single.html")
+}
+
+func TestPartialHTML(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/all.html --
+<html>
+<head>
+{{ partial "css.html" .}}
+</head>
+</html>
+-- layouts/partials/css.html --
+<link rel="stylesheet" href="/css/style.css">
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "<link rel=\"stylesheet\" href=\"/css/style.css\">")
+}
+
+// Issue #13515
+func TestPrintPathWarningOnDotRemoval(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+printPathWarnings = true
+-- content/v0.124.0.md --
+-- content/v0.123.0.md --
+-- layouts/all.html --
+All.
+-- layouts/_default/single.html --
+{{ .Title }}|
+`
+
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ b.AssertLogContains("Duplicate content path")
+}
--- /dev/null
+package tplimpl
+
+import (
+ "errors"
+ "fmt"
+ "slices"
+ "strings"
+
+ "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
+
+ htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
+ texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
+
+ "github.com/gohugoio/hugo/common/hashing"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/mitchellh/mapstructure"
+)
+
+type templateTransformContext struct {
+ visited map[string]bool
+ templateNotFound map[string]bool
+ deferNodes map[string]*parse.ListNode
+ lookupFn func(name string, in *TemplInfo) *TemplInfo
+
+ // The last error encountered.
+ err error
+
+ // Set when we're done checking for config header.
+ configChecked bool
+
+ t *TemplInfo
+
+ // Store away the return node in partials.
+ returnNode *parse.CommandNode
+}
+
+func (c templateTransformContext) getIfNotVisited(name string) *TemplInfo {
+ if c.visited[name] {
+ return nil
+ }
+ c.visited[name] = true
+ templ := c.lookupFn(name, c.t)
+ if templ == nil {
+ // This may be a inline template defined outside of this file
+ // and not yet parsed. Unusual, but it happens.
+ // Store the name to try again later.
+ c.templateNotFound[name] = true
+ }
+
+ return templ
+}
+
+func newTemplateTransformContext(
+ t *TemplInfo,
+ lookupFn func(name string, in *TemplInfo) *TemplInfo,
+) *templateTransformContext {
+ return &templateTransformContext{
+ t: t,
+ lookupFn: lookupFn,
+ visited: make(map[string]bool),
+ templateNotFound: make(map[string]bool),
+ deferNodes: make(map[string]*parse.ListNode),
+ }
+}
+
+func applyTemplateTransformers(
+ t *TemplInfo,
+ lookupFn func(name string, in *TemplInfo) *TemplInfo,
+) (*templateTransformContext, error) {
+ if t == nil {
+ return nil, errors.New("expected template, but none provided")
+ }
+
+ c := newTemplateTransformContext(t, lookupFn)
+ c.t.ParseInfo = defaultParseInfo
+ tree := getParseTree(t.Template)
+ if tree == nil {
+ panic(fmt.Errorf("template %s not parsed", t))
+ }
+
+ _, err := c.applyTransformations(tree.Root)
+
+ if err == nil && c.returnNode != nil {
+ // This is a partial with a return statement.
+ c.t.ParseInfo.HasReturn = true
+ tree.Root = c.wrapInPartialReturnWrapper(tree.Root)
+ }
+
+ return c, err
+}
+
+func getParseTree(templ tpl.Template) *parse.Tree {
+ if text, ok := templ.(*texttemplate.Template); ok {
+ return text.Tree
+ }
+ return templ.(*htmltemplate.Template).Tree
+}
+
+const (
+ // We parse this template and modify the nodes in order to assign
+ // the return value of a partial to a contextWrapper via Set. We use
+ // "range" over a one-element slice so we can shift dot to the
+ // partial's argument, Arg, while allowing Arg to be falsy.
+ partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ range (slice .Arg) }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
+
+ doDeferTempl = `{{ doDefer ("PLACEHOLDER1") ("PLACEHOLDER2") }}`
+)
+
+var (
+ partialReturnWrapper *parse.ListNode
+ doDefer *parse.ListNode
+)
+
+func init() {
+ templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
+ if err != nil {
+ panic(err)
+ }
+ partialReturnWrapper = templ.Tree.Root
+
+ templ, err = texttemplate.New("").Funcs(texttemplate.FuncMap{"doDefer": func(string, string) string { return "" }}).Parse(doDeferTempl)
+ if err != nil {
+ panic(err)
+ }
+ doDefer = templ.Tree.Root
+}
+
+// wrapInPartialReturnWrapper copies and modifies the parsed nodes of a
+// predefined partial return wrapper to insert those of a user-defined partial.
+func (c *templateTransformContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
+ wrapper := partialReturnWrapper.CopyList()
+ rangeNode := wrapper.Nodes[2].(*parse.RangeNode)
+ retn := rangeNode.List.Nodes[0]
+ setCmd := retn.(*parse.ActionNode).Pipe.Cmds[0]
+ setPipe := setCmd.Args[1].(*parse.PipeNode)
+ // Replace PLACEHOLDER with the real return value.
+ // Note that this is a PipeNode, so it will be wrapped in parens.
+ setPipe.Cmds = []*parse.CommandNode{c.returnNode}
+ rangeNode.List.Nodes = append(n.Nodes, retn)
+
+ return wrapper
+}
+
+// applyTransformations do 2 things:
+// 1) Parses partial return statement.
+// 2) Tracks template (partial) dependencies and some other info.
+func (c *templateTransformContext) applyTransformations(n parse.Node) (bool, error) {
+ switch x := n.(type) {
+ case *parse.ListNode:
+ if x != nil {
+ c.applyTransformationsToNodes(x.Nodes...)
+ }
+ case *parse.ActionNode:
+ c.applyTransformationsToNodes(x.Pipe)
+ case *parse.IfNode:
+ c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
+ case *parse.WithNode:
+ c.handleDefer(x)
+ c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
+ case *parse.RangeNode:
+ c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
+ case *parse.TemplateNode:
+ subTempl := c.getIfNotVisited(x.Name)
+ if subTempl != nil {
+ c.applyTransformationsToNodes(getParseTree(subTempl.Template).Root)
+ }
+ case *parse.PipeNode:
+ c.collectConfig(x)
+ for i, cmd := range x.Cmds {
+ keep, _ := c.applyTransformations(cmd)
+ if !keep {
+ x.Cmds = slices.Delete(x.Cmds, i, i+1)
+ }
+ }
+
+ case *parse.CommandNode:
+ c.collectInner(x)
+ keep := c.collectReturnNode(x)
+
+ for _, elem := range x.Args {
+ switch an := elem.(type) {
+ case *parse.PipeNode:
+ c.applyTransformations(an)
+ }
+ }
+ return keep, c.err
+ }
+
+ return true, c.err
+}
+
+func (c *templateTransformContext) handleDefer(withNode *parse.WithNode) {
+ if len(withNode.Pipe.Cmds) != 1 {
+ return
+ }
+ cmd := withNode.Pipe.Cmds[0]
+ if len(cmd.Args) != 1 {
+ return
+ }
+ idArg := cmd.Args[0]
+
+ p, ok := idArg.(*parse.PipeNode)
+ if !ok {
+ return
+ }
+
+ if len(p.Cmds) != 1 {
+ return
+ }
+
+ cmd = p.Cmds[0]
+
+ if len(cmd.Args) != 2 {
+ return
+ }
+
+ idArg = cmd.Args[0]
+
+ id, ok := idArg.(*parse.ChainNode)
+ if !ok || len(id.Field) != 1 || id.Field[0] != "Defer" {
+ return
+ }
+ if id2, ok := id.Node.(*parse.IdentifierNode); !ok || id2.Ident != "templates" {
+ return
+ }
+
+ deferArg := cmd.Args[1]
+ cmd.Args = []parse.Node{idArg}
+
+ l := doDefer.CopyList()
+ n := l.Nodes[0].(*parse.ActionNode)
+
+ inner := withNode.List.CopyList()
+ s := inner.String()
+ if strings.Contains(s, "resources.PostProcess") {
+ c.err = errors.New("resources.PostProcess cannot be used in a deferred template")
+ return
+ }
+ innerHash := hashing.XxHashFromStringHexEncoded(s)
+ deferredID := tpl.HugoDeferredTemplatePrefix + innerHash
+
+ c.deferNodes[deferredID] = inner
+ withNode.List = l
+
+ n.Pipe.Cmds[0].Args[1].(*parse.PipeNode).Cmds[0].Args[0].(*parse.StringNode).Text = deferredID
+ n.Pipe.Cmds[0].Args[2] = deferArg
+}
+
+func (c *templateTransformContext) applyTransformationsToNodes(nodes ...parse.Node) {
+ for _, node := range nodes {
+ c.applyTransformations(node)
+ }
+}
+
+func (c *templateTransformContext) hasIdent(idents []string, ident string) bool {
+ return slices.Contains(idents, ident)
+}
+
+// collectConfig collects and parses any leading template config variable declaration.
+// This will be the first PipeNode in the template, and will be a variable declaration
+// on the form:
+//
+// {{ $_hugo_config:= `{ "version": 1 }` }}
+func (c *templateTransformContext) collectConfig(n *parse.PipeNode) {
+ if c.t.Category != CategoryShortcode {
+ return
+ }
+ if c.configChecked {
+ return
+ }
+ c.configChecked = true
+
+ if len(n.Decl) != 1 || len(n.Cmds) != 1 {
+ // This cannot be a config declaration
+ return
+ }
+
+ v := n.Decl[0]
+
+ if len(v.Ident) == 0 || v.Ident[0] != "$_hugo_config" {
+ return
+ }
+
+ cmd := n.Cmds[0]
+
+ if len(cmd.Args) == 0 {
+ return
+ }
+
+ if s, ok := cmd.Args[0].(*parse.StringNode); ok {
+ errMsg := "failed to decode $_hugo_config in template: %w"
+ m, err := maps.ToStringMapE(s.Text)
+ if err != nil {
+ c.err = fmt.Errorf(errMsg, err)
+ return
+ }
+ if err := mapstructure.WeakDecode(m, &c.t.ParseInfo.Config); err != nil {
+ c.err = fmt.Errorf(errMsg, err)
+ }
+ }
+}
+
+// collectInner determines if the given CommandNode represents a
+// shortcode call to its .Inner.
+func (c *templateTransformContext) collectInner(n *parse.CommandNode) {
+ if c.t.Category != CategoryShortcode {
+ return
+ }
+ if c.t.ParseInfo.IsInner || len(n.Args) == 0 {
+ return
+ }
+
+ for _, arg := range n.Args {
+ var idents []string
+ switch nt := arg.(type) {
+ case *parse.FieldNode:
+ idents = nt.Ident
+ case *parse.VariableNode:
+ idents = nt.Ident
+ }
+
+ if c.hasIdent(idents, "Inner") || c.hasIdent(idents, "InnerDeindent") {
+ c.t.ParseInfo.IsInner = true
+ break
+ }
+ }
+}
+
+func (c *templateTransformContext) collectReturnNode(n *parse.CommandNode) bool {
+ if c.t.Category != CategoryPartial || c.returnNode != nil {
+ return true
+ }
+
+ if len(n.Args) < 2 {
+ return true
+ }
+
+ ident, ok := n.Args[0].(*parse.IdentifierNode)
+ if !ok || ident.Ident != "return" {
+ return true
+ }
+
+ c.returnNode = n
+ // Remove the "return" identifiers
+ c.returnNode.Args = c.returnNode.Args[1:]
+
+ return false
+}
package tplimpl_test
import (
- "path/filepath"
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib"
- "github.com/gohugoio/hugo/tpl"
)
-func TestPrintUnusedTemplates(t *testing.T) {
- t.Parallel()
-
- files := `
--- config.toml --
-baseURL = 'http://example.com/'
-printUnusedTemplates=true
--- content/p1.md --
----
-title: "P1"
----
-{{< usedshortcode >}}
--- layouts/baseof.html --
-{{ block "main" . }}{{ end }}
--- layouts/baseof.json --
-{{ block "main" . }}{{ end }}
--- layouts/index.html --
-{{ define "main" }}FOO{{ end }}
--- layouts/_default/single.json --
--- layouts/_default/single.html --
-{{ define "main" }}MAIN{{ end }}
--- layouts/post/single.html --
-{{ define "main" }}MAIN{{ end }}
--- layouts/partials/usedpartial.html --
--- layouts/partials/unusedpartial.html --
--- layouts/shortcodes/usedshortcode.html --
-{{ partial "usedpartial.html" }}
--- layouts/shortcodes/unusedshortcode.html --
-
- `
-
- b := hugolib.NewIntegrationTestBuilder(
- hugolib.IntegrationTestConfig{
- T: t,
- TxtarString: files,
- NeedsOsFS: true,
- },
- )
- b.Build()
-
- unused := b.H.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
-
- var names []string
- for _, tmpl := range unused {
- names = append(names, tmpl.Name())
- }
-
- b.Assert(names, qt.DeepEquals, []string{"_default/single.json", "baseof.json", "partials/unusedpartial.html", "post/single.html", "shortcodes/unusedshortcode.html"})
- b.Assert(unused[0].Filename(), qt.Equals, filepath.Join(b.Cfg.WorkingDir, "layouts/_default/single.json"))
-}
-
// Verify that the new keywords in Go 1.18 is available.
func TestGo18Constructs(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, files)
b.AssertFileContent("public/index.html", "Home!")
- b.EditFileReplaceAll("layouts/_default/baseof.html", "Baseof", "Baseof!").Build()
+ b.EditFileReplaceAll("layouts/_default/baseof.html", "baseof", "Baseof!").Build()
b.BuildPartial("/")
- b.AssertFileContent("public/index.html", "Baseof!!")
+ b.AssertFileContent("public/index.html", "Baseof!")
b.BuildPartial("/mybundle1/")
- b.AssertFileContent("public/mybundle1/index.html", "Baseof!!")
+ b.AssertFileContent("public/mybundle1/index.html", "Baseof!")
}
--- /dev/null
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Portions Copyright The Go Authors.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tplimplinit
+
+import (
+ // Init the template funcs namespaces
+ "context"
+ "html/template"
+
+ "github.com/gohugoio/hugo/deps"
+ _ "github.com/gohugoio/hugo/tpl/cast"
+ _ "github.com/gohugoio/hugo/tpl/collections"
+ _ "github.com/gohugoio/hugo/tpl/compare"
+ _ "github.com/gohugoio/hugo/tpl/crypto"
+ _ "github.com/gohugoio/hugo/tpl/css"
+ _ "github.com/gohugoio/hugo/tpl/data"
+ _ "github.com/gohugoio/hugo/tpl/debug"
+ _ "github.com/gohugoio/hugo/tpl/diagrams"
+ _ "github.com/gohugoio/hugo/tpl/encoding"
+ _ "github.com/gohugoio/hugo/tpl/fmt"
+ _ "github.com/gohugoio/hugo/tpl/hash"
+ _ "github.com/gohugoio/hugo/tpl/hugo"
+ _ "github.com/gohugoio/hugo/tpl/images"
+ _ "github.com/gohugoio/hugo/tpl/inflect"
+ "github.com/gohugoio/hugo/tpl/internal"
+ _ "github.com/gohugoio/hugo/tpl/js"
+ _ "github.com/gohugoio/hugo/tpl/lang"
+ _ "github.com/gohugoio/hugo/tpl/math"
+ _ "github.com/gohugoio/hugo/tpl/openapi/openapi3"
+ _ "github.com/gohugoio/hugo/tpl/os"
+ _ "github.com/gohugoio/hugo/tpl/page"
+ _ "github.com/gohugoio/hugo/tpl/partials"
+ _ "github.com/gohugoio/hugo/tpl/path"
+ _ "github.com/gohugoio/hugo/tpl/reflect"
+ _ "github.com/gohugoio/hugo/tpl/resources"
+ _ "github.com/gohugoio/hugo/tpl/safe"
+ _ "github.com/gohugoio/hugo/tpl/site"
+ _ "github.com/gohugoio/hugo/tpl/strings"
+ _ "github.com/gohugoio/hugo/tpl/templates"
+ _ "github.com/gohugoio/hugo/tpl/time"
+ _ "github.com/gohugoio/hugo/tpl/transform"
+ _ "github.com/gohugoio/hugo/tpl/urls"
+)
+
+// CreateFuncMap creates a template.FuncMap with all of Hugo's template funcs,
+// excluding the Go built-ins.
+func CreateFuncMap(d *deps.Deps) map[string]any {
+ funcMap := template.FuncMap{}
+ nsMap := make(map[string]any)
+ var onCreated []func(namespaces map[string]any)
+
+ // Merge the namespace funcs
+ for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
+ ns := nsf(d)
+ if _, exists := funcMap[ns.Name]; exists {
+ panic(ns.Name + " is a duplicate template func")
+ }
+ funcMap[ns.Name] = ns.Context
+ contextV, err := ns.Context(context.Background())
+ if err != nil {
+ panic(err)
+ }
+ nsMap[ns.Name] = contextV
+ for _, mm := range ns.MethodMappings {
+ for _, alias := range mm.Aliases {
+ if _, exists := funcMap[alias]; exists {
+ panic(alias + " is a duplicate template func")
+ }
+ funcMap[alias] = mm.Method
+ }
+ }
+
+ if ns.OnCreated != nil {
+ onCreated = append(onCreated, ns.OnCreated)
+ }
+ }
+
+ for _, f := range onCreated {
+ f(nsMap)
+ }
+
+ return funcMap
+}