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
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
}
}
// 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.
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
// 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 {
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
// 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,
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) {
}
}
-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))
callbacks[i] = callback
}
- expanders[k] = func(p Page) (string, error) {
+ return func(p Page) (string, error) {
if matches == nil {
return pattern, nil
}
}
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
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/")
+}
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