]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
resources/page: Expand parmalinks tokens in `url`
authorn1xx1 <me@simonemiraglia.it>
Thu, 1 Aug 2024 10:14:29 +0000 (12:14 +0200)
committerGitHub <noreply@github.com>
Thu, 1 Aug 2024 10:14:29 +0000 (12:14 +0200)
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

common/hreflect/helpers.go
common/maps/cache.go
hugolib/page__paths.go
hugolib/page_permalink_test.go
resources/page/permalinks.go
resources/page/permalinks_integration_test.go
tpl/templates/templates.go

index b5a8bacc9293cff16f27979f9381441fc6cf43b7..5113a388613a010dcadd86f98a0f076428345230 100644 (file)
@@ -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
 }
index 7cd7410c2162aa4dc9e13e38579dcbadeb04dc9a..0175974b5b4517ab4db1413ea4d9743b85ef5d83 100644 (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.
-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.
index d89388f81e0de64eef9df39fc0293e27d67c2634..6324b587125bd98e4d785097f67789b17729db35 100644 (file)
@@ -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
index bc89638d3ac176b4e3e01605758658bfe3b25832..d8fd99d79e564044e9d66cac82e3df31d0ec46df 100644 (file)
@@ -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 {
index 67c63c4b21f8a55aa56f0143d5b5498dcc40de13..05911f0eaa638fd8580880008f9ce696eaf567a4 100644 (file)
@@ -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
index 9a76ac602261e24b6f2b8cce43e9196db0b394c8..2b9e878b168d383ad648b8d1feaaa4e8c6afe8c3 100644 (file)
@@ -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/")
+}
index 98b4b4c38735221354c5d2d17c4b02fb413d6aaa..0be44a013ae99ed2636b247c03974d9b0a166a32 100644 (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)
 
-       _ = 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