mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-14 20:37:55 -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 isContextCache.GetOrCreate(tp, func() bool {
|
||||
return tp.Implements(contextInterface)
|
||||
isContext, _ := isContextCache.GetOrCreate(tp, func() (bool, error) {
|
||||
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.
|
||||
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()
|
||||
v, found := c.m[key]
|
||||
c.RUnlock()
|
||||
if found {
|
||||
return v
|
||||
return v, nil
|
||||
}
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
v, found = c.m[key]
|
||||
if found {
|
||||
return v
|
||||
return v, nil
|
||||
}
|
||||
v, err := create()
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
v = create()
|
||||
c.m[key] = v
|
||||
return v
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// 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.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)
|
||||
if err != nil {
|
||||
return desc, err
|
||||
|
|
|
@ -59,6 +59,8 @@ func TestPermalink(t *testing.T) {
|
|||
|
||||
// test URL overrides
|
||||
{"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 {
|
||||
|
|
|
@ -40,6 +40,8 @@ type PermalinkExpander struct {
|
|||
expanders map[string]map[string]func(Page) (string, error)
|
||||
|
||||
urlize func(uri string) string
|
||||
|
||||
patternCache *maps.Cache[string, func(Page) (string, error)]
|
||||
}
|
||||
|
||||
// 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
|
||||
// urlize func.
|
||||
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{
|
||||
"year": p.pageToPermalinkDate,
|
||||
|
@ -102,6 +107,16 @@ func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]ma
|
|||
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.
|
||||
// If no rules are found for the given key, an empty string is returned.
|
||||
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) {
|
||||
expanders := make(map[string]func(Page) (string, error))
|
||||
|
||||
for k, pattern := range patterns {
|
||||
k = strings.Trim(k, sectionCutSet)
|
||||
|
||||
func (l PermalinkExpander) getOrParsePattern(pattern string) (func(Page) (string, error), error) {
|
||||
return l.patternCache.GetOrCreate(pattern, func() (func(Page) (string, error), error) {
|
||||
if !l.validate(pattern) {
|
||||
return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkIllFormed}
|
||||
}
|
||||
|
||||
pattern := pattern
|
||||
matches := attributeRegexp.FindAllStringSubmatch(pattern, -1)
|
||||
|
||||
callbacks := make([]pageToPermaAttribute, len(matches))
|
||||
|
@ -157,7 +166,7 @@ func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Pa
|
|||
callbacks[i] = callback
|
||||
}
|
||||
|
||||
expanders[k] = func(p Page) (string, error) {
|
||||
return func(p Page) (string, error) {
|
||||
if matches == 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)
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -193,3 +193,42 @@ List.
|
|||
b.AssertFileContent("public/libros/fiction/index.html", "List.")
|
||||
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)
|
||||
|
||||
_ = ns.deps.BuildState.DeferredExecutions.Executions.GetOrCreate(id,
|
||||
func() *tpl.DeferredExecution {
|
||||
_, _ = ns.deps.BuildState.DeferredExecutions.Executions.GetOrCreate(id,
|
||||
func() (*tpl.DeferredExecution, error) {
|
||||
return &tpl.DeferredExecution{
|
||||
TemplateName: templateName,
|
||||
Ctx: ctx,
|
||||
Data: opts.Data,
|
||||
Executed: false,
|
||||
}
|
||||
}, nil
|
||||
})
|
||||
|
||||
return id
|
||||
|
|
Loading…
Reference in a new issue