From a10b2cd372798c4e4b862f0ec03010d2aea2ff1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Mon, 24 Oct 2016 13:45:30 +0200 Subject: [PATCH] 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 --- helpers/configProvider.go | 39 ++++++++++++++++ helpers/language.go | 21 +++++++-- helpers/language_test.go | 32 +++++++++++++ helpers/path.go | 16 +++---- helpers/path_test.go | 18 ++++++-- helpers/pathspec.go | 54 ++++++++++++++++++++++ helpers/pathspec_test.go | 45 ++++++++++++++++++ helpers/url.go | 28 ++++++------ helpers/url_test.go | 12 +++-- hugolib/hugo_sites_test.go | 1 + hugolib/node.go | 2 +- hugolib/page.go | 9 ++-- hugolib/pageSort_test.go | 5 +- hugolib/page_permalink_test.go | 6 ++- hugolib/page_test.go | 3 +- hugolib/pagination.go | 6 +-- hugolib/pagination_test.go | 4 +- hugolib/permalinks.go | 8 ++-- hugolib/permalinks_test.go | 4 ++ hugolib/site.go | 83 ++++++++++++++++++++++------------ hugolib/taxonomy.go | 2 +- tpl/template.go | 4 ++ tpl/template_funcs.go | 16 ++++--- tpl/template_funcs_test.go | 8 ++++ tpl/template_i18n.go | 8 ++-- tpl/template_i18n_test.go | 4 +- 26 files changed, 348 insertions(+), 90 deletions(-) create mode 100644 helpers/language_test.go create mode 100644 helpers/pathspec.go create mode 100644 helpers/pathspec_test.go diff --git a/helpers/configProvider.go b/helpers/configProvider.go index 35500f6de..a631ea63d 100644 --- 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 +} diff --git a/helpers/language.go b/helpers/language.go index 9ebec5a65..0a1affd18 100644 --- 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) } diff --git a/helpers/language_test.go b/helpers/language_test.go new file mode 100644 index 000000000..f812d26a2 --- /dev/null +++ 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")) +} diff --git a/helpers/path.go b/helpers/path.go index b8f642470..cf49858ee 100644 --- 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)) diff --git a/helpers/path_test.go b/helpers/path_test.go index cbdcd8da2..ef0c16505 100644 --- 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) } diff --git a/helpers/pathspec.go b/helpers/pathspec.go new file mode 100644 index 000000000..4c578cdf7 --- /dev/null +++ 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 +} diff --git a/helpers/pathspec_test.go b/helpers/pathspec_test.go new file mode 100644 index 000000000..9cd0af80e --- /dev/null +++ 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) +} diff --git a/helpers/url.go b/helpers/url.go index 49f670da2..ffb0ba234 100644 --- 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 { diff --git a/helpers/url_test.go b/helpers/url_test.go index 2cf93b859..5394e9573 100644 --- 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 { diff --git a/hugolib/hugo_sites_test.go b/hugolib/hugo_sites_test.go index b7a52b1e4..be2ba9803 100644 --- 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 diff --git a/hugolib/node.go b/hugolib/node.go index eefb78bec..16c83e2e8 100644 --- 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. diff --git a/hugolib/page.go b/hugolib/page.go index fee7c334a..484425697 100644 --- 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))) } diff --git a/hugolib/pageSort_test.go b/hugolib/pageSort_test.go index 23a3fd07c..738fcc80e 100644 --- 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)))}, } diff --git a/hugolib/page_permalink_test.go b/hugolib/page_permalink_test.go index d185bebab..eb3a6c878 100644 --- 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))}, } diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 0bb924f01..342ff3bd5 100644 --- 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 diff --git a/hugolib/pagination.go b/hugolib/pagination.go index 5bba5d89f..81f3e0cda 100644 --- 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) } } diff --git a/hugolib/pagination_test.go b/hugolib/pagination_test.go index 786650469..7bbc38058 100644 --- 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)))}, } diff --git a/hugolib/permalinks.go b/hugolib/permalinks.go index 581f8740f..c149ae13d 100644 --- 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) } diff --git a/hugolib/permalinks_test.go b/hugolib/permalinks_test.go index 672cf2d66..bdc96d41a 100644 --- 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) } diff --git a/hugolib/site.go b/hugolib/site.go index f6a787298..7f96bb5aa 100644 --- 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) } diff --git a/hugolib/taxonomy.go b/hugolib/taxonomy.go index ffe586ffc..112d22b37 100644 --- 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. diff --git a/tpl/template.go b/tpl/template.go index 1fba368e4..fa358ca37 100644 --- 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 } diff --git a/tpl/template_funcs.go b/tpl/template_funcs.go index e820ccf8d..d24bc7ee8 100644 --- 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, diff --git a/tpl/template_funcs_test.go b/tpl/template_funcs_test.go index 7f46aeba9..071910579 100644 --- 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 diff --git a/tpl/template_i18n.go b/tpl/template_i18n.go index 453351a10..080bfbd54 100644 --- 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 diff --git a/tpl/template_i18n_test.go b/tpl/template_i18n_test.go index ffb27ae1d..29235b462 100644 --- 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} {