tpl/tplimpl: Support sub paths in layouts passed to .Render

E.g. {{ .Render "foo/mylayout" }} matches mylayout templates in
<dir>/foo for every dir from the page's layout path up to the layouts root.

Closes #15056

Co-authored-by: Joe Mooring <joe.mooring@veriphor.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bjørn Erik Pedersen
2026-07-05 11:41:56 +02:00
parent 5a5f4a5495
commit d83ce27ae0
3 changed files with 236 additions and 26 deletions
+49 -23
View File
@@ -388,6 +388,10 @@ type TemplateQuery struct {
// The path to walk down to.
Path string
// Currently only set for page.Render, e.g. using "foo/bar/mylayout";
// in that example SubPath will be "foo/bar" and LayoutFromUser will be "mylayout".
SubPath string
// The name to look for. Used for shortcode queries.
Name string
@@ -426,6 +430,9 @@ func (q *TemplateQuery) init() {
q.Name = strings.ToLower(q.Name)
q.Desc.LayoutFromUser = strings.ToLower(q.Desc.LayoutFromUser)
if q.SubPath != "" {
q.SubPath = paths.AddLeadingSlash(strings.ToLower(paths.ToSlashTrim(q.SubPath)))
}
if q.Category == 0 {
panic("category not set")
@@ -857,36 +864,55 @@ func (s *TemplateStore) inPath(k1, k2 string) bool {
}
func (s *TemplateStore) findBestMatchWalkPath(q TemplateQuery, k1 string, slashCountK1 int, best *bestMatch) {
if q.SubPath != "" {
// Match templates in <dir><SubPath> only, for every ancestor dir of k1.
// Walk from the root down (as WalkPath does) so the nearest match wins
// on equal-weight ties.
for i, distance := 0, slashCountK1; ; distance-- {
k2 := k1[:i] + q.SubPath
if v := s.treeMain.Get(k2); v != nil {
s.findBestMatchIn(q, k2, distance, v, best)
}
if i == len(k1) {
break
}
if j := strings.IndexByte(k1[i+1:], '/'); j >= 0 {
i += j + 1
} else {
i = len(k1)
}
}
return
}
s.treeMain.WalkPath(k1, func(k2 string, v map[nodeKey]*TemplInfo) (bool, error) {
if !s.inPath(k1, k2) {
return false, nil
}
slashCountK2 := strings.Count(k2, "/")
distance := slashCountK1 - slashCountK2
for k, vv := range v {
if vv.category != q.Category {
continue
}
if !q.Consider(vv) {
continue
}
weight := s.dh.compareDescriptors(q.Category, q.Desc, k.d, q.Sites, vv.matrix)
weight.distance = distance
isBetter := best.isBetter(weight, vv)
if isBetter {
best.updateValues(weight, k2, k.d, vv)
}
}
s.findBestMatchIn(q, k2, slashCountK1-strings.Count(k2, "/"), v, best)
return false, nil
})
}
func (s *TemplateStore) findBestMatchIn(q TemplateQuery, k2 string, distance int, v map[nodeKey]*TemplInfo, best *bestMatch) {
for k, vv := range v {
if vv.category != q.Category {
continue
}
if !q.Consider(vv) {
continue
}
weight := s.dh.compareDescriptors(q.Category, q.Desc, k.d, q.Sites, vv.matrix)
weight.distance = distance
if best.isBetter(weight, vv) {
best.updateValues(weight, k2, k.d, vv)
}
}
}
func (t *TemplateStore) addDeferredTemplate(owner *TemplInfo, name string, n *parse.ListNode) error {
if _, found := t.templatesByPath.Get(name); found {
return nil
@@ -1678,7 +1704,7 @@ func (s *TemplateStore) parseTemplates() error {
return err
}
// Prese shortcodes.
// Parse shortcodes.
for _, v := range s.treeShortcodes.All() {
for _, vv := range v {
for _, vvv := range vv {