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:
n1xx1 2024-08-01 12:14:29 +02:00 committed by GitHub
parent 92573012e8
commit 566fe7ba12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 101 additions and 21 deletions

View file

@ -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
} }

View file

@ -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.

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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/")
}

View file

@ -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