hugo

Unnamed repository; edit this file 'description' to name the repository.

git clone git://git.shimmy1996.com/hugo.git
commit e58a540895c28b8884823dcb1b64c272422f9923
parent 20162518c450770ebfd54e0e987f34a5cccf236b
Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Date:   Wed, 20 Oct 2021 10:11:48 +0200

resources: Create a common ResourceFinder interface

And make both .Resources and resources implement it.

This gets us 2 new methods/functions, so you can now also do:

* .Resources.Get
* resources.ByType

Note that GetRemote is not covered by this interface, as that is only available as a global template function.

Fixes #8653

Diffstat:
Mcommon/types/hstring/stringtypes_test.go | 2+-
Mhugolib/hugo_sites_build_test.go | 2++
Mhugolib/pagebundler_test.go | 3++-
Mhugolib/resource_chain_test.go | 11+++++++++--
Mresources/resource/resources.go | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mresources/resource_factories/create/create.go | 25+++++++++++++++----------
Mtpl/resources/resources.go | 49++++++++++++++++++++++++++++++++++++-------------
7 files changed, 148 insertions(+), 36 deletions(-)
diff --git a/common/types/hstring/stringtypes_test.go b/common/types/hstring/stringtypes_test.go
@@ -21,7 +21,7 @@ import (
 	"github.com/spf13/cast"
 )
 
-func TestStringTypes(t *testing.T) {
+func TestRenderedString(t *testing.T) {
 	c := qt.New(t)
 
 	// Validate that it will behave like a string in Hugo settings.
diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go
@@ -397,6 +397,8 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) {
 	c.Assert(bundleFr, qt.Not(qt.IsNil))
 	c.Assert(len(bundleFr.Resources()), qt.Equals, 1)
 	logoFr := bundleFr.Resources().GetMatch("logo*")
+	logoFrGet := bundleFr.Resources().Get("logo.png")
+	c.Assert(logoFrGet, qt.Equals, logoFr)
 	c.Assert(logoFr, qt.Not(qt.IsNil))
 	b.AssertFileContent("public/fr/bundles/b1/index.html", "Resources: image/png: /blog/fr/bundles/b1/logo.png")
 	b.AssertFileContent("public/fr/bundles/b1/logo.png", "PNG Data")
diff --git a/hugolib/pagebundler_test.go b/hugolib/pagebundler_test.go
@@ -553,7 +553,8 @@ HEADLESS {{< myShort >}}
 
 	headlessResources := headless.Resources()
 	c.Assert(len(headlessResources), qt.Equals, 3)
-	c.Assert(len(headlessResources.Match("l*")), qt.Equals, 2)
+	res := headlessResources.Match("l*")
+	c.Assert(len(res), qt.Equals, 2)
 	pageResource := headlessResources.GetMatch("p*")
 	c.Assert(pageResource, qt.Not(qt.IsNil))
 	p := pageResource.(page.Page)
diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go
@@ -696,6 +696,8 @@ func TestResourcesMatch(t *testing.T) {
 	b.WithContent("page.md", "")
 
 	b.WithSourceFile(
+		"assets/images/img1.png", "png",
+		"assets/images/img2.jpg", "jpg",
 		"assets/jsons/data1.json", "json1 content",
 		"assets/jsons/data2.json", "json2 content",
 		"assets/jsons/data3.xml", "xml content",
@@ -704,7 +706,9 @@ func TestResourcesMatch(t *testing.T) {
 	b.WithTemplates("index.html", `
 {{ $jsons := (resources.Match "jsons/*.json") }}
 {{ $json := (resources.GetMatch "jsons/*.json") }}
-{{ printf "JSONS: %d"  (len $jsons) }}
+{{ printf "jsonsMatch: %d"  (len $jsons) }}
+{{ printf "imagesByType: %d"  (len (resources.ByType "image") ) }}
+{{ printf "applicationByType: %d"  (len (resources.ByType "application") ) }}
 JSON: {{ $json.RelPermalink }}: {{ $json.Content }}
 {{ range $jsons }}
 {{- .RelPermalink }}: {{ .Content }}
@@ -715,7 +719,10 @@ JSON: {{ $json.RelPermalink }}: {{ $json.Content }}
 
 	b.AssertFileContent("public/index.html",
 		"JSON: /jsons/data1.json: json1 content",
-		"JSONS: 2", "/jsons/data1.json: json1 content")
+		"jsonsMatch: 2",
+		"imagesByType: 2",
+		"applicationByType: 3",
+		"/jsons/data1.json: json1 content")
 }
 
 func TestResourceMinifyDisabled(t *testing.T) {
diff --git a/resources/resource/resources.go b/resources/resource/resources.go
@@ -18,35 +18,64 @@ import (
 	"strings"
 
 	"github.com/gohugoio/hugo/hugofs/glob"
+	"github.com/spf13/cast"
 )
 
+var _ ResourceFinder = (*Resources)(nil)
+
 // Resources represents a slice of resources, which can be a mix of different types.
 // I.e. both pages and images etc.
 type Resources []Resource
 
+// var _ resource.ResourceFinder = (*Namespace)(nil)
 // ResourcesConverter converts a given slice of Resource objects to Resources.
 type ResourcesConverter interface {
 	ToResources() Resources
 }
 
-// ByType returns resources of a given resource type (ie. "image").
-func (r Resources) ByType(tp string) Resources {
+// ByType returns resources of a given resource type (e.g. "image").
+func (r Resources) ByType(typ any) Resources {
+	tpstr, err := cast.ToStringE(typ)
+	if err != nil {
+		panic(err)
+	}
 	var filtered Resources
 
 	for _, resource := range r {
-		if resource.ResourceType() == tp {
+		if resource.ResourceType() == tpstr {
 			filtered = append(filtered, resource)
 		}
 	}
 	return filtered
 }
 
+// Get locates the name given in Resources.
+// The search is case insensitive.
+func (r Resources) Get(name any) Resource {
+	namestr, err := cast.ToStringE(name)
+	if err != nil {
+		panic(err)
+	}
+	namestr = strings.ToLower(namestr)
+	for _, resource := range r {
+		if strings.EqualFold(namestr, resource.Name()) {
+			return resource
+		}
+	}
+	return nil
+}
+
 // GetMatch finds the first Resource matching the given pattern, or nil if none found.
 // See Match for a more complete explanation about the rules used.
-func (r Resources) GetMatch(pattern string) Resource {
-	g, err := glob.GetGlob(pattern)
+func (r Resources) GetMatch(pattern any) Resource {
+	patternstr, err := cast.ToStringE(pattern)
+	if err != nil {
+		panic(err)
+	}
+
+	g, err := glob.GetGlob(patternstr)
 	if err != nil {
-		return nil
+		panic(err)
 	}
 
 	for _, resource := range r {
@@ -67,10 +96,15 @@ func (r Resources) GetMatch(pattern string) Resource {
 // Match matches by using the value of Resource.Name, which, by default, is a filename with
 // path relative to the bundle root with Unix style slashes (/) and no leading slash, e.g. "images/logo.png".
 // See https://github.com/gobwas/glob for the full rules set.
-func (r Resources) Match(pattern string) Resources {
-	g, err := glob.GetGlob(pattern)
+func (r Resources) Match(pattern any) Resources {
+	patternstr, err := cast.ToStringE(pattern)
 	if err != nil {
-		return nil
+		panic(err)
+	}
+
+	g, err := glob.GetGlob(patternstr)
+	if err != nil {
+		panic(err)
 	}
 
 	var matches Resources
@@ -121,3 +155,43 @@ func (r Resources) MergeByLanguageInterface(in any) (any, error) {
 type Source interface {
 	Publish() error
 }
+
+// ResourceFinder provides methods to find Resources.
+// Note that GetRemote (as found in resources.GetRemote) is
+// not covered by this interface, as this is only available as a global template function.
+type ResourceFinder interface {
+
+	// Get locates the Resource with the given name in the current context (e.g. in .Page.Resources).
+	//
+	// It returns nil if no Resource could found, panics if name is invalid.
+	Get(name any) Resource
+
+	// GetMatch finds the first Resource matching the given pattern, or nil if none found.
+	//
+	// See Match for a more complete explanation about the rules used.
+	//
+	// It returns nil if no Resource could found, panics if pattern is invalid.
+	GetMatch(pattern any) Resource
+
+	// Match gets all resources matching the given base path prefix, e.g
+	// "*.png" will match all png files. The "*" does not match path delimiters (/),
+	// so if you organize your resources in sub-folders, you need to be explicit about it, e.g.:
+	// "images/*.png". To match any PNG image anywhere in the bundle you can do "**.png", and
+	// to match all PNG images below the images folder, use "images/**.jpg".
+	//
+	// The matching is case insensitive.
+	//
+	// Match matches by using a relative pathwith Unix style slashes (/) and no
+	// leading slash, e.g. "images/logo.png".
+	//
+	// See https://github.com/gobwas/glob for the full rules set.
+	//
+	// See Match for a more complete explanation about the rules used.
+	//
+	// It returns nil if no Resources could found, panics if pattern is invalid.
+	Match(pattern any) Resources
+
+	// ByType returns resources of a given resource type (e.g. "image").
+	// It returns nil if no Resources could found, panics if typ is invalid.
+	ByType(typ any) Resources
+}
diff --git a/resources/resource_factories/create/create.go b/resources/resource_factories/create/create.go
@@ -65,26 +65,27 @@ func (c *Client) Get(filename string) (resource.Resource, error) {
 
 // Match gets the resources matching the given pattern from the assets filesystem.
 func (c *Client) Match(pattern string) (resource.Resources, error) {
-	return c.match(pattern, false)
+	return c.match("__match", pattern, nil, false)
+}
+
+func (c *Client) ByType(tp string) resource.Resources {
+	res, err := c.match(path.Join("_byType", tp), "**", func(r resource.Resource) bool { return r.ResourceType() == tp }, false)
+	if err != nil {
+		panic(err)
+	}
+	return res
 }
 
 // GetMatch gets first resource matching the given pattern from the assets filesystem.
 func (c *Client) GetMatch(pattern string) (resource.Resource, error) {
-	res, err := c.match(pattern, true)
+	res, err := c.match("__get-match", pattern, nil, true)
 	if err != nil || len(res) == 0 {
 		return nil, err
 	}
 	return res[0], err
 }
 
-func (c *Client) match(pattern string, firstOnly bool) (resource.Resources, error) {
-	var name string
-	if firstOnly {
-		name = "__get-match"
-	} else {
-		name = "__match"
-	}
-
+func (c *Client) match(name, pattern string, matchFunc func(r resource.Resource) bool, firstOnly bool) (resource.Resources, error) {
 	pattern = glob.NormalizePath(pattern)
 	partitions := glob.FilterGlobParts(strings.Split(pattern, "/"))
 	if len(partitions) == 0 {
@@ -110,6 +111,10 @@ func (c *Client) match(pattern string, firstOnly bool) (resource.Resources, erro
 				return true, err
 			}
 
+			if matchFunc != nil && !matchFunc(r) {
+				return false, nil
+			}
+
 			res = append(res, r)
 
 			return firstOnly, nil
diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go
@@ -16,9 +16,10 @@ package resources
 
 import (
 	"fmt"
-	"path/filepath"
 	"sync"
 
+	"github.com/gohugoio/hugo/common/herrors"
+
 	"github.com/gohugoio/hugo/common/maps"
 	"github.com/pkg/errors"
 
@@ -73,6 +74,8 @@ func New(deps *deps.Deps) (*Namespace, error) {
 	}, nil
 }
 
+var _ resource.ResourceFinder = (*Namespace)(nil)
+
 // Namespace provides template functions for the "resources" namespace.
 type Namespace struct {
 	deps *deps.Deps
@@ -107,15 +110,19 @@ func (ns *Namespace) getscssClientDartSass() (*dartsass.Client, error) {
 	return ns.scssClientDartSass, err
 }
 
-// Get locates the filename given in Hugo's assets filesystem and
-// creates a Resource object that can be used for
-// further transformations.
-func (ns *Namespace) Get(filename any) (resource.Resource, error) {
+// Get locates the filename given in Hugo's assets filesystem
+// and creates a Resource object that can be used for further transformations.
+func (ns *Namespace) Get(filename any) resource.Resource {
 	filenamestr, err := cast.ToStringE(filename)
 	if err != nil {
-		return nil, err
+		panic(err)
+	}
+	r, err := ns.createClient.Get(filenamestr)
+	if err != nil {
+		panic(err)
 	}
-	return ns.createClient.Get(filepath.Clean(filenamestr))
+
+	return r
 }
 
 // GetRemote gets the URL (via HTTP(s)) in the first argument in args and creates Resource object that can be used for
@@ -168,13 +175,23 @@ func (ns *Namespace) GetRemote(args ...any) resource.Resource {
 // It looks for files in the assets file system.
 //
 // See Match for a more complete explanation about the rules used.
-func (ns *Namespace) GetMatch(pattern any) (resource.Resource, error) {
+func (ns *Namespace) GetMatch(pattern any) resource.Resource {
 	patternStr, err := cast.ToStringE(pattern)
 	if err != nil {
-		return nil, err
+		panic(err)
+	}
+
+	r, err := ns.createClient.GetMatch(patternStr)
+	if err != nil {
+		panic(err)
 	}
 
-	return ns.createClient.GetMatch(patternStr)
+	return r
+}
+
+// ByType returns resources of a given resource type (e.g. "image").
+func (ns *Namespace) ByType(typ any) resource.Resources {
+	return ns.createClient.ByType(cast.ToString(typ))
 }
 
 // Match gets all resources matching the given base path prefix, e.g
@@ -193,13 +210,19 @@ func (ns *Namespace) GetMatch(pattern any) (resource.Resource, error) {
 // It looks for files in the assets file system.
 //
 // See Match for a more complete explanation about the rules used.
-func (ns *Namespace) Match(pattern any) (resource.Resources, error) {
+func (ns *Namespace) Match(pattern any) resource.Resources {
+	defer herrors.Recover()
 	patternStr, err := cast.ToStringE(pattern)
 	if err != nil {
-		return nil, err
+		panic(err)
 	}
 
-	return ns.createClient.Match(patternStr)
+	r, err := ns.createClient.Match(patternStr)
+	if err != nil {
+		panic(err)
+	}
+
+	return r
 }
 
 // Concat concatenates a slice of Resource objects. These resources must