mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
resources/page: Expand parmalinks tokens in url
This change allows to use permalink tokens in url front matter fields. This should be useful to target more specific pages instead of using a global permalink configuration. It's expected to be used with cascade. Fixes #9714
This commit is contained in:
parent
92573012e8
commit
566fe7ba12
7 changed files with 101 additions and 21 deletions
|
@ -268,7 +268,8 @@ func IsContextType(tp reflect.Type) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return isContextCache.GetOrCreate(tp, func() bool {
|
isContext, _ := isContextCache.GetOrCreate(tp, func() (bool, error) {
|
||||||
return tp.Implements(contextInterface)
|
return tp.Implements(contextInterface), nil
|
||||||
})
|
})
|
||||||
|
return isContext
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,22 +40,25 @@ func (c *Cache[K, T]) Get(key K) (T, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
|
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
|
||||||
func (c *Cache[K, T]) GetOrCreate(key K, create func() T) T {
|
func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
|
||||||
c.RLock()
|
c.RLock()
|
||||||
v, found := c.m[key]
|
v, found := c.m[key]
|
||||||
c.RUnlock()
|
c.RUnlock()
|
||||||
if found {
|
if found {
|
||||||
return v
|
return v, nil
|
||||||
}
|
}
|
||||||
c.Lock()
|
c.Lock()
|
||||||
defer c.Unlock()
|
defer c.Unlock()
|
||||||
v, found = c.m[key]
|
v, found = c.m[key]
|
||||||
if found {
|
if found {
|
||||||
return v
|
return v, nil
|
||||||
|
}
|
||||||
|
v, err := create()
|
||||||
|
if err != nil {
|
||||||
|
return v, err
|
||||||
}
|
}
|
||||||
v = create()
|
|
||||||
c.m[key] = v
|
c.m[key] = v
|
||||||
return v
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set sets the given key to the given value.
|
// Set sets the given key to the given value.
|
||||||
|
|
|
@ -141,6 +141,19 @@ func createTargetPathDescriptor(p *pageState) (page.TargetPathDescriptor, error)
|
||||||
desc.PrefixFilePath = s.getLanguageTargetPathLang(alwaysInSubDir)
|
desc.PrefixFilePath = s.getLanguageTargetPathLang(alwaysInSubDir)
|
||||||
desc.PrefixLink = s.getLanguagePermalinkLang(alwaysInSubDir)
|
desc.PrefixLink = s.getLanguagePermalinkLang(alwaysInSubDir)
|
||||||
|
|
||||||
|
if desc.URL != "" && strings.IndexByte(desc.URL, ':') >= 0 {
|
||||||
|
// Attempt to parse and expand an url
|
||||||
|
opath, err := d.ResourceSpec.Permalinks.ExpandPattern(desc.URL, p)
|
||||||
|
if err != nil {
|
||||||
|
return desc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opath != "" {
|
||||||
|
opath, _ = url.QueryUnescape(opath)
|
||||||
|
desc.URL = opath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
opath, err := d.ResourceSpec.Permalinks.Expand(p.Section(), p)
|
opath, err := d.ResourceSpec.Permalinks.Expand(p.Section(), p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return desc, err
|
return desc, err
|
||||||
|
|
|
@ -59,6 +59,8 @@ func TestPermalink(t *testing.T) {
|
||||||
|
|
||||||
// test URL overrides
|
// test URL overrides
|
||||||
{"x/y/z/boofar.md", "", "", "/z/y/q/", false, false, "/z/y/q/", "/z/y/q/"},
|
{"x/y/z/boofar.md", "", "", "/z/y/q/", false, false, "/z/y/q/", "/z/y/q/"},
|
||||||
|
// test URL override with expands
|
||||||
|
{"x/y/z/boofar.md", "", "test", "/z/:slug/", false, false, "/z/test/", "/z/test/"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
|
@ -40,6 +40,8 @@ type PermalinkExpander struct {
|
||||||
expanders map[string]map[string]func(Page) (string, error)
|
expanders map[string]map[string]func(Page) (string, error)
|
||||||
|
|
||||||
urlize func(uri string) string
|
urlize func(uri string) string
|
||||||
|
|
||||||
|
patternCache *maps.Cache[string, func(Page) (string, error)]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time for checking date formats. Every field is different than the
|
// Time for checking date formats. Every field is different than the
|
||||||
|
@ -71,7 +73,10 @@ func (p PermalinkExpander) callback(attr string) (pageToPermaAttribute, bool) {
|
||||||
// NewPermalinkExpander creates a new PermalinkExpander configured by the given
|
// NewPermalinkExpander creates a new PermalinkExpander configured by the given
|
||||||
// urlize func.
|
// urlize func.
|
||||||
func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]map[string]string) (PermalinkExpander, error) {
|
func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]map[string]string) (PermalinkExpander, error) {
|
||||||
p := PermalinkExpander{urlize: urlize}
|
p := PermalinkExpander{
|
||||||
|
urlize: urlize,
|
||||||
|
patternCache: maps.NewCache[string, func(Page) (string, error)](),
|
||||||
|
}
|
||||||
|
|
||||||
p.knownPermalinkAttributes = map[string]pageToPermaAttribute{
|
p.knownPermalinkAttributes = map[string]pageToPermaAttribute{
|
||||||
"year": p.pageToPermalinkDate,
|
"year": p.pageToPermalinkDate,
|
||||||
|
@ -102,6 +107,16 @@ func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]ma
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExpandPattern expands the path in p with the specified expand pattern.
|
||||||
|
func (l PermalinkExpander) ExpandPattern(pattern string, p Page) (string, error) {
|
||||||
|
expander, err := l.getOrParsePattern(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return expander(p)
|
||||||
|
}
|
||||||
|
|
||||||
// Expand expands the path in p according to the rules defined for the given key.
|
// Expand expands the path in p according to the rules defined for the given key.
|
||||||
// If no rules are found for the given key, an empty string is returned.
|
// If no rules are found for the given key, an empty string is returned.
|
||||||
func (l PermalinkExpander) Expand(key string, p Page) (string, error) {
|
func (l PermalinkExpander) Expand(key string, p Page) (string, error) {
|
||||||
|
@ -129,17 +144,11 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Page) (string, error), error) {
|
func (l PermalinkExpander) getOrParsePattern(pattern string) (func(Page) (string, error), error) {
|
||||||
expanders := make(map[string]func(Page) (string, error))
|
return l.patternCache.GetOrCreate(pattern, func() (func(Page) (string, error), error) {
|
||||||
|
|
||||||
for k, pattern := range patterns {
|
|
||||||
k = strings.Trim(k, sectionCutSet)
|
|
||||||
|
|
||||||
if !l.validate(pattern) {
|
if !l.validate(pattern) {
|
||||||
return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkIllFormed}
|
return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkIllFormed}
|
||||||
}
|
}
|
||||||
|
|
||||||
pattern := pattern
|
|
||||||
matches := attributeRegexp.FindAllStringSubmatch(pattern, -1)
|
matches := attributeRegexp.FindAllStringSubmatch(pattern, -1)
|
||||||
|
|
||||||
callbacks := make([]pageToPermaAttribute, len(matches))
|
callbacks := make([]pageToPermaAttribute, len(matches))
|
||||||
|
@ -157,7 +166,7 @@ func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Pa
|
||||||
callbacks[i] = callback
|
callbacks[i] = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
expanders[k] = func(p Page) (string, error) {
|
return func(p Page) (string, error) {
|
||||||
if matches == nil {
|
if matches == nil {
|
||||||
return pattern, nil
|
return pattern, nil
|
||||||
}
|
}
|
||||||
|
@ -173,12 +182,25 @@ func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Pa
|
||||||
}
|
}
|
||||||
|
|
||||||
newField = strings.Replace(newField, replacement, newAttr, 1)
|
newField = strings.Replace(newField, replacement, newAttr, 1)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return newField, nil
|
return newField, nil
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Page) (string, error), error) {
|
||||||
|
expanders := make(map[string]func(Page) (string, error))
|
||||||
|
|
||||||
|
for k, pattern := range patterns {
|
||||||
|
k = strings.Trim(k, sectionCutSet)
|
||||||
|
|
||||||
|
expander, err := l.getOrParsePattern(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expanders[k] = expander
|
||||||
}
|
}
|
||||||
|
|
||||||
return expanders, nil
|
return expanders, nil
|
||||||
|
|
|
@ -193,3 +193,42 @@ List.
|
||||||
b.AssertFileContent("public/libros/fiction/index.html", "List.")
|
b.AssertFileContent("public/libros/fiction/index.html", "List.")
|
||||||
b.AssertFileContent("public/libros/fiction/2023/book1/index.html", "Single.")
|
b.AssertFileContent("public/libros/fiction/2023/book1/index.html", "Single.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPermalinksUrlCascade(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
files := `
|
||||||
|
-- layouts/_default/list.html --
|
||||||
|
List|{{ .Kind }}|{{ .RelPermalink }}|
|
||||||
|
-- layouts/_default/single.html --
|
||||||
|
Single|{{ .Kind }}|{{ .RelPermalink }}|
|
||||||
|
-- hugo.toml --
|
||||||
|
-- content/cooking/delicious-recipes/_index.md --
|
||||||
|
---
|
||||||
|
url: /delicious-recipe/
|
||||||
|
cascade:
|
||||||
|
url: /delicious-recipe/:slug/
|
||||||
|
---
|
||||||
|
-- content/cooking/delicious-recipes/example1.md --
|
||||||
|
---
|
||||||
|
title: Recipe 1
|
||||||
|
---
|
||||||
|
-- content/cooking/delicious-recipes/example2.md --
|
||||||
|
---
|
||||||
|
title: Recipe 2
|
||||||
|
slug: custom-recipe-2
|
||||||
|
---
|
||||||
|
`
|
||||||
|
b := hugolib.NewIntegrationTestBuilder(
|
||||||
|
hugolib.IntegrationTestConfig{
|
||||||
|
T: t,
|
||||||
|
TxtarString: files,
|
||||||
|
LogLevel: logg.LevelWarn,
|
||||||
|
}).Build()
|
||||||
|
|
||||||
|
t.Log(b.LogString())
|
||||||
|
b.Assert(b.H.Log.LoggCount(logg.LevelWarn), qt.Equals, 0)
|
||||||
|
b.AssertFileContent("public/delicious-recipe/index.html", "List|section|/delicious-recipe/")
|
||||||
|
b.AssertFileContent("public/delicious-recipe/recipe-1/index.html", "Single|page|/delicious-recipe/recipe-1/")
|
||||||
|
b.AssertFileContent("public/delicious-recipe/custom-recipe-2/index.html", "Single|page|/delicious-recipe/custom-recipe-2/")
|
||||||
|
}
|
||||||
|
|
|
@ -90,14 +90,14 @@ func (ns *Namespace) DoDefer(ctx context.Context, id string, optsv any) string {
|
||||||
|
|
||||||
id = fmt.Sprintf("%s_%s%s", id, key, tpl.HugoDeferredTemplateSuffix)
|
id = fmt.Sprintf("%s_%s%s", id, key, tpl.HugoDeferredTemplateSuffix)
|
||||||
|
|
||||||
_ = ns.deps.BuildState.DeferredExecutions.Executions.GetOrCreate(id,
|
_, _ = ns.deps.BuildState.DeferredExecutions.Executions.GetOrCreate(id,
|
||||||
func() *tpl.DeferredExecution {
|
func() (*tpl.DeferredExecution, error) {
|
||||||
return &tpl.DeferredExecution{
|
return &tpl.DeferredExecution{
|
||||||
TemplateName: templateName,
|
TemplateName: templateName,
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Data: opts.Data,
|
Data: opts.Data,
|
||||||
Executed: false,
|
Executed: false,
|
||||||
}
|
}, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return id
|
return id
|
||||||
|
|
Loading…
Reference in a new issue