summaryrefslogtreecommitdiffstats
path: root/resource
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2018-01-15 20:40:39 +0100
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2018-01-17 16:22:33 +0100
commit20c9b6ec81171d1c586ea31d5d08b40b0edaffc6 (patch)
tree990d2709c1333663dce2ff97f16f8791fef3bac9 /resource
parentf8a119b606d55aa4f31f16e5a3cadc929c99e4f8 (diff)
resource: Add front matter metadata to Resource
This commit expands the Resource interface with 3 new methods: * Name * Title * Params All of these can be set in the Page front matter. `Name` will get its default value from the base filename, and is the value used in the ByPrefix and GetByPrefix lookup methods. Fixes #4244
Diffstat (limited to 'resource')
-rw-r--r--resource/image.go2
-rw-r--r--resource/image_cache.go18
-rw-r--r--resource/image_test.go22
-rw-r--r--resource/resource.go171
-rw-r--r--resource/resource_test.go192
5 files changed, 382 insertions, 23 deletions
diff --git a/resource/image.go b/resource/image.go
index e9a617f97..7ec65f3bc 100644
--- a/resource/image.go
+++ b/resource/image.go
@@ -208,7 +208,7 @@ func (i *Image) doWithImageConfig(action, spec string, f func(src image.Image, c
key := i.relTargetPathForRel(i.filenameFromConfig(conf), false)
- return i.spec.imageCache.getOrCreate(i.spec, key, func(resourceCacheFilename string) (*Image, error) {
+ return i.spec.imageCache.getOrCreate(i, key, func(resourceCacheFilename string) (*Image, error) {
ci := i.clone()
ci.setBasePath(conf)
diff --git a/resource/image_cache.go b/resource/image_cache.go
index c2d5d0ad5..5720fb623 100644
--- a/resource/image_cache.go
+++ b/resource/image_cache.go
@@ -15,7 +15,6 @@ package resource
import (
"fmt"
- "os"
"path/filepath"
"strings"
"sync"
@@ -50,7 +49,7 @@ func (c *imageCache) deleteByPrefix(prefix string) {
}
func (c *imageCache) getOrCreate(
- spec *Spec, key string, create func(resourceCacheFilename string) (*Image, error)) (*Image, error) {
+ parent *Image, key string, create func(resourceCacheFilename string) (*Image, error)) (*Image, error) {
relTargetFilename := key
@@ -77,19 +76,20 @@ func (c *imageCache) getOrCreate(
// but the count of processed image variations for this site.
c.pathSpec.ProcessingStats.Incr(&c.pathSpec.ProcessingStats.ProcessedImages)
- r, err := spec.NewResourceFromFilename(nil, c.absPublishDir, cacheFilename, relTargetFilename)
- notFound := err != nil && os.IsNotExist(err)
- if err != nil && !os.IsNotExist(err) {
+ exists, err := helpers.Exists(cacheFilename, c.pathSpec.Fs.Source)
+ if err != nil {
return nil, err
}
- if notFound {
+ if exists {
+ img = parent.clone()
+ img.relTargetPath = relTargetFilename
+ img.absSourceFilename = cacheFilename
+ } else {
img, err = create(cacheFilename)
if err != nil {
return nil, err
}
- } else {
- img = r.(*Image)
}
c.mu.Lock()
@@ -102,7 +102,7 @@ func (c *imageCache) getOrCreate(
c.mu.Unlock()
- if notFound {
+ if !exists {
// File already written to destination
return img, nil
}
diff --git a/resource/image_test.go b/resource/image_test.go
index 28f68a46c..bf097b319 100644
--- a/resource/image_test.go
+++ b/resource/image_test.go
@@ -147,3 +147,25 @@ func TestDecodeImaging(t *testing.T) {
assert.Equal(42, imaging.Quality)
assert.Equal("nearestneighbor", imaging.ResampleFilter)
}
+
+func TestImageWithMetadata(t *testing.T) {
+ assert := require.New(t)
+
+ image := fetchSunset(assert)
+
+ var meta = []map[string]interface{}{
+ map[string]interface{}{
+ "title": "My Sunset",
+ "name": "Sunset #:counter",
+ "src": "*.jpg",
+ },
+ }
+
+ assert.NoError(AssignMetadata(meta, image))
+ assert.Equal("Sunset #1", image.Name())
+
+ resized, err := image.Resize("200x")
+ assert.NoError(err)
+ assert.Equal("Sunset #1", resized.Name())
+
+}
diff --git a/resource/resource.go b/resource/resource.go
index bea53856e..951f1d9a7 100644
--- a/resource/resource.go
+++ b/resource/resource.go
@@ -19,8 +19,11 @@ import (
"os"
"path"
"path/filepath"
+ "strconv"
"strings"
+ "github.com/spf13/cast"
+
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/source"
@@ -28,9 +31,10 @@ import (
)
var (
- _ Resource = (*genericResource)(nil)
- _ Source = (*genericResource)(nil)
- _ Cloner = (*genericResource)(nil)
+ _ Resource = (*genericResource)(nil)
+ _ metaAssigner = (*genericResource)(nil)
+ _ Source = (*genericResource)(nil)
+ _ Cloner = (*genericResource)(nil)
)
const DefaultResourceType = "unknown"
@@ -48,11 +52,38 @@ type Cloner interface {
WithNewBase(base string) Resource
}
+type metaAssigner interface {
+ setTitle(title string)
+ setName(name string)
+ setParams(params map[string]interface{})
+}
+
// Resource represents a linkable resource, i.e. a content page, image etc.
type Resource interface {
+ // Permalink represents the absolute link to this resource.
Permalink() string
+
+ // RelPermalink represents the host relative link to this resource.
RelPermalink() string
+
+ // ResourceType is the resource type. For most file types, this is the main
+ // part of the MIME type, e.g. "image", "application", "text" etc.
+ // For content pages, this value is "page".
ResourceType() string
+
+ // Name is the logical name of this resource. This can be set in the front matter
+ // metadata for this resource. If not set, Hugo will assign a value.
+ // This will in most cases be the base filename.
+ // So, for the image "/some/path/sunset.jpg" this will be "sunset.jpg".
+ // The value returned by this method will be used in the GetByPrefix and ByPrefix methods
+ // on Resources.
+ Name() string
+
+ // Title returns the title if set in front matter. For content pages, this will be the expected value.
+ Title() string
+
+ // Params set in front matter for this resource.
+ Params() map[string]interface{}
}
// Resources represents a slice of resources, which can be a mix of different types.
@@ -97,16 +128,7 @@ func (r Resources) ByPrefix(prefix string) Resources {
}
func matchesPrefix(r Resource, prefix string) bool {
- var name string
- f, ok := r.(source.File)
- if ok {
- name = f.BaseFileName()
- } else {
- _, name = filepath.Split(r.RelPermalink())
- }
- name = strings.ToLower(name)
-
- return strings.HasPrefix(name, prefix)
+ return strings.HasPrefix(strings.ToLower(r.Name()), prefix)
}
type Spec struct {
@@ -238,6 +260,10 @@ type genericResource struct {
// Base is set when the output format's path has a offset, e.g. for AMP.
base string
+ title string
+ name string
+ params map[string]interface{}
+
// Absolute filename to the source, including any content folder path.
absSourceFilename string
absPublishDir string
@@ -256,6 +282,30 @@ func (l *genericResource) RelPermalink() string {
return l.relPermalinkForRel(l.relTargetPath, true)
}
+func (l *genericResource) Name() string {
+ return l.name
+}
+
+func (l *genericResource) Title() string {
+ return l.title
+}
+
+func (l *genericResource) Params() map[string]interface{} {
+ return l.params
+}
+
+func (l *genericResource) setTitle(title string) {
+ l.title = title
+}
+
+func (l *genericResource) setName(name string) {
+ l.name = name
+}
+
+func (l *genericResource) setParams(params map[string]interface{}) {
+ l.params = params
+}
+
// Implement the Cloner interface.
func (l genericResource) WithNewBase(base string) Resource {
l.base = base
@@ -306,6 +356,98 @@ func (l *genericResource) Publish() error {
return helpers.WriteToDisk(target, f, l.spec.Fs.Destination)
}
+// AssignMetadata assigns the given metadata to those resources that supports updates
+// and matching by wildcard given in `src` using `filepath.Match` with lower cased values.
+// This assignment is additive, but the most specific match needs to be first.
+// The `name` and `title` metadata field support shell-matched collection it got a match in.
+// See https://golang.org/pkg/path/filepath/#Match
+func AssignMetadata(metadata []map[string]interface{}, resources ...Resource) error {
+
+ counters := make(map[string]int)
+
+ for _, r := range resources {
+ if _, ok := r.(metaAssigner); !ok {
+ continue
+ }
+
+ var (
+ nameSet, titleSet, paramsSet bool
+ currentCounter = 0
+ resourceSrcKey = strings.ToLower(r.Name())
+ )
+
+ ma := r.(metaAssigner)
+ for _, meta := range metadata {
+ if nameSet && titleSet && paramsSet {
+ // No need to look further
+ break
+ }
+
+ src, found := meta["src"]
+ if !found {
+ return fmt.Errorf("missing 'src' in metadata for resource")
+ }
+
+ srcKey := strings.ToLower(cast.ToString(src))
+
+ match, err := filepath.Match(srcKey, resourceSrcKey)
+ if err != nil {
+ return fmt.Errorf("failed to match resource with metadata: %s", err)
+ }
+
+ if match {
+ if !nameSet {
+ name, found := meta["name"]
+ if found {
+ if currentCounter == 0 {
+ currentCounter = counters[srcKey] + 1
+ counters[srcKey] = currentCounter
+ }
+
+ ma.setName(replaceResourcePlaceholders(cast.ToString(name), currentCounter))
+ nameSet = true
+ }
+ }
+
+ if !titleSet {
+ title, found := meta["title"]
+ if found {
+ if currentCounter == 0 {
+ currentCounter = counters[srcKey] + 1
+ counters[srcKey] = currentCounter
+ }
+ ma.setTitle((replaceResourcePlaceholders(cast.ToString(title), currentCounter)))
+ titleSet = true
+ }
+ }
+
+ if !paramsSet {
+ params, found := meta["params"]
+ if found {
+ m := cast.ToStringMap(params)
+ // Needed for case insensitive fetching of params values
+ helpers.ToLowerMap(m)
+ ma.setParams(m)
+
+ if currentCounter == 0 {
+ currentCounter = counters[srcKey] + 1
+ counters[srcKey] = currentCounter
+ }
+
+ paramsSet = true
+ }
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+func replaceResourcePlaceholders(in string, counter int) string {
+ return strings.Replace(in, ":counter", strconv.Itoa(counter), -1)
+}
+
func (l *genericResource) target() string {
target := l.relTargetPathForRel(l.relTargetPath, false)
if l.spec.PathSpec.Languages.IsMultihost() {
@@ -330,5 +472,8 @@ func (r *Spec) newGenericResource(
relTargetPath: baseFilename,
resourceType: resourceType,
spec: r,
+ params: make(map[string]interface{}),
+ name: baseFilename,
+ title: baseFilename,
}
}
diff --git a/resource/resource_test.go b/resource/resource_test.go
index 73d98d62a..4670ef632 100644
--- a/resource/resource_test.go
+++ b/resource/resource_test.go
@@ -14,6 +14,7 @@
package resource
import (
+ "fmt"
"path"
"path/filepath"
"testing"
@@ -129,4 +130,195 @@ func TestResourcesGetByPrefix(t *testing.T) {
assert.Equal(2, len(resources.ByPrefix("logo")))
assert.Equal(1, len(resources.ByPrefix("logo2")))
+ logo := resources.GetByPrefix("logo")
+ assert.NotNil(logo.Params())
+ assert.Equal("logo1.png", logo.Name())
+ assert.Equal("logo1.png", logo.Title())
+
+}
+
+func TestAssignMetadata(t *testing.T) {
+ assert := require.New(t)
+ spec := newTestResourceSpec(assert)
+
+ var foo1, foo2, foo3, logo1, logo2, logo3 Resource
+ var resources Resources
+
+ for _, this := range []struct {
+ metaData []map[string]interface{}
+ assertFunc func(err error)
+ }{
+ {[]map[string]interface{}{
+ map[string]interface{}{
+ "title": "My Resource",
+ "name": "My Name",
+ "src": "*",
+ },
+ }, func(err error) {
+ assert.Equal("My Resource", logo1.Title())
+ assert.Equal("My Name", logo1.Name())
+ assert.Equal("My Name", foo2.Name())
+
+ }},
+ {[]map[string]interface{}{
+ map[string]interface{}{
+ "title": "My Logo",
+ "src": "*loGo*",
+ },
+ map[string]interface{}{
+ "title": "My Resource",
+ "name": "My Name",
+ "src": "*",
+ },
+ }, func(err error) {
+ assert.Equal("My Logo", logo1.Title())
+ assert.Equal("My Logo", logo2.Title())
+ assert.Equal("My Name", logo1.Name())
+ assert.Equal("My Name", foo2.Name())
+ assert.Equal("My Name", foo3.Name())
+ assert.Equal("My Resource", foo3.Title())
+
+ }},
+ {[]map[string]interface{}{
+ map[string]interface{}{
+ "title": "My Logo",
+ "src": "*loGo*",
+ "params": map[string]interface{}{
+ "Param1": true,
+ },
+ },
+ map[string]interface{}{
+ "title": "My Resource",
+ "src": "*",
+ "params": map[string]interface{}{
+ "Param2": true,
+ },
+ },
+ }, func(err error) {
+ assert.NoError(err)
+ assert.Equal("My Logo", logo1.Title())
+ assert.Equal("My Resource", foo3.Title())
+ _, p1 := logo2.Params()["param1"]
+ _, p2 := foo2.Params()["param2"]
+ assert.True(p1)
+ assert.True(p2)
+
+ }},
+ {[]map[string]interface{}{
+ map[string]interface{}{
+ "name": "Logo Name #:counter",
+ "src": "*logo*",
+ },
+ map[string]interface{}{
+ "title": "Resource #:counter",
+ "name": "Name #:counter",
+ "src": "*",
+ },
+ }, func(err error) {
+ assert.NoError(err)
+ assert.Equal("Resource #1", logo2.Title())
+ assert.Equal("Logo Name #1", logo2.Name())
+ assert.Equal("Resource #2", logo1.Title())
+ assert.Equal("Logo Name #2", logo1.Name())
+ assert.Equal("Resource #1", foo2.Title())
+ assert.Equal("Resource #2", foo1.Title())
+ assert.Equal("Name #2", foo1.Name())
+ assert.Equal("Resource #3", foo3.Title())
+
+ assert.Equal(logo2, resources.GetByPrefix("logo name #1"))
+
+ }},
+ {[]map[string]interface{}{
+ map[string]interface{}{
+ "title": "Third Logo #:counter",
+ "src": "logo3.png",
+ },
+ map[string]interface{}{
+ "title": "Other Logo #:counter",
+ "name": "Name #:counter",
+ "src": "logo*",
+ },
+ }, func(err error) {
+ assert.NoError(err)
+ assert.Equal("Third Logo #1", logo3.Title())
+ assert.Equal("Name #1", logo3.Name())
+ assert.Equal("Other Logo #1", logo2.Title())
+ assert.Equal("Name #1", logo2.Name())
+ assert.Equal("Other Logo #2", logo1.Title())
+ assert.Equal("Name #2", logo1.Name())
+
+ }},
+ {[]map[string]interface{}{
+ map[string]interface{}{
+ "title": "Third Logo #:counter",
+ },
+ }, func(err error) {
+ // Missing src
+ assert.Error(err)
+
+ }},
+ {[]map[string]interface{}{
+ map[string]interface{}{
+ "title": "Title",
+ "src": "[]",
+ },
+ }, func(err error) {
+ // Invalid pattern
+ assert.Error(err)
+
+ }},
+ } {
+
+ foo2 = spec.newGenericResource(nil, nil, "/public", "/b/foo2.css", "foo2.css", "css")
+ logo2 = spec.newGenericResource(nil, nil, "/public", "/b/Logo2.png", "Logo2.png", "image")
+ foo1 = spec.newGenericResource(nil, nil, "/public", "/a/foo1.css", "foo1.css", "css")
+ logo1 = spec.newGenericResource(nil, nil, "/public", "/a/logo1.png", "logo1.png", "image")
+ foo3 = spec.newGenericResource(nil, nil, "/public", "/b/foo3.css", "foo3.css", "css")
+ logo3 = spec.newGenericResource(nil, nil, "/public", "/b/logo3.png", "logo3.png", "image")
+
+ resources = Resources{
+ foo2,
+ logo2,
+ foo1,
+ logo1,
+ foo3,
+ logo3,
+ }
+
+ this.assertFunc(AssignMetadata(this.metaData, resources...))
+ }
+
+}
+
+func BenchmarkAssignMetadata(b *testing.B) {
+ assert := require.New(b)
+ spec := newTestResourceSpec(assert)
+
+ for i := 0; i < b.N; i++ {
+ b.StopTimer()
+ var resources Resources
+ var meta = []map[string]interface{}{
+ map[string]interface{}{
+ "title": "Foo #:counter",
+ "name": "Foo Name #:counter",
+ "src": "foo1*",
+ },
+ map[string]interface{}{
+ "title": "Rest #:counter",
+ "name": "Rest Name #:counter",
+ "src": "*",
+ },
+ }
+ for i := 0; i < 20; i++ {
+ name := fmt.Sprintf("foo%d_%d.css", i%5, i)
+ resources = append(resources, spec.newGenericResource(nil, nil, "/public", "/a/"+name, name, "css"))
+ }
+ b.StartTimer()
+
+ if err := AssignMetadata(meta, resources...); err != nil {
+ b.Fatal(err)
+ }
+
+ }
+
}