Add a newScratch template func
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 6 Jul 2018 12:12:10 +0000 (14:12 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 6 Jul 2018 15:51:38 +0000 (17:51 +0200)
Fixes #4685

16 files changed:
common/maps/scratch.go [new file with mode: 0644]
common/maps/scratch_test.go [new file with mode: 0644]
common/math/math.go [new file with mode: 0644]
common/math/math_test.go [new file with mode: 0644]
docs/content/en/functions/scratch.md
hugolib/page.go
hugolib/page_test.go
hugolib/scratch.go [deleted file]
hugolib/scratch_test.go [deleted file]
hugolib/shortcode.go
hugolib/site.go
tpl/collections/collections.go
tpl/collections/init.go
tpl/math/math.go
tpl/math/math_test.go
tpl/tplimpl/template_funcs_test.go

diff --git a/common/maps/scratch.go b/common/maps/scratch.go
new file mode 100644 (file)
index 0000000..d009714
--- /dev/null
@@ -0,0 +1,135 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package maps
+
+import (
+       "reflect"
+       "sort"
+       "sync"
+
+       "github.com/gohugoio/hugo/common/math"
+)
+
+// Scratch is a writable context used for stateful operations in Page/Node rendering.
+type Scratch struct {
+       values map[string]interface{}
+       mu     sync.RWMutex
+}
+
+// Add will, for single values, add (using the + operator) the addend to the existing addend (if found).
+// Supports numeric values and strings.
+//
+// If the first add for a key is an array or slice, then the next value(s) will be appended.
+func (c *Scratch) Add(key string, newAddend interface{}) (string, error) {
+
+       var newVal interface{}
+       c.mu.RLock()
+       existingAddend, found := c.values[key]
+       c.mu.RUnlock()
+       if found {
+               var err error
+
+               addendV := reflect.ValueOf(existingAddend)
+
+               if addendV.Kind() == reflect.Slice || addendV.Kind() == reflect.Array {
+                       nav := reflect.ValueOf(newAddend)
+                       if nav.Kind() == reflect.Slice || nav.Kind() == reflect.Array {
+                               newVal = reflect.AppendSlice(addendV, nav).Interface()
+                       } else {
+                               newVal = reflect.Append(addendV, nav).Interface()
+                       }
+               } else {
+                       newVal, err = math.DoArithmetic(existingAddend, newAddend, '+')
+                       if err != nil {
+                               return "", err
+                       }
+               }
+       } else {
+               newVal = newAddend
+       }
+       c.mu.Lock()
+       c.values[key] = newVal
+       c.mu.Unlock()
+       return "", nil // have to return something to make it work with the Go templates
+}
+
+// Set stores a value with the given key in the Node context.
+// This value can later be retrieved with Get.
+func (c *Scratch) Set(key string, value interface{}) string {
+       c.mu.Lock()
+       c.values[key] = value
+       c.mu.Unlock()
+       return ""
+}
+
+// Reset deletes the given key
+func (c *Scratch) Delete(key string) string {
+       c.mu.Lock()
+       delete(c.values, key)
+       c.mu.Unlock()
+       return ""
+}
+
+// Get returns a value previously set by Add or Set
+func (c *Scratch) Get(key string) interface{} {
+       c.mu.RLock()
+       val := c.values[key]
+       c.mu.RUnlock()
+
+       return val
+}
+
+// SetInMap stores a value to a map with the given key in the Node context.
+// This map can later be retrieved with GetSortedMapValues.
+func (c *Scratch) SetInMap(key string, mapKey string, value interface{}) string {
+       c.mu.Lock()
+       _, found := c.values[key]
+       if !found {
+               c.values[key] = make(map[string]interface{})
+       }
+
+       c.values[key].(map[string]interface{})[mapKey] = value
+       c.mu.Unlock()
+       return ""
+}
+
+// GetSortedMapValues returns a sorted map previously filled with SetInMap
+func (c *Scratch) GetSortedMapValues(key string) interface{} {
+       c.mu.RLock()
+
+       if c.values[key] == nil {
+               c.mu.RUnlock()
+               return nil
+       }
+
+       unsortedMap := c.values[key].(map[string]interface{})
+       c.mu.RUnlock()
+       var keys []string
+       for mapKey := range unsortedMap {
+               keys = append(keys, mapKey)
+       }
+
+       sort.Strings(keys)
+
+       sortedArray := make([]interface{}, len(unsortedMap))
+       for i, mapKey := range keys {
+               sortedArray[i] = unsortedMap[mapKey]
+       }
+
+       return sortedArray
+}
+
+func NewScratch() *Scratch {
+       return &Scratch{values: make(map[string]interface{})}
+}
diff --git a/common/maps/scratch_test.go b/common/maps/scratch_test.go
new file mode 100644 (file)
index 0000000..8397ba8
--- /dev/null
@@ -0,0 +1,170 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package maps
+
+import (
+       "reflect"
+       "sync"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestScratchAdd(t *testing.T) {
+       t.Parallel()
+       scratch := NewScratch()
+       scratch.Add("int1", 10)
+       scratch.Add("int1", 20)
+       scratch.Add("int2", 20)
+
+       assert.Equal(t, int64(30), scratch.Get("int1"))
+       assert.Equal(t, 20, scratch.Get("int2"))
+
+       scratch.Add("float1", float64(10.5))
+       scratch.Add("float1", float64(20.1))
+
+       assert.Equal(t, float64(30.6), scratch.Get("float1"))
+
+       scratch.Add("string1", "Hello ")
+       scratch.Add("string1", "big ")
+       scratch.Add("string1", "World!")
+
+       assert.Equal(t, "Hello big World!", scratch.Get("string1"))
+
+       scratch.Add("scratch", scratch)
+       _, err := scratch.Add("scratch", scratch)
+
+       if err == nil {
+               t.Errorf("Expected error from invalid arithmetic")
+       }
+
+}
+
+func TestScratchAddSlice(t *testing.T) {
+       t.Parallel()
+       scratch := NewScratch()
+
+       _, err := scratch.Add("intSlice", []int{1, 2})
+       assert.Nil(t, err)
+       _, err = scratch.Add("intSlice", 3)
+       assert.Nil(t, err)
+
+       sl := scratch.Get("intSlice")
+       expected := []int{1, 2, 3}
+
+       if !reflect.DeepEqual(expected, sl) {
+               t.Errorf("Slice difference, go %q expected %q", sl, expected)
+       }
+
+       _, err = scratch.Add("intSlice", []int{4, 5})
+
+       assert.Nil(t, err)
+
+       sl = scratch.Get("intSlice")
+       expected = []int{1, 2, 3, 4, 5}
+
+       if !reflect.DeepEqual(expected, sl) {
+               t.Errorf("Slice difference, go %q expected %q", sl, expected)
+       }
+
+}
+
+func TestScratchSet(t *testing.T) {
+       t.Parallel()
+       scratch := NewScratch()
+       scratch.Set("key", "val")
+       assert.Equal(t, "val", scratch.Get("key"))
+}
+
+func TestScratchDelete(t *testing.T) {
+       t.Parallel()
+       scratch := NewScratch()
+       scratch.Set("key", "val")
+       scratch.Delete("key")
+       scratch.Add("key", "Lucy Parsons")
+       assert.Equal(t, "Lucy Parsons", scratch.Get("key"))
+}
+
+// Issue #2005
+func TestScratchInParallel(t *testing.T) {
+       var wg sync.WaitGroup
+       scratch := NewScratch()
+       key := "counter"
+       scratch.Set(key, int64(1))
+       for i := 1; i <= 10; i++ {
+               wg.Add(1)
+               go func(j int) {
+                       for k := 0; k < 10; k++ {
+                               newVal := int64(k + j)
+
+                               _, err := scratch.Add(key, newVal)
+                               if err != nil {
+                                       t.Errorf("Got err %s", err)
+                               }
+
+                               scratch.Set(key, newVal)
+
+                               val := scratch.Get(key)
+
+                               if counter, ok := val.(int64); ok {
+                                       if counter < 1 {
+                                               t.Errorf("Got %d", counter)
+                                       }
+                               } else {
+                                       t.Errorf("Got %T", val)
+                               }
+                       }
+                       wg.Done()
+               }(i)
+       }
+       wg.Wait()
+}
+
+func TestScratchGet(t *testing.T) {
+       t.Parallel()
+       scratch := NewScratch()
+       nothing := scratch.Get("nothing")
+       if nothing != nil {
+               t.Errorf("Should not return anything, but got %v", nothing)
+       }
+}
+
+func TestScratchSetInMap(t *testing.T) {
+       t.Parallel()
+       scratch := NewScratch()
+       scratch.SetInMap("key", "lux", "Lux")
+       scratch.SetInMap("key", "abc", "Abc")
+       scratch.SetInMap("key", "zyx", "Zyx")
+       scratch.SetInMap("key", "abc", "Abc (updated)")
+       scratch.SetInMap("key", "def", "Def")
+       assert.Equal(t, []interface{}{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"}, scratch.GetSortedMapValues("key"))
+}
+
+func TestScratchGetSortedMapValues(t *testing.T) {
+       t.Parallel()
+       scratch := NewScratch()
+       nothing := scratch.GetSortedMapValues("nothing")
+       if nothing != nil {
+               t.Errorf("Should not return anything, but got %v", nothing)
+       }
+}
+
+func BenchmarkScratchGet(b *testing.B) {
+       scratch := NewScratch()
+       scratch.Add("A", 1)
+       b.ResetTimer()
+       for i := 0; i < b.N; i++ {
+               scratch.Get("A")
+       }
+}
diff --git a/common/math/math.go b/common/math/math.go
new file mode 100644 (file)
index 0000000..3c5ef1f
--- /dev/null
@@ -0,0 +1,135 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package math
+
+import (
+       "errors"
+       "reflect"
+)
+
+// DoArithmetic performs arithmetic operations (+,-,*,/) using reflection to
+// determine the type of the two terms.
+func DoArithmetic(a, b interface{}, op rune) (interface{}, error) {
+       av := reflect.ValueOf(a)
+       bv := reflect.ValueOf(b)
+       var ai, bi int64
+       var af, bf float64
+       var au, bu uint64
+       switch av.Kind() {
+       case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+               ai = av.Int()
+               switch bv.Kind() {
+               case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+                       bi = bv.Int()
+               case reflect.Float32, reflect.Float64:
+                       af = float64(ai) // may overflow
+                       ai = 0
+                       bf = bv.Float()
+               case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+                       bu = bv.Uint()
+                       if ai >= 0 {
+                               au = uint64(ai)
+                               ai = 0
+                       } else {
+                               bi = int64(bu) // may overflow
+                               bu = 0
+                       }
+               default:
+                       return nil, errors.New("Can't apply the operator to the values")
+               }
+       case reflect.Float32, reflect.Float64:
+               af = av.Float()
+               switch bv.Kind() {
+               case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+                       bf = float64(bv.Int()) // may overflow
+               case reflect.Float32, reflect.Float64:
+                       bf = bv.Float()
+               case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+                       bf = float64(bv.Uint()) // may overflow
+               default:
+                       return nil, errors.New("Can't apply the operator to the values")
+               }
+       case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+               au = av.Uint()
+               switch bv.Kind() {
+               case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+                       bi = bv.Int()
+                       if bi >= 0 {
+                               bu = uint64(bi)
+                               bi = 0
+                       } else {
+                               ai = int64(au) // may overflow
+                               au = 0
+                       }
+               case reflect.Float32, reflect.Float64:
+                       af = float64(au) // may overflow
+                       au = 0
+                       bf = bv.Float()
+               case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+                       bu = bv.Uint()
+               default:
+                       return nil, errors.New("Can't apply the operator to the values")
+               }
+       case reflect.String:
+               as := av.String()
+               if bv.Kind() == reflect.String && op == '+' {
+                       bs := bv.String()
+                       return as + bs, nil
+               }
+               return nil, errors.New("Can't apply the operator to the values")
+       default:
+               return nil, errors.New("Can't apply the operator to the values")
+       }
+
+       switch op {
+       case '+':
+               if ai != 0 || bi != 0 {
+                       return ai + bi, nil
+               } else if af != 0 || bf != 0 {
+                       return af + bf, nil
+               } else if au != 0 || bu != 0 {
+                       return au + bu, nil
+               }
+               return 0, nil
+       case '-':
+               if ai != 0 || bi != 0 {
+                       return ai - bi, nil
+               } else if af != 0 || bf != 0 {
+                       return af - bf, nil
+               } else if au != 0 || bu != 0 {
+                       return au - bu, nil
+               }
+               return 0, nil
+       case '*':
+               if ai != 0 || bi != 0 {
+                       return ai * bi, nil
+               } else if af != 0 || bf != 0 {
+                       return af * bf, nil
+               } else if au != 0 || bu != 0 {
+                       return au * bu, nil
+               }
+               return 0, nil
+       case '/':
+               if bi != 0 {
+                       return ai / bi, nil
+               } else if bf != 0 {
+                       return af / bf, nil
+               } else if bu != 0 {
+                       return au / bu, nil
+               }
+               return nil, errors.New("Can't divide the value by 0")
+       default:
+               return nil, errors.New("There is no such an operation")
+       }
+}
diff --git a/common/math/math_test.go b/common/math/math_test.go
new file mode 100644 (file)
index 0000000..613ac30
--- /dev/null
@@ -0,0 +1,109 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package math
+
+import (
+       "fmt"
+       "testing"
+
+       "github.com/alecthomas/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func TestDoArithmetic(t *testing.T) {
+       t.Parallel()
+
+       for i, test := range []struct {
+               a      interface{}
+               b      interface{}
+               op     rune
+               expect interface{}
+       }{
+               {3, 2, '+', int64(5)},
+               {3, 2, '-', int64(1)},
+               {3, 2, '*', int64(6)},
+               {3, 2, '/', int64(1)},
+               {3.0, 2, '+', float64(5)},
+               {3.0, 2, '-', float64(1)},
+               {3.0, 2, '*', float64(6)},
+               {3.0, 2, '/', float64(1.5)},
+               {3, 2.0, '+', float64(5)},
+               {3, 2.0, '-', float64(1)},
+               {3, 2.0, '*', float64(6)},
+               {3, 2.0, '/', float64(1.5)},
+               {3.0, 2.0, '+', float64(5)},
+               {3.0, 2.0, '-', float64(1)},
+               {3.0, 2.0, '*', float64(6)},
+               {3.0, 2.0, '/', float64(1.5)},
+               {uint(3), uint(2), '+', uint64(5)},
+               {uint(3), uint(2), '-', uint64(1)},
+               {uint(3), uint(2), '*', uint64(6)},
+               {uint(3), uint(2), '/', uint64(1)},
+               {uint(3), 2, '+', uint64(5)},
+               {uint(3), 2, '-', uint64(1)},
+               {uint(3), 2, '*', uint64(6)},
+               {uint(3), 2, '/', uint64(1)},
+               {3, uint(2), '+', uint64(5)},
+               {3, uint(2), '-', uint64(1)},
+               {3, uint(2), '*', uint64(6)},
+               {3, uint(2), '/', uint64(1)},
+               {uint(3), -2, '+', int64(1)},
+               {uint(3), -2, '-', int64(5)},
+               {uint(3), -2, '*', int64(-6)},
+               {uint(3), -2, '/', int64(-1)},
+               {-3, uint(2), '+', int64(-1)},
+               {-3, uint(2), '-', int64(-5)},
+               {-3, uint(2), '*', int64(-6)},
+               {-3, uint(2), '/', int64(-1)},
+               {uint(3), 2.0, '+', float64(5)},
+               {uint(3), 2.0, '-', float64(1)},
+               {uint(3), 2.0, '*', float64(6)},
+               {uint(3), 2.0, '/', float64(1.5)},
+               {3.0, uint(2), '+', float64(5)},
+               {3.0, uint(2), '-', float64(1)},
+               {3.0, uint(2), '*', float64(6)},
+               {3.0, uint(2), '/', float64(1.5)},
+               {0, 0, '+', 0},
+               {0, 0, '-', 0},
+               {0, 0, '*', 0},
+               {"foo", "bar", '+', "foobar"},
+               {3, 0, '/', false},
+               {3.0, 0, '/', false},
+               {3, 0.0, '/', false},
+               {uint(3), uint(0), '/', false},
+               {3, uint(0), '/', false},
+               {-3, uint(0), '/', false},
+               {uint(3), 0, '/', false},
+               {3.0, uint(0), '/', false},
+               {uint(3), 0.0, '/', false},
+               {3, "foo", '+', false},
+               {3.0, "foo", '+', false},
+               {uint(3), "foo", '+', false},
+               {"foo", 3, '+', false},
+               {"foo", "bar", '-', false},
+               {3, 2, '%', false},
+       } {
+               errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+               result, err := DoArithmetic(test.a, test.b, test.op)
+
+               if b, ok := test.expect.(bool); ok && !b {
+                       require.Error(t, err, errMsg)
+                       continue
+               }
+
+               require.NoError(t, err, errMsg)
+               assert.Equal(t, test.expect, result, errMsg)
+       }
+}
index 9df78fe43d29305632fe7d9d5e903fd903cbf782..93a1e426a06dd12b804cc9bb93a35140fd80e856 100644 (file)
@@ -22,6 +22,9 @@ aliases: [/extras/scratch/,/doc/scratch/]
 
 In most cases you can do okay without `Scratch`, but due to scoping issues, there are many use cases that aren't solvable in Go Templates without `Scratch`'s help.
 
+`.Scratch` is available as methods on `Page` and `Shortcode`. Since Hugo 0.43 you can also create a locally scoped `Scratch` using the template func `newScratch`.
+
+
 {{% note %}}
 See [this Go issue](https://github.com/golang/go/issues/10608) for the main motivation behind Scratch.
 {{% /note %}}
index d9a3fe31c6cab9ff982942eeb7ea8ef14eeea8c6..6e21d75aea60d23ae9c31250b60fceab3865fcf2 100644 (file)
@@ -257,7 +257,7 @@ type Page struct {
 
        layoutDescriptor output.LayoutDescriptor
 
-       scratch *Scratch
+       scratch *maps.Scratch
 
        // It would be tempting to use the language set on the Site, but in they way we do
        // multi-site processing, these values may differ during the initial page processing.
@@ -2052,9 +2052,9 @@ func (p *Page) String() string {
 }
 
 // Scratch returns the writable context associated with this Page.
-func (p *Page) Scratch() *Scratch {
+func (p *Page) Scratch() *maps.Scratch {
        if p.scratch == nil {
-               p.scratch = newScratch()
+               p.scratch = maps.NewScratch()
        }
        return p.scratch
 }
index 985373a90a58f906502add36243e1092792410c3..b512a9a5aba683f33c8d80e40592e1674c79ad78 100644 (file)
@@ -1830,6 +1830,33 @@ Summary: In Chinese, 好 means good.
 
 }
 
+func TestScratchSite(t *testing.T) {
+       t.Parallel()
+
+       b := newTestSitesBuilder(t)
+       b.WithSimpleConfigFile().WithTemplatesAdded("index.html", `
+{{ .Scratch.Set "b" "bv" }}
+B: {{ .Scratch.Get "b" }}
+`,
+               "shortcodes/scratch.html", `
+{{ .Scratch.Set "c" "cv" }}
+C: {{ .Scratch.Get "c" }}
+`,
+       )
+
+       b.WithContentAdded("scratchme.md", `
+---
+title: Scratch Me!
+---
+
+{{< scratch >}}
+`)
+       b.Build(BuildCfg{})
+
+       b.AssertFileContent("public/index.html", "B: bv")
+       b.AssertFileContent("public/scratchme/index.html", "C: cv")
+}
+
 func BenchmarkParsePage(b *testing.B) {
        s := newTestSite(b)
        f, _ := os.Open("testdata/redis.cn.md")
diff --git a/hugolib/scratch.go b/hugolib/scratch.go
deleted file mode 100644 (file)
index 37ed5df..0000000
+++ /dev/null
@@ -1,135 +0,0 @@
-// Copyright 2015 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package hugolib
-
-import (
-       "reflect"
-       "sort"
-       "sync"
-
-       "github.com/gohugoio/hugo/tpl/math"
-)
-
-// Scratch is a writable context used for stateful operations in Page/Node rendering.
-type Scratch struct {
-       values map[string]interface{}
-       mu     sync.RWMutex
-}
-
-// Add will, for single values, add (using the + operator) the addend to the existing addend (if found).
-// Supports numeric values and strings.
-//
-// If the first add for a key is an array or slice, then the next value(s) will be appended.
-func (c *Scratch) Add(key string, newAddend interface{}) (string, error) {
-
-       var newVal interface{}
-       c.mu.RLock()
-       existingAddend, found := c.values[key]
-       c.mu.RUnlock()
-       if found {
-               var err error
-
-               addendV := reflect.ValueOf(existingAddend)
-
-               if addendV.Kind() == reflect.Slice || addendV.Kind() == reflect.Array {
-                       nav := reflect.ValueOf(newAddend)
-                       if nav.Kind() == reflect.Slice || nav.Kind() == reflect.Array {
-                               newVal = reflect.AppendSlice(addendV, nav).Interface()
-                       } else {
-                               newVal = reflect.Append(addendV, nav).Interface()
-                       }
-               } else {
-                       newVal, err = math.DoArithmetic(existingAddend, newAddend, '+')
-                       if err != nil {
-                               return "", err
-                       }
-               }
-       } else {
-               newVal = newAddend
-       }
-       c.mu.Lock()
-       c.values[key] = newVal
-       c.mu.Unlock()
-       return "", nil // have to return something to make it work with the Go templates
-}
-
-// Set stores a value with the given key in the Node context.
-// This value can later be retrieved with Get.
-func (c *Scratch) Set(key string, value interface{}) string {
-       c.mu.Lock()
-       c.values[key] = value
-       c.mu.Unlock()
-       return ""
-}
-
-// Reset deletes the given key
-func (c *Scratch) Delete(key string) string {
-       c.mu.Lock()
-       delete(c.values, key)
-       c.mu.Unlock()
-       return ""
-}
-
-// Get returns a value previously set by Add or Set
-func (c *Scratch) Get(key string) interface{} {
-       c.mu.RLock()
-       val := c.values[key]
-       c.mu.RUnlock()
-
-       return val
-}
-
-// SetInMap stores a value to a map with the given key in the Node context.
-// This map can later be retrieved with GetSortedMapValues.
-func (c *Scratch) SetInMap(key string, mapKey string, value interface{}) string {
-       c.mu.Lock()
-       _, found := c.values[key]
-       if !found {
-               c.values[key] = make(map[string]interface{})
-       }
-
-       c.values[key].(map[string]interface{})[mapKey] = value
-       c.mu.Unlock()
-       return ""
-}
-
-// GetSortedMapValues returns a sorted map previously filled with SetInMap
-func (c *Scratch) GetSortedMapValues(key string) interface{} {
-       c.mu.RLock()
-
-       if c.values[key] == nil {
-               c.mu.RUnlock()
-               return nil
-       }
-
-       unsortedMap := c.values[key].(map[string]interface{})
-       c.mu.RUnlock()
-       var keys []string
-       for mapKey := range unsortedMap {
-               keys = append(keys, mapKey)
-       }
-
-       sort.Strings(keys)
-
-       sortedArray := make([]interface{}, len(unsortedMap))
-       for i, mapKey := range keys {
-               sortedArray[i] = unsortedMap[mapKey]
-       }
-
-       return sortedArray
-}
-
-func newScratch() *Scratch {
-       return &Scratch{values: make(map[string]interface{})}
-}
diff --git a/hugolib/scratch_test.go b/hugolib/scratch_test.go
deleted file mode 100644 (file)
index 5ec2b89..0000000
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright 2015 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package hugolib
-
-import (
-       "reflect"
-       "sync"
-       "testing"
-
-       "github.com/stretchr/testify/assert"
-)
-
-func TestScratchAdd(t *testing.T) {
-       t.Parallel()
-       scratch := newScratch()
-       scratch.Add("int1", 10)
-       scratch.Add("int1", 20)
-       scratch.Add("int2", 20)
-
-       assert.Equal(t, int64(30), scratch.Get("int1"))
-       assert.Equal(t, 20, scratch.Get("int2"))
-
-       scratch.Add("float1", float64(10.5))
-       scratch.Add("float1", float64(20.1))
-
-       assert.Equal(t, float64(30.6), scratch.Get("float1"))
-
-       scratch.Add("string1", "Hello ")
-       scratch.Add("string1", "big ")
-       scratch.Add("string1", "World!")
-
-       assert.Equal(t, "Hello big World!", scratch.Get("string1"))
-
-       scratch.Add("scratch", scratch)
-       _, err := scratch.Add("scratch", scratch)
-
-       if err == nil {
-               t.Errorf("Expected error from invalid arithmetic")
-       }
-
-}
-
-func TestScratchAddSlice(t *testing.T) {
-       t.Parallel()
-       scratch := newScratch()
-
-       _, err := scratch.Add("intSlice", []int{1, 2})
-       assert.Nil(t, err)
-       _, err = scratch.Add("intSlice", 3)
-       assert.Nil(t, err)
-
-       sl := scratch.Get("intSlice")
-       expected := []int{1, 2, 3}
-
-       if !reflect.DeepEqual(expected, sl) {
-               t.Errorf("Slice difference, go %q expected %q", sl, expected)
-       }
-
-       _, err = scratch.Add("intSlice", []int{4, 5})
-
-       assert.Nil(t, err)
-
-       sl = scratch.Get("intSlice")
-       expected = []int{1, 2, 3, 4, 5}
-
-       if !reflect.DeepEqual(expected, sl) {
-               t.Errorf("Slice difference, go %q expected %q", sl, expected)
-       }
-
-}
-
-func TestScratchSet(t *testing.T) {
-       t.Parallel()
-       scratch := newScratch()
-       scratch.Set("key", "val")
-       assert.Equal(t, "val", scratch.Get("key"))
-}
-
-func TestScratchDelete(t *testing.T) {
-       t.Parallel()
-       scratch := newScratch()
-       scratch.Set("key", "val")
-       scratch.Delete("key")
-       scratch.Add("key", "Lucy Parsons")
-       assert.Equal(t, "Lucy Parsons", scratch.Get("key"))
-}
-
-// Issue #2005
-func TestScratchInParallel(t *testing.T) {
-       var wg sync.WaitGroup
-       scratch := newScratch()
-       key := "counter"
-       scratch.Set(key, int64(1))
-       for i := 1; i <= 10; i++ {
-               wg.Add(1)
-               go func(j int) {
-                       for k := 0; k < 10; k++ {
-                               newVal := int64(k + j)
-
-                               _, err := scratch.Add(key, newVal)
-                               if err != nil {
-                                       t.Errorf("Got err %s", err)
-                               }
-
-                               scratch.Set(key, newVal)
-
-                               val := scratch.Get(key)
-
-                               if counter, ok := val.(int64); ok {
-                                       if counter < 1 {
-                                               t.Errorf("Got %d", counter)
-                                       }
-                               } else {
-                                       t.Errorf("Got %T", val)
-                               }
-                       }
-                       wg.Done()
-               }(i)
-       }
-       wg.Wait()
-}
-
-func TestScratchGet(t *testing.T) {
-       t.Parallel()
-       scratch := newScratch()
-       nothing := scratch.Get("nothing")
-       if nothing != nil {
-               t.Errorf("Should not return anything, but got %v", nothing)
-       }
-}
-
-func TestScratchSetInMap(t *testing.T) {
-       t.Parallel()
-       scratch := newScratch()
-       scratch.SetInMap("key", "lux", "Lux")
-       scratch.SetInMap("key", "abc", "Abc")
-       scratch.SetInMap("key", "zyx", "Zyx")
-       scratch.SetInMap("key", "abc", "Abc (updated)")
-       scratch.SetInMap("key", "def", "Def")
-       assert.Equal(t, []interface{}{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"}, scratch.GetSortedMapValues("key"))
-}
-
-func TestScratchGetSortedMapValues(t *testing.T) {
-       t.Parallel()
-       scratch := newScratch()
-       nothing := scratch.GetSortedMapValues("nothing")
-       if nothing != nil {
-               t.Errorf("Should not return anything, but got %v", nothing)
-       }
-}
-
-func BenchmarkScratchGet(b *testing.B) {
-       scratch := newScratch()
-       scratch.Add("A", 1)
-       b.ResetTimer()
-       for i := 0; i < b.N; i++ {
-               scratch.Get("A")
-       }
-}
index c07a5586a8312ddf097ea2849b88f969ad3827d1..cf5b0ece061ace61b23a711a7d15a7d9b6178caa 100644 (file)
@@ -24,6 +24,7 @@ import (
        "strings"
        "sync"
 
+       "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/output"
 
        "github.com/gohugoio/hugo/media"
@@ -45,7 +46,7 @@ type ShortcodeWithPage struct {
        // this ordinal will represent the position of this shortcode in the page content.
        Ordinal int
 
-       scratch *Scratch
+       scratch *maps.Scratch
 }
 
 // Site returns information about the current site.
@@ -65,9 +66,9 @@ func (scp *ShortcodeWithPage) RelRef(ref string) (string, error) {
 
 // Scratch returns a scratch-pad scoped for this shortcode. This can be used
 // as a temporary storage for variables, counters etc.
-func (scp *ShortcodeWithPage) Scratch() *Scratch {
+func (scp *ShortcodeWithPage) Scratch() *maps.Scratch {
        if scp.scratch == nil {
-               scp.scratch = newScratch()
+               scp.scratch = maps.NewScratch()
        }
        return scp.scratch
 }
index df7e66d4a33c09c1504a250b2114ce17eeb5223b..a749bafd01de5ad74a22bedb5e8534dbe8515640 100644 (file)
@@ -27,6 +27,7 @@ import (
        "strings"
        "time"
 
+       "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/resource"
 
        "github.com/gohugoio/hugo/langs"
@@ -1509,7 +1510,7 @@ func (s *Site) resetBuildState() {
        for _, p := range s.rawAllPages {
                p.subSections = Pages{}
                p.parent = nil
-               p.scratch = newScratch()
+               p.scratch = maps.NewScratch()
                p.mainPageOutput = nil
        }
 }
index dd418d7d22f1f3598b59f791aa077c326d58dfca..51bc5c796548ca593b3e7ce04ec68cd131ef0312 100644 (file)
@@ -23,6 +23,7 @@ import (
        "strings"
        "time"
 
+       "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/common/types"
        "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/helpers"
@@ -650,3 +651,9 @@ func (ns *Namespace) Uniq(l interface{}) (interface{}, error) {
 func (ns *Namespace) KeyVals(key interface{}, vals ...interface{}) (types.KeyValues, error) {
        return types.KeyValues{Key: key, Values: vals}, nil
 }
+
+// NewScratch creates a new Scratch which can be used to store values in a
+// thread safe way.
+func (ns *Namespace) NewScratch() *maps.Scratch {
+       return maps.NewScratch()
+}
index 91b0dea01375056b4c426e77f2e6b3bb1bc4e7d1..b986b3b425bb5b361ab43131e44ca25886632831 100644 (file)
@@ -144,6 +144,14 @@ func init() {
                                {`{{ seq 3 }}`, `[1 2 3]`},
                        },
                )
+
+               ns.AddMethodMapping(ctx.NewScratch,
+                       []string{"newScratch"},
+                       [][2]string{
+                               {`{{ $scratch := newScratch }}{{ $scratch.Add "b" 2 }}{{ $scratch.Add "b" 2 }}{{ $scratch.Get "b" }}`, `4`},
+                       },
+               )
+
                ns.AddMethodMapping(ctx.Uniq,
                        []string{"uniq"},
                        [][2]string{
index 534f7f28489c1fd3ddc4ae27ecdc1de19b922685..fcc6abc3d04b5dcc3a6fa479760a3db421354c07 100644 (file)
@@ -16,7 +16,8 @@ package math
 import (
        "errors"
        "math"
-       "reflect"
+
+       _math "github.com/gohugoio/hugo/common/math"
 
        "github.com/spf13/cast"
 )
@@ -31,7 +32,7 @@ type Namespace struct{}
 
 // Add adds two numbers.
 func (ns *Namespace) Add(a, b interface{}) (interface{}, error) {
-       return DoArithmetic(a, b, '+')
+       return _math.DoArithmetic(a, b, '+')
 }
 
 // Ceil returns the least integer value greater than or equal to x.
@@ -46,7 +47,7 @@ func (ns *Namespace) Ceil(x interface{}) (float64, error) {
 
 // Div divides two numbers.
 func (ns *Namespace) Div(a, b interface{}) (interface{}, error) {
-       return DoArithmetic(a, b, '/')
+       return _math.DoArithmetic(a, b, '/')
 }
 
 // Floor returns the greatest integer value less than or equal to x.
@@ -98,7 +99,7 @@ func (ns *Namespace) ModBool(a, b interface{}) (bool, error) {
 
 // Mul multiplies two numbers.
 func (ns *Namespace) Mul(a, b interface{}) (interface{}, error) {
-       return DoArithmetic(a, b, '*')
+       return _math.DoArithmetic(a, b, '*')
 }
 
 // Round returns the nearest integer, rounding half away from zero.
@@ -113,121 +114,5 @@ func (ns *Namespace) Round(x interface{}) (float64, error) {
 
 // Sub subtracts two numbers.
 func (ns *Namespace) Sub(a, b interface{}) (interface{}, error) {
-       return DoArithmetic(a, b, '-')
-}
-
-// DoArithmetic performs arithmetic operations (+,-,*,/) using reflection to
-// determine the type of the two terms.
-func DoArithmetic(a, b interface{}, op rune) (interface{}, error) {
-       av := reflect.ValueOf(a)
-       bv := reflect.ValueOf(b)
-       var ai, bi int64
-       var af, bf float64
-       var au, bu uint64
-       switch av.Kind() {
-       case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-               ai = av.Int()
-               switch bv.Kind() {
-               case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-                       bi = bv.Int()
-               case reflect.Float32, reflect.Float64:
-                       af = float64(ai) // may overflow
-                       ai = 0
-                       bf = bv.Float()
-               case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
-                       bu = bv.Uint()
-                       if ai >= 0 {
-                               au = uint64(ai)
-                               ai = 0
-                       } else {
-                               bi = int64(bu) // may overflow
-                               bu = 0
-                       }
-               default:
-                       return nil, errors.New("Can't apply the operator to the values")
-               }
-       case reflect.Float32, reflect.Float64:
-               af = av.Float()
-               switch bv.Kind() {
-               case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-                       bf = float64(bv.Int()) // may overflow
-               case reflect.Float32, reflect.Float64:
-                       bf = bv.Float()
-               case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
-                       bf = float64(bv.Uint()) // may overflow
-               default:
-                       return nil, errors.New("Can't apply the operator to the values")
-               }
-       case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
-               au = av.Uint()
-               switch bv.Kind() {
-               case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-                       bi = bv.Int()
-                       if bi >= 0 {
-                               bu = uint64(bi)
-                               bi = 0
-                       } else {
-                               ai = int64(au) // may overflow
-                               au = 0
-                       }
-               case reflect.Float32, reflect.Float64:
-                       af = float64(au) // may overflow
-                       au = 0
-                       bf = bv.Float()
-               case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
-                       bu = bv.Uint()
-               default:
-                       return nil, errors.New("Can't apply the operator to the values")
-               }
-       case reflect.String:
-               as := av.String()
-               if bv.Kind() == reflect.String && op == '+' {
-                       bs := bv.String()
-                       return as + bs, nil
-               }
-               return nil, errors.New("Can't apply the operator to the values")
-       default:
-               return nil, errors.New("Can't apply the operator to the values")
-       }
-
-       switch op {
-       case '+':
-               if ai != 0 || bi != 0 {
-                       return ai + bi, nil
-               } else if af != 0 || bf != 0 {
-                       return af + bf, nil
-               } else if au != 0 || bu != 0 {
-                       return au + bu, nil
-               }
-               return 0, nil
-       case '-':
-               if ai != 0 || bi != 0 {
-                       return ai - bi, nil
-               } else if af != 0 || bf != 0 {
-                       return af - bf, nil
-               } else if au != 0 || bu != 0 {
-                       return au - bu, nil
-               }
-               return 0, nil
-       case '*':
-               if ai != 0 || bi != 0 {
-                       return ai * bi, nil
-               } else if af != 0 || bf != 0 {
-                       return af * bf, nil
-               } else if au != 0 || bu != 0 {
-                       return au * bu, nil
-               }
-               return 0, nil
-       case '/':
-               if bi != 0 {
-                       return ai / bi, nil
-               } else if bf != 0 {
-                       return af / bf, nil
-               } else if bu != 0 {
-                       return au / bu, nil
-               }
-               return nil, errors.New("Can't divide the value by 0")
-       default:
-               return nil, errors.New("There is no such an operation")
-       }
+       return _math.DoArithmetic(a, b, '-')
 }
index 97acfaebae770850f78491206af19b59949083ba..f2e6236af048e7c401a3c1a7ac489bc12013d8cb 100644 (file)
@@ -56,93 +56,6 @@ func TestBasicNSArithmetic(t *testing.T) {
        }
 }
 
-func TestDoArithmetic(t *testing.T) {
-       t.Parallel()
-
-       for i, test := range []struct {
-               a      interface{}
-               b      interface{}
-               op     rune
-               expect interface{}
-       }{
-               {3, 2, '+', int64(5)},
-               {3, 2, '-', int64(1)},
-               {3, 2, '*', int64(6)},
-               {3, 2, '/', int64(1)},
-               {3.0, 2, '+', float64(5)},
-               {3.0, 2, '-', float64(1)},
-               {3.0, 2, '*', float64(6)},
-               {3.0, 2, '/', float64(1.5)},
-               {3, 2.0, '+', float64(5)},
-               {3, 2.0, '-', float64(1)},
-               {3, 2.0, '*', float64(6)},
-               {3, 2.0, '/', float64(1.5)},
-               {3.0, 2.0, '+', float64(5)},
-               {3.0, 2.0, '-', float64(1)},
-               {3.0, 2.0, '*', float64(6)},
-               {3.0, 2.0, '/', float64(1.5)},
-               {uint(3), uint(2), '+', uint64(5)},
-               {uint(3), uint(2), '-', uint64(1)},
-               {uint(3), uint(2), '*', uint64(6)},
-               {uint(3), uint(2), '/', uint64(1)},
-               {uint(3), 2, '+', uint64(5)},
-               {uint(3), 2, '-', uint64(1)},
-               {uint(3), 2, '*', uint64(6)},
-               {uint(3), 2, '/', uint64(1)},
-               {3, uint(2), '+', uint64(5)},
-               {3, uint(2), '-', uint64(1)},
-               {3, uint(2), '*', uint64(6)},
-               {3, uint(2), '/', uint64(1)},
-               {uint(3), -2, '+', int64(1)},
-               {uint(3), -2, '-', int64(5)},
-               {uint(3), -2, '*', int64(-6)},
-               {uint(3), -2, '/', int64(-1)},
-               {-3, uint(2), '+', int64(-1)},
-               {-3, uint(2), '-', int64(-5)},
-               {-3, uint(2), '*', int64(-6)},
-               {-3, uint(2), '/', int64(-1)},
-               {uint(3), 2.0, '+', float64(5)},
-               {uint(3), 2.0, '-', float64(1)},
-               {uint(3), 2.0, '*', float64(6)},
-               {uint(3), 2.0, '/', float64(1.5)},
-               {3.0, uint(2), '+', float64(5)},
-               {3.0, uint(2), '-', float64(1)},
-               {3.0, uint(2), '*', float64(6)},
-               {3.0, uint(2), '/', float64(1.5)},
-               {0, 0, '+', 0},
-               {0, 0, '-', 0},
-               {0, 0, '*', 0},
-               {"foo", "bar", '+', "foobar"},
-               {3, 0, '/', false},
-               {3.0, 0, '/', false},
-               {3, 0.0, '/', false},
-               {uint(3), uint(0), '/', false},
-               {3, uint(0), '/', false},
-               {-3, uint(0), '/', false},
-               {uint(3), 0, '/', false},
-               {3.0, uint(0), '/', false},
-               {uint(3), 0.0, '/', false},
-               {3, "foo", '+', false},
-               {3.0, "foo", '+', false},
-               {uint(3), "foo", '+', false},
-               {"foo", 3, '+', false},
-               {"foo", "bar", '-', false},
-               {3, 2, '%', false},
-       } {
-               errMsg := fmt.Sprintf("[%d] %v", i, test)
-
-               result, err := DoArithmetic(test.a, test.b, test.op)
-
-               if b, ok := test.expect.(bool); ok && !b {
-                       require.Error(t, err, errMsg)
-                       continue
-               }
-
-               require.NoError(t, err, errMsg)
-               assert.Equal(t, test.expect, result, errMsg)
-       }
-}
-
 func TestCeil(t *testing.T) {
        t.Parallel()
 
index 341be805ad8c7ca6b3a828eb4989b8290a0619e3..8594c67a455a6cf6ab4abc02e6200bb6c3d4085e 100644 (file)
@@ -125,7 +125,6 @@ func TestTemplateFuncsExamples(t *testing.T) {
                        }
                }
        }
-
 }
 
 // TODO(bep) it would be dandy to put this one into the partials package, but