From e58a540895c28b8884823dcb1b64c272422f9923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 20 Oct 2021 10:11:48 +0200 Subject: [PATCH] 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 --- common/types/hstring/stringtypes_test.go | 2 +- hugolib/hugo_sites_build_test.go | 2 + hugolib/pagebundler_test.go | 3 +- hugolib/resource_chain_test.go | 11 ++- resources/resource/resources.go | 92 +++++++++++++++++-- resources/resource_factories/create/create.go | 25 +++-- tpl/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 index 8ff477f63..8fa1c9760 100644 --- 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 index cdd7b8054..d71e7c7a4 100644 --- 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 index d7a83d79d..cbad36520 100644 --- 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 index 471ea54e8..c7bf8a68a 100644 --- 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 index aaa747194..428decf84 100644 --- 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 { - return nil + panic(err) + } + + g, err := glob.GetGlob(patternstr) + if err != 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 index f8e7e18db..3827411a9 100644 --- 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 index 7e137c661..d3a98002f 100644 --- 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) } - return ns.createClient.Get(filepath.Clean(filenamestr)) + r, err := ns.createClient.Get(filenamestr) + if err != nil { + panic(err) + } + + 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) } - return ns.createClient.GetMatch(patternStr) + r, err := ns.createClient.GetMatch(patternStr) + if err != nil { + panic(err) + } + + 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