mirror of
https://github.com/gohugoio/hugo.git
synced 2025-06-28 19:59:51 +00:00
Reimplement and simplify Hugo's template system
See #13541 for details. Fixes #13545 Fixes #13515 Closes #7964 Closes #13365 Closes #12988 Closes #4891
This commit is contained in:
parent
812ea0b325
commit
83cfdd78ca
138 changed files with 5342 additions and 4396 deletions
|
@ -23,6 +23,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
|
@ -48,6 +49,7 @@ import (
|
|||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/common/urls"
|
||||
|
@ -57,7 +59,6 @@ import (
|
|||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/hugolib/filesystems"
|
||||
"github.com/gohugoio/hugo/livereload"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
"github.com/gohugoio/hugo/transform"
|
||||
"github.com/gohugoio/hugo/transform/livereloadinject"
|
||||
"github.com/spf13/afero"
|
||||
|
@ -65,7 +66,6 @@ import (
|
|||
"github.com/spf13/fsync"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"maps"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -897,16 +897,16 @@ func (c *serverCommand) serve() error {
|
|||
// To allow the en user to change the error template while the server is running, we use
|
||||
// the freshest template we can provide.
|
||||
var (
|
||||
errTempl tpl.Template
|
||||
templHandler tpl.TemplateHandler
|
||||
errTempl *tplimpl.TemplInfo
|
||||
templHandler *tplimpl.TemplateStore
|
||||
)
|
||||
getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (tpl.Template, tpl.TemplateHandler) {
|
||||
getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (*tplimpl.TemplInfo, *tplimpl.TemplateStore) {
|
||||
if h == nil {
|
||||
return errTempl, templHandler
|
||||
}
|
||||
templHandler := h.Tmpl()
|
||||
errTempl, found := templHandler.Lookup("_server/error.html")
|
||||
if !found {
|
||||
templHandler := h.GetTemplateStore()
|
||||
errTempl := templHandler.LookupByPath("/_server/error.html")
|
||||
if errTempl == nil {
|
||||
panic("template server/error.html not found")
|
||||
}
|
||||
return errTempl, templHandler
|
||||
|
|
|
@ -23,6 +23,7 @@ const (
|
|||
WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
|
||||
WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html"
|
||||
WarnGoldmarkRawHTML = "warning-goldmark-raw-html"
|
||||
WarnPartialSuperfluousPrefix = "warning-partial-superfluous-prefix"
|
||||
)
|
||||
|
||||
// Field/method names with special meaning.
|
||||
|
|
|
@ -16,11 +16,11 @@ package hstrings
|
|||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/compare"
|
||||
"slices"
|
||||
)
|
||||
|
||||
var _ compare.Eqer = StringEqualFold("")
|
||||
|
@ -128,7 +128,7 @@ func ToString(v any) (string, bool) {
|
|||
return "", false
|
||||
}
|
||||
|
||||
type Tuple struct {
|
||||
First string
|
||||
Second string
|
||||
}
|
||||
type (
|
||||
Strings2 [2]string
|
||||
Strings3 [3]string
|
||||
)
|
||||
|
|
|
@ -69,6 +69,14 @@ func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
|
|||
return v, nil
|
||||
}
|
||||
|
||||
// Contains returns whether the given key exists in the cache.
|
||||
func (c *Cache[K, T]) Contains(key K) bool {
|
||||
c.RLock()
|
||||
_, found := c.m[key]
|
||||
c.RUnlock()
|
||||
return found
|
||||
}
|
||||
|
||||
// InitAndGet initializes the cache if not already done and returns the value for the given key.
|
||||
// The init state will be reset on Reset or Drain.
|
||||
func (c *Cache[K, T]) InitAndGet(key K, init func(get func(key K) (T, bool), set func(key K, value T)) error) (T, error) {
|
||||
|
@ -108,6 +116,17 @@ func (c *Cache[K, T]) Set(key K, value T) {
|
|||
c.Unlock()
|
||||
}
|
||||
|
||||
// SetIfAbsent sets the given key to the given value if the key does not already exist in the cache.
|
||||
func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
|
||||
c.RLock()
|
||||
if _, found := c.get(key); !found {
|
||||
c.RUnlock()
|
||||
c.Set(key, value)
|
||||
} else {
|
||||
c.RUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache[K, T]) set(key K, value T) {
|
||||
c.m[key] = value
|
||||
}
|
||||
|
|
|
@ -14,8 +14,9 @@
|
|||
package maps
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/hashing"
|
||||
"slices"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hashing"
|
||||
)
|
||||
|
||||
// Ordered is a map that can be iterated in the order of insertion.
|
||||
|
@ -57,6 +58,15 @@ func (m *Ordered[K, T]) Get(key K) (T, bool) {
|
|||
return value, found
|
||||
}
|
||||
|
||||
// Has returns whether the given key exists in the map.
|
||||
func (m *Ordered[K, T]) Has(key K) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
_, found := m.values[key]
|
||||
return found
|
||||
}
|
||||
|
||||
// Delete deletes the value for the given key.
|
||||
func (m *Ordered[K, T]) Delete(key K) {
|
||||
if m == nil {
|
||||
|
|
|
@ -23,6 +23,11 @@ import (
|
|||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/resources/kinds"
|
||||
)
|
||||
|
||||
const (
|
||||
identifierBaseof = "baseof"
|
||||
)
|
||||
|
||||
// PathParser parses a path into a Path.
|
||||
|
@ -33,6 +38,10 @@ type PathParser struct {
|
|||
// Reports whether the given language is disabled.
|
||||
IsLangDisabled func(string) bool
|
||||
|
||||
// IsOutputFormat reports whether the given name is a valid output format.
|
||||
// The second argument is optional.
|
||||
IsOutputFormat func(name, ext string) bool
|
||||
|
||||
// Reports whether the given ext is a content file.
|
||||
IsContentExt func(string) bool
|
||||
}
|
||||
|
@ -83,13 +92,10 @@ func (pp *PathParser) Parse(c, s string) *Path {
|
|||
}
|
||||
|
||||
func (pp *PathParser) newPath(component string) *Path {
|
||||
return &Path{
|
||||
component: component,
|
||||
posContainerLow: -1,
|
||||
posContainerHigh: -1,
|
||||
posSectionHigh: -1,
|
||||
posIdentifierLanguage: -1,
|
||||
}
|
||||
p := &Path{}
|
||||
p.reset()
|
||||
p.component = component
|
||||
return p
|
||||
}
|
||||
|
||||
func (pp *PathParser) parse(component, s string) (*Path, error) {
|
||||
|
@ -114,10 +120,91 @@ func (pp *PathParser) parse(component, s string) (*Path, error) {
|
|||
return p, nil
|
||||
}
|
||||
|
||||
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
||||
hasLang := pp.LanguageIndex != nil
|
||||
hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
|
||||
func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot int) {
|
||||
if p.posContainerHigh != -1 {
|
||||
return
|
||||
}
|
||||
mayHaveLang := pp.LanguageIndex != nil
|
||||
mayHaveLang = mayHaveLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
|
||||
mayHaveOutputFormat := component == files.ComponentFolderLayouts
|
||||
mayHaveKind := mayHaveOutputFormat
|
||||
|
||||
var found bool
|
||||
var high int
|
||||
if len(p.identifiers) > 0 {
|
||||
high = lastDot
|
||||
} else {
|
||||
high = len(p.s)
|
||||
}
|
||||
id := types.LowHigh[string]{Low: i + 1, High: high}
|
||||
sid := p.s[id.Low:id.High]
|
||||
|
||||
if len(p.identifiers) == 0 {
|
||||
// The first is always the extension.
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
found = true
|
||||
|
||||
// May also be the output format.
|
||||
if mayHaveOutputFormat && pp.IsOutputFormat(sid, "") {
|
||||
p.posIdentifierOutputFormat = 0
|
||||
}
|
||||
} else {
|
||||
|
||||
var langFound bool
|
||||
|
||||
if mayHaveLang {
|
||||
var disabled bool
|
||||
_, langFound = pp.LanguageIndex[sid]
|
||||
if !langFound {
|
||||
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(sid)
|
||||
if disabled {
|
||||
p.disabled = true
|
||||
langFound = true
|
||||
}
|
||||
}
|
||||
found = langFound
|
||||
if langFound {
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
p.posIdentifierLanguage = len(p.identifiers) - 1
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if !found && mayHaveOutputFormat {
|
||||
// At this point we may already have resolved an output format,
|
||||
// but we need to keep looking for a more specific one, e.g. amp before html.
|
||||
// Use both name and extension to prevent
|
||||
// false positives on the form css.html.
|
||||
if pp.IsOutputFormat(sid, p.Ext()) {
|
||||
found = true
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
p.posIdentifierOutputFormat = len(p.identifiers) - 1
|
||||
}
|
||||
}
|
||||
|
||||
if !found && mayHaveKind {
|
||||
if kinds.GetKindMain(sid) != "" {
|
||||
found = true
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
p.posIdentifierKind = len(p.identifiers) - 1
|
||||
}
|
||||
}
|
||||
|
||||
if !found && sid == identifierBaseof {
|
||||
found = true
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
p.posIdentifierBaseof = len(p.identifiers) - 1
|
||||
}
|
||||
|
||||
if !found {
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
p.identifiersUnknown = append(p.identifiersUnknown, len(p.identifiers)-1)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
s = path.Clean(filepath.ToSlash(s))
|
||||
if s == "." {
|
||||
|
@ -140,46 +227,21 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
|||
|
||||
p.s = s
|
||||
slashCount := 0
|
||||
lastDot := 0
|
||||
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
c := s[i]
|
||||
|
||||
switch c {
|
||||
case '.':
|
||||
if p.posContainerHigh == -1 {
|
||||
var high int
|
||||
if len(p.identifiers) > 0 {
|
||||
high = p.identifiers[len(p.identifiers)-1].Low - 1
|
||||
} else {
|
||||
high = len(p.s)
|
||||
}
|
||||
id := types.LowHigh[string]{Low: i + 1, High: high}
|
||||
if len(p.identifiers) == 0 {
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
} else if len(p.identifiers) == 1 {
|
||||
// Check for a valid language.
|
||||
s := p.s[id.Low:id.High]
|
||||
|
||||
if hasLang {
|
||||
var disabled bool
|
||||
_, langFound := pp.LanguageIndex[s]
|
||||
if !langFound {
|
||||
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(s)
|
||||
if disabled {
|
||||
p.disabled = true
|
||||
langFound = true
|
||||
}
|
||||
}
|
||||
if langFound {
|
||||
p.posIdentifierLanguage = 1
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pp.parseIdentifier(component, s, p, i, lastDot)
|
||||
lastDot = i
|
||||
case '/':
|
||||
slashCount++
|
||||
if p.posContainerHigh == -1 {
|
||||
if lastDot > 0 {
|
||||
pp.parseIdentifier(component, s, p, i, lastDot)
|
||||
}
|
||||
p.posContainerHigh = i + 1
|
||||
} else if p.posContainerLow == -1 {
|
||||
p.posContainerLow = i + 1
|
||||
|
@ -194,22 +256,41 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
|||
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
|
||||
isContent := isContentComponent && pp.IsContentExt(p.Ext())
|
||||
id := p.identifiers[len(p.identifiers)-1]
|
||||
b := p.s[p.posContainerHigh : id.Low-1]
|
||||
|
||||
if id.High > p.posContainerHigh {
|
||||
b := p.s[p.posContainerHigh:id.High]
|
||||
if isContent {
|
||||
switch b {
|
||||
case "index":
|
||||
p.bundleType = PathTypeLeaf
|
||||
p.pathType = TypeLeaf
|
||||
case "_index":
|
||||
p.bundleType = PathTypeBranch
|
||||
p.pathType = TypeBranch
|
||||
default:
|
||||
p.bundleType = PathTypeContentSingle
|
||||
p.pathType = TypeContentSingle
|
||||
}
|
||||
|
||||
if slashCount == 2 && p.IsLeafBundle() {
|
||||
p.posSectionHigh = 0
|
||||
}
|
||||
} else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
|
||||
p.bundleType = PathTypeContentData
|
||||
p.pathType = TypeContentData
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if component == files.ComponentFolderLayouts {
|
||||
if p.posIdentifierBaseof != -1 {
|
||||
p.pathType = TypeBaseof
|
||||
} else {
|
||||
pth := p.Path()
|
||||
if strings.Contains(pth, "/_shortcodes/") {
|
||||
p.pathType = TypeShortcode
|
||||
} else if strings.Contains(pth, "/_markup/") {
|
||||
p.pathType = TypeMarkup
|
||||
} else if strings.HasPrefix(pth, "/_partials/") {
|
||||
p.pathType = TypePartial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,35 +299,44 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
|||
|
||||
func ModifyPathBundleTypeResource(p *Path) {
|
||||
if p.IsContent() {
|
||||
p.bundleType = PathTypeContentResource
|
||||
p.pathType = TypeContentResource
|
||||
} else {
|
||||
p.bundleType = PathTypeFile
|
||||
p.pathType = TypeFile
|
||||
}
|
||||
}
|
||||
|
||||
type PathType int
|
||||
//go:generate stringer -type Type
|
||||
|
||||
type Type int
|
||||
|
||||
const (
|
||||
|
||||
// A generic resource, e.g. a JSON file.
|
||||
PathTypeFile PathType = iota
|
||||
TypeFile Type = iota
|
||||
|
||||
// All below are content files.
|
||||
// A resource of a content type with front matter.
|
||||
PathTypeContentResource
|
||||
TypeContentResource
|
||||
|
||||
// E.g. /blog/my-post.md
|
||||
PathTypeContentSingle
|
||||
TypeContentSingle
|
||||
|
||||
// All below are bundled content files.
|
||||
|
||||
// Leaf bundles, e.g. /blog/my-post/index.md
|
||||
PathTypeLeaf
|
||||
TypeLeaf
|
||||
|
||||
// Branch bundles, e.g. /blog/_index.md
|
||||
PathTypeBranch
|
||||
TypeBranch
|
||||
|
||||
// Content data file, _content.gotmpl.
|
||||
PathTypeContentData
|
||||
TypeContentData
|
||||
|
||||
// Layout types.
|
||||
TypeMarkup
|
||||
TypeShortcode
|
||||
TypePartial
|
||||
TypeBaseof
|
||||
)
|
||||
|
||||
type Path struct {
|
||||
|
@ -258,11 +348,15 @@ type Path struct {
|
|||
posSectionHigh int
|
||||
|
||||
component string
|
||||
bundleType PathType
|
||||
pathType Type
|
||||
|
||||
identifiers []types.LowHigh[string]
|
||||
|
||||
posIdentifierLanguage int
|
||||
posIdentifierOutputFormat int
|
||||
posIdentifierKind int
|
||||
posIdentifierBaseof int
|
||||
identifiersUnknown []int
|
||||
disabled bool
|
||||
|
||||
trimLeadingSlash bool
|
||||
|
@ -293,9 +387,12 @@ func (p *Path) reset() {
|
|||
p.posContainerHigh = -1
|
||||
p.posSectionHigh = -1
|
||||
p.component = ""
|
||||
p.bundleType = 0
|
||||
p.pathType = 0
|
||||
p.identifiers = p.identifiers[:0]
|
||||
p.posIdentifierLanguage = -1
|
||||
p.posIdentifierOutputFormat = -1
|
||||
p.posIdentifierKind = -1
|
||||
p.posIdentifierBaseof = -1
|
||||
p.disabled = false
|
||||
p.trimLeadingSlash = false
|
||||
p.unnormalized = nil
|
||||
|
@ -316,6 +413,9 @@ func (p *Path) norm(s string) string {
|
|||
|
||||
// IdentifierBase satisfies identity.Identity.
|
||||
func (p *Path) IdentifierBase() string {
|
||||
if p.Component() == files.ComponentFolderLayouts {
|
||||
return p.Path()
|
||||
}
|
||||
return p.Base()
|
||||
}
|
||||
|
||||
|
@ -332,6 +432,13 @@ func (p *Path) Container() string {
|
|||
return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1])
|
||||
}
|
||||
|
||||
func (p *Path) String() string {
|
||||
if p == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return p.Path()
|
||||
}
|
||||
|
||||
// ContainerDir returns the container directory for this path.
|
||||
// For content bundles this will be the parent directory.
|
||||
func (p *Path) ContainerDir() string {
|
||||
|
@ -352,13 +459,13 @@ func (p *Path) Section() string {
|
|||
// IsContent returns true if the path is a content file (e.g. mypost.md).
|
||||
// Note that this will also return true for content files in a bundle.
|
||||
func (p *Path) IsContent() bool {
|
||||
return p.BundleType() >= PathTypeContentResource
|
||||
return p.Type() >= TypeContentResource && p.Type() <= TypeContentData
|
||||
}
|
||||
|
||||
// isContentPage returns true if the path is a content file (e.g. mypost.md),
|
||||
// but nof if inside a leaf bundle.
|
||||
func (p *Path) isContentPage() bool {
|
||||
return p.BundleType() >= PathTypeContentSingle
|
||||
return p.Type() >= TypeContentSingle && p.Type() <= TypeContentData
|
||||
}
|
||||
|
||||
// Name returns the last element of path.
|
||||
|
@ -398,10 +505,26 @@ func (p *Path) BaseNameNoIdentifier() string {
|
|||
|
||||
// NameNoIdentifier returns the last element of path without any identifier (e.g. no extension).
|
||||
func (p *Path) NameNoIdentifier() string {
|
||||
if len(p.identifiers) > 0 {
|
||||
return p.s[p.posContainerHigh : p.identifiers[len(p.identifiers)-1].Low-1]
|
||||
lowHigh := p.nameLowHigh()
|
||||
return p.s[lowHigh.Low:lowHigh.High]
|
||||
}
|
||||
|
||||
func (p *Path) nameLowHigh() types.LowHigh[string] {
|
||||
if len(p.identifiers) > 0 {
|
||||
lastID := p.identifiers[len(p.identifiers)-1]
|
||||
if p.posContainerHigh == lastID.Low {
|
||||
// The last identifier is the name.
|
||||
return lastID
|
||||
}
|
||||
return types.LowHigh[string]{
|
||||
Low: p.posContainerHigh,
|
||||
High: p.identifiers[len(p.identifiers)-1].Low - 1,
|
||||
}
|
||||
}
|
||||
return types.LowHigh[string]{
|
||||
Low: p.posContainerHigh,
|
||||
High: len(p.s),
|
||||
}
|
||||
return p.s[p.posContainerHigh:]
|
||||
}
|
||||
|
||||
// Dir returns all but the last element of path, typically the path's directory.
|
||||
|
@ -421,6 +544,11 @@ func (p *Path) Path() (d string) {
|
|||
return p.norm(p.s)
|
||||
}
|
||||
|
||||
// PathNoLeadingSlash returns the full path without the leading slash.
|
||||
func (p *Path) PathNoLeadingSlash() string {
|
||||
return p.Path()[1:]
|
||||
}
|
||||
|
||||
// Unnormalized returns the Path with the original case preserved.
|
||||
func (p *Path) Unnormalized() *Path {
|
||||
return p.unnormalized
|
||||
|
@ -436,6 +564,28 @@ func (p *Path) PathNoIdentifier() string {
|
|||
return p.base(false, false)
|
||||
}
|
||||
|
||||
// PathBeforeLangAndOutputFormatAndExt returns the path up to the first identifier that is not a language or output format.
|
||||
func (p *Path) PathBeforeLangAndOutputFormatAndExt() string {
|
||||
if len(p.identifiers) == 0 {
|
||||
return p.norm(p.s)
|
||||
}
|
||||
i := p.identifierIndex(0)
|
||||
|
||||
if j := p.posIdentifierOutputFormat; i == -1 || (j != -1 && j < i) {
|
||||
i = j
|
||||
}
|
||||
if j := p.posIdentifierLanguage; i == -1 || (j != -1 && j < i) {
|
||||
i = j
|
||||
}
|
||||
|
||||
if i == -1 {
|
||||
return p.norm(p.s)
|
||||
}
|
||||
|
||||
id := p.identifiers[i]
|
||||
return p.norm(p.s[:id.Low-1])
|
||||
}
|
||||
|
||||
// PathRel returns the path relative to the given owner.
|
||||
func (p *Path) PathRel(owner *Path) string {
|
||||
ob := owner.Base()
|
||||
|
@ -462,6 +612,21 @@ func (p *Path) Base() string {
|
|||
return p.base(!p.isContentPage(), p.IsBundle())
|
||||
}
|
||||
|
||||
// Used in template lookups.
|
||||
// For pages with Type set, we treat that as the section.
|
||||
func (p *Path) BaseReTyped(typ string) (d string) {
|
||||
base := p.Base()
|
||||
if typ == "" || p.Section() == typ {
|
||||
return base
|
||||
}
|
||||
d = "/" + typ
|
||||
if p.posSectionHigh != -1 {
|
||||
d += base[p.posSectionHigh:]
|
||||
}
|
||||
d = p.norm(d)
|
||||
return
|
||||
}
|
||||
|
||||
// BaseNoLeadingSlash returns the base path without the leading slash.
|
||||
func (p *Path) BaseNoLeadingSlash() string {
|
||||
return p.Base()[1:]
|
||||
|
@ -477,11 +642,12 @@ func (p *Path) base(preserveExt, isBundle bool) string {
|
|||
return p.norm(p.s)
|
||||
}
|
||||
|
||||
id := p.identifiers[len(p.identifiers)-1]
|
||||
high := id.Low - 1
|
||||
var high int
|
||||
|
||||
if isBundle {
|
||||
high = p.posContainerHigh - 1
|
||||
} else {
|
||||
high = p.nameLowHigh().High
|
||||
}
|
||||
|
||||
if high == 0 {
|
||||
|
@ -493,7 +659,7 @@ func (p *Path) base(preserveExt, isBundle bool) string {
|
|||
}
|
||||
|
||||
// For txt files etc. we want to preserve the extension.
|
||||
id = p.identifiers[0]
|
||||
id := p.identifiers[0]
|
||||
|
||||
return p.norm(p.s[:high] + p.s[id.Low-1:id.High])
|
||||
}
|
||||
|
@ -502,8 +668,16 @@ func (p *Path) Ext() string {
|
|||
return p.identifierAsString(0)
|
||||
}
|
||||
|
||||
func (p *Path) OutputFormat() string {
|
||||
return p.identifierAsString(p.posIdentifierOutputFormat)
|
||||
}
|
||||
|
||||
func (p *Path) Kind() string {
|
||||
return p.identifierAsString(p.posIdentifierKind)
|
||||
}
|
||||
|
||||
func (p *Path) Lang() string {
|
||||
return p.identifierAsString(1)
|
||||
return p.identifierAsString(p.posIdentifierLanguage)
|
||||
}
|
||||
|
||||
func (p *Path) Identifier(i int) string {
|
||||
|
@ -522,28 +696,36 @@ func (p *Path) Identifiers() []string {
|
|||
return ids
|
||||
}
|
||||
|
||||
func (p *Path) BundleType() PathType {
|
||||
return p.bundleType
|
||||
func (p *Path) IdentifiersUnknown() []string {
|
||||
ids := make([]string, len(p.identifiersUnknown))
|
||||
for i, id := range p.identifiersUnknown {
|
||||
ids[i] = p.s[p.identifiers[id].Low:p.identifiers[id].High]
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (p *Path) Type() Type {
|
||||
return p.pathType
|
||||
}
|
||||
|
||||
func (p *Path) IsBundle() bool {
|
||||
return p.bundleType >= PathTypeLeaf
|
||||
return p.pathType >= TypeLeaf && p.pathType <= TypeContentData
|
||||
}
|
||||
|
||||
func (p *Path) IsBranchBundle() bool {
|
||||
return p.bundleType == PathTypeBranch
|
||||
return p.pathType == TypeBranch
|
||||
}
|
||||
|
||||
func (p *Path) IsLeafBundle() bool {
|
||||
return p.bundleType == PathTypeLeaf
|
||||
return p.pathType == TypeLeaf
|
||||
}
|
||||
|
||||
func (p *Path) IsContentData() bool {
|
||||
return p.bundleType == PathTypeContentData
|
||||
return p.pathType == TypeContentData
|
||||
}
|
||||
|
||||
func (p Path) ForBundleType(t PathType) *Path {
|
||||
p.bundleType = t
|
||||
func (p Path) ForBundleType(t Type) *Path {
|
||||
p.pathType = t
|
||||
return &p
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
"github.com/gohugoio/hugo/resources/kinds"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
@ -26,10 +27,18 @@ var testParser = &PathParser{
|
|||
LanguageIndex: map[string]int{
|
||||
"no": 0,
|
||||
"en": 1,
|
||||
"fr": 2,
|
||||
},
|
||||
IsContentExt: func(ext string) bool {
|
||||
return ext == "md"
|
||||
},
|
||||
IsOutputFormat: func(name, ext string) bool {
|
||||
switch name {
|
||||
case "html", "amp", "csv", "rss":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
|
@ -105,17 +114,19 @@ func TestParse(t *testing.T) {
|
|||
"Basic Markdown file",
|
||||
"/a/b/c.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Type(), qt.Equals, TypeContentSingle)
|
||||
c.Assert(p.IsContent(), qt.IsTrue)
|
||||
c.Assert(p.IsLeafBundle(), qt.IsFalse)
|
||||
c.Assert(p.Name(), qt.Equals, "c.md")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/c")
|
||||
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b/c")
|
||||
c.Assert(p.Section(), qt.Equals, "a")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c")
|
||||
c.Assert(p.Path(), qt.Equals, "/a/b/c.md")
|
||||
c.Assert(p.Dir(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Container(), qt.Equals, "b")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -130,7 +141,7 @@ func TestParse(t *testing.T) {
|
|||
|
||||
// Reclassify it as a content resource.
|
||||
ModifyPathBundleTypeResource(p)
|
||||
c.Assert(p.BundleType(), qt.Equals, PathTypeContentResource)
|
||||
c.Assert(p.Type(), qt.Equals, TypeContentResource)
|
||||
c.Assert(p.IsContent(), qt.IsTrue)
|
||||
c.Assert(p.Name(), qt.Equals, "b.md")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b.md")
|
||||
|
@ -160,15 +171,16 @@ func TestParse(t *testing.T) {
|
|||
"/a/b.a.b.no.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Name(), qt.Equals, "b.a.b.no.txt")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "b")
|
||||
c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt")
|
||||
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no", "b", "a", "b"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"b", "a", "b"})
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b.txt")
|
||||
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.txt")
|
||||
c.Assert(p.Path(), qt.Equals, "/a/b.a.b.no.txt")
|
||||
c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt")
|
||||
c.Assert(p.PathNoLang(), qt.Equals, "/a/b.txt")
|
||||
c.Assert(p.Ext(), qt.Equals, "txt")
|
||||
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b")
|
||||
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b")
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -176,6 +188,7 @@ func TestParse(t *testing.T) {
|
|||
"/_index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo")
|
||||
c.Assert(p.Path(), qt.Equals, "/_index.md")
|
||||
c.Assert(p.Container(), qt.Equals, "")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/")
|
||||
|
@ -186,13 +199,14 @@ func TestParse(t *testing.T) {
|
|||
"/a/index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a")
|
||||
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/a")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "a")
|
||||
c.Assert(p.Container(), qt.Equals, "a")
|
||||
c.Assert(p.Container(), qt.Equals, "a")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "")
|
||||
c.Assert(p.Dir(), qt.Equals, "/a")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "index"})
|
||||
c.Assert(p.IsBranchBundle(), qt.IsFalse)
|
||||
c.Assert(p.IsBundle(), qt.IsTrue)
|
||||
c.Assert(p.IsLeafBundle(), qt.IsTrue)
|
||||
|
@ -209,11 +223,12 @@ func TestParse(t *testing.T) {
|
|||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
|
||||
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b")
|
||||
c.Assert(p.Container(), qt.Equals, "b")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/a")
|
||||
c.Assert(p.Dir(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no", "index"})
|
||||
c.Assert(p.IsBranchBundle(), qt.IsFalse)
|
||||
c.Assert(p.IsBundle(), qt.IsTrue)
|
||||
c.Assert(p.IsLeafBundle(), qt.IsTrue)
|
||||
|
@ -235,7 +250,7 @@ func TestParse(t *testing.T) {
|
|||
c.Assert(p.Container(), qt.Equals, "b")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/a")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no", "_index"})
|
||||
c.Assert(p.IsBranchBundle(), qt.IsTrue)
|
||||
c.Assert(p.IsBundle(), qt.IsTrue)
|
||||
c.Assert(p.IsLeafBundle(), qt.IsFalse)
|
||||
|
@ -274,7 +289,7 @@ func TestParse(t *testing.T) {
|
|||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/index.txt")
|
||||
c.Assert(p.Ext(), qt.Equals, "txt")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no", "index"})
|
||||
c.Assert(p.IsLeafBundle(), qt.IsFalse)
|
||||
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b/index")
|
||||
},
|
||||
|
@ -357,11 +372,140 @@ func TestParse(t *testing.T) {
|
|||
}
|
||||
for _, test := range tests {
|
||||
c.Run(test.name, func(c *qt.C) {
|
||||
if test.name != "Basic Markdown file" {
|
||||
// return
|
||||
}
|
||||
test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLayouts(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
assert func(c *qt.C, p *Path)
|
||||
}{
|
||||
{
|
||||
"Basic",
|
||||
"/list.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/list.html")
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Lang",
|
||||
"/list.no.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "list"})
|
||||
c.Assert(p.Base(), qt.Equals, "/list.html")
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Lang and output format",
|
||||
"/list.no.amp.not.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "not", "amp", "no", "list"})
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "amp")
|
||||
c.Assert(p.Ext(), qt.Equals, "html")
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
c.Assert(p.Base(), qt.Equals, "/list.html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Term",
|
||||
"/term.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/term.html")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "term"})
|
||||
c.Assert(p.PathNoIdentifier(), qt.Equals, "/term")
|
||||
c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/term")
|
||||
c.Assert(p.Lang(), qt.Equals, "")
|
||||
c.Assert(p.Kind(), qt.Equals, "term")
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Sub dir",
|
||||
"/pages/home.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "home"})
|
||||
c.Assert(p.Lang(), qt.Equals, "")
|
||||
c.Assert(p.Kind(), qt.Equals, "home")
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "html")
|
||||
c.Assert(p.Dir(), qt.Equals, "/pages")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Baseof",
|
||||
"/pages/baseof.list.section.fr.amp.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "amp", "fr", "section", "list", "baseof"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"list"})
|
||||
c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
|
||||
c.Assert(p.Lang(), qt.Equals, "fr")
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "amp")
|
||||
c.Assert(p.Dir(), qt.Equals, "/pages")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "baseof")
|
||||
c.Assert(p.Type(), qt.Equals, TypeBaseof)
|
||||
c.Assert(p.IdentifierBase(), qt.Equals, "/pages/baseof.list.section.fr.amp.html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Markup",
|
||||
"/_markup/render-link.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Type(), qt.Equals, TypeMarkup)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Markup nested",
|
||||
"/foo/_markup/render-link.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Type(), qt.Equals, TypeMarkup)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Shortcode",
|
||||
"/_shortcodes/myshortcode.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Type(), qt.Equals, TypeShortcode)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Shortcode nested",
|
||||
"/foo/_shortcodes/myshortcode.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Type(), qt.Equals, TypeShortcode)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Shortcode nested sub",
|
||||
"/foo/_shortcodes/foo/myshortcode.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Type(), qt.Equals, TypeShortcode)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Partials",
|
||||
"/_partials/foo.bar",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Type(), qt.Equals, TypePartial)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
c.Run(test.name, func(c *qt.C) {
|
||||
test.assert(c, testParser.Parse(files.ComponentFolderLayouts, test.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasExt(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
// Code generated by "stringer -type=PathType"; DO NOT EDIT.
|
||||
|
||||
package paths
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[PathTypeFile-0]
|
||||
_ = x[PathTypeContentResource-1]
|
||||
_ = x[PathTypeContentSingle-2]
|
||||
_ = x[PathTypeLeaf-3]
|
||||
_ = x[PathTypeBranch-4]
|
||||
}
|
||||
|
||||
const _PathType_name = "PathTypeFilePathTypeContentResourcePathTypeContentSinglePathTypeLeafPathTypeBranch"
|
||||
|
||||
var _PathType_index = [...]uint8{0, 12, 35, 56, 68, 82}
|
||||
|
||||
func (i PathType) String() string {
|
||||
if i < 0 || i >= PathType(len(_PathType_index)-1) {
|
||||
return "PathType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _PathType_name[_PathType_index[i]:_PathType_index[i+1]]
|
||||
}
|
32
common/paths/type_string.go
Normal file
32
common/paths/type_string.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Code generated by "stringer -type Type"; DO NOT EDIT.
|
||||
|
||||
package paths
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[TypeFile-0]
|
||||
_ = x[TypeContentResource-1]
|
||||
_ = x[TypeContentSingle-2]
|
||||
_ = x[TypeLeaf-3]
|
||||
_ = x[TypeBranch-4]
|
||||
_ = x[TypeContentData-5]
|
||||
_ = x[TypeMarkup-6]
|
||||
_ = x[TypeShortcode-7]
|
||||
_ = x[TypePartial-8]
|
||||
_ = x[TypeBaseof-9]
|
||||
}
|
||||
|
||||
const _Type_name = "TypeFileTypeContentResourceTypeContentSingleTypeLeafTypeBranchTypeContentDataTypeMarkupTypeShortcodeTypePartialTypeBaseof"
|
||||
|
||||
var _Type_index = [...]uint8{0, 8, 27, 44, 52, 62, 77, 87, 100, 111, 121}
|
||||
|
||||
func (i Type) String() string {
|
||||
if i < 0 || i >= Type(len(_Type_index)-1) {
|
||||
return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _Type_name[_Type_index[i]:_Type_index[i+1]]
|
||||
}
|
|
@ -28,6 +28,16 @@ type RLocker interface {
|
|||
RUnlock()
|
||||
}
|
||||
|
||||
type Locker interface {
|
||||
Lock()
|
||||
Unlock()
|
||||
}
|
||||
|
||||
type RWLocker interface {
|
||||
RLocker
|
||||
Locker
|
||||
}
|
||||
|
||||
// KeyValue is a interface{} tuple.
|
||||
type KeyValue struct {
|
||||
Key any
|
||||
|
|
|
@ -849,7 +849,24 @@ func (c *Configs) Init() error {
|
|||
c.Languages = languages
|
||||
c.LanguagesDefaultFirst = languagesDefaultFirst
|
||||
|
||||
c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix}
|
||||
c.ContentPathParser = &paths.PathParser{
|
||||
LanguageIndex: languagesDefaultFirst.AsIndexSet(),
|
||||
IsLangDisabled: c.Base.IsLangDisabled,
|
||||
IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix,
|
||||
IsOutputFormat: func(name, ext string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if of, ok := c.Base.OutputFormats.Config.GetByName(name); ok {
|
||||
if ext != "" && !of.MediaType.HasSuffix(ext) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
c.configLangs = make([]config.AllProvider, len(c.Languages))
|
||||
for i, l := range c.LanguagesDefaultFirst {
|
||||
|
|
|
@ -291,7 +291,7 @@ func (b *contentBuilder) applyArcheType(contentFilename string, archetypeFi hugo
|
|||
func (b *contentBuilder) mapArcheTypeDir() error {
|
||||
var m archetypeMap
|
||||
|
||||
seen := map[hstrings.Tuple]bool{}
|
||||
seen := map[hstrings.Strings2]bool{}
|
||||
|
||||
walkFn := func(path string, fim hugofs.FileMetaInfo) error {
|
||||
if fim.IsDir() {
|
||||
|
@ -301,7 +301,7 @@ func (b *contentBuilder) mapArcheTypeDir() error {
|
|||
pi := fim.Meta().PathInfo
|
||||
|
||||
if pi.IsContent() {
|
||||
pathLang := hstrings.Tuple{First: pi.PathNoIdentifier(), Second: fim.Meta().Lang}
|
||||
pathLang := hstrings.Strings2{pi.PathBeforeLangAndOutputFormatAndExt(), fim.Meta().Lang}
|
||||
if seen[pathLang] {
|
||||
// Duplicate content file, e.g. page.md and page.html.
|
||||
// In the regular build, we will filter out the duplicates, but
|
||||
|
|
47
deps/deps.go
vendored
47
deps/deps.go
vendored
|
@ -29,6 +29,7 @@ import (
|
|||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/resources/postpub"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
|
||||
"github.com/gohugoio/hugo/metrics"
|
||||
"github.com/gohugoio/hugo/resources"
|
||||
|
@ -46,12 +47,6 @@ type Deps struct {
|
|||
|
||||
ExecHelper *hexec.Exec
|
||||
|
||||
// The templates to use. This will usually implement the full tpl.TemplateManager.
|
||||
tmplHandlers *tpl.TemplateHandlers
|
||||
|
||||
// The template funcs.
|
||||
TmplFuncMap map[string]any
|
||||
|
||||
// The file systems to use.
|
||||
Fs *hugofs.Fs `json:"-"`
|
||||
|
||||
|
@ -79,7 +74,8 @@ type Deps struct {
|
|||
// The site building.
|
||||
Site page.Site
|
||||
|
||||
TemplateProvider ResourceProvider
|
||||
TemplateStore *tplimpl.TemplateStore
|
||||
|
||||
// Used in tests
|
||||
OverloadedTemplateFuncs map[string]any
|
||||
|
||||
|
@ -102,6 +98,9 @@ type Deps struct {
|
|||
// This is common/global for all sites.
|
||||
BuildState *BuildState
|
||||
|
||||
// Misc counters.
|
||||
Counters *Counters
|
||||
|
||||
// Holds RPC dispatchers for Katex etc.
|
||||
// TODO(bep) rethink this re. a plugin setup, but this will have to do for now.
|
||||
WasmDispatchers *warpc.Dispatchers
|
||||
|
@ -109,9 +108,6 @@ type Deps struct {
|
|||
// The JS batcher client.
|
||||
JSBatcherClient js.BatcherClient
|
||||
|
||||
// The JS batcher client.
|
||||
// JSBatcherClient *esbuild.BatcherClient
|
||||
|
||||
isClosed bool
|
||||
|
||||
*globalErrHandler
|
||||
|
@ -130,8 +126,8 @@ func (d Deps) Clone(s page.Site, conf config.AllProvider) (*Deps, error) {
|
|||
return &d, nil
|
||||
}
|
||||
|
||||
func (d *Deps) SetTempl(t *tpl.TemplateHandlers) {
|
||||
d.tmplHandlers = t
|
||||
func (d *Deps) GetTemplateStore() *tplimpl.TemplateStore {
|
||||
return d.TemplateStore
|
||||
}
|
||||
|
||||
func (d *Deps) Init() error {
|
||||
|
@ -153,10 +149,12 @@ func (d *Deps) Init() error {
|
|||
logger: d.Log,
|
||||
}
|
||||
}
|
||||
|
||||
if d.BuildState == nil {
|
||||
d.BuildState = &BuildState{}
|
||||
}
|
||||
if d.Counters == nil {
|
||||
d.Counters = &Counters{}
|
||||
}
|
||||
if d.BuildState.DeferredExecutions == nil {
|
||||
if d.BuildState.DeferredExecutionsGroupedByRenderingContext == nil {
|
||||
d.BuildState.DeferredExecutionsGroupedByRenderingContext = make(map[tpl.RenderingContext]*DeferredExecutions)
|
||||
|
@ -263,22 +261,17 @@ func (d *Deps) Init() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO(bep) rework this to get it in line with how we manage templates.
|
||||
func (d *Deps) Compile(prototype *Deps) error {
|
||||
var err error
|
||||
if prototype == nil {
|
||||
if err = d.TemplateProvider.NewResource(d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = d.TranslationProvider.NewResource(d); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = d.TemplateProvider.CloneResource(d, prototype); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = d.TranslationProvider.CloneResource(d, prototype); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -378,14 +371,6 @@ type ResourceProvider interface {
|
|||
CloneResource(dst, src *Deps) error
|
||||
}
|
||||
|
||||
func (d *Deps) Tmpl() tpl.TemplateHandler {
|
||||
return d.tmplHandlers.Tmpl
|
||||
}
|
||||
|
||||
func (d *Deps) TextTmpl() tpl.TemplateParseFinder {
|
||||
return d.tmplHandlers.TxtTmpl
|
||||
}
|
||||
|
||||
func (d *Deps) Close() error {
|
||||
if d.isClosed {
|
||||
return nil
|
||||
|
@ -454,6 +439,12 @@ type BuildState struct {
|
|||
DeferredExecutionsGroupedByRenderingContext map[tpl.RenderingContext]*DeferredExecutions
|
||||
}
|
||||
|
||||
// Misc counters.
|
||||
type Counters struct {
|
||||
// Counter for the math.Counter function.
|
||||
MathCounter atomic.Uint64
|
||||
}
|
||||
|
||||
type DeferredExecutions struct {
|
||||
// A set of filenames in /public that
|
||||
// contains a post-processing prefix.
|
||||
|
|
|
@ -29,16 +29,17 @@ import (
|
|||
"github.com/gohugoio/hugo/publisher"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
)
|
||||
|
||||
type aliasHandler struct {
|
||||
t tpl.TemplateHandler
|
||||
ts *tplimpl.TemplateStore
|
||||
log loggers.Logger
|
||||
allowRoot bool
|
||||
}
|
||||
|
||||
func newAliasHandler(t tpl.TemplateHandler, l loggers.Logger, allowRoot bool) aliasHandler {
|
||||
return aliasHandler{t, l, allowRoot}
|
||||
func newAliasHandler(ts *tplimpl.TemplateStore, l loggers.Logger, allowRoot bool) aliasHandler {
|
||||
return aliasHandler{ts, l, allowRoot}
|
||||
}
|
||||
|
||||
type aliasPage struct {
|
||||
|
@ -47,16 +48,24 @@ type aliasPage struct {
|
|||
}
|
||||
|
||||
func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, error) {
|
||||
var templ tpl.Template
|
||||
var found bool
|
||||
|
||||
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{
|
||||
|
@ -67,7 +76,7 @@ func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, err
|
|||
ctx := tpl.Context.Page.Set(context.Background(), p)
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
err := a.t.ExecuteWithContext(ctx, templ, buffer, data)
|
||||
err := a.ts.ExecuteWithContext(ctx, t, buffer, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -79,7 +88,7 @@ func (s *Site) writeDestAlias(path, permalink string, outputFormat output.Format
|
|||
}
|
||||
|
||||
func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) {
|
||||
handler := newAliasHandler(s.Tmpl(), s.Log, allowRoot)
|
||||
handler := newAliasHandler(s.GetTemplateStore(), s.Log, allowRoot)
|
||||
|
||||
targetPath, err := handler.targetPathAlias(path)
|
||||
if err != nil {
|
||||
|
|
|
@ -107,13 +107,26 @@ func TestAliasMultipleOutputFormats(t *testing.T) {
|
|||
func TestAliasTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := newTestSitesBuilder(t)
|
||||
b.WithSimpleConfigFile().WithContent("page.md", pageWithAlias).WithTemplatesAdded("alias.html", aliasTemplate)
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "http://example.com"
|
||||
-- layouts/single.html --
|
||||
Single.
|
||||
-- layouts/home.html --
|
||||
Home.
|
||||
-- layouts/alias.html --
|
||||
ALIASTEMPLATE
|
||||
-- content/page.md --
|
||||
---
|
||||
title: "Page"
|
||||
aliases: ["/foo/bar/"]
|
||||
---
|
||||
`
|
||||
|
||||
b.CreateSites().Build(BuildCfg{})
|
||||
b := Test(t, files)
|
||||
|
||||
// the real page
|
||||
b.AssertFileContent("public/page/index.html", "For some moments the old man")
|
||||
b.AssertFileContent("public/page/index.html", "Single.")
|
||||
// the alias redirector
|
||||
b.AssertFileContent("public/foo/bar/index.html", "ALIASTEMPLATE")
|
||||
}
|
||||
|
|
|
@ -72,12 +72,12 @@ func (f ContentFactory) ApplyArchetypeTemplate(w io.Writer, p page.Page, archety
|
|||
|
||||
templateSource = f.shortcodeReplacerPre.Replace(templateSource)
|
||||
|
||||
templ, err := ps.s.TextTmpl().Parse("archetype.md", string(templateSource))
|
||||
templ, err := ps.s.TemplateStore.TextParse("archetype.md", templateSource)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse archetype template: %s: %w", err, err)
|
||||
}
|
||||
|
||||
result, err := executeToString(context.Background(), ps.s.Tmpl(), templ, d)
|
||||
result, err := executeToString(context.Background(), ps.s.GetTemplateStore(), templ, d)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute archetype template: %s: %w", err, err)
|
||||
}
|
||||
|
|
|
@ -264,8 +264,8 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCoun
|
|||
meta := fi.Meta()
|
||||
pi := meta.PathInfo
|
||||
|
||||
switch pi.BundleType() {
|
||||
case paths.PathTypeFile, paths.PathTypeContentResource:
|
||||
switch pi.Type() {
|
||||
case paths.TypeFile, paths.TypeContentResource:
|
||||
m.s.Log.Trace(logg.StringFunc(
|
||||
func() string {
|
||||
return fmt.Sprintf("insert resource: %q", fi.Meta().Filename)
|
||||
|
@ -275,7 +275,7 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCoun
|
|||
addErr = err
|
||||
return
|
||||
}
|
||||
case paths.PathTypeContentData:
|
||||
case paths.TypeContentData:
|
||||
pc, rc, err := m.addPagesFromGoTmplFi(fi, buildConfig)
|
||||
pageCount += pc
|
||||
resourceCount += rc
|
||||
|
@ -349,8 +349,7 @@ func (m *pageMap) addPagesFromGoTmplFi(fi hugofs.FileMetaInfo, buildConfig *Buil
|
|||
DepsFromSite: func(s page.Site) pagesfromdata.PagesFromTemplateDeps {
|
||||
ss := s.(*Site)
|
||||
return pagesfromdata.PagesFromTemplateDeps{
|
||||
TmplFinder: ss.TextTmpl(),
|
||||
TmplExec: ss.Tmpl(),
|
||||
TemplateStore: ss.GetTemplateStore(),
|
||||
}
|
||||
},
|
||||
DependencyManager: s.Conf.NewIdentityManager("pagesfromdata"),
|
||||
|
|
|
@ -180,7 +180,7 @@ func (t *pageTrees) collectAndMarkStaleIdentities(p *paths.Path) []identity.Iden
|
|||
|
||||
if p.Component() == files.ComponentFolderContent {
|
||||
// It may also be a bundled content resource.
|
||||
key := p.ForBundleType(paths.PathTypeContentResource).Base()
|
||||
key := p.ForBundleType(paths.TypeContentResource).Base()
|
||||
tree = t.treeResources
|
||||
nCount = 0
|
||||
tree.ForEeachInDimension(key, doctree.DimensionLanguage.Index(),
|
||||
|
@ -1304,14 +1304,14 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
|
|||
checkedCounter atomic.Int64
|
||||
)
|
||||
|
||||
resetPo := func(po *pageOutput, r identity.FinderResult) {
|
||||
if po.pco != nil {
|
||||
resetPo := func(po *pageOutput, rebuildContent bool, r identity.FinderResult) {
|
||||
if rebuildContent && po.pco != nil {
|
||||
po.pco.Reset() // Will invalidate content cache.
|
||||
}
|
||||
|
||||
po.renderState = 0
|
||||
po.p.resourcesPublishInit = &sync.Once{}
|
||||
if r == identity.FinderFoundOneOfMany || po.f.Name == output.HTTPStatusHTMLFormat.Name {
|
||||
if r == identity.FinderFoundOneOfMany || po.f.Name == output.HTTPStatus404HTMLFormat.Name {
|
||||
// Will force a re-render even in fast render mode.
|
||||
po.renderOnce = false
|
||||
}
|
||||
|
@ -1323,7 +1323,7 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
|
|||
}
|
||||
|
||||
// This can be a relativeley expensive operations, so we do it in parallel.
|
||||
g := rungroup.Run[*pageState](ctx, rungroup.Config[*pageState]{
|
||||
g := rungroup.Run(ctx, rungroup.Config[*pageState]{
|
||||
NumWorkers: h.numWorkers,
|
||||
Handle: func(ctx context.Context, p *pageState) error {
|
||||
if !p.isRenderedAny() {
|
||||
|
@ -1335,7 +1335,8 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
|
|||
checkedCounter.Add(1)
|
||||
if r := depsFinder.Contains(id, p.dependencyManager, 2); r > identity.FinderFoundOneOfManyRepetition {
|
||||
for _, po := range p.pageOutputs {
|
||||
resetPo(po, r)
|
||||
// Note that p.dependencyManager is used when rendering content, so reset that.
|
||||
resetPo(po, true, r)
|
||||
}
|
||||
// Done.
|
||||
return nil
|
||||
|
@ -1351,7 +1352,8 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
|
|||
for _, id := range changes {
|
||||
checkedCounter.Add(1)
|
||||
if r := depsFinder.Contains(id, po.dependencyManagerOutput, 50); r > identity.FinderFoundOneOfManyRepetition {
|
||||
resetPo(po, r)
|
||||
// Note that dependencyManagerOutput is not used when rendering content, so don't reset that.
|
||||
resetPo(po, false, r)
|
||||
continue OUTPUTS
|
||||
}
|
||||
}
|
||||
|
@ -1954,7 +1956,7 @@ func (sa *sitePagesAssembler) addStandalonePages() error {
|
|||
tree.InsertIntoValuesDimension(key, p)
|
||||
}
|
||||
|
||||
addStandalone("/404", kinds.KindStatus404, output.HTTPStatusHTMLFormat)
|
||||
addStandalone("/404", kinds.KindStatus404, output.HTTPStatus404HTMLFormat)
|
||||
|
||||
if s.conf.EnableRobotsTXT {
|
||||
if m.i == 0 || s.Conf.IsMultihost() {
|
||||
|
|
|
@ -242,8 +242,13 @@ Data en
|
|||
}
|
||||
|
||||
func TestBundleMultipleContentPageWithSamePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
printPathWarnings = true
|
||||
-- layouts/all.html --
|
||||
All.
|
||||
-- content/bundle/index.md --
|
||||
---
|
||||
title: "Bundle md"
|
||||
|
@ -273,7 +278,10 @@ Bundle: {{ $bundle.Title }}|{{ $bundle.Params.foo }}|{{ $bundle.File.Filename }}
|
|||
P1: {{ $p1.Title }}|{{ $p1.Params.foo }}|{{ $p1.File.Filename }}|
|
||||
`
|
||||
|
||||
b := Test(t, files)
|
||||
for range 3 {
|
||||
b := Test(t, files, TestOptWarn())
|
||||
|
||||
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.
|
||||
|
@ -282,6 +290,7 @@ P1: {{ $p1.Title }}|{{ $p1.Params.foo }}|{{ $p1.File.Filename }}|
|
|||
filepath.FromSlash("P1: P1 md|md|/content/p1.md|"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Issue #11944
|
||||
func TestBundleResourcesGetWithSpacesInFilename(t *testing.T) {
|
||||
|
|
|
@ -17,6 +17,8 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestRenderHooksRSS(t *testing.T) {
|
||||
|
@ -129,6 +131,7 @@ P1: <p>P1. <a href="https://www.gohugo.io">I’m an inline-style link</a></p
|
|||
<h1 id="heading-in-p1">Heading in p1</h1>
|
||||
<h1 id="heading-in-p2">Heading in p2</h1>
|
||||
`)
|
||||
|
||||
b.AssertFileContent("public/index.xml", `
|
||||
P2: <p>P2. xml-link: https://www.bep.is|</p>
|
||||
P3: <p>P3. xml-link: https://www.example.org|</p>
|
||||
|
@ -378,3 +381,93 @@ Content: {{ .Content}}|
|
|||
"|Text: First line.\nSecond line.||\n",
|
||||
)
|
||||
}
|
||||
|
||||
func TestContentOutputReuseRenderHooksAndShortcodesHTMLOnly(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- layouts/index.html --
|
||||
HTML: {{ .Title }}|{{ .Content }}|
|
||||
-- layouts/index.xml --
|
||||
XML: {{ .Title }}|{{ .Content }}|
|
||||
-- layouts/_markup/render-heading.html --
|
||||
Render heading.
|
||||
-- layouts/shortcodes/myshortcode.html --
|
||||
My shortcode.
|
||||
-- content/_index.md --
|
||||
---
|
||||
title: "Home"
|
||||
---
|
||||
## Heading
|
||||
|
||||
{{< myshortcode >}}
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
s := b.H.Sites[0]
|
||||
b.Assert(s.home.pageOutputTemplateVariationsState.Load(), qt.Equals, uint32(1))
|
||||
b.AssertFileContent("public/index.html", "HTML: Home|Render heading.\nMy shortcode.\n|")
|
||||
b.AssertFileContent("public/index.xml", "XML: Home|Render heading.\nMy shortcode.\n|")
|
||||
}
|
||||
|
||||
func TestContentOutputNoReuseRenderHooksInBothHTMLAnXML(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term"]
|
||||
-- layouts/index.html --
|
||||
HTML: {{ .Title }}|{{ .Content }}|
|
||||
-- layouts/index.xml --
|
||||
XML: {{ .Title }}|{{ .Content }}|
|
||||
-- layouts/_markup/render-heading.html --
|
||||
Render heading.
|
||||
-- layouts/_markup/render-heading.xml --
|
||||
Render heading XML.
|
||||
-- content/_index.md --
|
||||
---
|
||||
title: "Home"
|
||||
---
|
||||
## Heading
|
||||
|
||||
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
s := b.H.Sites[0]
|
||||
b.Assert(s.home.pageOutputTemplateVariationsState.Load() > 1, qt.IsTrue)
|
||||
b.AssertFileContentExact("public/index.xml", "XML: Home|Render heading XML.|")
|
||||
b.AssertFileContentExact("public/index.html", "HTML: Home|Render heading.|")
|
||||
}
|
||||
|
||||
func TestContentOutputNoReuseShortcodesInBothHTMLAnXML(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term"]
|
||||
-- layouts/index.html --
|
||||
HTML: {{ .Title }}|{{ .Content }}|
|
||||
-- layouts/index.xml --
|
||||
XML: {{ .Title }}|{{ .Content }}|
|
||||
-- layouts/_markup/render-heading.html --
|
||||
Render heading.
|
||||
|
||||
-- layouts/shortcodes/myshortcode.html --
|
||||
My shortcode HTML.
|
||||
-- layouts/shortcodes/myshortcode.xml --
|
||||
My shortcode XML.
|
||||
-- content/_index.md --
|
||||
---
|
||||
title: "Home"
|
||||
---
|
||||
## Heading
|
||||
|
||||
{{< myshortcode >}}
|
||||
|
||||
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
// b.DebugPrint("", tplimpl.CategoryShortcode)
|
||||
|
||||
b.AssertFileContentExact("public/index.xml", "My shortcode XML.")
|
||||
b.AssertFileContentExact("public/index.html", "My shortcode HTML.")
|
||||
s := b.H.Sites[0]
|
||||
b.Assert(s.home.pageOutputTemplateVariationsState.Load() > 1, qt.IsTrue)
|
||||
}
|
||||
|
|
|
@ -14,35 +14,46 @@
|
|||
package doctree
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"sync"
|
||||
|
||||
radix "github.com/armon/go-radix"
|
||||
)
|
||||
|
||||
// Tree is a radix tree that holds T.
|
||||
// Tree is a non thread safe radix tree that holds T.
|
||||
type Tree[T any] interface {
|
||||
TreeCommon[T]
|
||||
WalkPrefix(s string, f func(s string, v T) (bool, error)) error
|
||||
WalkPath(s string, f func(s string, v T) (bool, error)) error
|
||||
All() iter.Seq2[string, T]
|
||||
}
|
||||
|
||||
// TreeThreadSafe is a thread safe radix tree that holds T.
|
||||
type TreeThreadSafe[T any] interface {
|
||||
TreeCommon[T]
|
||||
WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error
|
||||
WalkPath(lockType LockType, s string, f func(s string, v T) (bool, error)) error
|
||||
All(lockType LockType) iter.Seq2[string, T]
|
||||
}
|
||||
|
||||
type TreeCommon[T any] interface {
|
||||
Get(s string) T
|
||||
LongestPrefix(s string) (string, T)
|
||||
Insert(s string, v T) T
|
||||
WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error
|
||||
}
|
||||
|
||||
// NewSimpleTree creates a new SimpleTree.
|
||||
func NewSimpleTree[T comparable]() *SimpleTree[T] {
|
||||
func NewSimpleTree[T any]() *SimpleTree[T] {
|
||||
return &SimpleTree[T]{tree: radix.New()}
|
||||
}
|
||||
|
||||
// SimpleTree is a thread safe radix tree that holds T.
|
||||
type SimpleTree[T comparable] struct {
|
||||
mu sync.RWMutex
|
||||
// SimpleTree is a radix tree that holds T.
|
||||
// This tree is not thread safe.
|
||||
type SimpleTree[T any] struct {
|
||||
tree *radix.Tree
|
||||
zero T
|
||||
}
|
||||
|
||||
func (tree *SimpleTree[T]) Get(s string) T {
|
||||
tree.mu.RLock()
|
||||
defer tree.mu.RUnlock()
|
||||
|
||||
if v, ok := tree.tree.Get(s); ok {
|
||||
return v.(T)
|
||||
}
|
||||
|
@ -50,9 +61,6 @@ func (tree *SimpleTree[T]) Get(s string) T {
|
|||
}
|
||||
|
||||
func (tree *SimpleTree[T]) LongestPrefix(s string) (string, T) {
|
||||
tree.mu.RLock()
|
||||
defer tree.mu.RUnlock()
|
||||
|
||||
if s, v, ok := tree.tree.LongestPrefix(s); ok {
|
||||
return s, v.(T)
|
||||
}
|
||||
|
@ -60,17 +68,121 @@ func (tree *SimpleTree[T]) LongestPrefix(s string) (string, T) {
|
|||
}
|
||||
|
||||
func (tree *SimpleTree[T]) Insert(s string, v T) T {
|
||||
tree.tree.Insert(s, v)
|
||||
return v
|
||||
}
|
||||
|
||||
func (tree *SimpleTree[T]) Walk(f func(s string, v T) (bool, error)) error {
|
||||
var err error
|
||||
tree.tree.Walk(func(s string, v any) bool {
|
||||
var b bool
|
||||
b, err = f(s, v.(T))
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return b
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (tree *SimpleTree[T]) WalkPrefix(s string, f func(s string, v T) (bool, error)) error {
|
||||
var err error
|
||||
tree.tree.WalkPrefix(s, func(s string, v any) bool {
|
||||
var b bool
|
||||
b, err = f(s, v.(T))
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return b
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (tree *SimpleTree[T]) WalkPath(s string, f func(s string, v T) (bool, error)) error {
|
||||
var err error
|
||||
tree.tree.WalkPath(s, func(s string, v any) bool {
|
||||
var b bool
|
||||
b, err = f(s, v.(T))
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return b
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (tree *SimpleTree[T]) All() iter.Seq2[string, T] {
|
||||
return func(yield func(s string, v T) bool) {
|
||||
tree.tree.Walk(func(s string, v any) bool {
|
||||
return !yield(s, v.(T))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// NewSimpleThreadSafeTree creates a new SimpleTree.
|
||||
func NewSimpleThreadSafeTree[T any]() *SimpleThreadSafeTree[T] {
|
||||
return &SimpleThreadSafeTree[T]{tree: radix.New(), mu: new(sync.RWMutex)}
|
||||
}
|
||||
|
||||
// SimpleThreadSafeTree is a thread safe radix tree that holds T.
|
||||
type SimpleThreadSafeTree[T any] struct {
|
||||
mu *sync.RWMutex
|
||||
noLock bool
|
||||
tree *radix.Tree
|
||||
zero T
|
||||
}
|
||||
|
||||
var noopFunc = func() {}
|
||||
|
||||
func (tree *SimpleThreadSafeTree[T]) readLock() func() {
|
||||
if tree.noLock {
|
||||
return noopFunc
|
||||
}
|
||||
tree.mu.RLock()
|
||||
return tree.mu.RUnlock
|
||||
}
|
||||
|
||||
func (tree *SimpleThreadSafeTree[T]) writeLock() func() {
|
||||
if tree.noLock {
|
||||
return noopFunc
|
||||
}
|
||||
tree.mu.Lock()
|
||||
defer tree.mu.Unlock()
|
||||
return tree.mu.Unlock
|
||||
}
|
||||
|
||||
func (tree *SimpleThreadSafeTree[T]) Get(s string) T {
|
||||
unlock := tree.readLock()
|
||||
defer unlock()
|
||||
|
||||
if v, ok := tree.tree.Get(s); ok {
|
||||
return v.(T)
|
||||
}
|
||||
return tree.zero
|
||||
}
|
||||
|
||||
func (tree *SimpleThreadSafeTree[T]) LongestPrefix(s string) (string, T) {
|
||||
unlock := tree.readLock()
|
||||
defer unlock()
|
||||
|
||||
if s, v, ok := tree.tree.LongestPrefix(s); ok {
|
||||
return s, v.(T)
|
||||
}
|
||||
return "", tree.zero
|
||||
}
|
||||
|
||||
func (tree *SimpleThreadSafeTree[T]) Insert(s string, v T) T {
|
||||
unlock := tree.writeLock()
|
||||
defer unlock()
|
||||
|
||||
tree.tree.Insert(s, v)
|
||||
return v
|
||||
}
|
||||
|
||||
func (tree *SimpleTree[T]) Lock(lockType LockType) func() {
|
||||
func (tree *SimpleThreadSafeTree[T]) Lock(lockType LockType) func() {
|
||||
switch lockType {
|
||||
case LockTypeNone:
|
||||
return func() {}
|
||||
return noopFunc
|
||||
case LockTypeRead:
|
||||
tree.mu.RLock()
|
||||
return tree.mu.RUnlock
|
||||
|
@ -78,10 +190,16 @@ func (tree *SimpleTree[T]) Lock(lockType LockType) func() {
|
|||
tree.mu.Lock()
|
||||
return tree.mu.Unlock
|
||||
}
|
||||
return func() {}
|
||||
return noopFunc
|
||||
}
|
||||
|
||||
func (tree *SimpleTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
|
||||
func (tree SimpleThreadSafeTree[T]) LockTree(lockType LockType) (TreeThreadSafe[T], func()) {
|
||||
unlock := tree.Lock(lockType)
|
||||
tree.noLock = true
|
||||
return &tree, unlock // create a copy of tree with the noLock flag set to true.
|
||||
}
|
||||
|
||||
func (tree *SimpleThreadSafeTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
|
||||
commit := tree.Lock(lockType)
|
||||
defer commit()
|
||||
var err error
|
||||
|
@ -96,3 +214,31 @@ func (tree *SimpleTree[T]) WalkPrefix(lockType LockType, s string, f func(s stri
|
|||
|
||||
return err
|
||||
}
|
||||
|
||||
func (tree *SimpleThreadSafeTree[T]) WalkPath(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
|
||||
commit := tree.Lock(lockType)
|
||||
defer commit()
|
||||
var err error
|
||||
tree.tree.WalkPath(s, func(s string, v any) bool {
|
||||
var b bool
|
||||
b, err = f(s, v.(T))
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return b
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (tree *SimpleThreadSafeTree[T]) All(lockType LockType) iter.Seq2[string, T] {
|
||||
commit := tree.Lock(lockType)
|
||||
defer commit()
|
||||
return func(yield func(s string, v T) bool) {
|
||||
tree.tree.Walk(func(s string, v any) bool {
|
||||
return !yield(s, v.(T))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// iter.Seq[*TemplWithBaseApplied]
|
||||
|
|
|
@ -17,8 +17,6 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
radix "github.com/armon/go-radix"
|
||||
)
|
||||
|
||||
var _ MutableTrees = MutableTrees{}
|
||||
|
@ -60,11 +58,9 @@ func (ctx *WalkContext[T]) AddPostHook(handler func() error) {
|
|||
ctx.HooksPost = append(ctx.HooksPost, handler)
|
||||
}
|
||||
|
||||
func (ctx *WalkContext[T]) Data() *SimpleTree[any] {
|
||||
func (ctx *WalkContext[T]) Data() *SimpleThreadSafeTree[any] {
|
||||
ctx.dataInit.Do(func() {
|
||||
ctx.data = &SimpleTree[any]{
|
||||
tree: radix.New(),
|
||||
}
|
||||
ctx.data = NewSimpleThreadSafeTree[any]()
|
||||
})
|
||||
return ctx.data
|
||||
}
|
||||
|
@ -191,7 +187,7 @@ func (t MutableTrees) CanLock() bool {
|
|||
|
||||
// WalkContext is passed to the Walk callback.
|
||||
type WalkContext[T any] struct {
|
||||
data *SimpleTree[any]
|
||||
data *SimpleThreadSafeTree[any]
|
||||
dataInit sync.Once
|
||||
eventHandlers eventHandlers[T]
|
||||
events []*Event[T]
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
|
||||
package doctree
|
||||
|
||||
var _ Tree[string] = (*TreeShiftTree[string])(nil)
|
||||
import "iter"
|
||||
|
||||
var _ TreeThreadSafe[string] = (*TreeShiftTree[string])(nil)
|
||||
|
||||
type TreeShiftTree[T comparable] struct {
|
||||
// This tree is shiftable in one dimension.
|
||||
|
@ -26,16 +28,16 @@ type TreeShiftTree[T comparable] struct {
|
|||
zero T
|
||||
|
||||
// Will be of length equal to the length of the dimension.
|
||||
trees []*SimpleTree[T]
|
||||
trees []*SimpleThreadSafeTree[T]
|
||||
}
|
||||
|
||||
func NewTreeShiftTree[T comparable](d, length int) *TreeShiftTree[T] {
|
||||
if length <= 0 {
|
||||
panic("length must be > 0")
|
||||
}
|
||||
trees := make([]*SimpleTree[T], length)
|
||||
trees := make([]*SimpleThreadSafeTree[T], length)
|
||||
for i := range length {
|
||||
trees[i] = NewSimpleTree[T]()
|
||||
trees[i] = NewSimpleThreadSafeTree[T]()
|
||||
}
|
||||
return &TreeShiftTree[T]{d: d, trees: trees}
|
||||
}
|
||||
|
@ -91,6 +93,14 @@ func (t *TreeShiftTree[T]) WalkPrefixRaw(lockType LockType, s string, f func(s s
|
|||
return nil
|
||||
}
|
||||
|
||||
func (t *TreeShiftTree[T]) WalkPath(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
|
||||
return t.trees[t.v].WalkPath(lockType, s, f)
|
||||
}
|
||||
|
||||
func (t *TreeShiftTree[T]) All(lockType LockType) iter.Seq2[string, T] {
|
||||
return t.trees[t.v].All(lockType)
|
||||
}
|
||||
|
||||
func (t *TreeShiftTree[T]) LenRaw() int {
|
||||
var count int
|
||||
for _, tt := range t.trees {
|
||||
|
|
|
@ -134,7 +134,6 @@ func (h *HugoSites) resolveSite(lang string) *Site {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Only used in tests.
|
||||
type buildCounters struct {
|
||||
contentRenderCounter atomic.Uint64
|
||||
pageRenderCounter atomic.Uint64
|
||||
|
@ -557,7 +556,6 @@ func (h *HugoSites) handleDataFile(r *source.File) error {
|
|||
higherPrecedentData := current[r.BaseFileName()]
|
||||
|
||||
switch data.(type) {
|
||||
case nil:
|
||||
case map[string]any:
|
||||
|
||||
switch higherPrecedentData.(type) {
|
||||
|
|
|
@ -494,17 +494,17 @@ func (s *Site) executeDeferredTemplates(de *deps.DeferredExecutions) error {
|
|||
defer deferred.Mu.Unlock()
|
||||
|
||||
if !deferred.Executed {
|
||||
tmpl := s.Deps.Tmpl()
|
||||
templ, found := tmpl.Lookup(deferred.TemplateName)
|
||||
if !found {
|
||||
panic(fmt.Sprintf("template %q not found", deferred.TemplateName))
|
||||
tmpl := s.Deps.GetTemplateStore()
|
||||
ti := s.TemplateStore.LookupByPath(deferred.TemplatePath)
|
||||
if ti == nil {
|
||||
panic(fmt.Sprintf("template %q not found", deferred.TemplatePath))
|
||||
}
|
||||
|
||||
if err := func() error {
|
||||
buf := bufferpool.GetBuffer()
|
||||
defer bufferpool.PutBuffer(buf)
|
||||
|
||||
err = tmpl.ExecuteWithContext(deferred.Ctx, templ, buf, deferred.Data)
|
||||
err = tmpl.ExecuteWithContext(deferred.Ctx, ti, buf, deferred.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -577,9 +577,13 @@ func (h *HugoSites) printUnusedTemplatesOnce() error {
|
|||
h.printUnusedTemplatesInit.Do(func() {
|
||||
conf := h.Configs.Base
|
||||
if conf.PrintUnusedTemplates {
|
||||
unusedTemplates := h.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
|
||||
unusedTemplates := h.GetTemplateStore().UnusedTemplates()
|
||||
for _, unusedTemplate := range unusedTemplates {
|
||||
h.Log.Warnf("Template %s is unused, source file %s", unusedTemplate.Name(), unusedTemplate.Filename())
|
||||
if unusedTemplate.Fi != nil {
|
||||
h.Log.Warnf("Template %s is unused, source %q", unusedTemplate.PathInfo.Path(), unusedTemplate.Fi.Meta().Filename)
|
||||
} else {
|
||||
h.Log.Warnf("Template %s is unused", unusedTemplate.PathInfo.Path())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -954,7 +958,7 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
|
|||
case files.ComponentFolderLayouts:
|
||||
tmplChanged = true
|
||||
templatePath := pathInfo.Unnormalized().TrimLeadingSlash().PathNoLang()
|
||||
if !h.Tmpl().HasTemplate(templatePath) {
|
||||
if !h.GetTemplateStore().HasTemplate(templatePath) {
|
||||
tmplAdded = true
|
||||
}
|
||||
|
||||
|
@ -974,8 +978,9 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
|
|||
}
|
||||
} else {
|
||||
logger.Println("Template changed", pathInfo.Path())
|
||||
if templ, found := h.Tmpl().GetIdentity(templatePath); found {
|
||||
changes = append(changes, templ)
|
||||
id := h.GetTemplateStore().GetIdentity(pathInfo.Path())
|
||||
if id != nil {
|
||||
changes = append(changes, id)
|
||||
} else {
|
||||
changes = append(changes, pathInfo)
|
||||
}
|
||||
|
@ -1084,7 +1089,6 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
|
|||
|
||||
changed := &WhatChanged{
|
||||
needsPagesAssembly: needsPagesAssemble,
|
||||
identitySet: make(identity.Identities),
|
||||
}
|
||||
changed.Add(changes...)
|
||||
|
||||
|
@ -1106,17 +1110,39 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
|
|||
}
|
||||
}
|
||||
|
||||
h.Deps.OnChangeListeners.Notify(changed.Changes()...)
|
||||
changes2 := changed.Changes()
|
||||
h.Deps.OnChangeListeners.Notify(changes2...)
|
||||
|
||||
if err := h.resolveAndClearStateForIdentities(ctx, l, cacheBusterOr, changed.Drain()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tmplChanged || i18nChanged {
|
||||
if tmplChanged {
|
||||
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
|
||||
// TODO(bep) this could probably be optimized to somehow
|
||||
// only load the changed templates and its dependencies, but that is non-trivial.
|
||||
depsFinder := identity.NewFinder(identity.FinderConfig{})
|
||||
ll := l.WithField("substep", "rebuild templates")
|
||||
s := h.Sites[0]
|
||||
if err := s.Deps.TemplateStore.RefreshFiles(func(fi hugofs.FileMetaInfo) bool {
|
||||
pi := fi.Meta().PathInfo
|
||||
for _, id := range changes2 {
|
||||
if depsFinder.Contains(pi, id, -1) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}); err != nil {
|
||||
return ll, err
|
||||
}
|
||||
|
||||
return ll, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if i18nChanged {
|
||||
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
|
||||
ll := l.WithField("substep", "rebuild i18n")
|
||||
var prototype *deps.Deps
|
||||
for i, s := range h.Sites {
|
||||
if err := s.Deps.Compile(prototype); err != nil {
|
||||
|
|
|
@ -76,13 +76,14 @@ Single: {{ .Title }}|{{ .RelPermalink}}|{{ range .OutputFormats }}{{ .Name }}: {
|
|||
|
||||
`
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
|
|
@ -219,19 +219,31 @@ type IntegrationTestBuilder struct {
|
|||
|
||||
type lockingBuffer struct {
|
||||
sync.Mutex
|
||||
bytes.Buffer
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *lockingBuffer) String() string {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
return b.buf.String()
|
||||
}
|
||||
|
||||
func (b *lockingBuffer) Reset() {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
b.buf.Reset()
|
||||
}
|
||||
|
||||
func (b *lockingBuffer) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
b.Lock()
|
||||
n, err = b.Buffer.ReadFrom(r)
|
||||
n, err = b.buf.ReadFrom(r)
|
||||
b.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (b *lockingBuffer) Write(p []byte) (n int, err error) {
|
||||
b.Lock()
|
||||
n, err = b.Buffer.Write(p)
|
||||
n, err = b.buf.Write(p)
|
||||
b.Unlock()
|
||||
return
|
||||
}
|
||||
|
|
|
@ -28,15 +28,13 @@ import (
|
|||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/output"
|
||||
"github.com/gohugoio/hugo/output/layouts"
|
||||
"github.com/gohugoio/hugo/related"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gohugoio/hugo/markup/converter"
|
||||
"github.com/gohugoio/hugo/markup/tableofcontents"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
|
@ -116,6 +114,14 @@ type pageState struct {
|
|||
resourcesPublishInit *sync.Once
|
||||
}
|
||||
|
||||
func (p *pageState) incrPageOutputTemplateVariation() {
|
||||
p.pageOutputTemplateVariationsState.Add(1)
|
||||
}
|
||||
|
||||
func (p *pageState) canReusePageOutputContent() bool {
|
||||
return p.pageOutputTemplateVariationsState.Load() == 1
|
||||
}
|
||||
|
||||
func (p *pageState) IdentifierBase() string {
|
||||
return p.Path()
|
||||
}
|
||||
|
@ -169,10 +175,6 @@ func (p *pageState) resetBuildState() {
|
|||
// Nothing to do for now.
|
||||
}
|
||||
|
||||
func (p *pageState) reusePageOutputContent() bool {
|
||||
return p.pageOutputTemplateVariationsState.Load() == 1
|
||||
}
|
||||
|
||||
func (p *pageState) skipRender() bool {
|
||||
b := p.s.conf.C.SegmentFilter.ShouldExcludeFine(
|
||||
segments.SegmentMatcherFields{
|
||||
|
@ -474,49 +476,40 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *pageState) getLayoutDescriptor() layouts.LayoutDescriptor {
|
||||
p.layoutDescriptorInit.Do(func() {
|
||||
var section string
|
||||
sections := p.SectionsEntries()
|
||||
|
||||
switch p.Kind() {
|
||||
case kinds.KindSection:
|
||||
if len(sections) > 0 {
|
||||
section = sections[0]
|
||||
}
|
||||
case kinds.KindTaxonomy, kinds.KindTerm:
|
||||
|
||||
if p.m.singular != "" {
|
||||
section = p.m.singular
|
||||
} else if len(sections) > 0 {
|
||||
section = sections[0]
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
p.layoutDescriptor = layouts.LayoutDescriptor{
|
||||
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(),
|
||||
Type: p.Type(),
|
||||
Lang: p.Language().Lang,
|
||||
Layout: p.Layout(),
|
||||
Section: section,
|
||||
OutputFormat: f.Name,
|
||||
MediaType: f.MediaType.Type,
|
||||
IsPlainText: f.IsPlainText,
|
||||
}
|
||||
})
|
||||
|
||||
return p.layoutDescriptor
|
||||
}
|
||||
|
||||
func (p *pageState) resolveTemplate(layouts ...string) (tpl.Template, bool, error) {
|
||||
f := p.outputFormat()
|
||||
|
||||
d := p.getLayoutDescriptor()
|
||||
func (p *pageState) resolveTemplate(layouts ...string) (*tplimpl.TemplInfo, bool, error) {
|
||||
dir, d := p.getTemplateBasePathAndDescriptor()
|
||||
|
||||
if len(layouts) > 0 {
|
||||
d.Layout = layouts[0]
|
||||
d.LayoutOverride = true
|
||||
d.LayoutMustMatch = true
|
||||
}
|
||||
|
||||
return p.s.Tmpl().LookupLayout(d, f)
|
||||
q := tplimpl.TemplateQuery{
|
||||
Path: dir,
|
||||
Category: tplimpl.CategoryLayout,
|
||||
Desc: d,
|
||||
}
|
||||
|
||||
tinfo := p.s.TemplateStore.LookupPagesLayout(q)
|
||||
if tinfo == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
return tinfo, true, nil
|
||||
}
|
||||
|
||||
// Must be run after the site section tree etc. is built and ready.
|
||||
|
@ -705,7 +698,7 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
|
|||
|
||||
if isRenderingSite {
|
||||
cp := p.pageOutput.pco
|
||||
if cp == nil && p.reusePageOutputContent() {
|
||||
if cp == nil && p.canReusePageOutputContent() {
|
||||
// Look for content to reuse.
|
||||
for i := range p.pageOutputs {
|
||||
if i == idx {
|
||||
|
|
|
@ -21,7 +21,6 @@ import (
|
|||
"github.com/gohugoio/hugo/lazy"
|
||||
"github.com/gohugoio/hugo/markup/converter"
|
||||
"github.com/gohugoio/hugo/navigation"
|
||||
"github.com/gohugoio/hugo/output/layouts"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/gohugoio/hugo/source"
|
||||
|
@ -86,9 +85,6 @@ type pageCommon struct {
|
|||
// should look like.
|
||||
targetPathDescriptor page.TargetPathDescriptor
|
||||
|
||||
layoutDescriptor layouts.LayoutDescriptor
|
||||
layoutDescriptorInit sync.Once
|
||||
|
||||
// Set if feature enabled and this is in a Git repo.
|
||||
gitInfo source.GitInfo
|
||||
codeowners []string
|
||||
|
|
|
@ -24,6 +24,8 @@ import (
|
|||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
maps0 "maps"
|
||||
|
||||
"github.com/bep/logg"
|
||||
"github.com/gohugoio/hugo/common/hcontext"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
|
@ -32,7 +34,6 @@ import (
|
|||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/types/hstring"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/markup"
|
||||
"github.com/gohugoio/hugo/markup/converter"
|
||||
"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
|
||||
|
@ -45,7 +46,6 @@ import (
|
|||
"github.com/gohugoio/hugo/tpl"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/cast"
|
||||
maps0 "maps"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -600,7 +600,7 @@ func (c *cachedContentScope) contentRendered(ctx context.Context) (contentSummar
|
|||
return nil, err
|
||||
}
|
||||
if hasShortcodeVariants {
|
||||
cp.po.p.pageOutputTemplateVariationsState.Add(1)
|
||||
cp.po.p.incrPageOutputTemplateVariation()
|
||||
}
|
||||
|
||||
var result contentSummary
|
||||
|
@ -684,10 +684,9 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
|
|||
if err := cp.initRenderHooks(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f := cp.po.f
|
||||
po := cp.po
|
||||
p := po.p
|
||||
ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, p, f, false)
|
||||
ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, po, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -701,16 +700,14 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
|
|||
|
||||
if p.s.conf.Internal.Watch {
|
||||
for _, s := range cp2.po.p.m.content.shortcodeState.shortcodes {
|
||||
for _, templ := range s.templs {
|
||||
cp.trackDependency(templ.(identity.IdentityProvider))
|
||||
}
|
||||
cp.trackDependency(s.templ)
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer shortcode names so HasShortcode works for shortcodes from included pages.
|
||||
cp.po.p.m.content.shortcodeState.transferNames(cp2.po.p.m.content.shortcodeState)
|
||||
if cp2.po.p.pageOutputTemplateVariationsState.Load() > 0 {
|
||||
cp.po.p.pageOutputTemplateVariationsState.Add(1)
|
||||
cp.po.p.incrPageOutputTemplateVariation()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -723,7 +720,7 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
|
|||
}
|
||||
|
||||
if hasVariants {
|
||||
p.pageOutputTemplateVariationsState.Add(1)
|
||||
p.incrPageOutputTemplateVariation()
|
||||
}
|
||||
|
||||
isHTML := cp.po.p.m.pageConfig.ContentMediaType.IsHTML()
|
||||
|
@ -980,7 +977,7 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
|
|||
return "", err
|
||||
}
|
||||
|
||||
placeholders, err := s.prepareShortcodesForPage(ctx, pco.po.p, pco.po.f, true)
|
||||
placeholders, err := s.prepareShortcodesForPage(ctx, pco.po, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -990,7 +987,7 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
|
|||
return "", err
|
||||
}
|
||||
if hasVariants {
|
||||
pco.po.p.pageOutputTemplateVariationsState.Add(1)
|
||||
pco.po.p.incrPageOutputTemplateVariation()
|
||||
}
|
||||
b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false)
|
||||
if err != nil {
|
||||
|
@ -1028,7 +1025,7 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
|
|||
return "", err
|
||||
}
|
||||
if hasShortcodeVariants {
|
||||
pco.po.p.pageOutputTemplateVariationsState.Add(1)
|
||||
pco.po.p.incrPageOutputTemplateVariation()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1110,7 +1107,7 @@ func (c *cachedContentScope) RenderShortcodes(ctx context.Context) (template.HTM
|
|||
}
|
||||
|
||||
if hasVariants {
|
||||
pco.po.p.pageOutputTemplateVariationsState.Add(1)
|
||||
pco.po.p.incrPageOutputTemplateVariation()
|
||||
}
|
||||
|
||||
if cb != nil {
|
||||
|
|
|
@ -72,8 +72,11 @@ type pageMeta struct {
|
|||
|
||||
// Prepare for a rebuild of the data passed in from front matter.
|
||||
func (m *pageMeta) setMetaPostPrepareRebuild() {
|
||||
params := xmaps.Clone[map[string]any](m.paramsOriginal)
|
||||
params := xmaps.Clone(m.paramsOriginal)
|
||||
m.pageMetaParams.pageConfig = &pagemeta.PageConfig{
|
||||
Kind: m.pageConfig.Kind,
|
||||
Lang: m.pageConfig.Lang,
|
||||
Path: m.pageConfig.Path,
|
||||
Params: params,
|
||||
}
|
||||
m.pageMetaFrontMatter = pageMetaFrontMatter{}
|
||||
|
@ -108,10 +111,10 @@ func (p *pageMeta) Aliases() []string {
|
|||
}
|
||||
|
||||
func (p *pageMeta) BundleType() string {
|
||||
switch p.pathInfo.BundleType() {
|
||||
case paths.PathTypeLeaf:
|
||||
switch p.pathInfo.Type() {
|
||||
case paths.TypeLeaf:
|
||||
return "leaf"
|
||||
case paths.PathTypeBranch:
|
||||
case paths.TypeBranch:
|
||||
return "branch"
|
||||
default:
|
||||
return ""
|
||||
|
|
|
@ -19,23 +19,21 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||
"github.com/gohugoio/hugo/markup/highlight/chromalexers"
|
||||
"github.com/gohugoio/hugo/markup/tableofcontents"
|
||||
|
||||
"github.com/gohugoio/hugo/markup/converter"
|
||||
|
||||
bp "github.com/gohugoio/hugo/bufferpool"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
|
||||
"github.com/gohugoio/hugo/output"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
|
@ -120,9 +118,9 @@ func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (tem
|
|||
}
|
||||
|
||||
// Make sure to send the *pageState and not the *pageContentOutput to the template.
|
||||
res, err := executeToString(ctx, pco.po.p.s.Tmpl(), templ, pco.po.p)
|
||||
res, err := executeToString(ctx, pco.po.p.s.GetTemplateStore(), templ, pco.po.p)
|
||||
if err != nil {
|
||||
return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err))
|
||||
return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Template.Name(), err))
|
||||
}
|
||||
return template.HTML(res), nil
|
||||
}
|
||||
|
@ -274,103 +272,100 @@ func (pco *pageContentOutput) initRenderHooks() error {
|
|||
return r
|
||||
}
|
||||
|
||||
layoutDescriptor := pco.po.p.getLayoutDescriptor()
|
||||
layoutDescriptor.RenderingHook = true
|
||||
layoutDescriptor.LayoutOverride = false
|
||||
layoutDescriptor.Layout = ""
|
||||
// Inherit the descriptor from the page/current output format.
|
||||
// This allows for fine-grained control of the template used for
|
||||
// rendering of e.g. links.
|
||||
base, layoutDescriptor := pco.po.p.getTemplateBasePathAndDescriptor()
|
||||
|
||||
switch tp {
|
||||
case hooks.LinkRendererType:
|
||||
layoutDescriptor.Kind = "render-link"
|
||||
layoutDescriptor.Variant1 = "link"
|
||||
case hooks.ImageRendererType:
|
||||
layoutDescriptor.Kind = "render-image"
|
||||
layoutDescriptor.Variant1 = "image"
|
||||
case hooks.HeadingRendererType:
|
||||
layoutDescriptor.Kind = "render-heading"
|
||||
layoutDescriptor.Variant1 = "heading"
|
||||
case hooks.PassthroughRendererType:
|
||||
layoutDescriptor.Kind = "render-passthrough"
|
||||
layoutDescriptor.Variant1 = "passthrough"
|
||||
if id != nil {
|
||||
layoutDescriptor.KindVariants = id.(string)
|
||||
layoutDescriptor.Variant2 = id.(string)
|
||||
}
|
||||
case hooks.BlockquoteRendererType:
|
||||
layoutDescriptor.Kind = "render-blockquote"
|
||||
layoutDescriptor.Variant1 = "blockquote"
|
||||
if id != nil {
|
||||
layoutDescriptor.KindVariants = id.(string)
|
||||
layoutDescriptor.Variant2 = id.(string)
|
||||
}
|
||||
case hooks.TableRendererType:
|
||||
layoutDescriptor.Kind = "render-table"
|
||||
layoutDescriptor.Variant1 = "table"
|
||||
case hooks.CodeBlockRendererType:
|
||||
layoutDescriptor.Kind = "render-codeblock"
|
||||
layoutDescriptor.Variant1 = "codeblock"
|
||||
if id != nil {
|
||||
lang := id.(string)
|
||||
lexer := chromalexers.Get(lang)
|
||||
if lexer != nil {
|
||||
layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",")
|
||||
} else {
|
||||
layoutDescriptor.KindVariants = lang
|
||||
layoutDescriptor.Variant2 = id.(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getHookTemplate := func(f output.Format) (tpl.Template, bool) {
|
||||
templ, found, err := pco.po.p.s.Tmpl().LookupLayout(layoutDescriptor, f)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if found {
|
||||
if isitp, ok := templ.(tpl.IsInternalTemplateProvider); ok && isitp.IsInternalTemplate() {
|
||||
|
||||
renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
|
||||
|
||||
switch templ.Name() {
|
||||
case "_default/_markup/render-link.html":
|
||||
if !renderHookConfig.Link.IsEnableDefault() {
|
||||
return nil, false
|
||||
var ignoreInternal bool
|
||||
switch layoutDescriptor.Variant1 {
|
||||
case "link":
|
||||
ignoreInternal = !renderHookConfig.Link.IsEnableDefault()
|
||||
case "image":
|
||||
ignoreInternal = !renderHookConfig.Image.IsEnableDefault()
|
||||
}
|
||||
case "_default/_markup/render-image.html":
|
||||
if !renderHookConfig.Image.IsEnableDefault() {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return templ, found
|
||||
}
|
||||
|
||||
templ, found1 := getHookTemplate(pco.po.f)
|
||||
if !found1 || pco.po.p.reusePageOutputContent() {
|
||||
defaultOutputFormat := pco.po.p.s.conf.C.DefaultOutputFormat
|
||||
|
||||
candidates := pco.po.p.s.renderFormats
|
||||
|
||||
// Some hooks may only be available in HTML, and if
|
||||
// this site is configured to not have HTML output, we need to
|
||||
// make sure we have a fallback. This should be very rare.
|
||||
if pco.po.f.MediaType.FirstSuffix.Suffix != "html" {
|
||||
if _, found := candidates.GetBySuffix("html"); !found {
|
||||
candidates = append(candidates, output.HTMLFormat)
|
||||
}
|
||||
var numCandidatesFound int
|
||||
consider := func(candidate *tplimpl.TemplInfo) bool {
|
||||
if layoutDescriptor.Variant1 != candidate.D.Variant1 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if some of the other output formats would give a different template.
|
||||
for _, f := range candidates {
|
||||
if f.Name == pco.po.f.Name {
|
||||
continue
|
||||
}
|
||||
templ2, found2 := getHookTemplate(f)
|
||||
|
||||
if found2 {
|
||||
if !found1 && f.Name == defaultOutputFormat.Name {
|
||||
templ = templ2
|
||||
found1 = true
|
||||
break
|
||||
if layoutDescriptor.Variant2 != "" && candidate.D.Variant2 != "" && layoutDescriptor.Variant2 != candidate.D.Variant2 {
|
||||
return false
|
||||
}
|
||||
|
||||
if templ != templ2 {
|
||||
pco.po.p.pageOutputTemplateVariationsState.Add(1)
|
||||
break
|
||||
if ignoreInternal && candidate.SubCategory == tplimpl.SubCategoryEmbedded {
|
||||
// Don't consider the internal hook templates.
|
||||
return false
|
||||
}
|
||||
|
||||
if pco.po.p.pageOutputTemplateVariationsState.Load() > 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
if candidate.D.OutputFormat == "" {
|
||||
numCandidatesFound++
|
||||
} else if _, found := candidates.GetByName(candidate.D.OutputFormat); found {
|
||||
numCandidatesFound++
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
getHookTemplate := func() (*tplimpl.TemplInfo, bool) {
|
||||
q := tplimpl.TemplateQuery{
|
||||
Path: base,
|
||||
Category: tplimpl.CategoryMarkup,
|
||||
Desc: layoutDescriptor,
|
||||
Consider: consider,
|
||||
}
|
||||
|
||||
v := pco.po.p.s.TemplateStore.LookupPagesLayout(q)
|
||||
return v, v != nil
|
||||
}
|
||||
|
||||
templ, found1 := getHookTemplate()
|
||||
if found1 && templ == nil {
|
||||
panic("found1 is true, but templ is nil")
|
||||
}
|
||||
|
||||
if !found1 && layoutDescriptor.OutputFormat == pco.po.p.s.conf.DefaultOutputFormat {
|
||||
numCandidatesFound++
|
||||
}
|
||||
|
||||
if numCandidatesFound > 1 {
|
||||
// More than one output format candidate found for this hook temoplate,
|
||||
// so we cannot reuse the same rendered content.
|
||||
pco.po.p.incrPageOutputTemplateVariation()
|
||||
}
|
||||
|
||||
if !found1 {
|
||||
|
@ -384,7 +379,7 @@ func (pco *pageContentOutput) initRenderHooks() error {
|
|||
}
|
||||
|
||||
r := hookRendererTemplate{
|
||||
templateHandler: pco.po.p.s.Tmpl(),
|
||||
templateHandler: pco.po.p.s.GetTemplateStore(),
|
||||
templ: templ,
|
||||
resolvePosition: resolvePosition,
|
||||
}
|
||||
|
@ -488,7 +483,7 @@ func (t targetPathsHolder) targetPaths() page.TargetPaths {
|
|||
return t.paths
|
||||
}
|
||||
|
||||
func executeToString(ctx context.Context, h tpl.TemplateHandler, templ tpl.Template, data any) (string, error) {
|
||||
func executeToString(ctx context.Context, h *tplimpl.TemplateStore, templ *tplimpl.TemplInfo, data any) (string, error) {
|
||||
b := bp.GetBuffer()
|
||||
defer bp.PutBuffer(b)
|
||||
if err := h.ExecuteWithContext(ctx, templ, b, data); err != nil {
|
||||
|
|
|
@ -195,7 +195,7 @@ func (c *pagesCollector) Collect() (collectErr error) {
|
|||
return id.p.Dir() == fim.Meta().PathInfo.Dir()
|
||||
}
|
||||
|
||||
if fim.Meta().PathInfo.IsLeafBundle() && id.p.BundleType() == paths.PathTypeContentSingle {
|
||||
if fim.Meta().PathInfo.IsLeafBundle() && id.p.Type() == paths.TypeContentSingle {
|
||||
return id.p.Dir() == fim.Meta().PathInfo.Dir()
|
||||
}
|
||||
|
||||
|
@ -314,7 +314,7 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
|
|||
return nil, filepath.SkipDir
|
||||
}
|
||||
|
||||
seen := map[hstrings.Tuple]bool{}
|
||||
seen := map[hstrings.Strings2]hugofs.FileMetaInfo{}
|
||||
for _, fi := range readdir {
|
||||
if fi.IsDir() {
|
||||
continue
|
||||
|
@ -327,11 +327,14 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
|
|||
// These would eventually have been filtered out as duplicates when
|
||||
// inserting them into the document store,
|
||||
// but doing it here will preserve a consistent ordering.
|
||||
baseLang := hstrings.Tuple{First: pi.Base(), Second: meta.Lang}
|
||||
if seen[baseLang] {
|
||||
baseLang := hstrings.Strings2{pi.Base(), meta.Lang}
|
||||
if fi2, ok := seen[baseLang]; ok {
|
||||
if c.h.Configs.Base.PrintPathWarnings && !c.h.isRebuild() {
|
||||
c.logger.Warnf("Duplicate content path: %q file: %q file: %q", pi.Base(), fi2.Meta().Filename, meta.Filename)
|
||||
}
|
||||
continue
|
||||
}
|
||||
seen[baseLang] = true
|
||||
seen[baseLang] = fi
|
||||
|
||||
if pi == nil {
|
||||
panic(fmt.Sprintf("no path info for %q", meta.Filename))
|
||||
|
@ -374,7 +377,7 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
|
|||
|
||||
func (c *pagesCollector) handleBundleLeaf(dir, bundle hugofs.FileMetaInfo, inPath string, readdir []hugofs.FileMetaInfo) error {
|
||||
bundlePi := bundle.Meta().PathInfo
|
||||
seen := map[hstrings.Tuple]bool{}
|
||||
seen := map[hstrings.Strings2]bool{}
|
||||
|
||||
walk := func(path string, info hugofs.FileMetaInfo) error {
|
||||
if info.IsDir() {
|
||||
|
@ -396,7 +399,7 @@ func (c *pagesCollector) handleBundleLeaf(dir, bundle hugofs.FileMetaInfo, inPat
|
|||
// These would eventually have been filtered out as duplicates when
|
||||
// inserting them into the document store,
|
||||
// but doing it here will preserve a consistent ordering.
|
||||
baseLang := hstrings.Tuple{First: pi.Base(), Second: info.Meta().Lang}
|
||||
baseLang := hstrings.Strings2{pi.Base(), info.Meta().Lang}
|
||||
if seen[baseLang] {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import (
|
|||
"github.com/gohugoio/hugo/resources/page/pagemeta"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
@ -167,8 +168,7 @@ type PagesFromTemplateOptions struct {
|
|||
}
|
||||
|
||||
type PagesFromTemplateDeps struct {
|
||||
TmplFinder tpl.TemplateParseFinder
|
||||
TmplExec tpl.TemplateExecutor
|
||||
TemplateStore *tplimpl.TemplateStore
|
||||
}
|
||||
|
||||
var _ resource.Staler = (*PagesFromTemplate)(nil)
|
||||
|
@ -303,7 +303,7 @@ func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
|
|||
}
|
||||
defer f.Close()
|
||||
|
||||
tmpl, err := p.TmplFinder.Parse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
|
||||
tmpl, err := p.TemplateStore.TextParse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
|
||||
if err != nil {
|
||||
return BuildInfo{}, err
|
||||
}
|
||||
|
@ -314,7 +314,7 @@ func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
|
|||
|
||||
ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p)
|
||||
|
||||
if err := p.TmplExec.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil {
|
||||
if err := p.TemplateStore.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil {
|
||||
return BuildInfo{}, err
|
||||
}
|
||||
|
||||
|
|
|
@ -98,7 +98,8 @@ ADD_MORE_PLACEHOLDER
|
|||
|
||||
func TestPagesFromGoTmplMisc(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := hugolib.Test(t, filesPagesFromDataTempleBasic)
|
||||
b := hugolib.Test(t, filesPagesFromDataTempleBasic, hugolib.TestOptWarn())
|
||||
b.AssertLogContains("! WARN")
|
||||
b.AssertPublishDir(`
|
||||
docs/p1/mytext.txt
|
||||
docs/p1/sub/mytex2.tx
|
||||
|
|
|
@ -15,7 +15,6 @@ package hugolib
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
|
@ -102,10 +101,18 @@ URL: {{ $pag.URL }}
|
|||
|
||||
// Issue 6023
|
||||
func TestPaginateWithSort(t *testing.T) {
|
||||
b := newTestSitesBuilder(t).WithSimpleConfigFile()
|
||||
b.WithTemplatesAdded("index.html", `{{ range (.Paginate (sort .Site.RegularPages ".File.Filename" "desc")).Pages }}|{{ .File.Filename }}{{ end }}`)
|
||||
b.Build(BuildCfg{}).AssertFileContent("public/index.html",
|
||||
filepath.FromSlash("|content/sect/doc1.nn.md|content/sect/doc1.nb.md|content/sect/doc1.fr.md|content/sect/doc1.en.md"))
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- content/a/a.md --
|
||||
-- content/z/b.md --
|
||||
-- content/x/b.md --
|
||||
-- content/x/a.md --
|
||||
-- layouts/home.html --
|
||||
Paginate: {{ range (.Paginate (sort .Site.RegularPages ".File.Filename" "desc")).Pages }}|{{ .Path }}{{ end }}
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "Paginate: |/z/b|/x/b|/x/a|/a/a")
|
||||
}
|
||||
|
||||
// https://github.com/gohugoio/hugo/issues/6797
|
||||
|
@ -183,5 +190,5 @@ 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 }}`)
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ My Section Bundle Content Content.
|
|||
title: "My Section"
|
||||
---
|
||||
-- content/mysection/mysectiontext.txt --
|
||||
Content.
|
||||
-- content/_index.md --
|
||||
---
|
||||
title: "Home"
|
||||
|
@ -99,16 +100,18 @@ My Other Text: {{ $r.Content }}|{{ $r.Permalink }}|
|
|||
`
|
||||
|
||||
func TestRebuildEditLeafBundleHeaderOnly(t *testing.T) {
|
||||
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 := TestRunning(t, rebuildFilesSimple)
|
||||
|
@ -119,7 +122,7 @@ func TestRebuildEditTextFileInLeafBundle(t *testing.T) {
|
|||
b.AssertFileContent("public/mysection/mysectionbundle/index.html",
|
||||
"Text 2 Content Edited")
|
||||
b.AssertRenderCountPage(1)
|
||||
b.AssertRenderCountContent(1)
|
||||
b.AssertRenderCountContent(0)
|
||||
}
|
||||
|
||||
func TestRebuildEditTextFileInShortcode(t *testing.T) {
|
||||
|
@ -180,17 +183,17 @@ func TestRebuildEditTextFileInHomeBundle(t *testing.T) {
|
|||
b.AssertFileContent("public/index.html", "Home Content.")
|
||||
b.AssertFileContent("public/index.html", "Home Text Content Edited.")
|
||||
b.AssertRenderCountPage(1)
|
||||
b.AssertRenderCountContent(1)
|
||||
b.AssertRenderCountContent(0)
|
||||
}
|
||||
|
||||
func TestRebuildEditTextFileInBranchBundle(t *testing.T) {
|
||||
b := TestRunning(t, rebuildFilesSimple)
|
||||
b.AssertFileContent("public/mysection/index.html", "My Section")
|
||||
b.AssertFileContent("public/mysection/index.html", "My Section", "0:/mysection/mysectiontext.txt|Content.|")
|
||||
|
||||
b.EditFileReplaceAll("content/mysection/mysectiontext.txt", "Content.", "Content Edited.").Build()
|
||||
b.AssertFileContent("public/mysection/index.html", "My Section")
|
||||
b.AssertFileContent("public/mysection/index.html", "My Section", "0:/mysection/mysectiontext.txt|Content Edited.|")
|
||||
b.AssertRenderCountPage(1)
|
||||
b.AssertRenderCountContent(1)
|
||||
b.AssertRenderCountContent(0)
|
||||
}
|
||||
|
||||
func testRebuildBothWatchingAndRunning(t *testing.T, files string, withB func(b *IntegrationTestBuilder)) {
|
||||
|
@ -484,7 +487,43 @@ Home: {{ .Title }}|{{ .Content }}|
|
|||
})
|
||||
}
|
||||
|
||||
func TestRebuildSingleWithBaseof(t *testing.T) {
|
||||
func TestRebuildSingle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
title = "Hugo Site"
|
||||
baseURL = "https://example.com"
|
||||
disableKinds = ["term", "taxonomy", "sitemap", "robotstxt", "404"]
|
||||
disableLiveReload = true
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: "P1"
|
||||
---
|
||||
P1 Content.
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
-- layouts/single.html --
|
||||
Single: {{ .Title }}|{{ .Content }}|
|
||||
{{ with (templates.Defer (dict "key" "global")) }}
|
||||
Defer.
|
||||
{{ end }}
|
||||
`
|
||||
b := Test(t, files, TestOptRunning())
|
||||
b.AssertFileContent("public/p1/index.html", "Single: P1|", "Defer.")
|
||||
b.AssertRenderCountPage(3)
|
||||
b.AssertRenderCountContent(1)
|
||||
b.EditFileReplaceFunc("layouts/single.html", func(s string) string {
|
||||
s = strings.Replace(s, "Single", "Single Edited", 1)
|
||||
s = strings.Replace(s, "Defer.", "Defer Edited.", 1)
|
||||
return s
|
||||
}).Build()
|
||||
b.AssertFileContent("public/p1/index.html", "Single Edited: P1|", "Defer Edited.")
|
||||
b.AssertRenderCountPage(1)
|
||||
b.AssertRenderCountContent(0)
|
||||
}
|
||||
|
||||
func TestRebuildSingleWithBaseofEditSingle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
|
@ -498,9 +537,13 @@ disableLiveReload = true
|
|||
title: "P1"
|
||||
---
|
||||
P1 Content.
|
||||
[foo](/foo)
|
||||
-- layouts/_default/baseof.html --
|
||||
Baseof: {{ .Title }}|
|
||||
{{ block "main" . }}default{{ end }}
|
||||
{{ with (templates.Defer (dict "foo" "bar")) }}
|
||||
Defer.
|
||||
{{ end }}
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
-- layouts/_default/single.html --
|
||||
|
@ -509,11 +552,81 @@ Single: {{ .Title }}|{{ .Content }}|
|
|||
{{ end }}
|
||||
`
|
||||
b := Test(t, files, TestOptRunning())
|
||||
b.AssertFileContent("public/p1/index.html", "Baseof: P1|\n\nSingle: P1|<p>P1 Content.</p>\n|")
|
||||
b.AssertFileContent("public/p1/index.html", "Single: P1|")
|
||||
b.EditFileReplaceFunc("layouts/_default/single.html", func(s string) string {
|
||||
return strings.Replace(s, "Single", "Single Edited", 1)
|
||||
}).Build()
|
||||
b.AssertFileContent("public/p1/index.html", "Baseof: P1|\n\nSingle Edited: P1|<p>P1 Content.</p>\n|")
|
||||
b.AssertFileContent("public/p1/index.html", "Single Edited")
|
||||
}
|
||||
|
||||
func TestRebuildSingleWithBaseofEditBaseof(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
title = "Hugo Site"
|
||||
baseURL = "https://example.com"
|
||||
disableKinds = ["term", "taxonomy"]
|
||||
disableLiveReload = true
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: "P1"
|
||||
---
|
||||
P1 Content.
|
||||
[foo](/foo)
|
||||
-- layouts/_default/baseof.html --
|
||||
Baseof: {{ .Title }}|
|
||||
{{ block "main" . }}default{{ end }}
|
||||
{{ with (templates.Defer (dict "foo" "bar")) }}
|
||||
Defer.
|
||||
{{ end }}
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
-- layouts/_default/single.html --
|
||||
{{ define "main" }}
|
||||
Single: {{ .Title }}|{{ .Content }}|
|
||||
{{ end }}
|
||||
`
|
||||
b := Test(t, files, TestOptRunning())
|
||||
b.AssertFileContent("public/p1/index.html", "Single: P1|")
|
||||
fmt.Println("===============")
|
||||
b.EditFileReplaceAll("layouts/_default/baseof.html", "Baseof", "Baseof Edited").Build()
|
||||
b.AssertFileContent("public/p1/index.html", "Baseof Edited")
|
||||
}
|
||||
|
||||
func TestRebuildWithDeferEditRenderHook(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
title = "Hugo Site"
|
||||
baseURL = "https://example.com"
|
||||
disableKinds = ["term", "taxonomy"]
|
||||
disableLiveReload = true
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: "P1"
|
||||
---
|
||||
P1 Content.
|
||||
[foo](/foo)
|
||||
-- layouts/_default/baseof.html --
|
||||
Baseof: {{ .Title }}|
|
||||
{{ block "main" . }}default{{ end }}
|
||||
{{ with (templates.Defer (dict "foo" "bar")) }}
|
||||
Defer.
|
||||
{{ end }}
|
||||
-- layouts/single.html --
|
||||
{{ define "main" }}
|
||||
Single: {{ .Title }}|{{ .Content }}|
|
||||
{{ end }}
|
||||
-- layouts/_default/_markup/render-link.html --
|
||||
Render Link.
|
||||
`
|
||||
b := Test(t, files, TestOptRunning())
|
||||
// Edit render hook.
|
||||
b.EditFileReplaceAll("layouts/_default/_markup/render-link.html", "Render Link", "Render Link Edited").Build()
|
||||
|
||||
b.AssertFileContent("public/p1/index.html", "Render Link Edited")
|
||||
}
|
||||
|
||||
func TestRebuildFromString(t *testing.T) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -29,6 +29,7 @@ import (
|
|||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
|
||||
"github.com/gohugoio/hugo/parser/pageparser"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
|
@ -36,7 +37,6 @@ import (
|
|||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
"github.com/gohugoio/hugo/common/urls"
|
||||
"github.com/gohugoio/hugo/output"
|
||||
|
||||
bp "github.com/gohugoio/hugo/bufferpool"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
|
@ -205,8 +205,7 @@ type shortcode struct {
|
|||
|
||||
indentation string // indentation from source.
|
||||
|
||||
info tpl.Info // One of the output formats (arbitrary)
|
||||
templs []tpl.Template // All output formats
|
||||
templ *tplimpl.TemplInfo
|
||||
|
||||
// If set, the rendered shortcode is sent as part of the surrounding content
|
||||
// to Goldmark and similar.
|
||||
|
@ -230,16 +229,15 @@ func (s shortcode) insertPlaceholder() bool {
|
|||
}
|
||||
|
||||
func (s shortcode) needsInner() bool {
|
||||
return s.info != nil && s.info.ParseInfo().IsInner
|
||||
return s.templ != nil && s.templ.ParseInfo.IsInner
|
||||
}
|
||||
|
||||
func (s shortcode) configVersion() int {
|
||||
if s.info == nil {
|
||||
if s.templ == nil {
|
||||
// Not set for inline shortcodes.
|
||||
return 2
|
||||
}
|
||||
|
||||
return s.info.ParseInfo().Config.Version
|
||||
return s.templ.ParseInfo.Config.Version
|
||||
}
|
||||
|
||||
func (s shortcode) innerString() string {
|
||||
|
@ -315,12 +313,12 @@ func prepareShortcode(
|
|||
ctx context.Context,
|
||||
level int,
|
||||
s *Site,
|
||||
tplVariants tpl.TemplateVariants,
|
||||
sc *shortcode,
|
||||
parent *ShortcodeWithPage,
|
||||
p *pageState,
|
||||
po *pageOutput,
|
||||
isRenderString bool,
|
||||
) (shortcodeRenderer, error) {
|
||||
p := po.p
|
||||
toParseErr := func(err error) error {
|
||||
source := p.m.content.mustSource()
|
||||
return p.parseError(fmt.Errorf("failed to render shortcode %q: %w", sc.name, err), source, sc.pos)
|
||||
|
@ -333,7 +331,7 @@ func prepareShortcode(
|
|||
// parsed and rendered by Goldmark.
|
||||
ctx = tpl.Context.IsInGoldmark.Set(ctx, true)
|
||||
}
|
||||
r, err := doRenderShortcode(ctx, level, s, tplVariants, sc, parent, p, isRenderString)
|
||||
r, err := doRenderShortcode(ctx, level, s, sc, parent, po, isRenderString)
|
||||
if err != nil {
|
||||
return nil, false, toParseErr(err)
|
||||
}
|
||||
|
@ -352,30 +350,29 @@ func doRenderShortcode(
|
|||
ctx context.Context,
|
||||
level int,
|
||||
s *Site,
|
||||
tplVariants tpl.TemplateVariants,
|
||||
sc *shortcode,
|
||||
parent *ShortcodeWithPage,
|
||||
p *pageState,
|
||||
po *pageOutput,
|
||||
isRenderString bool,
|
||||
) (shortcodeRenderer, error) {
|
||||
var tmpl tpl.Template
|
||||
var tmpl *tplimpl.TemplInfo
|
||||
p := po.p
|
||||
|
||||
// Tracks whether this shortcode or any of its children has template variations
|
||||
// in other languages or output formats. We are currently only interested in
|
||||
// the output formats, so we may get some false positives -- we
|
||||
// should improve on that.
|
||||
// the output formats.
|
||||
var hasVariants bool
|
||||
|
||||
if sc.isInline {
|
||||
if !p.s.ExecHelper.Sec().EnableInlineShortcodes {
|
||||
return zeroShortcode, nil
|
||||
}
|
||||
templName := path.Join("_inline_shortcode", p.Path(), sc.name)
|
||||
templatePath := path.Join("_inline_shortcode", p.Path(), sc.name)
|
||||
if sc.isClosing {
|
||||
templStr := sc.innerString()
|
||||
|
||||
var err error
|
||||
tmpl, err = s.TextTmpl().Parse(templName, templStr)
|
||||
tmpl, err = s.TemplateStore.TextParse(templatePath, templStr)
|
||||
if err != nil {
|
||||
if isRenderString {
|
||||
return zeroShortcode, p.wrapError(err)
|
||||
|
@ -389,21 +386,32 @@ func doRenderShortcode(
|
|||
|
||||
} else {
|
||||
// Re-use of shortcode defined earlier in the same page.
|
||||
var found bool
|
||||
tmpl, found = s.TextTmpl().Lookup(templName)
|
||||
if !found {
|
||||
tmpl = s.TemplateStore.TextLookup(templatePath)
|
||||
if tmpl == nil {
|
||||
return zeroShortcode, fmt.Errorf("no earlier definition of shortcode %q found", sc.name)
|
||||
}
|
||||
}
|
||||
tmpl = tpl.AddIdentity(tmpl)
|
||||
} else {
|
||||
var found, more bool
|
||||
tmpl, found, more = s.Tmpl().LookupVariant(sc.name, tplVariants)
|
||||
if !found {
|
||||
ofCount := map[string]int{}
|
||||
include := func(match *tplimpl.TemplInfo) bool {
|
||||
ofCount[match.D.OutputFormat]++
|
||||
return true
|
||||
}
|
||||
base, layoutDescriptor := po.getTemplateBasePathAndDescriptor()
|
||||
q := tplimpl.TemplateQuery{
|
||||
Path: base,
|
||||
Name: sc.name,
|
||||
Category: tplimpl.CategoryShortcode,
|
||||
Desc: layoutDescriptor,
|
||||
Consider: include,
|
||||
}
|
||||
v := s.TemplateStore.LookupShortcode(q)
|
||||
if v == nil {
|
||||
s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path())
|
||||
return zeroShortcode, nil
|
||||
}
|
||||
hasVariants = hasVariants || more
|
||||
tmpl = v
|
||||
hasVariants = hasVariants || len(ofCount) > 1
|
||||
}
|
||||
|
||||
data := &ShortcodeWithPage{
|
||||
|
@ -427,7 +435,7 @@ func doRenderShortcode(
|
|||
case string:
|
||||
inner += innerData
|
||||
case *shortcode:
|
||||
s, err := prepareShortcode(ctx, level+1, s, tplVariants, innerData, data, p, isRenderString)
|
||||
s, err := prepareShortcode(ctx, level+1, s, innerData, data, po, isRenderString)
|
||||
if err != nil {
|
||||
return zeroShortcode, err
|
||||
}
|
||||
|
@ -484,7 +492,7 @@ func doRenderShortcode(
|
|||
|
||||
}
|
||||
|
||||
result, err := renderShortcodeWithPage(ctx, s.Tmpl(), tmpl, data)
|
||||
result, err := renderShortcodeWithPage(ctx, s.GetTemplateStore(), tmpl, data)
|
||||
|
||||
if err != nil && sc.isInline {
|
||||
fe := herrors.NewFileErrorFromName(err, p.File().Filename())
|
||||
|
@ -534,16 +542,11 @@ func (s *shortcodeHandler) hasName(name string) bool {
|
|||
return ok
|
||||
}
|
||||
|
||||
func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, p *pageState, f output.Format, isRenderString bool) (map[string]shortcodeRenderer, error) {
|
||||
func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, po *pageOutput, isRenderString bool) (map[string]shortcodeRenderer, error) {
|
||||
rendered := make(map[string]shortcodeRenderer)
|
||||
|
||||
tplVariants := tpl.TemplateVariants{
|
||||
Language: p.Language().Lang,
|
||||
OutputFormat: f,
|
||||
}
|
||||
|
||||
for _, v := range s.shortcodes {
|
||||
s, err := prepareShortcode(ctx, 0, s.s, tplVariants, v, nil, p, isRenderString)
|
||||
s, err := prepareShortcode(ctx, 0, s.s, v, nil, po, isRenderString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -636,7 +639,7 @@ Loop:
|
|||
// we trust the template on this:
|
||||
// if there's no inner, we're done
|
||||
if !sc.isInline {
|
||||
if !sc.info.ParseInfo().IsInner {
|
||||
if !sc.templ.ParseInfo.IsInner {
|
||||
return sc, nil
|
||||
}
|
||||
}
|
||||
|
@ -672,14 +675,19 @@ Loop:
|
|||
|
||||
sc.name = currItem.ValStr(source)
|
||||
|
||||
// Used to check if the template expects inner content.
|
||||
templs := s.s.Tmpl().LookupVariants(sc.name)
|
||||
if templs == nil {
|
||||
// Used to check if the template expects inner content,
|
||||
// so just pick one arbitrarily with the same name.
|
||||
q := tplimpl.TemplateQuery{
|
||||
Path: "",
|
||||
Name: sc.name,
|
||||
Category: tplimpl.CategoryShortcode,
|
||||
Consider: nil,
|
||||
}
|
||||
templ := s.s.TemplateStore.LookupShortcode(q)
|
||||
if templ == nil {
|
||||
return nil, fmt.Errorf("%s: template for shortcode %q not found", errorPrefix, sc.name)
|
||||
}
|
||||
|
||||
sc.info = templs[0].(tpl.Info)
|
||||
sc.templs = templs
|
||||
sc.templ = templ
|
||||
case currItem.IsInlineShortcodeName():
|
||||
sc.name = currItem.ValStr(source)
|
||||
sc.isInline = true
|
||||
|
@ -778,7 +786,7 @@ func expandShortcodeTokens(
|
|||
return source, nil
|
||||
}
|
||||
|
||||
func renderShortcodeWithPage(ctx context.Context, h tpl.TemplateHandler, tmpl tpl.Template, data *ShortcodeWithPage) (string, error) {
|
||||
func renderShortcodeWithPage(ctx context.Context, h *tplimpl.TemplateStore, tmpl *tplimpl.TemplInfo, data *ShortcodeWithPage) (string, error) {
|
||||
buffer := bp.GetBuffer()
|
||||
defer bp.PutBuffer(buffer)
|
||||
|
||||
|
|
|
@ -33,14 +33,14 @@ func TestExtractShortcodes(t *testing.T) {
|
|||
b := newTestSitesBuilder(t).WithSimpleConfigFile()
|
||||
|
||||
b.WithTemplates(
|
||||
"default/single.html", `EMPTY`,
|
||||
"_internal/shortcodes/tag.html", `tag`,
|
||||
"_internal/shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`,
|
||||
"_internal/shortcodes/sc1.html", `sc1`,
|
||||
"_internal/shortcodes/sc2.html", `sc2`,
|
||||
"_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`,
|
||||
"_internal/shortcodes/inner2.html", `{{.Inner}}`,
|
||||
"_internal/shortcodes/inner3.html", `{{.Inner}}`,
|
||||
"pages/single.html", `EMPTY`,
|
||||
"shortcodes/tag.html", `tag`,
|
||||
"shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`,
|
||||
"shortcodes/sc1.html", `sc1`,
|
||||
"shortcodes/sc2.html", `sc2`,
|
||||
"shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`,
|
||||
"shortcodes/inner2.html", `{{.Inner}}`,
|
||||
"shortcodes/inner3.html", `{{.Inner}}`,
|
||||
).WithContent("page.md", `---
|
||||
title: "Shortcodes Galore!"
|
||||
---
|
||||
|
@ -57,10 +57,9 @@ title: "Shortcodes Galore!"
|
|||
if s == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
var version int
|
||||
if s.info != nil {
|
||||
version = s.info.ParseInfo().Config.Version
|
||||
if s.templ != nil {
|
||||
version = s.templ.ParseInfo.Config.Version
|
||||
}
|
||||
return strReplacer.Replace(fmt.Sprintf("%s;inline:%t;closing:%t;inner:%v;params:%v;ordinal:%d;markup:%t;version:%d;pos:%d",
|
||||
s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, version, s.pos))
|
||||
|
@ -69,7 +68,7 @@ title: "Shortcodes Galore!"
|
|||
regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) {
|
||||
return func(c *qt.C, shortcode *shortcode, err error) {
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(str(shortcode), qt.Matches, ".*"+re+".*")
|
||||
c.Assert(str(shortcode), qt.Matches, ".*"+re+".*", qt.Commentf("%s", shortcode.name))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -888,6 +887,7 @@ outputs: ["html", "css", "csv", "json"]
|
|||
"_default/single.json", "{{ .Content }}",
|
||||
"shortcodes/myshort.html", `Short-HTML`,
|
||||
"shortcodes/myshort.csv", `Short-CSV`,
|
||||
"shortcodes/myshort.txt", `Short-TXT`,
|
||||
)
|
||||
|
||||
b.Build(BuildCfg{})
|
||||
|
@ -897,12 +897,12 @@ outputs: ["html", "css", "csv", "json"]
|
|||
for i := range numPages {
|
||||
b.AssertFileContent(fmt.Sprintf("public/page%d/index.html", i), "Short-HTML")
|
||||
b.AssertFileContent(fmt.Sprintf("public/page%d/index.csv", i), "Short-CSV")
|
||||
b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-HTML")
|
||||
b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-CSV")
|
||||
|
||||
}
|
||||
|
||||
for i := range numPages {
|
||||
b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-HTML")
|
||||
b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-CSV")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -47,7 +47,13 @@ import (
|
|||
"github.com/gohugoio/hugo/langs/i18n"
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
"github.com/gohugoio/hugo/resources"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
"github.com/gohugoio/hugo/tpl/tplimplinit"
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
|
||||
// Loads the template funcs namespaces.
|
||||
|
||||
"golang.org/x/text/unicode/norm"
|
||||
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
|
@ -188,8 +194,8 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
|
|||
BuildState: &deps.BuildState{
|
||||
OnSignalRebuild: onSignalRebuild,
|
||||
},
|
||||
Counters: &deps.Counters{},
|
||||
MemCache: memCache,
|
||||
TemplateProvider: tplimpl.DefaultTemplateProvider,
|
||||
TranslationProvider: i18n.NewTranslationProvider(),
|
||||
WasmDispatchers: warpc.AllDispatchers(
|
||||
warpc.Options{
|
||||
|
@ -385,6 +391,34 @@ func newHugoSites(cfg deps.DepsCfg, d *deps.Deps, pageTrees *pageTrees, sites []
|
|||
var prototype *deps.Deps
|
||||
for i, s := range sites {
|
||||
s.h = h
|
||||
// The template store needs to be initialized after the h container is set on s.
|
||||
if i == 0 {
|
||||
templateStore, err := tplimpl.NewStore(
|
||||
tplimpl.StoreOptions{
|
||||
Fs: s.BaseFs.Layouts.Fs,
|
||||
DefaultContentLanguage: s.Conf.DefaultContentLanguage(),
|
||||
Watching: s.Conf.Watching(),
|
||||
PathParser: s.Conf.PathParser(),
|
||||
Metrics: d.Metrics,
|
||||
OutputFormats: s.conf.OutputFormats.Config,
|
||||
MediaTypes: s.conf.MediaTypes.Config,
|
||||
DefaultOutputFormat: s.conf.DefaultOutputFormat,
|
||||
TaxonomySingularPlural: s.conf.Taxonomies,
|
||||
}, tplimpl.SiteOptions{
|
||||
Site: s,
|
||||
TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.Deps.TemplateStore = templateStore
|
||||
} else {
|
||||
s.Deps.TemplateStore = prototype.TemplateStore.WithSiteOpts(
|
||||
tplimpl.SiteOptions{
|
||||
Site: s,
|
||||
TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps),
|
||||
})
|
||||
}
|
||||
if err := s.Deps.Compile(prototype); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -464,7 +498,10 @@ func (s *Site) MainSections() []string {
|
|||
|
||||
// Returns a struct with some information about the build.
|
||||
func (s *Site) Hugo() hugo.HugoInfo {
|
||||
if s.h == nil || s.h.hugoInfo.Environment == "" {
|
||||
if s.h == nil {
|
||||
panic("site: hugo: h not initialized")
|
||||
}
|
||||
if s.h.hugoInfo.Environment == "" {
|
||||
panic("site: hugo: hugoInfo not initialized")
|
||||
}
|
||||
return s.h.hugoInfo
|
||||
|
@ -797,7 +834,7 @@ func (s *Site) initRenderFormats() {
|
|||
s.renderFormats = formats
|
||||
}
|
||||
|
||||
func (s *Site) GetRelatedDocsHandler() *page.RelatedDocsHandler {
|
||||
func (s *Site) GetInternalRelatedDocsHandler() *page.RelatedDocsHandler {
|
||||
return s.relatedDocsHandler
|
||||
}
|
||||
|
||||
|
@ -923,19 +960,24 @@ type WhatChanged struct {
|
|||
mu sync.Mutex
|
||||
|
||||
needsPagesAssembly bool
|
||||
identitySet identity.Identities
|
||||
|
||||
ids map[identity.Identity]bool
|
||||
}
|
||||
|
||||
func (w *WhatChanged) init() {
|
||||
if w.ids == nil {
|
||||
w.ids = make(map[identity.Identity]bool)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WhatChanged) Add(ids ...identity.Identity) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.identitySet == nil {
|
||||
w.identitySet = make(identity.Identities)
|
||||
}
|
||||
w.init()
|
||||
|
||||
for _, id := range ids {
|
||||
w.identitySet[id] = true
|
||||
w.ids[id] = true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -946,20 +988,20 @@ func (w *WhatChanged) Clear() {
|
|||
}
|
||||
|
||||
func (w *WhatChanged) clear() {
|
||||
w.identitySet = identity.Identities{}
|
||||
w.ids = nil
|
||||
}
|
||||
|
||||
func (w *WhatChanged) Changes() []identity.Identity {
|
||||
if w == nil || w.identitySet == nil {
|
||||
if w == nil || w.ids == nil {
|
||||
return nil
|
||||
}
|
||||
return w.identitySet.AsSlice()
|
||||
return xmaps.Keys(w.ids)
|
||||
}
|
||||
|
||||
func (w *WhatChanged) Drain() []identity.Identity {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
ids := w.identitySet.AsSlice()
|
||||
ids := w.Changes()
|
||||
w.clear()
|
||||
return ids
|
||||
}
|
||||
|
@ -1394,7 +1436,7 @@ const (
|
|||
pageDependencyScopeGlobal
|
||||
)
|
||||
|
||||
func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ tpl.Template) error {
|
||||
func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ *tplimpl.TemplInfo) error {
|
||||
s.h.buildCounters.pageRenderCounter.Add(1)
|
||||
renderBuffer := bp.GetBuffer()
|
||||
defer bp.PutBuffer(renderBuffer)
|
||||
|
@ -1453,8 +1495,8 @@ var infoOnMissingLayout = map[string]bool{
|
|||
// hookRendererTemplate is the canonical implementation of all hooks.ITEMRenderer,
|
||||
// where ITEM is the thing being hooked.
|
||||
type hookRendererTemplate struct {
|
||||
templateHandler tpl.TemplateHandler
|
||||
templ tpl.Template
|
||||
templateHandler *tplimpl.TemplateStore
|
||||
templ *tplimpl.TemplInfo
|
||||
resolvePosition func(ctx any) text.Position
|
||||
}
|
||||
|
||||
|
@ -1490,7 +1532,7 @@ func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ tpl.Template) (err error) {
|
||||
func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ *tplimpl.TemplInfo) (err error) {
|
||||
if templ == nil {
|
||||
s.logMissingLayout(name, "", "", outputFormat)
|
||||
return nil
|
||||
|
@ -1500,7 +1542,7 @@ func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string,
|
|||
panic("nil context")
|
||||
}
|
||||
|
||||
if err = s.Tmpl().ExecuteWithContext(ctx, templ, w, d); err != nil {
|
||||
if err = s.GetTemplateStore().ExecuteWithContext(ctx, templ, w, d); err != nil {
|
||||
filename := name
|
||||
if p, ok := d.(*pageState); ok {
|
||||
filename = p.String()
|
||||
|
|
|
@ -27,6 +27,7 @@ func createDefaultOutputFormats(allFormats output.Formats) map[string]output.For
|
|||
htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name)
|
||||
robotsOut, _ := allFormats.GetByName(output.RobotsTxtFormat.Name)
|
||||
sitemapOut, _ := allFormats.GetByName(output.SitemapFormat.Name)
|
||||
httpStatus404Out, _ := allFormats.GetByName(output.HTTPStatus404HTMLFormat.Name)
|
||||
|
||||
defaultListTypes := output.Formats{htmlOut}
|
||||
if rssFound {
|
||||
|
@ -42,7 +43,7 @@ func createDefaultOutputFormats(allFormats output.Formats) map[string]output.For
|
|||
// Below are for consistency. They are currently not used during rendering.
|
||||
kinds.KindSitemap: {sitemapOut},
|
||||
kinds.KindRobotsTXT: {robotsOut},
|
||||
kinds.KindStatus404: {htmlOut},
|
||||
kinds.KindStatus404: {httpStatus404Out},
|
||||
}
|
||||
|
||||
// May be disabled
|
||||
|
|
|
@ -387,7 +387,7 @@ func TestCreateSiteOutputFormats(t *testing.T) {
|
|||
c.Assert(outputs[kinds.KindRSS], deepEqualsOutputFormats, output.Formats{output.RSSFormat})
|
||||
c.Assert(outputs[kinds.KindSitemap], deepEqualsOutputFormats, output.Formats{output.SitemapFormat})
|
||||
c.Assert(outputs[kinds.KindRobotsTXT], deepEqualsOutputFormats, output.Formats{output.RobotsTxtFormat})
|
||||
c.Assert(outputs[kinds.KindStatus404], deepEqualsOutputFormats, output.Formats{output.HTMLFormat})
|
||||
c.Assert(outputs[kinds.KindStatus404], deepEqualsOutputFormats, output.Formats{output.HTTPStatus404HTMLFormat})
|
||||
})
|
||||
|
||||
// Issue #4528
|
||||
|
@ -481,6 +481,7 @@ permalinkable = true
|
|||
[outputFormats.nobase]
|
||||
mediaType = "application/json"
|
||||
permalinkable = true
|
||||
isPlainText = true
|
||||
|
||||
`
|
||||
|
||||
|
|
|
@ -23,9 +23,9 @@ import (
|
|||
"github.com/bep/logg"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/hugolib/doctree"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/kinds"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
|
@ -57,7 +57,7 @@ func (s siteRenderContext) shouldRenderStandalonePage(kind string) bool {
|
|||
return s.outIdx == 0
|
||||
}
|
||||
|
||||
if kind == kinds.KindStatus404 {
|
||||
if kind == kinds.KindTemporary || kind == kinds.KindStatus404 {
|
||||
// 1 for all output formats
|
||||
return s.outIdx == 0
|
||||
}
|
||||
|
@ -168,7 +168,7 @@ func pageRenderer(
|
|||
|
||||
s.Log.Trace(
|
||||
func() string {
|
||||
return fmt.Sprintf("rendering outputFormat %q kind %q using layout %q to %q", p.pageOutput.f.Name, p.Kind(), templ.Name(), targetPath)
|
||||
return fmt.Sprintf("rendering outputFormat %q kind %q using layout %q to %q", p.pageOutput.f.Name, p.Kind(), templ.Template.Name(), targetPath)
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -225,7 +225,7 @@ func (s *Site) logMissingLayout(name, layout, kind, outputFormat string) {
|
|||
}
|
||||
|
||||
// renderPaginator must be run after the owning Page has been rendered.
|
||||
func (s *Site) renderPaginator(p *pageState, templ tpl.Template) error {
|
||||
func (s *Site) renderPaginator(p *pageState, templ *tplimpl.TemplInfo) error {
|
||||
paginatePath := s.Conf.Pagination().Path
|
||||
|
||||
d := p.targetPathDescriptor
|
||||
|
|
|
@ -978,8 +978,7 @@ func TestRefLinking(t *testing.T) {
|
|||
{".", "", true, "/level2/level3/"},
|
||||
{"./", "", true, "/level2/level3/"},
|
||||
|
||||
// try to confuse parsing
|
||||
{"embedded.dot.md", "", true, "/level2/level3/embedded.dot/"},
|
||||
{"embedded.dot.md", "", true, "/level2/level3/embedded/"},
|
||||
|
||||
// test empty link, as well as fragment only link
|
||||
{"", "", true, ""},
|
||||
|
|
|
@ -76,6 +76,8 @@ func TestTaxonomiesWithAndWithoutContentFile(t *testing.T) {
|
|||
}
|
||||
|
||||
func doTestTaxonomiesWithAndWithoutContentFile(t *testing.T, uglyURLs bool) {
|
||||
t.Helper()
|
||||
|
||||
siteConfig := `
|
||||
baseURL = "http://example.com/blog"
|
||||
titleCaseStyle = "firstupper"
|
||||
|
|
|
@ -26,6 +26,8 @@ import (
|
|||
"github.com/gohugoio/hugo/hugofs"
|
||||
)
|
||||
|
||||
// TODO(bep) keep this until we release v0.146.0 as a security against breaking changes, but it's rather messy and mostly duplicate of
|
||||
// tests in the tplimpl package, so eventually just remove it.
|
||||
func TestTemplateLookupOrder(t *testing.T) {
|
||||
var (
|
||||
fs *hugofs.Fs
|
||||
|
@ -185,6 +187,9 @@ func TestTemplateLookupOrder(t *testing.T) {
|
|||
} {
|
||||
|
||||
this := this
|
||||
if this.name != "Variant 1" {
|
||||
continue
|
||||
}
|
||||
t.Run(this.name, func(t *testing.T) {
|
||||
// TODO(bep) there are some function vars need to pull down here to enable => t.Parallel()
|
||||
cfg, fs = newTestCfg()
|
||||
|
@ -200,7 +205,7 @@ Some content
|
|||
}
|
||||
|
||||
buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{})
|
||||
// helpers.PrintFs(s.BaseFs.Layouts.Fs, "", os.Stdout)
|
||||
// s.TemplateStore.PrintDebug("", 0, os.Stdout)
|
||||
this.assert(t)
|
||||
})
|
||||
|
||||
|
@ -344,7 +349,11 @@ title: %s
|
|||
b.AssertFileContent("public/p1/index.html", `Single: P1`)
|
||||
})
|
||||
|
||||
t.Run("baseof", func(t *testing.T) {
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateLookupSitBaseOf(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
|
||||
|
||||
|
@ -370,7 +379,6 @@ title: My Page
|
|||
b.AssertFileContent("public/fr/index.html", `Baseof fr: Main Home Fr`)
|
||||
b.AssertFileContent("public/en/mysection/index.html", `Baseof mysection: Main Default List`)
|
||||
b.AssertFileContent("public/en/mysection/p1/index.html", `Baseof mysection: Main Default Single`)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplateFuncs(t *testing.T) {
|
||||
|
@ -707,6 +715,7 @@ a: {{ $a }}
|
|||
b.AssertFileContent("public/index.html", `a: [a b c]`)
|
||||
}
|
||||
|
||||
// Legacy behavior for internal templates.
|
||||
func TestOverrideInternalTemplate(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
|
|
|
@ -509,6 +509,10 @@ func probablyEq(a, b Identity) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
if a2, ok := a.(compare.ProbablyEqer); ok && a2.ProbablyEq(b) {
|
||||
return true
|
||||
}
|
||||
|
||||
if a2, ok := a.(IsProbablyDependentProvider); ok {
|
||||
return a2.IsProbablyDependent(b)
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ import (
|
|||
"github.com/gohugoio/hugo/resources"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/gohugoio/hugo/resources/resource_factories/create"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
@ -192,7 +192,7 @@ type BatcherClient struct {
|
|||
d *deps.Deps
|
||||
|
||||
once sync.Once
|
||||
runnerTemplate tpl.Template
|
||||
runnerTemplate *tplimpl.TemplInfo
|
||||
|
||||
createClient *create.Client
|
||||
buildClient *BuildClient
|
||||
|
@ -208,7 +208,7 @@ func (c *BatcherClient) New(id string) (js.Batcher, error) {
|
|||
c.once.Do(func() {
|
||||
// We should fix the initialization order here (or use the Go template package directly), but we need to wait
|
||||
// for the Hugo templates to be ready.
|
||||
tmpl, err := c.d.TextTmpl().Parse("batch-esm-runner", runnerTemplateStr)
|
||||
tmpl, err := c.d.TemplateStore.TextParse("batch-esm-runner", runnerTemplateStr)
|
||||
if err != nil {
|
||||
initErr = err
|
||||
return
|
||||
|
@ -287,7 +287,7 @@ func (c *BatcherClient) Store() *maps.Cache[string, js.Batcher] {
|
|||
func (c *BatcherClient) buildBatchGroup(ctx context.Context, t *batchGroupTemplateContext) (resource.Resource, string, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
if err := c.d.Tmpl().ExecuteWithContext(ctx, c.runnerTemplate, &buf, t); err != nil {
|
||||
if err := c.d.GetTemplateStore().ExecuteWithContext(ctx, c.runnerTemplate, &buf, t); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
|
|
|
@ -23,8 +23,6 @@ import (
|
|||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/config/testconfig"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
|
@ -472,7 +470,6 @@ func prepareTranslationProvider(t testing.TB, test i18nTest, cfg config.Provider
|
|||
func prepareDeps(afs afero.Fs, cfg config.Provider) (*deps.Deps, *TranslationProvider) {
|
||||
d := testconfig.GetTestDeps(afs, cfg)
|
||||
translationProvider := NewTranslationProvider()
|
||||
d.TemplateProvider = tplimpl.DefaultTemplateProvider
|
||||
d.TranslationProvider = translationProvider
|
||||
d.Site = page.NewDummyHugoSite(d.Conf)
|
||||
if err := d.Compile(nil); err != nil {
|
||||
|
|
|
@ -69,7 +69,7 @@ fmt.Println("Hello, World!");
|
|||
|
||||
## Golang Code
|
||||
|
||||
§§§golang
|
||||
§§§go
|
||||
fmt.Println("Hello, Golang!");
|
||||
§§§
|
||||
|
||||
|
@ -97,14 +97,14 @@ Go Language: go|
|
|||
Go Code: fmt.Println("Hello, World!");
|
||||
|
||||
Go Code: fmt.Println("Hello, Golang!");
|
||||
Go Language: golang|
|
||||
Go Language: go|
|
||||
|
||||
|
||||
`,
|
||||
"Goat SVG:<svg class='diagram' xmlns='http://www.w3.org/2000/svg' version='1.1' height='25' width='40'",
|
||||
"Goat Attribute: 600|",
|
||||
"<h2 id=\"go-code\">Go Code</h2>\nGo Code: fmt.Println(\"Hello, World!\");\n|\nGo Language: go|",
|
||||
"<h2 id=\"golang-code\">Golang Code</h2>\nGo Code: fmt.Println(\"Hello, Golang!\");\n|\nGo Language: golang|",
|
||||
"<h2 id=\"golang-code\">Golang Code</h2>\nGo Code: fmt.Println(\"Hello, Golang!\");\n|\nGo Language: go|",
|
||||
"<h2 id=\"bash-code\">Bash Code</h2>\n<div class=\"highlight blue\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"ln\">32</span><span class=\"cl\"><span class=\"nb\">echo</span> <span class=\"s2\">"l1"</span><span class=\"p\">;</span>\n</span></span><span class=\"line hl\"><span class=\"ln\">33</span>",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ type BuiltinTypes struct {
|
|||
CSSType Type
|
||||
SCSSType Type
|
||||
SASSType Type
|
||||
GotmplType Type
|
||||
CSVType Type
|
||||
HTMLType Type
|
||||
JavascriptType Type
|
||||
|
@ -60,6 +61,7 @@ var Builtin = BuiltinTypes{
|
|||
CSSType: Type{Type: "text/css"},
|
||||
SCSSType: Type{Type: "text/x-scss"},
|
||||
SASSType: Type{Type: "text/x-sass"},
|
||||
GotmplType: Type{Type: "text/x-gotmpl"},
|
||||
CSVType: Type{Type: "text/csv"},
|
||||
HTMLType: Type{Type: "text/html"},
|
||||
JavascriptType: Type{Type: "text/javascript"},
|
||||
|
@ -121,6 +123,7 @@ var defaultMediaTypesConfig = map[string]any{
|
|||
"text/typescript": map[string]any{"suffixes": []string{"ts"}},
|
||||
"text/tsx": map[string]any{"suffixes": []string{"tsx"}},
|
||||
"text/jsx": map[string]any{"suffixes": []string{"jsx"}},
|
||||
"text/x-gotmpl": map[string]any{"suffixes": []string{"gotmpl"}},
|
||||
|
||||
"application/json": map[string]any{"suffixes": []string{"json"}},
|
||||
"application/manifest+json": map[string]any{"suffixes": []string{"webmanifest"}},
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
|
@ -26,7 +27,6 @@ import (
|
|||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/cast"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// DefaultTypes is the default media types supported by Hugo.
|
||||
|
@ -271,4 +271,7 @@ var DefaultPathParser = &paths.PathParser{
|
|||
IsContentExt: func(ext string) bool {
|
||||
panic("not supported")
|
||||
},
|
||||
IsOutputFormat: func(name, ext string) bool {
|
||||
panic("DefaultPathParser: not supported")
|
||||
},
|
||||
}
|
||||
|
|
|
@ -151,5 +151,5 @@ func TestDefaultTypes(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
c.Assert(len(DefaultTypes), qt.Equals, 40)
|
||||
c.Assert(len(DefaultTypes), qt.Equals, 41)
|
||||
}
|
||||
|
|
|
@ -282,7 +282,7 @@ func (t Types) BySuffix(suffix string) []Type {
|
|||
suffix = t.normalizeSuffix(suffix)
|
||||
var types []Type
|
||||
for _, tt := range t {
|
||||
if tt.hasSuffix(suffix) {
|
||||
if tt.HasSuffix(suffix) {
|
||||
types = append(types, tt)
|
||||
}
|
||||
}
|
||||
|
@ -293,7 +293,7 @@ func (t Types) BySuffix(suffix string) []Type {
|
|||
func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
|
||||
suffix = t.normalizeSuffix(suffix)
|
||||
for _, tt := range t {
|
||||
if tt.hasSuffix(suffix) {
|
||||
if tt.HasSuffix(suffix) {
|
||||
return tt, SuffixInfo{
|
||||
FullSuffix: tt.Delimiter + suffix,
|
||||
Suffix: suffix,
|
||||
|
@ -310,7 +310,7 @@ func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
|
|||
func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
|
||||
suffix = t.normalizeSuffix(suffix)
|
||||
for _, tt := range t {
|
||||
if tt.hasSuffix(suffix) {
|
||||
if tt.HasSuffix(suffix) {
|
||||
if found {
|
||||
// ambiguous
|
||||
found = false
|
||||
|
@ -330,14 +330,14 @@ func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
|
|||
func (t Types) IsTextSuffix(suffix string) bool {
|
||||
suffix = t.normalizeSuffix(suffix)
|
||||
for _, tt := range t {
|
||||
if tt.hasSuffix(suffix) {
|
||||
if tt.HasSuffix(suffix) {
|
||||
return tt.IsText()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m Type) hasSuffix(suffix string) bool {
|
||||
func (m Type) HasSuffix(suffix string) bool {
|
||||
return strings.Contains(","+m.SuffixesCSV+",", ","+suffix+",")
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
// "fmt"
|
||||
|
||||
"github.com/gohugoio/hugo/docshelper"
|
||||
"github.com/gohugoio/hugo/output/layouts"
|
||||
)
|
||||
|
||||
// This is is just some helpers used to create some JSON used in the Hugo docs.
|
||||
|
@ -14,90 +12,12 @@ func init() {
|
|||
docsProvider := func() docshelper.DocProvider {
|
||||
return docshelper.DocProvider{
|
||||
"output": map[string]any{
|
||||
"layouts": createLayoutExamples(),
|
||||
// TODO(bep), maybe revisit this later, but I hope this isn't needed.
|
||||
// "layouts": createLayoutExamples(),
|
||||
"layouts": map[string]any{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
docshelper.AddDocProviderFunc(docsProvider)
|
||||
}
|
||||
|
||||
func createLayoutExamples() any {
|
||||
type Example struct {
|
||||
Example string
|
||||
Kind string
|
||||
OutputFormat string
|
||||
Suffix string
|
||||
Layouts []string `json:"Template Lookup Order"`
|
||||
}
|
||||
|
||||
var (
|
||||
basicExamples []Example
|
||||
demoLayout = "demolayout"
|
||||
demoType = "demotype"
|
||||
)
|
||||
|
||||
for _, example := range []struct {
|
||||
name string
|
||||
d layouts.LayoutDescriptor
|
||||
}{
|
||||
// Taxonomy layouts.LayoutDescriptor={categories category taxonomy en false Type Section
|
||||
{"Single page in \"posts\" section", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Base template for single page in \"posts\" section", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Single page in \"posts\" section with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Base template for single page in \"posts\" section with layout set to \"demolayout\"", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
|
||||
{"AMP single page in \"posts\" section", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "amp", Suffix: "html"}},
|
||||
{"AMP single page in \"posts\" section, French language", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}},
|
||||
// Typeless pages get "page" as type
|
||||
{"Home page", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Base template for home page", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Home page with type set to \"demotype\"", layouts.LayoutDescriptor{Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Base template for home page with type set to \"demotype\"", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Home page with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "home", Type: "page", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
|
||||
{"AMP home, French language", layouts.LayoutDescriptor{Kind: "home", Type: "page", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}},
|
||||
{"JSON home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "json", Suffix: "json"}},
|
||||
{"RSS home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "rss", Suffix: "xml"}},
|
||||
|
||||
{"Section list for \"posts\"", layouts.LayoutDescriptor{Kind: "section", Type: "posts", Section: "posts", OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Section list for \"posts\" with type set to \"blog\"", layouts.LayoutDescriptor{Kind: "section", Type: "blog", Section: "posts", OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Section list for \"posts\" with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "section", Layout: demoLayout, Section: "posts", OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Section list for \"posts\"", layouts.LayoutDescriptor{Kind: "section", Type: "posts", OutputFormatName: "rss", Suffix: "xml"}},
|
||||
|
||||
{"Taxonomy list for \"categories\"", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Taxonomy list for \"categories\"", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "xml"}},
|
||||
|
||||
{"Term list for \"categories\"", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Term list for \"categories\"", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "xml"}},
|
||||
} {
|
||||
|
||||
l := layouts.NewLayoutHandler()
|
||||
layouts, _ := l.For(example.d)
|
||||
|
||||
basicExamples = append(basicExamples, Example{
|
||||
Example: example.name,
|
||||
Kind: example.d.Kind,
|
||||
OutputFormat: example.d.OutputFormatName,
|
||||
Suffix: example.d.Suffix,
|
||||
Layouts: makeLayoutsPresentable(layouts),
|
||||
})
|
||||
}
|
||||
|
||||
return basicExamples
|
||||
}
|
||||
|
||||
func makeLayoutsPresentable(l []string) []string {
|
||||
var filtered []string
|
||||
for _, ll := range l {
|
||||
if strings.Contains(ll, "page/") {
|
||||
// This is a valid lookup, but it's more confusing than useful.
|
||||
continue
|
||||
}
|
||||
ll = "layouts/" + strings.TrimPrefix(ll, "_text/")
|
||||
|
||||
if !strings.Contains(ll, "indexes") {
|
||||
filtered = append(filtered, ll)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
|
|
@ -1,336 +0,0 @@
|
|||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package layouts
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// These may be used as content sections with potential conflicts. Avoid that.
|
||||
var reservedSections = map[string]bool{
|
||||
"shortcodes": true,
|
||||
"partials": true,
|
||||
}
|
||||
|
||||
// LayoutDescriptor describes how a layout should be chosen. This is
|
||||
// typically built from a Page.
|
||||
type LayoutDescriptor struct {
|
||||
Type string
|
||||
Section string
|
||||
|
||||
// E.g. "page", but also used for the _markup render kinds, e.g. "render-image".
|
||||
Kind string
|
||||
|
||||
// Comma-separated list of kind variants, e.g. "go,json" as variants which would find "render-codeblock-go.html"
|
||||
KindVariants string
|
||||
|
||||
Lang string
|
||||
Layout string
|
||||
// LayoutOverride indicates what we should only look for the above layout.
|
||||
LayoutOverride bool
|
||||
|
||||
// From OutputFormat and MediaType.
|
||||
OutputFormatName string
|
||||
Suffix string
|
||||
|
||||
RenderingHook bool
|
||||
Baseof bool
|
||||
}
|
||||
|
||||
func (d LayoutDescriptor) isList() bool {
|
||||
return !d.RenderingHook && d.Kind != "page" && d.Kind != "404" && d.Kind != "sitemap" && d.Kind != "sitemapindex"
|
||||
}
|
||||
|
||||
// LayoutHandler calculates the layout template to use to render a given output type.
|
||||
type LayoutHandler struct {
|
||||
mu sync.RWMutex
|
||||
cache map[LayoutDescriptor][]string
|
||||
}
|
||||
|
||||
// NewLayoutHandler creates a new LayoutHandler.
|
||||
func NewLayoutHandler() *LayoutHandler {
|
||||
return &LayoutHandler{cache: make(map[LayoutDescriptor][]string)}
|
||||
}
|
||||
|
||||
// For returns a layout for the given LayoutDescriptor and options.
|
||||
// Layouts are rendered and cached internally.
|
||||
func (l *LayoutHandler) For(d LayoutDescriptor) ([]string, error) {
|
||||
// We will get lots of requests for the same layouts, so avoid recalculations.
|
||||
l.mu.RLock()
|
||||
if cacheVal, found := l.cache[d]; found {
|
||||
l.mu.RUnlock()
|
||||
return cacheVal, nil
|
||||
}
|
||||
l.mu.RUnlock()
|
||||
|
||||
layouts := resolvePageTemplate(d)
|
||||
|
||||
layouts = uniqueStringsReuse(layouts)
|
||||
|
||||
l.mu.Lock()
|
||||
l.cache[d] = layouts
|
||||
l.mu.Unlock()
|
||||
|
||||
return layouts, nil
|
||||
}
|
||||
|
||||
type layoutBuilder struct {
|
||||
layoutVariations []string
|
||||
typeVariations []string
|
||||
d LayoutDescriptor
|
||||
// f Format
|
||||
}
|
||||
|
||||
func (l *layoutBuilder) addLayoutVariations(vars ...string) {
|
||||
for _, layoutVar := range vars {
|
||||
if l.d.Baseof && layoutVar != "baseof" {
|
||||
l.layoutVariations = append(l.layoutVariations, layoutVar+"-baseof")
|
||||
continue
|
||||
}
|
||||
if !l.d.RenderingHook && !l.d.Baseof && l.d.LayoutOverride && layoutVar != l.d.Layout {
|
||||
continue
|
||||
}
|
||||
l.layoutVariations = append(l.layoutVariations, layoutVar)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *layoutBuilder) addTypeVariations(vars ...string) {
|
||||
for _, typeVar := range vars {
|
||||
if !reservedSections[typeVar] {
|
||||
if l.d.RenderingHook {
|
||||
typeVar = typeVar + renderingHookRoot
|
||||
}
|
||||
l.typeVariations = append(l.typeVariations, typeVar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *layoutBuilder) addSectionType() {
|
||||
if l.d.Section != "" {
|
||||
l.addTypeVariations(l.d.Section)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *layoutBuilder) addKind() {
|
||||
l.addLayoutVariations(l.d.Kind)
|
||||
l.addTypeVariations(l.d.Kind)
|
||||
}
|
||||
|
||||
const renderingHookRoot = "/_markup"
|
||||
|
||||
func resolvePageTemplate(d LayoutDescriptor) []string {
|
||||
b := &layoutBuilder{d: d}
|
||||
|
||||
if !d.RenderingHook && d.Layout != "" {
|
||||
b.addLayoutVariations(d.Layout)
|
||||
}
|
||||
if d.Type != "" {
|
||||
b.addTypeVariations(d.Type)
|
||||
}
|
||||
|
||||
if d.RenderingHook {
|
||||
if d.KindVariants != "" {
|
||||
// Add the more specific variants first.
|
||||
for _, variant := range strings.Split(d.KindVariants, ",") {
|
||||
b.addLayoutVariations(d.Kind + "-" + variant)
|
||||
}
|
||||
}
|
||||
b.addLayoutVariations(d.Kind)
|
||||
b.addSectionType()
|
||||
}
|
||||
|
||||
switch d.Kind {
|
||||
case "page":
|
||||
b.addLayoutVariations("single")
|
||||
b.addSectionType()
|
||||
case "home":
|
||||
b.addLayoutVariations("index", "home")
|
||||
// Also look in the root
|
||||
b.addTypeVariations("")
|
||||
case "section":
|
||||
if d.Section != "" {
|
||||
b.addLayoutVariations(d.Section)
|
||||
}
|
||||
b.addSectionType()
|
||||
b.addKind()
|
||||
case "term":
|
||||
b.addKind()
|
||||
if d.Section != "" {
|
||||
b.addLayoutVariations(d.Section)
|
||||
}
|
||||
b.addLayoutVariations("taxonomy")
|
||||
b.addTypeVariations("taxonomy")
|
||||
b.addSectionType()
|
||||
case "taxonomy":
|
||||
if d.Section != "" {
|
||||
b.addLayoutVariations(d.Section + ".terms")
|
||||
}
|
||||
b.addSectionType()
|
||||
b.addLayoutVariations("terms")
|
||||
// For legacy reasons this is deliberately put last.
|
||||
b.addKind()
|
||||
case "404":
|
||||
b.addLayoutVariations("404")
|
||||
b.addTypeVariations("")
|
||||
case "robotstxt":
|
||||
b.addLayoutVariations("robots")
|
||||
b.addTypeVariations("")
|
||||
case "sitemap":
|
||||
b.addLayoutVariations("sitemap")
|
||||
b.addTypeVariations("")
|
||||
case "sitemapindex":
|
||||
b.addLayoutVariations("sitemapindex")
|
||||
b.addTypeVariations("")
|
||||
}
|
||||
|
||||
isRSS := d.OutputFormatName == "rss"
|
||||
if !d.RenderingHook && !d.Baseof && isRSS {
|
||||
// The historic and common rss.xml case
|
||||
b.addLayoutVariations("")
|
||||
}
|
||||
|
||||
if d.Baseof || d.Kind != "404" {
|
||||
// Most have _default in their lookup path
|
||||
b.addTypeVariations("_default")
|
||||
}
|
||||
|
||||
if d.isList() {
|
||||
// Add the common list type
|
||||
b.addLayoutVariations("list")
|
||||
}
|
||||
|
||||
if d.Baseof {
|
||||
b.addLayoutVariations("baseof")
|
||||
}
|
||||
|
||||
layouts := b.resolveVariations()
|
||||
|
||||
if !d.RenderingHook && !d.Baseof && isRSS {
|
||||
layouts = append(layouts, "_internal/_default/rss.xml")
|
||||
}
|
||||
|
||||
switch d.Kind {
|
||||
case "robotstxt":
|
||||
layouts = append(layouts, "_internal/_default/robots.txt")
|
||||
case "sitemap":
|
||||
layouts = append(layouts, "_internal/_default/sitemap.xml")
|
||||
case "sitemapindex":
|
||||
layouts = append(layouts, "_internal/_default/sitemapindex.xml")
|
||||
}
|
||||
|
||||
return layouts
|
||||
}
|
||||
|
||||
func (l *layoutBuilder) resolveVariations() []string {
|
||||
var layouts []string
|
||||
|
||||
var variations []string
|
||||
name := strings.ToLower(l.d.OutputFormatName)
|
||||
|
||||
if l.d.Lang != "" {
|
||||
// We prefer the most specific type before language.
|
||||
variations = append(variations, []string{l.d.Lang + "." + name, name, l.d.Lang}...)
|
||||
} else {
|
||||
variations = append(variations, name)
|
||||
}
|
||||
|
||||
variations = append(variations, "")
|
||||
|
||||
for _, typeVar := range l.typeVariations {
|
||||
for _, variation := range variations {
|
||||
for _, layoutVar := range l.layoutVariations {
|
||||
if variation == "" && layoutVar == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
s := constructLayoutPath(typeVar, layoutVar, variation, l.d.Suffix)
|
||||
if s != "" {
|
||||
layouts = append(layouts, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return layouts
|
||||
}
|
||||
|
||||
// constructLayoutPath constructs a layout path given a type, layout,
|
||||
// variations, and extension. The path constructed follows the pattern of
|
||||
// type/layout.variations.extension. If any value is empty, it will be left out
|
||||
// of the path construction.
|
||||
//
|
||||
// Path construction requires at least 2 of 3 out of layout, variations, and extension.
|
||||
// If more than one of those is empty, an empty string is returned.
|
||||
func constructLayoutPath(typ, layout, variations, extension string) string {
|
||||
// we already know that layout and variations are not both empty because of
|
||||
// checks in resolveVariants().
|
||||
if extension == "" && (layout == "" || variations == "") {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Commence valid path construction...
|
||||
|
||||
var (
|
||||
p strings.Builder
|
||||
needDot bool
|
||||
)
|
||||
|
||||
if typ != "" {
|
||||
p.WriteString(typ)
|
||||
p.WriteString("/")
|
||||
}
|
||||
|
||||
if layout != "" {
|
||||
p.WriteString(layout)
|
||||
needDot = true
|
||||
}
|
||||
|
||||
if variations != "" {
|
||||
if needDot {
|
||||
p.WriteString(".")
|
||||
}
|
||||
p.WriteString(variations)
|
||||
needDot = true
|
||||
}
|
||||
|
||||
if extension != "" {
|
||||
if needDot {
|
||||
p.WriteString(".")
|
||||
}
|
||||
p.WriteString(extension)
|
||||
}
|
||||
|
||||
return p.String()
|
||||
}
|
||||
|
||||
// Inline this here so we can use tinygo to compile a wasm binary of this package.
|
||||
func uniqueStringsReuse(s []string) []string {
|
||||
result := s[:0]
|
||||
for i, val := range s {
|
||||
var seen bool
|
||||
|
||||
for j := range i {
|
||||
if s[j] == val {
|
||||
seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !seen {
|
||||
result = append(result, val)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
|
@ -1,982 +0,0 @@
|
|||
// Copyright 2017-present The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package layouts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/kylelemons/godebug/diff"
|
||||
)
|
||||
|
||||
func TestLayout(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
for _, this := range []struct {
|
||||
name string
|
||||
layoutDescriptor LayoutDescriptor
|
||||
layoutOverride string
|
||||
expect []string
|
||||
}{
|
||||
{
|
||||
"Home",
|
||||
LayoutDescriptor{Kind: "home", OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"index.amp.html",
|
||||
"home.amp.html",
|
||||
"list.amp.html",
|
||||
"index.html",
|
||||
"home.html",
|
||||
"list.html",
|
||||
"_default/index.amp.html",
|
||||
"_default/home.amp.html",
|
||||
"_default/list.amp.html",
|
||||
"_default/index.html",
|
||||
"_default/home.html",
|
||||
"_default/list.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Home baseof",
|
||||
LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"index-baseof.amp.html",
|
||||
"home-baseof.amp.html",
|
||||
"list-baseof.amp.html",
|
||||
"baseof.amp.html",
|
||||
"index-baseof.html",
|
||||
"home-baseof.html",
|
||||
"list-baseof.html",
|
||||
"baseof.html",
|
||||
"_default/index-baseof.amp.html",
|
||||
"_default/home-baseof.amp.html",
|
||||
"_default/list-baseof.amp.html",
|
||||
"_default/baseof.amp.html",
|
||||
"_default/index-baseof.html",
|
||||
"_default/home-baseof.html",
|
||||
"_default/list-baseof.html",
|
||||
"_default/baseof.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Home, HTML",
|
||||
LayoutDescriptor{Kind: "home", OutputFormatName: "html", Suffix: "html"},
|
||||
"",
|
||||
// We will eventually get to index.html. This looks stuttery, but makes the lookup logic easy to understand.
|
||||
[]string{
|
||||
"index.html.html",
|
||||
"home.html.html",
|
||||
"list.html.html",
|
||||
"index.html",
|
||||
"home.html",
|
||||
"list.html",
|
||||
"_default/index.html.html",
|
||||
"_default/home.html.html",
|
||||
"_default/list.html.html",
|
||||
"_default/index.html",
|
||||
"_default/home.html",
|
||||
"_default/list.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Home, HTML, baseof",
|
||||
LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "html", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"index-baseof.html.html",
|
||||
"home-baseof.html.html",
|
||||
"list-baseof.html.html",
|
||||
"baseof.html.html",
|
||||
"index-baseof.html",
|
||||
"home-baseof.html",
|
||||
"list-baseof.html",
|
||||
"baseof.html",
|
||||
"_default/index-baseof.html.html",
|
||||
"_default/home-baseof.html.html",
|
||||
"_default/list-baseof.html.html",
|
||||
"_default/baseof.html.html",
|
||||
"_default/index-baseof.html",
|
||||
"_default/home-baseof.html",
|
||||
"_default/list-baseof.html",
|
||||
"_default/baseof.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Home, french language",
|
||||
LayoutDescriptor{Kind: "home", Lang: "fr", OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"index.fr.amp.html",
|
||||
"home.fr.amp.html",
|
||||
"list.fr.amp.html",
|
||||
"index.amp.html",
|
||||
"home.amp.html",
|
||||
"list.amp.html",
|
||||
"index.fr.html",
|
||||
"home.fr.html",
|
||||
"list.fr.html",
|
||||
"index.html",
|
||||
"home.html",
|
||||
"list.html",
|
||||
"_default/index.fr.amp.html",
|
||||
"_default/home.fr.amp.html",
|
||||
"_default/list.fr.amp.html",
|
||||
"_default/index.amp.html",
|
||||
"_default/home.amp.html",
|
||||
"_default/list.amp.html",
|
||||
"_default/index.fr.html",
|
||||
"_default/home.fr.html",
|
||||
"_default/list.fr.html",
|
||||
"_default/index.html",
|
||||
"_default/home.html",
|
||||
"_default/list.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Home, no ext or delim",
|
||||
LayoutDescriptor{Kind: "home", OutputFormatName: "nem", Suffix: ""},
|
||||
"",
|
||||
[]string{
|
||||
"index.nem",
|
||||
"home.nem",
|
||||
"list.nem",
|
||||
"_default/index.nem",
|
||||
"_default/home.nem",
|
||||
"_default/list.nem",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Home, no ext",
|
||||
LayoutDescriptor{Kind: "home", OutputFormatName: "nex", Suffix: ""},
|
||||
"",
|
||||
[]string{
|
||||
"index.nex",
|
||||
"home.nex",
|
||||
"list.nex",
|
||||
"_default/index.nex",
|
||||
"_default/home.nex",
|
||||
"_default/list.nex",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Page, no ext or delim",
|
||||
LayoutDescriptor{Kind: "page", OutputFormatName: "nem", Suffix: ""},
|
||||
"",
|
||||
[]string{"_default/single.nem"},
|
||||
},
|
||||
{
|
||||
"Section",
|
||||
LayoutDescriptor{Kind: "section", Section: "sect1", OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"sect1/sect1.amp.html",
|
||||
"sect1/section.amp.html",
|
||||
"sect1/list.amp.html",
|
||||
"sect1/sect1.html",
|
||||
"sect1/section.html",
|
||||
"sect1/list.html",
|
||||
"section/sect1.amp.html",
|
||||
"section/section.amp.html",
|
||||
"section/list.amp.html",
|
||||
"section/sect1.html",
|
||||
"section/section.html",
|
||||
"section/list.html",
|
||||
"_default/sect1.amp.html",
|
||||
"_default/section.amp.html",
|
||||
"_default/list.amp.html",
|
||||
"_default/sect1.html",
|
||||
"_default/section.html",
|
||||
"_default/list.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Section, baseof",
|
||||
LayoutDescriptor{Kind: "section", Section: "sect1", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"sect1/sect1-baseof.amp.html",
|
||||
"sect1/section-baseof.amp.html",
|
||||
"sect1/list-baseof.amp.html",
|
||||
"sect1/baseof.amp.html",
|
||||
"sect1/sect1-baseof.html",
|
||||
"sect1/section-baseof.html",
|
||||
"sect1/list-baseof.html",
|
||||
"sect1/baseof.html",
|
||||
"section/sect1-baseof.amp.html",
|
||||
"section/section-baseof.amp.html",
|
||||
"section/list-baseof.amp.html",
|
||||
"section/baseof.amp.html",
|
||||
"section/sect1-baseof.html",
|
||||
"section/section-baseof.html",
|
||||
"section/list-baseof.html",
|
||||
"section/baseof.html",
|
||||
"_default/sect1-baseof.amp.html",
|
||||
"_default/section-baseof.amp.html",
|
||||
"_default/list-baseof.amp.html",
|
||||
"_default/baseof.amp.html",
|
||||
"_default/sect1-baseof.html",
|
||||
"_default/section-baseof.html",
|
||||
"_default/list-baseof.html",
|
||||
"_default/baseof.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Section, baseof, French, AMP",
|
||||
LayoutDescriptor{Kind: "section", Section: "sect1", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"sect1/sect1-baseof.fr.amp.html",
|
||||
"sect1/section-baseof.fr.amp.html",
|
||||
"sect1/list-baseof.fr.amp.html",
|
||||
"sect1/baseof.fr.amp.html",
|
||||
"sect1/sect1-baseof.amp.html",
|
||||
"sect1/section-baseof.amp.html",
|
||||
"sect1/list-baseof.amp.html",
|
||||
"sect1/baseof.amp.html",
|
||||
"sect1/sect1-baseof.fr.html",
|
||||
"sect1/section-baseof.fr.html",
|
||||
"sect1/list-baseof.fr.html",
|
||||
"sect1/baseof.fr.html",
|
||||
"sect1/sect1-baseof.html",
|
||||
"sect1/section-baseof.html",
|
||||
"sect1/list-baseof.html",
|
||||
"sect1/baseof.html",
|
||||
"section/sect1-baseof.fr.amp.html",
|
||||
"section/section-baseof.fr.amp.html",
|
||||
"section/list-baseof.fr.amp.html",
|
||||
"section/baseof.fr.amp.html",
|
||||
"section/sect1-baseof.amp.html",
|
||||
"section/section-baseof.amp.html",
|
||||
"section/list-baseof.amp.html",
|
||||
"section/baseof.amp.html",
|
||||
"section/sect1-baseof.fr.html",
|
||||
"section/section-baseof.fr.html",
|
||||
"section/list-baseof.fr.html",
|
||||
"section/baseof.fr.html",
|
||||
"section/sect1-baseof.html",
|
||||
"section/section-baseof.html",
|
||||
"section/list-baseof.html",
|
||||
"section/baseof.html",
|
||||
"_default/sect1-baseof.fr.amp.html",
|
||||
"_default/section-baseof.fr.amp.html",
|
||||
"_default/list-baseof.fr.amp.html",
|
||||
"_default/baseof.fr.amp.html",
|
||||
"_default/sect1-baseof.amp.html",
|
||||
"_default/section-baseof.amp.html",
|
||||
"_default/list-baseof.amp.html",
|
||||
"_default/baseof.amp.html",
|
||||
"_default/sect1-baseof.fr.html",
|
||||
"_default/section-baseof.fr.html",
|
||||
"_default/list-baseof.fr.html",
|
||||
"_default/baseof.fr.html",
|
||||
"_default/sect1-baseof.html",
|
||||
"_default/section-baseof.html",
|
||||
"_default/list-baseof.html",
|
||||
"_default/baseof.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Section with layout",
|
||||
LayoutDescriptor{Kind: "section", Section: "sect1", Layout: "mylayout", OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"sect1/mylayout.amp.html",
|
||||
"sect1/sect1.amp.html",
|
||||
"sect1/section.amp.html",
|
||||
"sect1/list.amp.html",
|
||||
"sect1/mylayout.html",
|
||||
"sect1/sect1.html",
|
||||
"sect1/section.html",
|
||||
"sect1/list.html",
|
||||
"section/mylayout.amp.html",
|
||||
"section/sect1.amp.html",
|
||||
"section/section.amp.html",
|
||||
"section/list.amp.html",
|
||||
"section/mylayout.html",
|
||||
"section/sect1.html",
|
||||
"section/section.html",
|
||||
"section/list.html",
|
||||
"_default/mylayout.amp.html",
|
||||
"_default/sect1.amp.html",
|
||||
"_default/section.amp.html",
|
||||
"_default/list.amp.html",
|
||||
"_default/mylayout.html",
|
||||
"_default/sect1.html",
|
||||
"_default/section.html",
|
||||
"_default/list.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Term, French, AMP",
|
||||
LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"term/term.fr.amp.html",
|
||||
"term/tags.fr.amp.html",
|
||||
"term/taxonomy.fr.amp.html",
|
||||
"term/list.fr.amp.html",
|
||||
"term/term.amp.html",
|
||||
"term/tags.amp.html",
|
||||
"term/taxonomy.amp.html",
|
||||
"term/list.amp.html",
|
||||
"term/term.fr.html",
|
||||
"term/tags.fr.html",
|
||||
"term/taxonomy.fr.html",
|
||||
"term/list.fr.html",
|
||||
"term/term.html",
|
||||
"term/tags.html",
|
||||
"term/taxonomy.html",
|
||||
"term/list.html",
|
||||
"taxonomy/term.fr.amp.html",
|
||||
"taxonomy/tags.fr.amp.html",
|
||||
"taxonomy/taxonomy.fr.amp.html",
|
||||
"taxonomy/list.fr.amp.html",
|
||||
"taxonomy/term.amp.html",
|
||||
"taxonomy/tags.amp.html",
|
||||
"taxonomy/taxonomy.amp.html",
|
||||
"taxonomy/list.amp.html",
|
||||
"taxonomy/term.fr.html",
|
||||
"taxonomy/tags.fr.html",
|
||||
"taxonomy/taxonomy.fr.html",
|
||||
"taxonomy/list.fr.html",
|
||||
"taxonomy/term.html",
|
||||
"taxonomy/tags.html",
|
||||
"taxonomy/taxonomy.html",
|
||||
"taxonomy/list.html",
|
||||
"tags/term.fr.amp.html",
|
||||
"tags/tags.fr.amp.html",
|
||||
"tags/taxonomy.fr.amp.html",
|
||||
"tags/list.fr.amp.html",
|
||||
"tags/term.amp.html",
|
||||
"tags/tags.amp.html",
|
||||
"tags/taxonomy.amp.html",
|
||||
"tags/list.amp.html",
|
||||
"tags/term.fr.html",
|
||||
"tags/tags.fr.html",
|
||||
"tags/taxonomy.fr.html",
|
||||
"tags/list.fr.html",
|
||||
"tags/term.html",
|
||||
"tags/tags.html",
|
||||
"tags/taxonomy.html",
|
||||
"tags/list.html",
|
||||
"_default/term.fr.amp.html",
|
||||
"_default/tags.fr.amp.html",
|
||||
"_default/taxonomy.fr.amp.html",
|
||||
"_default/list.fr.amp.html",
|
||||
"_default/term.amp.html",
|
||||
"_default/tags.amp.html",
|
||||
"_default/taxonomy.amp.html",
|
||||
"_default/list.amp.html",
|
||||
"_default/term.fr.html",
|
||||
"_default/tags.fr.html",
|
||||
"_default/taxonomy.fr.html",
|
||||
"_default/list.fr.html",
|
||||
"_default/term.html",
|
||||
"_default/tags.html",
|
||||
"_default/taxonomy.html",
|
||||
"_default/list.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Term, baseof, French, AMP",
|
||||
LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"term/term-baseof.fr.amp.html",
|
||||
"term/tags-baseof.fr.amp.html",
|
||||
"term/taxonomy-baseof.fr.amp.html",
|
||||
"term/list-baseof.fr.amp.html",
|
||||
"term/baseof.fr.amp.html",
|
||||
"term/term-baseof.amp.html",
|
||||
"term/tags-baseof.amp.html",
|
||||
"term/taxonomy-baseof.amp.html",
|
||||
"term/list-baseof.amp.html",
|
||||
"term/baseof.amp.html",
|
||||
"term/term-baseof.fr.html",
|
||||
"term/tags-baseof.fr.html",
|
||||
"term/taxonomy-baseof.fr.html",
|
||||
"term/list-baseof.fr.html",
|
||||
"term/baseof.fr.html",
|
||||
"term/term-baseof.html",
|
||||
"term/tags-baseof.html",
|
||||
"term/taxonomy-baseof.html",
|
||||
"term/list-baseof.html",
|
||||
"term/baseof.html",
|
||||
"taxonomy/term-baseof.fr.amp.html",
|
||||
"taxonomy/tags-baseof.fr.amp.html",
|
||||
"taxonomy/taxonomy-baseof.fr.amp.html",
|
||||
"taxonomy/list-baseof.fr.amp.html",
|
||||
"taxonomy/baseof.fr.amp.html",
|
||||
"taxonomy/term-baseof.amp.html",
|
||||
"taxonomy/tags-baseof.amp.html",
|
||||
"taxonomy/taxonomy-baseof.amp.html",
|
||||
"taxonomy/list-baseof.amp.html",
|
||||
"taxonomy/baseof.amp.html",
|
||||
"taxonomy/term-baseof.fr.html",
|
||||
"taxonomy/tags-baseof.fr.html",
|
||||
"taxonomy/taxonomy-baseof.fr.html",
|
||||
"taxonomy/list-baseof.fr.html",
|
||||
"taxonomy/baseof.fr.html",
|
||||
"taxonomy/term-baseof.html",
|
||||
"taxonomy/tags-baseof.html",
|
||||
"taxonomy/taxonomy-baseof.html",
|
||||
"taxonomy/list-baseof.html",
|
||||
"taxonomy/baseof.html",
|
||||
"tags/term-baseof.fr.amp.html",
|
||||
"tags/tags-baseof.fr.amp.html",
|
||||
"tags/taxonomy-baseof.fr.amp.html",
|
||||
"tags/list-baseof.fr.amp.html",
|
||||
"tags/baseof.fr.amp.html",
|
||||
"tags/term-baseof.amp.html",
|
||||
"tags/tags-baseof.amp.html",
|
||||
"tags/taxonomy-baseof.amp.html",
|
||||
"tags/list-baseof.amp.html",
|
||||
"tags/baseof.amp.html",
|
||||
"tags/term-baseof.fr.html",
|
||||
"tags/tags-baseof.fr.html",
|
||||
"tags/taxonomy-baseof.fr.html",
|
||||
"tags/list-baseof.fr.html",
|
||||
"tags/baseof.fr.html",
|
||||
"tags/term-baseof.html",
|
||||
"tags/tags-baseof.html",
|
||||
"tags/taxonomy-baseof.html",
|
||||
"tags/list-baseof.html",
|
||||
"tags/baseof.html",
|
||||
"_default/term-baseof.fr.amp.html",
|
||||
"_default/tags-baseof.fr.amp.html",
|
||||
"_default/taxonomy-baseof.fr.amp.html",
|
||||
"_default/list-baseof.fr.amp.html",
|
||||
"_default/baseof.fr.amp.html",
|
||||
"_default/term-baseof.amp.html",
|
||||
"_default/tags-baseof.amp.html",
|
||||
"_default/taxonomy-baseof.amp.html",
|
||||
"_default/list-baseof.amp.html",
|
||||
"_default/baseof.amp.html",
|
||||
"_default/term-baseof.fr.html",
|
||||
"_default/tags-baseof.fr.html",
|
||||
"_default/taxonomy-baseof.fr.html",
|
||||
"_default/list-baseof.fr.html",
|
||||
"_default/baseof.fr.html",
|
||||
"_default/term-baseof.html",
|
||||
"_default/tags-baseof.html",
|
||||
"_default/taxonomy-baseof.html",
|
||||
"_default/list-baseof.html",
|
||||
"_default/baseof.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Term",
|
||||
LayoutDescriptor{Kind: "term", Section: "tags", OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"term/term.amp.html",
|
||||
"term/tags.amp.html",
|
||||
"term/taxonomy.amp.html",
|
||||
"term/list.amp.html",
|
||||
"term/term.html",
|
||||
"term/tags.html",
|
||||
"term/taxonomy.html",
|
||||
"term/list.html",
|
||||
"taxonomy/term.amp.html",
|
||||
"taxonomy/tags.amp.html",
|
||||
"taxonomy/taxonomy.amp.html",
|
||||
"taxonomy/list.amp.html",
|
||||
"taxonomy/term.html",
|
||||
"taxonomy/tags.html",
|
||||
"taxonomy/taxonomy.html",
|
||||
"taxonomy/list.html",
|
||||
"tags/term.amp.html",
|
||||
"tags/tags.amp.html",
|
||||
"tags/taxonomy.amp.html",
|
||||
"tags/list.amp.html",
|
||||
"tags/term.html",
|
||||
"tags/tags.html",
|
||||
"tags/taxonomy.html",
|
||||
"tags/list.html",
|
||||
"_default/term.amp.html",
|
||||
"_default/tags.amp.html",
|
||||
"_default/taxonomy.amp.html",
|
||||
"_default/list.amp.html",
|
||||
"_default/term.html",
|
||||
"_default/tags.html",
|
||||
"_default/taxonomy.html",
|
||||
"_default/list.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Taxonomy",
|
||||
LayoutDescriptor{Kind: "taxonomy", Section: "categories", OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"categories/categories.terms.amp.html",
|
||||
"categories/terms.amp.html",
|
||||
"categories/taxonomy.amp.html",
|
||||
"categories/list.amp.html",
|
||||
"categories/categories.terms.html",
|
||||
"categories/terms.html",
|
||||
"categories/taxonomy.html",
|
||||
"categories/list.html",
|
||||
"taxonomy/categories.terms.amp.html",
|
||||
"taxonomy/terms.amp.html",
|
||||
"taxonomy/taxonomy.amp.html",
|
||||
"taxonomy/list.amp.html",
|
||||
"taxonomy/categories.terms.html",
|
||||
"taxonomy/terms.html",
|
||||
"taxonomy/taxonomy.html",
|
||||
"taxonomy/list.html",
|
||||
"_default/categories.terms.amp.html",
|
||||
"_default/terms.amp.html",
|
||||
"_default/taxonomy.amp.html",
|
||||
"_default/list.amp.html",
|
||||
"_default/categories.terms.html",
|
||||
"_default/terms.html",
|
||||
"_default/taxonomy.html",
|
||||
"_default/list.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Page",
|
||||
LayoutDescriptor{Kind: "page", OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"_default/single.amp.html",
|
||||
"_default/single.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Page, baseof",
|
||||
LayoutDescriptor{Kind: "page", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"_default/single-baseof.amp.html",
|
||||
"_default/baseof.amp.html",
|
||||
"_default/single-baseof.html",
|
||||
"_default/baseof.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Page with layout",
|
||||
LayoutDescriptor{Kind: "page", Layout: "mylayout", OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"_default/mylayout.amp.html",
|
||||
"_default/single.amp.html",
|
||||
"_default/mylayout.html",
|
||||
"_default/single.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Page with layout, baseof",
|
||||
LayoutDescriptor{Kind: "page", Layout: "mylayout", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"_default/mylayout-baseof.amp.html",
|
||||
"_default/single-baseof.amp.html",
|
||||
"_default/baseof.amp.html",
|
||||
"_default/mylayout-baseof.html",
|
||||
"_default/single-baseof.html",
|
||||
"_default/baseof.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Page with layout and type",
|
||||
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"myttype/mylayout.amp.html",
|
||||
"myttype/single.amp.html",
|
||||
"myttype/mylayout.html",
|
||||
"myttype/single.html",
|
||||
"_default/mylayout.amp.html",
|
||||
"_default/single.amp.html",
|
||||
"_default/mylayout.html",
|
||||
"_default/single.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Page baseof with layout and type",
|
||||
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"myttype/mylayout-baseof.amp.html",
|
||||
"myttype/single-baseof.amp.html",
|
||||
"myttype/baseof.amp.html",
|
||||
"myttype/mylayout-baseof.html",
|
||||
"myttype/single-baseof.html",
|
||||
"myttype/baseof.html",
|
||||
"_default/mylayout-baseof.amp.html",
|
||||
"_default/single-baseof.amp.html",
|
||||
"_default/baseof.amp.html",
|
||||
"_default/mylayout-baseof.html",
|
||||
"_default/single-baseof.html",
|
||||
"_default/baseof.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Page baseof with layout and type in French",
|
||||
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"myttype/mylayout-baseof.fr.amp.html",
|
||||
"myttype/single-baseof.fr.amp.html",
|
||||
"myttype/baseof.fr.amp.html",
|
||||
"myttype/mylayout-baseof.amp.html",
|
||||
"myttype/single-baseof.amp.html",
|
||||
"myttype/baseof.amp.html",
|
||||
"myttype/mylayout-baseof.fr.html",
|
||||
"myttype/single-baseof.fr.html",
|
||||
"myttype/baseof.fr.html",
|
||||
"myttype/mylayout-baseof.html",
|
||||
"myttype/single-baseof.html",
|
||||
"myttype/baseof.html",
|
||||
"_default/mylayout-baseof.fr.amp.html",
|
||||
"_default/single-baseof.fr.amp.html",
|
||||
"_default/baseof.fr.amp.html",
|
||||
"_default/mylayout-baseof.amp.html",
|
||||
"_default/single-baseof.amp.html",
|
||||
"_default/baseof.amp.html",
|
||||
"_default/mylayout-baseof.fr.html",
|
||||
"_default/single-baseof.fr.html",
|
||||
"_default/baseof.fr.html",
|
||||
"_default/mylayout-baseof.html",
|
||||
"_default/single-baseof.html",
|
||||
"_default/baseof.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Page with layout and type with subtype",
|
||||
LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype/mysubtype", OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"myttype/mysubtype/mylayout.amp.html",
|
||||
"myttype/mysubtype/single.amp.html",
|
||||
"myttype/mysubtype/mylayout.html",
|
||||
"myttype/mysubtype/single.html",
|
||||
"_default/mylayout.amp.html",
|
||||
"_default/single.amp.html",
|
||||
"_default/mylayout.html",
|
||||
"_default/single.html",
|
||||
},
|
||||
},
|
||||
// RSS
|
||||
{
|
||||
"RSS Home",
|
||||
LayoutDescriptor{Kind: "home", OutputFormatName: "rss", Suffix: "xml"},
|
||||
"",
|
||||
[]string{
|
||||
"index.rss.xml",
|
||||
"home.rss.xml",
|
||||
"rss.xml",
|
||||
"list.rss.xml",
|
||||
"index.xml",
|
||||
"home.xml",
|
||||
"list.xml",
|
||||
"_default/index.rss.xml",
|
||||
"_default/home.rss.xml",
|
||||
"_default/rss.xml",
|
||||
"_default/list.rss.xml",
|
||||
"_default/index.xml",
|
||||
"_default/home.xml",
|
||||
"_default/list.xml",
|
||||
"_internal/_default/rss.xml",
|
||||
},
|
||||
},
|
||||
{
|
||||
"RSS Home, baseof",
|
||||
LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "rss", Suffix: "xml"},
|
||||
"",
|
||||
[]string{
|
||||
"index-baseof.rss.xml",
|
||||
"home-baseof.rss.xml",
|
||||
"list-baseof.rss.xml",
|
||||
"baseof.rss.xml",
|
||||
"index-baseof.xml",
|
||||
"home-baseof.xml",
|
||||
"list-baseof.xml",
|
||||
"baseof.xml",
|
||||
"_default/index-baseof.rss.xml",
|
||||
"_default/home-baseof.rss.xml",
|
||||
"_default/list-baseof.rss.xml",
|
||||
"_default/baseof.rss.xml",
|
||||
"_default/index-baseof.xml",
|
||||
"_default/home-baseof.xml",
|
||||
"_default/list-baseof.xml",
|
||||
"_default/baseof.xml",
|
||||
},
|
||||
},
|
||||
{
|
||||
"RSS Section",
|
||||
LayoutDescriptor{Kind: "section", Section: "sect1", OutputFormatName: "rss", Suffix: "xml"},
|
||||
"",
|
||||
[]string{
|
||||
"sect1/sect1.rss.xml",
|
||||
"sect1/section.rss.xml",
|
||||
"sect1/rss.xml",
|
||||
"sect1/list.rss.xml",
|
||||
"sect1/sect1.xml",
|
||||
"sect1/section.xml",
|
||||
"sect1/list.xml",
|
||||
"section/sect1.rss.xml",
|
||||
"section/section.rss.xml",
|
||||
"section/rss.xml",
|
||||
"section/list.rss.xml",
|
||||
"section/sect1.xml",
|
||||
"section/section.xml",
|
||||
"section/list.xml",
|
||||
"_default/sect1.rss.xml",
|
||||
"_default/section.rss.xml",
|
||||
"_default/rss.xml",
|
||||
"_default/list.rss.xml",
|
||||
"_default/sect1.xml",
|
||||
"_default/section.xml",
|
||||
"_default/list.xml",
|
||||
"_internal/_default/rss.xml",
|
||||
},
|
||||
},
|
||||
{
|
||||
"RSS Term",
|
||||
LayoutDescriptor{Kind: "term", Section: "tag", OutputFormatName: "rss", Suffix: "xml"},
|
||||
"",
|
||||
[]string{
|
||||
"term/term.rss.xml",
|
||||
"term/tag.rss.xml",
|
||||
"term/taxonomy.rss.xml",
|
||||
"term/rss.xml",
|
||||
"term/list.rss.xml",
|
||||
"term/term.xml",
|
||||
"term/tag.xml",
|
||||
"term/taxonomy.xml",
|
||||
"term/list.xml",
|
||||
"taxonomy/term.rss.xml",
|
||||
"taxonomy/tag.rss.xml",
|
||||
"taxonomy/taxonomy.rss.xml",
|
||||
"taxonomy/rss.xml",
|
||||
"taxonomy/list.rss.xml",
|
||||
"taxonomy/term.xml",
|
||||
"taxonomy/tag.xml",
|
||||
"taxonomy/taxonomy.xml",
|
||||
"taxonomy/list.xml",
|
||||
"tag/term.rss.xml",
|
||||
"tag/tag.rss.xml",
|
||||
"tag/taxonomy.rss.xml",
|
||||
"tag/rss.xml",
|
||||
"tag/list.rss.xml",
|
||||
"tag/term.xml",
|
||||
"tag/tag.xml",
|
||||
"tag/taxonomy.xml",
|
||||
"tag/list.xml",
|
||||
"_default/term.rss.xml",
|
||||
"_default/tag.rss.xml",
|
||||
"_default/taxonomy.rss.xml",
|
||||
"_default/rss.xml",
|
||||
"_default/list.rss.xml",
|
||||
"_default/term.xml",
|
||||
"_default/tag.xml",
|
||||
"_default/taxonomy.xml",
|
||||
"_default/list.xml",
|
||||
"_internal/_default/rss.xml",
|
||||
},
|
||||
},
|
||||
{
|
||||
"RSS Taxonomy",
|
||||
LayoutDescriptor{Kind: "taxonomy", Section: "tag", OutputFormatName: "rss", Suffix: "xml"},
|
||||
"",
|
||||
[]string{
|
||||
"tag/tag.terms.rss.xml",
|
||||
"tag/terms.rss.xml",
|
||||
"tag/taxonomy.rss.xml",
|
||||
"tag/rss.xml",
|
||||
"tag/list.rss.xml",
|
||||
"tag/tag.terms.xml",
|
||||
"tag/terms.xml",
|
||||
"tag/taxonomy.xml",
|
||||
"tag/list.xml",
|
||||
"taxonomy/tag.terms.rss.xml",
|
||||
"taxonomy/terms.rss.xml",
|
||||
"taxonomy/taxonomy.rss.xml",
|
||||
"taxonomy/rss.xml",
|
||||
"taxonomy/list.rss.xml",
|
||||
"taxonomy/tag.terms.xml",
|
||||
"taxonomy/terms.xml",
|
||||
"taxonomy/taxonomy.xml",
|
||||
"taxonomy/list.xml",
|
||||
"_default/tag.terms.rss.xml",
|
||||
"_default/terms.rss.xml",
|
||||
"_default/taxonomy.rss.xml",
|
||||
"_default/rss.xml",
|
||||
"_default/list.rss.xml",
|
||||
"_default/tag.terms.xml",
|
||||
"_default/terms.xml",
|
||||
"_default/taxonomy.xml",
|
||||
"_default/list.xml",
|
||||
"_internal/_default/rss.xml",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Home plain text",
|
||||
LayoutDescriptor{Kind: "home", OutputFormatName: "json", Suffix: "json"},
|
||||
"",
|
||||
[]string{
|
||||
"index.json.json",
|
||||
"home.json.json",
|
||||
"list.json.json",
|
||||
"index.json",
|
||||
"home.json",
|
||||
"list.json",
|
||||
"_default/index.json.json",
|
||||
"_default/home.json.json",
|
||||
"_default/list.json.json",
|
||||
"_default/index.json",
|
||||
"_default/home.json",
|
||||
"_default/list.json",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Page plain text",
|
||||
LayoutDescriptor{Kind: "page", OutputFormatName: "json", Suffix: "json"},
|
||||
"",
|
||||
[]string{
|
||||
"_default/single.json.json",
|
||||
"_default/single.json",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Reserved section, shortcodes",
|
||||
LayoutDescriptor{Kind: "section", Section: "shortcodes", Type: "shortcodes", OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"section/shortcodes.amp.html",
|
||||
"section/section.amp.html",
|
||||
"section/list.amp.html",
|
||||
"section/shortcodes.html",
|
||||
"section/section.html",
|
||||
"section/list.html",
|
||||
"_default/shortcodes.amp.html",
|
||||
"_default/section.amp.html",
|
||||
"_default/list.amp.html",
|
||||
"_default/shortcodes.html",
|
||||
"_default/section.html",
|
||||
"_default/list.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Reserved section, partials",
|
||||
LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials", OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"section/partials.amp.html",
|
||||
"section/section.amp.html",
|
||||
"section/list.amp.html",
|
||||
"section/partials.html",
|
||||
"section/section.html",
|
||||
"section/list.html",
|
||||
"_default/partials.amp.html",
|
||||
"_default/section.amp.html",
|
||||
"_default/list.amp.html",
|
||||
"_default/partials.html",
|
||||
"_default/section.html",
|
||||
"_default/list.html",
|
||||
},
|
||||
},
|
||||
// This is currently always HTML only
|
||||
{
|
||||
"404, HTML",
|
||||
LayoutDescriptor{Kind: "404", OutputFormatName: "html", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"404.html.html",
|
||||
"404.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"404, HTML baseof",
|
||||
LayoutDescriptor{Kind: "404", Baseof: true, OutputFormatName: "html", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"404-baseof.html.html",
|
||||
"baseof.html.html",
|
||||
"404-baseof.html",
|
||||
"baseof.html",
|
||||
"_default/404-baseof.html.html",
|
||||
"_default/baseof.html.html",
|
||||
"_default/404-baseof.html",
|
||||
"_default/baseof.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Content hook",
|
||||
LayoutDescriptor{Kind: "render-link", RenderingHook: true, Layout: "mylayout", Section: "blog", OutputFormatName: "amp", Suffix: "html"},
|
||||
"",
|
||||
[]string{
|
||||
"blog/_markup/render-link.amp.html",
|
||||
"blog/_markup/render-link.html",
|
||||
"_default/_markup/render-link.amp.html",
|
||||
"_default/_markup/render-link.html",
|
||||
},
|
||||
},
|
||||
} {
|
||||
c.Run(this.name, func(c *qt.C) {
|
||||
l := NewLayoutHandler()
|
||||
|
||||
layouts, err := l.For(this.layoutDescriptor)
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(layouts, qt.Not(qt.IsNil), qt.Commentf(this.layoutDescriptor.Kind))
|
||||
|
||||
if !reflect.DeepEqual(layouts, this.expect) {
|
||||
r := strings.NewReplacer(
|
||||
"[", "\t\"",
|
||||
"]", "\",",
|
||||
" ", "\",\n\t\"",
|
||||
)
|
||||
fmtGot := r.Replace(fmt.Sprintf("%v", layouts))
|
||||
fmtExp := r.Replace(fmt.Sprintf("%v", this.expect))
|
||||
|
||||
c.Fatalf("got %d items, expected %d:\nGot:\n\t%v\nExpected:\n\t%v\nDiff:\n%s", len(layouts), len(this.expect), layouts, this.expect, diff.Diff(fmtExp, fmtGot))
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func BenchmarkLayout(b *testing.B) {
|
||||
descriptor := LayoutDescriptor{Kind: "taxonomy", Section: "categories"}
|
||||
l := NewLayoutHandler()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := l.For(descriptor, HTMLFormat)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLayoutUncached(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
descriptor := LayoutDescriptor{Kind: "taxonomy", Section: "categories"}
|
||||
l := NewLayoutHandler()
|
||||
|
||||
_, err := l.For(descriptor, HTMLFormat)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -133,6 +133,15 @@ var (
|
|||
Weight: 10,
|
||||
}
|
||||
|
||||
// Alias is the output format used for alias redirects.
|
||||
AliasHTMLFormat = Format{
|
||||
Name: "alias",
|
||||
MediaType: media.Builtin.HTMLType,
|
||||
IsHTML: true,
|
||||
Ugly: true,
|
||||
Permalinkable: false,
|
||||
}
|
||||
|
||||
MarkdownFormat = Format{
|
||||
Name: "markdown",
|
||||
MediaType: media.Builtin.MarkdownType,
|
||||
|
@ -192,8 +201,17 @@ var (
|
|||
Rel: "sitemap",
|
||||
}
|
||||
|
||||
HTTPStatusHTMLFormat = Format{
|
||||
Name: "httpstatus",
|
||||
GotmplFormat = Format{
|
||||
Name: "gotmpl",
|
||||
MediaType: media.Builtin.GotmplType,
|
||||
IsPlainText: true,
|
||||
NotAlternative: true,
|
||||
}
|
||||
|
||||
// I'm not sure having a 404 format is a good idea,
|
||||
// for one, we would want to have multiple formats for this.
|
||||
HTTPStatus404HTMLFormat = Format{
|
||||
Name: "404",
|
||||
MediaType: media.Builtin.HTMLType,
|
||||
NotAlternative: true,
|
||||
Ugly: true,
|
||||
|
@ -209,12 +227,16 @@ var DefaultFormats = Formats{
|
|||
CSSFormat,
|
||||
CSVFormat,
|
||||
HTMLFormat,
|
||||
GotmplFormat,
|
||||
HTTPStatus404HTMLFormat,
|
||||
AliasHTMLFormat,
|
||||
JSONFormat,
|
||||
MarkdownFormat,
|
||||
WebAppManifestFormat,
|
||||
RobotsTxtFormat,
|
||||
RSSFormat,
|
||||
SitemapFormat,
|
||||
SitemapIndexFormat,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
@ -68,7 +68,7 @@ func TestDefaultTypes(t *testing.T) {
|
|||
c.Assert(RSSFormat.NoUgly, qt.Equals, true)
|
||||
c.Assert(CalendarFormat.IsHTML, qt.Equals, false)
|
||||
|
||||
c.Assert(len(DefaultFormats), qt.Equals, 11)
|
||||
c.Assert(len(DefaultFormats), qt.Equals, 15)
|
||||
}
|
||||
|
||||
func TestGetFormatByName(t *testing.T) {
|
||||
|
@ -140,7 +140,7 @@ func TestGetFormatByFilename(t *testing.T) {
|
|||
func TestSort(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
c.Assert(DefaultFormats[0].Name, qt.Equals, "html")
|
||||
c.Assert(DefaultFormats[1].Name, qt.Equals, "amp")
|
||||
c.Assert(DefaultFormats[1].Name, qt.Equals, "404")
|
||||
|
||||
json := JSONFormat
|
||||
json.Weight = 1
|
||||
|
|
|
@ -34,6 +34,7 @@ const (
|
|||
|
||||
// The following are (currently) temporary nodes,
|
||||
// i.e. nodes we create just to render in isolation.
|
||||
KindTemporary = "temporary"
|
||||
KindRSS = "rss"
|
||||
KindSitemap = "sitemap"
|
||||
KindSitemapIndex = "sitemapindex"
|
||||
|
|
|
@ -150,8 +150,8 @@ type InSectionPositioner interface {
|
|||
|
||||
// InternalDependencies is considered an internal interface.
|
||||
type InternalDependencies interface {
|
||||
// GetRelatedDocsHandler is for internal use only.
|
||||
GetRelatedDocsHandler() *RelatedDocsHandler
|
||||
// GetInternalRelatedDocsHandler is for internal use only.
|
||||
GetInternalRelatedDocsHandler() *RelatedDocsHandler
|
||||
}
|
||||
|
||||
// OutputFormatsProvider provides the OutputFormats of a Page.
|
||||
|
|
|
@ -145,7 +145,7 @@ func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) {
|
|||
pb.isUgly = true
|
||||
}
|
||||
|
||||
if d.Type == output.HTTPStatusHTMLFormat || d.Type == output.SitemapFormat || d.Type == output.RobotsTxtFormat {
|
||||
if d.Type == output.HTTPStatus404HTMLFormat || d.Type == output.SitemapFormat || d.Type == output.RobotsTxtFormat {
|
||||
pb.noSubResources = true
|
||||
} else if d.Kind != kinds.KindPage && d.URL == "" && d.Section.Base() != "/" {
|
||||
if d.ExpandedPermalink != "" {
|
||||
|
|
|
@ -129,7 +129,7 @@ func (p Pages) withInvertedIndex(ctx context.Context, search func(idx *related.I
|
|||
return nil, fmt.Errorf("invalid type %T in related search", p[0])
|
||||
}
|
||||
|
||||
cache := d.GetRelatedDocsHandler()
|
||||
cache := d.GetInternalRelatedDocsHandler()
|
||||
|
||||
searchIndex, err := cache.getOrCreateIndex(ctx, p)
|
||||
if err != nil {
|
||||
|
|
|
@ -221,7 +221,7 @@ func (p *testPage) GetTerms(taxonomy string) Pages {
|
|||
panic("testpage: not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) GetRelatedDocsHandler() *RelatedDocsHandler {
|
||||
func (p *testPage) GetInternalRelatedDocsHandler() *RelatedDocsHandler {
|
||||
return relatedDocsHandler
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,6 @@ import (
|
|||
"github.com/gohugoio/hugo/resources/images"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
)
|
||||
|
||||
func NewSpec(
|
||||
|
@ -123,8 +122,6 @@ type Spec struct {
|
|||
BuildClosers types.CloseAdder
|
||||
Rebuilder identity.SignalRebuilder
|
||||
|
||||
TextTemplates tpl.TemplateParseFinder
|
||||
|
||||
Permalinks page.PermalinkExpander
|
||||
|
||||
ImageCache *ImageCache
|
||||
|
|
|
@ -23,17 +23,17 @@ import (
|
|||
"github.com/gohugoio/hugo/resources"
|
||||
"github.com/gohugoio/hugo/resources/internal"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
)
|
||||
|
||||
// Client contains methods to perform template processing of Resource objects.
|
||||
type Client struct {
|
||||
rs *resources.Spec
|
||||
t tpl.TemplatesProvider
|
||||
t tplimpl.TemplateStoreProvider
|
||||
}
|
||||
|
||||
// New creates a new Client with the given specification.
|
||||
func New(rs *resources.Spec, t tpl.TemplatesProvider) *Client {
|
||||
func New(rs *resources.Spec, t tplimpl.TemplateStoreProvider) *Client {
|
||||
if rs == nil {
|
||||
panic("must provide a resource Spec")
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ func New(rs *resources.Spec, t tpl.TemplatesProvider) *Client {
|
|||
|
||||
type executeAsTemplateTransform struct {
|
||||
rs *resources.Spec
|
||||
t tpl.TemplatesProvider
|
||||
t tplimpl.TemplateStoreProvider
|
||||
targetPath string
|
||||
data any
|
||||
}
|
||||
|
@ -56,14 +56,13 @@ func (t *executeAsTemplateTransform) Key() internal.ResourceTransformationKey {
|
|||
|
||||
func (t *executeAsTemplateTransform) Transform(ctx *resources.ResourceTransformationCtx) error {
|
||||
tplStr := helpers.ReaderToString(ctx.From)
|
||||
templ, err := t.t.TextTmpl().Parse(ctx.InPath, tplStr)
|
||||
th := t.t.GetTemplateStore()
|
||||
ti, err := th.TextParse(ctx.InPath, tplStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse Resource %q as Template:: %w", ctx.InPath, err)
|
||||
}
|
||||
|
||||
ctx.OutPath = t.targetPath
|
||||
|
||||
return t.t.Tmpl().ExecuteWithContext(ctx.Ctx, templ, ctx.To, t.data)
|
||||
return th.ExecuteWithContext(ctx.Ctx, ti, ctx.To, t.data)
|
||||
}
|
||||
|
||||
func (c *Client) ExecuteAsTemplate(ctx context.Context, res resources.ResourceTransformer, targetPath string, data any) (resource.Resource, error) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
hugo --printUnusedTemplates
|
||||
|
||||
stderr 'Template _default/list.html is unused'
|
||||
stderr 'Template /list.html is unused'
|
||||
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "404", "section", "page"]
|
||||
|
|
|
@ -21,7 +21,6 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hreflect"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
)
|
||||
|
||||
// Apply takes an array or slice c and returns a new slice with the function fname applied over it.
|
||||
|
@ -109,8 +108,7 @@ func applyFnToThis(ctx context.Context, fn, this reflect.Value, args ...any) (re
|
|||
func (ns *Namespace) lookupFunc(ctx context.Context, fname string) (reflect.Value, bool) {
|
||||
namespace, methodName, ok := strings.Cut(fname, ".")
|
||||
if !ok {
|
||||
templ := ns.deps.Tmpl().(tpl.TemplateFuncGetter)
|
||||
return templ.GetFunc(fname)
|
||||
return ns.deps.GetTemplateStore().GetFunc(fname)
|
||||
}
|
||||
|
||||
// Namespace
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/config/testconfig"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/output"
|
||||
"github.com/gohugoio/hugo/output/layouts"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
)
|
||||
|
||||
type templateFinder int
|
||||
|
||||
func (templateFinder) GetIdentity(string) (identity.Identity, bool) {
|
||||
return identity.StringIdentity("test"), true
|
||||
}
|
||||
|
||||
func (templateFinder) Lookup(name string) (tpl.Template, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (templateFinder) HasTemplate(name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (templateFinder) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
func (templateFinder) LookupVariants(name string) []tpl.Template {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (templateFinder) LookupLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (templateFinder) Execute(t tpl.Template, wr io.Writer, data any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (templateFinder) ExecuteWithContext(ctx context.Context, t tpl.Template, wr io.Writer, data any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (templateFinder) GetFunc(name string) (reflect.Value, bool) {
|
||||
if name == "dobedobedo" {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
|
||||
return reflect.ValueOf(fmt.Sprint), true
|
||||
}
|
||||
|
||||
func TestApply(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
d := testconfig.GetTestDeps(nil, nil)
|
||||
d.SetTempl(&tpl.TemplateHandlers{
|
||||
Tmpl: new(templateFinder),
|
||||
})
|
||||
ns := New(d)
|
||||
|
||||
strings := []any{"a\n", "b\n"}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := ns.Apply(ctx, strings, "print", "a", "b", "c")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(result, qt.DeepEquals, []any{"abc", "abc"})
|
||||
|
||||
_, err = ns.Apply(ctx, strings, "apply", ".")
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
|
||||
var nilErr *error
|
||||
_, err = ns.Apply(ctx, nilErr, "chomp", ".")
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
|
||||
_, err = ns.Apply(ctx, strings, "dobedobedo", ".")
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
|
||||
_, err = ns.Apply(ctx, strings, "foo.Chomp", "c\n")
|
||||
if err == nil {
|
||||
t.Errorf("apply with unknown func should fail")
|
||||
}
|
||||
}
|
|
@ -14,6 +14,8 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
)
|
||||
|
@ -51,3 +53,28 @@ func indirect(a any) any {
|
|||
|
||||
return in
|
||||
}
|
||||
|
||||
// CloneShallow creates a shallow copy of the template. It does not clone or copy the nested templates.
|
||||
func (t *Template) CloneShallow() (*Template, error) {
|
||||
t.nameSpace.mu.Lock()
|
||||
defer t.nameSpace.mu.Unlock()
|
||||
if t.escapeErr != nil {
|
||||
return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
|
||||
}
|
||||
textClone, err := t.text.Clone()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ns := &nameSpace{set: make(map[string]*Template)}
|
||||
ns.esc = makeEscaper(ns)
|
||||
ret := &Template{
|
||||
nil,
|
||||
textClone,
|
||||
textClone.Tree,
|
||||
ns,
|
||||
}
|
||||
ret.set[ret.Name()] = ret
|
||||
|
||||
// Return the template associated with the name of this template.
|
||||
return ret.set[ret.Name()], nil
|
||||
}
|
||||
|
|
|
@ -267,7 +267,7 @@ func (t *Template) Clone() (*Template, error) {
|
|||
name := x.Name()
|
||||
src := t.set[name]
|
||||
if src == nil || src.escapeErr != nil {
|
||||
return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
|
||||
return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed, %q not found", t.Name(), name)
|
||||
}
|
||||
x.Tree = x.Tree.Copy()
|
||||
ret.set[name] = &Template{
|
||||
|
|
|
@ -35,7 +35,7 @@ Josie
|
|||
Name, Gift string
|
||||
Attended bool
|
||||
}
|
||||
var recipients = []Recipient{
|
||||
recipients := []Recipient{
|
||||
{"Aunt Mildred", "bone china tea set", true},
|
||||
{"Uncle John", "moleskin pants", false},
|
||||
{"Cousin Rodney", "", false},
|
||||
|
|
|
@ -24,7 +24,7 @@ const name = "math"
|
|||
|
||||
func init() {
|
||||
f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
|
||||
ctx := New()
|
||||
ctx := New(d)
|
||||
|
||||
ns := &internal.TemplateFuncsNamespace{
|
||||
Name: name,
|
||||
|
|
|
@ -20,9 +20,9 @@ import (
|
|||
"math"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
|
||||
_math "github.com/gohugoio/hugo/common/math"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
|
@ -32,12 +32,16 @@ var (
|
|||
)
|
||||
|
||||
// New returns a new instance of the math-namespaced template functions.
|
||||
func New() *Namespace {
|
||||
return &Namespace{}
|
||||
func New(d *deps.Deps) *Namespace {
|
||||
return &Namespace{
|
||||
d: d,
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace provides template functions for the "math" namespace.
|
||||
type Namespace struct{}
|
||||
type Namespace struct {
|
||||
d *deps.Deps
|
||||
}
|
||||
|
||||
// Abs returns the absolute value of n.
|
||||
func (ns *Namespace) Abs(n any) (float64, error) {
|
||||
|
@ -345,8 +349,6 @@ func (ns *Namespace) doArithmetic(inputs []any, operation rune) (value any, err
|
|||
return
|
||||
}
|
||||
|
||||
var counter uint64
|
||||
|
||||
// Counter increments and returns a global counter.
|
||||
// This was originally added to be used in tests where now.UnixNano did not
|
||||
// have the needed precision (especially on Windows).
|
||||
|
@ -354,5 +356,5 @@ var counter uint64
|
|||
// and the counter will reset on new builds.
|
||||
// <docsmeta>{"identifiers": ["now.UnixNano"] }</docsmeta>
|
||||
func (ns *Namespace) Counter() uint64 {
|
||||
return atomic.AddUint64(&counter, uint64(1))
|
||||
return ns.d.Counters.MathCounter.Add(1)
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ func TestBasicNSArithmetic(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
type TestCase struct {
|
||||
fn func(inputs ...any) (any, error)
|
||||
|
@ -66,7 +66,7 @@ func TestBasicNSArithmetic(t *testing.T) {
|
|||
func TestAbs(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
x any
|
||||
|
@ -93,7 +93,7 @@ func TestAbs(t *testing.T) {
|
|||
func TestCeil(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
x any
|
||||
|
@ -126,7 +126,7 @@ func TestFloor(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
x any
|
||||
|
@ -159,7 +159,7 @@ func TestLog(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
a any
|
||||
|
@ -200,7 +200,7 @@ func TestSqrt(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
a any
|
||||
|
@ -239,7 +239,7 @@ func TestMod(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
a any
|
||||
|
@ -279,7 +279,7 @@ func TestModBool(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
a any
|
||||
|
@ -325,7 +325,7 @@ func TestRound(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
x any
|
||||
|
@ -358,7 +358,7 @@ func TestPow(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
a any
|
||||
|
@ -398,7 +398,7 @@ func TestMax(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
type TestCase struct {
|
||||
values []any
|
||||
|
@ -452,7 +452,7 @@ func TestMin(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
type TestCase struct {
|
||||
values []any
|
||||
|
@ -507,7 +507,7 @@ func TestSum(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
mustSum := func(values ...any) any {
|
||||
result, err := ns.Sum(values...)
|
||||
|
@ -530,7 +530,7 @@ func TestProduct(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
mustProduct := func(values ...any) any {
|
||||
result, err := ns.Product(values...)
|
||||
|
@ -554,7 +554,7 @@ func TestPi(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
expect := 3.1415
|
||||
result := ns.Pi()
|
||||
|
@ -570,7 +570,7 @@ func TestSin(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
a any
|
||||
|
@ -604,7 +604,7 @@ func TestCos(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
a any
|
||||
|
@ -638,7 +638,7 @@ func TestTan(t *testing.T) {
|
|||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
a any
|
||||
|
@ -680,7 +680,7 @@ func TestTan(t *testing.T) {
|
|||
func TestAsin(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
x any
|
||||
|
@ -715,7 +715,7 @@ func TestAsin(t *testing.T) {
|
|||
func TestAcos(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
x any
|
||||
|
@ -751,7 +751,7 @@ func TestAcos(t *testing.T) {
|
|||
func TestAtan(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
x any
|
||||
|
@ -782,7 +782,7 @@ func TestAtan(t *testing.T) {
|
|||
func TestAtan2(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
x any
|
||||
|
@ -821,7 +821,7 @@ func TestAtan2(t *testing.T) {
|
|||
func TestToDegrees(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
x any
|
||||
|
@ -852,7 +852,7 @@ func TestToDegrees(t *testing.T) {
|
|||
func TestToRadians(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
ns := New()
|
||||
ns := New(nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
x any
|
||||
|
|
|
@ -25,12 +25,12 @@ import (
|
|||
|
||||
"github.com/bep/lazycache"
|
||||
|
||||
"github.com/gohugoio/hugo/common/constants"
|
||||
"github.com/gohugoio/hugo/common/hashing"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
|
||||
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
|
||||
bp "github.com/gohugoio/hugo/bufferpool"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
|
@ -54,13 +54,6 @@ func (k partialCacheKey) Key() string {
|
|||
return hashing.HashString(append([]any{k.Name}, k.Variants...)...)
|
||||
}
|
||||
|
||||
func (k partialCacheKey) templateName() string {
|
||||
if !strings.HasPrefix(k.Name, "partials/") {
|
||||
return "partials/" + k.Name
|
||||
}
|
||||
return k.Name
|
||||
}
|
||||
|
||||
// partialCache represents a LRU cache of partials.
|
||||
type partialCache struct {
|
||||
cache *lazycache.Cache[string, includeResult]
|
||||
|
@ -129,6 +122,11 @@ func (ns *Namespace) Include(ctx context.Context, name string, contextList ...an
|
|||
}
|
||||
|
||||
func (ns *Namespace) includWithTimeout(ctx context.Context, name string, dataList ...any) includeResult {
|
||||
if strings.HasPrefix(name, "partials/") {
|
||||
// This is most likely not what the user intended.
|
||||
// This worked before Hugo 0.146.0.
|
||||
ns.deps.Log.Warnidf(constants.WarnPartialSuperfluousPrefix, "Partial name %q starting with 'partials/' (as in {{ partial \"%s\"}}) is most likely not what you want. Before 0.146.0 we did a double lookup in this situation.", name, name)
|
||||
}
|
||||
// Create a new context with a timeout not connected to the incoming context.
|
||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), ns.deps.Conf.Timeout())
|
||||
defer cancel()
|
||||
|
@ -159,28 +157,14 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
|
|||
if len(dataList) > 0 {
|
||||
data = dataList[0]
|
||||
}
|
||||
|
||||
var n string
|
||||
if strings.HasPrefix(name, "partials/") {
|
||||
n = name
|
||||
} else {
|
||||
n = "partials/" + name
|
||||
}
|
||||
|
||||
templ, found := ns.deps.Tmpl().Lookup(n)
|
||||
if !found {
|
||||
// For legacy reasons.
|
||||
templ, found = ns.deps.Tmpl().Lookup(n + ".html")
|
||||
}
|
||||
|
||||
if !found {
|
||||
name, desc := ns.deps.TemplateStore.TemplateDescriptorFromPath(name)
|
||||
v := ns.deps.TemplateStore.LookupPartial(name, desc)
|
||||
if v == nil {
|
||||
return includeResult{err: fmt.Errorf("partial %q not found", name)}
|
||||
}
|
||||
|
||||
var info tpl.ParseInfo
|
||||
if ip, ok := templ.(tpl.Info); ok {
|
||||
info = ip.ParseInfo()
|
||||
}
|
||||
templ := v
|
||||
info := v.ParseInfo
|
||||
|
||||
var w io.Writer
|
||||
|
||||
|
@ -200,7 +184,7 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
|
|||
w = b
|
||||
}
|
||||
|
||||
if err := ns.deps.Tmpl().ExecuteWithContext(ctx, templ, w, data); err != nil {
|
||||
if err := ns.deps.GetTemplateStore().ExecuteWithContext(ctx, templ, w, data); err != nil {
|
||||
return includeResult{err: err}
|
||||
}
|
||||
|
||||
|
@ -208,14 +192,14 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
|
|||
|
||||
if ctx, ok := data.(*contextWrapper); ok {
|
||||
result = ctx.Result
|
||||
} else if _, ok := templ.(*texttemplate.Template); ok {
|
||||
} else if _, ok := templ.Template.(*texttemplate.Template); ok {
|
||||
result = w.(fmt.Stringer).String()
|
||||
} else {
|
||||
result = template.HTML(w.(fmt.Stringer).String())
|
||||
}
|
||||
|
||||
return includeResult{
|
||||
name: templ.Name(),
|
||||
name: templ.Template.Name(),
|
||||
result: result,
|
||||
}
|
||||
}
|
||||
|
@ -253,9 +237,9 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
|
|||
// The templates that gets executed is measured in Execute.
|
||||
// We need to track the time spent in the cache to
|
||||
// get the totals correct.
|
||||
ns.deps.Metrics.MeasureSince(key.templateName(), start)
|
||||
ns.deps.Metrics.MeasureSince(r.name, start)
|
||||
}
|
||||
ns.deps.Metrics.TrackValue(key.templateName(), r.result, found)
|
||||
ns.deps.Metrics.TrackValue(r.name, r.result, found)
|
||||
}
|
||||
|
||||
if r.mangager != nil && depsManagerIn != nil {
|
||||
|
|
|
@ -170,7 +170,7 @@ D1
|
|||
got := buf.String()
|
||||
|
||||
// Get rid of all the durations, they are never the same.
|
||||
durationRe := regexp.MustCompile(`\b[\.\d]*(ms|µs|s)\b`)
|
||||
durationRe := regexp.MustCompile(`\b[\.\d]*(ms|ns|µs|s)\b`)
|
||||
|
||||
normalize := func(s string) string {
|
||||
s = durationRe.ReplaceAllString(s, "")
|
||||
|
@ -193,10 +193,10 @@ D1
|
|||
|
||||
expect := `
|
||||
0 0 0 1 index.html
|
||||
100 0 0 1 partials/static2.html
|
||||
100 50 1 2 partials/static1.html
|
||||
25 50 2 4 partials/dynamic1.html
|
||||
66 33 1 3 partials/halfdynamic1.html
|
||||
100 0 0 1 _partials/static2.html
|
||||
100 50 1 2 _partials/static1.html
|
||||
25 50 2 4 _partials/dynamic1.html
|
||||
66 33 1 3 _partials/halfdynamic1.html
|
||||
`
|
||||
|
||||
b.Assert(got, hqt.IsSameString, expect)
|
||||
|
|
134
tpl/template.go
134
tpl/template.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -16,9 +16,6 @@ package tpl
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
@ -27,140 +24,18 @@ import (
|
|||
"github.com/gohugoio/hugo/common/hcontext"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/output/layouts"
|
||||
|
||||
"github.com/gohugoio/hugo/output"
|
||||
|
||||
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
)
|
||||
|
||||
// TemplateManager manages the collection of templates.
|
||||
type TemplateManager interface {
|
||||
TemplateHandler
|
||||
TemplateFuncGetter
|
||||
AddTemplate(name, tpl string) error
|
||||
}
|
||||
|
||||
// TemplateVariants describes the possible variants of a template.
|
||||
// All of these may be empty.
|
||||
type TemplateVariants struct {
|
||||
Language string
|
||||
OutputFormat output.Format
|
||||
}
|
||||
|
||||
// TemplateFinder finds templates.
|
||||
type TemplateFinder interface {
|
||||
TemplateLookup
|
||||
TemplateLookupVariant
|
||||
}
|
||||
|
||||
// UnusedTemplatesProvider lists unused templates if the build is configured to track those.
|
||||
type UnusedTemplatesProvider interface {
|
||||
UnusedTemplates() []FileInfo
|
||||
}
|
||||
|
||||
// TemplateHandlers holds the templates needed by Hugo.
|
||||
type TemplateHandlers struct {
|
||||
Tmpl TemplateHandler
|
||||
TxtTmpl TemplateParseFinder
|
||||
}
|
||||
|
||||
type TemplateExecutor interface {
|
||||
ExecuteWithContext(ctx context.Context, t Template, wr io.Writer, data any) error
|
||||
}
|
||||
|
||||
// TemplateHandler finds and executes templates.
|
||||
type TemplateHandler interface {
|
||||
TemplateFinder
|
||||
TemplateExecutor
|
||||
LookupLayout(d layouts.LayoutDescriptor, f output.Format) (Template, bool, error)
|
||||
HasTemplate(name string) bool
|
||||
GetIdentity(name string) (identity.Identity, bool)
|
||||
}
|
||||
|
||||
type TemplateLookup interface {
|
||||
Lookup(name string) (Template, bool)
|
||||
}
|
||||
|
||||
type TemplateLookupVariant interface {
|
||||
// TODO(bep) this currently only works for shortcodes.
|
||||
// We may unify and expand this variant pattern to the
|
||||
// other templates, but we need this now for the shortcodes to
|
||||
// quickly determine if a shortcode has a template for a given
|
||||
// output format.
|
||||
// It returns the template, if it was found or not and if there are
|
||||
// alternative representations (output format, language).
|
||||
// We are currently only interested in output formats, so we should improve
|
||||
// this for speed.
|
||||
LookupVariant(name string, variants TemplateVariants) (Template, bool, bool)
|
||||
LookupVariants(name string) []Template
|
||||
}
|
||||
|
||||
// Template is the common interface between text/template and html/template.
|
||||
type Template interface {
|
||||
Name() string
|
||||
Prepare() (*texttemplate.Template, error)
|
||||
}
|
||||
|
||||
// AddIdentity checks if t is an identity.Identity and returns it if so.
|
||||
// Else it wraps it in a templateIdentity using its name as the base.
|
||||
func AddIdentity(t Template) Template {
|
||||
if _, ok := t.(identity.IdentityProvider); ok {
|
||||
return t
|
||||
}
|
||||
return templateIdentityProvider{
|
||||
Template: t,
|
||||
id: identity.StringIdentity(t.Name()),
|
||||
}
|
||||
}
|
||||
|
||||
type templateIdentityProvider struct {
|
||||
Template
|
||||
id identity.Identity
|
||||
}
|
||||
|
||||
func (t templateIdentityProvider) GetIdentity() identity.Identity {
|
||||
return t.id
|
||||
}
|
||||
|
||||
// TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain.
|
||||
type TemplateParser interface {
|
||||
Parse(name, tpl string) (Template, error)
|
||||
}
|
||||
|
||||
// TemplateParseFinder provides both parsing and finding.
|
||||
type TemplateParseFinder interface {
|
||||
TemplateParser
|
||||
TemplateFinder
|
||||
}
|
||||
|
||||
// TemplateDebugger prints some debug info to stdout.
|
||||
type TemplateDebugger interface {
|
||||
Debug()
|
||||
}
|
||||
|
||||
// TemplatesProvider as implemented by deps.Deps.
|
||||
type TemplatesProvider interface {
|
||||
Tmpl() TemplateHandler
|
||||
TextTmpl() TemplateParseFinder
|
||||
}
|
||||
|
||||
var baseOfRe = regexp.MustCompile("template: (.*?):")
|
||||
|
||||
func extractBaseOf(err string) string {
|
||||
m := baseOfRe.FindStringSubmatch(err)
|
||||
if len(m) == 2 {
|
||||
return m[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// TemplateFuncGetter allows to find a template func by name.
|
||||
type TemplateFuncGetter interface {
|
||||
GetFunc(name string) (reflect.Value, bool)
|
||||
}
|
||||
|
||||
// RenderingContext represents the currently rendered site/language.
|
||||
type RenderingContext struct {
|
||||
Site site
|
||||
SiteOutIdx int
|
||||
|
@ -201,7 +76,9 @@ type site interface {
|
|||
}
|
||||
|
||||
const (
|
||||
// HugoDeferredTemplatePrefix is the prefix for placeholders for deferred templates.
|
||||
HugoDeferredTemplatePrefix = "__hdeferred/"
|
||||
// HugoDeferredTemplateSuffix is the suffix for placeholders for deferred templates.
|
||||
HugoDeferredTemplateSuffix = "__d="
|
||||
)
|
||||
|
||||
|
@ -243,10 +120,11 @@ func StripHTML(s string) string {
|
|||
return s
|
||||
}
|
||||
|
||||
// DeferredExecution holds the template and data for a deferred execution.
|
||||
type DeferredExecution struct {
|
||||
Mu sync.Mutex
|
||||
Ctx context.Context
|
||||
TemplateName string
|
||||
TemplatePath string
|
||||
Data any
|
||||
|
||||
Executed bool
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -15,20 +15,8 @@ package tpl
|
|||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestExtractBaseof(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
replaced := extractBaseOf(`failed: template: _default/baseof.html:37:11: executing "_default/baseof.html" at <.Parents>: can't evaluate field Parents in type *hugolib.PageOutput`)
|
||||
|
||||
c.Assert(replaced, qt.Equals, "_default/baseof.html")
|
||||
c.Assert(extractBaseOf("not baseof for you"), qt.Equals, "")
|
||||
c.Assert(extractBaseOf("template: blog/baseof.html:23:11:"), qt.Equals, "blog/baseof.html")
|
||||
}
|
||||
|
||||
func TestStripHTML(t *testing.T) {
|
||||
type test struct {
|
||||
input, expected string
|
||||
|
|
|
@ -71,6 +71,81 @@ AMP.
|
|||
|
||||
`
|
||||
|
||||
func TestDeferNoBaseof(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
{{ with (templates.Defer (dict "key" "foo")) }}
|
||||
Defer
|
||||
{{ end }}
|
||||
-- content/_index.md --
|
||||
---
|
||||
title: "Home"
|
||||
---
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "Home.\n\n Defer")
|
||||
}
|
||||
|
||||
func TestDeferBaseof(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- layouts/baseof.html --
|
||||
{{ with (templates.Defer (dict "key" "foo")) }}
|
||||
Defer
|
||||
{{ end }}
|
||||
Block:{{ block "main" . }}{{ end }}$
|
||||
-- layouts/index.html --
|
||||
{{ define "main" }}
|
||||
Home.
|
||||
{{ end }}
|
||||
-- content/_index.md --
|
||||
---
|
||||
title: "Home"
|
||||
---
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "Home.\n\n Defer")
|
||||
}
|
||||
|
||||
func TestDeferMain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- layouts/baseof.html --
|
||||
|
||||
Block:{{ block "main" . }}{{ end }}$
|
||||
-- layouts/index.html --
|
||||
{{ define "main" }}
|
||||
Home.
|
||||
{{ with (templates.Defer (dict "key" "foo")) }}
|
||||
Defer
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
-- content/_index.md --
|
||||
---
|
||||
title: "Home"
|
||||
---
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "Home.\n\n Defer")
|
||||
}
|
||||
|
||||
func TestDeferBasic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ type Namespace struct {
|
|||
// Note that this is the Unix-styled relative path including filename suffix,
|
||||
// e.g. partials/header.html
|
||||
func (ns *Namespace) Exists(name string) bool {
|
||||
return ns.deps.Tmpl().HasTemplate(name)
|
||||
return ns.deps.GetTemplateStore().HasTemplate(name)
|
||||
}
|
||||
|
||||
// Defer defers the execution of a template block.
|
||||
|
@ -93,7 +93,7 @@ func (ns *Namespace) DoDefer(ctx context.Context, id string, optsv any) string {
|
|||
_, _ = ns.deps.BuildState.DeferredExecutions.Executions.GetOrCreate(id,
|
||||
func() (*tpl.DeferredExecution, error) {
|
||||
return &tpl.DeferredExecution{
|
||||
TemplateName: templateName,
|
||||
TemplatePath: templateName,
|
||||
Ctx: ctx,
|
||||
Data: opts.Data,
|
||||
Executed: false,
|
||||
|
|
30
tpl/tplimpl/category_string.go
Normal file
30
tpl/tplimpl/category_string.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
// Code generated by "stringer -type Category"; DO NOT EDIT.
|
||||
|
||||
package tplimpl
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[CategoryLayout-1]
|
||||
_ = x[CategoryBaseof-2]
|
||||
_ = x[CategoryMarkup-3]
|
||||
_ = x[CategoryShortcode-4]
|
||||
_ = x[CategoryPartial-5]
|
||||
_ = x[CategoryServer-6]
|
||||
_ = x[CategoryHugo-7]
|
||||
}
|
||||
|
||||
const _Category_name = "CategoryLayoutCategoryBaseofCategoryMarkupCategoryShortcodeCategoryPartialCategoryServerCategoryHugo"
|
||||
|
||||
var _Category_index = [...]uint8{0, 14, 28, 42, 59, 74, 88, 100}
|
||||
|
||||
func (i Category) String() string {
|
||||
i -= 1
|
||||
if i < 0 || i >= Category(len(_Category_index)-1) {
|
||||
return "Category(" + strconv.FormatInt(int64(i+1), 10) + ")"
|
||||
}
|
||||
return _Category_name[_Category_index[i]:_Category_index[i+1]]
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
window.disqus_config = function () {
|
||||
{{with .Params.disqus_identifier }}this.page.identifier = '{{ . }}';{{end}}
|
||||
{{with .Params.disqus_title }}this.page.title = '{{ . }}';{{end}}
|
||||
{{with .Params.disqus_url }}this.page.url = '{{ . | html }}';{{end}}
|
||||
{{with .Params.disqus_url }}this.page.url = '{{ . | transform.HTMLEscape | safeURL }}';{{end}}
|
||||
};
|
||||
(function() {
|
||||
if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) {
|
|
@ -20,7 +20,7 @@
|
|||
{{- if in $validFormats $format }}
|
||||
{{- if gt $page.Paginator.TotalPages 1 }}
|
||||
<ul class="pagination pagination-{{ $format }}">
|
||||
{{- partial (printf "partials/inline/pagination/%s" $format) $page }}
|
||||
{{- partial (printf "inline/pagination/%s" $format) $page }}
|
||||
</ul>
|
||||
{{- end }}
|
||||
{{- else }}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue