URI: 
       Avoid reading from Viper for path and URL funcs - hugo - [fork] hugo port for 9front
  HTML git clone git@git.drkhsh.at/hugo.git
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
   DIR README
   DIR LICENSE
       ---
   DIR commit a10b2cd372798c4e4b862f0ec03010d2aea2ff1e
   DIR parent dffd7da07c3fb198acfa6c4664b53132c4cabe55
  HTML Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
       Date:   Mon, 24 Oct 2016 13:45:30 +0200
       
       Avoid reading from Viper for path and URL funcs
       
       The gain, given the "real sites benchmark" below, is obvious:
       
       ```
       benchmark           old ns/op       new ns/op       delta
       BenchmarkHugo-4     14497594101     13084156335     -9.75%
       
       benchmark           old allocs     new allocs     delta
       BenchmarkHugo-4     57404335       48282002       -15.89%
       
       benchmark           old bytes       new bytes      delta
       BenchmarkHugo-4     9933505624      9721984424     -2.13%
       ```
       
       Fixes #2495
       Diffstat:
         M helpers/configProvider.go           |      39 +++++++++++++++++++++++++++++++
         M helpers/language.go                 |      21 ++++++++++++++++++---
         A helpers/language_test.go            |      32 +++++++++++++++++++++++++++++++
         M helpers/path.go                     |      16 ++++++++--------
         M helpers/path_test.go                |      18 +++++++++++++++---
         A helpers/pathspec.go                 |      54 +++++++++++++++++++++++++++++++
         A helpers/pathspec_test.go            |      45 +++++++++++++++++++++++++++++++
         M helpers/url.go                      |      28 ++++++++++++++--------------
         M helpers/url_test.go                 |      12 +++++++++---
         M hugolib/hugo_sites_test.go          |       1 +
         M hugolib/node.go                     |       2 +-
         M hugolib/page.go                     |       9 +++++----
         M hugolib/pageSort_test.go            |       5 ++++-
         M hugolib/page_permalink_test.go      |       6 ++++--
         M hugolib/page_test.go                |       3 ++-
         M hugolib/pagination.go               |       6 +++---
         M hugolib/pagination_test.go          |       4 +++-
         M hugolib/permalinks.go               |       8 +++-----
         M hugolib/permalinks_test.go          |       4 ++++
         M hugolib/site.go                     |      83 ++++++++++++++++++++-----------
         M hugolib/taxonomy.go                 |       2 +-
         M tpl/template.go                     |       4 ++++
         M tpl/template_funcs.go               |      16 +++++++++-------
         M tpl/template_funcs_test.go          |       8 ++++++++
         M tpl/template_i18n.go                |       8 +++++---
         M tpl/template_i18n_test.go           |       4 +++-
       
       26 files changed, 348 insertions(+), 90 deletions(-)
       ---
   DIR diff --git a/helpers/configProvider.go b/helpers/configProvider.go
       @@ -21,16 +21,55 @@ import (
                "github.com/spf13/viper"
        )
        
       +// A cached version of the current ConfigProvider (language) and relatives. These globals
       +// are unfortunate, but we still have some places that needs this that does
       +// not have access to the site configuration.
       +// These values will be set on initialization when rendering a new language.
       +//
       +// TODO(bep) Get rid of these.
       +var (
       +        currentConfigProvider ConfigProvider
       +        currentPathSpec       *PathSpec
       +)
       +
        // ConfigProvider provides the configuration settings for Hugo.
        type ConfigProvider interface {
                GetString(key string) string
                GetInt(key string) int
       +        GetBool(key string) bool
                GetStringMap(key string) map[string]interface{}
                GetStringMapString(key string) map[string]string
       +        Get(key string) interface{}
        }
        
        // Config returns the currently active Hugo config. This will be set
        // per site (language) rendered.
        func Config() ConfigProvider {
       +        if currentConfigProvider != nil {
       +                return currentConfigProvider
       +        }
       +        // Some tests rely on this. We will fix that, eventually.
                return viper.Get("CurrentContentLanguage").(ConfigProvider)
        }
       +
       +// CurrentPathSpec returns the current PathSpec.
       +// If it is not set, a new will be created based in the currently active Hugo config.
       +func CurrentPathSpec() *PathSpec {
       +        if currentPathSpec != nil {
       +                return currentPathSpec
       +        }
       +        // Some tests rely on this. We will fix that, eventually.
       +        return NewPathSpecFromConfig(Config())
       +}
       +
       +// InitConfigProviderForCurrentContentLanguage does what it says.
       +func InitConfigProviderForCurrentContentLanguage() {
       +        currentConfigProvider = viper.Get("CurrentContentLanguage").(ConfigProvider)
       +        currentPathSpec = NewPathSpecFromConfig(currentConfigProvider)
       +}
       +
       +// ResetConfigProvider is used in tests.
       +func ResetConfigProvider() {
       +        currentConfigProvider = nil
       +        currentPathSpec = nil
       +}
   DIR diff --git a/helpers/language.go b/helpers/language.go
       @@ -23,6 +23,19 @@ import (
                "github.com/spf13/viper"
        )
        
       +// These are the settings that should only be looked up in the global Viper
       +// config and not per language.
       +// This list may not be complete, but contains only settings that we know
       +// will be looked up in both.
       +// This isn't perfect, but it is ultimately the user who shoots him/herself in
       +// the foot.
       +// See the pathSpec.
       +var globalOnlySettings = map[string]bool{
       +        strings.ToLower("defaultContentLanguageInSubdir"): true,
       +        strings.ToLower("defaultContentLanguage"):         true,
       +        strings.ToLower("multilingual"):                   true,
       +}
       +
        type Language struct {
                Lang         string
                LanguageName string
       @@ -81,7 +94,7 @@ func (l *Language) Params() map[string]interface{} {
        }
        
        func (l *Language) SetParam(k string, v interface{}) {
       -        l.params[k] = v
       +        l.params[strings.ToLower(k)] = v
        }
        
        func (l *Language) GetBool(key string) bool     { return cast.ToBool(l.Get(key)) }
       @@ -101,8 +114,10 @@ func (l *Language) Get(key string) interface{} {
                        panic("language not set")
                }
                key = strings.ToLower(key)
       -        if v, ok := l.params[key]; ok {
       -                return v
       +        if !globalOnlySettings[key] {
       +                if v, ok := l.params[key]; ok {
       +                        return v
       +                }
                }
                return viper.Get(key)
        }
   DIR diff --git a/helpers/language_test.go b/helpers/language_test.go
       @@ -0,0 +1,32 @@
       +// Copyright 2016-present 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 helpers
       +
       +import (
       +        "testing"
       +
       +        "github.com/spf13/viper"
       +        "github.com/stretchr/testify/require"
       +)
       +
       +func TestGetGlobalOnlySetting(t *testing.T) {
       +        lang := NewDefaultLanguage()
       +        lang.SetParam("defaultContentLanguageInSubdir", false)
       +        lang.SetParam("paginatePath", "side")
       +        viper.Set("defaultContentLanguageInSubdir", true)
       +        viper.Set("paginatePath", "page")
       +
       +        require.True(t, lang.GetBool("defaultContentLanguageInSubdir"))
       +        require.Equal(t, "side", lang.GetString("paginatePath"))
       +}
   DIR diff --git a/helpers/path.go b/helpers/path.go
       @@ -75,16 +75,16 @@ var fpb filepathBridge
        // It does so by creating a Unicode-sanitized string, with the spaces replaced,
        // whilst preserving the original casing of the string.
        // E.g. Social Media -> Social-Media
       -func MakePath(s string) string {
       -        return UnicodeSanitize(strings.Replace(strings.TrimSpace(s), " ", "-", -1))
       +func (p *PathSpec) MakePath(s string) string {
       +        return p.UnicodeSanitize(strings.Replace(strings.TrimSpace(s), " ", "-", -1))
        }
        
        // MakePathSanitized creates a Unicode-sanitized string, with the spaces replaced
       -func MakePathSanitized(s string) string {
       -        if viper.GetBool("DisablePathToLower") {
       -                return MakePath(s)
       +func (p *PathSpec) MakePathSanitized(s string) string {
       +        if p.disablePathToLower {
       +                return p.MakePath(s)
                }
       -        return strings.ToLower(MakePath(s))
       +        return strings.ToLower(p.MakePath(s))
        }
        
        // MakeTitle converts the path given to a suitable title, trimming whitespace
       @@ -110,7 +110,7 @@ func ishex(c rune) bool {
        // a predefined set of special Unicode characters.
        // If RemovePathAccents configuration flag is enabled, Uniccode accents
        // are also removed.
       -func UnicodeSanitize(s string) string {
       +func (p *PathSpec) UnicodeSanitize(s string) string {
                source := []rune(s)
                target := make([]rune, 0, len(source))
        
       @@ -124,7 +124,7 @@ func UnicodeSanitize(s string) string {
        
                var result string
        
       -        if viper.GetBool("RemovePathAccents") {
       +        if p.removePathAccents {
                        // remove accents - see https://blog.golang.org/normalization
                        t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC)
                        result, _, _ = transform.String(t, string(target))
   DIR diff --git a/helpers/path_test.go b/helpers/path_test.go
       @@ -33,9 +33,14 @@ import (
                "github.com/spf13/viper"
        )
        
       +func initCommonTestConfig() {
       +        viper.Set("CurrentContentLanguage", NewLanguage("en"))
       +}
       +
        func TestMakePath(t *testing.T) {
                viper.Reset()
                defer viper.Reset()
       +        initCommonTestConfig()
        
                tests := []struct {
                        input         string
       @@ -57,7 +62,8 @@ func TestMakePath(t *testing.T) {
        
                for _, test := range tests {
                        viper.Set("RemovePathAccents", test.removeAccents)
       -                output := MakePath(test.input)
       +                p := NewPathSpecFromConfig(viper.GetViper())
       +                output := p.MakePath(test.input)
                        if output != test.expected {
                                t.Errorf("Expected %#v, got %#v\n", test.expected, output)
                        }
       @@ -67,6 +73,9 @@ func TestMakePath(t *testing.T) {
        func TestMakePathSanitized(t *testing.T) {
                viper.Reset()
                defer viper.Reset()
       +        initCommonTestConfig()
       +
       +        p := NewPathSpecFromConfig(viper.GetViper())
        
                tests := []struct {
                        input    string
       @@ -81,7 +90,7 @@ func TestMakePathSanitized(t *testing.T) {
                }
        
                for _, test := range tests {
       -                output := MakePathSanitized(test.input)
       +                output := p.MakePathSanitized(test.input)
                        if output != test.expected {
                                t.Errorf("Expected %#v, got %#v\n", test.expected, output)
                        }
       @@ -91,7 +100,10 @@ func TestMakePathSanitized(t *testing.T) {
        func TestMakePathSanitizedDisablePathToLower(t *testing.T) {
                viper.Reset()
                defer viper.Reset()
       +
       +        initCommonTestConfig()
                viper.Set("DisablePathToLower", true)
       +        p := NewPathSpecFromConfig(viper.GetViper())
        
                tests := []struct {
                        input    string
       @@ -106,7 +118,7 @@ func TestMakePathSanitizedDisablePathToLower(t *testing.T) {
                }
        
                for _, test := range tests {
       -                output := MakePathSanitized(test.input)
       +                output := p.MakePathSanitized(test.input)
                        if output != test.expected {
                                t.Errorf("Expected %#v, got %#v\n", test.expected, output)
                        }
   DIR diff --git a/helpers/pathspec.go b/helpers/pathspec.go
       @@ -0,0 +1,54 @@
       +// Copyright 2016-present 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 helpers
       +
       +// PathSpec holds methods that decides how paths in URLs and files in Hugo should look like.
       +type PathSpec struct {
       +        disablePathToLower bool
       +        removePathAccents  bool
       +        uglyURLs           bool
       +        canonifyURLs       bool
       +
       +        currentContentLanguage *Language
       +
       +        // pagination path handling
       +        paginatePath string
       +
       +        // The PathSpec looks up its config settings in both the current language
       +        // and then in the global Viper config.
       +        // Some settings, the settings listed below, does not make sense to be set
       +        // on per-language-basis. We have no good way of protecting against this
       +        // other than a "white-list". See language.go.
       +        defaultContentLanguageInSubdir bool
       +        defaultContentLanguage         string
       +        multilingual                   bool
       +}
       +
       +func NewPathSpecFromConfig(config ConfigProvider) *PathSpec {
       +        return &PathSpec{
       +                disablePathToLower:             config.GetBool("disablePathToLower"),
       +                removePathAccents:              config.GetBool("removePathAccents"),
       +                uglyURLs:                       config.GetBool("uglyURLs"),
       +                canonifyURLs:                   config.GetBool("canonifyURLs"),
       +                multilingual:                   config.GetBool("multilingual"),
       +                defaultContentLanguageInSubdir: config.GetBool("defaultContentLanguageInSubdir"),
       +                defaultContentLanguage:         config.GetString("defaultContentLanguage"),
       +                currentContentLanguage:         config.Get("currentContentLanguage").(*Language),
       +                paginatePath:                   config.GetString("paginatePath"),
       +        }
       +}
       +
       +func (p *PathSpec) PaginatePath() string {
       +        return p.paginatePath
       +}
   DIR diff --git a/helpers/pathspec_test.go b/helpers/pathspec_test.go
       @@ -0,0 +1,45 @@
       +// Copyright 2016-present 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 helpers
       +
       +import (
       +        "testing"
       +
       +        "github.com/spf13/viper"
       +        "github.com/stretchr/testify/require"
       +)
       +
       +func TestNewPathSpecFromConfig(t *testing.T) {
       +        viper.Set("disablePathToLower", true)
       +        viper.Set("removePathAccents", true)
       +        viper.Set("uglyURLs", true)
       +        viper.Set("multilingual", true)
       +        viper.Set("defaultContentLanguageInSubdir", true)
       +        viper.Set("defaultContentLanguage", "no")
       +        viper.Set("currentContentLanguage", NewLanguage("no"))
       +        viper.Set("canonifyURLs", true)
       +        viper.Set("paginatePath", "side")
       +
       +        pathSpec := NewPathSpecFromConfig(viper.GetViper())
       +
       +        require.True(t, pathSpec.canonifyURLs)
       +        require.True(t, pathSpec.defaultContentLanguageInSubdir)
       +        require.True(t, pathSpec.disablePathToLower)
       +        require.True(t, pathSpec.multilingual)
       +        require.True(t, pathSpec.removePathAccents)
       +        require.True(t, pathSpec.uglyURLs)
       +        require.Equal(t, "no", pathSpec.defaultContentLanguage)
       +        require.Equal(t, "no", pathSpec.currentContentLanguage.Lang)
       +        require.Equal(t, "side", pathSpec.paginatePath)
       +}
   DIR diff --git a/helpers/url.go b/helpers/url.go
       @@ -101,8 +101,8 @@ func SanitizeURLKeepTrailingSlash(in string) string {
        // Example:
        //     uri: Vim (text editor)
        //     urlize: vim-text-editor
       -func URLize(uri string) string {
       -        sanitized := MakePathSanitized(uri)
       +func (p *PathSpec) URLize(uri string) string {
       +        sanitized := p.MakePathSanitized(uri)
        
                // escape unicode letters
                parsedURI, err := url.Parse(sanitized)
       @@ -147,7 +147,7 @@ func MakePermalink(host, plink string) *url.URL {
        }
        
        // AbsURL creates a absolute URL from the relative path given and the BaseURL set in config.
       -func AbsURL(in string, addLanguage bool) string {
       +func (p *PathSpec) AbsURL(in string, addLanguage bool) string {
                url, err := url.Parse(in)
                if err != nil {
                        return in
       @@ -168,7 +168,7 @@ func AbsURL(in string, addLanguage bool) string {
                }
        
                if addLanguage {
       -                prefix := getLanguagePrefix()
       +                prefix := p.getLanguagePrefix()
                        if prefix != "" {
                                hasPrefix := false
                                // avoid adding language prefix if already present
       @@ -191,15 +191,15 @@ func AbsURL(in string, addLanguage bool) string {
                return MakePermalink(baseURL, in).String()
        }
        
       -func getLanguagePrefix() string {
       -        if !viper.GetBool("Multilingual") {
       +func (p *PathSpec) getLanguagePrefix() string {
       +        if !p.multilingual {
                        return ""
                }
        
       -        defaultLang := viper.GetString("DefaultContentLanguage")
       -        defaultInSubDir := viper.GetBool("DefaultContentLanguageInSubdir")
       +        defaultLang := p.defaultContentLanguage
       +        defaultInSubDir := p.defaultContentLanguageInSubdir
        
       -        currentLang := viper.Get("CurrentContentLanguage").(*Language).Lang
       +        currentLang := p.currentContentLanguage.Lang
                if currentLang == "" || (currentLang == defaultLang && !defaultInSubDir) {
                        return ""
                }
       @@ -218,9 +218,9 @@ func IsAbsURL(path string) bool {
        
        // RelURL creates a URL relative to the BaseURL root.
        // Note: The result URL will not include the context root if canonifyURLs is enabled.
       -func RelURL(in string, addLanguage bool) string {
       +func (p *PathSpec) RelURL(in string, addLanguage bool) string {
                baseURL := viper.GetString("BaseURL")
       -        canonifyURLs := viper.GetBool("canonifyURLs")
       +        canonifyURLs := p.canonifyURLs
                if (!strings.HasPrefix(in, baseURL) && strings.HasPrefix(in, "http")) || strings.HasPrefix(in, "//") {
                        return in
                }
       @@ -232,7 +232,7 @@ func RelURL(in string, addLanguage bool) string {
                }
        
                if addLanguage {
       -                prefix := getLanguagePrefix()
       +                prefix := p.getLanguagePrefix()
                        if prefix != "" {
                                hasPrefix := false
                                // avoid adding language prefix if already present
       @@ -288,8 +288,8 @@ func AddContextRoot(baseURL, relativePath string) string {
                return newPath
        }
        
       -func URLizeAndPrep(in string) string {
       -        return URLPrep(viper.GetBool("UglyURLs"), URLize(in))
       +func (p *PathSpec) URLizeAndPrep(in string) string {
       +        return URLPrep(p.uglyURLs, p.URLize(in))
        }
        
        func URLPrep(ugly bool, in string) string {
   DIR diff --git a/helpers/url_test.go b/helpers/url_test.go
       @@ -24,6 +24,10 @@ import (
        )
        
        func TestURLize(t *testing.T) {
       +        initCommonTestConfig()
       +
       +        p := NewPathSpecFromConfig(viper.GetViper())
       +
                tests := []struct {
                        input    string
                        expected string
       @@ -37,7 +41,7 @@ func TestURLize(t *testing.T) {
                }
        
                for _, test := range tests {
       -                output := URLize(test.input)
       +                output := p.URLize(test.input)
                        if output != test.expected {
                                t.Errorf("Expected %#v, got %#v\n", test.expected, output)
                        }
       @@ -83,7 +87,8 @@ func doTestAbsURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool,
        
                for _, test := range tests {
                        viper.Set("BaseURL", test.baseURL)
       -                output := AbsURL(test.input, addLanguage)
       +                p := NewPathSpecFromConfig(viper.GetViper())
       +                output := p.AbsURL(test.input, addLanguage)
                        expected := test.expected
                        if multilingual && addLanguage {
                                if !defaultInSubDir && lang == "en" {
       @@ -159,8 +164,9 @@ func doTestRelURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool,
                for i, test := range tests {
                        viper.Set("BaseURL", test.baseURL)
                        viper.Set("canonifyURLs", test.canonify)
       +                p := NewPathSpecFromConfig(viper.GetViper())
        
       -                output := RelURL(test.input, addLanguage)
       +                output := p.RelURL(test.input, addLanguage)
        
                        expected := test.expected
                        if multilingual && addLanguage {
   DIR diff --git a/hugolib/hugo_sites_test.go b/hugolib/hugo_sites_test.go
       @@ -35,6 +35,7 @@ func testCommonResetState() {
                hugofs.InitMemFs()
                viper.Reset()
                viper.SetFs(hugofs.Source())
       +        helpers.ResetConfigProvider()
                loadDefaultSettings()
        
                // Default is false, but true is easier to use as default in tests
   DIR diff --git a/hugolib/node.go b/hugolib/node.go
       @@ -178,7 +178,7 @@ func (n *Node) URL() string {
        }
        
        func (n *Node) Permalink() string {
       -        return permalink(n.URL())
       +        return n.Site.permalink(n.URL())
        }
        
        // Scratch returns the writable context associated with this Node.
   DIR diff --git a/hugolib/page.go b/hugolib/page.go
       @@ -569,9 +569,9 @@ func (p *Page) analyzePage() {
        
        func (p *Page) permalink() (*url.URL, error) {
                baseURL := string(p.Site.BaseURL)
       -        dir := strings.TrimSpace(helpers.MakePath(filepath.ToSlash(strings.ToLower(p.Source.Dir()))))
       -        pSlug := strings.TrimSpace(helpers.URLize(p.Slug))
       -        pURL := strings.TrimSpace(helpers.URLize(p.URLPath.URL))
       +        dir := strings.TrimSpace(p.Site.pathSpec.MakePath(filepath.ToSlash(strings.ToLower(p.Source.Dir()))))
       +        pSlug := strings.TrimSpace(p.Site.pathSpec.URLize(p.Slug))
       +        pURL := strings.TrimSpace(p.Site.pathSpec.URLize(p.URLPath.URL))
                var permalink string
                var err error
        
       @@ -1171,5 +1171,6 @@ func (p *Page) TargetPath() (outfile string) {
                        outfile = helpers.ReplaceExtension(p.Source.TranslationBaseName(), p.Extension())
                }
        
       -        return p.addLangFilepathPrefix(filepath.Join(strings.ToLower(helpers.MakePath(p.Source.Dir())), strings.TrimSpace(outfile)))
       +        return p.addLangFilepathPrefix(filepath.Join(strings.ToLower(
       +                p.Site.pathSpec.MakePath(p.Source.Dir())), strings.TrimSpace(outfile)))
        }
   DIR diff --git a/hugolib/pageSort_test.go b/hugolib/pageSort_test.go
       @@ -20,6 +20,7 @@ import (
                "testing"
                "time"
        
       +        "github.com/spf13/hugo/helpers"
                "github.com/spf13/hugo/source"
                "github.com/stretchr/testify/assert"
        )
       @@ -134,6 +135,8 @@ func setSortVals(dates [3]time.Time, titles [3]string, weights [3]int, pages Pag
        func createSortTestPages(num int) Pages {
                pages := make(Pages, num)
        
       +        info := newSiteInfo(siteBuilderCfg{baseURL: "http://base", language: helpers.NewDefaultLanguage()})
       +
                for i := 0; i < num; i++ {
                        pages[i] = &Page{
                                Node: Node{
       @@ -141,7 +144,7 @@ func createSortTestPages(num int) Pages {
                                                Section: "z",
                                                URL:     fmt.Sprintf("http://base/x/y/p%d.html", i),
                                        },
       -                                Site: newSiteInfoDefaultLanguage("http://base/"),
       +                                Site: &info,
                                },
                                Source: Source{File: *source.NewFile(filepath.FromSlash(fmt.Sprintf("/x/y/p%d.md", i)))},
                        }
   DIR diff --git a/hugolib/page_permalink_test.go b/hugolib/page_permalink_test.go
       @@ -18,6 +18,7 @@ import (
                "path/filepath"
                "testing"
        
       +        "github.com/spf13/hugo/helpers"
                "github.com/spf13/hugo/source"
                "github.com/spf13/viper"
        )
       @@ -59,17 +60,18 @@ func TestPermalink(t *testing.T) {
                }
        
                viper.Set("DefaultExtension", "html")
       -
                for i, test := range tests {
                        viper.Set("uglyurls", test.uglyURLs)
                        viper.Set("canonifyurls", test.canonifyURLs)
       +                info := newSiteInfo(siteBuilderCfg{baseURL: string(test.base), language: helpers.NewDefaultLanguage()})
       +
                        p := &Page{
                                Node: Node{
                                        URLPath: URLPath{
                                                Section: "z",
                                                URL:     test.url,
                                        },
       -                                Site: newSiteInfoDefaultLanguage(string(test.base)),
       +                                Site: &info,
                                },
                                Source: Source{File: *source.NewFile(filepath.FromSlash(test.file))},
                        }
   DIR diff --git a/hugolib/page_test.go b/hugolib/page_test.go
       @@ -1122,7 +1122,8 @@ func TestPagePaths(t *testing.T) {
        
                for _, test := range tests {
                        p, _ := NewPageFrom(strings.NewReader(test.content), filepath.FromSlash(test.path))
       -                p.Node.Site = newSiteInfoDefaultLanguage("")
       +                info := newSiteInfo(siteBuilderCfg{language: helpers.NewDefaultLanguage()})
       +                p.Node.Site = &info
        
                        if test.hasPermalink {
                                p.Node.Site.Permalinks = siteParmalinksSetting
   DIR diff --git a/hugolib/pagination.go b/hugolib/pagination.go
       @@ -508,16 +508,16 @@ func newPaginator(elements []paginatedElement, total, size int, urlFactory pagin
        }
        
        func newPaginationURLFactory(pathElements ...string) paginationURLFactory {
       -        paginatePath := helpers.Config().GetString("paginatePath")
       +        pathSpec := helpers.CurrentPathSpec()
        
                return func(page int) string {
                        var rel string
                        if page == 1 {
                                rel = fmt.Sprintf("/%s/", path.Join(pathElements...))
                        } else {
       -                        rel = fmt.Sprintf("/%s/%s/%d/", path.Join(pathElements...), paginatePath, page)
       +                        rel = fmt.Sprintf("/%s/%s/%d/", path.Join(pathElements...), pathSpec.PaginatePath(), page)
                        }
        
       -                return helpers.URLizeAndPrep(rel)
       +                return pathSpec.URLizeAndPrep(rel)
                }
        }
   DIR diff --git a/hugolib/pagination_test.go b/hugolib/pagination_test.go
       @@ -19,6 +19,7 @@ import (
                "path/filepath"
                "testing"
        
       +        "github.com/spf13/hugo/helpers"
                "github.com/spf13/hugo/source"
                "github.com/spf13/viper"
                "github.com/stretchr/testify/assert"
       @@ -453,6 +454,7 @@ func TestPage(t *testing.T) {
        func createTestPages(num int) Pages {
                pages := make(Pages, num)
        
       +        info := newSiteInfo(siteBuilderCfg{baseURL: "http://base/", language: helpers.NewDefaultLanguage()})
                for i := 0; i < num; i++ {
                        pages[i] = &Page{
                                Node: Node{
       @@ -460,7 +462,7 @@ func createTestPages(num int) Pages {
                                                Section: "z",
                                                URL:     fmt.Sprintf("http://base/x/y/p%d.html", i),
                                        },
       -                                Site: newSiteInfoDefaultLanguage("http://base/"),
       +                                Site: &info,
                                },
                                Source: Source{File: *source.NewFile(filepath.FromSlash(fmt.Sprintf("/x/y/p%d.md", i)))},
                        }
   DIR diff --git a/hugolib/permalinks.go b/hugolib/permalinks.go
       @@ -19,8 +19,6 @@ import (
                "regexp"
                "strconv"
                "strings"
       -
       -        "github.com/spf13/hugo/helpers"
        )
        
        // pathPattern represents a string which builds up a URL from attributes
       @@ -152,14 +150,14 @@ func pageToPermalinkDate(p *Page, dateField string) (string, error) {
        func pageToPermalinkTitle(p *Page, _ string) (string, error) {
                // Page contains Node which has Title
                // (also contains URLPath which has Slug, sometimes)
       -        return helpers.URLize(p.Title), nil
       +        return p.Site.pathSpec.URLize(p.Title), nil
        }
        
        // pageToPermalinkFilename returns the URL-safe form of the filename
        func pageToPermalinkFilename(p *Page, _ string) (string, error) {
                //var extension = p.Source.Ext
                //var name = p.Source.Path()[0 : len(p.Source.Path())-len(extension)]
       -        return helpers.URLize(p.Source.TranslationBaseName()), nil
       +        return p.Site.pathSpec.URLize(p.Source.TranslationBaseName()), nil
        }
        
        // if the page has a slug, return the slug, else return the title
       @@ -173,7 +171,7 @@ func pageToPermalinkSlugElseTitle(p *Page, a string) (string, error) {
                        if strings.HasSuffix(p.Slug, "-") {
                                p.Slug = p.Slug[0 : len(p.Slug)-1]
                        }
       -                return helpers.URLize(p.Slug), nil
       +                return p.Site.pathSpec.URLize(p.Slug), nil
                }
                return pageToPermalinkTitle(p, a)
        }
   DIR diff --git a/hugolib/permalinks_test.go b/hugolib/permalinks_test.go
       @@ -16,6 +16,8 @@ package hugolib
        import (
                "strings"
                "testing"
       +
       +        "github.com/spf13/hugo/helpers"
        )
        
        // testdataPermalinks is used by a couple of tests; the expandsTo content is
       @@ -70,6 +72,8 @@ func TestPermalinkValidation(t *testing.T) {
        
        func TestPermalinkExpansion(t *testing.T) {
                page, err := NewPageFrom(strings.NewReader(simplePageJSON), "blue/test-page.md")
       +        info := newSiteInfo(siteBuilderCfg{language: helpers.NewDefaultLanguage()})
       +        page.Site = &info
                if err != nil {
                        t.Fatalf("failed before we began, could not parse SIMPLE_PAGE_JSON: %s", err)
                }
   DIR diff --git a/hugolib/site.go b/hugolib/site.go
       @@ -117,7 +117,8 @@ func (s *Site) reset() *Site {
        
        // newSite creates a new site in the given language.
        func newSite(lang *helpers.Language) *Site {
       -        return &Site{Language: lang, Info: SiteInfo{multilingual: newMultiLingualForLanguage(lang)}}
       +        return &Site{Language: lang, Info: newSiteInfo(siteBuilderCfg{language: lang})}
       +
        }
        
        // newSite creates a new site in the default language.
       @@ -139,9 +140,12 @@ func newSiteFromSources(pathContentPairs ...string) *Site {
                        sources = append(sources, source.ByteSource{Name: filepath.FromSlash(path), Content: []byte(content)})
                }
        
       +        lang := helpers.NewDefaultLanguage()
       +
                return &Site{
                        Source:   &source.InMemorySource{ByteSource: sources},
       -                Language: helpers.NewDefaultLanguage(),
       +                Language: lang,
       +                Info:     newSiteInfo(siteBuilderCfg{language: lang}),
                }
        }
        
       @@ -195,16 +199,25 @@ type SiteInfo struct {
                LanguagePrefix                 string
                Languages                      helpers.Languages
                defaultContentLanguageInSubdir bool
       +
       +        pathSpec *helpers.PathSpec
        }
        
        // Used in tests.
       -func newSiteInfoDefaultLanguage(baseURL string, pages ...*Page) *SiteInfo {
       -        ps := Pages(pages)
        
       -        return &SiteInfo{
       -                BaseURL:      template.URL(baseURL),
       -                rawAllPages:  &ps,
       -                multilingual: newMultiLingualDefaultLanguage(),
       +type siteBuilderCfg struct {
       +        language *helpers.Language
       +        baseURL  string
       +
       +        pages *Pages
       +}
       +
       +func newSiteInfo(cfg siteBuilderCfg) SiteInfo {
       +        return SiteInfo{
       +                BaseURL:      template.URL(cfg.baseURL),
       +                rawAllPages:  cfg.pages,
       +                pathSpec:     helpers.NewPathSpecFromConfig(cfg.language),
       +                multilingual: newMultiLingualForLanguage(cfg.language),
                }
        }
        
       @@ -808,7 +821,9 @@ func (s *Site) setCurrentLanguageConfig() error {
                // There are sadly some global template funcs etc. that need the language information.
                viper.Set("Multilingual", s.multilingualEnabled())
                viper.Set("CurrentContentLanguage", s.Language)
       -        return tpl.SetTranslateLang(s.Language.Lang)
       +        // Cache the current config.
       +        helpers.InitConfigProviderForCurrentContentLanguage()
       +        return tpl.SetTranslateLang(s.Language)
        }
        
        func (s *Site) render() (err error) {
       @@ -887,7 +902,7 @@ func (s *SiteInfo) HomeAbsURL() string {
                if s.IsMultiLingual() {
                        base = s.Language.Lang
                }
       -        return helpers.AbsURL(base, false)
       +        return s.pathSpec.AbsURL(base, false)
        }
        
        // SitemapAbsURL is a convenience method giving the absolute URL to the sitemap.
       @@ -946,7 +961,6 @@ func (s *Site) initializeSiteInfo() {
                        Languages:                      languages,
                        defaultContentLanguageInSubdir: defaultContentInSubDir,
                        GoogleAnalytics:                lang.GetString("GoogleAnalytics"),
       -                RSSLink:                        permalinkStr(lang.GetString("RSSUri")),
                        BuildDrafts:                    viper.GetBool("BuildDrafts"),
                        canonifyURLs:                   viper.GetBool("CanonifyURLs"),
                        preserveTaxonomyNames:          lang.GetBool("PreserveTaxonomyNames"),
       @@ -959,7 +973,10 @@ func (s *Site) initializeSiteInfo() {
                        Permalinks:                     permalinks,
                        Data:                           &s.Data,
                        owner:                          s.owner,
       +                pathSpec:                       helpers.NewPathSpecFromConfig(lang),
                }
       +
       +        s.Info.RSSLink = s.Info.permalinkStr(lang.GetString("RSSUri"))
        }
        
        func (s *Site) hasTheme() bool {
       @@ -1407,7 +1424,7 @@ func (s *SiteInfo) createNodeMenuEntryURL(in string) string {
                }
                // make it match the nodes
                menuEntryURL := in
       -        menuEntryURL = helpers.SanitizeURLKeepTrailingSlash(helpers.URLize(menuEntryURL))
       +        menuEntryURL = helpers.SanitizeURLKeepTrailingSlash(s.pathSpec.URLize(menuEntryURL))
                if !s.canonifyURLs {
                        menuEntryURL = helpers.AddContextRoot(string(s.BaseURL), menuEntryURL)
                }
       @@ -1586,13 +1603,13 @@ func (s *Site) renderAliases() error {
                if s.owner.multilingual.enabled() {
                        mainLang := s.owner.multilingual.DefaultLang.Lang
                        if s.Info.defaultContentLanguageInSubdir {
       -                        mainLangURL := helpers.AbsURL(mainLang, false)
       +                        mainLangURL := s.Info.pathSpec.AbsURL(mainLang, false)
                                jww.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
                                if err := s.publishDestAlias(s.languageAliasTarget(), "/", mainLangURL, nil); err != nil {
                                        return err
                                }
                        } else {
       -                        mainLangURL := helpers.AbsURL("", false)
       +                        mainLangURL := s.Info.pathSpec.AbsURL("", false)
                                jww.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
                                if err := s.publishDestAlias(s.languageAliasTarget(), mainLang, mainLangURL, nil); err != nil {
                                        return err
       @@ -1763,7 +1780,7 @@ func (s *Site) newTaxonomyNode(prepare bool, t taxRenderInfo, counter int) (*Nod
                n := s.nodeLookup(fmt.Sprintf("tax-%s-%s", t.plural, key), counter, prepare)
        
                if s.Info.preserveTaxonomyNames {
       -                key = helpers.MakePathSanitized(key)
       +                key = s.Info.pathSpec.MakePathSanitized(key)
                }
                base := t.plural + "/" + key
        
       @@ -1952,7 +1969,7 @@ func (s *Site) renderSectionLists(prepare bool) error {
                                []string{"section/" + section + ".html", "_default/section.html", "_default/list.html", "indexes/" + section + ".html", "_default/indexes.html"})
        
                        if s.Info.preserveTaxonomyNames {
       -                        section = helpers.MakePathSanitized(section)
       +                        section = s.Info.pathSpec.MakePathSanitized(section)
                        }
        
                        base := n.addLangPathPrefix(section)
       @@ -1966,7 +1983,7 @@ func (s *Site) renderSectionLists(prepare bool) error {
                                paginatePath := helpers.Config().GetString("paginatePath")
        
                                // write alias for page 1
       -                        s.writeDestAlias(helpers.PaginateAliasPath(base, 1), permalink(base), nil)
       +                        s.writeDestAlias(helpers.PaginateAliasPath(base, 1), s.Info.permalink(base), nil)
        
                                pagers := n.paginator.Pagers()
        
       @@ -2111,6 +2128,15 @@ func (s *Site) newHomeNode(prepare bool, counter int) *Node {
                return n
        }
        
       +func (s *Site) newPage() *Page {
       +        page := &Page{}
       +        page.language = s.Language
       +        page.Date = s.Info.LastChange
       +        page.Lastmod = s.Info.LastChange
       +        page.Site = &s.Info
       +        return page
       +}
       +
        func (s *Site) renderSitemap() error {
                if viper.GetBool("DisableSitemap") {
                        return nil
       @@ -2123,11 +2149,7 @@ func (s *Site) renderSitemap() error {
                // Prepend homepage to the list of pages
                pages := make(Pages, 0)
        
       -        page := &Page{}
       -        page.language = s.Language
       -        page.Date = s.Info.LastChange
       -        page.Lastmod = s.Info.LastChange
       -        page.Site = &s.Info
       +        page := s.newPage()
                page.URLPath.URL = ""
                page.Sitemap.ChangeFreq = sitemapDefault.ChangeFreq
                page.Sitemap.Priority = sitemapDefault.Priority
       @@ -2199,18 +2221,21 @@ func (s *Site) Stats() {
        }
        
        func (s *Site) setURLs(n *Node, in string) {
       -        n.URLPath.URL = helpers.URLizeAndPrep(in)
       -        n.URLPath.Permalink = permalink(n.URLPath.URL)
       -        n.RSSLink = template.HTML(permalink(in + ".xml"))
       +        n.URLPath.URL = s.Info.pathSpec.URLizeAndPrep(in)
       +        n.URLPath.Permalink = s.Info.permalink(n.URLPath.URL)
       +        n.RSSLink = template.HTML(s.Info.permalink(in + ".xml"))
        }
        
       -func permalink(plink string) string {
       -        return permalinkStr(plink)
       +func (s *SiteInfo) permalink(plink string) string {
       +        return s.permalinkStr(plink)
        }
        
       -func permalinkStr(plink string) string {
       -        return helpers.MakePermalink(viper.GetString("BaseURL"), helpers.URLizeAndPrep(plink)).String()
       +func (s *SiteInfo) permalinkStr(plink string) string {
       +        return helpers.MakePermalink(
       +                viper.GetString("BaseURL"),
       +                s.pathSpec.URLizeAndPrep(plink)).String()
        }
       +
        func (s *Site) newNode(nodeID string) *Node {
                return s.nodeLookup(nodeID, 0, true)
        }
   DIR diff --git a/hugolib/taxonomy.go b/hugolib/taxonomy.go
       @@ -52,7 +52,7 @@ type OrderedTaxonomyEntry struct {
        
        // KeyPrep... Taxonomies should be case insensitive. Can make it easily conditional later.
        func kp(in string) string {
       -        return helpers.MakePathSanitized(in)
       +        return helpers.CurrentPathSpec().MakePathSanitized(in)
        }
        
        // Get the weighted pages for the given key.
   DIR diff --git a/tpl/template.go b/tpl/template.go
       @@ -94,6 +94,10 @@ func New() Template {
        
                localTemplates = &templates.Template
        
       +        // The URL funcs in the funcMap is somewhat language dependent,
       +        // so need to be reinit per site.
       +        initFuncMap()
       +
                for k, v := range funcMap {
                        amber.FuncMap[k] = v
                }
   DIR diff --git a/tpl/template_funcs.go b/tpl/template_funcs.go
       @@ -47,7 +47,9 @@ import (
                "github.com/spf13/viper"
        )
        
       -var funcMap template.FuncMap
       +var (
       +        funcMap template.FuncMap
       +)
        
        // eq returns the boolean truth of arg1 == arg2.
        func eq(x, y interface{}) bool {
       @@ -1940,7 +1942,7 @@ func absURL(a interface{}) (template.HTML, error) {
                if err != nil {
                        return "", nil
                }
       -        return template.HTML(helpers.AbsURL(s, false)), nil
       +        return template.HTML(helpers.CurrentPathSpec().AbsURL(s, false)), nil
        }
        
        func relURL(a interface{}) (template.HTML, error) {
       @@ -1948,10 +1950,10 @@ func relURL(a interface{}) (template.HTML, error) {
                if err != nil {
                        return "", nil
                }
       -        return template.HTML(helpers.RelURL(s, false)), nil
       +        return template.HTML(helpers.CurrentPathSpec().RelURL(s, false)), nil
        }
        
       -func init() {
       +func initFuncMap() {
                funcMap = template.FuncMap{
                        "absURL": absURL,
                        "absLangURL": func(i interface{}) (template.HTML, error) {
       @@ -1959,7 +1961,7 @@ func init() {
                                if err != nil {
                                        return "", err
                                }
       -                        return template.HTML(helpers.AbsURL(s, true)), nil
       +                        return template.HTML(helpers.CurrentPathSpec().AbsURL(s, true)), nil
                        },
                        "add":           func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '+') },
                        "after":         after,
       @@ -2020,7 +2022,7 @@ func init() {
                                if err != nil {
                                        return "", err
                                }
       -                        return template.HTML(helpers.RelURL(s, true)), nil
       +                        return template.HTML(helpers.CurrentPathSpec().RelURL(s, true)), nil
                        },
                        "relref":       relRef,
                        "replace":      replace,
       @@ -2047,7 +2049,7 @@ func init() {
                        "time":         asTime,
                        "trim":         trim,
                        "upper":        func(a string) string { return strings.ToUpper(a) },
       -                "urlize":       helpers.URLize,
       +                "urlize":       helpers.CurrentPathSpec().URLize,
                        "where":        where,
                        "i18n":         I18nTranslate,
                        "T":            I18nTranslate,
   DIR diff --git a/tpl/template_funcs_test.go b/tpl/template_funcs_test.go
       @@ -63,6 +63,11 @@ func tstIsLt(tp tstCompareType) bool {
                return tp == tstLt || tp == tstLe
        }
        
       +func tstInitTemplates() {
       +        viper.Set("CurrentContentLanguage", helpers.NewLanguage("en"))
       +        helpers.ResetConfigProvider()
       +}
       +
        func TestFuncsInTemplate(t *testing.T) {
        
                viper.Reset()
       @@ -234,6 +239,8 @@ urlize: bat-man
        
                viper.Set("baseURL", "http://mysite.com/hugo/")
        
       +        tstInitTemplates()
       +
                if err != nil {
                        t.Fatal("Got error on parse", err)
                }
       @@ -2498,6 +2505,7 @@ func TestPartialCached(t *testing.T) {
                data.Section = "blog"
                data.Params = map[string]interface{}{"langCode": "en"}
        
       +        tstInitTemplates()
                InitializeT()
                for i, tc := range testCases {
                        var tmp string
   DIR diff --git a/tpl/template_i18n.go b/tpl/template_i18n.go
       @@ -25,6 +25,7 @@ import (
        var (
                Logi18nWarnings   bool
                i18nWarningLogger = helpers.NewDistinctFeedbackLogger()
       +        currentLanguage   *helpers.Language
        )
        
        type translate struct {
       @@ -37,11 +38,12 @@ var translator *translate
        
        // SetTranslateLang sets the translations language to use during template processing.
        // This construction is unfortunate, but the template system is currently global.
       -func SetTranslateLang(lang string) error {
       -        if f, ok := translator.translateFuncs[lang]; ok {
       +func SetTranslateLang(language *helpers.Language) error {
       +        currentLanguage = language
       +        if f, ok := translator.translateFuncs[language.Lang]; ok {
                        translator.current = f
                } else {
       -                jww.WARN.Printf("Translation func for language %v not found, use default.", lang)
       +                jww.WARN.Printf("Translation func for language %v not found, use default.", language.Lang)
                        translator.current = translator.translateFuncs[viper.GetString("DefaultContentLanguage")]
                }
                return nil
   DIR diff --git a/tpl/template_i18n_test.go b/tpl/template_i18n_test.go
       @@ -17,6 +17,7 @@ import (
                "testing"
        
                "github.com/nicksnyder/go-i18n/i18n/bundle"
       +        "github.com/spf13/hugo/helpers"
                "github.com/spf13/viper"
                "github.com/stretchr/testify/assert"
        )
       @@ -116,7 +117,7 @@ func doTestI18nTranslate(t *testing.T, data map[string][]byte, lang, id string, 
                }
        
                SetI18nTfuncs(i18nBundle)
       -        SetTranslateLang(lang)
       +        SetTranslateLang(helpers.NewLanguage(lang))
        
                translated, err := I18nTranslate(id, args)
                if err != nil {
       @@ -129,6 +130,7 @@ func TestI18nTranslate(t *testing.T) {
                var actual, expected string
        
                viper.SetDefault("DefaultContentLanguage", "en")
       +        viper.Set("CurrentContentLanguage", helpers.NewLanguage("en"))
        
                // Test without and with placeholders
                for _, enablePlaceholders := range []bool{false, true} {