shithub: hugo

Download patch

ref: 2b8d907ab731627f4e2a30442cd729064516c8bb
parent: 43338c3a99769eb7d0df0c12559b8b3d42b67dba
author: Bjørn Erik Pedersen <[email protected]>
date: Fri Jul 6 10:12:10 EDT 2018

Add a newScratch template func

Fixes #4685

--- /dev/null
+++ b/common/maps/scratch.go
@@ -1,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{})}
+}
--- /dev/null
+++ b/common/maps/scratch_test.go
@@ -1,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")
+	}
+}
--- /dev/null
+++ b/common/math/math.go
@@ -1,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")
+	}
+}
--- /dev/null
+++ b/common/math/math_test.go
@@ -1,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)
+	}
+}
--- a/docs/content/en/functions/scratch.md
+++ b/docs/content/en/functions/scratch.md
@@ -22,6 +22,9 @@
 
 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 %}}
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -257,7 +257,7 @@
 
 	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 @@
 }
 
 // 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
 }
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -1830,6 +1830,33 @@
 
 }
 
+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")
--- a/hugolib/scratch.go
+++ /dev/null
@@ -1,135 +1,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{})}
-}
--- a/hugolib/scratch_test.go
+++ /dev/null
@@ -1,170 +1,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")
-	}
-}
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -24,6 +24,7 @@
 	"strings"
 	"sync"
 
+	"github.com/gohugoio/hugo/common/maps"
 	"github.com/gohugoio/hugo/output"
 
 	"github.com/gohugoio/hugo/media"
@@ -45,7 +46,7 @@
 	// 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 @@
 
 // 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
 }
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -27,6 +27,7 @@
 	"strings"
 	"time"
 
+	"github.com/gohugoio/hugo/common/maps"
 	"github.com/gohugoio/hugo/resource"
 
 	"github.com/gohugoio/hugo/langs"
@@ -1509,7 +1510,7 @@
 	for _, p := range s.rawAllPages {
 		p.subSections = Pages{}
 		p.parent = nil
-		p.scratch = newScratch()
+		p.scratch = maps.NewScratch()
 		p.mainPageOutput = nil
 	}
 }
--- a/tpl/collections/collections.go
+++ b/tpl/collections/collections.go
@@ -23,6 +23,7 @@
 	"strings"
 	"time"
 
+	"github.com/gohugoio/hugo/common/maps"
 	"github.com/gohugoio/hugo/common/types"
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/helpers"
@@ -649,4 +650,10 @@
 // KeyVals creates a key and values wrapper.
 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()
 }
--- a/tpl/collections/init.go
+++ b/tpl/collections/init.go
@@ -144,6 +144,14 @@
 				{`{{ 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{
--- a/tpl/math/math.go
+++ b/tpl/math/math.go
@@ -16,8 +16,9 @@
 import (
 	"errors"
 	"math"
-	"reflect"
 
+	_math "github.com/gohugoio/hugo/common/math"
+
 	"github.com/spf13/cast"
 )
 
@@ -31,7 +32,7 @@
 
 // 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 @@
 
 // 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 @@
 
 // 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 @@
 
 // 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, '-')
 }
--- a/tpl/math/math_test.go
+++ b/tpl/math/math_test.go
@@ -56,93 +56,6 @@
 	}
 }
 
-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()
 
--- a/tpl/tplimpl/template_funcs_test.go
+++ b/tpl/tplimpl/template_funcs_test.go
@@ -125,7 +125,6 @@
 			}
 		}
 	}
-
 }
 
 // TODO(bep) it would be dandy to put this one into the partials package, but