From: Bjørn Erik Pedersen Date: Fri, 17 Feb 2017 12:30:50 +0000 (+0100) Subject: tpl: Refactor package X-Git-Tag: v0.19~69 X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=c507e2717df7dd4b870478033bc5ece0b039a8c4;p=brevno-suite%2Fhugo tpl: Refactor package Now: * The template API lives in /tpl * The rest lives in /tpl/tplimpl This is bound te be more improved in the future. Updates #2701 --- diff --git a/deps/deps.go b/deps/deps.go index 39a3d31a..de1b955c 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -8,7 +8,7 @@ import ( "github.com/spf13/hugo/config" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/hugofs" - "github.com/spf13/hugo/tplapi" + "github.com/spf13/hugo/tpl" jww "github.com/spf13/jwalterweatherman" ) @@ -20,7 +20,7 @@ type Deps struct { Log *jww.Notepad `json:"-"` // The templates to use. - Tmpl tplapi.Template `json:"-"` + Tmpl tpl.Template `json:"-"` // The file systems to use. Fs *hugofs.Fs `json:"-"` @@ -40,7 +40,7 @@ type Deps struct { Language *helpers.Language templateProvider ResourceProvider - WithTemplate func(templ tplapi.Template) error `json:"-"` + WithTemplate func(templ tpl.Template) error `json:"-"` translationProvider ResourceProvider } @@ -147,7 +147,7 @@ type DepsCfg struct { // Template handling. TemplateProvider ResourceProvider - WithTemplate func(templ tplapi.Template) error + WithTemplate func(templ tpl.Template) error // i18n handling. TranslationProvider ResourceProvider diff --git a/hugolib/embedded_shortcodes_test.go b/hugolib/embedded_shortcodes_test.go index 513767eb..a98ca136 100644 --- a/hugolib/embedded_shortcodes_test.go +++ b/hugolib/embedded_shortcodes_test.go @@ -25,7 +25,7 @@ import ( "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" - "github.com/spf13/hugo/tplapi" + "github.com/spf13/hugo/tpl" "github.com/stretchr/testify/require" ) @@ -335,7 +335,7 @@ func TestShortcodeTweet(t *testing.T) { th = testHelper{cfg} ) - withTemplate := func(templ tplapi.Template) error { + withTemplate := func(templ tpl.Template) error { templ.Funcs(tweetFuncMap) return nil } @@ -390,7 +390,7 @@ func TestShortcodeInstagram(t *testing.T) { th = testHelper{cfg} ) - withTemplate := func(templ tplapi.Template) error { + withTemplate := func(templ tpl.Template) error { templ.Funcs(instagramFuncMap) return nil } diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index f0feec23..3cbe4fa9 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -24,7 +24,7 @@ import ( "github.com/spf13/hugo/i18n" "github.com/spf13/hugo/tpl" - "github.com/spf13/hugo/tplapi" + "github.com/spf13/hugo/tpl/tplimpl" ) // HugoSites represents the sites to build. Each site represents a language. @@ -72,7 +72,7 @@ func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) { func applyDepsIfNeeded(cfg deps.DepsCfg, sites ...*Site) error { if cfg.TemplateProvider == nil { - cfg.TemplateProvider = tpl.DefaultTemplateProvider + cfg.TemplateProvider = tplimpl.DefaultTemplateProvider } if cfg.TranslationProvider == nil { @@ -121,8 +121,8 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { return newHugoSites(cfg, sites...) } -func (s *Site) withSiteTemplates(withTemplates ...func(templ tplapi.Template) error) func(templ tplapi.Template) error { - return func(templ tplapi.Template) error { +func (s *Site) withSiteTemplates(withTemplates ...func(templ tpl.Template) error) func(templ tpl.Template) error { + return func(templ tpl.Template) error { templ.LoadTemplates(s.absLayoutDir()) if s.hasTheme() { templ.LoadTemplatesWithPrefix(s.absThemeDir()+"/layouts", "theme") @@ -191,7 +191,7 @@ func (h *HugoSites) reset() { h.Sites[i] = s.reset() } - tpl.ResetCaches() + tplimpl.ResetCaches() } func (h *HugoSites) createSitesFromConfig() error { @@ -553,7 +553,7 @@ func (h *HugoSites) Pages() Pages { return h.Sites[0].AllPages } -func handleShortcodes(p *Page, t tplapi.Template, rawContentCopy []byte) ([]byte, error) { +func handleShortcodes(p *Page, t tpl.Template, rawContentCopy []byte) ([]byte, error) { if len(p.contentShortCodes) > 0 { p.s.Log.DEBUG.Printf("Replace %d shortcodes in %q", len(p.contentShortCodes), p.BaseFileName()) shortcodes, err := executeShortcodeFuncMap(p.contentShortCodes) diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index 4ce6df7e..775c57e8 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -26,7 +26,7 @@ import ( bp "github.com/spf13/hugo/bufferpool" "github.com/spf13/hugo/helpers" - "github.com/spf13/hugo/tplapi" + "github.com/spf13/hugo/tpl" ) // ShortcodeWithPage is the "." context in a shortcode template. @@ -541,7 +541,7 @@ func replaceShortcodeTokens(source []byte, prefix string, replacements map[strin return source, nil } -func getShortcodeTemplate(name string, t tplapi.Template) *template.Template { +func getShortcodeTemplate(name string, t tpl.Template) *template.Template { if x := t.Lookup("shortcodes/" + name + ".html"); x != nil { return x } diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index b1f28d53..665e3a94 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -25,12 +25,12 @@ import ( "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/source" - "github.com/spf13/hugo/tplapi" + "github.com/spf13/hugo/tpl" "github.com/stretchr/testify/require" ) // TODO(bep) remove -func pageFromString(in, filename string, withTemplate ...func(templ tplapi.Template) error) (*Page, error) { +func pageFromString(in, filename string, withTemplate ...func(templ tpl.Template) error) (*Page, error) { s := newTestSite(nil) if len(withTemplate) > 0 { // Have to create a new site @@ -47,11 +47,11 @@ func pageFromString(in, filename string, withTemplate ...func(templ tplapi.Templ return s.NewPageFrom(strings.NewReader(in), filename) } -func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tplapi.Template) error) { +func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.Template) error) { CheckShortCodeMatchAndError(t, input, expected, withTemplate, false) } -func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tplapi.Template) error, expectError bool) { +func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.Template) error, expectError bool) { cfg, fs := newTestCfg() @@ -100,7 +100,7 @@ func TestNonSC(t *testing.T) { // Issue #929 func TestHyphenatedSC(t *testing.T) { t.Parallel() - wt := func(tem tplapi.Template) error { + wt := func(tem tpl.Template) error { tem.AddInternalShortcode("hyphenated-video.html", `Playing Video {{ .Get 0 }}`) return nil } @@ -111,7 +111,7 @@ func TestHyphenatedSC(t *testing.T) { // Issue #1753 func TestNoTrailingNewline(t *testing.T) { t.Parallel() - wt := func(tem tplapi.Template) error { + wt := func(tem tpl.Template) error { tem.AddInternalShortcode("a.html", `{{ .Get 0 }}`) return nil } @@ -121,7 +121,7 @@ func TestNoTrailingNewline(t *testing.T) { func TestPositionalParamSC(t *testing.T) { t.Parallel() - wt := func(tem tplapi.Template) error { + wt := func(tem tpl.Template) error { tem.AddInternalShortcode("video.html", `Playing Video {{ .Get 0 }}`) return nil } @@ -135,7 +135,7 @@ func TestPositionalParamSC(t *testing.T) { func TestPositionalParamIndexOutOfBounds(t *testing.T) { t.Parallel() - wt := func(tem tplapi.Template) error { + wt := func(tem tpl.Template) error { tem.AddInternalShortcode("video.html", `Playing Video {{ .Get 1 }}`) return nil } @@ -146,7 +146,7 @@ func TestPositionalParamIndexOutOfBounds(t *testing.T) { func TestNamedParamSC(t *testing.T) { t.Parallel() - wt := func(tem tplapi.Template) error { + wt := func(tem tpl.Template) error { tem.AddInternalShortcode("img.html", ``) return nil } @@ -161,7 +161,7 @@ func TestNamedParamSC(t *testing.T) { // Issue #2294 func TestNestedNamedMissingParam(t *testing.T) { t.Parallel() - wt := func(tem tplapi.Template) error { + wt := func(tem tpl.Template) error { tem.AddInternalShortcode("acc.html", `
{{ .Inner }}
`) tem.AddInternalShortcode("div.html", `
{{ .Inner }}
`) tem.AddInternalShortcode("div2.html", `
{{ .Inner }}
`) @@ -174,7 +174,7 @@ func TestNestedNamedMissingParam(t *testing.T) { func TestIsNamedParamsSC(t *testing.T) { t.Parallel() - wt := func(tem tplapi.Template) error { + wt := func(tem tpl.Template) error { tem.AddInternalShortcode("byposition.html", `
`) tem.AddInternalShortcode("byname.html", `
`) tem.AddInternalShortcode("ifnamedparams.html", `
`) @@ -190,7 +190,7 @@ func TestIsNamedParamsSC(t *testing.T) { func TestInnerSC(t *testing.T) { t.Parallel() - wt := func(tem tplapi.Template) error { + wt := func(tem tpl.Template) error { tem.AddInternalShortcode("inside.html", `{{ .Inner }}
`) return nil } @@ -201,7 +201,7 @@ func TestInnerSC(t *testing.T) { func TestInnerSCWithMarkdown(t *testing.T) { t.Parallel() - wt := func(tem tplapi.Template) error { + wt := func(tem tpl.Template) error { tem.AddInternalShortcode("inside.html", `{{ .Inner }}
`) return nil } @@ -215,7 +215,7 @@ func TestInnerSCWithMarkdown(t *testing.T) { func TestInnerSCWithAndWithoutMarkdown(t *testing.T) { t.Parallel() - wt := func(tem tplapi.Template) error { + wt := func(tem tpl.Template) error { tem.AddInternalShortcode("inside.html", `{{ .Inner }}
`) return nil } @@ -246,7 +246,7 @@ func TestEmbeddedSC(t *testing.T) { func TestNestedSC(t *testing.T) { t.Parallel() - wt := func(tem tplapi.Template) error { + wt := func(tem tpl.Template) error { tem.AddInternalShortcode("scn1.html", `
Outer, inner is {{ .Inner }}
`) tem.AddInternalShortcode("scn2.html", `
SC2
`) return nil @@ -258,7 +258,7 @@ func TestNestedSC(t *testing.T) { func TestNestedComplexSC(t *testing.T) { t.Parallel() - wt := func(tem tplapi.Template) error { + wt := func(tem tpl.Template) error { tem.AddInternalShortcode("row.html", `-row-{{ .Inner}}-rowStop-`) tem.AddInternalShortcode("column.html", `-col-{{.Inner }}-colStop-`) tem.AddInternalShortcode("aside.html", `-aside-{{ .Inner }}-asideStop-`) @@ -274,7 +274,7 @@ func TestNestedComplexSC(t *testing.T) { func TestParentShortcode(t *testing.T) { t.Parallel() - wt := func(tem tplapi.Template) error { + wt := func(tem tpl.Template) error { tem.AddInternalShortcode("r1.html", `1: {{ .Get "pr1" }} {{ .Inner }}`) tem.AddInternalShortcode("r2.html", `2: {{ .Parent.Get "pr1" }}{{ .Get "pr2" }} {{ .Inner }}`) tem.AddInternalShortcode("r3.html", `3: {{ .Parent.Parent.Get "pr1" }}{{ .Parent.Get "pr2" }}{{ .Get "pr3" }} {{ .Inner }}`) @@ -342,7 +342,7 @@ func TestExtractShortcodes(t *testing.T) { fmt.Sprintf("Hello %sworld%s. And that's it.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""}, } { - p, _ := pageFromString(simplePage, "simple.md", func(templ tplapi.Template) error { + p, _ := pageFromString(simplePage, "simple.md", func(templ tpl.Template) error { templ.AddInternalShortcode("tag.html", `tag`) templ.AddInternalShortcode("sc1.html", `sc1`) templ.AddInternalShortcode("sc2.html", `sc2`) @@ -514,7 +514,7 @@ tags: sources[i] = source.ByteSource{Name: filepath.FromSlash(test.contentPath), Content: []byte(test.content)} } - addTemplates := func(templ tplapi.Template) error { + addTemplates := func(templ tpl.Template) error { templ.AddTemplate("_default/single.html", "{{.Content}}") templ.AddInternalShortcode("b.html", `b`) diff --git a/hugolib/site.go b/hugolib/site.go index bd015684..a5555d0e 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -40,7 +40,7 @@ import ( "github.com/spf13/hugo/parser" "github.com/spf13/hugo/source" "github.com/spf13/hugo/target" - "github.com/spf13/hugo/tplapi" + "github.com/spf13/hugo/tpl" "github.com/spf13/hugo/transform" "github.com/spf13/nitro" "github.com/spf13/viper" @@ -149,7 +149,7 @@ func NewSite(cfg deps.DepsCfg) (*Site, error) { // NewSiteDefaultLang creates a new site in the default language. // The site will have a template system loaded and ready to use. // Note: This is mainly used in single site tests. -func NewSiteDefaultLang(withTemplate ...func(templ tplapi.Template) error) (*Site, error) { +func NewSiteDefaultLang(withTemplate ...func(templ tpl.Template) error) (*Site, error) { v := viper.New() loadDefaultSettingsFor(v) return newSiteForLang(helpers.NewDefaultLanguage(v), withTemplate...) @@ -158,15 +158,15 @@ func NewSiteDefaultLang(withTemplate ...func(templ tplapi.Template) error) (*Sit // NewEnglishSite creates a new site in English language. // The site will have a template system loaded and ready to use. // Note: This is mainly used in single site tests. -func NewEnglishSite(withTemplate ...func(templ tplapi.Template) error) (*Site, error) { +func NewEnglishSite(withTemplate ...func(templ tpl.Template) error) (*Site, error) { v := viper.New() loadDefaultSettingsFor(v) return newSiteForLang(helpers.NewLanguage("en", v), withTemplate...) } // newSiteForLang creates a new site in the given language. -func newSiteForLang(lang *helpers.Language, withTemplate ...func(templ tplapi.Template) error) (*Site, error) { - withTemplates := func(templ tplapi.Template) error { +func newSiteForLang(lang *helpers.Language, withTemplate ...func(templ tpl.Template) error) (*Site, error) { + withTemplates := func(templ tpl.Template) error { for _, wt := range withTemplate { if err := wt(templ); err != nil { return err diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go index 8bbcb487..daefde52 100644 --- a/hugolib/sitemap_test.go +++ b/hugolib/sitemap_test.go @@ -19,7 +19,7 @@ import ( "reflect" "github.com/spf13/hugo/deps" - "github.com/spf13/hugo/tplapi" + "github.com/spf13/hugo/tpl" ) const sitemapTemplate = ` @@ -48,7 +48,7 @@ func doTestSitemapOutput(t *testing.T, internal bool) { depsCfg := deps.DepsCfg{Fs: fs, Cfg: cfg} if !internal { - depsCfg.WithTemplate = func(templ tplapi.Template) error { + depsCfg.WithTemplate = func(templ tpl.Template) error { templ.AddTemplate("sitemap.xml", sitemapTemplate) return nil } diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index 33e78e12..f0fcd953 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/source" - "github.com/spf13/hugo/tplapi" + "github.com/spf13/hugo/tpl" "github.com/spf13/viper" "io/ioutil" @@ -66,9 +66,9 @@ func newDebugLogger() *jww.Notepad { return jww.NewNotepad(jww.LevelDebug, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) } -func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tplapi.Template) error { +func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.Template) error { - return func(templ tplapi.Template) error { + return func(templ tpl.Template) error { for i := 0; i < len(additionalTemplates); i += 2 { err := templ.AddTemplate(additionalTemplates[i], additionalTemplates[i+1]) if err != nil { diff --git a/tpl/amber_compiler.go b/tpl/amber_compiler.go deleted file mode 100644 index 4477f6ac..00000000 --- a/tpl/amber_compiler.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2017 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 tpl - -import ( - "html/template" - - "github.com/eknkc/amber" -) - -func (gt *GoHTMLTemplate) CompileAmberWithTemplate(b []byte, path string, t *template.Template) (*template.Template, error) { - c := amber.New() - - if err := c.ParseData(b, path); err != nil { - return nil, err - } - - data, err := c.CompileString() - - if err != nil { - return nil, err - } - - tpl, err := t.Funcs(gt.amberFuncMap).Parse(data) - - if err != nil { - return nil, err - } - - return tpl, nil -} diff --git a/tpl/reflect_helpers.go b/tpl/reflect_helpers.go deleted file mode 100644 index f2ce722a..00000000 --- a/tpl/reflect_helpers.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2016 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 tpl - -import ( - "reflect" - "time" -) - -// toInt returns the int value if possible, -1 if not. -func toInt(v reflect.Value) int64 { - switch v.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() - case reflect.Interface: - return toInt(v.Elem()) - } - return -1 -} - -// toString returns the string value if possible, "" if not. -func toString(v reflect.Value) string { - switch v.Kind() { - case reflect.String: - return v.String() - case reflect.Interface: - return toString(v.Elem()) - } - return "" -} - -var ( - zero reflect.Value - errorType = reflect.TypeOf((*error)(nil)).Elem() - timeType = reflect.TypeOf((*time.Time)(nil)).Elem() -) - -func toTimeUnix(v reflect.Value) int64 { - if v.Kind() == reflect.Interface { - return toTimeUnix(v.Elem()) - } - if v.Type() != timeType { - panic("coding error: argument must be time.Time type reflect Value") - } - return v.MethodByName("Unix").Call([]reflect.Value{})[0].Int() -} - -// indirect is taken from 'text/template/exec.go' -func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { - for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { - if v.IsNil() { - return v, true - } - if v.Kind() == reflect.Interface && v.NumMethod() > 0 { - break - } - } - return v, false -} diff --git a/tpl/template.go b/tpl/template.go index 9a6364d5..aaf7fc8c 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -1,575 +1,27 @@ -// Copyright 2016 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 tpl import ( - "fmt" "html/template" "io" - "os" - "path/filepath" - "strings" - - "sync" - - "github.com/eknkc/amber" - "github.com/spf13/afero" - bp "github.com/spf13/hugo/bufferpool" - "github.com/spf13/hugo/deps" - "github.com/spf13/hugo/helpers" - "github.com/yosssi/ace" ) -// TODO(bep) globals get rid of the rest of the jww.ERR etc. - -// Protecting global map access (Amber) -var amberMu sync.Mutex - -type templateErr struct { - name string - err error -} - -type GoHTMLTemplate struct { - *template.Template - - clone *template.Template - - // a separate storage for the overlays created from cloned master templates. - // note: No mutex protection, so we add these in one Go routine, then just read. - overlays map[string]*template.Template - - errors []*templateErr - - funcster *templateFuncster - - amberFuncMap template.FuncMap - - *deps.Deps -} - -type TemplateProvider struct{} - -var DefaultTemplateProvider *TemplateProvider - -// Update updates the Hugo Template System in the provided Deps. -// with all the additional features, templates & functions -func (*TemplateProvider) Update(deps *deps.Deps) error { - // TODO(bep) check that this isn't called too many times. - tmpl := &GoHTMLTemplate{ - Template: template.New(""), - overlays: make(map[string]*template.Template), - errors: make([]*templateErr, 0), - Deps: deps, - } - - deps.Tmpl = tmpl - - tmpl.initFuncs(deps) - - tmpl.LoadEmbedded() - - if deps.WithTemplate != nil { - err := deps.WithTemplate(tmpl) - if err != nil { - tmpl.errors = append(tmpl.errors, &templateErr{"init", err}) - } - - } - - tmpl.MarkReady() - - return nil - -} - -// Clone clones -func (*TemplateProvider) Clone(d *deps.Deps) error { - - t := d.Tmpl.(*GoHTMLTemplate) - - // 1. Clone the clone with new template funcs - // 2. Clone any overlays with new template funcs - - tmpl := &GoHTMLTemplate{ - Template: template.Must(t.Template.Clone()), - overlays: make(map[string]*template.Template), - errors: make([]*templateErr, 0), - Deps: d, - } - - d.Tmpl = tmpl - tmpl.initFuncs(d) - - for k, v := range t.overlays { - vc := template.Must(v.Clone()) - // The extra lookup is a workaround, see - // * https://github.com/golang/go/issues/16101 - // * https://github.com/spf13/hugo/issues/2549 - vc = vc.Lookup(vc.Name()) - vc.Funcs(tmpl.funcster.funcMap) - tmpl.overlays[k] = vc - } - - tmpl.MarkReady() - - return nil -} - -func (t *GoHTMLTemplate) initFuncs(d *deps.Deps) { - - t.funcster = newTemplateFuncster(d) - - // The URL funcs in the funcMap is somewhat language dependent, - // so we need to wait until the language and site config is loaded. - t.funcster.initFuncMap() - - t.amberFuncMap = template.FuncMap{} - - amberMu.Lock() - for k, v := range amber.FuncMap { - t.amberFuncMap[k] = v - } - - for k, v := range t.funcster.funcMap { - t.amberFuncMap[k] = v - // Hacky, but we need to make sure that the func names are in the global map. - amber.FuncMap[k] = func() string { - panic("should never be invoked") - } - } - amberMu.Unlock() - -} - -func (t *GoHTMLTemplate) Funcs(funcMap template.FuncMap) { - t.Template.Funcs(funcMap) -} - -func (t *GoHTMLTemplate) Partial(name string, contextList ...interface{}) template.HTML { - if strings.HasPrefix("partials/", name) { - name = name[8:] - } - var context interface{} - - if len(contextList) == 0 { - context = nil - } else { - context = contextList[0] - } - return t.ExecuteTemplateToHTML(context, "partials/"+name, "theme/partials/"+name) -} - -func (t *GoHTMLTemplate) executeTemplate(context interface{}, w io.Writer, layouts ...string) { - var worked bool - for _, layout := range layouts { - templ := t.Lookup(layout) - if templ == nil { - layout += ".html" - templ = t.Lookup(layout) - } - - if templ != nil { - if err := templ.Execute(w, context); err != nil { - helpers.DistinctErrorLog.Println(layout, err) - } - worked = true - break - } - } - if !worked { - t.Log.ERROR.Println("Unable to render", layouts) - t.Log.ERROR.Println("Expecting to find a template in either the theme/layouts or /layouts in one of the following relative locations", layouts) - } -} - -func (t *GoHTMLTemplate) ExecuteTemplateToHTML(context interface{}, layouts ...string) template.HTML { - b := bp.GetBuffer() - defer bp.PutBuffer(b) - t.executeTemplate(context, b, layouts...) - return template.HTML(b.String()) -} - -func (t *GoHTMLTemplate) Lookup(name string) *template.Template { - - if templ := t.Template.Lookup(name); templ != nil { - return templ - } - - if t.overlays != nil { - if templ, ok := t.overlays[name]; ok { - return templ - } - } - - // The clone is used for the non-renderable HTML pages (p.IsRenderable == false) that is parsed - // as Go templates late in the build process. - if t.clone != nil { - if templ := t.clone.Lookup(name); templ != nil { - return templ - } - } - - return nil - -} - -func (t *GoHTMLTemplate) GetClone() *template.Template { - return t.clone -} - -func (t *GoHTMLTemplate) LoadEmbedded() { - t.EmbedShortcodes() - t.EmbedTemplates() -} - -// MarkReady marks the template as "ready for execution". No changes allowed -// after this is set. -func (t *GoHTMLTemplate) MarkReady() { - if t.clone == nil { - t.clone = template.Must(t.Template.Clone()) - } -} - -func (t *GoHTMLTemplate) checkState() { - if t.clone != nil { - panic("template is cloned and cannot be modfified") - } -} - -func (t *GoHTMLTemplate) AddInternalTemplate(prefix, name, tpl string) error { - if prefix != "" { - return t.AddTemplate("_internal/"+prefix+"/"+name, tpl) - } - return t.AddTemplate("_internal/"+name, tpl) -} - -func (t *GoHTMLTemplate) AddInternalShortcode(name, content string) error { - return t.AddInternalTemplate("shortcodes", name, content) -} - -func (t *GoHTMLTemplate) AddTemplate(name, tpl string) error { - t.checkState() - templ, err := t.New(name).Parse(tpl) - if err != nil { - t.errors = append(t.errors, &templateErr{name: name, err: err}) - return err - } - if err := applyTemplateTransformers(templ); err != nil { - return err - } - - return nil -} - -func (t *GoHTMLTemplate) AddTemplateFileWithMaster(name, overlayFilename, masterFilename string) error { - - // There is currently no known way to associate a cloned template with an existing one. - // This funky master/overlay design will hopefully improve in a future version of Go. - // - // Simplicity is hard. - // - // Until then we'll have to live with this hackery. - // - // See https://github.com/golang/go/issues/14285 - // - // So, to do minimum amount of changes to get this to work: - // - // 1. Lookup or Parse the master - // 2. Parse and store the overlay in a separate map - - masterTpl := t.Lookup(masterFilename) - - if masterTpl == nil { - b, err := afero.ReadFile(t.Fs.Source, masterFilename) - if err != nil { - return err - } - masterTpl, err = t.New(masterFilename).Parse(string(b)) - - if err != nil { - // TODO(bep) Add a method that does this - t.errors = append(t.errors, &templateErr{name: name, err: err}) - return err - } - } - - b, err := afero.ReadFile(t.Fs.Source, overlayFilename) - if err != nil { - return err - } - - overlayTpl, err := template.Must(masterTpl.Clone()).Parse(string(b)) - if err != nil { - t.errors = append(t.errors, &templateErr{name: name, err: err}) - } else { - // The extra lookup is a workaround, see - // * https://github.com/golang/go/issues/16101 - // * https://github.com/spf13/hugo/issues/2549 - overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) - if err := applyTemplateTransformers(overlayTpl); err != nil { - return err - } - t.overlays[name] = overlayTpl - } - - return err -} - -func (t *GoHTMLTemplate) AddAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error { - t.checkState() - var base, inner *ace.File - name = name[:len(name)-len(filepath.Ext(innerPath))] + ".html" - - // Fixes issue #1178 - basePath = strings.Replace(basePath, "\\", "/", -1) - innerPath = strings.Replace(innerPath, "\\", "/", -1) - - if basePath != "" { - base = ace.NewFile(basePath, baseContent) - inner = ace.NewFile(innerPath, innerContent) - } else { - base = ace.NewFile(innerPath, innerContent) - inner = ace.NewFile("", []byte{}) - } - parsed, err := ace.ParseSource(ace.NewSource(base, inner, []*ace.File{}), nil) - if err != nil { - t.errors = append(t.errors, &templateErr{name: name, err: err}) - return err - } - templ, err := ace.CompileResultWithTemplate(t.New(name), parsed, nil) - if err != nil { - t.errors = append(t.errors, &templateErr{name: name, err: err}) - return err - } - return applyTemplateTransformers(templ) -} - -func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) error { - t.checkState() - // get the suffix and switch on that - ext := filepath.Ext(path) - switch ext { - case ".amber": - templateName := strings.TrimSuffix(name, filepath.Ext(name)) + ".html" - b, err := afero.ReadFile(t.Fs.Source, path) - - if err != nil { - return err - } - - amberMu.Lock() - templ, err := t.CompileAmberWithTemplate(b, path, t.New(templateName)) - amberMu.Unlock() - if err != nil { - return err - } - - return applyTemplateTransformers(templ) - case ".ace": - var innerContent, baseContent []byte - innerContent, err := afero.ReadFile(t.Fs.Source, path) - - if err != nil { - return err - } - - if baseTemplatePath != "" { - baseContent, err = afero.ReadFile(t.Fs.Source, baseTemplatePath) - if err != nil { - return err - } - } - - return t.AddAceTemplate(name, baseTemplatePath, path, baseContent, innerContent) - default: - - if baseTemplatePath != "" { - return t.AddTemplateFileWithMaster(name, path, baseTemplatePath) - } - - b, err := afero.ReadFile(t.Fs.Source, path) - - if err != nil { - return err - } - - t.Log.DEBUG.Printf("Add template file from path %s", path) - - return t.AddTemplate(name, string(b)) - } - -} - -func (t *GoHTMLTemplate) GenerateTemplateNameFrom(base, path string) string { - name, _ := filepath.Rel(base, path) - return filepath.ToSlash(name) -} - -func isDotFile(path string) bool { - return filepath.Base(path)[0] == '.' -} - -func isBackupFile(path string) bool { - return path[len(path)-1] == '~' -} - -const baseFileBase = "baseof" - -var aceTemplateInnerMarkers = [][]byte{[]byte("= content")} -var goTemplateInnerMarkers = [][]byte{[]byte("{{define"), []byte("{{ define")} - -func isBaseTemplate(path string) bool { - return strings.Contains(path, baseFileBase) -} - -func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { - t.Log.DEBUG.Printf("Load templates from path %q prefix %q", absPath, prefix) - walker := func(path string, fi os.FileInfo, err error) error { - if err != nil { - return nil - } - t.Log.DEBUG.Println("Template path", path) - if fi.Mode()&os.ModeSymlink == os.ModeSymlink { - link, err := filepath.EvalSymlinks(absPath) - if err != nil { - t.Log.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", absPath, err) - return nil - } - linkfi, err := t.Fs.Source.Stat(link) - if err != nil { - t.Log.ERROR.Printf("Cannot stat '%s', error was: %s", link, err) - return nil - } - if !linkfi.Mode().IsRegular() { - t.Log.ERROR.Printf("Symbolic links for directories not supported, skipping '%s'", absPath) - } - return nil - } - - if !fi.IsDir() { - if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) { - return nil - } - - tplName := t.GenerateTemplateNameFrom(absPath, path) - - if prefix != "" { - tplName = strings.Trim(prefix, "/") + "/" + tplName - } - - var baseTemplatePath string - - // Ace and Go templates may have both a base and inner template. - pathDir := filepath.Dir(path) - if filepath.Ext(path) != ".amber" && !strings.HasSuffix(pathDir, "partials") && !strings.HasSuffix(pathDir, "shortcodes") { - - innerMarkers := goTemplateInnerMarkers - baseFileName := fmt.Sprintf("%s.html", baseFileBase) - - if filepath.Ext(path) == ".ace" { - innerMarkers = aceTemplateInnerMarkers - baseFileName = fmt.Sprintf("%s.ace", baseFileBase) - } - - // This may be a view that shouldn't have base template - // Have to look inside it to make sure - needsBase, err := helpers.FileContainsAny(path, innerMarkers, t.Fs.Source) - if err != nil { - return err - } - if needsBase { - - layoutDir := t.PathSpec.GetLayoutDirPath() - currBaseFilename := fmt.Sprintf("%s-%s", helpers.Filename(path), baseFileName) - templateDir := filepath.Dir(path) - themeDir := filepath.Join(t.PathSpec.GetThemeDir()) - relativeThemeLayoutsDir := filepath.Join(t.PathSpec.GetRelativeThemeDir(), "layouts") - - var baseTemplatedDir string - - if strings.HasPrefix(templateDir, relativeThemeLayoutsDir) { - baseTemplatedDir = strings.TrimPrefix(templateDir, relativeThemeLayoutsDir) - } else { - baseTemplatedDir = strings.TrimPrefix(templateDir, layoutDir) - } - - baseTemplatedDir = strings.TrimPrefix(baseTemplatedDir, helpers.FilePathSeparator) - - // Look for base template in the follwing order: - // 1. /-baseof., e.g. list-baseof.. - // 2. /baseof. - // 3. _default/-baseof., e.g. list-baseof.. - // 4. _default/baseof. - // For each of the steps above, it will first look in the project, then, if theme is set, - // in the theme's layouts folder. - - pairsToCheck := [][]string{ - []string{baseTemplatedDir, currBaseFilename}, - []string{baseTemplatedDir, baseFileName}, - []string{"_default", currBaseFilename}, - []string{"_default", baseFileName}, - } - - Loop: - for _, pair := range pairsToCheck { - pathsToCheck := basePathsToCheck(pair, layoutDir, themeDir) - for _, pathToCheck := range pathsToCheck { - if ok, err := helpers.Exists(pathToCheck, t.Fs.Source); err == nil && ok { - baseTemplatePath = pathToCheck - break Loop - } - } - } - } - } - - if err := t.AddTemplateFile(tplName, baseTemplatePath, path); err != nil { - t.Log.ERROR.Printf("Failed to add template %s in path %s: %s", tplName, path, err) - } - - } - return nil - } - if err := helpers.SymbolicWalk(t.Fs.Source, absPath, walker); err != nil { - t.Log.ERROR.Printf("Failed to load templates: %s", err) - } -} - -func basePathsToCheck(path []string, layoutDir, themeDir string) []string { - // Always look in the project. - pathsToCheck := []string{filepath.Join((append([]string{layoutDir}, path...))...)} - - // May have a theme - if themeDir != "" { - pathsToCheck = append(pathsToCheck, filepath.Join((append([]string{themeDir, "layouts"}, path...))...)) - } - - return pathsToCheck - -} - -func (t *GoHTMLTemplate) LoadTemplatesWithPrefix(absPath string, prefix string) { - t.loadTemplates(absPath, prefix) -} - -func (t *GoHTMLTemplate) LoadTemplates(absPath string) { - t.loadTemplates(absPath, "") -} - -func (t *GoHTMLTemplate) PrintErrors() { - for i, e := range t.errors { - t.Log.ERROR.Println(i, ":", e.err) - } +// TODO(bep) make smaller +type Template interface { + ExecuteTemplate(wr io.Writer, name string, data interface{}) error + ExecuteTemplateToHTML(context interface{}, layouts ...string) template.HTML + Lookup(name string) *template.Template + Templates() []*template.Template + New(name string) *template.Template + GetClone() *template.Template + LoadTemplates(absPath string) + LoadTemplatesWithPrefix(absPath, prefix string) + AddTemplate(name, tpl string) error + AddTemplateFileWithMaster(name, overlayFilename, masterFilename string) error + AddAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error + AddInternalTemplate(prefix, name, tpl string) error + AddInternalShortcode(name, tpl string) error + Partial(name string, contextList ...interface{}) template.HTML + PrintErrors() + Funcs(funcMap template.FuncMap) + MarkReady() } diff --git a/tpl/template_ast_transformers.go b/tpl/template_ast_transformers.go deleted file mode 100644 index 19b772ad..00000000 --- a/tpl/template_ast_transformers.go +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright 2016 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 tpl - -import ( - "errors" - "html/template" - "strings" - "text/template/parse" -) - -// decl keeps track of the variable mappings, i.e. $mysite => .Site etc. -type decl map[string]string - -var paramsPaths = [][]string{ - {"Params"}, - {"Site", "Params"}, - - // Site and Pag referenced from shortcodes - {"Page", "Site", "Params"}, - {"Page", "Params"}, - - {"Site", "Language", "Params"}, -} - -type templateContext struct { - decl decl - templ *template.Template -} - -func newTemplateContext(templ *template.Template) *templateContext { - return &templateContext{templ: templ, decl: make(map[string]string)} - -} - -func applyTemplateTransformers(templ *template.Template) error { - if templ == nil || templ.Tree == nil { - return errors.New("expected template, but none provided") - } - - c := newTemplateContext(templ) - - c.paramsKeysToLower(templ.Tree.Root) - - return nil -} - -// paramsKeysToLower is made purposely non-generic to make it not so tempting -// to do more of these hard-to-maintain AST transformations. -func (c *templateContext) paramsKeysToLower(n parse.Node) { - - switch x := n.(type) { - case *parse.ListNode: - if x != nil { - c.paramsKeysToLowerForNodes(x.Nodes...) - } - case *parse.ActionNode: - c.paramsKeysToLowerForNodes(x.Pipe) - case *parse.IfNode: - c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) - case *parse.WithNode: - c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) - case *parse.RangeNode: - c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) - case *parse.TemplateNode: - subTempl := c.templ.Lookup(x.Name) - if subTempl != nil { - c.paramsKeysToLowerForNodes(subTempl.Tree.Root) - } - case *parse.PipeNode: - for i, elem := range x.Decl { - if len(x.Cmds) > i { - // maps $site => .Site etc. - c.decl[elem.Ident[0]] = x.Cmds[i].String() - } - } - - for _, cmd := range x.Cmds { - c.paramsKeysToLower(cmd) - } - - case *parse.CommandNode: - for _, elem := range x.Args { - switch an := elem.(type) { - case *parse.FieldNode: - c.updateIdentsIfNeeded(an.Ident) - case *parse.VariableNode: - c.updateIdentsIfNeeded(an.Ident) - case *parse.PipeNode: - c.paramsKeysToLower(an) - } - - } - } -} - -func (c *templateContext) paramsKeysToLowerForNodes(nodes ...parse.Node) { - for _, node := range nodes { - c.paramsKeysToLower(node) - } -} - -func (c *templateContext) updateIdentsIfNeeded(idents []string) { - index := c.decl.indexOfReplacementStart(idents) - - if index == -1 { - return - } - - for i := index; i < len(idents); i++ { - idents[i] = strings.ToLower(idents[i]) - } -} - -// indexOfReplacementStart will return the index of where to start doing replacement, -// -1 if none needed. -func (d decl) indexOfReplacementStart(idents []string) int { - - l := len(idents) - - if l == 0 { - return -1 - } - - first := idents[0] - firstIsVar := first[0] == '$' - - if l == 1 && !firstIsVar { - // This can not be a Params.x - return -1 - } - - if !firstIsVar { - found := false - for _, paramsPath := range paramsPaths { - if first == paramsPath[0] { - found = true - break - } - } - if !found { - return -1 - } - } - - var ( - resolvedIdents []string - replacements []string - replaced []string - ) - - // An Ident can start out as one of - // [Params] [$blue] [$colors.Blue] - // We need to resolve the variables, so - // $blue => [Params Colors Blue] - // etc. - replacements = []string{idents[0]} - - // Loop until there are no more $vars to resolve. - for i := 0; i < len(replacements); i++ { - - if i > 20 { - // bail out - return -1 - } - - potentialVar := replacements[i] - - if potentialVar == "$" { - continue - } - - if potentialVar == "" || potentialVar[0] != '$' { - // leave it as is - replaced = append(replaced, strings.Split(potentialVar, ".")...) - continue - } - - replacement, ok := d[potentialVar] - - if !ok { - // Temporary range vars. We do not care about those. - return -1 - } - - replacement = strings.TrimPrefix(replacement, ".") - - if replacement == "" { - continue - } - - if replacement[0] == '$' { - // Needs further expansion - replacements = append(replacements, strings.Split(replacement, ".")...) - } else { - replaced = append(replaced, strings.Split(replacement, ".")...) - } - } - - resolvedIdents = append(replaced, idents[1:]...) - - for _, paramPath := range paramsPaths { - if index := indexOfFirstRealIdentAfterWords(resolvedIdents, idents, paramPath...); index != -1 { - return index - } - } - - return -1 - -} - -func indexOfFirstRealIdentAfterWords(resolvedIdents, idents []string, words ...string) int { - if !sliceStartsWith(resolvedIdents, words...) { - return -1 - } - - for i, ident := range idents { - if ident == "" || ident[0] == '$' { - continue - } - found := true - for _, word := range words { - if ident == word { - found = false - break - } - } - if found { - return i - } - } - - return -1 -} - -func sliceStartsWith(slice []string, words ...string) bool { - - if len(slice) < len(words) { - return false - } - - for i, word := range words { - if word != slice[i] { - return false - } - } - return true -} diff --git a/tpl/template_ast_transformers_test.go b/tpl/template_ast_transformers_test.go deleted file mode 100644 index 43d78284..00000000 --- a/tpl/template_ast_transformers_test.go +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright 2016 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 tpl - -import ( - "bytes" - "testing" - - "html/template" - - "github.com/stretchr/testify/require" -) - -var ( - testFuncs = map[string]interface{}{ - "Echo": func(v interface{}) interface{} { return v }, - } - - paramsData = map[string]interface{}{ - "NotParam": "Hi There", - "Slice": []int{1, 3}, - "Params": map[string]interface{}{ - "lower": "P1L", - }, - "Site": map[string]interface{}{ - "Params": map[string]interface{}{ - "lower": "P2L", - "slice": []int{1, 3}, - }, - "Language": map[string]interface{}{ - "Params": map[string]interface{}{ - "lower": "P22L", - }, - }, - "Data": map[string]interface{}{ - "Params": map[string]interface{}{ - "NOLOW": "P3H", - }, - }, - }, - } - - paramsTempl = ` -{{ $page := . }} -{{ $pageParams := .Params }} -{{ $site := .Site }} -{{ $siteParams := .Site.Params }} -{{ $data := .Site.Data }} -{{ $notparam := .NotParam }} - -P1: {{ .Params.LOWER }} -P1_2: {{ $.Params.LOWER }} -P1_3: {{ $page.Params.LOWER }} -P1_4: {{ $pageParams.LOWER }} -P2: {{ .Site.Params.LOWER }} -P2_2: {{ $.Site.Params.LOWER }} -P2_3: {{ $site.Params.LOWER }} -P2_4: {{ $siteParams.LOWER }} -P22: {{ .Site.Language.Params.LOWER }} -P3: {{ .Site.Data.Params.NOLOW }} -P3_2: {{ $.Site.Data.Params.NOLOW }} -P3_3: {{ $site.Data.Params.NOLOW }} -P3_4: {{ $data.Params.NOLOW }} -P4: {{ range $i, $e := .Site.Params.SLICE }}{{ $e }}{{ end }} -P5: {{ Echo .Params.LOWER }} -P5_2: {{ Echo $site.Params.LOWER }} -{{ if .Params.LOWER }} -IF: {{ .Params.LOWER }} -{{ end }} -{{ if .Params.NOT_EXIST }} -{{ else }} -ELSE: {{ .Params.LOWER }} -{{ end }} - - -{{ with .Params.LOWER }} -WITH: {{ . }} -{{ end }} - - -{{ range .Slice }} -RANGE: {{ . }}: {{ $.Params.LOWER }} -{{ end }} -{{ index .Slice 1 }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ $notparam }} - - -{{ $lower := .Site.Params.LOWER }} -F1: {{ printf "themes/%s-theme" .Site.Params.LOWER }} -F2: {{ Echo (printf "themes/%s-theme" $lower) }} -F3: {{ Echo (printf "themes/%s-theme" .Site.Params.LOWER) }} -` -) - -func TestParamsKeysToLower(t *testing.T) { - t.Parallel() - - require.Error(t, applyTemplateTransformers(nil)) - - templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl) - - require.NoError(t, err) - - c := newTemplateContext(templ) - - require.Equal(t, -1, c.decl.indexOfReplacementStart([]string{})) - - c.paramsKeysToLower(templ.Tree.Root) - - var b bytes.Buffer - - require.NoError(t, templ.Execute(&b, paramsData)) - - result := b.String() - - require.Contains(t, result, "P1: P1L") - require.Contains(t, result, "P1_2: P1L") - require.Contains(t, result, "P1_3: P1L") - require.Contains(t, result, "P1_4: P1L") - require.Contains(t, result, "P2: P2L") - require.Contains(t, result, "P2_2: P2L") - require.Contains(t, result, "P2_3: P2L") - require.Contains(t, result, "P2_4: P2L") - require.Contains(t, result, "P22: P22L") - require.Contains(t, result, "P3: P3H") - require.Contains(t, result, "P3_2: P3H") - require.Contains(t, result, "P3_3: P3H") - require.Contains(t, result, "P3_4: P3H") - require.Contains(t, result, "P4: 13") - require.Contains(t, result, "P5: P1L") - require.Contains(t, result, "P5_2: P2L") - - require.Contains(t, result, "IF: P1L") - require.Contains(t, result, "ELSE: P1L") - - require.Contains(t, result, "WITH: P1L") - - require.Contains(t, result, "RANGE: 3: P1L") - - require.Contains(t, result, "Hi There") - - // Issue #2740 - require.Contains(t, result, "F1: themes/P2L-theme") - require.Contains(t, result, "F2: themes/P2L-theme") - require.Contains(t, result, "F3: themes/P2L-theme") - -} - -func BenchmarkTemplateParamsKeysToLower(b *testing.B) { - templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl) - - if err != nil { - b.Fatal(err) - } - - templates := make([]*template.Template, b.N) - - for i := 0; i < b.N; i++ { - templates[i], err = templ.Clone() - if err != nil { - b.Fatal(err) - } - } - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - c := newTemplateContext(templates[i]) - c.paramsKeysToLower(templ.Tree.Root) - } -} - -func TestParamsKeysToLowerVars(t *testing.T) { - t.Parallel() - var ( - ctx = map[string]interface{}{ - "Params": map[string]interface{}{ - "colors": map[string]interface{}{ - "blue": "Amber", - }, - }, - } - - // This is how Amber behaves: - paramsTempl = ` -{{$__amber_1 := .Params.Colors}} -{{$__amber_2 := $__amber_1.Blue}} -Color: {{$__amber_2}} -Blue: {{ $__amber_1.Blue}} -` - ) - - templ, err := template.New("foo").Parse(paramsTempl) - - require.NoError(t, err) - - c := newTemplateContext(templ) - - c.paramsKeysToLower(templ.Tree.Root) - - var b bytes.Buffer - - require.NoError(t, templ.Execute(&b, ctx)) - - result := b.String() - - require.Contains(t, result, "Color: Amber") - -} - -func TestParamsKeysToLowerInBlockTemplate(t *testing.T) { - t.Parallel() - - var ( - ctx = map[string]interface{}{ - "Params": map[string]interface{}{ - "lower": "P1L", - }, - } - - master = ` -P1: {{ .Params.LOWER }} -{{ block "main" . }}DEFAULT{{ end }}` - overlay = ` -{{ define "main" }} -P2: {{ .Params.LOWER }} -{{ end }}` - ) - - masterTpl, err := template.New("foo").Parse(master) - require.NoError(t, err) - - overlayTpl, err := template.Must(masterTpl.Clone()).Parse(overlay) - require.NoError(t, err) - overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) - - c := newTemplateContext(overlayTpl) - - c.paramsKeysToLower(overlayTpl.Tree.Root) - - var b bytes.Buffer - - require.NoError(t, overlayTpl.Execute(&b, ctx)) - - result := b.String() - - require.Contains(t, result, "P1: P1L") - require.Contains(t, result, "P2: P1L") -} diff --git a/tpl/template_embedded.go b/tpl/template_embedded.go deleted file mode 100644 index f782a31e..00000000 --- a/tpl/template_embedded.go +++ /dev/null @@ -1,266 +0,0 @@ -// Copyright 2015 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package tpl - -type Tmpl struct { - Name string - Data string -} - -func (t *GoHTMLTemplate) EmbedShortcodes() { - t.AddInternalShortcode("ref.html", `{{ .Get 0 | ref .Page }}`) - t.AddInternalShortcode("relref.html", `{{ .Get 0 | relref .Page }}`) - t.AddInternalShortcode("highlight.html", `{{ if len .Params | eq 2 }}{{ highlight .Inner (.Get 0) (.Get 1) }}{{ else }}{{ highlight .Inner (.Get 0) "" }}{{ end }}`) - t.AddInternalShortcode("test.html", `This is a simple Test`) - t.AddInternalShortcode("figure.html", ` -
- {{ with .Get "link"}}{{ end }} - - {{ if .Get "link"}}{{ end }} - {{ if or (or (.Get "title") (.Get "caption")) (.Get "attr")}} -
{{ if isset .Params "title" }} -

{{ .Get "title" }}

{{ end }} - {{ if or (.Get "caption") (.Get "attr")}}

- {{ .Get "caption" }} - {{ with .Get "attrlink"}} {{ end }} - {{ .Get "attr" }} - {{ if .Get "attrlink"}} {{ end }} -

{{ end }} -
- {{ end }} -
-`) - t.AddInternalShortcode("speakerdeck.html", "") - t.AddInternalShortcode("youtube.html", `{{ if .IsNamedParams }} -
- -
{{ else }} -
- -
-{{ end }}`) - t.AddInternalShortcode("vimeo.html", `{{ if .IsNamedParams }}
- -
{{ else }} -
- -
-{{ end }}`) - t.AddInternalShortcode("gist.html", ``) - t.AddInternalShortcode("tweet.html", `{{ (getJSON "https://api.twitter.com/1/statuses/oembed.json?id=" (index .Params 0)).html | safeHTML }}`) - t.AddInternalShortcode("instagram.html", `{{ if len .Params | eq 2 }}{{ if eq (.Get 1) "hidecaption" }}{{ (getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=1").html | safeHTML }}{{ end }}{{ else }}{{ (getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=0").html | safeHTML }}{{ end }}`) -} - -func (t *GoHTMLTemplate) EmbedTemplates() { - - t.AddInternalTemplate("_default", "rss.xml", ` - - {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }} - {{ .Permalink }} - Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }} - Hugo -- gohugo.io{{ with .Site.LanguageCode }} - {{.}}{{end}}{{ with .Site.Author.email }} - {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Author.email }} - {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Copyright }} - {{.}}{{end}}{{ if not .Date.IsZero }} - {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }} - - {{ range first 15 .Data.Pages }} - - {{ .Title }} - {{ .Permalink }} - {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }} - {{ with .Site.Author.email }}{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}} - {{ .Permalink }} - {{ .Content | html }} - - {{ end }} - -`) - - t.AddInternalTemplate("_default", "sitemap.xml", ` - {{ range .Data.Pages }} - - {{ .Permalink }}{{ if not .Lastmod.IsZero }} - {{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}{{ end }}{{ with .Sitemap.ChangeFreq }} - {{ . }}{{ end }}{{ if ge .Sitemap.Priority 0.0 }} - {{ .Sitemap.Priority }}{{ end }} - - {{ end }} -`) - - // For multilanguage sites - t.AddInternalTemplate("_default", "sitemapindex.xml", ` - {{ range . }} - - {{ .SitemapAbsURL }} - {{ if not .LastChange.IsZero }} - {{ .LastChange.Format "2006-01-02T15:04:05-07:00" | safeHTML }} - {{ end }} - - {{ end }} - -`) - - t.AddInternalTemplate("", "pagination.html", `{{ $pag := $.Paginator }} - {{ if gt $pag.TotalPages 1 }} -
    - {{ with $pag.First }} -
  • - -
  • - {{ end }} -
  • - -
  • - {{ range $pag.Pagers }} -
  • {{ .PageNumber }}
  • - {{ end }} -
  • - -
  • - {{ with $pag.Last }} -
  • - -
  • - {{ end }} -
- {{ end }}`) - - t.AddInternalTemplate("", "disqus.html", `{{ if .Site.DisqusShortname }}
- - -comments powered by Disqus{{end}}`) - - // Add SEO & Social metadata - t.AddInternalTemplate("", "opengraph.html", ` - - - -{{ with .Params.images }}{{ range first 6 . }} - -{{ end }}{{ end }} - -{{ if .IsPage }} -{{ if not .PublishDate.IsZero }} -{{ else if not .Date.IsZero }}{{ end }} -{{ if not .Lastmod.IsZero }}{{ end }} -{{ else }} -{{ if not .Date.IsZero }}{{ end }} -{{ end }}{{ with .Params.audio }} -{{ end }}{{ with .Params.locale }} -{{ end }}{{ with .Site.Params.title }} -{{ end }}{{ with .Params.videos }} -{{ range .Params.videos }} - -{{ end }}{{ end }} - - -{{ $permalink := .Permalink }} -{{ $siteSeries := .Site.Taxonomies.series }}{{ with .Params.series }} -{{ range $name := . }} - {{ $series := index $siteSeries $name }} - {{ range $page := first 6 $series.Pages }} - {{ if ne $page.Permalink $permalink }}{{ end }} - {{ end }} -{{ end }}{{ end }} - -{{ if .IsPage }} -{{ range .Site.Authors }}{{ with .Social.facebook }} -{{ end }}{{ with .Site.Social.facebook }} -{{ end }} - -{{ with .Params.tags }}{{ range first 6 . }} - {{ end }}{{ end }} -{{ end }}{{ end }} - - -{{ with .Site.Social.facebook_admin }}{{ end }}`) - - t.AddInternalTemplate("", "twitter_cards.html", `{{ if .IsPage }} -{{ with .Params.images }} - - - -{{ else }} - -{{ end }} - - - - -{{ with .Site.Social.twitter }}{{ end }} -{{ with .Site.Social.twitter_domain }}{{ end }} -{{ range .Site.Authors }} - {{ with .twitter }}{{ end }} -{{ end }}{{ end }}`) - - t.AddInternalTemplate("", "google_news.html", `{{ if .IsPage }}{{ with .Params.news_keywords }} - -{{ end }}{{ end }}`) - - t.AddInternalTemplate("", "schema.html", `{{ with .Site.Social.GooglePlus }}{{ end }} - - - -{{if .IsPage}}{{ $ISO8601 := "2006-01-02T15:04:05-07:00" }}{{ if not .PublishDate.IsZero }} -{{ end }} -{{ if not .Date.IsZero }}{{ end }} - -{{ with .Params.images }}{{ range first 6 . }} - -{{ end }}{{ end }} - - - -{{ end }}`) - - t.AddInternalTemplate("", "google_analytics.html", `{{ with .Site.GoogleAnalytics }} - -{{ end }}`) - - t.AddInternalTemplate("", "google_analytics_async.html", `{{ with .Site.GoogleAnalytics }} - - -{{ end }}`) - - t.AddInternalTemplate("_default", "robots.txt", "User-agent: *") -} diff --git a/tpl/template_func_truncate.go b/tpl/template_func_truncate.go deleted file mode 100644 index b5886eda..00000000 --- a/tpl/template_func_truncate.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2016 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 tpl - -import ( - "errors" - "html" - "html/template" - "regexp" - "unicode" - "unicode/utf8" - - "github.com/spf13/cast" -) - -var ( - tagRE = regexp.MustCompile(`^<(/)?([^ ]+?)(?:(\s*/)| .*?)?>`) - htmlSinglets = map[string]bool{ - "br": true, "col": true, "link": true, - "base": true, "img": true, "param": true, - "area": true, "hr": true, "input": true, - } -) - -type htmlTag struct { - name string - pos int - openTag bool -} - -func truncate(a interface{}, options ...interface{}) (template.HTML, error) { - length, err := cast.ToIntE(a) - if err != nil { - return "", err - } - var textParam interface{} - var ellipsis string - - switch len(options) { - case 0: - return "", errors.New("truncate requires a length and a string") - case 1: - textParam = options[0] - ellipsis = " …" - case 2: - textParam = options[1] - ellipsis, err = cast.ToStringE(options[0]) - if err != nil { - return "", errors.New("ellipsis must be a string") - } - if _, ok := options[0].(template.HTML); !ok { - ellipsis = html.EscapeString(ellipsis) - } - default: - return "", errors.New("too many arguments passed to truncate") - } - if err != nil { - return "", errors.New("text to truncate must be a string") - } - text, err := cast.ToStringE(textParam) - if err != nil { - return "", errors.New("text must be a string") - } - - _, isHTML := textParam.(template.HTML) - - if utf8.RuneCountInString(text) <= length { - if isHTML { - return template.HTML(text), nil - } - return template.HTML(html.EscapeString(text)), nil - } - - tags := []htmlTag{} - var lastWordIndex, lastNonSpace, currentLen, endTextPos, nextTag int - - for i, r := range text { - if i < nextTag { - continue - } - - if isHTML { - // Make sure we keep tag of HTML tags - slice := text[i:] - m := tagRE.FindStringSubmatchIndex(slice) - if len(m) > 0 && m[0] == 0 { - nextTag = i + m[1] - tagname := slice[m[4]:m[5]] - lastWordIndex = lastNonSpace - _, singlet := htmlSinglets[tagname] - if !singlet && m[6] == -1 { - tags = append(tags, htmlTag{name: tagname, pos: i, openTag: m[2] == -1}) - } - - continue - } - } - - currentLen++ - if unicode.IsSpace(r) { - lastWordIndex = lastNonSpace - } else if unicode.In(r, unicode.Han, unicode.Hangul, unicode.Hiragana, unicode.Katakana) { - lastWordIndex = i - } else { - lastNonSpace = i + utf8.RuneLen(r) - } - - if currentLen > length { - if lastWordIndex == 0 { - endTextPos = i - } else { - endTextPos = lastWordIndex - } - out := text[0:endTextPos] - if isHTML { - out += ellipsis - // Close out any open HTML tags - var currentTag *htmlTag - for i := len(tags) - 1; i >= 0; i-- { - tag := tags[i] - if tag.pos >= endTextPos || currentTag != nil { - if currentTag != nil && currentTag.name == tag.name { - currentTag = nil - } - continue - } - - if tag.openTag { - out += ("") - } else { - currentTag = &tag - } - } - - return template.HTML(out), nil - } - return template.HTML(html.EscapeString(out) + ellipsis), nil - } - } - - if isHTML { - return template.HTML(text), nil - } - return template.HTML(html.EscapeString(text)), nil -} diff --git a/tpl/template_func_truncate_test.go b/tpl/template_func_truncate_test.go deleted file mode 100644 index 9213c6fa..00000000 --- a/tpl/template_func_truncate_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2016 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 tpl - -import ( - "html/template" - "reflect" - "strings" - "testing" -) - -func TestTruncate(t *testing.T) { - t.Parallel() - var err error - cases := []struct { - v1 interface{} - v2 interface{} - v3 interface{} - want interface{} - isErr bool - }{ - {10, "I am a test sentence", nil, template.HTML("I am a …"), false}, - {10, "", "I am a test sentence", template.HTML("I am a"), false}, - {10, "", "a b c d e f g h i j k", template.HTML("a b c d e"), false}, - {12, "", "Should be escaped", template.HTML("<b>Should be"), false}, - {10, template.HTML(" Read more"), "I am a test sentence", template.HTML("I am a Read more"), false}, - {20, template.HTML("I have a Markdown link inside."), nil, template.HTML("I have a Markdown …"), false}, - {10, "IamanextremelylongwordthatjustgoesonandonandonjusttoannoyyoualmostasifIwaswritteninGermanActuallyIbettheresagermanwordforthis", nil, template.HTML("Iamanextre …"), false}, - {10, template.HTML("

IamanextremelylongwordthatjustgoesonandonandonjusttoannoyyoualmostasifIwaswritteninGermanActuallyIbettheresagermanwordforthis

"), nil, template.HTML("

Iamanextre …

"), false}, - {13, template.HTML("With Markdown inside."), nil, template.HTML("With Markdown …"), false}, - {14, "Hello中国 Good 好的", nil, template.HTML("Hello中国 Good 好 …"), false}, - {15, "", template.HTML("A
tag that's not closed"), template.HTML("A
tag that's"), false}, - {14, template.HTML("

Hello中国 Good 好的

"), nil, template.HTML("

Hello中国 Good 好 …

"), false}, - {2, template.HTML("

P1

P2

"), nil, template.HTML("

P1 …

"), false}, - {3, template.HTML(strings.Repeat("

P

", 20)), nil, template.HTML("

P

P

P …

"), false}, - {18, template.HTML("

test hello test something

"), nil, template.HTML("

test hello test …

"), false}, - {4, template.HTML("

abc d e

"), nil, template.HTML("

abc …

"), false}, - {10, nil, nil, template.HTML(""), true}, - {nil, nil, nil, template.HTML(""), true}, - } - for i, c := range cases { - var result template.HTML - if c.v2 == nil { - result, err = truncate(c.v1) - } else if c.v3 == nil { - result, err = truncate(c.v1, c.v2) - } else { - result, err = truncate(c.v1, c.v2, c.v3) - } - - if c.isErr { - if err == nil { - t.Errorf("[%d] Slice didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, c.want) { - t.Errorf("[%d] got '%s' but expected '%s'", i, result, c.want) - } - } - } - - // Too many arguments - _, err = truncate(10, " ...", "I am a test sentence", "wrong") - if err == nil { - t.Errorf("Should have errored") - } - -} diff --git a/tpl/template_funcs.go b/tpl/template_funcs.go deleted file mode 100644 index 9777bf61..00000000 --- a/tpl/template_funcs.go +++ /dev/null @@ -1,2217 +0,0 @@ -// Copyright 2016 The Hugo Authors. All rights reserved. -// -// Portions Copyright The Go Authors. - -// 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 tpl - -import ( - "bytes" - _md5 "crypto/md5" - _sha1 "crypto/sha1" - _sha256 "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "html" - "html/template" - "image" - "math/rand" - "net/url" - "os" - "reflect" - "regexp" - "sort" - "strconv" - "strings" - "sync" - "time" - "unicode/utf8" - - "github.com/bep/inflect" - "github.com/spf13/afero" - "github.com/spf13/cast" - "github.com/spf13/hugo/deps" - "github.com/spf13/hugo/helpers" - jww "github.com/spf13/jwalterweatherman" - - // Importing image codecs for image.DecodeConfig - _ "image/gif" - _ "image/jpeg" - _ "image/png" -) - -// Some of the template funcs are'nt entirely stateless. -type templateFuncster struct { - funcMap template.FuncMap - cachedPartials partialCache - *deps.Deps -} - -func newTemplateFuncster(deps *deps.Deps) *templateFuncster { - return &templateFuncster{ - Deps: deps, - cachedPartials: partialCache{p: make(map[string]template.HTML)}, - } -} - -// eq returns the boolean truth of arg1 == arg2. -func eq(x, y interface{}) bool { - normalize := func(v interface{}) interface{} { - vv := reflect.ValueOf(v) - switch vv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return vv.Int() - case reflect.Float32, reflect.Float64: - return vv.Float() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return vv.Uint() - default: - return v - } - } - x = normalize(x) - y = normalize(y) - return reflect.DeepEqual(x, y) -} - -// ne returns the boolean truth of arg1 != arg2. -func ne(x, y interface{}) bool { - return !eq(x, y) -} - -// ge returns the boolean truth of arg1 >= arg2. -func ge(a, b interface{}) bool { - left, right := compareGetFloat(a, b) - return left >= right -} - -// gt returns the boolean truth of arg1 > arg2. -func gt(a, b interface{}) bool { - left, right := compareGetFloat(a, b) - return left > right -} - -// le returns the boolean truth of arg1 <= arg2. -func le(a, b interface{}) bool { - left, right := compareGetFloat(a, b) - return left <= right -} - -// lt returns the boolean truth of arg1 < arg2. -func lt(a, b interface{}) bool { - left, right := compareGetFloat(a, b) - return left < right -} - -// dictionary creates a map[string]interface{} from the given parameters by -// walking the parameters and treating them as key-value pairs. The number -// of parameters must be even. -func dictionary(values ...interface{}) (map[string]interface{}, error) { - if len(values)%2 != 0 { - return nil, errors.New("invalid dict call") - } - dict := make(map[string]interface{}, len(values)/2) - for i := 0; i < len(values); i += 2 { - key, ok := values[i].(string) - if !ok { - return nil, errors.New("dict keys must be strings") - } - dict[key] = values[i+1] - } - return dict, nil -} - -// slice returns a slice of all passed arguments -func slice(args ...interface{}) []interface{} { - return args -} - -func compareGetFloat(a interface{}, b interface{}) (float64, float64) { - var left, right float64 - var leftStr, rightStr *string - av := reflect.ValueOf(a) - - switch av.Kind() { - case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: - left = float64(av.Len()) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - left = float64(av.Int()) - case reflect.Float32, reflect.Float64: - left = av.Float() - case reflect.String: - var err error - left, err = strconv.ParseFloat(av.String(), 64) - if err != nil { - str := av.String() - leftStr = &str - } - case reflect.Struct: - switch av.Type() { - case timeType: - left = float64(toTimeUnix(av)) - } - } - - bv := reflect.ValueOf(b) - - switch bv.Kind() { - case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: - right = float64(bv.Len()) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - right = float64(bv.Int()) - case reflect.Float32, reflect.Float64: - right = bv.Float() - case reflect.String: - var err error - right, err = strconv.ParseFloat(bv.String(), 64) - if err != nil { - str := bv.String() - rightStr = &str - } - case reflect.Struct: - switch bv.Type() { - case timeType: - right = float64(toTimeUnix(bv)) - } - } - - switch { - case leftStr == nil || rightStr == nil: - case *leftStr < *rightStr: - return 0, 1 - case *leftStr > *rightStr: - return 1, 0 - default: - return 0, 0 - } - - return left, right -} - -// slicestr slices a string by specifying a half-open range with -// two indices, start and end. 1 and 4 creates a slice including elements 1 through 3. -// The end index can be omitted, it defaults to the string's length. -func slicestr(a interface{}, startEnd ...interface{}) (string, error) { - aStr, err := cast.ToStringE(a) - if err != nil { - return "", err - } - - var argStart, argEnd int - - argNum := len(startEnd) - - if argNum > 0 { - if argStart, err = cast.ToIntE(startEnd[0]); err != nil { - return "", errors.New("start argument must be integer") - } - } - if argNum > 1 { - if argEnd, err = cast.ToIntE(startEnd[1]); err != nil { - return "", errors.New("end argument must be integer") - } - } - - if argNum > 2 { - return "", errors.New("too many arguments") - } - - asRunes := []rune(aStr) - - if argNum > 0 && (argStart < 0 || argStart >= len(asRunes)) { - return "", errors.New("slice bounds out of range") - } - - if argNum == 2 { - if argEnd < 0 || argEnd > len(asRunes) { - return "", errors.New("slice bounds out of range") - } - return string(asRunes[argStart:argEnd]), nil - } else if argNum == 1 { - return string(asRunes[argStart:]), nil - } else { - return string(asRunes[:]), nil - } - -} - -// hasPrefix tests whether the input s begins with prefix. -func hasPrefix(s, prefix interface{}) (bool, error) { - ss, err := cast.ToStringE(s) - if err != nil { - return false, err - } - - sp, err := cast.ToStringE(prefix) - if err != nil { - return false, err - } - - return strings.HasPrefix(ss, sp), nil -} - -// substr extracts parts of a string, beginning at the character at the specified -// position, and returns the specified number of characters. -// -// It normally takes two parameters: start and length. -// It can also take one parameter: start, i.e. length is omitted, in which case -// the substring starting from start until the end of the string will be returned. -// -// To extract characters from the end of the string, use a negative start number. -// -// In addition, borrowing from the extended behavior described at http://php.net/substr, -// if length is given and is negative, then that many characters will be omitted from -// the end of string. -func substr(a interface{}, nums ...interface{}) (string, error) { - aStr, err := cast.ToStringE(a) - if err != nil { - return "", err - } - - var start, length int - - asRunes := []rune(aStr) - - switch len(nums) { - case 0: - return "", errors.New("too less arguments") - case 1: - if start, err = cast.ToIntE(nums[0]); err != nil { - return "", errors.New("start argument must be integer") - } - length = len(asRunes) - case 2: - if start, err = cast.ToIntE(nums[0]); err != nil { - return "", errors.New("start argument must be integer") - } - if length, err = cast.ToIntE(nums[1]); err != nil { - return "", errors.New("length argument must be integer") - } - default: - return "", errors.New("too many arguments") - } - - if start < -len(asRunes) { - start = 0 - } - if start > len(asRunes) { - return "", fmt.Errorf("start position out of bounds for %d-byte string", len(aStr)) - } - - var s, e int - if start >= 0 && length >= 0 { - s = start - e = start + length - } else if start < 0 && length >= 0 { - s = len(asRunes) + start - length + 1 - e = len(asRunes) + start + 1 - } else if start >= 0 && length < 0 { - s = start - e = len(asRunes) + length - } else { - s = len(asRunes) + start - e = len(asRunes) + length - } - - if s > e { - return "", fmt.Errorf("calculated start position greater than end position: %d > %d", s, e) - } - if e > len(asRunes) { - e = len(asRunes) - } - - return string(asRunes[s:e]), nil -} - -// split slices an input string into all substrings separated by delimiter. -func split(a interface{}, delimiter string) ([]string, error) { - aStr, err := cast.ToStringE(a) - if err != nil { - return []string{}, err - } - return strings.Split(aStr, delimiter), nil -} - -// intersect returns the common elements in the given sets, l1 and l2. l1 and -// l2 must be of the same type and may be either arrays or slices. -func intersect(l1, l2 interface{}) (interface{}, error) { - if l1 == nil || l2 == nil { - return make([]interface{}, 0), nil - } - - l1v := reflect.ValueOf(l1) - l2v := reflect.ValueOf(l2) - - switch l1v.Kind() { - case reflect.Array, reflect.Slice: - switch l2v.Kind() { - case reflect.Array, reflect.Slice: - r := reflect.MakeSlice(l1v.Type(), 0, 0) - for i := 0; i < l1v.Len(); i++ { - l1vv := l1v.Index(i) - for j := 0; j < l2v.Len(); j++ { - l2vv := l2v.Index(j) - switch l1vv.Kind() { - case reflect.String: - if l1vv.Type() == l2vv.Type() && l1vv.String() == l2vv.String() && !in(r.Interface(), l2vv.Interface()) { - r = reflect.Append(r, l2vv) - } - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - switch l2vv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if l1vv.Int() == l2vv.Int() && !in(r.Interface(), l2vv.Interface()) { - r = reflect.Append(r, l2vv) - } - } - case reflect.Float32, reflect.Float64: - switch l2vv.Kind() { - case reflect.Float32, reflect.Float64: - if l1vv.Float() == l2vv.Float() && !in(r.Interface(), l2vv.Interface()) { - r = reflect.Append(r, l2vv) - } - } - } - } - } - return r.Interface(), nil - default: - return nil, errors.New("can't iterate over " + reflect.ValueOf(l2).Type().String()) - } - default: - return nil, errors.New("can't iterate over " + reflect.ValueOf(l1).Type().String()) - } -} - -// ResetCaches resets all caches that might be used during build. -// TODO(bep) globals move image config cache to funcster -func ResetCaches() { - resetImageConfigCache() -} - -// imageConfigCache is a lockable cache for image.Config objects. It must be -// locked before reading or writing to config. -type imageConfigCache struct { - config map[string]image.Config - sync.RWMutex -} - -var defaultImageConfigCache = imageConfigCache{ - config: map[string]image.Config{}, -} - -// resetImageConfigCache initializes and resets the imageConfig cache for the -// imageConfig template function. This should be run once before every batch of -// template renderers so the cache is cleared for new data. -func resetImageConfigCache() { - defaultImageConfigCache.Lock() - defer defaultImageConfigCache.Unlock() - - defaultImageConfigCache.config = map[string]image.Config{} -} - -// imageConfig returns the image.Config for the specified path relative to the -// working directory. resetImageConfigCache must be run beforehand. -func (t *templateFuncster) imageConfig(path interface{}) (image.Config, error) { - filename, err := cast.ToStringE(path) - if err != nil { - return image.Config{}, err - } - - if filename == "" { - return image.Config{}, errors.New("imageConfig needs a filename") - } - - // Check cache for image config. - defaultImageConfigCache.RLock() - config, ok := defaultImageConfigCache.config[filename] - defaultImageConfigCache.RUnlock() - - if ok { - return config, nil - } - - f, err := t.Fs.WorkingDir.Open(filename) - if err != nil { - return image.Config{}, err - } - - config, _, err = image.DecodeConfig(f) - - defaultImageConfigCache.Lock() - defaultImageConfigCache.config[filename] = config - defaultImageConfigCache.Unlock() - - return config, err -} - -// in returns whether v is in the set l. l may be an array or slice. -func in(l interface{}, v interface{}) bool { - lv := reflect.ValueOf(l) - vv := reflect.ValueOf(v) - - switch lv.Kind() { - case reflect.Array, reflect.Slice: - for i := 0; i < lv.Len(); i++ { - lvv := lv.Index(i) - lvv, isNil := indirect(lvv) - if isNil { - continue - } - switch lvv.Kind() { - case reflect.String: - if vv.Type() == lvv.Type() && vv.String() == lvv.String() { - return true - } - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - switch vv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if vv.Int() == lvv.Int() { - return true - } - } - case reflect.Float32, reflect.Float64: - switch vv.Kind() { - case reflect.Float32, reflect.Float64: - if vv.Float() == lvv.Float() { - return true - } - } - } - } - case reflect.String: - if vv.Type() == lv.Type() && strings.Contains(lv.String(), vv.String()) { - return true - } - } - return false -} - -// first returns the first N items in a rangeable list. -func first(limit interface{}, seq interface{}) (interface{}, error) { - if limit == nil || seq == nil { - return nil, errors.New("both limit and seq must be provided") - } - - limitv, err := cast.ToIntE(limit) - - if err != nil { - return nil, err - } - - if limitv < 1 { - return nil, errors.New("can't return negative/empty count of items from sequence") - } - - seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) - if isNil { - return nil, errors.New("can't iterate over a nil value") - } - - switch seqv.Kind() { - case reflect.Array, reflect.Slice, reflect.String: - // okay - default: - return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) - } - if limitv > seqv.Len() { - limitv = seqv.Len() - } - return seqv.Slice(0, limitv).Interface(), nil -} - -// findRE returns a list of strings that match the regular expression. By default all matches -// will be included. The number of matches can be limited with an optional third parameter. -func findRE(expr string, content interface{}, limit ...interface{}) ([]string, error) { - re, err := reCache.Get(expr) - if err != nil { - return nil, err - } - - conv, err := cast.ToStringE(content) - if err != nil { - return nil, err - } - - if len(limit) == 0 { - return re.FindAllString(conv, -1), nil - } - - lim, err := cast.ToIntE(limit[0]) - if err != nil { - return nil, err - } - - return re.FindAllString(conv, lim), nil -} - -// last returns the last N items in a rangeable list. -func last(limit interface{}, seq interface{}) (interface{}, error) { - if limit == nil || seq == nil { - return nil, errors.New("both limit and seq must be provided") - } - - limitv, err := cast.ToIntE(limit) - - if err != nil { - return nil, err - } - - if limitv < 1 { - return nil, errors.New("can't return negative/empty count of items from sequence") - } - - seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) - if isNil { - return nil, errors.New("can't iterate over a nil value") - } - - switch seqv.Kind() { - case reflect.Array, reflect.Slice, reflect.String: - // okay - default: - return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) - } - if limitv > seqv.Len() { - limitv = seqv.Len() - } - return seqv.Slice(seqv.Len()-limitv, seqv.Len()).Interface(), nil -} - -// after returns all the items after the first N in a rangeable list. -func after(index interface{}, seq interface{}) (interface{}, error) { - if index == nil || seq == nil { - return nil, errors.New("both limit and seq must be provided") - } - - indexv, err := cast.ToIntE(index) - - if err != nil { - return nil, err - } - - if indexv < 1 { - return nil, errors.New("can't return negative/empty count of items from sequence") - } - - seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) - if isNil { - return nil, errors.New("can't iterate over a nil value") - } - - switch seqv.Kind() { - case reflect.Array, reflect.Slice, reflect.String: - // okay - default: - return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) - } - if indexv >= seqv.Len() { - return nil, errors.New("no items left") - } - return seqv.Slice(indexv, seqv.Len()).Interface(), nil -} - -// shuffle returns the given rangeable list in a randomised order. -func shuffle(seq interface{}) (interface{}, error) { - if seq == nil { - return nil, errors.New("both count and seq must be provided") - } - - seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) - if isNil { - return nil, errors.New("can't iterate over a nil value") - } - - switch seqv.Kind() { - case reflect.Array, reflect.Slice, reflect.String: - // okay - default: - return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) - } - - shuffled := reflect.MakeSlice(reflect.TypeOf(seq), seqv.Len(), seqv.Len()) - - rand.Seed(time.Now().UTC().UnixNano()) - randomIndices := rand.Perm(seqv.Len()) - - for index, value := range randomIndices { - shuffled.Index(value).Set(seqv.Index(index)) - } - - return shuffled.Interface(), nil -} - -func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error) { - if !obj.IsValid() { - return zero, errors.New("can't evaluate an invalid value") - } - typ := obj.Type() - obj, isNil := indirect(obj) - - // first, check whether obj has a method. In this case, obj is - // an interface, a struct or its pointer. If obj is a struct, - // to check all T and *T method, use obj pointer type Value - objPtr := obj - if objPtr.Kind() != reflect.Interface && objPtr.CanAddr() { - objPtr = objPtr.Addr() - } - mt, ok := objPtr.Type().MethodByName(elemName) - if ok { - if mt.PkgPath != "" { - return zero, fmt.Errorf("%s is an unexported method of type %s", elemName, typ) - } - // struct pointer has one receiver argument and interface doesn't have an argument - if mt.Type.NumIn() > 1 || mt.Type.NumOut() == 0 || mt.Type.NumOut() > 2 { - return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) - } - if mt.Type.NumOut() == 1 && mt.Type.Out(0).Implements(errorType) { - return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) - } - if mt.Type.NumOut() == 2 && !mt.Type.Out(1).Implements(errorType) { - return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) - } - res := objPtr.Method(mt.Index).Call([]reflect.Value{}) - if len(res) == 2 && !res[1].IsNil() { - return zero, fmt.Errorf("error at calling a method %s of type %s: %s", elemName, typ, res[1].Interface().(error)) - } - return res[0], nil - } - - // elemName isn't a method so next start to check whether it is - // a struct field or a map value. In both cases, it mustn't be - // a nil value - if isNil { - return zero, fmt.Errorf("can't evaluate a nil pointer of type %s by a struct field or map key name %s", typ, elemName) - } - switch obj.Kind() { - case reflect.Struct: - ft, ok := obj.Type().FieldByName(elemName) - if ok { - if ft.PkgPath != "" && !ft.Anonymous { - return zero, fmt.Errorf("%s is an unexported field of struct type %s", elemName, typ) - } - return obj.FieldByIndex(ft.Index), nil - } - return zero, fmt.Errorf("%s isn't a field of struct type %s", elemName, typ) - case reflect.Map: - kv := reflect.ValueOf(elemName) - if kv.Type().AssignableTo(obj.Type().Key()) { - return obj.MapIndex(kv), nil - } - return zero, fmt.Errorf("%s isn't a key of map type %s", elemName, typ) - } - return zero, fmt.Errorf("%s is neither a struct field, a method nor a map element of type %s", elemName, typ) -} - -func checkCondition(v, mv reflect.Value, op string) (bool, error) { - v, vIsNil := indirect(v) - if !v.IsValid() { - vIsNil = true - } - mv, mvIsNil := indirect(mv) - if !mv.IsValid() { - mvIsNil = true - } - if vIsNil || mvIsNil { - switch op { - case "", "=", "==", "eq": - return vIsNil == mvIsNil, nil - case "!=", "<>", "ne": - return vIsNil != mvIsNil, nil - } - return false, nil - } - - if v.Kind() == reflect.Bool && mv.Kind() == reflect.Bool { - switch op { - case "", "=", "==", "eq": - return v.Bool() == mv.Bool(), nil - case "!=", "<>", "ne": - return v.Bool() != mv.Bool(), nil - } - return false, nil - } - - var ivp, imvp *int64 - var svp, smvp *string - var slv, slmv interface{} - var ima []int64 - var sma []string - if mv.Type() == v.Type() { - switch v.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - iv := v.Int() - ivp = &iv - imv := mv.Int() - imvp = &imv - case reflect.String: - sv := v.String() - svp = &sv - smv := mv.String() - smvp = &smv - case reflect.Struct: - switch v.Type() { - case timeType: - iv := toTimeUnix(v) - ivp = &iv - imv := toTimeUnix(mv) - imvp = &imv - } - case reflect.Array, reflect.Slice: - slv = v.Interface() - slmv = mv.Interface() - } - } else { - if mv.Kind() != reflect.Array && mv.Kind() != reflect.Slice { - return false, nil - } - - if mv.Len() == 0 { - return false, nil - } - - if v.Kind() != reflect.Interface && mv.Type().Elem().Kind() != reflect.Interface && mv.Type().Elem() != v.Type() { - return false, nil - } - switch v.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - iv := v.Int() - ivp = &iv - for i := 0; i < mv.Len(); i++ { - if anInt := toInt(mv.Index(i)); anInt != -1 { - ima = append(ima, anInt) - } - - } - case reflect.String: - sv := v.String() - svp = &sv - for i := 0; i < mv.Len(); i++ { - if aString := toString(mv.Index(i)); aString != "" { - sma = append(sma, aString) - } - } - case reflect.Struct: - switch v.Type() { - case timeType: - iv := toTimeUnix(v) - ivp = &iv - for i := 0; i < mv.Len(); i++ { - ima = append(ima, toTimeUnix(mv.Index(i))) - } - } - } - } - - switch op { - case "", "=", "==", "eq": - if ivp != nil && imvp != nil { - return *ivp == *imvp, nil - } else if svp != nil && smvp != nil { - return *svp == *smvp, nil - } - case "!=", "<>", "ne": - if ivp != nil && imvp != nil { - return *ivp != *imvp, nil - } else if svp != nil && smvp != nil { - return *svp != *smvp, nil - } - case ">=", "ge": - if ivp != nil && imvp != nil { - return *ivp >= *imvp, nil - } else if svp != nil && smvp != nil { - return *svp >= *smvp, nil - } - case ">", "gt": - if ivp != nil && imvp != nil { - return *ivp > *imvp, nil - } else if svp != nil && smvp != nil { - return *svp > *smvp, nil - } - case "<=", "le": - if ivp != nil && imvp != nil { - return *ivp <= *imvp, nil - } else if svp != nil && smvp != nil { - return *svp <= *smvp, nil - } - case "<", "lt": - if ivp != nil && imvp != nil { - return *ivp < *imvp, nil - } else if svp != nil && smvp != nil { - return *svp < *smvp, nil - } - case "in", "not in": - var r bool - if ivp != nil && len(ima) > 0 { - r = in(ima, *ivp) - } else if svp != nil { - if len(sma) > 0 { - r = in(sma, *svp) - } else if smvp != nil { - r = in(*smvp, *svp) - } - } else { - return false, nil - } - if op == "not in" { - return !r, nil - } - return r, nil - case "intersect": - r, err := intersect(slv, slmv) - if err != nil { - return false, err - } - - if reflect.TypeOf(r).Kind() == reflect.Slice { - s := reflect.ValueOf(r) - - if s.Len() > 0 { - return true, nil - } - return false, nil - } - return false, errors.New("invalid intersect values") - default: - return false, errors.New("no such operator") - } - return false, nil -} - -// parseWhereArgs parses the end arguments to the where function. Return a -// match value and an operator, if one is defined. -func parseWhereArgs(args ...interface{}) (mv reflect.Value, op string, err error) { - switch len(args) { - case 1: - mv = reflect.ValueOf(args[0]) - case 2: - var ok bool - if op, ok = args[0].(string); !ok { - err = errors.New("operator argument must be string type") - return - } - op = strings.TrimSpace(strings.ToLower(op)) - mv = reflect.ValueOf(args[1]) - default: - err = errors.New("can't evaluate the array by no match argument or more than or equal to two arguments") - } - return -} - -// checkWhereArray handles the where-matching logic when the seqv value is an -// Array or Slice. -func checkWhereArray(seqv, kv, mv reflect.Value, path []string, op string) (interface{}, error) { - rv := reflect.MakeSlice(seqv.Type(), 0, 0) - for i := 0; i < seqv.Len(); i++ { - var vvv reflect.Value - rvv := seqv.Index(i) - if kv.Kind() == reflect.String { - vvv = rvv - for _, elemName := range path { - var err error - vvv, err = evaluateSubElem(vvv, elemName) - if err != nil { - return nil, err - } - } - } else { - vv, _ := indirect(rvv) - if vv.Kind() == reflect.Map && kv.Type().AssignableTo(vv.Type().Key()) { - vvv = vv.MapIndex(kv) - } - } - - if ok, err := checkCondition(vvv, mv, op); ok { - rv = reflect.Append(rv, rvv) - } else if err != nil { - return nil, err - } - } - return rv.Interface(), nil -} - -// checkWhereMap handles the where-matching logic when the seqv value is a Map. -func checkWhereMap(seqv, kv, mv reflect.Value, path []string, op string) (interface{}, error) { - rv := reflect.MakeMap(seqv.Type()) - keys := seqv.MapKeys() - for _, k := range keys { - elemv := seqv.MapIndex(k) - switch elemv.Kind() { - case reflect.Array, reflect.Slice: - r, err := checkWhereArray(elemv, kv, mv, path, op) - if err != nil { - return nil, err - } - - switch rr := reflect.ValueOf(r); rr.Kind() { - case reflect.Slice: - if rr.Len() > 0 { - rv.SetMapIndex(k, elemv) - } - } - case reflect.Interface: - elemvv, isNil := indirect(elemv) - if isNil { - continue - } - - switch elemvv.Kind() { - case reflect.Array, reflect.Slice: - r, err := checkWhereArray(elemvv, kv, mv, path, op) - if err != nil { - return nil, err - } - - switch rr := reflect.ValueOf(r); rr.Kind() { - case reflect.Slice: - if rr.Len() > 0 { - rv.SetMapIndex(k, elemv) - } - } - } - } - } - return rv.Interface(), nil -} - -// where returns a filtered subset of a given data type. -func where(seq, key interface{}, args ...interface{}) (interface{}, error) { - seqv, isNil := indirect(reflect.ValueOf(seq)) - if isNil { - return nil, errors.New("can't iterate over a nil value of type " + reflect.ValueOf(seq).Type().String()) - } - - mv, op, err := parseWhereArgs(args...) - if err != nil { - return nil, err - } - - var path []string - kv := reflect.ValueOf(key) - if kv.Kind() == reflect.String { - path = strings.Split(strings.Trim(kv.String(), "."), ".") - } - - switch seqv.Kind() { - case reflect.Array, reflect.Slice: - return checkWhereArray(seqv, kv, mv, path, op) - case reflect.Map: - return checkWhereMap(seqv, kv, mv, path, op) - default: - return nil, fmt.Errorf("can't iterate over %v", seq) - } -} - -// apply takes a map, array, or slice and returns a new slice with the function fname applied over it. -func (t *templateFuncster) apply(seq interface{}, fname string, args ...interface{}) (interface{}, error) { - if seq == nil { - return make([]interface{}, 0), nil - } - - if fname == "apply" { - return nil, errors.New("can't apply myself (no turtles allowed)") - } - - seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) - if isNil { - return nil, errors.New("can't iterate over a nil value") - } - - fn, found := t.funcMap[fname] - if !found { - return nil, errors.New("can't find function " + fname) - } - - fnv := reflect.ValueOf(fn) - - switch seqv.Kind() { - case reflect.Array, reflect.Slice: - r := make([]interface{}, seqv.Len()) - for i := 0; i < seqv.Len(); i++ { - vv := seqv.Index(i) - - vvv, err := applyFnToThis(fnv, vv, args...) - - if err != nil { - return nil, err - } - - r[i] = vvv.Interface() - } - - return r, nil - default: - return nil, fmt.Errorf("can't apply over %v", seq) - } -} - -func applyFnToThis(fn, this reflect.Value, args ...interface{}) (reflect.Value, error) { - n := make([]reflect.Value, len(args)) - for i, arg := range args { - if arg == "." { - n[i] = this - } else { - n[i] = reflect.ValueOf(arg) - } - } - - num := fn.Type().NumIn() - - if fn.Type().IsVariadic() { - num-- - } - - // TODO(bep) see #1098 - also see template_tests.go - /*if len(args) < num { - return reflect.ValueOf(nil), errors.New("Too few arguments") - } else if len(args) > num { - return reflect.ValueOf(nil), errors.New("Too many arguments") - }*/ - - for i := 0; i < num; i++ { - if xt, targ := n[i].Type(), fn.Type().In(i); !xt.AssignableTo(targ) { - return reflect.ValueOf(nil), errors.New("called apply using " + xt.String() + " as type " + targ.String()) - } - } - - res := fn.Call(n) - - if len(res) == 1 || res[1].IsNil() { - return res[0], nil - } - return reflect.ValueOf(nil), res[1].Interface().(error) -} - -// delimit takes a given sequence and returns a delimited HTML string. -// If last is passed to the function, it will be used as the final delimiter. -func delimit(seq, delimiter interface{}, last ...interface{}) (template.HTML, error) { - d, err := cast.ToStringE(delimiter) - if err != nil { - return "", err - } - - var dLast *string - if len(last) > 0 { - l := last[0] - dStr, err := cast.ToStringE(l) - if err != nil { - dLast = nil - } - dLast = &dStr - } - - seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) - if isNil { - return "", errors.New("can't iterate over a nil value") - } - - var str string - switch seqv.Kind() { - case reflect.Map: - sortSeq, err := sortSeq(seq) - if err != nil { - return "", err - } - seqv = reflect.ValueOf(sortSeq) - fallthrough - case reflect.Array, reflect.Slice, reflect.String: - for i := 0; i < seqv.Len(); i++ { - val := seqv.Index(i).Interface() - valStr, err := cast.ToStringE(val) - if err != nil { - continue - } - switch { - case i == seqv.Len()-2 && dLast != nil: - str += valStr + *dLast - case i == seqv.Len()-1: - str += valStr - default: - str += valStr + d - } - } - - default: - return "", fmt.Errorf("can't iterate over %v", seq) - } - - return template.HTML(str), nil -} - -// sortSeq returns a sorted sequence. -func sortSeq(seq interface{}, args ...interface{}) (interface{}, error) { - if seq == nil { - return nil, errors.New("sequence must be provided") - } - - seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) - if isNil { - return nil, errors.New("can't iterate over a nil value") - } - - switch seqv.Kind() { - case reflect.Array, reflect.Slice, reflect.Map: - // ok - default: - return nil, errors.New("can't sort " + reflect.ValueOf(seq).Type().String()) - } - - // Create a list of pairs that will be used to do the sort - p := pairList{SortAsc: true, SliceType: reflect.SliceOf(seqv.Type().Elem())} - p.Pairs = make([]pair, seqv.Len()) - - var sortByField string - for i, l := range args { - dStr, err := cast.ToStringE(l) - switch { - case i == 0 && err != nil: - sortByField = "" - case i == 0 && err == nil: - sortByField = dStr - case i == 1 && err == nil && dStr == "desc": - p.SortAsc = false - case i == 1: - p.SortAsc = true - } - } - path := strings.Split(strings.Trim(sortByField, "."), ".") - - switch seqv.Kind() { - case reflect.Array, reflect.Slice: - for i := 0; i < seqv.Len(); i++ { - p.Pairs[i].Value = seqv.Index(i) - if sortByField == "" || sortByField == "value" { - p.Pairs[i].Key = p.Pairs[i].Value - } else { - v := p.Pairs[i].Value - var err error - for _, elemName := range path { - v, err = evaluateSubElem(v, elemName) - if err != nil { - return nil, err - } - } - p.Pairs[i].Key = v - } - } - - case reflect.Map: - keys := seqv.MapKeys() - for i := 0; i < seqv.Len(); i++ { - p.Pairs[i].Value = seqv.MapIndex(keys[i]) - if sortByField == "" { - p.Pairs[i].Key = keys[i] - } else if sortByField == "value" { - p.Pairs[i].Key = p.Pairs[i].Value - } else { - v := p.Pairs[i].Value - var err error - for _, elemName := range path { - v, err = evaluateSubElem(v, elemName) - if err != nil { - return nil, err - } - } - p.Pairs[i].Key = v - } - } - } - return p.sort(), nil -} - -// Credit for pair sorting method goes to Andrew Gerrand -// https://groups.google.com/forum/#!topic/golang-nuts/FT7cjmcL7gw -// A data structure to hold a key/value pair. -type pair struct { - Key reflect.Value - Value reflect.Value -} - -// A slice of pairs that implements sort.Interface to sort by Value. -type pairList struct { - Pairs []pair - SortAsc bool - SliceType reflect.Type -} - -func (p pairList) Swap(i, j int) { p.Pairs[i], p.Pairs[j] = p.Pairs[j], p.Pairs[i] } -func (p pairList) Len() int { return len(p.Pairs) } -func (p pairList) Less(i, j int) bool { - iv := p.Pairs[i].Key - jv := p.Pairs[j].Key - - if iv.IsValid() { - if jv.IsValid() { - // can only call Interface() on valid reflect Values - return lt(iv.Interface(), jv.Interface()) - } - // if j is invalid, test i against i's zero value - return lt(iv.Interface(), reflect.Zero(iv.Type())) - } - - if jv.IsValid() { - // if i is invalid, test j against j's zero value - return lt(reflect.Zero(jv.Type()), jv.Interface()) - } - - return false -} - -// sorts a pairList and returns a slice of sorted values -func (p pairList) sort() interface{} { - if p.SortAsc { - sort.Sort(p) - } else { - sort.Sort(sort.Reverse(p)) - } - sorted := reflect.MakeSlice(p.SliceType, len(p.Pairs), len(p.Pairs)) - for i, v := range p.Pairs { - sorted.Index(i).Set(v.Value) - } - - return sorted.Interface() -} - -// isSet returns whether a given array, channel, slice, or map has a key -// defined. -func isSet(a interface{}, key interface{}) bool { - av := reflect.ValueOf(a) - kv := reflect.ValueOf(key) - - switch av.Kind() { - case reflect.Array, reflect.Chan, reflect.Slice: - if int64(av.Len()) > kv.Int() { - return true - } - case reflect.Map: - if kv.Type() == av.Type().Key() { - return av.MapIndex(kv).IsValid() - } - } - - return false -} - -// returnWhenSet returns a given value if it set. Otherwise, it returns an -// empty string. -func returnWhenSet(a, k interface{}) interface{} { - av, isNil := indirect(reflect.ValueOf(a)) - if isNil { - return "" - } - - var avv reflect.Value - switch av.Kind() { - case reflect.Array, reflect.Slice: - index, ok := k.(int) - if ok && av.Len() > index { - avv = av.Index(index) - } - case reflect.Map: - kv := reflect.ValueOf(k) - if kv.Type().AssignableTo(av.Type().Key()) { - avv = av.MapIndex(kv) - } - } - - avv, isNil = indirect(avv) - - if isNil { - return "" - } - - if avv.IsValid() { - switch avv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return avv.Int() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return avv.Uint() - case reflect.Float32, reflect.Float64: - return avv.Float() - case reflect.String: - return avv.String() - } - } - - return "" -} - -// highlight returns an HTML string with syntax highlighting applied. -func (t *templateFuncster) highlight(in interface{}, lang, opts string) (template.HTML, error) { - str, err := cast.ToStringE(in) - - if err != nil { - return "", err - } - - return template.HTML(helpers.Highlight(t.Cfg, html.UnescapeString(str), lang, opts)), nil -} - -var markdownTrimPrefix = []byte("

") -var markdownTrimSuffix = []byte("

\n") - -// markdownify renders a given string from Markdown to HTML. -func (t *templateFuncster) markdownify(in interface{}) (template.HTML, error) { - text, err := cast.ToStringE(in) - if err != nil { - return "", err - } - - m := t.ContentSpec.RenderBytes(&helpers.RenderingContext{ - Cfg: t.Cfg, - Content: []byte(text), PageFmt: "markdown"}) - m = bytes.TrimPrefix(m, markdownTrimPrefix) - m = bytes.TrimSuffix(m, markdownTrimSuffix) - return template.HTML(m), nil -} - -// jsonify encodes a given object to JSON. -func jsonify(v interface{}) (template.HTML, error) { - b, err := json.Marshal(v) - if err != nil { - return "", err - } - return template.HTML(b), nil -} - -// emojify "emojifies" the given string. -// -// See http://www.emoji-cheat-sheet.com/ -func emojify(in interface{}) (template.HTML, error) { - str, err := cast.ToStringE(in) - - if err != nil { - return "", err - } - - return template.HTML(helpers.Emojify([]byte(str))), nil -} - -// plainify strips any HTML and returns the plain text version. -func plainify(in interface{}) (string, error) { - s, err := cast.ToStringE(in) - - if err != nil { - return "", err - } - - return helpers.StripHTML(s), nil -} - -func refPage(page interface{}, ref, methodName string) template.HTML { - value := reflect.ValueOf(page) - - method := value.MethodByName(methodName) - - if method.IsValid() && method.Type().NumIn() == 1 && method.Type().NumOut() == 2 { - result := method.Call([]reflect.Value{reflect.ValueOf(ref)}) - - url, err := result[0], result[1] - - if !err.IsNil() { - jww.ERROR.Printf("%s", err.Interface()) - return template.HTML(fmt.Sprintf("%s", err.Interface())) - } - - if url.String() == "" { - jww.ERROR.Printf("ref %s could not be found\n", ref) - return template.HTML(ref) - } - - return template.HTML(url.String()) - } - - jww.ERROR.Printf("Can only create references from Page and Node objects.") - return template.HTML(ref) -} - -// ref returns the absolute URL path to a given content item. -func ref(page interface{}, ref string) template.HTML { - return refPage(page, ref, "Ref") -} - -// relRef returns the relative URL path to a given content item. -func relRef(page interface{}, ref string) template.HTML { - return refPage(page, ref, "RelRef") -} - -// chomp removes trailing newline characters from a string. -func chomp(text interface{}) (template.HTML, error) { - s, err := cast.ToStringE(text) - if err != nil { - return "", err - } - - return template.HTML(strings.TrimRight(s, "\r\n")), nil -} - -// lower returns a copy of the input s with all Unicode letters mapped to their -// lower case. -func lower(s interface{}) (string, error) { - ss, err := cast.ToStringE(s) - if err != nil { - return "", err - } - - return strings.ToLower(ss), nil -} - -// title returns a copy of the input s with all Unicode letters that begin words -// mapped to their title case. -func title(s interface{}) (string, error) { - ss, err := cast.ToStringE(s) - if err != nil { - return "", err - } - - return strings.Title(ss), nil -} - -// upper returns a copy of the input s with all Unicode letters mapped to their -// upper case. -func upper(s interface{}) (string, error) { - ss, err := cast.ToStringE(s) - if err != nil { - return "", err - } - - return strings.ToUpper(ss), nil -} - -// trim leading/trailing characters defined by b from a -func trim(a interface{}, b string) (string, error) { - aStr, err := cast.ToStringE(a) - if err != nil { - return "", err - } - return strings.Trim(aStr, b), nil -} - -// replace all occurrences of b with c in a -func replace(a, b, c interface{}) (string, error) { - aStr, err := cast.ToStringE(a) - if err != nil { - return "", err - } - bStr, err := cast.ToStringE(b) - if err != nil { - return "", err - } - cStr, err := cast.ToStringE(c) - if err != nil { - return "", err - } - return strings.Replace(aStr, bStr, cStr, -1), nil -} - -// partialCache represents a cache of partials protected by a mutex. -type partialCache struct { - sync.RWMutex - p map[string]template.HTML -} - -// Get retrieves partial output from the cache based upon the partial name. -// If the partial is not found in the cache, the partial is rendered and added -// to the cache. -func (t *templateFuncster) Get(key, name string, context interface{}) (p template.HTML) { - var ok bool - - t.cachedPartials.RLock() - p, ok = t.cachedPartials.p[key] - t.cachedPartials.RUnlock() - - if ok { - return p - } - - t.cachedPartials.Lock() - if p, ok = t.cachedPartials.p[key]; !ok { - t.cachedPartials.Unlock() - p = t.Tmpl.Partial(name, context) - - t.cachedPartials.Lock() - t.cachedPartials.p[key] = p - - } - t.cachedPartials.Unlock() - - return p -} - -// partialCached executes and caches partial templates. An optional variant -// string parameter (a string slice actually, but be only use a variadic -// argument to make it optional) can be passed so that a given partial can have -// multiple uses. The cache is created with name+variant as the key. -func (t *templateFuncster) partialCached(name string, context interface{}, variant ...string) template.HTML { - key := name - if len(variant) > 0 { - for i := 0; i < len(variant); i++ { - key += variant[i] - } - } - return t.Get(key, name, context) -} - -// regexpCache represents a cache of regexp objects protected by a mutex. -type regexpCache struct { - mu sync.RWMutex - re map[string]*regexp.Regexp -} - -// Get retrieves a regexp object from the cache based upon the pattern. -// If the pattern is not found in the cache, create one -func (rc *regexpCache) Get(pattern string) (re *regexp.Regexp, err error) { - var ok bool - - if re, ok = rc.get(pattern); !ok { - re, err = regexp.Compile(pattern) - if err != nil { - return nil, err - } - rc.set(pattern, re) - } - - return re, nil -} - -func (rc *regexpCache) get(key string) (re *regexp.Regexp, ok bool) { - rc.mu.RLock() - re, ok = rc.re[key] - rc.mu.RUnlock() - return -} - -func (rc *regexpCache) set(key string, re *regexp.Regexp) { - rc.mu.Lock() - rc.re[key] = re - rc.mu.Unlock() -} - -var reCache = regexpCache{re: make(map[string]*regexp.Regexp)} - -// replaceRE exposes a regular expression replacement function to the templates. -func replaceRE(pattern, repl, src interface{}) (_ string, err error) { - patternStr, err := cast.ToStringE(pattern) - if err != nil { - return - } - - replStr, err := cast.ToStringE(repl) - if err != nil { - return - } - - srcStr, err := cast.ToStringE(src) - if err != nil { - return - } - - re, err := reCache.Get(patternStr) - if err != nil { - return "", err - } - return re.ReplaceAllString(srcStr, replStr), nil -} - -// asTime converts the textual representation of the datetime string into -// a time.Time interface. -func asTime(v interface{}) (interface{}, error) { - t, err := cast.ToTimeE(v) - if err != nil { - return nil, err - } - return t, nil -} - -// dateFormat converts the textual representation of the datetime string into -// the other form or returns it of the time.Time value. These are formatted -// with the layout string -func dateFormat(layout string, v interface{}) (string, error) { - t, err := cast.ToTimeE(v) - if err != nil { - return "", err - } - return t.Format(layout), nil -} - -// dfault checks whether a given value is set and returns a default value if it -// is not. "Set" in this context means non-zero for numeric types and times; -// non-zero length for strings, arrays, slices, and maps; -// any boolean or struct value; or non-nil for any other types. -func dfault(dflt interface{}, given ...interface{}) (interface{}, error) { - // given is variadic because the following construct will not pass a piped - // argument when the key is missing: {{ index . "key" | default "foo" }} - // The Go template will complain that we got 1 argument when we expectd 2. - - if len(given) == 0 { - return dflt, nil - } - if len(given) != 1 { - return nil, fmt.Errorf("wrong number of args for default: want 2 got %d", len(given)+1) - } - - g := reflect.ValueOf(given[0]) - if !g.IsValid() { - return dflt, nil - } - - set := false - - switch g.Kind() { - case reflect.Bool: - set = true - case reflect.String, reflect.Array, reflect.Slice, reflect.Map: - set = g.Len() != 0 - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - set = g.Int() != 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - set = g.Uint() != 0 - case reflect.Float32, reflect.Float64: - set = g.Float() != 0 - case reflect.Complex64, reflect.Complex128: - set = g.Complex() != 0 - case reflect.Struct: - switch actual := given[0].(type) { - case time.Time: - set = !actual.IsZero() - default: - set = true - } - default: - set = !g.IsNil() - } - - if set { - return given[0], nil - } - - return dflt, nil -} - -// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero. -// -// Copied from Go stdlib src/text/template/exec.go. -func canBeNil(typ reflect.Type) bool { - switch typ.Kind() { - case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: - return true - } - return false -} - -// prepareArg checks if value can be used as an argument of type argType, and -// converts an invalid value to appropriate zero if possible. -// -// Copied from Go stdlib src/text/template/funcs.go. -func prepareArg(value reflect.Value, argType reflect.Type) (reflect.Value, error) { - if !value.IsValid() { - if !canBeNil(argType) { - return reflect.Value{}, fmt.Errorf("value is nil; should be of type %s", argType) - } - value = reflect.Zero(argType) - } - if !value.Type().AssignableTo(argType) { - return reflect.Value{}, fmt.Errorf("value has type %s; should be %s", value.Type(), argType) - } - return value, nil -} - -// index returns the result of indexing its first argument by the following -// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each -// indexed item must be a map, slice, or array. -// -// Copied from Go stdlib src/text/template/funcs.go. -// Can hopefully be removed in Go 1.7, see https://github.com/golang/go/issues/14751 -func index(item interface{}, indices ...interface{}) (interface{}, error) { - v := reflect.ValueOf(item) - if !v.IsValid() { - return nil, errors.New("index of untyped nil") - } - for _, i := range indices { - index := reflect.ValueOf(i) - var isNil bool - if v, isNil = indirect(v); isNil { - return nil, errors.New("index of nil pointer") - } - switch v.Kind() { - case reflect.Array, reflect.Slice, reflect.String: - var x int64 - switch index.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - x = index.Int() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - x = int64(index.Uint()) - case reflect.Invalid: - return nil, errors.New("cannot index slice/array with nil") - default: - return nil, fmt.Errorf("cannot index slice/array with type %s", index.Type()) - } - if x < 0 || x >= int64(v.Len()) { - // We deviate from stdlib here. Don't return an error if the - // index is out of range. - return nil, nil - } - v = v.Index(int(x)) - case reflect.Map: - index, err := prepareArg(index, v.Type().Key()) - if err != nil { - return nil, err - } - if x := v.MapIndex(index); x.IsValid() { - v = x - } else { - v = reflect.Zero(v.Type().Elem()) - } - case reflect.Invalid: - // the loop holds invariant: v.IsValid() - panic("unreachable") - default: - return nil, fmt.Errorf("can't index item of type %s", v.Type()) - } - } - return v.Interface(), nil -} - -// readFile reads the file named by filename relative to the given basepath -// and returns the contents as a string. -// There is a upper size limit set at 1 megabytes. -func readFile(fs *afero.BasePathFs, filename string) (string, error) { - if filename == "" { - return "", errors.New("readFile needs a filename") - } - - if info, err := fs.Stat(filename); err == nil { - if info.Size() > 1000000 { - return "", fmt.Errorf("File %q is too big", filename) - } - } else { - return "", err - } - b, err := afero.ReadFile(fs, filename) - - if err != nil { - return "", err - } - - return string(b), nil -} - -// readFileFromWorkingDir reads the file named by filename relative to the -// configured WorkingDir. -// It returns the contents as a string. -// There is a upper size limit set at 1 megabytes. -func (t *templateFuncster) readFileFromWorkingDir(i interface{}) (string, error) { - s, err := cast.ToStringE(i) - if err != nil { - return "", err - } - return readFile(t.Fs.WorkingDir, s) -} - -// readDirFromWorkingDir listst the directory content relative to the -// configured WorkingDir. -func (t *templateFuncster) readDirFromWorkingDir(i interface{}) ([]os.FileInfo, error) { - path, err := cast.ToStringE(i) - if err != nil { - return nil, err - } - - list, err := afero.ReadDir(t.Fs.WorkingDir, path) - - if err != nil { - return nil, fmt.Errorf("Failed to read Directory %s with error message %s", path, err) - } - - return list, nil -} - -// safeHTMLAttr returns a given string as html/template HTMLAttr content. -func safeHTMLAttr(a interface{}) (template.HTMLAttr, error) { - s, err := cast.ToStringE(a) - return template.HTMLAttr(s), err -} - -// safeCSS returns a given string as html/template CSS content. -func safeCSS(a interface{}) (template.CSS, error) { - s, err := cast.ToStringE(a) - return template.CSS(s), err -} - -// safeURL returns a given string as html/template URL content. -func safeURL(a interface{}) (template.URL, error) { - s, err := cast.ToStringE(a) - return template.URL(s), err -} - -// safeHTML returns a given string as html/template HTML content. -func safeHTML(a interface{}) (template.HTML, error) { - s, err := cast.ToStringE(a) - return template.HTML(s), err -} - -// safeJS returns the given string as a html/template JS content. -func safeJS(a interface{}) (template.JS, error) { - s, err := cast.ToStringE(a) - return template.JS(s), err -} - -// mod returns a % b. -func mod(a, b interface{}) (int64, error) { - av := reflect.ValueOf(a) - bv := reflect.ValueOf(b) - var ai, bi int64 - - switch av.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - ai = av.Int() - default: - return 0, errors.New("Modulo operator can't be used with non integer value") - } - - switch bv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - bi = bv.Int() - default: - return 0, errors.New("Modulo operator can't be used with non integer value") - } - - if bi == 0 { - return 0, errors.New("The number can't be divided by zero at modulo operation") - } - - return ai % bi, nil -} - -// modBool returns the boolean of a % b. If a % b == 0, return true. -func modBool(a, b interface{}) (bool, error) { - res, err := mod(a, b) - if err != nil { - return false, err - } - return res == int64(0), nil -} - -// base64Decode returns the base64 decoding of the given content. -func base64Decode(content interface{}) (string, error) { - conv, err := cast.ToStringE(content) - - if err != nil { - return "", err - } - - dec, err := base64.StdEncoding.DecodeString(conv) - - return string(dec), err -} - -// base64Encode returns the base64 encoding of the given content. -func base64Encode(content interface{}) (string, error) { - conv, err := cast.ToStringE(content) - - if err != nil { - return "", err - } - - return base64.StdEncoding.EncodeToString([]byte(conv)), nil -} - -// countWords returns the approximate word count of the given content. -func countWords(content interface{}) (int, error) { - conv, err := cast.ToStringE(content) - - if err != nil { - return 0, fmt.Errorf("Failed to convert content to string: %s", err.Error()) - } - - counter := 0 - for _, word := range strings.Fields(helpers.StripHTML(conv)) { - runeCount := utf8.RuneCountInString(word) - if len(word) == runeCount { - counter++ - } else { - counter += runeCount - } - } - - return counter, nil -} - -// countRunes returns the approximate rune count of the given content. -func countRunes(content interface{}) (int, error) { - conv, err := cast.ToStringE(content) - - if err != nil { - return 0, fmt.Errorf("Failed to convert content to string: %s", err.Error()) - } - - counter := 0 - for _, r := range helpers.StripHTML(conv) { - if !helpers.IsWhitespace(r) { - counter++ - } - } - - return counter, nil -} - -// humanize returns the humanized form of a single parameter. -// If the parameter is either an integer or a string containing an integer -// value, the behavior is to add the appropriate ordinal. -// Example: "my-first-post" -> "My first post" -// Example: "103" -> "103rd" -// Example: 52 -> "52nd" -func humanize(in interface{}) (string, error) { - word, err := cast.ToStringE(in) - if err != nil { - return "", err - } - - if word == "" { - return "", nil - } - - _, ok := in.(int) // original param was literal int value - _, err = strconv.Atoi(word) // original param was string containing an int value - if ok || err == nil { - return inflect.Ordinalize(word), nil - } - return inflect.Humanize(word), nil -} - -// pluralize returns the plural form of a single word. -func pluralize(in interface{}) (string, error) { - word, err := cast.ToStringE(in) - if err != nil { - return "", err - } - return inflect.Pluralize(word), nil -} - -// singularize returns the singular form of a single word. -func singularize(in interface{}) (string, error) { - word, err := cast.ToStringE(in) - if err != nil { - return "", err - } - return inflect.Singularize(word), nil -} - -// md5 hashes the given input and returns its MD5 checksum -func md5(in interface{}) (string, error) { - conv, err := cast.ToStringE(in) - if err != nil { - return "", err - } - - hash := _md5.Sum([]byte(conv)) - return hex.EncodeToString(hash[:]), nil -} - -// sha1 hashes the given input and returns its SHA1 checksum -func sha1(in interface{}) (string, error) { - conv, err := cast.ToStringE(in) - if err != nil { - return "", err - } - - hash := _sha1.Sum([]byte(conv)) - return hex.EncodeToString(hash[:]), nil -} - -// sha256 hashes the given input and returns its SHA256 checksum -func sha256(in interface{}) (string, error) { - conv, err := cast.ToStringE(in) - if err != nil { - return "", err - } - - hash := _sha256.Sum256([]byte(conv)) - return hex.EncodeToString(hash[:]), nil -} - -// querify encodes the given parameters “URL encoded” form ("bar=baz&foo=quux") sorted by key. -func querify(params ...interface{}) (string, error) { - qs := url.Values{} - vals, err := dictionary(params...) - if err != nil { - return "", errors.New("querify keys must be strings") - } - - for name, value := range vals { - qs.Add(name, fmt.Sprintf("%v", value)) - } - - return qs.Encode(), nil -} - -func htmlEscape(in interface{}) (string, error) { - conv, err := cast.ToStringE(in) - if err != nil { - return "", err - } - return html.EscapeString(conv), nil -} - -func htmlUnescape(in interface{}) (string, error) { - conv, err := cast.ToStringE(in) - if err != nil { - return "", err - } - return html.UnescapeString(conv), nil -} - -func (t *templateFuncster) absURL(a interface{}) (template.HTML, error) { - s, err := cast.ToStringE(a) - if err != nil { - return "", nil - } - return template.HTML(t.PathSpec.AbsURL(s, false)), nil -} - -func (t *templateFuncster) relURL(a interface{}) (template.HTML, error) { - s, err := cast.ToStringE(a) - if err != nil { - return "", nil - } - return template.HTML(t.PathSpec.RelURL(s, false)), nil -} - -// getenv retrieves the value of the environment variable named by the key. -// It returns the value, which will be empty if the variable is not present. -func getenv(key interface{}) (string, error) { - skey, err := cast.ToStringE(key) - if err != nil { - return "", nil - } - - return os.Getenv(skey), nil -} - -func (t *templateFuncster) initFuncMap() { - funcMap := template.FuncMap{ - "absURL": t.absURL, - "absLangURL": func(i interface{}) (template.HTML, error) { - s, err := cast.ToStringE(i) - if err != nil { - return "", err - } - return template.HTML(t.PathSpec.AbsURL(s, true)), nil - }, - "add": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '+') }, - "after": after, - "apply": t.apply, - "base64Decode": base64Decode, - "base64Encode": base64Encode, - "chomp": chomp, - "countrunes": countRunes, - "countwords": countWords, - "default": dfault, - "dateFormat": dateFormat, - "delimit": delimit, - "dict": dictionary, - "div": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '/') }, - "echoParam": returnWhenSet, - "emojify": emojify, - "eq": eq, - "findRE": findRE, - "first": first, - "ge": ge, - "getCSV": t.getCSV, - "getJSON": t.getJSON, - "getenv": getenv, - "gt": gt, - "hasPrefix": hasPrefix, - "highlight": t.highlight, - "htmlEscape": htmlEscape, - "htmlUnescape": htmlUnescape, - "humanize": humanize, - "imageConfig": t.imageConfig, - "in": in, - "index": index, - "int": func(v interface{}) (int, error) { return cast.ToIntE(v) }, - "intersect": intersect, - "isSet": isSet, - "isset": isSet, - "jsonify": jsonify, - "last": last, - "le": le, - "lower": lower, - "lt": lt, - "markdownify": t.markdownify, - "md5": md5, - "mod": mod, - "modBool": modBool, - "mul": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '*') }, - "ne": ne, - "now": func() time.Time { return time.Now() }, - "partial": t.Tmpl.Partial, - "partialCached": t.partialCached, - "plainify": plainify, - "pluralize": pluralize, - "querify": querify, - "readDir": t.readDirFromWorkingDir, - "readFile": t.readFileFromWorkingDir, - "ref": ref, - "relURL": t.relURL, - "relLangURL": func(i interface{}) (template.HTML, error) { - s, err := cast.ToStringE(i) - if err != nil { - return "", err - } - return template.HTML(t.PathSpec.RelURL(s, true)), nil - }, - "relref": relRef, - "replace": replace, - "replaceRE": replaceRE, - "safeCSS": safeCSS, - "safeHTML": safeHTML, - "safeHTMLAttr": safeHTMLAttr, - "safeJS": safeJS, - "safeURL": safeURL, - "sanitizeURL": helpers.SanitizeURL, - "sanitizeurl": helpers.SanitizeURL, - "seq": helpers.Seq, - "sha1": sha1, - "sha256": sha256, - "shuffle": shuffle, - "singularize": singularize, - "slice": slice, - "slicestr": slicestr, - "sort": sortSeq, - "split": split, - "string": func(v interface{}) (string, error) { return cast.ToStringE(v) }, - "sub": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '-') }, - "substr": substr, - "title": title, - "time": asTime, - "trim": trim, - "truncate": truncate, - "upper": upper, - "urlize": t.PathSpec.URLize, - "where": where, - "i18n": t.Translate, - "T": t.Translate, - } - - t.funcMap = funcMap - t.Tmpl.Funcs(funcMap) -} diff --git a/tpl/template_funcs_test.go b/tpl/template_funcs_test.go deleted file mode 100644 index 35214b64..00000000 --- a/tpl/template_funcs_test.go +++ /dev/null @@ -1,2993 +0,0 @@ -// Copyright 2016 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 tpl - -import ( - "bytes" - "encoding/base64" - "errors" - "fmt" - "html/template" - "image" - "image/color" - "image/png" - "math/rand" - "path" - "path/filepath" - "reflect" - "runtime" - "strings" - "testing" - "time" - - "github.com/spf13/hugo/tplapi" - - "github.com/spf13/hugo/deps" - "github.com/spf13/hugo/helpers" - - "io/ioutil" - "log" - "os" - - "github.com/spf13/afero" - "github.com/spf13/cast" - "github.com/spf13/hugo/config" - "github.com/spf13/hugo/hugofs" - "github.com/spf13/hugo/i18n" - jww "github.com/spf13/jwalterweatherman" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - logger = jww.NewNotepad(jww.LevelFatal, jww.LevelFatal, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) -) - -func newDepsConfig(cfg config.Provider) deps.DepsCfg { - l := helpers.NewLanguage("en", cfg) - l.Set("i18nDir", "i18n") - return deps.DepsCfg{ - Language: l, - Cfg: cfg, - Fs: hugofs.NewMem(l), - Logger: logger, - TemplateProvider: DefaultTemplateProvider, - TranslationProvider: i18n.NewTranslationProvider(), - } -} - -type tstNoStringer struct { -} - -type tstCompareType int - -const ( - tstEq tstCompareType = iota - tstNe - tstGt - tstGe - tstLt - tstLe -) - -func tstIsEq(tp tstCompareType) bool { - return tp == tstEq || tp == tstGe || tp == tstLe -} - -func tstIsGt(tp tstCompareType) bool { - return tp == tstGt || tp == tstGe -} - -func tstIsLt(tp tstCompareType) bool { - return tp == tstLt || tp == tstLe -} - -func TestFuncsInTemplate(t *testing.T) { - t.Parallel() - - workingDir := "/home/hugo" - - v := viper.New() - - v.Set("workingDir", workingDir) - v.Set("multilingual", true) - - fs := hugofs.NewMem(v) - - afero.WriteFile(fs.Source, filepath.Join(workingDir, "README.txt"), []byte("Hugo Rocks!"), 0755) - - // Add the examples from the docs: As a smoke test and to make sure the examples work. - // TODO(bep): docs: fix title example - in := - `absLangURL: {{ "index.html" | absLangURL }} -absURL: {{ "http://gohugo.io/" | absURL }} -absURL: {{ "mystyle.css" | absURL }} -absURL: {{ 42 | absURL }} -add: {{add 1 2}} -base64Decode 1: {{ "SGVsbG8gd29ybGQ=" | base64Decode }} -base64Decode 2: {{ 42 | base64Encode | base64Decode }} -base64Encode: {{ "Hello world" | base64Encode }} -chomp: {{chomp "

Blockhead

\n" }} -dateFormat: {{ dateFormat "Monday, Jan 2, 2006" "2015-01-21" }} -delimit: {{ delimit (slice "A" "B" "C") ", " " and " }} -div: {{div 6 3}} -echoParam: {{ echoParam .Params "langCode" }} -emojify: {{ "I :heart: Hugo" | emojify }} -eq: {{ if eq .Section "blog" }}current{{ end }} -findRE: {{ findRE "[G|g]o" "Hugo is a static side generator written in Go." "1" }} -hasPrefix 1: {{ hasPrefix "Hugo" "Hu" }} -hasPrefix 2: {{ hasPrefix "Hugo" "Fu" }} -htmlEscape 1: {{ htmlEscape "Cathal Garvey & The Sunshine Band " | safeHTML}} -htmlEscape 2: {{ htmlEscape "Cathal Garvey & The Sunshine Band "}} -htmlUnescape 1: {{htmlUnescape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" | safeHTML}} -htmlUnescape 2: {{"Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;" | htmlUnescape | htmlUnescape | safeHTML}} -htmlUnescape 3: {{"Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;" | htmlUnescape | htmlUnescape }} -htmlUnescape 4: {{ htmlEscape "Cathal Garvey & The Sunshine Band " | htmlUnescape | safeHTML }} -htmlUnescape 5: {{ htmlUnescape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" | htmlEscape | safeHTML }} -humanize 1: {{ humanize "my-first-post" }} -humanize 2: {{ humanize "myCamelPost" }} -humanize 3: {{ humanize "52" }} -humanize 4: {{ humanize 103 }} -in: {{ if in "this string contains a substring" "substring" }}Substring found!{{ end }} -jsonify: {{ (slice "A" "B" "C") | jsonify }} -lower: {{lower "BatMan"}} -markdownify: {{ .Title | markdownify}} -md5: {{ md5 "Hello world, gophers!" }} -mod: {{mod 15 3}} -modBool: {{modBool 15 3}} -mul: {{mul 2 3}} -plainify: {{ plainify "Hello world, gophers!" }} -pluralize: {{ "cat" | pluralize }} -querify 1: {{ (querify "foo" 1 "bar" 2 "baz" "with spaces" "qux" "this&that=those") | safeHTML }} -querify 2: Search -readDir: {{ range (readDir ".") }}{{ .Name }}{{ end }} -readFile: {{ readFile "README.txt" }} -relLangURL: {{ "index.html" | relLangURL }} -relURL 1: {{ "http://gohugo.io/" | relURL }} -relURL 2: {{ "mystyle.css" | relURL }} -relURL 3: {{ mul 2 21 | relURL }} -replace: {{ replace "Batman and Robin" "Robin" "Catwoman" }} -replaceRE: {{ "http://gohugo.io/docs" | replaceRE "^https?://([^/]+).*" "$1" }} -safeCSS: {{ "Bat&Man" | safeCSS | safeCSS }} -safeHTML: {{ "Bat&Man" | safeHTML | safeHTML }} -safeHTML: {{ "Bat&Man" | safeHTML }} -safeJS: {{ "(1*2)" | safeJS | safeJS }} -safeURL: {{ "http://gohugo.io" | safeURL | safeURL }} -seq: {{ seq 3 }} -sha1: {{ sha1 "Hello world, gophers!" }} -sha256: {{ sha256 "Hello world, gophers!" }} -singularize: {{ "cats" | singularize }} -slicestr: {{slicestr "BatMan" 0 3}} -slicestr: {{slicestr "BatMan" 3}} -sort: {{ slice "B" "C" "A" | sort }} -sub: {{sub 3 2}} -substr: {{substr "BatMan" 0 -3}} -substr: {{substr "BatMan" 3 3}} -title: {{title "Bat man"}} -time: {{ (time "2015-01-21").Year }} -trim: {{ trim "++Batman--" "+-" }} -truncate: {{ "this is a very long text" | truncate 10 " ..." }} -truncate: {{ "With [Markdown](/markdown) inside." | markdownify | truncate 14 }} -upper: {{upper "BatMan"}} -urlize: {{ "Bat Man" | urlize }} -` - - expected := `absLangURL: http://mysite.com/hugo/en/index.html -absURL: http://gohugo.io/ -absURL: http://mysite.com/hugo/mystyle.css -absURL: http://mysite.com/hugo/42 -add: 3 -base64Decode 1: Hello world -base64Decode 2: 42 -base64Encode: SGVsbG8gd29ybGQ= -chomp:

Blockhead

-dateFormat: Wednesday, Jan 21, 2015 -delimit: A, B and C -div: 2 -echoParam: en -emojify: I ❤️ Hugo -eq: current -findRE: [go] -hasPrefix 1: true -hasPrefix 2: false -htmlEscape 1: Cathal Garvey & The Sunshine Band <cathal@foo.bar> -htmlEscape 2: Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt; -htmlUnescape 1: Cathal Garvey & The Sunshine Band -htmlUnescape 2: Cathal Garvey & The Sunshine Band -htmlUnescape 3: Cathal Garvey & The Sunshine Band <cathal@foo.bar> -htmlUnescape 4: Cathal Garvey & The Sunshine Band -htmlUnescape 5: Cathal Garvey & The Sunshine Band <cathal@foo.bar> -humanize 1: My first post -humanize 2: My camel post -humanize 3: 52nd -humanize 4: 103rd -in: Substring found! -jsonify: ["A","B","C"] -lower: batman -markdownify: BatMan -md5: b3029f756f98f79e7f1b7f1d1f0dd53b -mod: 0 -modBool: true -mul: 6 -plainify: Hello world, gophers! -pluralize: cats -querify 1: bar=2&baz=with+spaces&foo=1&qux=this%26that%3Dthose -querify 2: Search -readDir: README.txt -readFile: Hugo Rocks! -relLangURL: /hugo/en/index.html -relURL 1: http://gohugo.io/ -relURL 2: /hugo/mystyle.css -relURL 3: /hugo/42 -replace: Batman and Catwoman -replaceRE: gohugo.io -safeCSS: Bat&Man -safeHTML: Bat&Man -safeHTML: Bat&Man -safeJS: (1*2) -safeURL: http://gohugo.io -seq: [1 2 3] -sha1: c8b5b0e33d408246e30f53e32b8f7627a7a649d4 -sha256: 6ec43b78da9669f50e4e422575c54bf87536954ccd58280219c393f2ce352b46 -singularize: cat -slicestr: Bat -slicestr: Man -sort: [A B C] -sub: 1 -substr: Bat -substr: Man -title: Bat Man -time: 2015 -trim: Batman -truncate: this is a ... -truncate: With Markdown … -upper: BATMAN -urlize: bat-man -` - - var b bytes.Buffer - - var data struct { - Title string - Section string - Params map[string]interface{} - } - - data.Title = "**BatMan**" - data.Section = "blog" - data.Params = map[string]interface{}{"langCode": "en"} - - v.Set("baseURL", "http://mysite.com/hugo/") - v.Set("CurrentContentLanguage", helpers.NewLanguage("en", v)) - - config := newDepsConfig(v) - config.WithTemplate = func(templ tplapi.Template) error { - if _, err := templ.New("test").Parse(in); err != nil { - t.Fatal("Got error on parse", err) - } - return nil - } - config.Fs = fs - - d := deps.New(config) - if err := d.LoadResources(); err != nil { - t.Fatal(err) - } - - err := d.Tmpl.Lookup("test").Execute(&b, &data) - - if err != nil { - t.Fatal("Got error on execute", err) - } - - if b.String() != expected { - sl1 := strings.Split(b.String(), "\n") - sl2 := strings.Split(expected, "\n") - t.Errorf("Diff:\n%q", helpers.DiffStringSlices(sl1, sl2)) - } -} - -func TestCompare(t *testing.T) { - t.Parallel() - for _, this := range []struct { - tstCompareType - funcUnderTest func(a, b interface{}) bool - }{ - {tstGt, gt}, - {tstLt, lt}, - {tstGe, ge}, - {tstLe, le}, - {tstEq, eq}, - {tstNe, ne}, - } { - doTestCompare(t, this.tstCompareType, this.funcUnderTest) - } -} - -func doTestCompare(t *testing.T, tp tstCompareType, funcUnderTest func(a, b interface{}) bool) { - for i, this := range []struct { - left interface{} - right interface{} - expectIndicator int - }{ - {5, 8, -1}, - {8, 5, 1}, - {5, 5, 0}, - {int(5), int64(5), 0}, - {int32(5), int(5), 0}, - {int16(4), int(5), -1}, - {uint(15), uint64(15), 0}, - {-2, 1, -1}, - {2, -5, 1}, - {0.0, 1.23, -1}, - {1.1, 1.1, 0}, - {float32(1.0), float64(1.0), 0}, - {1.23, 0.0, 1}, - {"5", "5", 0}, - {"8", "5", 1}, - {"5", "0001", 1}, - {[]int{100, 99}, []int{1, 2, 3, 4}, -1}, - {cast.ToTime("2015-11-20"), cast.ToTime("2015-11-20"), 0}, - {cast.ToTime("2015-11-19"), cast.ToTime("2015-11-20"), -1}, - {cast.ToTime("2015-11-20"), cast.ToTime("2015-11-19"), 1}, - } { - result := funcUnderTest(this.left, this.right) - success := false - - if this.expectIndicator == 0 { - if tstIsEq(tp) { - success = result - } else { - success = !result - } - } - - if this.expectIndicator < 0 { - success = result && (tstIsLt(tp) || tp == tstNe) - success = success || (!result && !tstIsLt(tp)) - } - - if this.expectIndicator > 0 { - success = result && (tstIsGt(tp) || tp == tstNe) - success = success || (!result && (!tstIsGt(tp) || tp != tstNe)) - } - - if !success { - t.Errorf("[%d][%s] %v compared to %v: %t", i, path.Base(runtime.FuncForPC(reflect.ValueOf(funcUnderTest).Pointer()).Name()), this.left, this.right, result) - } - } -} - -func TestMod(t *testing.T) { - t.Parallel() - for i, this := range []struct { - a interface{} - b interface{} - expect interface{} - }{ - {3, 2, int64(1)}, - {3, 1, int64(0)}, - {3, 0, false}, - {0, 3, int64(0)}, - {3.1, 2, false}, - {3, 2.1, false}, - {3.1, 2.1, false}, - {int8(3), int8(2), int64(1)}, - {int16(3), int16(2), int64(1)}, - {int32(3), int32(2), int64(1)}, - {int64(3), int64(2), int64(1)}, - } { - result, err := mod(this.a, this.b) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] modulo didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] modulo got %v but expected %v", i, result, this.expect) - } - } - } -} - -func TestModBool(t *testing.T) { - t.Parallel() - for i, this := range []struct { - a interface{} - b interface{} - expect interface{} - }{ - {3, 3, true}, - {3, 2, false}, - {3, 1, true}, - {3, 0, nil}, - {0, 3, true}, - {3.1, 2, nil}, - {3, 2.1, nil}, - {3.1, 2.1, nil}, - {int8(3), int8(3), true}, - {int8(3), int8(2), false}, - {int16(3), int16(3), true}, - {int16(3), int16(2), false}, - {int32(3), int32(3), true}, - {int32(3), int32(2), false}, - {int64(3), int64(3), true}, - {int64(3), int64(2), false}, - } { - result, err := modBool(this.a, this.b) - if this.expect == nil { - if err == nil { - t.Errorf("[%d] modulo didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] modulo got %v but expected %v", i, result, this.expect) - } - } - } -} - -func TestFirst(t *testing.T) { - t.Parallel() - for i, this := range []struct { - count interface{} - sequence interface{} - expect interface{} - }{ - {int(2), []string{"a", "b", "c"}, []string{"a", "b"}}, - {int32(3), []string{"a", "b"}, []string{"a", "b"}}, - {int64(2), []int{100, 200, 300}, []int{100, 200}}, - {100, []int{100, 200}, []int{100, 200}}, - {"1", []int{100, 200, 300}, []int{100}}, - {int64(-1), []int{100, 200, 300}, false}, - {"noint", []int{100, 200, 300}, false}, - {1, nil, false}, - {nil, []int{100}, false}, - {1, t, false}, - {1, (*string)(nil), false}, - } { - results, err := first(this.count, this.sequence) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] First didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(results, this.expect) { - t.Errorf("[%d] First %d items, got %v but expected %v", i, this.count, results, this.expect) - } - } - } -} - -func TestLast(t *testing.T) { - t.Parallel() - for i, this := range []struct { - count interface{} - sequence interface{} - expect interface{} - }{ - {int(2), []string{"a", "b", "c"}, []string{"b", "c"}}, - {int32(3), []string{"a", "b"}, []string{"a", "b"}}, - {int64(2), []int{100, 200, 300}, []int{200, 300}}, - {100, []int{100, 200}, []int{100, 200}}, - {"1", []int{100, 200, 300}, []int{300}}, - {int64(-1), []int{100, 200, 300}, false}, - {"noint", []int{100, 200, 300}, false}, - {1, nil, false}, - {nil, []int{100}, false}, - {1, t, false}, - {1, (*string)(nil), false}, - } { - results, err := last(this.count, this.sequence) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] First didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(results, this.expect) { - t.Errorf("[%d] First %d items, got %v but expected %v", i, this.count, results, this.expect) - } - } - } -} - -func TestAfter(t *testing.T) { - t.Parallel() - for i, this := range []struct { - count interface{} - sequence interface{} - expect interface{} - }{ - {int(2), []string{"a", "b", "c", "d"}, []string{"c", "d"}}, - {int32(3), []string{"a", "b"}, false}, - {int64(2), []int{100, 200, 300}, []int{300}}, - {100, []int{100, 200}, false}, - {"1", []int{100, 200, 300}, []int{200, 300}}, - {int64(-1), []int{100, 200, 300}, false}, - {"noint", []int{100, 200, 300}, false}, - {1, nil, false}, - {nil, []int{100}, false}, - {1, t, false}, - {1, (*string)(nil), false}, - } { - results, err := after(this.count, this.sequence) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] First didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(results, this.expect) { - t.Errorf("[%d] First %d items, got %v but expected %v", i, this.count, results, this.expect) - } - } - } -} - -func TestShuffleInputAndOutputFormat(t *testing.T) { - t.Parallel() - for i, this := range []struct { - sequence interface{} - success bool - }{ - {[]string{"a", "b", "c", "d"}, true}, - {[]int{100, 200, 300}, true}, - {[]int{100, 200, 300}, true}, - {[]int{100, 200}, true}, - {[]string{"a", "b"}, true}, - {[]int{100, 200, 300}, true}, - {[]int{100, 200, 300}, true}, - {[]int{100}, true}, - {nil, false}, - {t, false}, - {(*string)(nil), false}, - } { - results, err := shuffle(this.sequence) - if !this.success { - if err == nil { - t.Errorf("[%d] First didn't return an expected error", i) - } - } else { - resultsv := reflect.ValueOf(results) - sequencev := reflect.ValueOf(this.sequence) - - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - - if resultsv.Len() != sequencev.Len() { - t.Errorf("Expected %d items, got %d items", sequencev.Len(), resultsv.Len()) - } - } - } -} - -func TestShuffleRandomising(t *testing.T) { - t.Parallel() - // Note that this test can fail with false negative result if the shuffle - // of the sequence happens to be the same as the original sequence. However - // the propability of the event is 10^-158 which is negligible. - sequenceLength := 100 - rand.Seed(time.Now().UTC().UnixNano()) - - for _, this := range []struct { - sequence []int - }{ - {rand.Perm(sequenceLength)}, - } { - results, _ := shuffle(this.sequence) - - resultsv := reflect.ValueOf(results) - - allSame := true - for index, value := range this.sequence { - allSame = allSame && (resultsv.Index(index).Interface() == value) - } - - if allSame { - t.Error("Expected sequence to be shuffled but was in the same order") - } - } -} - -func TestDictionary(t *testing.T) { - t.Parallel() - for i, this := range []struct { - v1 []interface{} - expecterr bool - expectedValue map[string]interface{} - }{ - {[]interface{}{"a", "b"}, false, map[string]interface{}{"a": "b"}}, - {[]interface{}{5, "b"}, true, nil}, - {[]interface{}{"a", 12, "b", []int{4}}, false, map[string]interface{}{"a": 12, "b": []int{4}}}, - {[]interface{}{"a", "b", "c"}, true, nil}, - } { - r, e := dictionary(this.v1...) - - if (this.expecterr && e == nil) || (!this.expecterr && e != nil) { - t.Errorf("[%d] got an unexpected error: %s", i, e) - } else if !this.expecterr { - if !reflect.DeepEqual(r, this.expectedValue) { - t.Errorf("[%d] got %v but expected %v", i, r, this.expectedValue) - } - } - } -} - -func blankImage(width, height int) []byte { - var buf bytes.Buffer - img := image.NewRGBA(image.Rect(0, 0, width, height)) - if err := png.Encode(&buf, img); err != nil { - panic(err) - } - return buf.Bytes() -} - -func TestImageConfig(t *testing.T) { - t.Parallel() - - workingDir := "/home/hugo" - - v := viper.New() - - v.Set("workingDir", workingDir) - - f := newTestFuncsterWithViper(v) - - for i, this := range []struct { - resetCache bool - path string - input []byte - expected image.Config - }{ - // Make sure that the cache is initialized by default. - { - resetCache: false, - path: "a.png", - input: blankImage(10, 10), - expected: image.Config{ - Width: 10, - Height: 10, - ColorModel: color.NRGBAModel, - }, - }, - { - resetCache: true, - path: "a.png", - input: blankImage(10, 10), - expected: image.Config{ - Width: 10, - Height: 10, - ColorModel: color.NRGBAModel, - }, - }, - { - resetCache: false, - path: "b.png", - input: blankImage(20, 15), - expected: image.Config{ - Width: 20, - Height: 15, - ColorModel: color.NRGBAModel, - }, - }, - { - resetCache: false, - path: "a.png", - input: blankImage(20, 15), - expected: image.Config{ - Width: 10, - Height: 10, - ColorModel: color.NRGBAModel, - }, - }, - { - resetCache: true, - path: "a.png", - input: blankImage(20, 15), - expected: image.Config{ - Width: 20, - Height: 15, - ColorModel: color.NRGBAModel, - }, - }, - } { - afero.WriteFile(f.Fs.Source, filepath.Join(workingDir, this.path), this.input, 0755) - - if this.resetCache { - resetImageConfigCache() - } - - result, err := f.imageConfig(this.path) - if err != nil { - t.Errorf("imageConfig returned error: %s", err) - } - - if !reflect.DeepEqual(result, this.expected) { - t.Errorf("[%d] imageConfig: expected '%v', got '%v'", i, this.expected, result) - } - - if len(defaultImageConfigCache.config) == 0 { - t.Error("defaultImageConfigCache should have at least 1 item") - } - } - - if _, err := f.imageConfig(t); err == nil { - t.Error("Expected error from imageConfig when passed invalid path") - } - - if _, err := f.imageConfig("non-existent.png"); err == nil { - t.Error("Expected error from imageConfig when passed non-existent file") - } - - if _, err := f.imageConfig(""); err == nil { - t.Error("Expected error from imageConfig when passed empty path") - } - - // test cache clearing - ResetCaches() - - if len(defaultImageConfigCache.config) != 0 { - t.Error("ResetCaches should have cleared defaultImageConfigCache") - } -} - -func TestIn(t *testing.T) { - t.Parallel() - for i, this := range []struct { - v1 interface{} - v2 interface{} - expect bool - }{ - {[]string{"a", "b", "c"}, "b", true}, - {[]interface{}{"a", "b", "c"}, "b", true}, - {[]interface{}{"a", "b", "c"}, "d", false}, - {[]string{"a", "b", "c"}, "d", false}, - {[]string{"a", "12", "c"}, 12, false}, - {[]int{1, 2, 4}, 2, true}, - {[]interface{}{1, 2, 4}, 2, true}, - {[]interface{}{1, 2, 4}, nil, false}, - {[]interface{}{nil}, nil, false}, - {[]int{1, 2, 4}, 3, false}, - {[]float64{1.23, 2.45, 4.67}, 1.23, true}, - {[]float64{1.234567, 2.45, 4.67}, 1.234568, false}, - {"this substring should be found", "substring", true}, - {"this substring should not be found", "subseastring", false}, - } { - result := in(this.v1, this.v2) - - if result != this.expect { - t.Errorf("[%d] got %v but expected %v", i, result, this.expect) - } - } -} - -func TestSlicestr(t *testing.T) { - t.Parallel() - var err error - for i, this := range []struct { - v1 interface{} - v2 interface{} - v3 interface{} - expect interface{} - }{ - {"abc", 1, 2, "b"}, - {"abc", 1, 3, "bc"}, - {"abcdef", 1, int8(3), "bc"}, - {"abcdef", 1, int16(3), "bc"}, - {"abcdef", 1, int32(3), "bc"}, - {"abcdef", 1, int64(3), "bc"}, - {"abc", 0, 1, "a"}, - {"abcdef", nil, nil, "abcdef"}, - {"abcdef", 0, 6, "abcdef"}, - {"abcdef", 0, 2, "ab"}, - {"abcdef", 2, nil, "cdef"}, - {"abcdef", int8(2), nil, "cdef"}, - {"abcdef", int16(2), nil, "cdef"}, - {"abcdef", int32(2), nil, "cdef"}, - {"abcdef", int64(2), nil, "cdef"}, - {123, 1, 3, "23"}, - {"abcdef", 6, nil, false}, - {"abcdef", 4, 7, false}, - {"abcdef", -1, nil, false}, - {"abcdef", -1, 7, false}, - {"abcdef", 1, -1, false}, - {tstNoStringer{}, 0, 1, false}, - {"ĀĀĀ", 0, 1, "Ā"}, // issue #1333 - {"a", t, nil, false}, - {"a", 1, t, false}, - } { - var result string - if this.v2 == nil { - result, err = slicestr(this.v1) - } else if this.v3 == nil { - result, err = slicestr(this.v1, this.v2) - } else { - result, err = slicestr(this.v1, this.v2, this.v3) - } - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] Slice didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] got %s but expected %s", i, result, this.expect) - } - } - } - - // Too many arguments - _, err = slicestr("a", 1, 2, 3) - if err == nil { - t.Errorf("Should have errored") - } -} - -func TestHasPrefix(t *testing.T) { - t.Parallel() - cases := []struct { - s interface{} - prefix interface{} - want interface{} - isErr bool - }{ - {"abcd", "ab", true, false}, - {"abcd", "cd", false, false}, - {template.HTML("abcd"), "ab", true, false}, - {template.HTML("abcd"), "cd", false, false}, - {template.HTML("1234"), 12, true, false}, - {template.HTML("1234"), 34, false, false}, - {[]byte("abcd"), "ab", true, false}, - } - - for i, c := range cases { - res, err := hasPrefix(c.s, c.prefix) - if (err != nil) != c.isErr { - t.Fatalf("[%d] unexpected isErr state: want %v, got %v, err = %v", i, c.isErr, err != nil, err) - } - if res != c.want { - t.Errorf("[%d] want %v, got %v", i, c.want, res) - } - } -} - -func TestSubstr(t *testing.T) { - t.Parallel() - var err error - var n int - for i, this := range []struct { - v1 interface{} - v2 interface{} - v3 interface{} - expect interface{} - }{ - {"abc", 1, 2, "bc"}, - {"abc", 0, 1, "a"}, - {"abcdef", -1, 2, "ef"}, - {"abcdef", -3, 3, "bcd"}, - {"abcdef", 0, -1, "abcde"}, - {"abcdef", 2, -1, "cde"}, - {"abcdef", 4, -4, false}, - {"abcdef", 7, 1, false}, - {"abcdef", 1, 100, "bcdef"}, - {"abcdef", -100, 3, "abc"}, - {"abcdef", -3, -1, "de"}, - {"abcdef", 2, nil, "cdef"}, - {"abcdef", int8(2), nil, "cdef"}, - {"abcdef", int16(2), nil, "cdef"}, - {"abcdef", int32(2), nil, "cdef"}, - {"abcdef", int64(2), nil, "cdef"}, - {"abcdef", 2, int8(3), "cde"}, - {"abcdef", 2, int16(3), "cde"}, - {"abcdef", 2, int32(3), "cde"}, - {"abcdef", 2, int64(3), "cde"}, - {123, 1, 3, "23"}, - {1.2e3, 0, 4, "1200"}, - {tstNoStringer{}, 0, 1, false}, - {"abcdef", 2.0, nil, "cdef"}, - {"abcdef", 2.0, 2, "cd"}, - {"abcdef", 2, 2.0, "cd"}, - {"ĀĀĀ", 1, 2, "ĀĀ"}, // # issue 1333 - {"abcdef", "doo", nil, false}, - {"abcdef", "doo", "doo", false}, - {"abcdef", 1, "doo", false}, - } { - var result string - n = i - - if this.v3 == nil { - result, err = substr(this.v1, this.v2) - } else { - result, err = substr(this.v1, this.v2, this.v3) - } - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] Substr didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] got %s but expected %s", i, result, this.expect) - } - } - } - - n++ - _, err = substr("abcdef") - if err == nil { - t.Errorf("[%d] Substr didn't return an expected error", n) - } - - n++ - _, err = substr("abcdef", 1, 2, 3) - if err == nil { - t.Errorf("[%d] Substr didn't return an expected error", n) - } -} - -func TestSplit(t *testing.T) { - t.Parallel() - for i, this := range []struct { - v1 interface{} - v2 string - expect interface{} - }{ - {"a, b", ", ", []string{"a", "b"}}, - {"a & b & c", " & ", []string{"a", "b", "c"}}, - {"http://example.com", "http://", []string{"", "example.com"}}, - {123, "2", []string{"1", "3"}}, - {tstNoStringer{}, ",", false}, - } { - result, err := split(this.v1, this.v2) - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] Split didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] got %s but expected %s", i, result, this.expect) - } - } - } -} - -func TestIntersect(t *testing.T) { - t.Parallel() - for i, this := range []struct { - sequence1 interface{} - sequence2 interface{} - expect interface{} - }{ - {[]string{"a", "b", "c", "c"}, []string{"a", "b", "b"}, []string{"a", "b"}}, - {[]string{"a", "b"}, []string{"a", "b", "c"}, []string{"a", "b"}}, - {[]string{"a", "b", "c"}, []string{"d", "e"}, []string{}}, - {[]string{}, []string{}, []string{}}, - {[]string{"a", "b"}, nil, make([]interface{}, 0)}, - {nil, []string{"a", "b"}, make([]interface{}, 0)}, - {nil, nil, make([]interface{}, 0)}, - {[]string{"1", "2"}, []int{1, 2}, []string{}}, - {[]int{1, 2}, []string{"1", "2"}, []int{}}, - {[]int{1, 2, 4}, []int{2, 4}, []int{2, 4}}, - {[]int{2, 4}, []int{1, 2, 4}, []int{2, 4}}, - {[]int{1, 2, 4}, []int{3, 6}, []int{}}, - {[]float64{2.2, 4.4}, []float64{1.1, 2.2, 4.4}, []float64{2.2, 4.4}}, - } { - results, err := intersect(this.sequence1, this.sequence2) - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(results, this.expect) { - t.Errorf("[%d] got %v but expected %v", i, results, this.expect) - } - } - - _, err1 := intersect("not an array or slice", []string{"a"}) - - if err1 == nil { - t.Error("Expected error for non array as first arg") - } - - _, err2 := intersect([]string{"a"}, "not an array or slice") - - if err2 == nil { - t.Error("Expected error for non array as second arg") - } -} - -func TestIsSet(t *testing.T) { - t.Parallel() - aSlice := []interface{}{1, 2, 3, 5} - aMap := map[string]interface{}{"a": 1, "b": 2} - - assert.True(t, isSet(aSlice, 2)) - assert.True(t, isSet(aMap, "b")) - assert.False(t, isSet(aSlice, 22)) - assert.False(t, isSet(aMap, "bc")) -} - -func (x *TstX) TstRp() string { - return "r" + x.A -} - -func (x TstX) TstRv() string { - return "r" + x.B -} - -func (x TstX) unexportedMethod() string { - return x.unexported -} - -func (x TstX) MethodWithArg(s string) string { - return s -} - -func (x TstX) MethodReturnNothing() {} - -func (x TstX) MethodReturnErrorOnly() error { - return errors.New("some error occurred") -} - -func (x TstX) MethodReturnTwoValues() (string, string) { - return "foo", "bar" -} - -func (x TstX) MethodReturnValueWithError() (string, error) { - return "", errors.New("some error occurred") -} - -func (x TstX) String() string { - return fmt.Sprintf("A: %s, B: %s", x.A, x.B) -} - -type TstX struct { - A, B string - unexported string -} - -func TestTimeUnix(t *testing.T) { - t.Parallel() - var sec int64 = 1234567890 - tv := reflect.ValueOf(time.Unix(sec, 0)) - i := 1 - - res := toTimeUnix(tv) - if sec != res { - t.Errorf("[%d] timeUnix got %v but expected %v", i, res, sec) - } - - i++ - func(t *testing.T) { - defer func() { - if err := recover(); err == nil { - t.Errorf("[%d] timeUnix didn't return an expected error", i) - } - }() - iv := reflect.ValueOf(sec) - toTimeUnix(iv) - }(t) -} - -func TestEvaluateSubElem(t *testing.T) { - t.Parallel() - tstx := TstX{A: "foo", B: "bar"} - var inner struct { - S fmt.Stringer - } - inner.S = tstx - interfaceValue := reflect.ValueOf(&inner).Elem().Field(0) - - for i, this := range []struct { - value reflect.Value - key string - expect interface{} - }{ - {reflect.ValueOf(tstx), "A", "foo"}, - {reflect.ValueOf(&tstx), "TstRp", "rfoo"}, - {reflect.ValueOf(tstx), "TstRv", "rbar"}, - //{reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), 1, "foo"}, - {reflect.ValueOf(map[string]string{"key1": "foo", "key2": "bar"}), "key1", "foo"}, - {interfaceValue, "String", "A: foo, B: bar"}, - {reflect.Value{}, "foo", false}, - //{reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), 1.2, false}, - {reflect.ValueOf(tstx), "unexported", false}, - {reflect.ValueOf(tstx), "unexportedMethod", false}, - {reflect.ValueOf(tstx), "MethodWithArg", false}, - {reflect.ValueOf(tstx), "MethodReturnNothing", false}, - {reflect.ValueOf(tstx), "MethodReturnErrorOnly", false}, - {reflect.ValueOf(tstx), "MethodReturnTwoValues", false}, - {reflect.ValueOf(tstx), "MethodReturnValueWithError", false}, - {reflect.ValueOf((*TstX)(nil)), "A", false}, - {reflect.ValueOf(tstx), "C", false}, - {reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), "1", false}, - {reflect.ValueOf([]string{"foo", "bar"}), "1", false}, - } { - result, err := evaluateSubElem(this.value, this.key) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] evaluateSubElem didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if result.Kind() != reflect.String || result.String() != this.expect { - t.Errorf("[%d] evaluateSubElem with %v got %v but expected %v", i, this.key, result, this.expect) - } - } - } -} - -func TestCheckCondition(t *testing.T) { - t.Parallel() - type expect struct { - result bool - isError bool - } - - for i, this := range []struct { - value reflect.Value - match reflect.Value - op string - expect - }{ - {reflect.ValueOf(123), reflect.ValueOf(123), "", expect{true, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf("foo"), "", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - "", - expect{true, false}, - }, - {reflect.ValueOf(true), reflect.ValueOf(true), "", expect{true, false}}, - {reflect.ValueOf(nil), reflect.ValueOf(nil), "", expect{true, false}}, - {reflect.ValueOf(123), reflect.ValueOf(456), "!=", expect{true, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf("bar"), "!=", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), - "!=", - expect{true, false}, - }, - {reflect.ValueOf(true), reflect.ValueOf(false), "!=", expect{true, false}}, - {reflect.ValueOf(123), reflect.ValueOf(nil), "!=", expect{true, false}}, - {reflect.ValueOf(456), reflect.ValueOf(123), ">=", expect{true, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf("bar"), ">=", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), - ">=", - expect{true, false}, - }, - {reflect.ValueOf(456), reflect.ValueOf(123), ">", expect{true, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf("bar"), ">", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), - ">", - expect{true, false}, - }, - {reflect.ValueOf(123), reflect.ValueOf(456), "<=", expect{true, false}}, - {reflect.ValueOf("bar"), reflect.ValueOf("foo"), "<=", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - "<=", - expect{true, false}, - }, - {reflect.ValueOf(123), reflect.ValueOf(456), "<", expect{true, false}}, - {reflect.ValueOf("bar"), reflect.ValueOf("foo"), "<", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - "<", - expect{true, false}, - }, - {reflect.ValueOf(123), reflect.ValueOf([]int{123, 45, 678}), "in", expect{true, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf([]string{"foo", "bar", "baz"}), "in", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf([]time.Time{ - time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC), - time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC), - time.Date(2015, time.June, 26, 19, 18, 56, 12345, time.UTC), - }), - "in", - expect{true, false}, - }, - {reflect.ValueOf(123), reflect.ValueOf([]int{45, 678}), "not in", expect{true, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf([]string{"bar", "baz"}), "not in", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf([]time.Time{ - time.Date(2015, time.February, 26, 19, 18, 56, 12345, time.UTC), - time.Date(2015, time.March, 26, 19, 18, 56, 12345, time.UTC), - time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC), - }), - "not in", - expect{true, false}, - }, - {reflect.ValueOf("foo"), reflect.ValueOf("bar-foo-baz"), "in", expect{true, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf("bar--baz"), "not in", expect{true, false}}, - {reflect.Value{}, reflect.ValueOf("foo"), "", expect{false, false}}, - {reflect.ValueOf("foo"), reflect.Value{}, "", expect{false, false}}, - {reflect.ValueOf((*TstX)(nil)), reflect.ValueOf("foo"), "", expect{false, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf((*TstX)(nil)), "", expect{false, false}}, - {reflect.ValueOf(true), reflect.ValueOf("foo"), "", expect{false, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf(true), "", expect{false, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf(map[int]string{}), "", expect{false, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf([]int{1, 2}), "", expect{false, false}}, - {reflect.ValueOf((*TstX)(nil)), reflect.ValueOf((*TstX)(nil)), ">", expect{false, false}}, - {reflect.ValueOf(true), reflect.ValueOf(false), ">", expect{false, false}}, - {reflect.ValueOf(123), reflect.ValueOf([]int{}), "in", expect{false, false}}, - {reflect.ValueOf(123), reflect.ValueOf(123), "op", expect{false, true}}, - } { - result, err := checkCondition(this.value, this.match, this.op) - if this.expect.isError { - if err == nil { - t.Errorf("[%d] checkCondition didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if result != this.expect.result { - t.Errorf("[%d] check condition %v %s %v, got %v but expected %v", i, this.value, this.op, this.match, result, this.expect.result) - } - } - } -} - -func TestWhere(t *testing.T) { - t.Parallel() - - type Mid struct { - Tst TstX - } - - d1 := time.Now() - d2 := d1.Add(1 * time.Hour) - d3 := d2.Add(1 * time.Hour) - d4 := d3.Add(1 * time.Hour) - d5 := d4.Add(1 * time.Hour) - d6 := d5.Add(1 * time.Hour) - - for i, this := range []struct { - sequence interface{} - key interface{} - op string - match interface{} - expect interface{} - }{ - { - sequence: []map[int]string{ - {1: "a", 2: "m"}, {1: "c", 2: "d"}, {1: "e", 3: "m"}, - }, - key: 2, match: "m", - expect: []map[int]string{ - {1: "a", 2: "m"}, - }, - }, - { - sequence: []map[string]int{ - {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "x": 4}, - }, - key: "b", match: 4, - expect: []map[string]int{ - {"a": 3, "b": 4}, - }, - }, - { - sequence: []TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, - }, - key: "B", match: "f", - expect: []TstX{ - {A: "e", B: "f"}, - }, - }, - { - sequence: []*map[int]string{ - {1: "a", 2: "m"}, {1: "c", 2: "d"}, {1: "e", 3: "m"}, - }, - key: 2, match: "m", - expect: []*map[int]string{ - {1: "a", 2: "m"}, - }, - }, - { - sequence: []*TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, - }, - key: "B", match: "f", - expect: []*TstX{ - {A: "e", B: "f"}, - }, - }, - { - sequence: []*TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "c"}, - }, - key: "TstRp", match: "rc", - expect: []*TstX{ - {A: "c", B: "d"}, - }, - }, - { - sequence: []TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "c"}, - }, - key: "TstRv", match: "rc", - expect: []TstX{ - {A: "e", B: "c"}, - }, - }, - { - sequence: []map[string]TstX{ - {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, - }, - key: "foo.B", match: "d", - expect: []map[string]TstX{ - {"foo": TstX{A: "c", B: "d"}}, - }, - }, - { - sequence: []map[string]TstX{ - {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, - }, - key: ".foo.B", match: "d", - expect: []map[string]TstX{ - {"foo": TstX{A: "c", B: "d"}}, - }, - }, - { - sequence: []map[string]TstX{ - {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, - }, - key: "foo.TstRv", match: "rd", - expect: []map[string]TstX{ - {"foo": TstX{A: "c", B: "d"}}, - }, - }, - { - sequence: []map[string]*TstX{ - {"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}, - }, - key: "foo.TstRp", match: "rc", - expect: []map[string]*TstX{ - {"foo": &TstX{A: "c", B: "d"}}, - }, - }, - { - sequence: []map[string]Mid{ - {"foo": Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": Mid{Tst: TstX{A: "e", B: "f"}}}, - }, - key: "foo.Tst.B", match: "d", - expect: []map[string]Mid{ - {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, - }, - }, - { - sequence: []map[string]Mid{ - {"foo": Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": Mid{Tst: TstX{A: "e", B: "f"}}}, - }, - key: "foo.Tst.TstRv", match: "rd", - expect: []map[string]Mid{ - {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, - }, - }, - { - sequence: []map[string]*Mid{ - {"foo": &Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": &Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": &Mid{Tst: TstX{A: "e", B: "f"}}}, - }, - key: "foo.Tst.TstRp", match: "rc", - expect: []map[string]*Mid{ - {"foo": &Mid{Tst: TstX{A: "c", B: "d"}}}, - }, - }, - { - sequence: []map[string]int{ - {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, - }, - key: "b", op: ">", match: 3, - expect: []map[string]int{ - {"a": 3, "b": 4}, {"a": 5, "b": 6}, - }, - }, - { - sequence: []TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, - }, - key: "B", op: "!=", match: "f", - expect: []TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, - }, - }, - { - sequence: []map[string]int{ - {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, - }, - key: "b", op: "in", match: []int{3, 4, 5}, - expect: []map[string]int{ - {"a": 3, "b": 4}, - }, - }, - { - sequence: []map[string][]string{ - {"a": []string{"A", "B", "C"}, "b": []string{"D", "E", "F"}}, {"a": []string{"G", "H", "I"}, "b": []string{"J", "K", "L"}}, {"a": []string{"M", "N", "O"}, "b": []string{"P", "Q", "R"}}, - }, - key: "b", op: "intersect", match: []string{"D", "P", "Q"}, - expect: []map[string][]string{ - {"a": []string{"A", "B", "C"}, "b": []string{"D", "E", "F"}}, {"a": []string{"M", "N", "O"}, "b": []string{"P", "Q", "R"}}, - }, - }, - { - sequence: []map[string][]int{ - {"a": []int{1, 2, 3}, "b": []int{4, 5, 6}}, {"a": []int{7, 8, 9}, "b": []int{10, 11, 12}}, {"a": []int{13, 14, 15}, "b": []int{16, 17, 18}}, - }, - key: "b", op: "intersect", match: []int{4, 10, 12}, - expect: []map[string][]int{ - {"a": []int{1, 2, 3}, "b": []int{4, 5, 6}}, {"a": []int{7, 8, 9}, "b": []int{10, 11, 12}}, - }, - }, - { - sequence: []map[string][]int8{ - {"a": []int8{1, 2, 3}, "b": []int8{4, 5, 6}}, {"a": []int8{7, 8, 9}, "b": []int8{10, 11, 12}}, {"a": []int8{13, 14, 15}, "b": []int8{16, 17, 18}}, - }, - key: "b", op: "intersect", match: []int8{4, 10, 12}, - expect: []map[string][]int8{ - {"a": []int8{1, 2, 3}, "b": []int8{4, 5, 6}}, {"a": []int8{7, 8, 9}, "b": []int8{10, 11, 12}}, - }, - }, - { - sequence: []map[string][]int16{ - {"a": []int16{1, 2, 3}, "b": []int16{4, 5, 6}}, {"a": []int16{7, 8, 9}, "b": []int16{10, 11, 12}}, {"a": []int16{13, 14, 15}, "b": []int16{16, 17, 18}}, - }, - key: "b", op: "intersect", match: []int16{4, 10, 12}, - expect: []map[string][]int16{ - {"a": []int16{1, 2, 3}, "b": []int16{4, 5, 6}}, {"a": []int16{7, 8, 9}, "b": []int16{10, 11, 12}}, - }, - }, - { - sequence: []map[string][]int32{ - {"a": []int32{1, 2, 3}, "b": []int32{4, 5, 6}}, {"a": []int32{7, 8, 9}, "b": []int32{10, 11, 12}}, {"a": []int32{13, 14, 15}, "b": []int32{16, 17, 18}}, - }, - key: "b", op: "intersect", match: []int32{4, 10, 12}, - expect: []map[string][]int32{ - {"a": []int32{1, 2, 3}, "b": []int32{4, 5, 6}}, {"a": []int32{7, 8, 9}, "b": []int32{10, 11, 12}}, - }, - }, - { - sequence: []map[string][]int64{ - {"a": []int64{1, 2, 3}, "b": []int64{4, 5, 6}}, {"a": []int64{7, 8, 9}, "b": []int64{10, 11, 12}}, {"a": []int64{13, 14, 15}, "b": []int64{16, 17, 18}}, - }, - key: "b", op: "intersect", match: []int64{4, 10, 12}, - expect: []map[string][]int64{ - {"a": []int64{1, 2, 3}, "b": []int64{4, 5, 6}}, {"a": []int64{7, 8, 9}, "b": []int64{10, 11, 12}}, - }, - }, - { - sequence: []map[string][]float32{ - {"a": []float32{1.0, 2.0, 3.0}, "b": []float32{4.0, 5.0, 6.0}}, {"a": []float32{7.0, 8.0, 9.0}, "b": []float32{10.0, 11.0, 12.0}}, {"a": []float32{13.0, 14.0, 15.0}, "b": []float32{16.0, 17.0, 18.0}}, - }, - key: "b", op: "intersect", match: []float32{4, 10, 12}, - expect: []map[string][]float32{ - {"a": []float32{1.0, 2.0, 3.0}, "b": []float32{4.0, 5.0, 6.0}}, {"a": []float32{7.0, 8.0, 9.0}, "b": []float32{10.0, 11.0, 12.0}}, - }, - }, - { - sequence: []map[string][]float64{ - {"a": []float64{1.0, 2.0, 3.0}, "b": []float64{4.0, 5.0, 6.0}}, {"a": []float64{7.0, 8.0, 9.0}, "b": []float64{10.0, 11.0, 12.0}}, {"a": []float64{13.0, 14.0, 15.0}, "b": []float64{16.0, 17.0, 18.0}}, - }, - key: "b", op: "intersect", match: []float64{4, 10, 12}, - expect: []map[string][]float64{ - {"a": []float64{1.0, 2.0, 3.0}, "b": []float64{4.0, 5.0, 6.0}}, {"a": []float64{7.0, 8.0, 9.0}, "b": []float64{10.0, 11.0, 12.0}}, - }, - }, - { - sequence: []map[string]int{ - {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, - }, - key: "b", op: "in", match: slice(3, 4, 5), - expect: []map[string]int{ - {"a": 3, "b": 4}, - }, - }, - { - sequence: []map[string]time.Time{ - {"a": d1, "b": d2}, {"a": d3, "b": d4}, {"a": d5, "b": d6}, - }, - key: "b", op: "in", match: slice(d3, d4, d5), - expect: []map[string]time.Time{ - {"a": d3, "b": d4}, - }, - }, - { - sequence: []TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, - }, - key: "B", op: "not in", match: []string{"c", "d", "e"}, - expect: []TstX{ - {A: "a", B: "b"}, {A: "e", B: "f"}, - }, - }, - { - sequence: []TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, - }, - key: "B", op: "not in", match: slice("c", t, "d", "e"), - expect: []TstX{ - {A: "a", B: "b"}, {A: "e", B: "f"}, - }, - }, - { - sequence: []map[string]int{ - {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, - }, - key: "b", op: "", match: nil, - expect: []map[string]int{ - {"a": 3}, - }, - }, - { - sequence: []map[string]int{ - {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, - }, - key: "b", op: "!=", match: nil, - expect: []map[string]int{ - {"a": 1, "b": 2}, {"a": 5, "b": 6}, - }, - }, - { - sequence: []map[string]int{ - {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, - }, - key: "b", op: ">", match: nil, - expect: []map[string]int{}, - }, - { - sequence: []map[string]bool{ - {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, - }, - key: "b", op: "", match: true, - expect: []map[string]bool{ - {"c": true, "b": true}, - }, - }, - { - sequence: []map[string]bool{ - {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, - }, - key: "b", op: "!=", match: true, - expect: []map[string]bool{ - {"a": true, "b": false}, {"d": true, "b": false}, - }, - }, - { - sequence: []map[string]bool{ - {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, - }, - key: "b", op: ">", match: false, - expect: []map[string]bool{}, - }, - {sequence: (*[]TstX)(nil), key: "A", match: "a", expect: false}, - {sequence: TstX{A: "a", B: "b"}, key: "A", match: "a", expect: false}, - {sequence: []map[string]*TstX{{"foo": nil}}, key: "foo.B", match: "d", expect: false}, - { - sequence: []TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, - }, - key: "B", op: "op", match: "f", - expect: false, - }, - { - sequence: map[string]interface{}{ - "foo": []interface{}{map[interface{}]interface{}{"a": 1, "b": 2}}, - "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, - "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, - }, - key: "b", op: "in", match: slice(3, 4, 5), - expect: map[string]interface{}{ - "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, - }, - }, - { - sequence: map[string]interface{}{ - "foo": []interface{}{map[interface{}]interface{}{"a": 1, "b": 2}}, - "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, - "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, - }, - key: "b", op: ">", match: 3, - expect: map[string]interface{}{ - "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, - "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, - }, - }, - } { - var results interface{} - var err error - - if len(this.op) > 0 { - results, err = where(this.sequence, this.key, this.op, this.match) - } else { - results, err = where(this.sequence, this.key, this.match) - } - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] Where didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(results, this.expect) { - t.Errorf("[%d] Where clause matching %v with %v, got %v but expected %v", i, this.key, this.match, results, this.expect) - } - } - } - - var err error - _, err = where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1) - if err == nil { - t.Errorf("Where called with none string op value didn't return an expected error") - } - - _, err = where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1, 2) - if err == nil { - t.Errorf("Where called with more than two variable arguments didn't return an expected error") - } - - _, err = where(map[string]int{"a": 1, "b": 2}, "a") - if err == nil { - t.Errorf("Where called with no variable arguments didn't return an expected error") - } -} - -func TestDelimit(t *testing.T) { - t.Parallel() - for i, this := range []struct { - sequence interface{} - delimiter interface{} - last interface{} - expect template.HTML - }{ - {[]string{"class1", "class2", "class3"}, " ", nil, "class1 class2 class3"}, - {[]int{1, 2, 3, 4, 5}, ",", nil, "1,2,3,4,5"}, - {[]int{1, 2, 3, 4, 5}, ", ", nil, "1, 2, 3, 4, 5"}, - {[]string{"class1", "class2", "class3"}, " ", " and ", "class1 class2 and class3"}, - {[]int{1, 2, 3, 4, 5}, ",", ",", "1,2,3,4,5"}, - {[]int{1, 2, 3, 4, 5}, ", ", ", and ", "1, 2, 3, 4, and 5"}, - // test maps with and without sorting required - {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "--", nil, "10--20--30--40--50"}, - {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "--", nil, "30--20--10--40--50"}, - {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, "--", nil, "10--20--30--40--50"}, - {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, "--", nil, "30--20--10--40--50"}, - {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, "--", nil, "50--40--10--30--20"}, - {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, "--", nil, "10--20--30--40--50"}, - {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, "--", nil, "30--20--10--40--50"}, - {map[float64]string{3.3: "10", 2.3: "20", 1.3: "30", 4.3: "40", 5.3: "50"}, "--", nil, "30--20--10--40--50"}, - // test maps with a last delimiter - {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "--", "--and--", "10--20--30--40--and--50"}, - {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "--", "--and--", "30--20--10--40--and--50"}, - {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, "--", "--and--", "10--20--30--40--and--50"}, - {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, "--", "--and--", "30--20--10--40--and--50"}, - {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, "--", "--and--", "50--40--10--30--and--20"}, - {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, "--", "--and--", "10--20--30--40--and--50"}, - {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, "--", "--and--", "30--20--10--40--and--50"}, - {map[float64]string{3.5: "10", 2.5: "20", 1.5: "30", 4.5: "40", 5.5: "50"}, "--", "--and--", "30--20--10--40--and--50"}, - } { - var result template.HTML - var err error - if this.last == nil { - result, err = delimit(this.sequence, this.delimiter) - } else { - result, err = delimit(this.sequence, this.delimiter, this.last) - } - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] Delimit called on sequence: %v | delimiter: `%v` | last: `%v`, got %v but expected %v", i, this.sequence, this.delimiter, this.last, result, this.expect) - } - } -} - -func TestSort(t *testing.T) { - t.Parallel() - type ts struct { - MyInt int - MyFloat float64 - MyString string - } - type mid struct { - Tst TstX - } - - for i, this := range []struct { - sequence interface{} - sortByField interface{} - sortAsc string - expect interface{} - }{ - {[]string{"class1", "class2", "class3"}, nil, "asc", []string{"class1", "class2", "class3"}}, - {[]string{"class3", "class1", "class2"}, nil, "asc", []string{"class1", "class2", "class3"}}, - {[]int{1, 2, 3, 4, 5}, nil, "asc", []int{1, 2, 3, 4, 5}}, - {[]int{5, 4, 3, 1, 2}, nil, "asc", []int{1, 2, 3, 4, 5}}, - // test sort key parameter is focibly set empty - {[]string{"class3", "class1", "class2"}, map[int]string{1: "a"}, "asc", []string{"class1", "class2", "class3"}}, - // test map sorting by keys - {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, nil, "asc", []int{10, 20, 30, 40, 50}}, - {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, nil, "asc", []int{30, 20, 10, 40, 50}}, - {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, nil, "asc", []string{"10", "20", "30", "40", "50"}}, - {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, - {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, nil, "asc", []string{"50", "40", "10", "30", "20"}}, - {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, nil, "asc", []string{"10", "20", "30", "40", "50"}}, - {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, - {map[float64]string{3.3: "10", 2.3: "20", 1.3: "30", 4.3: "40", 5.3: "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, - // test map sorting by value - {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "value", "asc", []int{10, 20, 30, 40, 50}}, - {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "value", "asc", []int{10, 20, 30, 40, 50}}, - // test map sorting by field value - { - map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, - "MyInt", - "asc", - []ts{{10, 10.5, "ten"}, {20, 20.5, "twenty"}, {30, 30.5, "thirty"}, {40, 40.5, "forty"}, {50, 50.5, "fifty"}}, - }, - { - map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, - "MyFloat", - "asc", - []ts{{10, 10.5, "ten"}, {20, 20.5, "twenty"}, {30, 30.5, "thirty"}, {40, 40.5, "forty"}, {50, 50.5, "fifty"}}, - }, - { - map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, - "MyString", - "asc", - []ts{{50, 50.5, "fifty"}, {40, 40.5, "forty"}, {10, 10.5, "ten"}, {30, 30.5, "thirty"}, {20, 20.5, "twenty"}}, - }, - // test sort desc - {[]string{"class1", "class2", "class3"}, "value", "desc", []string{"class3", "class2", "class1"}}, - {[]string{"class3", "class1", "class2"}, "value", "desc", []string{"class3", "class2", "class1"}}, - // test sort by struct's method - { - []TstX{{A: "i", B: "j"}, {A: "e", B: "f"}, {A: "c", B: "d"}, {A: "g", B: "h"}, {A: "a", B: "b"}}, - "TstRv", - "asc", - []TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, - }, - { - []*TstX{{A: "i", B: "j"}, {A: "e", B: "f"}, {A: "c", B: "d"}, {A: "g", B: "h"}, {A: "a", B: "b"}}, - "TstRp", - "asc", - []*TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, - }, - // test map sorting by struct's method - { - map[string]TstX{"1": {A: "i", B: "j"}, "2": {A: "e", B: "f"}, "3": {A: "c", B: "d"}, "4": {A: "g", B: "h"}, "5": {A: "a", B: "b"}}, - "TstRv", - "asc", - []TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, - }, - { - map[string]*TstX{"1": {A: "i", B: "j"}, "2": {A: "e", B: "f"}, "3": {A: "c", B: "d"}, "4": {A: "g", B: "h"}, "5": {A: "a", B: "b"}}, - "TstRp", - "asc", - []*TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, - }, - // test sort by dot chaining key argument - { - []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, - "foo.A", - "asc", - []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, - }, - { - []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, - ".foo.A", - "asc", - []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, - }, - { - []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, - "foo.TstRv", - "asc", - []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, - }, - { - []map[string]*TstX{{"foo": &TstX{A: "e", B: "f"}}, {"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}}, - "foo.TstRp", - "asc", - []map[string]*TstX{{"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}}, - }, - { - []map[string]mid{{"foo": mid{Tst: TstX{A: "e", B: "f"}}}, {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, - "foo.Tst.A", - "asc", - []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, - }, - { - []map[string]mid{{"foo": mid{Tst: TstX{A: "e", B: "f"}}}, {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, - "foo.Tst.TstRv", - "asc", - []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, - }, - // test map sorting by dot chaining key argument - { - map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, - "foo.A", - "asc", - []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, - }, - { - map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, - ".foo.A", - "asc", - []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, - }, - { - map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, - "foo.TstRv", - "asc", - []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, - }, - { - map[string]map[string]*TstX{"1": {"foo": &TstX{A: "e", B: "f"}}, "2": {"foo": &TstX{A: "a", B: "b"}}, "3": {"foo": &TstX{A: "c", B: "d"}}}, - "foo.TstRp", - "asc", - []map[string]*TstX{{"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}}, - }, - { - map[string]map[string]mid{"1": {"foo": mid{Tst: TstX{A: "e", B: "f"}}}, "2": {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, "3": {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, - "foo.Tst.A", - "asc", - []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, - }, - { - map[string]map[string]mid{"1": {"foo": mid{Tst: TstX{A: "e", B: "f"}}}, "2": {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, "3": {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, - "foo.Tst.TstRv", - "asc", - []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, - }, - // interface slice with missing elements - { - []interface{}{ - map[interface{}]interface{}{"Title": "Foo", "Weight": 10}, - map[interface{}]interface{}{"Title": "Bar"}, - map[interface{}]interface{}{"Title": "Zap", "Weight": 5}, - }, - "Weight", - "asc", - []interface{}{ - map[interface{}]interface{}{"Title": "Bar"}, - map[interface{}]interface{}{"Title": "Zap", "Weight": 5}, - map[interface{}]interface{}{"Title": "Foo", "Weight": 10}, - }, - }, - // test error cases - {(*[]TstX)(nil), nil, "asc", false}, - {TstX{A: "a", B: "b"}, nil, "asc", false}, - { - []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, - "foo.NotAvailable", - "asc", - false, - }, - { - map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, - "foo.NotAvailable", - "asc", - false, - }, - {nil, nil, "asc", false}, - } { - var result interface{} - var err error - if this.sortByField == nil { - result, err = sortSeq(this.sequence) - } else { - result, err = sortSeq(this.sequence, this.sortByField, this.sortAsc) - } - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] Sort didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] Sort called on sequence: %v | sortByField: `%v` | got %v but expected %v", i, this.sequence, this.sortByField, result, this.expect) - } - } - } -} - -func TestReturnWhenSet(t *testing.T) { - t.Parallel() - for i, this := range []struct { - data interface{} - key interface{} - expect interface{} - }{ - {[]int{1, 2, 3}, 1, int64(2)}, - {[]uint{1, 2, 3}, 1, uint64(2)}, - {[]float64{1.1, 2.2, 3.3}, 1, float64(2.2)}, - {[]string{"foo", "bar", "baz"}, 1, "bar"}, - {[]TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}}, 1, ""}, - {map[string]int{"foo": 1, "bar": 2, "baz": 3}, "bar", int64(2)}, - {map[string]uint{"foo": 1, "bar": 2, "baz": 3}, "bar", uint64(2)}, - {map[string]float64{"foo": 1.1, "bar": 2.2, "baz": 3.3}, "bar", float64(2.2)}, - {map[string]string{"foo": "FOO", "bar": "BAR", "baz": "BAZ"}, "bar", "BAR"}, - {map[string]TstX{"foo": {A: "a", B: "b"}, "bar": {A: "c", B: "d"}, "baz": {A: "e", B: "f"}}, "bar", ""}, - {(*[]string)(nil), "bar", ""}, - } { - result := returnWhenSet(this.data, this.key) - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] ReturnWhenSet got %v (type %v) but expected %v (type %v)", i, result, reflect.TypeOf(result), this.expect, reflect.TypeOf(this.expect)) - } - } -} - -func TestMarkdownify(t *testing.T) { - t.Parallel() - v := viper.New() - - f := newTestFuncsterWithViper(v) - - for i, this := range []struct { - in interface{} - expect interface{} - }{ - {"Hello **World!**", template.HTML("Hello World!")}, - {[]byte("Hello Bytes **World!**"), template.HTML("Hello Bytes World!")}, - } { - result, err := f.markdownify(this.in) - if err != nil { - t.Fatalf("[%d] unexpected error in markdownify: %s", i, err) - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] markdownify got %v (type %v) but expected %v (type %v)", i, result, reflect.TypeOf(result), this.expect, reflect.TypeOf(this.expect)) - } - } - - if _, err := f.markdownify(t); err == nil { - t.Fatalf("markdownify should have errored") - } -} - -func TestApply(t *testing.T) { - t.Parallel() - - f := newTestFuncster() - - strings := []interface{}{"a\n", "b\n"} - noStringers := []interface{}{tstNoStringer{}, tstNoStringer{}} - - chomped, _ := f.apply(strings, "chomp", ".") - assert.Equal(t, []interface{}{template.HTML("a"), template.HTML("b")}, chomped) - - chomped, _ = f.apply(strings, "chomp", "c\n") - assert.Equal(t, []interface{}{template.HTML("c"), template.HTML("c")}, chomped) - - chomped, _ = f.apply(nil, "chomp", ".") - assert.Equal(t, []interface{}{}, chomped) - - _, err := f.apply(strings, "apply", ".") - if err == nil { - t.Errorf("apply with apply should fail") - } - - var nilErr *error - _, err = f.apply(nilErr, "chomp", ".") - if err == nil { - t.Errorf("apply with nil in seq should fail") - } - - _, err = f.apply(strings, "dobedobedo", ".") - if err == nil { - t.Errorf("apply with unknown func should fail") - } - - _, err = f.apply(noStringers, "chomp", ".") - if err == nil { - t.Errorf("apply when func fails should fail") - } - - _, err = f.apply(tstNoStringer{}, "chomp", ".") - if err == nil { - t.Errorf("apply with non-sequence should fail") - } -} - -func TestChomp(t *testing.T) { - t.Parallel() - base := "\n This is\na story " - for i, item := range []string{ - "\n", "\n\n", - "\r", "\r\r", - "\r\n", "\r\n\r\n", - } { - c, _ := chomp(base + item) - chomped := string(c) - - if chomped != base { - t.Errorf("[%d] Chomp failed, got '%v'", i, chomped) - } - - _, err := chomp(tstNoStringer{}) - - if err == nil { - t.Errorf("Chomp should fail") - } - } -} - -func TestLower(t *testing.T) { - t.Parallel() - cases := []struct { - s interface{} - want string - isErr bool - }{ - {"TEST", "test", false}, - {template.HTML("LoWeR"), "lower", false}, - {[]byte("BYTES"), "bytes", false}, - } - - for i, c := range cases { - res, err := lower(c.s) - if (err != nil) != c.isErr { - t.Fatalf("[%d] unexpected isErr state: want %v, got %v, err = %v", i, c.want, (err != nil), err) - } - - if res != c.want { - t.Errorf("[%d] lower failed: want %v, got %v", i, c.want, res) - } - } -} - -func TestTitle(t *testing.T) { - t.Parallel() - cases := []struct { - s interface{} - want string - isErr bool - }{ - {"test", "Test", false}, - {template.HTML("hypertext"), "Hypertext", false}, - {[]byte("bytes"), "Bytes", false}, - } - - for i, c := range cases { - res, err := title(c.s) - if (err != nil) != c.isErr { - t.Fatalf("[%d] unexpected isErr state: want %v, got %v, err = %v", i, c.want, (err != nil), err) - } - - if res != c.want { - t.Errorf("[%d] title failed: want %v, got %v", i, c.want, res) - } - } -} - -func TestUpper(t *testing.T) { - t.Parallel() - cases := []struct { - s interface{} - want string - isErr bool - }{ - {"test", "TEST", false}, - {template.HTML("UpPeR"), "UPPER", false}, - {[]byte("bytes"), "BYTES", false}, - } - - for i, c := range cases { - res, err := upper(c.s) - if (err != nil) != c.isErr { - t.Fatalf("[%d] unexpected isErr state: want %v, got %v, err = %v", i, c.want, (err != nil), err) - } - - if res != c.want { - t.Errorf("[%d] upper failed: want %v, got %v", i, c.want, res) - } - } -} - -func TestHighlight(t *testing.T) { - t.Parallel() - code := "func boo() {}" - - f := newTestFuncster() - - highlighted, err := f.highlight(code, "go", "") - - if err != nil { - t.Fatal("Highlight returned error:", err) - } - - // this depends on a Pygments installation, but will always contain the function name. - if !strings.Contains(string(highlighted), "boo") { - t.Errorf("Highlight mismatch, got %v", highlighted) - } - - _, err = f.highlight(t, "go", "") - - if err == nil { - t.Error("Expected highlight error") - } -} - -func TestInflect(t *testing.T) { - t.Parallel() - for i, this := range []struct { - inflectFunc func(i interface{}) (string, error) - in interface{} - expected string - }{ - {humanize, "MyCamel", "My camel"}, - {humanize, "", ""}, - {humanize, "103", "103rd"}, - {humanize, "41", "41st"}, - {humanize, 103, "103rd"}, - {humanize, int64(92), "92nd"}, - {humanize, "5.5", "5.5"}, - {pluralize, "cat", "cats"}, - {pluralize, "", ""}, - {singularize, "cats", "cat"}, - {singularize, "", ""}, - } { - - result, err := this.inflectFunc(this.in) - - if err != nil { - t.Errorf("[%d] Unexpected Inflect error: %s", i, err) - } else if result != this.expected { - t.Errorf("[%d] Inflect method error, got %v expected %v", i, result, this.expected) - } - - _, err = this.inflectFunc(t) - if err == nil { - t.Errorf("[%d] Expected Inflect error", i) - } - } -} - -func TestCounterFuncs(t *testing.T) { - t.Parallel() - for i, this := range []struct { - countFunc func(i interface{}) (int, error) - in string - expected int - }{ - {countWords, "Do Be Do Be Do", 5}, - {countWords, "旁边", 2}, - {countRunes, "旁边", 2}, - } { - - result, err := this.countFunc(this.in) - - if err != nil { - t.Errorf("[%d] Unexpected counter error: %s", i, err) - } else if result != this.expected { - t.Errorf("[%d] Count method error, got %v expected %v", i, result, this.expected) - } - - _, err = this.countFunc(t) - if err == nil { - t.Errorf("[%d] Expected Count error", i) - } - } -} - -func TestReplace(t *testing.T) { - t.Parallel() - v, _ := replace("aab", "a", "b") - assert.Equal(t, "bbb", v) - v, _ = replace("11a11", 1, 2) - assert.Equal(t, "22a22", v) - v, _ = replace(12345, 1, 2) - assert.Equal(t, "22345", v) - _, e := replace(tstNoStringer{}, "a", "b") - assert.NotNil(t, e, "tstNoStringer isn't trimmable") - _, e = replace("a", tstNoStringer{}, "b") - assert.NotNil(t, e, "tstNoStringer cannot be converted to string") - _, e = replace("a", "b", tstNoStringer{}) - assert.NotNil(t, e, "tstNoStringer cannot be converted to string") -} - -func TestReplaceRE(t *testing.T) { - t.Parallel() - for i, val := range []struct { - pattern interface{} - repl interface{} - src interface{} - expect string - ok bool - }{ - {"^https?://([^/]+).*", "$1", "http://gohugo.io/docs", "gohugo.io", true}, - {"^https?://([^/]+).*", "$2", "http://gohugo.io/docs", "", true}, - {tstNoStringer{}, "$2", "http://gohugo.io/docs", "", false}, - {"^https?://([^/]+).*", tstNoStringer{}, "http://gohugo.io/docs", "", false}, - {"^https?://([^/]+).*", "$2", tstNoStringer{}, "", false}, - {"(ab)", "AB", "aabbaab", "aABbaAB", true}, - {"(ab", "AB", "aabb", "", false}, // invalid re - } { - v, err := replaceRE(val.pattern, val.repl, val.src) - if (err == nil) != val.ok { - t.Errorf("[%d] %s", i, err) - } - assert.Equal(t, val.expect, v) - } -} - -func TestFindRE(t *testing.T) { - t.Parallel() - for i, this := range []struct { - expr string - content interface{} - limit interface{} - expect []string - ok bool - }{ - {"[G|g]o", "Hugo is a static site generator written in Go.", 2, []string{"go", "Go"}, true}, - {"[G|g]o", "Hugo is a static site generator written in Go.", -1, []string{"go", "Go"}, true}, - {"[G|g]o", "Hugo is a static site generator written in Go.", 1, []string{"go"}, true}, - {"[G|g]o", "Hugo is a static site generator written in Go.", "1", []string{"go"}, true}, - {"[G|g]o", "Hugo is a static site generator written in Go.", nil, []string(nil), true}, - {"[G|go", "Hugo is a static site generator written in Go.", nil, []string(nil), false}, - {"[G|g]o", t, nil, []string(nil), false}, - } { - var ( - res []string - err error - ) - - res, err = findRE(this.expr, this.content, this.limit) - if err != nil && this.ok { - t.Errorf("[%d] returned an unexpected error: %s", i, err) - } - - assert.Equal(t, this.expect, res) - } -} - -func TestTrim(t *testing.T) { - t.Parallel() - - for i, this := range []struct { - v1 interface{} - v2 string - expect interface{} - }{ - {"1234 my way 13", "123 ", "4 my way"}, - {" my way ", " ", "my way"}, - {1234, "14", "23"}, - {tstNoStringer{}, " ", false}, - } { - result, err := trim(this.v1, this.v2) - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] trim didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] got '%s' but expected %s", i, result, this.expect) - } - } - } -} - -func TestDateFormat(t *testing.T) { - t.Parallel() - for i, this := range []struct { - layout string - value interface{} - expect interface{} - }{ - {"Monday, Jan 2, 2006", "2015-01-21", "Wednesday, Jan 21, 2015"}, - {"Monday, Jan 2, 2006", time.Date(2015, time.January, 21, 0, 0, 0, 0, time.UTC), "Wednesday, Jan 21, 2015"}, - {"This isn't a date layout string", "2015-01-21", "This isn't a date layout string"}, - // The following test case gives either "Tuesday, Jan 20, 2015" or "Monday, Jan 19, 2015" depending on the local time zone - {"Monday, Jan 2, 2006", 1421733600, time.Unix(1421733600, 0).Format("Monday, Jan 2, 2006")}, - {"Monday, Jan 2, 2006", 1421733600.123, false}, - {time.RFC3339, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), "2016-03-03T04:05:00Z"}, - {time.RFC1123, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), "Thu, 03 Mar 2016 04:05:00 UTC"}, - {time.RFC3339, "Thu, 03 Mar 2016 04:05:00 UTC", "2016-03-03T04:05:00Z"}, - {time.RFC1123, "2016-03-03T04:05:00Z", "Thu, 03 Mar 2016 04:05:00 UTC"}, - } { - result, err := dateFormat(this.layout, this.value) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] DateFormat didn't return an expected error, got %v", i, result) - } - } else { - if err != nil { - t.Errorf("[%d] DateFormat failed: %s", i, err) - continue - } - if result != this.expect { - t.Errorf("[%d] DateFormat got %v but expected %v", i, result, this.expect) - } - } - } -} - -func TestDefaultFunc(t *testing.T) { - t.Parallel() - then := time.Now() - now := time.Now() - - for i, this := range []struct { - dflt interface{} - given interface{} - expected interface{} - }{ - {true, false, false}, - {"5", 0, "5"}, - - {"test1", "set", "set"}, - {"test2", "", "test2"}, - {"test3", nil, "test3"}, - - {[2]int{10, 20}, [2]int{1, 2}, [2]int{1, 2}}, - {[2]int{10, 20}, [0]int{}, [2]int{10, 20}}, - {[2]int{100, 200}, nil, [2]int{100, 200}}, - - {[]string{"one"}, []string{"uno"}, []string{"uno"}}, - {[]string{"two"}, []string{}, []string{"two"}}, - {[]string{"three"}, nil, []string{"three"}}, - - {map[string]int{"one": 1}, map[string]int{"uno": 1}, map[string]int{"uno": 1}}, - {map[string]int{"one": 1}, map[string]int{}, map[string]int{"one": 1}}, - {map[string]int{"two": 2}, nil, map[string]int{"two": 2}}, - - {10, 1, 1}, - {10, 0, 10}, - {20, nil, 20}, - - {float32(10), float32(1), float32(1)}, - {float32(10), 0, float32(10)}, - {float32(20), nil, float32(20)}, - - {complex(2, -2), complex(1, -1), complex(1, -1)}, - {complex(2, -2), complex(0, 0), complex(2, -2)}, - {complex(3, -3), nil, complex(3, -3)}, - - {struct{ f string }{f: "one"}, struct{ f string }{}, struct{ f string }{}}, - {struct{ f string }{f: "two"}, nil, struct{ f string }{f: "two"}}, - - {then, now, now}, - {then, time.Time{}, then}, - } { - res, err := dfault(this.dflt, this.given) - if err != nil { - t.Errorf("[%d] default returned an error: %s", i, err) - continue - } - if !reflect.DeepEqual(this.expected, res) { - t.Errorf("[%d] default returned %v, but expected %v", i, res, this.expected) - } - } -} - -func TestDefault(t *testing.T) { - t.Parallel() - for i, this := range []struct { - input interface{} - tpl string - expected string - ok bool - }{ - {map[string]string{"foo": "bar"}, `{{ index . "foo" | default "nope" }}`, `bar`, true}, - {map[string]string{"foo": "pop"}, `{{ index . "bar" | default "nada" }}`, `nada`, true}, - {map[string]string{"foo": "cat"}, `{{ default "nope" .foo }}`, `cat`, true}, - {map[string]string{"foo": "dog"}, `{{ default "nope" .foo "extra" }}`, ``, false}, - {map[string]interface{}{"images": []string{}}, `{{ default "default.jpg" (index .images 0) }}`, `default.jpg`, true}, - } { - - tmpl := newTestTemplate(t, "test", this.tpl) - - buf := new(bytes.Buffer) - err := tmpl.Execute(buf, this.input) - if (err == nil) != this.ok { - t.Errorf("[%d] execute template returned unexpected error: %s", i, err) - continue - } - - if buf.String() != this.expected { - t.Errorf("[%d] execute template got %v, but expected %v", i, buf.String(), this.expected) - } - } -} - -func TestSafeHTML(t *testing.T) { - t.Parallel() - for i, this := range []struct { - str string - tmplStr string - expectWithoutEscape string - expectWithEscape string - }{ - {`
`, `{{ . }}`, `<div></div>`, `
`}, - } { - tmpl, err := template.New("test").Parse(this.tmplStr) - if err != nil { - t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) - continue - } - - buf := new(bytes.Buffer) - err = tmpl.Execute(buf, this.str) - if err != nil { - t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithoutEscape { - t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) - } - - buf.Reset() - v, err := safeHTML(this.str) - if err != nil { - t.Fatalf("[%d] unexpected error in safeHTML: %s", i, err) - } - - err = tmpl.Execute(buf, v) - if err != nil { - t.Errorf("[%d] execute template with an escaped string value by safeHTML returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithEscape { - t.Errorf("[%d] execute template with an escaped string value by safeHTML, got %v but expected %v", i, buf.String(), this.expectWithEscape) - } - } -} - -func TestSafeHTMLAttr(t *testing.T) { - t.Parallel() - for i, this := range []struct { - str string - tmplStr string - expectWithoutEscape string - expectWithEscape string - }{ - {`href="irc://irc.freenode.net/#golang"`, `irc`, `irc`, `irc`}, - } { - tmpl, err := template.New("test").Parse(this.tmplStr) - if err != nil { - t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) - continue - } - - buf := new(bytes.Buffer) - err = tmpl.Execute(buf, this.str) - if err != nil { - t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithoutEscape { - t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) - } - - buf.Reset() - v, err := safeHTMLAttr(this.str) - if err != nil { - t.Fatalf("[%d] unexpected error in safeHTMLAttr: %s", i, err) - } - - err = tmpl.Execute(buf, v) - if err != nil { - t.Errorf("[%d] execute template with an escaped string value by safeHTMLAttr returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithEscape { - t.Errorf("[%d] execute template with an escaped string value by safeHTMLAttr, got %v but expected %v", i, buf.String(), this.expectWithEscape) - } - } -} - -func TestSafeCSS(t *testing.T) { - t.Parallel() - for i, this := range []struct { - str string - tmplStr string - expectWithoutEscape string - expectWithEscape string - }{ - {`width: 60px;`, `
`, `
`, `
`}, - } { - tmpl, err := template.New("test").Parse(this.tmplStr) - if err != nil { - t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) - continue - } - - buf := new(bytes.Buffer) - err = tmpl.Execute(buf, this.str) - if err != nil { - t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithoutEscape { - t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) - } - - buf.Reset() - v, err := safeCSS(this.str) - if err != nil { - t.Fatalf("[%d] unexpected error in safeCSS: %s", i, err) - } - - err = tmpl.Execute(buf, v) - if err != nil { - t.Errorf("[%d] execute template with an escaped string value by safeCSS returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithEscape { - t.Errorf("[%d] execute template with an escaped string value by safeCSS, got %v but expected %v", i, buf.String(), this.expectWithEscape) - } - } -} - -// TODO(bep) what is this? Also look above. -func TestSafeJS(t *testing.T) { - t.Parallel() - for i, this := range []struct { - str string - tmplStr string - expectWithoutEscape string - expectWithEscape string - }{ - {`619c16f`, ``, ``, ``}, - } { - tmpl, err := template.New("test").Parse(this.tmplStr) - if err != nil { - t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) - continue - } - - buf := new(bytes.Buffer) - err = tmpl.Execute(buf, this.str) - if err != nil { - t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithoutEscape { - t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) - } - - buf.Reset() - v, err := safeJS(this.str) - if err != nil { - t.Fatalf("[%d] unexpected error in safeJS: %s", i, err) - } - - err = tmpl.Execute(buf, v) - if err != nil { - t.Errorf("[%d] execute template with an escaped string value by safeJS returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithEscape { - t.Errorf("[%d] execute template with an escaped string value by safeJS, got %v but expected %v", i, buf.String(), this.expectWithEscape) - } - } -} - -// TODO(bep) what is this? -func TestSafeURL(t *testing.T) { - t.Parallel() - for i, this := range []struct { - str string - tmplStr string - expectWithoutEscape string - expectWithEscape string - }{ - {`irc://irc.freenode.net/#golang`, `IRC`, `IRC`, `IRC`}, - } { - tmpl, err := template.New("test").Parse(this.tmplStr) - if err != nil { - t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) - continue - } - - buf := new(bytes.Buffer) - err = tmpl.Execute(buf, this.str) - if err != nil { - t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithoutEscape { - t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) - } - - buf.Reset() - v, err := safeURL(this.str) - if err != nil { - t.Fatalf("[%d] unexpected error in safeURL: %s", i, err) - } - - err = tmpl.Execute(buf, v) - if err != nil { - t.Errorf("[%d] execute template with an escaped string value by safeURL returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithEscape { - t.Errorf("[%d] execute template with an escaped string value by safeURL, got %v but expected %v", i, buf.String(), this.expectWithEscape) - } - } -} - -func TestBase64Decode(t *testing.T) { - t.Parallel() - testStr := "abc123!?$*&()'-=@~" - enc := base64.StdEncoding.EncodeToString([]byte(testStr)) - result, err := base64Decode(enc) - - if err != nil { - t.Error("base64Decode returned error:", err) - } - - if result != testStr { - t.Errorf("base64Decode: got '%s', expected '%s'", result, testStr) - } - - _, err = base64Decode(t) - if err == nil { - t.Error("Expected error from base64Decode") - } -} - -func TestBase64Encode(t *testing.T) { - t.Parallel() - testStr := "YWJjMTIzIT8kKiYoKSctPUB+" - dec, err := base64.StdEncoding.DecodeString(testStr) - - if err != nil { - t.Error("base64Encode: the DecodeString function of the base64 package returned an error:", err) - } - - result, err := base64Encode(string(dec)) - - if err != nil { - t.Errorf("base64Encode: Can't cast arg '%s' into a string:", testStr) - } - - if result != testStr { - t.Errorf("base64Encode: got '%s', expected '%s'", result, testStr) - } - - _, err = base64Encode(t) - if err == nil { - t.Error("Expected error from base64Encode") - } -} - -func TestMD5(t *testing.T) { - t.Parallel() - for i, this := range []struct { - input string - expectedHash string - }{ - {"Hello world, gophers!", "b3029f756f98f79e7f1b7f1d1f0dd53b"}, - {"Lorem ipsum dolor", "06ce65ac476fc656bea3fca5d02cfd81"}, - } { - result, err := md5(this.input) - if err != nil { - t.Errorf("md5 returned error: %s", err) - } - - if result != this.expectedHash { - t.Errorf("[%d] md5: expected '%s', got '%s'", i, this.expectedHash, result) - } - } - - _, err := md5(t) - if err == nil { - t.Error("Expected error from md5") - } -} - -func TestSHA1(t *testing.T) { - t.Parallel() - for i, this := range []struct { - input string - expectedHash string - }{ - {"Hello world, gophers!", "c8b5b0e33d408246e30f53e32b8f7627a7a649d4"}, - {"Lorem ipsum dolor", "45f75b844be4d17b3394c6701768daf39419c99b"}, - } { - result, err := sha1(this.input) - if err != nil { - t.Errorf("sha1 returned error: %s", err) - } - - if result != this.expectedHash { - t.Errorf("[%d] sha1: expected '%s', got '%s'", i, this.expectedHash, result) - } - } - - _, err := sha1(t) - if err == nil { - t.Error("Expected error from sha1") - } -} - -func TestSHA256(t *testing.T) { - t.Parallel() - for i, this := range []struct { - input string - expectedHash string - }{ - {"Hello world, gophers!", "6ec43b78da9669f50e4e422575c54bf87536954ccd58280219c393f2ce352b46"}, - {"Lorem ipsum dolor", "9b3e1beb7053e0f900a674dd1c99aca3355e1275e1b03d3cb1bc977f5154e196"}, - } { - result, err := sha256(this.input) - if err != nil { - t.Errorf("sha256 returned error: %s", err) - } - - if result != this.expectedHash { - t.Errorf("[%d] sha256: expected '%s', got '%s'", i, this.expectedHash, result) - } - } - - _, err := sha256(t) - if err == nil { - t.Error("Expected error from sha256") - } -} - -func TestReadFile(t *testing.T) { - t.Parallel() - - workingDir := "/home/hugo" - - v := viper.New() - - v.Set("workingDir", workingDir) - - f := newTestFuncsterWithViper(v) - - afero.WriteFile(f.Fs.Source, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755) - afero.WriteFile(f.Fs.Source, filepath.Join("/home", "f2.txt"), []byte("f2-content"), 0755) - - for i, this := range []struct { - filename string - expect interface{} - }{ - {"", false}, - {"b", false}, - {filepath.FromSlash("/f/f1.txt"), "f1-content"}, - {filepath.FromSlash("f/f1.txt"), "f1-content"}, - {filepath.FromSlash("../f2.txt"), false}, - } { - result, err := f.readFileFromWorkingDir(this.filename) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] readFile didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] readFile failed: %s", i, err) - continue - } - if result != this.expect { - t.Errorf("[%d] readFile got %q but expected %q", i, result, this.expect) - } - } - } -} - -func TestPartialCached(t *testing.T) { - t.Parallel() - testCases := []struct { - name string - partial string - tmpl string - variant string - }{ - // name and partial should match between test cases. - {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . }}`, ""}, - {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "header"}, - {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "footer"}, - {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "header"}, - } - - var data struct { - Title string - Section string - Params map[string]interface{} - } - - data.Title = "**BatMan**" - data.Section = "blog" - data.Params = map[string]interface{}{"langCode": "en"} - - for i, tc := range testCases { - var tmp string - if tc.variant != "" { - tmp = fmt.Sprintf(tc.tmpl, tc.variant) - } else { - tmp = tc.tmpl - } - - config := newDepsConfig(viper.New()) - - config.WithTemplate = func(templ tplapi.Template) error { - err := templ.AddTemplate("testroot", tmp) - if err != nil { - return err - } - err = templ.AddTemplate("partials/"+tc.name, tc.partial) - if err != nil { - return err - } - - return nil - } - - de := deps.New(config) - require.NoError(t, de.LoadResources()) - - buf := new(bytes.Buffer) - templ := de.Tmpl.Lookup("testroot") - err := templ.Execute(buf, &data) - if err != nil { - t.Fatalf("[%d] error executing template: %s", i, err) - } - - for j := 0; j < 10; j++ { - buf2 := new(bytes.Buffer) - err := templ.Execute(buf2, nil) - if err != nil { - t.Fatalf("[%d] error executing template 2nd time: %s", i, err) - } - - if !reflect.DeepEqual(buf, buf2) { - t.Fatalf("[%d] cached results do not match:\nResult 1:\n%q\nResult 2:\n%q", i, buf, buf2) - } - } - } -} - -func BenchmarkPartial(b *testing.B) { - config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tplapi.Template) error { - err := templ.AddTemplate("testroot", `{{ partial "bench1" . }}`) - if err != nil { - return err - } - err = templ.AddTemplate("partials/bench1", `{{ shuffle (seq 1 10) }}`) - if err != nil { - return err - } - - return nil - } - - de := deps.New(config) - require.NoError(b, de.LoadResources()) - - buf := new(bytes.Buffer) - tmpl := de.Tmpl.Lookup("testroot") - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if err := tmpl.Execute(buf, nil); err != nil { - b.Fatalf("error executing template: %s", err) - } - buf.Reset() - } -} - -func BenchmarkPartialCached(b *testing.B) { - config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tplapi.Template) error { - err := templ.AddTemplate("testroot", `{{ partialCached "bench1" . }}`) - if err != nil { - return err - } - err = templ.AddTemplate("partials/bench1", `{{ shuffle (seq 1 10) }}`) - if err != nil { - return err - } - - return nil - } - - de := deps.New(config) - require.NoError(b, de.LoadResources()) - - buf := new(bytes.Buffer) - tmpl := de.Tmpl.Lookup("testroot") - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if err := tmpl.Execute(buf, nil); err != nil { - b.Fatalf("error executing template: %s", err) - } - buf.Reset() - } -} - -func newTestFuncster() *templateFuncster { - return newTestFuncsterWithViper(viper.New()) -} - -func newTestFuncsterWithViper(v *viper.Viper) *templateFuncster { - config := newDepsConfig(v) - d := deps.New(config) - - if err := d.LoadResources(); err != nil { - panic(err) - } - - return d.Tmpl.(*GoHTMLTemplate).funcster -} - -func newTestTemplate(t *testing.T, name, template string) *template.Template { - config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tplapi.Template) error { - err := templ.AddTemplate(name, template) - if err != nil { - return err - } - return nil - } - - de := deps.New(config) - require.NoError(t, de.LoadResources()) - - return de.Tmpl.Lookup(name) -} diff --git a/tpl/template_resources.go b/tpl/template_resources.go deleted file mode 100644 index bb1b11eb..00000000 --- a/tpl/template_resources.go +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright 2016 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 tpl - -import ( - "bytes" - "encoding/csv" - "encoding/json" - "errors" - "io/ioutil" - "net/http" - "net/url" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/spf13/afero" - "github.com/spf13/hugo/config" - "github.com/spf13/hugo/helpers" - jww "github.com/spf13/jwalterweatherman" -) - -var ( - remoteURLLock = &remoteLock{m: make(map[string]*sync.Mutex)} - resSleep = time.Second * 2 // if JSON decoding failed sleep for n seconds before retrying - resRetries = 1 // number of retries to load the JSON from URL or local file system -) - -type remoteLock struct { - sync.RWMutex - m map[string]*sync.Mutex -} - -// URLLock locks an URL during download -func (l *remoteLock) URLLock(url string) { - l.Lock() - if _, ok := l.m[url]; !ok { - l.m[url] = &sync.Mutex{} - } - l.Unlock() // call this Unlock before the next lock will be called. NFI why but defer doesn't work. - l.m[url].Lock() -} - -// URLUnlock unlocks an URL when the download has been finished. Use only in defer calls. -func (l *remoteLock) URLUnlock(url string) { - l.RLock() - defer l.RUnlock() - if um, ok := l.m[url]; ok { - um.Unlock() - } -} - -// getCacheFileID returns the cache ID for a string -func getCacheFileID(cfg config.Provider, id string) string { - return cfg.GetString("cacheDir") + url.QueryEscape(id) -} - -// resGetCache returns the content for an ID from the file cache or an error -// if the file is not found returns nil,nil -func resGetCache(id string, fs afero.Fs, cfg config.Provider, ignoreCache bool) ([]byte, error) { - if ignoreCache { - return nil, nil - } - fID := getCacheFileID(cfg, id) - isExists, err := helpers.Exists(fID, fs) - if err != nil { - return nil, err - } - if !isExists { - return nil, nil - } - - return afero.ReadFile(fs, fID) - -} - -// resWriteCache writes bytes to an ID into the file cache -func resWriteCache(id string, c []byte, fs afero.Fs, cfg config.Provider, ignoreCache bool) error { - if ignoreCache { - return nil - } - fID := getCacheFileID(cfg, id) - f, err := fs.Create(fID) - if err != nil { - return errors.New("Error: " + err.Error() + ". Failed to create file: " + fID) - } - defer f.Close() - n, err := f.Write(c) - if n == 0 { - return errors.New("No bytes written to file: " + fID) - } - if err != nil { - return errors.New("Error: " + err.Error() + ". Failed to write to file: " + fID) - } - return nil -} - -func resDeleteCache(id string, fs afero.Fs, cfg config.Provider) error { - return fs.Remove(getCacheFileID(cfg, id)) -} - -// resGetRemote loads the content of a remote file. This method is thread safe. -func resGetRemote(url string, fs afero.Fs, cfg config.Provider, hc *http.Client) ([]byte, error) { - c, err := resGetCache(url, fs, cfg, cfg.GetBool("ignoreCache")) - if c != nil && err == nil { - return c, nil - } - if err != nil { - return nil, err - } - - // avoid race condition with locks, block other goroutines if the current url is processing - remoteURLLock.URLLock(url) - defer func() { remoteURLLock.URLUnlock(url) }() - - // avoid multiple locks due to calling resGetCache twice - c, err = resGetCache(url, fs, cfg, cfg.GetBool("ignoreCache")) - if c != nil && err == nil { - return c, nil - } - if err != nil { - return nil, err - } - - jww.INFO.Printf("Downloading: %s ...", url) - res, err := hc.Get(url) - if err != nil { - return nil, err - } - c, err = ioutil.ReadAll(res.Body) - res.Body.Close() - if err != nil { - return nil, err - } - err = resWriteCache(url, c, fs, cfg, cfg.GetBool("ignoreCache")) - if err != nil { - return nil, err - } - jww.INFO.Printf("... and cached to: %s", getCacheFileID(cfg, url)) - return c, nil -} - -// resGetLocal loads the content of a local file -func resGetLocal(url string, fs afero.Fs, cfg config.Provider) ([]byte, error) { - filename := filepath.Join(cfg.GetString("workingDir"), url) - if e, err := helpers.Exists(filename, fs); !e { - return nil, err - } - - return afero.ReadFile(fs, filename) - -} - -// resGetResource loads the content of a local or remote file -func (t *templateFuncster) resGetResource(url string) ([]byte, error) { - if url == "" { - return nil, nil - } - if strings.Contains(url, "://") { - return resGetRemote(url, t.Fs.Source, t.Cfg, http.DefaultClient) - } - return resGetLocal(url, t.Fs.Source, t.Cfg) -} - -// getJSON expects one or n-parts of a URL to a resource which can either be a local or a remote one. -// If you provide multiple parts they will be joined together to the final URL. -// GetJSON returns nil or parsed JSON to use in a short code. -func (t *templateFuncster) getJSON(urlParts ...string) interface{} { - var v interface{} - url := strings.Join(urlParts, "") - - for i := 0; i <= resRetries; i++ { - c, err := t.resGetResource(url) - if err != nil { - jww.ERROR.Printf("Failed to get json resource %s with error message %s", url, err) - return nil - } - - err = json.Unmarshal(c, &v) - if err != nil { - jww.ERROR.Printf("Cannot read json from resource %s with error message %s", url, err) - jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep) - time.Sleep(resSleep) - resDeleteCache(url, t.Fs.Source, t.Cfg) - continue - } - break - } - return v -} - -// parseCSV parses bytes of CSV data into a slice slice string or an error -func parseCSV(c []byte, sep string) ([][]string, error) { - if len(sep) != 1 { - return nil, errors.New("Incorrect length of csv separator: " + sep) - } - b := bytes.NewReader(c) - r := csv.NewReader(b) - rSep := []rune(sep) - r.Comma = rSep[0] - r.FieldsPerRecord = 0 - return r.ReadAll() -} - -// getCSV expects a data separator and one or n-parts of a URL to a resource which -// can either be a local or a remote one. -// The data separator can be a comma, semi-colon, pipe, etc, but only one character. -// If you provide multiple parts for the URL they will be joined together to the final URL. -// GetCSV returns nil or a slice slice to use in a short code. -func (t *templateFuncster) getCSV(sep string, urlParts ...string) [][]string { - var d [][]string - url := strings.Join(urlParts, "") - - var clearCacheSleep = func(i int, u string) { - jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep) - time.Sleep(resSleep) - resDeleteCache(url, t.Fs.Source, t.Cfg) - } - - for i := 0; i <= resRetries; i++ { - c, err := t.resGetResource(url) - - if err == nil && !bytes.Contains(c, []byte(sep)) { - err = errors.New("Cannot find separator " + sep + " in CSV.") - } - - if err != nil { - jww.ERROR.Printf("Failed to read csv resource %s with error message %s", url, err) - clearCacheSleep(i, url) - continue - } - - if d, err = parseCSV(c, sep); err != nil { - jww.ERROR.Printf("Failed to parse csv file %s with error message %s", url, err) - clearCacheSleep(i, url) - continue - } - break - } - return d -} diff --git a/tpl/template_resources_test.go b/tpl/template_resources_test.go deleted file mode 100644 index 82764977..00000000 --- a/tpl/template_resources_test.go +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright 2016 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 tpl - -import ( - "bytes" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - - "github.com/spf13/afero" - "github.com/spf13/hugo/helpers" - "github.com/spf13/hugo/hugofs" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" -) - -func TestScpCache(t *testing.T) { - t.Parallel() - - tests := []struct { - path string - content []byte - ignore bool - }{ - {"http://Foo.Bar/foo_Bar-Foo", []byte(`T€st Content 123`), false}, - {"fOO,bar:foo%bAR", []byte(`T€st Content 123 fOO,bar:foo%bAR`), false}, - {"FOo/BaR.html", []byte(`FOo/BaR.html T€st Content 123`), false}, - {"трям/трям", []byte(`T€st трям/трям Content 123`), false}, - {"은행", []byte(`T€st C은행ontent 123`), false}, - {"Банковский кассир", []byte(`Банковский кассир T€st Content 123`), false}, - {"Банковский кассир", []byte(`Банковский кассир T€st Content 456`), true}, - } - - fs := new(afero.MemMapFs) - - for _, test := range tests { - cfg := viper.New() - c, err := resGetCache(test.path, fs, cfg, test.ignore) - if err != nil { - t.Errorf("Error getting cache: %s", err) - } - if c != nil { - t.Errorf("There is content where there should not be anything: %s", string(c)) - } - - err = resWriteCache(test.path, test.content, fs, cfg, test.ignore) - if err != nil { - t.Errorf("Error writing cache: %s", err) - } - - c, err = resGetCache(test.path, fs, cfg, test.ignore) - if err != nil { - t.Errorf("Error getting cache after writing: %s", err) - } - if test.ignore { - if c != nil { - t.Errorf("Cache ignored but content is not nil: %s", string(c)) - } - } else { - if !bytes.Equal(c, test.content) { - t.Errorf("\nExpected: %s\nActual: %s\n", string(test.content), string(c)) - } - } - } -} - -func TestScpGetLocal(t *testing.T) { - t.Parallel() - v := viper.New() - fs := hugofs.NewMem(v) - ps := helpers.FilePathSeparator - - tests := []struct { - path string - content []byte - }{ - {"testpath" + ps + "test.txt", []byte(`T€st Content 123 fOO,bar:foo%bAR`)}, - {"FOo" + ps + "BaR.html", []byte(`FOo/BaR.html T€st Content 123`)}, - {"трям" + ps + "трям", []byte(`T€st трям/трям Content 123`)}, - {"은행", []byte(`T€st C은행ontent 123`)}, - {"Банковский кассир", []byte(`Банковский кассир T€st Content 123`)}, - } - - for _, test := range tests { - r := bytes.NewReader(test.content) - err := helpers.WriteToDisk(test.path, r, fs.Source) - if err != nil { - t.Error(err) - } - - c, err := resGetLocal(test.path, fs.Source, v) - if err != nil { - t.Errorf("Error getting resource content: %s", err) - } - if !bytes.Equal(c, test.content) { - t.Errorf("\nExpected: %s\nActual: %s\n", string(test.content), string(c)) - } - } - -} - -func getTestServer(handler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, *http.Client) { - testServer := httptest.NewServer(http.HandlerFunc(handler)) - client := &http.Client{ - Transport: &http.Transport{Proxy: func(r *http.Request) (*url.URL, error) { - // Remove when https://github.com/golang/go/issues/13686 is fixed - r.Host = "gohugo.io" - return url.Parse(testServer.URL) - }}, - } - return testServer, client -} - -func TestScpGetRemote(t *testing.T) { - t.Parallel() - fs := new(afero.MemMapFs) - - tests := []struct { - path string - content []byte - ignore bool - }{ - {"http://Foo.Bar/foo_Bar-Foo", []byte(`T€st Content 123`), false}, - {"http://Doppel.Gänger/foo_Bar-Foo", []byte(`T€st Cont€nt 123`), false}, - {"http://Doppel.Gänger/Fizz_Bazz-Foo", []byte(`T€st Банковский кассир Cont€nt 123`), false}, - {"http://Doppel.Gänger/Fizz_Bazz-Bar", []byte(`T€st Банковский кассир Cont€nt 456`), true}, - } - - for _, test := range tests { - - srv, cl := getTestServer(func(w http.ResponseWriter, r *http.Request) { - w.Write(test.content) - }) - defer func() { srv.Close() }() - - cfg := viper.New() - - c, err := resGetRemote(test.path, fs, cfg, cl) - if err != nil { - t.Errorf("Error getting resource content: %s", err) - } - if !bytes.Equal(c, test.content) { - t.Errorf("\nNet Expected: %s\nNet Actual: %s\n", string(test.content), string(c)) - } - cc, cErr := resGetCache(test.path, fs, cfg, test.ignore) - if cErr != nil { - t.Error(cErr) - } - if test.ignore { - if cc != nil { - t.Errorf("Cache ignored but content is not nil: %s", string(cc)) - } - } else { - if !bytes.Equal(cc, test.content) { - t.Errorf("\nCache Expected: %s\nCache Actual: %s\n", string(test.content), string(cc)) - } - } - } -} - -func TestParseCSV(t *testing.T) { - t.Parallel() - - tests := []struct { - csv []byte - sep string - exp string - err bool - }{ - {[]byte("a,b,c\nd,e,f\n"), "", "", true}, - {[]byte("a,b,c\nd,e,f\n"), "~/", "", true}, - {[]byte("a,b,c\nd,e,f"), "|", "a,b,cd,e,f", false}, - {[]byte("q,w,e\nd,e,f"), ",", "qwedef", false}, - {[]byte("a|b|c\nd|e|f|g"), "|", "abcdefg", true}, - {[]byte("z|y|c\nd|e|f"), "|", "zycdef", false}, - } - for _, test := range tests { - csv, err := parseCSV(test.csv, test.sep) - if test.err && err == nil { - t.Error("Expecting an error") - } - if test.err { - continue - } - if !test.err && err != nil { - t.Error(err) - } - - act := "" - for _, v := range csv { - act = act + strings.Join(v, "") - } - - if act != test.exp { - t.Errorf("\nExpected: %s\nActual: %s\n%#v\n", test.exp, act, csv) - } - - } -} - -func TestGetJSONFailParse(t *testing.T) { - t.Parallel() - - f := newTestFuncster() - - reqCount := 0 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if reqCount > 0 { - w.Header().Add("Content-type", "application/json") - fmt.Fprintln(w, `{"gomeetup":["Sydney", "San Francisco", "Stockholm"]}`) - } else { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintln(w, `ERROR 500`) - } - reqCount++ - })) - defer ts.Close() - url := ts.URL + "/test.json" - - want := map[string]interface{}{"gomeetup": []interface{}{"Sydney", "San Francisco", "Stockholm"}} - have := f.getJSON(url) - assert.NotNil(t, have) - if have != nil { - assert.EqualValues(t, want, have) - } -} - -func TestGetCSVFailParseSep(t *testing.T) { - t.Parallel() - f := newTestFuncster() - - reqCount := 0 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if reqCount > 0 { - w.Header().Add("Content-type", "application/json") - fmt.Fprintln(w, `gomeetup,city`) - fmt.Fprintln(w, `yes,Sydney`) - fmt.Fprintln(w, `yes,San Francisco`) - fmt.Fprintln(w, `yes,Stockholm`) - } else { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintln(w, `ERROR 500`) - } - reqCount++ - })) - defer ts.Close() - url := ts.URL + "/test.csv" - - want := [][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}} - have := f.getCSV(",", url) - assert.NotNil(t, have) - if have != nil { - assert.EqualValues(t, want, have) - } -} - -func TestGetCSVFailParse(t *testing.T) { - t.Parallel() - - f := newTestFuncster() - - reqCount := 0 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-type", "application/json") - if reqCount > 0 { - fmt.Fprintln(w, `gomeetup,city`) - fmt.Fprintln(w, `yes,Sydney`) - fmt.Fprintln(w, `yes,San Francisco`) - fmt.Fprintln(w, `yes,Stockholm`) - } else { - fmt.Fprintln(w, `gomeetup,city`) - fmt.Fprintln(w, `yes,Sydney,Bondi,`) // wrong number of fields in line - fmt.Fprintln(w, `yes,San Francisco`) - fmt.Fprintln(w, `yes,Stockholm`) - } - reqCount++ - })) - defer ts.Close() - url := ts.URL + "/test.csv" - - want := [][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}} - have := f.getCSV(",", url) - assert.NotNil(t, have) - if have != nil { - assert.EqualValues(t, want, have) - } -} diff --git a/tpl/template_test.go b/tpl/template_test.go deleted file mode 100644 index 5bb6d89d..00000000 --- a/tpl/template_test.go +++ /dev/null @@ -1,347 +0,0 @@ -// Copyright 2016 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 tpl - -import ( - "bytes" - "errors" - "html/template" - "io/ioutil" - "os" - "path/filepath" - "runtime" - "strings" - "testing" - - "github.com/spf13/afero" - "github.com/spf13/hugo/deps" - - "github.com/spf13/hugo/tplapi" - "github.com/spf13/viper" - "github.com/stretchr/testify/require" -) - -// Some tests for Issue #1178 -- Ace -func TestAceTemplates(t *testing.T) { - t.Parallel() - - for i, this := range []struct { - basePath string - innerPath string - baseContent string - innerContent string - expect string - expectErr int - }{ - {"", filepath.FromSlash("_default/single.ace"), "", "{{ . }}", "DATA", 0}, - {filepath.FromSlash("_default/baseof.ace"), filepath.FromSlash("_default/single.ace"), - `= content main - h2 This is a content named "main" of an inner template. {{ . }}`, - `= doctype html -html lang=en - head - meta charset=utf-8 - title Base and Inner Template - body - h1 This is a base template {{ . }} - = yield main`, `Base and Inner Template

This is a base template DATA

`, 0}, - } { - - for _, root := range []string{"", os.TempDir()} { - - basePath := this.basePath - innerPath := this.innerPath - - if basePath != "" && root != "" { - basePath = filepath.Join(root, basePath) - } - - if innerPath != "" && root != "" { - innerPath = filepath.Join(root, innerPath) - } - - d := "DATA" - - config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tplapi.Template) error { - return templ.AddAceTemplate("mytemplate.ace", basePath, innerPath, - []byte(this.baseContent), []byte(this.innerContent)) - } - - a := deps.New(config) - - if err := a.LoadResources(); err != nil { - t.Fatal(err) - } - - templ := a.Tmpl.(*GoHTMLTemplate) - - if len(templ.errors) > 0 && this.expectErr == 0 { - t.Errorf("Test %d with root '%s' errored: %v", i, root, templ.errors) - } else if len(templ.errors) == 0 && this.expectErr == 1 { - t.Errorf("#1 Test %d with root '%s' should have errored", i, root) - } - - var buff bytes.Buffer - err := a.Tmpl.ExecuteTemplate(&buff, "mytemplate.html", d) - - if err != nil && this.expectErr == 0 { - t.Errorf("Test %d with root '%s' errored: %s", i, root, err) - } else if err == nil && this.expectErr == 2 { - t.Errorf("#2 Test with root '%s' %d should have errored", root, i) - } else { - result := buff.String() - if result != this.expect { - t.Errorf("Test %d with root '%s' got\n%s\nexpected\n%s", i, root, result, this.expect) - } - } - - } - } - -} - -func isAtLeastGo16() bool { - version := runtime.Version() - return strings.Contains(version, "1.6") || strings.Contains(version, "1.7") -} - -func TestAddTemplateFileWithMaster(t *testing.T) { - t.Parallel() - - if !isAtLeastGo16() { - t.Skip("This test only runs on Go >= 1.6") - } - - for i, this := range []struct { - masterTplContent string - overlayTplContent string - writeSkipper int - expect interface{} - }{ - {`A{{block "main" .}}C{{end}}C`, `{{define "main"}}B{{end}}`, 0, "ABC"}, - {`A{{block "main" .}}C{{end}}C{{block "sub" .}}D{{end}}E`, `{{define "main"}}B{{end}}`, 0, "ABCDE"}, - {`A{{block "main" .}}C{{end}}C{{block "sub" .}}D{{end}}E`, `{{define "main"}}B{{end}}{{define "sub"}}Z{{end}}`, 0, "ABCZE"}, - {`tpl`, `tpl`, 1, false}, - {`tpl`, `tpl`, 2, false}, - {`{{.0.E}}`, `tpl`, 0, false}, - {`tpl`, `{{.0.E}}`, 0, false}, - } { - - overlayTplName := "ot" - masterTplName := "mt" - finalTplName := "tp" - - config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tplapi.Template) error { - - err := templ.AddTemplateFileWithMaster(finalTplName, overlayTplName, masterTplName) - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] AddTemplateFileWithMaster didn't return an expected error", i) - } - } else { - - if err != nil { - t.Errorf("[%d] AddTemplateFileWithMaster failed: %s", i, err) - return nil - } - - resultTpl := templ.Lookup(finalTplName) - - if resultTpl == nil { - t.Errorf("[%d] AddTemplateFileWithMaster: Result template not found", i) - return nil - } - - var b bytes.Buffer - err := resultTpl.Execute(&b, nil) - - if err != nil { - t.Errorf("[%d] AddTemplateFileWithMaster execute failed: %s", i, err) - return nil - } - resultContent := b.String() - - if resultContent != this.expect { - t.Errorf("[%d] AddTemplateFileWithMaster got \n%s but expected \n%v", i, resultContent, this.expect) - } - } - - return nil - } - - if this.writeSkipper != 1 { - afero.WriteFile(config.Fs.Source, masterTplName, []byte(this.masterTplContent), 0644) - } - if this.writeSkipper != 2 { - afero.WriteFile(config.Fs.Source, overlayTplName, []byte(this.overlayTplContent), 0644) - } - - deps.New(config) - - } - -} - -// A Go stdlib test for linux/arm. Will remove later. -// See #1771 -func TestBigIntegerFunc(t *testing.T) { - t.Parallel() - var func1 = func(v int64) error { - return nil - } - var funcs = map[string]interface{}{ - "A": func1, - } - - tpl, err := template.New("foo").Funcs(funcs).Parse("{{ A 3e80 }}") - if err != nil { - t.Fatal("Parse failed:", err) - } - err = tpl.Execute(ioutil.Discard, "foo") - - if err == nil { - t.Fatal("Execute should have failed") - } - - t.Log("Got expected error:", err) - -} - -// A Go stdlib test for linux/arm. Will remove later. -// See #1771 -type BI struct { -} - -func (b BI) A(v int64) error { - return nil -} -func TestBigIntegerMethod(t *testing.T) { - t.Parallel() - - data := &BI{} - - tpl, err := template.New("foo2").Parse("{{ .A 3e80 }}") - if err != nil { - t.Fatal("Parse failed:", err) - } - err = tpl.ExecuteTemplate(ioutil.Discard, "foo2", data) - - if err == nil { - t.Fatal("Execute should have failed") - } - - t.Log("Got expected error:", err) - -} - -// Test for bugs discovered by https://github.com/dvyukov/go-fuzz -func TestTplGoFuzzReports(t *testing.T) { - t.Parallel() - - // The following test case(s) also fail - // See https://github.com/golang/go/issues/10634 - //{"{{ seq 433937734937734969526500969526500 }}", 2}} - - for i, this := range []struct { - data string - expectErr int - }{ - // Issue #1089 - //{"{{apply .C \"first\" }}", 2}, - // Issue #1090 - {"{{ slicestr \"000000\" 10}}", 2}, - // Issue #1091 - //{"{{apply .C \"first\" 0 0 0}}", 2}, - {"{{seq 3e80}}", 2}, - // Issue #1095 - {"{{apply .C \"urlize\" " + - "\".\"}}", 2}} { - - d := &Data{ - A: 42, - B: "foo", - C: []int{1, 2, 3}, - D: map[int]string{1: "foo", 2: "bar"}, - E: Data1{42, "foo"}, - F: []string{"a", "b", "c"}, - G: []string{"a", "b", "c", "d", "e"}, - H: "a,b,c,d,e,f", - } - - config := newDepsConfig(viper.New()) - - config.WithTemplate = func(templ tplapi.Template) error { - return templ.AddTemplate("fuzz", this.data) - } - - de := deps.New(config) - require.NoError(t, de.LoadResources()) - - templ := de.Tmpl.(*GoHTMLTemplate) - - if len(templ.errors) > 0 && this.expectErr == 0 { - t.Errorf("Test %d errored: %v", i, templ.errors) - } else if len(templ.errors) == 0 && this.expectErr == 1 { - t.Errorf("#1 Test %d should have errored", i) - } - - err := de.Tmpl.ExecuteTemplate(ioutil.Discard, "fuzz", d) - - if err != nil && this.expectErr == 0 { - t.Fatalf("Test %d errored: %s", i, err) - } else if err == nil && this.expectErr == 2 { - t.Fatalf("#2 Test %d should have errored", i) - } - - } -} - -type Data struct { - A int - B string - C []int - D map[int]string - E Data1 - F []string - G []string - H string -} - -type Data1 struct { - A int - B string -} - -func (Data1) Q() string { - return "foo" -} - -func (Data1) W() (string, error) { - return "foo", nil -} - -func (Data1) E() (string, error) { - return "foo", errors.New("Data.E error") -} - -func (Data1) R(v int) (string, error) { - return "foo", nil -} - -func (Data1) T(s string) (string, error) { - return s, nil -} diff --git a/tpl/tplimpl/amber_compiler.go b/tpl/tplimpl/amber_compiler.go new file mode 100644 index 00000000..252c39ff --- /dev/null +++ b/tpl/tplimpl/amber_compiler.go @@ -0,0 +1,42 @@ +// Copyright 2017 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 tplimpl + +import ( + "html/template" + + "github.com/eknkc/amber" +) + +func (gt *GoHTMLTemplate) CompileAmberWithTemplate(b []byte, path string, t *template.Template) (*template.Template, error) { + c := amber.New() + + if err := c.ParseData(b, path); err != nil { + return nil, err + } + + data, err := c.CompileString() + + if err != nil { + return nil, err + } + + tpl, err := t.Funcs(gt.amberFuncMap).Parse(data) + + if err != nil { + return nil, err + } + + return tpl, nil +} diff --git a/tpl/tplimpl/reflect_helpers.go b/tpl/tplimpl/reflect_helpers.go new file mode 100644 index 00000000..7463683f --- /dev/null +++ b/tpl/tplimpl/reflect_helpers.go @@ -0,0 +1,70 @@ +// Copyright 2017 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 tplimpl + +import ( + "reflect" + "time" +) + +// toInt returns the int value if possible, -1 if not. +func toInt(v reflect.Value) int64 { + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() + case reflect.Interface: + return toInt(v.Elem()) + } + return -1 +} + +// toString returns the string value if possible, "" if not. +func toString(v reflect.Value) string { + switch v.Kind() { + case reflect.String: + return v.String() + case reflect.Interface: + return toString(v.Elem()) + } + return "" +} + +var ( + zero reflect.Value + errorType = reflect.TypeOf((*error)(nil)).Elem() + timeType = reflect.TypeOf((*time.Time)(nil)).Elem() +) + +func toTimeUnix(v reflect.Value) int64 { + if v.Kind() == reflect.Interface { + return toTimeUnix(v.Elem()) + } + if v.Type() != timeType { + panic("coding error: argument must be time.Time type reflect Value") + } + return v.MethodByName("Unix").Call([]reflect.Value{})[0].Int() +} + +// indirect is taken from 'text/template/exec.go' +func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { + for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { + if v.IsNil() { + return v, true + } + if v.Kind() == reflect.Interface && v.NumMethod() > 0 { + break + } + } + return v, false +} diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go new file mode 100644 index 00000000..cf1fc562 --- /dev/null +++ b/tpl/tplimpl/template.go @@ -0,0 +1,575 @@ +// Copyright 2016 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 tplimpl + +import ( + "fmt" + "html/template" + "io" + "os" + "path/filepath" + "strings" + + "sync" + + "github.com/eknkc/amber" + "github.com/spf13/afero" + bp "github.com/spf13/hugo/bufferpool" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/helpers" + "github.com/yosssi/ace" +) + +// TODO(bep) globals get rid of the rest of the jww.ERR etc. + +// Protecting global map access (Amber) +var amberMu sync.Mutex + +type templateErr struct { + name string + err error +} + +type GoHTMLTemplate struct { + *template.Template + + clone *template.Template + + // a separate storage for the overlays created from cloned master templates. + // note: No mutex protection, so we add these in one Go routine, then just read. + overlays map[string]*template.Template + + errors []*templateErr + + funcster *templateFuncster + + amberFuncMap template.FuncMap + + *deps.Deps +} + +type TemplateProvider struct{} + +var DefaultTemplateProvider *TemplateProvider + +// Update updates the Hugo Template System in the provided Deps. +// with all the additional features, templates & functions +func (*TemplateProvider) Update(deps *deps.Deps) error { + // TODO(bep) check that this isn't called too many times. + tmpl := &GoHTMLTemplate{ + Template: template.New(""), + overlays: make(map[string]*template.Template), + errors: make([]*templateErr, 0), + Deps: deps, + } + + deps.Tmpl = tmpl + + tmpl.initFuncs(deps) + + tmpl.LoadEmbedded() + + if deps.WithTemplate != nil { + err := deps.WithTemplate(tmpl) + if err != nil { + tmpl.errors = append(tmpl.errors, &templateErr{"init", err}) + } + + } + + tmpl.MarkReady() + + return nil + +} + +// Clone clones +func (*TemplateProvider) Clone(d *deps.Deps) error { + + t := d.Tmpl.(*GoHTMLTemplate) + + // 1. Clone the clone with new template funcs + // 2. Clone any overlays with new template funcs + + tmpl := &GoHTMLTemplate{ + Template: template.Must(t.Template.Clone()), + overlays: make(map[string]*template.Template), + errors: make([]*templateErr, 0), + Deps: d, + } + + d.Tmpl = tmpl + tmpl.initFuncs(d) + + for k, v := range t.overlays { + vc := template.Must(v.Clone()) + // The extra lookup is a workaround, see + // * https://github.com/golang/go/issues/16101 + // * https://github.com/spf13/hugo/issues/2549 + vc = vc.Lookup(vc.Name()) + vc.Funcs(tmpl.funcster.funcMap) + tmpl.overlays[k] = vc + } + + tmpl.MarkReady() + + return nil +} + +func (t *GoHTMLTemplate) initFuncs(d *deps.Deps) { + + t.funcster = newTemplateFuncster(d) + + // The URL funcs in the funcMap is somewhat language dependent, + // so we need to wait until the language and site config is loaded. + t.funcster.initFuncMap() + + t.amberFuncMap = template.FuncMap{} + + amberMu.Lock() + for k, v := range amber.FuncMap { + t.amberFuncMap[k] = v + } + + for k, v := range t.funcster.funcMap { + t.amberFuncMap[k] = v + // Hacky, but we need to make sure that the func names are in the global map. + amber.FuncMap[k] = func() string { + panic("should never be invoked") + } + } + amberMu.Unlock() + +} + +func (t *GoHTMLTemplate) Funcs(funcMap template.FuncMap) { + t.Template.Funcs(funcMap) +} + +func (t *GoHTMLTemplate) Partial(name string, contextList ...interface{}) template.HTML { + if strings.HasPrefix("partials/", name) { + name = name[8:] + } + var context interface{} + + if len(contextList) == 0 { + context = nil + } else { + context = contextList[0] + } + return t.ExecuteTemplateToHTML(context, "partials/"+name, "theme/partials/"+name) +} + +func (t *GoHTMLTemplate) executeTemplate(context interface{}, w io.Writer, layouts ...string) { + var worked bool + for _, layout := range layouts { + templ := t.Lookup(layout) + if templ == nil { + layout += ".html" + templ = t.Lookup(layout) + } + + if templ != nil { + if err := templ.Execute(w, context); err != nil { + helpers.DistinctErrorLog.Println(layout, err) + } + worked = true + break + } + } + if !worked { + t.Log.ERROR.Println("Unable to render", layouts) + t.Log.ERROR.Println("Expecting to find a template in either the theme/layouts or /layouts in one of the following relative locations", layouts) + } +} + +func (t *GoHTMLTemplate) ExecuteTemplateToHTML(context interface{}, layouts ...string) template.HTML { + b := bp.GetBuffer() + defer bp.PutBuffer(b) + t.executeTemplate(context, b, layouts...) + return template.HTML(b.String()) +} + +func (t *GoHTMLTemplate) Lookup(name string) *template.Template { + + if templ := t.Template.Lookup(name); templ != nil { + return templ + } + + if t.overlays != nil { + if templ, ok := t.overlays[name]; ok { + return templ + } + } + + // The clone is used for the non-renderable HTML pages (p.IsRenderable == false) that is parsed + // as Go templates late in the build process. + if t.clone != nil { + if templ := t.clone.Lookup(name); templ != nil { + return templ + } + } + + return nil + +} + +func (t *GoHTMLTemplate) GetClone() *template.Template { + return t.clone +} + +func (t *GoHTMLTemplate) LoadEmbedded() { + t.EmbedShortcodes() + t.EmbedTemplates() +} + +// MarkReady marks the template as "ready for execution". No changes allowed +// after this is set. +func (t *GoHTMLTemplate) MarkReady() { + if t.clone == nil { + t.clone = template.Must(t.Template.Clone()) + } +} + +func (t *GoHTMLTemplate) checkState() { + if t.clone != nil { + panic("template is cloned and cannot be modfified") + } +} + +func (t *GoHTMLTemplate) AddInternalTemplate(prefix, name, tpl string) error { + if prefix != "" { + return t.AddTemplate("_internal/"+prefix+"/"+name, tpl) + } + return t.AddTemplate("_internal/"+name, tpl) +} + +func (t *GoHTMLTemplate) AddInternalShortcode(name, content string) error { + return t.AddInternalTemplate("shortcodes", name, content) +} + +func (t *GoHTMLTemplate) AddTemplate(name, tpl string) error { + t.checkState() + templ, err := t.New(name).Parse(tpl) + if err != nil { + t.errors = append(t.errors, &templateErr{name: name, err: err}) + return err + } + if err := applyTemplateTransformers(templ); err != nil { + return err + } + + return nil +} + +func (t *GoHTMLTemplate) AddTemplateFileWithMaster(name, overlayFilename, masterFilename string) error { + + // There is currently no known way to associate a cloned template with an existing one. + // This funky master/overlay design will hopefully improve in a future version of Go. + // + // Simplicity is hard. + // + // Until then we'll have to live with this hackery. + // + // See https://github.com/golang/go/issues/14285 + // + // So, to do minimum amount of changes to get this to work: + // + // 1. Lookup or Parse the master + // 2. Parse and store the overlay in a separate map + + masterTpl := t.Lookup(masterFilename) + + if masterTpl == nil { + b, err := afero.ReadFile(t.Fs.Source, masterFilename) + if err != nil { + return err + } + masterTpl, err = t.New(masterFilename).Parse(string(b)) + + if err != nil { + // TODO(bep) Add a method that does this + t.errors = append(t.errors, &templateErr{name: name, err: err}) + return err + } + } + + b, err := afero.ReadFile(t.Fs.Source, overlayFilename) + if err != nil { + return err + } + + overlayTpl, err := template.Must(masterTpl.Clone()).Parse(string(b)) + if err != nil { + t.errors = append(t.errors, &templateErr{name: name, err: err}) + } else { + // The extra lookup is a workaround, see + // * https://github.com/golang/go/issues/16101 + // * https://github.com/spf13/hugo/issues/2549 + overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) + if err := applyTemplateTransformers(overlayTpl); err != nil { + return err + } + t.overlays[name] = overlayTpl + } + + return err +} + +func (t *GoHTMLTemplate) AddAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error { + t.checkState() + var base, inner *ace.File + name = name[:len(name)-len(filepath.Ext(innerPath))] + ".html" + + // Fixes issue #1178 + basePath = strings.Replace(basePath, "\\", "/", -1) + innerPath = strings.Replace(innerPath, "\\", "/", -1) + + if basePath != "" { + base = ace.NewFile(basePath, baseContent) + inner = ace.NewFile(innerPath, innerContent) + } else { + base = ace.NewFile(innerPath, innerContent) + inner = ace.NewFile("", []byte{}) + } + parsed, err := ace.ParseSource(ace.NewSource(base, inner, []*ace.File{}), nil) + if err != nil { + t.errors = append(t.errors, &templateErr{name: name, err: err}) + return err + } + templ, err := ace.CompileResultWithTemplate(t.New(name), parsed, nil) + if err != nil { + t.errors = append(t.errors, &templateErr{name: name, err: err}) + return err + } + return applyTemplateTransformers(templ) +} + +func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) error { + t.checkState() + // get the suffix and switch on that + ext := filepath.Ext(path) + switch ext { + case ".amber": + templateName := strings.TrimSuffix(name, filepath.Ext(name)) + ".html" + b, err := afero.ReadFile(t.Fs.Source, path) + + if err != nil { + return err + } + + amberMu.Lock() + templ, err := t.CompileAmberWithTemplate(b, path, t.New(templateName)) + amberMu.Unlock() + if err != nil { + return err + } + + return applyTemplateTransformers(templ) + case ".ace": + var innerContent, baseContent []byte + innerContent, err := afero.ReadFile(t.Fs.Source, path) + + if err != nil { + return err + } + + if baseTemplatePath != "" { + baseContent, err = afero.ReadFile(t.Fs.Source, baseTemplatePath) + if err != nil { + return err + } + } + + return t.AddAceTemplate(name, baseTemplatePath, path, baseContent, innerContent) + default: + + if baseTemplatePath != "" { + return t.AddTemplateFileWithMaster(name, path, baseTemplatePath) + } + + b, err := afero.ReadFile(t.Fs.Source, path) + + if err != nil { + return err + } + + t.Log.DEBUG.Printf("Add template file from path %s", path) + + return t.AddTemplate(name, string(b)) + } + +} + +func (t *GoHTMLTemplate) GenerateTemplateNameFrom(base, path string) string { + name, _ := filepath.Rel(base, path) + return filepath.ToSlash(name) +} + +func isDotFile(path string) bool { + return filepath.Base(path)[0] == '.' +} + +func isBackupFile(path string) bool { + return path[len(path)-1] == '~' +} + +const baseFileBase = "baseof" + +var aceTemplateInnerMarkers = [][]byte{[]byte("= content")} +var goTemplateInnerMarkers = [][]byte{[]byte("{{define"), []byte("{{ define")} + +func isBaseTemplate(path string) bool { + return strings.Contains(path, baseFileBase) +} + +func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { + t.Log.DEBUG.Printf("Load templates from path %q prefix %q", absPath, prefix) + walker := func(path string, fi os.FileInfo, err error) error { + if err != nil { + return nil + } + t.Log.DEBUG.Println("Template path", path) + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + link, err := filepath.EvalSymlinks(absPath) + if err != nil { + t.Log.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", absPath, err) + return nil + } + linkfi, err := t.Fs.Source.Stat(link) + if err != nil { + t.Log.ERROR.Printf("Cannot stat '%s', error was: %s", link, err) + return nil + } + if !linkfi.Mode().IsRegular() { + t.Log.ERROR.Printf("Symbolic links for directories not supported, skipping '%s'", absPath) + } + return nil + } + + if !fi.IsDir() { + if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) { + return nil + } + + tplName := t.GenerateTemplateNameFrom(absPath, path) + + if prefix != "" { + tplName = strings.Trim(prefix, "/") + "/" + tplName + } + + var baseTemplatePath string + + // Ace and Go templates may have both a base and inner template. + pathDir := filepath.Dir(path) + if filepath.Ext(path) != ".amber" && !strings.HasSuffix(pathDir, "partials") && !strings.HasSuffix(pathDir, "shortcodes") { + + innerMarkers := goTemplateInnerMarkers + baseFileName := fmt.Sprintf("%s.html", baseFileBase) + + if filepath.Ext(path) == ".ace" { + innerMarkers = aceTemplateInnerMarkers + baseFileName = fmt.Sprintf("%s.ace", baseFileBase) + } + + // This may be a view that shouldn't have base template + // Have to look inside it to make sure + needsBase, err := helpers.FileContainsAny(path, innerMarkers, t.Fs.Source) + if err != nil { + return err + } + if needsBase { + + layoutDir := t.PathSpec.GetLayoutDirPath() + currBaseFilename := fmt.Sprintf("%s-%s", helpers.Filename(path), baseFileName) + templateDir := filepath.Dir(path) + themeDir := filepath.Join(t.PathSpec.GetThemeDir()) + relativeThemeLayoutsDir := filepath.Join(t.PathSpec.GetRelativeThemeDir(), "layouts") + + var baseTemplatedDir string + + if strings.HasPrefix(templateDir, relativeThemeLayoutsDir) { + baseTemplatedDir = strings.TrimPrefix(templateDir, relativeThemeLayoutsDir) + } else { + baseTemplatedDir = strings.TrimPrefix(templateDir, layoutDir) + } + + baseTemplatedDir = strings.TrimPrefix(baseTemplatedDir, helpers.FilePathSeparator) + + // Look for base template in the follwing order: + // 1. /-baseof., e.g. list-baseof.. + // 2. /baseof. + // 3. _default/-baseof., e.g. list-baseof.. + // 4. _default/baseof. + // For each of the steps above, it will first look in the project, then, if theme is set, + // in the theme's layouts folder. + + pairsToCheck := [][]string{ + []string{baseTemplatedDir, currBaseFilename}, + []string{baseTemplatedDir, baseFileName}, + []string{"_default", currBaseFilename}, + []string{"_default", baseFileName}, + } + + Loop: + for _, pair := range pairsToCheck { + pathsToCheck := basePathsToCheck(pair, layoutDir, themeDir) + for _, pathToCheck := range pathsToCheck { + if ok, err := helpers.Exists(pathToCheck, t.Fs.Source); err == nil && ok { + baseTemplatePath = pathToCheck + break Loop + } + } + } + } + } + + if err := t.AddTemplateFile(tplName, baseTemplatePath, path); err != nil { + t.Log.ERROR.Printf("Failed to add template %s in path %s: %s", tplName, path, err) + } + + } + return nil + } + if err := helpers.SymbolicWalk(t.Fs.Source, absPath, walker); err != nil { + t.Log.ERROR.Printf("Failed to load templates: %s", err) + } +} + +func basePathsToCheck(path []string, layoutDir, themeDir string) []string { + // Always look in the project. + pathsToCheck := []string{filepath.Join((append([]string{layoutDir}, path...))...)} + + // May have a theme + if themeDir != "" { + pathsToCheck = append(pathsToCheck, filepath.Join((append([]string{themeDir, "layouts"}, path...))...)) + } + + return pathsToCheck + +} + +func (t *GoHTMLTemplate) LoadTemplatesWithPrefix(absPath string, prefix string) { + t.loadTemplates(absPath, prefix) +} + +func (t *GoHTMLTemplate) LoadTemplates(absPath string) { + t.loadTemplates(absPath, "") +} + +func (t *GoHTMLTemplate) PrintErrors() { + for i, e := range t.errors { + t.Log.ERROR.Println(i, ":", e.err) + } +} diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go new file mode 100644 index 00000000..68090497 --- /dev/null +++ b/tpl/tplimpl/template_ast_transformers.go @@ -0,0 +1,259 @@ +// Copyright 2016 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 tplimpl + +import ( + "errors" + "html/template" + "strings" + "text/template/parse" +) + +// decl keeps track of the variable mappings, i.e. $mysite => .Site etc. +type decl map[string]string + +var paramsPaths = [][]string{ + {"Params"}, + {"Site", "Params"}, + + // Site and Pag referenced from shortcodes + {"Page", "Site", "Params"}, + {"Page", "Params"}, + + {"Site", "Language", "Params"}, +} + +type templateContext struct { + decl decl + templ *template.Template +} + +func newTemplateContext(templ *template.Template) *templateContext { + return &templateContext{templ: templ, decl: make(map[string]string)} + +} + +func applyTemplateTransformers(templ *template.Template) error { + if templ == nil || templ.Tree == nil { + return errors.New("expected template, but none provided") + } + + c := newTemplateContext(templ) + + c.paramsKeysToLower(templ.Tree.Root) + + return nil +} + +// paramsKeysToLower is made purposely non-generic to make it not so tempting +// to do more of these hard-to-maintain AST transformations. +func (c *templateContext) paramsKeysToLower(n parse.Node) { + + switch x := n.(type) { + case *parse.ListNode: + if x != nil { + c.paramsKeysToLowerForNodes(x.Nodes...) + } + case *parse.ActionNode: + c.paramsKeysToLowerForNodes(x.Pipe) + case *parse.IfNode: + c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) + case *parse.WithNode: + c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) + case *parse.RangeNode: + c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) + case *parse.TemplateNode: + subTempl := c.templ.Lookup(x.Name) + if subTempl != nil { + c.paramsKeysToLowerForNodes(subTempl.Tree.Root) + } + case *parse.PipeNode: + for i, elem := range x.Decl { + if len(x.Cmds) > i { + // maps $site => .Site etc. + c.decl[elem.Ident[0]] = x.Cmds[i].String() + } + } + + for _, cmd := range x.Cmds { + c.paramsKeysToLower(cmd) + } + + case *parse.CommandNode: + for _, elem := range x.Args { + switch an := elem.(type) { + case *parse.FieldNode: + c.updateIdentsIfNeeded(an.Ident) + case *parse.VariableNode: + c.updateIdentsIfNeeded(an.Ident) + case *parse.PipeNode: + c.paramsKeysToLower(an) + } + + } + } +} + +func (c *templateContext) paramsKeysToLowerForNodes(nodes ...parse.Node) { + for _, node := range nodes { + c.paramsKeysToLower(node) + } +} + +func (c *templateContext) updateIdentsIfNeeded(idents []string) { + index := c.decl.indexOfReplacementStart(idents) + + if index == -1 { + return + } + + for i := index; i < len(idents); i++ { + idents[i] = strings.ToLower(idents[i]) + } +} + +// indexOfReplacementStart will return the index of where to start doing replacement, +// -1 if none needed. +func (d decl) indexOfReplacementStart(idents []string) int { + + l := len(idents) + + if l == 0 { + return -1 + } + + first := idents[0] + firstIsVar := first[0] == '$' + + if l == 1 && !firstIsVar { + // This can not be a Params.x + return -1 + } + + if !firstIsVar { + found := false + for _, paramsPath := range paramsPaths { + if first == paramsPath[0] { + found = true + break + } + } + if !found { + return -1 + } + } + + var ( + resolvedIdents []string + replacements []string + replaced []string + ) + + // An Ident can start out as one of + // [Params] [$blue] [$colors.Blue] + // We need to resolve the variables, so + // $blue => [Params Colors Blue] + // etc. + replacements = []string{idents[0]} + + // Loop until there are no more $vars to resolve. + for i := 0; i < len(replacements); i++ { + + if i > 20 { + // bail out + return -1 + } + + potentialVar := replacements[i] + + if potentialVar == "$" { + continue + } + + if potentialVar == "" || potentialVar[0] != '$' { + // leave it as is + replaced = append(replaced, strings.Split(potentialVar, ".")...) + continue + } + + replacement, ok := d[potentialVar] + + if !ok { + // Temporary range vars. We do not care about those. + return -1 + } + + replacement = strings.TrimPrefix(replacement, ".") + + if replacement == "" { + continue + } + + if replacement[0] == '$' { + // Needs further expansion + replacements = append(replacements, strings.Split(replacement, ".")...) + } else { + replaced = append(replaced, strings.Split(replacement, ".")...) + } + } + + resolvedIdents = append(replaced, idents[1:]...) + + for _, paramPath := range paramsPaths { + if index := indexOfFirstRealIdentAfterWords(resolvedIdents, idents, paramPath...); index != -1 { + return index + } + } + + return -1 + +} + +func indexOfFirstRealIdentAfterWords(resolvedIdents, idents []string, words ...string) int { + if !sliceStartsWith(resolvedIdents, words...) { + return -1 + } + + for i, ident := range idents { + if ident == "" || ident[0] == '$' { + continue + } + found := true + for _, word := range words { + if ident == word { + found = false + break + } + } + if found { + return i + } + } + + return -1 +} + +func sliceStartsWith(slice []string, words ...string) bool { + + if len(slice) < len(words) { + return false + } + + for i, word := range words { + if word != slice[i] { + return false + } + } + return true +} diff --git a/tpl/tplimpl/template_ast_transformers_test.go b/tpl/tplimpl/template_ast_transformers_test.go new file mode 100644 index 00000000..048d52fe --- /dev/null +++ b/tpl/tplimpl/template_ast_transformers_test.go @@ -0,0 +1,269 @@ +// Copyright 2016 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 tplimpl + +import ( + "bytes" + "testing" + + "html/template" + + "github.com/stretchr/testify/require" +) + +var ( + testFuncs = map[string]interface{}{ + "Echo": func(v interface{}) interface{} { return v }, + } + + paramsData = map[string]interface{}{ + "NotParam": "Hi There", + "Slice": []int{1, 3}, + "Params": map[string]interface{}{ + "lower": "P1L", + }, + "Site": map[string]interface{}{ + "Params": map[string]interface{}{ + "lower": "P2L", + "slice": []int{1, 3}, + }, + "Language": map[string]interface{}{ + "Params": map[string]interface{}{ + "lower": "P22L", + }, + }, + "Data": map[string]interface{}{ + "Params": map[string]interface{}{ + "NOLOW": "P3H", + }, + }, + }, + } + + paramsTempl = ` +{{ $page := . }} +{{ $pageParams := .Params }} +{{ $site := .Site }} +{{ $siteParams := .Site.Params }} +{{ $data := .Site.Data }} +{{ $notparam := .NotParam }} + +P1: {{ .Params.LOWER }} +P1_2: {{ $.Params.LOWER }} +P1_3: {{ $page.Params.LOWER }} +P1_4: {{ $pageParams.LOWER }} +P2: {{ .Site.Params.LOWER }} +P2_2: {{ $.Site.Params.LOWER }} +P2_3: {{ $site.Params.LOWER }} +P2_4: {{ $siteParams.LOWER }} +P22: {{ .Site.Language.Params.LOWER }} +P3: {{ .Site.Data.Params.NOLOW }} +P3_2: {{ $.Site.Data.Params.NOLOW }} +P3_3: {{ $site.Data.Params.NOLOW }} +P3_4: {{ $data.Params.NOLOW }} +P4: {{ range $i, $e := .Site.Params.SLICE }}{{ $e }}{{ end }} +P5: {{ Echo .Params.LOWER }} +P5_2: {{ Echo $site.Params.LOWER }} +{{ if .Params.LOWER }} +IF: {{ .Params.LOWER }} +{{ end }} +{{ if .Params.NOT_EXIST }} +{{ else }} +ELSE: {{ .Params.LOWER }} +{{ end }} + + +{{ with .Params.LOWER }} +WITH: {{ . }} +{{ end }} + + +{{ range .Slice }} +RANGE: {{ . }}: {{ $.Params.LOWER }} +{{ end }} +{{ index .Slice 1 }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ $notparam }} + + +{{ $lower := .Site.Params.LOWER }} +F1: {{ printf "themes/%s-theme" .Site.Params.LOWER }} +F2: {{ Echo (printf "themes/%s-theme" $lower) }} +F3: {{ Echo (printf "themes/%s-theme" .Site.Params.LOWER) }} +` +) + +func TestParamsKeysToLower(t *testing.T) { + t.Parallel() + + require.Error(t, applyTemplateTransformers(nil)) + + templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl) + + require.NoError(t, err) + + c := newTemplateContext(templ) + + require.Equal(t, -1, c.decl.indexOfReplacementStart([]string{})) + + c.paramsKeysToLower(templ.Tree.Root) + + var b bytes.Buffer + + require.NoError(t, templ.Execute(&b, paramsData)) + + result := b.String() + + require.Contains(t, result, "P1: P1L") + require.Contains(t, result, "P1_2: P1L") + require.Contains(t, result, "P1_3: P1L") + require.Contains(t, result, "P1_4: P1L") + require.Contains(t, result, "P2: P2L") + require.Contains(t, result, "P2_2: P2L") + require.Contains(t, result, "P2_3: P2L") + require.Contains(t, result, "P2_4: P2L") + require.Contains(t, result, "P22: P22L") + require.Contains(t, result, "P3: P3H") + require.Contains(t, result, "P3_2: P3H") + require.Contains(t, result, "P3_3: P3H") + require.Contains(t, result, "P3_4: P3H") + require.Contains(t, result, "P4: 13") + require.Contains(t, result, "P5: P1L") + require.Contains(t, result, "P5_2: P2L") + + require.Contains(t, result, "IF: P1L") + require.Contains(t, result, "ELSE: P1L") + + require.Contains(t, result, "WITH: P1L") + + require.Contains(t, result, "RANGE: 3: P1L") + + require.Contains(t, result, "Hi There") + + // Issue #2740 + require.Contains(t, result, "F1: themes/P2L-theme") + require.Contains(t, result, "F2: themes/P2L-theme") + require.Contains(t, result, "F3: themes/P2L-theme") + +} + +func BenchmarkTemplateParamsKeysToLower(b *testing.B) { + templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl) + + if err != nil { + b.Fatal(err) + } + + templates := make([]*template.Template, b.N) + + for i := 0; i < b.N; i++ { + templates[i], err = templ.Clone() + if err != nil { + b.Fatal(err) + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + c := newTemplateContext(templates[i]) + c.paramsKeysToLower(templ.Tree.Root) + } +} + +func TestParamsKeysToLowerVars(t *testing.T) { + t.Parallel() + var ( + ctx = map[string]interface{}{ + "Params": map[string]interface{}{ + "colors": map[string]interface{}{ + "blue": "Amber", + }, + }, + } + + // This is how Amber behaves: + paramsTempl = ` +{{$__amber_1 := .Params.Colors}} +{{$__amber_2 := $__amber_1.Blue}} +Color: {{$__amber_2}} +Blue: {{ $__amber_1.Blue}} +` + ) + + templ, err := template.New("foo").Parse(paramsTempl) + + require.NoError(t, err) + + c := newTemplateContext(templ) + + c.paramsKeysToLower(templ.Tree.Root) + + var b bytes.Buffer + + require.NoError(t, templ.Execute(&b, ctx)) + + result := b.String() + + require.Contains(t, result, "Color: Amber") + +} + +func TestParamsKeysToLowerInBlockTemplate(t *testing.T) { + t.Parallel() + + var ( + ctx = map[string]interface{}{ + "Params": map[string]interface{}{ + "lower": "P1L", + }, + } + + master = ` +P1: {{ .Params.LOWER }} +{{ block "main" . }}DEFAULT{{ end }}` + overlay = ` +{{ define "main" }} +P2: {{ .Params.LOWER }} +{{ end }}` + ) + + masterTpl, err := template.New("foo").Parse(master) + require.NoError(t, err) + + overlayTpl, err := template.Must(masterTpl.Clone()).Parse(overlay) + require.NoError(t, err) + overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) + + c := newTemplateContext(overlayTpl) + + c.paramsKeysToLower(overlayTpl.Tree.Root) + + var b bytes.Buffer + + require.NoError(t, overlayTpl.Execute(&b, ctx)) + + result := b.String() + + require.Contains(t, result, "P1: P1L") + require.Contains(t, result, "P2: P1L") +} diff --git a/tpl/tplimpl/template_embedded.go b/tpl/tplimpl/template_embedded.go new file mode 100644 index 00000000..50397b28 --- /dev/null +++ b/tpl/tplimpl/template_embedded.go @@ -0,0 +1,266 @@ +// 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 tplimpl + +type Tmpl struct { + Name string + Data string +} + +func (t *GoHTMLTemplate) EmbedShortcodes() { + t.AddInternalShortcode("ref.html", `{{ .Get 0 | ref .Page }}`) + t.AddInternalShortcode("relref.html", `{{ .Get 0 | relref .Page }}`) + t.AddInternalShortcode("highlight.html", `{{ if len .Params | eq 2 }}{{ highlight .Inner (.Get 0) (.Get 1) }}{{ else }}{{ highlight .Inner (.Get 0) "" }}{{ end }}`) + t.AddInternalShortcode("test.html", `This is a simple Test`) + t.AddInternalShortcode("figure.html", ` +
+ {{ with .Get "link"}}{{ end }} + + {{ if .Get "link"}}{{ end }} + {{ if or (or (.Get "title") (.Get "caption")) (.Get "attr")}} +
{{ if isset .Params "title" }} +

{{ .Get "title" }}

{{ end }} + {{ if or (.Get "caption") (.Get "attr")}}

+ {{ .Get "caption" }} + {{ with .Get "attrlink"}} {{ end }} + {{ .Get "attr" }} + {{ if .Get "attrlink"}} {{ end }} +

{{ end }} +
+ {{ end }} +
+`) + t.AddInternalShortcode("speakerdeck.html", "") + t.AddInternalShortcode("youtube.html", `{{ if .IsNamedParams }} +
+ +
{{ else }} +
+ +
+{{ end }}`) + t.AddInternalShortcode("vimeo.html", `{{ if .IsNamedParams }}
+ +
{{ else }} +
+ +
+{{ end }}`) + t.AddInternalShortcode("gist.html", ``) + t.AddInternalShortcode("tweet.html", `{{ (getJSON "https://api.twitter.com/1/statuses/oembed.json?id=" (index .Params 0)).html | safeHTML }}`) + t.AddInternalShortcode("instagram.html", `{{ if len .Params | eq 2 }}{{ if eq (.Get 1) "hidecaption" }}{{ (getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=1").html | safeHTML }}{{ end }}{{ else }}{{ (getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=0").html | safeHTML }}{{ end }}`) +} + +func (t *GoHTMLTemplate) EmbedTemplates() { + + t.AddInternalTemplate("_default", "rss.xml", ` + + {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }} + {{ .Permalink }} + Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }} + Hugo -- gohugo.io{{ with .Site.LanguageCode }} + {{.}}{{end}}{{ with .Site.Author.email }} + {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Author.email }} + {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Copyright }} + {{.}}{{end}}{{ if not .Date.IsZero }} + {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }} + + {{ range first 15 .Data.Pages }} + + {{ .Title }} + {{ .Permalink }} + {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }} + {{ with .Site.Author.email }}{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}} + {{ .Permalink }} + {{ .Content | html }} + + {{ end }} + +`) + + t.AddInternalTemplate("_default", "sitemap.xml", ` + {{ range .Data.Pages }} + + {{ .Permalink }}{{ if not .Lastmod.IsZero }} + {{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}{{ end }}{{ with .Sitemap.ChangeFreq }} + {{ . }}{{ end }}{{ if ge .Sitemap.Priority 0.0 }} + {{ .Sitemap.Priority }}{{ end }} + + {{ end }} +`) + + // For multilanguage sites + t.AddInternalTemplate("_default", "sitemapindex.xml", ` + {{ range . }} + + {{ .SitemapAbsURL }} + {{ if not .LastChange.IsZero }} + {{ .LastChange.Format "2006-01-02T15:04:05-07:00" | safeHTML }} + {{ end }} + + {{ end }} + +`) + + t.AddInternalTemplate("", "pagination.html", `{{ $pag := $.Paginator }} + {{ if gt $pag.TotalPages 1 }} +
    + {{ with $pag.First }} +
  • + +
  • + {{ end }} +
  • + +
  • + {{ range $pag.Pagers }} +
  • {{ .PageNumber }}
  • + {{ end }} +
  • + +
  • + {{ with $pag.Last }} +
  • + +
  • + {{ end }} +
+ {{ end }}`) + + t.AddInternalTemplate("", "disqus.html", `{{ if .Site.DisqusShortname }}
+ + +comments powered by Disqus{{end}}`) + + // Add SEO & Social metadata + t.AddInternalTemplate("", "opengraph.html", ` + + + +{{ with .Params.images }}{{ range first 6 . }} + +{{ end }}{{ end }} + +{{ if .IsPage }} +{{ if not .PublishDate.IsZero }} +{{ else if not .Date.IsZero }}{{ end }} +{{ if not .Lastmod.IsZero }}{{ end }} +{{ else }} +{{ if not .Date.IsZero }}{{ end }} +{{ end }}{{ with .Params.audio }} +{{ end }}{{ with .Params.locale }} +{{ end }}{{ with .Site.Params.title }} +{{ end }}{{ with .Params.videos }} +{{ range .Params.videos }} + +{{ end }}{{ end }} + + +{{ $permalink := .Permalink }} +{{ $siteSeries := .Site.Taxonomies.series }}{{ with .Params.series }} +{{ range $name := . }} + {{ $series := index $siteSeries $name }} + {{ range $page := first 6 $series.Pages }} + {{ if ne $page.Permalink $permalink }}{{ end }} + {{ end }} +{{ end }}{{ end }} + +{{ if .IsPage }} +{{ range .Site.Authors }}{{ with .Social.facebook }} +{{ end }}{{ with .Site.Social.facebook }} +{{ end }} + +{{ with .Params.tags }}{{ range first 6 . }} + {{ end }}{{ end }} +{{ end }}{{ end }} + + +{{ with .Site.Social.facebook_admin }}{{ end }}`) + + t.AddInternalTemplate("", "twitter_cards.html", `{{ if .IsPage }} +{{ with .Params.images }} + + + +{{ else }} + +{{ end }} + + + + +{{ with .Site.Social.twitter }}{{ end }} +{{ with .Site.Social.twitter_domain }}{{ end }} +{{ range .Site.Authors }} + {{ with .twitter }}{{ end }} +{{ end }}{{ end }}`) + + t.AddInternalTemplate("", "google_news.html", `{{ if .IsPage }}{{ with .Params.news_keywords }} + +{{ end }}{{ end }}`) + + t.AddInternalTemplate("", "schema.html", `{{ with .Site.Social.GooglePlus }}{{ end }} + + + +{{if .IsPage}}{{ $ISO8601 := "2006-01-02T15:04:05-07:00" }}{{ if not .PublishDate.IsZero }} +{{ end }} +{{ if not .Date.IsZero }}{{ end }} + +{{ with .Params.images }}{{ range first 6 . }} + +{{ end }}{{ end }} + + + +{{ end }}`) + + t.AddInternalTemplate("", "google_analytics.html", `{{ with .Site.GoogleAnalytics }} + +{{ end }}`) + + t.AddInternalTemplate("", "google_analytics_async.html", `{{ with .Site.GoogleAnalytics }} + + +{{ end }}`) + + t.AddInternalTemplate("_default", "robots.txt", "User-agent: *") +} diff --git a/tpl/tplimpl/template_func_truncate.go b/tpl/tplimpl/template_func_truncate.go new file mode 100644 index 00000000..d4bb6327 --- /dev/null +++ b/tpl/tplimpl/template_func_truncate.go @@ -0,0 +1,156 @@ +// Copyright 2016 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 tplimpl + +import ( + "errors" + "html" + "html/template" + "regexp" + "unicode" + "unicode/utf8" + + "github.com/spf13/cast" +) + +var ( + tagRE = regexp.MustCompile(`^<(/)?([^ ]+?)(?:(\s*/)| .*?)?>`) + htmlSinglets = map[string]bool{ + "br": true, "col": true, "link": true, + "base": true, "img": true, "param": true, + "area": true, "hr": true, "input": true, + } +) + +type htmlTag struct { + name string + pos int + openTag bool +} + +func truncate(a interface{}, options ...interface{}) (template.HTML, error) { + length, err := cast.ToIntE(a) + if err != nil { + return "", err + } + var textParam interface{} + var ellipsis string + + switch len(options) { + case 0: + return "", errors.New("truncate requires a length and a string") + case 1: + textParam = options[0] + ellipsis = " …" + case 2: + textParam = options[1] + ellipsis, err = cast.ToStringE(options[0]) + if err != nil { + return "", errors.New("ellipsis must be a string") + } + if _, ok := options[0].(template.HTML); !ok { + ellipsis = html.EscapeString(ellipsis) + } + default: + return "", errors.New("too many arguments passed to truncate") + } + if err != nil { + return "", errors.New("text to truncate must be a string") + } + text, err := cast.ToStringE(textParam) + if err != nil { + return "", errors.New("text must be a string") + } + + _, isHTML := textParam.(template.HTML) + + if utf8.RuneCountInString(text) <= length { + if isHTML { + return template.HTML(text), nil + } + return template.HTML(html.EscapeString(text)), nil + } + + tags := []htmlTag{} + var lastWordIndex, lastNonSpace, currentLen, endTextPos, nextTag int + + for i, r := range text { + if i < nextTag { + continue + } + + if isHTML { + // Make sure we keep tag of HTML tags + slice := text[i:] + m := tagRE.FindStringSubmatchIndex(slice) + if len(m) > 0 && m[0] == 0 { + nextTag = i + m[1] + tagname := slice[m[4]:m[5]] + lastWordIndex = lastNonSpace + _, singlet := htmlSinglets[tagname] + if !singlet && m[6] == -1 { + tags = append(tags, htmlTag{name: tagname, pos: i, openTag: m[2] == -1}) + } + + continue + } + } + + currentLen++ + if unicode.IsSpace(r) { + lastWordIndex = lastNonSpace + } else if unicode.In(r, unicode.Han, unicode.Hangul, unicode.Hiragana, unicode.Katakana) { + lastWordIndex = i + } else { + lastNonSpace = i + utf8.RuneLen(r) + } + + if currentLen > length { + if lastWordIndex == 0 { + endTextPos = i + } else { + endTextPos = lastWordIndex + } + out := text[0:endTextPos] + if isHTML { + out += ellipsis + // Close out any open HTML tags + var currentTag *htmlTag + for i := len(tags) - 1; i >= 0; i-- { + tag := tags[i] + if tag.pos >= endTextPos || currentTag != nil { + if currentTag != nil && currentTag.name == tag.name { + currentTag = nil + } + continue + } + + if tag.openTag { + out += ("") + } else { + currentTag = &tag + } + } + + return template.HTML(out), nil + } + return template.HTML(html.EscapeString(out) + ellipsis), nil + } + } + + if isHTML { + return template.HTML(text), nil + } + return template.HTML(html.EscapeString(text)), nil +} diff --git a/tpl/tplimpl/template_func_truncate_test.go b/tpl/tplimpl/template_func_truncate_test.go new file mode 100644 index 00000000..9c4beecf --- /dev/null +++ b/tpl/tplimpl/template_func_truncate_test.go @@ -0,0 +1,83 @@ +// Copyright 2016 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 tplimpl + +import ( + "html/template" + "reflect" + "strings" + "testing" +) + +func TestTruncate(t *testing.T) { + t.Parallel() + var err error + cases := []struct { + v1 interface{} + v2 interface{} + v3 interface{} + want interface{} + isErr bool + }{ + {10, "I am a test sentence", nil, template.HTML("I am a …"), false}, + {10, "", "I am a test sentence", template.HTML("I am a"), false}, + {10, "", "a b c d e f g h i j k", template.HTML("a b c d e"), false}, + {12, "", "Should be escaped", template.HTML("<b>Should be"), false}, + {10, template.HTML(" Read more"), "I am a test sentence", template.HTML("I am a Read more"), false}, + {20, template.HTML("I have a Markdown link inside."), nil, template.HTML("I have a Markdown …"), false}, + {10, "IamanextremelylongwordthatjustgoesonandonandonjusttoannoyyoualmostasifIwaswritteninGermanActuallyIbettheresagermanwordforthis", nil, template.HTML("Iamanextre …"), false}, + {10, template.HTML("

IamanextremelylongwordthatjustgoesonandonandonjusttoannoyyoualmostasifIwaswritteninGermanActuallyIbettheresagermanwordforthis

"), nil, template.HTML("

Iamanextre …

"), false}, + {13, template.HTML("With Markdown inside."), nil, template.HTML("With Markdown …"), false}, + {14, "Hello中国 Good 好的", nil, template.HTML("Hello中国 Good 好 …"), false}, + {15, "", template.HTML("A
tag that's not closed"), template.HTML("A
tag that's"), false}, + {14, template.HTML("

Hello中国 Good 好的

"), nil, template.HTML("

Hello中国 Good 好 …

"), false}, + {2, template.HTML("

P1

P2

"), nil, template.HTML("

P1 …

"), false}, + {3, template.HTML(strings.Repeat("

P

", 20)), nil, template.HTML("

P

P

P …

"), false}, + {18, template.HTML("

test hello test something

"), nil, template.HTML("

test hello test …

"), false}, + {4, template.HTML("

abc d e

"), nil, template.HTML("

abc …

"), false}, + {10, nil, nil, template.HTML(""), true}, + {nil, nil, nil, template.HTML(""), true}, + } + for i, c := range cases { + var result template.HTML + if c.v2 == nil { + result, err = truncate(c.v1) + } else if c.v3 == nil { + result, err = truncate(c.v1, c.v2) + } else { + result, err = truncate(c.v1, c.v2, c.v3) + } + + if c.isErr { + if err == nil { + t.Errorf("[%d] Slice didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(result, c.want) { + t.Errorf("[%d] got '%s' but expected '%s'", i, result, c.want) + } + } + } + + // Too many arguments + _, err = truncate(10, " ...", "I am a test sentence", "wrong") + if err == nil { + t.Errorf("Should have errored") + } + +} diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go new file mode 100644 index 00000000..dae621ac --- /dev/null +++ b/tpl/tplimpl/template_funcs.go @@ -0,0 +1,2217 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Portions Copyright The Go Authors. + +// 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 tplimpl + +import ( + "bytes" + _md5 "crypto/md5" + _sha1 "crypto/sha1" + _sha256 "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "html" + "html/template" + "image" + "math/rand" + "net/url" + "os" + "reflect" + "regexp" + "sort" + "strconv" + "strings" + "sync" + "time" + "unicode/utf8" + + "github.com/bep/inflect" + "github.com/spf13/afero" + "github.com/spf13/cast" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/helpers" + jww "github.com/spf13/jwalterweatherman" + + // Importing image codecs for image.DecodeConfig + _ "image/gif" + _ "image/jpeg" + _ "image/png" +) + +// Some of the template funcs are'nt entirely stateless. +type templateFuncster struct { + funcMap template.FuncMap + cachedPartials partialCache + *deps.Deps +} + +func newTemplateFuncster(deps *deps.Deps) *templateFuncster { + return &templateFuncster{ + Deps: deps, + cachedPartials: partialCache{p: make(map[string]template.HTML)}, + } +} + +// eq returns the boolean truth of arg1 == arg2. +func eq(x, y interface{}) bool { + normalize := func(v interface{}) interface{} { + vv := reflect.ValueOf(v) + switch vv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return vv.Int() + case reflect.Float32, reflect.Float64: + return vv.Float() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return vv.Uint() + default: + return v + } + } + x = normalize(x) + y = normalize(y) + return reflect.DeepEqual(x, y) +} + +// ne returns the boolean truth of arg1 != arg2. +func ne(x, y interface{}) bool { + return !eq(x, y) +} + +// ge returns the boolean truth of arg1 >= arg2. +func ge(a, b interface{}) bool { + left, right := compareGetFloat(a, b) + return left >= right +} + +// gt returns the boolean truth of arg1 > arg2. +func gt(a, b interface{}) bool { + left, right := compareGetFloat(a, b) + return left > right +} + +// le returns the boolean truth of arg1 <= arg2. +func le(a, b interface{}) bool { + left, right := compareGetFloat(a, b) + return left <= right +} + +// lt returns the boolean truth of arg1 < arg2. +func lt(a, b interface{}) bool { + left, right := compareGetFloat(a, b) + return left < right +} + +// dictionary creates a map[string]interface{} from the given parameters by +// walking the parameters and treating them as key-value pairs. The number +// of parameters must be even. +func dictionary(values ...interface{}) (map[string]interface{}, error) { + if len(values)%2 != 0 { + return nil, errors.New("invalid dict call") + } + dict := make(map[string]interface{}, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, ok := values[i].(string) + if !ok { + return nil, errors.New("dict keys must be strings") + } + dict[key] = values[i+1] + } + return dict, nil +} + +// slice returns a slice of all passed arguments +func slice(args ...interface{}) []interface{} { + return args +} + +func compareGetFloat(a interface{}, b interface{}) (float64, float64) { + var left, right float64 + var leftStr, rightStr *string + av := reflect.ValueOf(a) + + switch av.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + left = float64(av.Len()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + left = float64(av.Int()) + case reflect.Float32, reflect.Float64: + left = av.Float() + case reflect.String: + var err error + left, err = strconv.ParseFloat(av.String(), 64) + if err != nil { + str := av.String() + leftStr = &str + } + case reflect.Struct: + switch av.Type() { + case timeType: + left = float64(toTimeUnix(av)) + } + } + + bv := reflect.ValueOf(b) + + switch bv.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + right = float64(bv.Len()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + right = float64(bv.Int()) + case reflect.Float32, reflect.Float64: + right = bv.Float() + case reflect.String: + var err error + right, err = strconv.ParseFloat(bv.String(), 64) + if err != nil { + str := bv.String() + rightStr = &str + } + case reflect.Struct: + switch bv.Type() { + case timeType: + right = float64(toTimeUnix(bv)) + } + } + + switch { + case leftStr == nil || rightStr == nil: + case *leftStr < *rightStr: + return 0, 1 + case *leftStr > *rightStr: + return 1, 0 + default: + return 0, 0 + } + + return left, right +} + +// slicestr slices a string by specifying a half-open range with +// two indices, start and end. 1 and 4 creates a slice including elements 1 through 3. +// The end index can be omitted, it defaults to the string's length. +func slicestr(a interface{}, startEnd ...interface{}) (string, error) { + aStr, err := cast.ToStringE(a) + if err != nil { + return "", err + } + + var argStart, argEnd int + + argNum := len(startEnd) + + if argNum > 0 { + if argStart, err = cast.ToIntE(startEnd[0]); err != nil { + return "", errors.New("start argument must be integer") + } + } + if argNum > 1 { + if argEnd, err = cast.ToIntE(startEnd[1]); err != nil { + return "", errors.New("end argument must be integer") + } + } + + if argNum > 2 { + return "", errors.New("too many arguments") + } + + asRunes := []rune(aStr) + + if argNum > 0 && (argStart < 0 || argStart >= len(asRunes)) { + return "", errors.New("slice bounds out of range") + } + + if argNum == 2 { + if argEnd < 0 || argEnd > len(asRunes) { + return "", errors.New("slice bounds out of range") + } + return string(asRunes[argStart:argEnd]), nil + } else if argNum == 1 { + return string(asRunes[argStart:]), nil + } else { + return string(asRunes[:]), nil + } + +} + +// hasPrefix tests whether the input s begins with prefix. +func hasPrefix(s, prefix interface{}) (bool, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return false, err + } + + sp, err := cast.ToStringE(prefix) + if err != nil { + return false, err + } + + return strings.HasPrefix(ss, sp), nil +} + +// substr extracts parts of a string, beginning at the character at the specified +// position, and returns the specified number of characters. +// +// It normally takes two parameters: start and length. +// It can also take one parameter: start, i.e. length is omitted, in which case +// the substring starting from start until the end of the string will be returned. +// +// To extract characters from the end of the string, use a negative start number. +// +// In addition, borrowing from the extended behavior described at http://php.net/substr, +// if length is given and is negative, then that many characters will be omitted from +// the end of string. +func substr(a interface{}, nums ...interface{}) (string, error) { + aStr, err := cast.ToStringE(a) + if err != nil { + return "", err + } + + var start, length int + + asRunes := []rune(aStr) + + switch len(nums) { + case 0: + return "", errors.New("too less arguments") + case 1: + if start, err = cast.ToIntE(nums[0]); err != nil { + return "", errors.New("start argument must be integer") + } + length = len(asRunes) + case 2: + if start, err = cast.ToIntE(nums[0]); err != nil { + return "", errors.New("start argument must be integer") + } + if length, err = cast.ToIntE(nums[1]); err != nil { + return "", errors.New("length argument must be integer") + } + default: + return "", errors.New("too many arguments") + } + + if start < -len(asRunes) { + start = 0 + } + if start > len(asRunes) { + return "", fmt.Errorf("start position out of bounds for %d-byte string", len(aStr)) + } + + var s, e int + if start >= 0 && length >= 0 { + s = start + e = start + length + } else if start < 0 && length >= 0 { + s = len(asRunes) + start - length + 1 + e = len(asRunes) + start + 1 + } else if start >= 0 && length < 0 { + s = start + e = len(asRunes) + length + } else { + s = len(asRunes) + start + e = len(asRunes) + length + } + + if s > e { + return "", fmt.Errorf("calculated start position greater than end position: %d > %d", s, e) + } + if e > len(asRunes) { + e = len(asRunes) + } + + return string(asRunes[s:e]), nil +} + +// split slices an input string into all substrings separated by delimiter. +func split(a interface{}, delimiter string) ([]string, error) { + aStr, err := cast.ToStringE(a) + if err != nil { + return []string{}, err + } + return strings.Split(aStr, delimiter), nil +} + +// intersect returns the common elements in the given sets, l1 and l2. l1 and +// l2 must be of the same type and may be either arrays or slices. +func intersect(l1, l2 interface{}) (interface{}, error) { + if l1 == nil || l2 == nil { + return make([]interface{}, 0), nil + } + + l1v := reflect.ValueOf(l1) + l2v := reflect.ValueOf(l2) + + switch l1v.Kind() { + case reflect.Array, reflect.Slice: + switch l2v.Kind() { + case reflect.Array, reflect.Slice: + r := reflect.MakeSlice(l1v.Type(), 0, 0) + for i := 0; i < l1v.Len(); i++ { + l1vv := l1v.Index(i) + for j := 0; j < l2v.Len(); j++ { + l2vv := l2v.Index(j) + switch l1vv.Kind() { + case reflect.String: + if l1vv.Type() == l2vv.Type() && l1vv.String() == l2vv.String() && !in(r.Interface(), l2vv.Interface()) { + r = reflect.Append(r, l2vv) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + switch l2vv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if l1vv.Int() == l2vv.Int() && !in(r.Interface(), l2vv.Interface()) { + r = reflect.Append(r, l2vv) + } + } + case reflect.Float32, reflect.Float64: + switch l2vv.Kind() { + case reflect.Float32, reflect.Float64: + if l1vv.Float() == l2vv.Float() && !in(r.Interface(), l2vv.Interface()) { + r = reflect.Append(r, l2vv) + } + } + } + } + } + return r.Interface(), nil + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(l2).Type().String()) + } + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(l1).Type().String()) + } +} + +// ResetCaches resets all caches that might be used during build. +// TODO(bep) globals move image config cache to funcster +func ResetCaches() { + resetImageConfigCache() +} + +// imageConfigCache is a lockable cache for image.Config objects. It must be +// locked before reading or writing to config. +type imageConfigCache struct { + config map[string]image.Config + sync.RWMutex +} + +var defaultImageConfigCache = imageConfigCache{ + config: map[string]image.Config{}, +} + +// resetImageConfigCache initializes and resets the imageConfig cache for the +// imageConfig template function. This should be run once before every batch of +// template renderers so the cache is cleared for new data. +func resetImageConfigCache() { + defaultImageConfigCache.Lock() + defer defaultImageConfigCache.Unlock() + + defaultImageConfigCache.config = map[string]image.Config{} +} + +// imageConfig returns the image.Config for the specified path relative to the +// working directory. resetImageConfigCache must be run beforehand. +func (t *templateFuncster) imageConfig(path interface{}) (image.Config, error) { + filename, err := cast.ToStringE(path) + if err != nil { + return image.Config{}, err + } + + if filename == "" { + return image.Config{}, errors.New("imageConfig needs a filename") + } + + // Check cache for image config. + defaultImageConfigCache.RLock() + config, ok := defaultImageConfigCache.config[filename] + defaultImageConfigCache.RUnlock() + + if ok { + return config, nil + } + + f, err := t.Fs.WorkingDir.Open(filename) + if err != nil { + return image.Config{}, err + } + + config, _, err = image.DecodeConfig(f) + + defaultImageConfigCache.Lock() + defaultImageConfigCache.config[filename] = config + defaultImageConfigCache.Unlock() + + return config, err +} + +// in returns whether v is in the set l. l may be an array or slice. +func in(l interface{}, v interface{}) bool { + lv := reflect.ValueOf(l) + vv := reflect.ValueOf(v) + + switch lv.Kind() { + case reflect.Array, reflect.Slice: + for i := 0; i < lv.Len(); i++ { + lvv := lv.Index(i) + lvv, isNil := indirect(lvv) + if isNil { + continue + } + switch lvv.Kind() { + case reflect.String: + if vv.Type() == lvv.Type() && vv.String() == lvv.String() { + return true + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + switch vv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if vv.Int() == lvv.Int() { + return true + } + } + case reflect.Float32, reflect.Float64: + switch vv.Kind() { + case reflect.Float32, reflect.Float64: + if vv.Float() == lvv.Float() { + return true + } + } + } + } + case reflect.String: + if vv.Type() == lv.Type() && strings.Contains(lv.String(), vv.String()) { + return true + } + } + return false +} + +// first returns the first N items in a rangeable list. +func first(limit interface{}, seq interface{}) (interface{}, error) { + if limit == nil || seq == nil { + return nil, errors.New("both limit and seq must be provided") + } + + limitv, err := cast.ToIntE(limit) + + if err != nil { + return nil, err + } + + if limitv < 1 { + return nil, errors.New("can't return negative/empty count of items from sequence") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + // okay + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) + } + if limitv > seqv.Len() { + limitv = seqv.Len() + } + return seqv.Slice(0, limitv).Interface(), nil +} + +// findRE returns a list of strings that match the regular expression. By default all matches +// will be included. The number of matches can be limited with an optional third parameter. +func findRE(expr string, content interface{}, limit ...interface{}) ([]string, error) { + re, err := reCache.Get(expr) + if err != nil { + return nil, err + } + + conv, err := cast.ToStringE(content) + if err != nil { + return nil, err + } + + if len(limit) == 0 { + return re.FindAllString(conv, -1), nil + } + + lim, err := cast.ToIntE(limit[0]) + if err != nil { + return nil, err + } + + return re.FindAllString(conv, lim), nil +} + +// last returns the last N items in a rangeable list. +func last(limit interface{}, seq interface{}) (interface{}, error) { + if limit == nil || seq == nil { + return nil, errors.New("both limit and seq must be provided") + } + + limitv, err := cast.ToIntE(limit) + + if err != nil { + return nil, err + } + + if limitv < 1 { + return nil, errors.New("can't return negative/empty count of items from sequence") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + // okay + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) + } + if limitv > seqv.Len() { + limitv = seqv.Len() + } + return seqv.Slice(seqv.Len()-limitv, seqv.Len()).Interface(), nil +} + +// after returns all the items after the first N in a rangeable list. +func after(index interface{}, seq interface{}) (interface{}, error) { + if index == nil || seq == nil { + return nil, errors.New("both limit and seq must be provided") + } + + indexv, err := cast.ToIntE(index) + + if err != nil { + return nil, err + } + + if indexv < 1 { + return nil, errors.New("can't return negative/empty count of items from sequence") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + // okay + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) + } + if indexv >= seqv.Len() { + return nil, errors.New("no items left") + } + return seqv.Slice(indexv, seqv.Len()).Interface(), nil +} + +// shuffle returns the given rangeable list in a randomised order. +func shuffle(seq interface{}) (interface{}, error) { + if seq == nil { + return nil, errors.New("both count and seq must be provided") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + // okay + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) + } + + shuffled := reflect.MakeSlice(reflect.TypeOf(seq), seqv.Len(), seqv.Len()) + + rand.Seed(time.Now().UTC().UnixNano()) + randomIndices := rand.Perm(seqv.Len()) + + for index, value := range randomIndices { + shuffled.Index(value).Set(seqv.Index(index)) + } + + return shuffled.Interface(), nil +} + +func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error) { + if !obj.IsValid() { + return zero, errors.New("can't evaluate an invalid value") + } + typ := obj.Type() + obj, isNil := indirect(obj) + + // first, check whether obj has a method. In this case, obj is + // an interface, a struct or its pointer. If obj is a struct, + // to check all T and *T method, use obj pointer type Value + objPtr := obj + if objPtr.Kind() != reflect.Interface && objPtr.CanAddr() { + objPtr = objPtr.Addr() + } + mt, ok := objPtr.Type().MethodByName(elemName) + if ok { + if mt.PkgPath != "" { + return zero, fmt.Errorf("%s is an unexported method of type %s", elemName, typ) + } + // struct pointer has one receiver argument and interface doesn't have an argument + if mt.Type.NumIn() > 1 || mt.Type.NumOut() == 0 || mt.Type.NumOut() > 2 { + return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) + } + if mt.Type.NumOut() == 1 && mt.Type.Out(0).Implements(errorType) { + return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) + } + if mt.Type.NumOut() == 2 && !mt.Type.Out(1).Implements(errorType) { + return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) + } + res := objPtr.Method(mt.Index).Call([]reflect.Value{}) + if len(res) == 2 && !res[1].IsNil() { + return zero, fmt.Errorf("error at calling a method %s of type %s: %s", elemName, typ, res[1].Interface().(error)) + } + return res[0], nil + } + + // elemName isn't a method so next start to check whether it is + // a struct field or a map value. In both cases, it mustn't be + // a nil value + if isNil { + return zero, fmt.Errorf("can't evaluate a nil pointer of type %s by a struct field or map key name %s", typ, elemName) + } + switch obj.Kind() { + case reflect.Struct: + ft, ok := obj.Type().FieldByName(elemName) + if ok { + if ft.PkgPath != "" && !ft.Anonymous { + return zero, fmt.Errorf("%s is an unexported field of struct type %s", elemName, typ) + } + return obj.FieldByIndex(ft.Index), nil + } + return zero, fmt.Errorf("%s isn't a field of struct type %s", elemName, typ) + case reflect.Map: + kv := reflect.ValueOf(elemName) + if kv.Type().AssignableTo(obj.Type().Key()) { + return obj.MapIndex(kv), nil + } + return zero, fmt.Errorf("%s isn't a key of map type %s", elemName, typ) + } + return zero, fmt.Errorf("%s is neither a struct field, a method nor a map element of type %s", elemName, typ) +} + +func checkCondition(v, mv reflect.Value, op string) (bool, error) { + v, vIsNil := indirect(v) + if !v.IsValid() { + vIsNil = true + } + mv, mvIsNil := indirect(mv) + if !mv.IsValid() { + mvIsNil = true + } + if vIsNil || mvIsNil { + switch op { + case "", "=", "==", "eq": + return vIsNil == mvIsNil, nil + case "!=", "<>", "ne": + return vIsNil != mvIsNil, nil + } + return false, nil + } + + if v.Kind() == reflect.Bool && mv.Kind() == reflect.Bool { + switch op { + case "", "=", "==", "eq": + return v.Bool() == mv.Bool(), nil + case "!=", "<>", "ne": + return v.Bool() != mv.Bool(), nil + } + return false, nil + } + + var ivp, imvp *int64 + var svp, smvp *string + var slv, slmv interface{} + var ima []int64 + var sma []string + if mv.Type() == v.Type() { + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + iv := v.Int() + ivp = &iv + imv := mv.Int() + imvp = &imv + case reflect.String: + sv := v.String() + svp = &sv + smv := mv.String() + smvp = &smv + case reflect.Struct: + switch v.Type() { + case timeType: + iv := toTimeUnix(v) + ivp = &iv + imv := toTimeUnix(mv) + imvp = &imv + } + case reflect.Array, reflect.Slice: + slv = v.Interface() + slmv = mv.Interface() + } + } else { + if mv.Kind() != reflect.Array && mv.Kind() != reflect.Slice { + return false, nil + } + + if mv.Len() == 0 { + return false, nil + } + + if v.Kind() != reflect.Interface && mv.Type().Elem().Kind() != reflect.Interface && mv.Type().Elem() != v.Type() { + return false, nil + } + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + iv := v.Int() + ivp = &iv + for i := 0; i < mv.Len(); i++ { + if anInt := toInt(mv.Index(i)); anInt != -1 { + ima = append(ima, anInt) + } + + } + case reflect.String: + sv := v.String() + svp = &sv + for i := 0; i < mv.Len(); i++ { + if aString := toString(mv.Index(i)); aString != "" { + sma = append(sma, aString) + } + } + case reflect.Struct: + switch v.Type() { + case timeType: + iv := toTimeUnix(v) + ivp = &iv + for i := 0; i < mv.Len(); i++ { + ima = append(ima, toTimeUnix(mv.Index(i))) + } + } + } + } + + switch op { + case "", "=", "==", "eq": + if ivp != nil && imvp != nil { + return *ivp == *imvp, nil + } else if svp != nil && smvp != nil { + return *svp == *smvp, nil + } + case "!=", "<>", "ne": + if ivp != nil && imvp != nil { + return *ivp != *imvp, nil + } else if svp != nil && smvp != nil { + return *svp != *smvp, nil + } + case ">=", "ge": + if ivp != nil && imvp != nil { + return *ivp >= *imvp, nil + } else if svp != nil && smvp != nil { + return *svp >= *smvp, nil + } + case ">", "gt": + if ivp != nil && imvp != nil { + return *ivp > *imvp, nil + } else if svp != nil && smvp != nil { + return *svp > *smvp, nil + } + case "<=", "le": + if ivp != nil && imvp != nil { + return *ivp <= *imvp, nil + } else if svp != nil && smvp != nil { + return *svp <= *smvp, nil + } + case "<", "lt": + if ivp != nil && imvp != nil { + return *ivp < *imvp, nil + } else if svp != nil && smvp != nil { + return *svp < *smvp, nil + } + case "in", "not in": + var r bool + if ivp != nil && len(ima) > 0 { + r = in(ima, *ivp) + } else if svp != nil { + if len(sma) > 0 { + r = in(sma, *svp) + } else if smvp != nil { + r = in(*smvp, *svp) + } + } else { + return false, nil + } + if op == "not in" { + return !r, nil + } + return r, nil + case "intersect": + r, err := intersect(slv, slmv) + if err != nil { + return false, err + } + + if reflect.TypeOf(r).Kind() == reflect.Slice { + s := reflect.ValueOf(r) + + if s.Len() > 0 { + return true, nil + } + return false, nil + } + return false, errors.New("invalid intersect values") + default: + return false, errors.New("no such operator") + } + return false, nil +} + +// parseWhereArgs parses the end arguments to the where function. Return a +// match value and an operator, if one is defined. +func parseWhereArgs(args ...interface{}) (mv reflect.Value, op string, err error) { + switch len(args) { + case 1: + mv = reflect.ValueOf(args[0]) + case 2: + var ok bool + if op, ok = args[0].(string); !ok { + err = errors.New("operator argument must be string type") + return + } + op = strings.TrimSpace(strings.ToLower(op)) + mv = reflect.ValueOf(args[1]) + default: + err = errors.New("can't evaluate the array by no match argument or more than or equal to two arguments") + } + return +} + +// checkWhereArray handles the where-matching logic when the seqv value is an +// Array or Slice. +func checkWhereArray(seqv, kv, mv reflect.Value, path []string, op string) (interface{}, error) { + rv := reflect.MakeSlice(seqv.Type(), 0, 0) + for i := 0; i < seqv.Len(); i++ { + var vvv reflect.Value + rvv := seqv.Index(i) + if kv.Kind() == reflect.String { + vvv = rvv + for _, elemName := range path { + var err error + vvv, err = evaluateSubElem(vvv, elemName) + if err != nil { + return nil, err + } + } + } else { + vv, _ := indirect(rvv) + if vv.Kind() == reflect.Map && kv.Type().AssignableTo(vv.Type().Key()) { + vvv = vv.MapIndex(kv) + } + } + + if ok, err := checkCondition(vvv, mv, op); ok { + rv = reflect.Append(rv, rvv) + } else if err != nil { + return nil, err + } + } + return rv.Interface(), nil +} + +// checkWhereMap handles the where-matching logic when the seqv value is a Map. +func checkWhereMap(seqv, kv, mv reflect.Value, path []string, op string) (interface{}, error) { + rv := reflect.MakeMap(seqv.Type()) + keys := seqv.MapKeys() + for _, k := range keys { + elemv := seqv.MapIndex(k) + switch elemv.Kind() { + case reflect.Array, reflect.Slice: + r, err := checkWhereArray(elemv, kv, mv, path, op) + if err != nil { + return nil, err + } + + switch rr := reflect.ValueOf(r); rr.Kind() { + case reflect.Slice: + if rr.Len() > 0 { + rv.SetMapIndex(k, elemv) + } + } + case reflect.Interface: + elemvv, isNil := indirect(elemv) + if isNil { + continue + } + + switch elemvv.Kind() { + case reflect.Array, reflect.Slice: + r, err := checkWhereArray(elemvv, kv, mv, path, op) + if err != nil { + return nil, err + } + + switch rr := reflect.ValueOf(r); rr.Kind() { + case reflect.Slice: + if rr.Len() > 0 { + rv.SetMapIndex(k, elemv) + } + } + } + } + } + return rv.Interface(), nil +} + +// where returns a filtered subset of a given data type. +func where(seq, key interface{}, args ...interface{}) (interface{}, error) { + seqv, isNil := indirect(reflect.ValueOf(seq)) + if isNil { + return nil, errors.New("can't iterate over a nil value of type " + reflect.ValueOf(seq).Type().String()) + } + + mv, op, err := parseWhereArgs(args...) + if err != nil { + return nil, err + } + + var path []string + kv := reflect.ValueOf(key) + if kv.Kind() == reflect.String { + path = strings.Split(strings.Trim(kv.String(), "."), ".") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice: + return checkWhereArray(seqv, kv, mv, path, op) + case reflect.Map: + return checkWhereMap(seqv, kv, mv, path, op) + default: + return nil, fmt.Errorf("can't iterate over %v", seq) + } +} + +// apply takes a map, array, or slice and returns a new slice with the function fname applied over it. +func (t *templateFuncster) apply(seq interface{}, fname string, args ...interface{}) (interface{}, error) { + if seq == nil { + return make([]interface{}, 0), nil + } + + if fname == "apply" { + return nil, errors.New("can't apply myself (no turtles allowed)") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + fn, found := t.funcMap[fname] + if !found { + return nil, errors.New("can't find function " + fname) + } + + fnv := reflect.ValueOf(fn) + + switch seqv.Kind() { + case reflect.Array, reflect.Slice: + r := make([]interface{}, seqv.Len()) + for i := 0; i < seqv.Len(); i++ { + vv := seqv.Index(i) + + vvv, err := applyFnToThis(fnv, vv, args...) + + if err != nil { + return nil, err + } + + r[i] = vvv.Interface() + } + + return r, nil + default: + return nil, fmt.Errorf("can't apply over %v", seq) + } +} + +func applyFnToThis(fn, this reflect.Value, args ...interface{}) (reflect.Value, error) { + n := make([]reflect.Value, len(args)) + for i, arg := range args { + if arg == "." { + n[i] = this + } else { + n[i] = reflect.ValueOf(arg) + } + } + + num := fn.Type().NumIn() + + if fn.Type().IsVariadic() { + num-- + } + + // TODO(bep) see #1098 - also see template_tests.go + /*if len(args) < num { + return reflect.ValueOf(nil), errors.New("Too few arguments") + } else if len(args) > num { + return reflect.ValueOf(nil), errors.New("Too many arguments") + }*/ + + for i := 0; i < num; i++ { + if xt, targ := n[i].Type(), fn.Type().In(i); !xt.AssignableTo(targ) { + return reflect.ValueOf(nil), errors.New("called apply using " + xt.String() + " as type " + targ.String()) + } + } + + res := fn.Call(n) + + if len(res) == 1 || res[1].IsNil() { + return res[0], nil + } + return reflect.ValueOf(nil), res[1].Interface().(error) +} + +// delimit takes a given sequence and returns a delimited HTML string. +// If last is passed to the function, it will be used as the final delimiter. +func delimit(seq, delimiter interface{}, last ...interface{}) (template.HTML, error) { + d, err := cast.ToStringE(delimiter) + if err != nil { + return "", err + } + + var dLast *string + if len(last) > 0 { + l := last[0] + dStr, err := cast.ToStringE(l) + if err != nil { + dLast = nil + } + dLast = &dStr + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return "", errors.New("can't iterate over a nil value") + } + + var str string + switch seqv.Kind() { + case reflect.Map: + sortSeq, err := sortSeq(seq) + if err != nil { + return "", err + } + seqv = reflect.ValueOf(sortSeq) + fallthrough + case reflect.Array, reflect.Slice, reflect.String: + for i := 0; i < seqv.Len(); i++ { + val := seqv.Index(i).Interface() + valStr, err := cast.ToStringE(val) + if err != nil { + continue + } + switch { + case i == seqv.Len()-2 && dLast != nil: + str += valStr + *dLast + case i == seqv.Len()-1: + str += valStr + default: + str += valStr + d + } + } + + default: + return "", fmt.Errorf("can't iterate over %v", seq) + } + + return template.HTML(str), nil +} + +// sortSeq returns a sorted sequence. +func sortSeq(seq interface{}, args ...interface{}) (interface{}, error) { + if seq == nil { + return nil, errors.New("sequence must be provided") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.Map: + // ok + default: + return nil, errors.New("can't sort " + reflect.ValueOf(seq).Type().String()) + } + + // Create a list of pairs that will be used to do the sort + p := pairList{SortAsc: true, SliceType: reflect.SliceOf(seqv.Type().Elem())} + p.Pairs = make([]pair, seqv.Len()) + + var sortByField string + for i, l := range args { + dStr, err := cast.ToStringE(l) + switch { + case i == 0 && err != nil: + sortByField = "" + case i == 0 && err == nil: + sortByField = dStr + case i == 1 && err == nil && dStr == "desc": + p.SortAsc = false + case i == 1: + p.SortAsc = true + } + } + path := strings.Split(strings.Trim(sortByField, "."), ".") + + switch seqv.Kind() { + case reflect.Array, reflect.Slice: + for i := 0; i < seqv.Len(); i++ { + p.Pairs[i].Value = seqv.Index(i) + if sortByField == "" || sortByField == "value" { + p.Pairs[i].Key = p.Pairs[i].Value + } else { + v := p.Pairs[i].Value + var err error + for _, elemName := range path { + v, err = evaluateSubElem(v, elemName) + if err != nil { + return nil, err + } + } + p.Pairs[i].Key = v + } + } + + case reflect.Map: + keys := seqv.MapKeys() + for i := 0; i < seqv.Len(); i++ { + p.Pairs[i].Value = seqv.MapIndex(keys[i]) + if sortByField == "" { + p.Pairs[i].Key = keys[i] + } else if sortByField == "value" { + p.Pairs[i].Key = p.Pairs[i].Value + } else { + v := p.Pairs[i].Value + var err error + for _, elemName := range path { + v, err = evaluateSubElem(v, elemName) + if err != nil { + return nil, err + } + } + p.Pairs[i].Key = v + } + } + } + return p.sort(), nil +} + +// Credit for pair sorting method goes to Andrew Gerrand +// https://groups.google.com/forum/#!topic/golang-nuts/FT7cjmcL7gw +// A data structure to hold a key/value pair. +type pair struct { + Key reflect.Value + Value reflect.Value +} + +// A slice of pairs that implements sort.Interface to sort by Value. +type pairList struct { + Pairs []pair + SortAsc bool + SliceType reflect.Type +} + +func (p pairList) Swap(i, j int) { p.Pairs[i], p.Pairs[j] = p.Pairs[j], p.Pairs[i] } +func (p pairList) Len() int { return len(p.Pairs) } +func (p pairList) Less(i, j int) bool { + iv := p.Pairs[i].Key + jv := p.Pairs[j].Key + + if iv.IsValid() { + if jv.IsValid() { + // can only call Interface() on valid reflect Values + return lt(iv.Interface(), jv.Interface()) + } + // if j is invalid, test i against i's zero value + return lt(iv.Interface(), reflect.Zero(iv.Type())) + } + + if jv.IsValid() { + // if i is invalid, test j against j's zero value + return lt(reflect.Zero(jv.Type()), jv.Interface()) + } + + return false +} + +// sorts a pairList and returns a slice of sorted values +func (p pairList) sort() interface{} { + if p.SortAsc { + sort.Sort(p) + } else { + sort.Sort(sort.Reverse(p)) + } + sorted := reflect.MakeSlice(p.SliceType, len(p.Pairs), len(p.Pairs)) + for i, v := range p.Pairs { + sorted.Index(i).Set(v.Value) + } + + return sorted.Interface() +} + +// isSet returns whether a given array, channel, slice, or map has a key +// defined. +func isSet(a interface{}, key interface{}) bool { + av := reflect.ValueOf(a) + kv := reflect.ValueOf(key) + + switch av.Kind() { + case reflect.Array, reflect.Chan, reflect.Slice: + if int64(av.Len()) > kv.Int() { + return true + } + case reflect.Map: + if kv.Type() == av.Type().Key() { + return av.MapIndex(kv).IsValid() + } + } + + return false +} + +// returnWhenSet returns a given value if it set. Otherwise, it returns an +// empty string. +func returnWhenSet(a, k interface{}) interface{} { + av, isNil := indirect(reflect.ValueOf(a)) + if isNil { + return "" + } + + var avv reflect.Value + switch av.Kind() { + case reflect.Array, reflect.Slice: + index, ok := k.(int) + if ok && av.Len() > index { + avv = av.Index(index) + } + case reflect.Map: + kv := reflect.ValueOf(k) + if kv.Type().AssignableTo(av.Type().Key()) { + avv = av.MapIndex(kv) + } + } + + avv, isNil = indirect(avv) + + if isNil { + return "" + } + + if avv.IsValid() { + switch avv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return avv.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return avv.Uint() + case reflect.Float32, reflect.Float64: + return avv.Float() + case reflect.String: + return avv.String() + } + } + + return "" +} + +// highlight returns an HTML string with syntax highlighting applied. +func (t *templateFuncster) highlight(in interface{}, lang, opts string) (template.HTML, error) { + str, err := cast.ToStringE(in) + + if err != nil { + return "", err + } + + return template.HTML(helpers.Highlight(t.Cfg, html.UnescapeString(str), lang, opts)), nil +} + +var markdownTrimPrefix = []byte("

") +var markdownTrimSuffix = []byte("

\n") + +// markdownify renders a given string from Markdown to HTML. +func (t *templateFuncster) markdownify(in interface{}) (template.HTML, error) { + text, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + m := t.ContentSpec.RenderBytes(&helpers.RenderingContext{ + Cfg: t.Cfg, + Content: []byte(text), PageFmt: "markdown"}) + m = bytes.TrimPrefix(m, markdownTrimPrefix) + m = bytes.TrimSuffix(m, markdownTrimSuffix) + return template.HTML(m), nil +} + +// jsonify encodes a given object to JSON. +func jsonify(v interface{}) (template.HTML, error) { + b, err := json.Marshal(v) + if err != nil { + return "", err + } + return template.HTML(b), nil +} + +// emojify "emojifies" the given string. +// +// See http://www.emoji-cheat-sheet.com/ +func emojify(in interface{}) (template.HTML, error) { + str, err := cast.ToStringE(in) + + if err != nil { + return "", err + } + + return template.HTML(helpers.Emojify([]byte(str))), nil +} + +// plainify strips any HTML and returns the plain text version. +func plainify(in interface{}) (string, error) { + s, err := cast.ToStringE(in) + + if err != nil { + return "", err + } + + return helpers.StripHTML(s), nil +} + +func refPage(page interface{}, ref, methodName string) template.HTML { + value := reflect.ValueOf(page) + + method := value.MethodByName(methodName) + + if method.IsValid() && method.Type().NumIn() == 1 && method.Type().NumOut() == 2 { + result := method.Call([]reflect.Value{reflect.ValueOf(ref)}) + + url, err := result[0], result[1] + + if !err.IsNil() { + jww.ERROR.Printf("%s", err.Interface()) + return template.HTML(fmt.Sprintf("%s", err.Interface())) + } + + if url.String() == "" { + jww.ERROR.Printf("ref %s could not be found\n", ref) + return template.HTML(ref) + } + + return template.HTML(url.String()) + } + + jww.ERROR.Printf("Can only create references from Page and Node objects.") + return template.HTML(ref) +} + +// ref returns the absolute URL path to a given content item. +func ref(page interface{}, ref string) template.HTML { + return refPage(page, ref, "Ref") +} + +// relRef returns the relative URL path to a given content item. +func relRef(page interface{}, ref string) template.HTML { + return refPage(page, ref, "RelRef") +} + +// chomp removes trailing newline characters from a string. +func chomp(text interface{}) (template.HTML, error) { + s, err := cast.ToStringE(text) + if err != nil { + return "", err + } + + return template.HTML(strings.TrimRight(s, "\r\n")), nil +} + +// lower returns a copy of the input s with all Unicode letters mapped to their +// lower case. +func lower(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return strings.ToLower(ss), nil +} + +// title returns a copy of the input s with all Unicode letters that begin words +// mapped to their title case. +func title(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return strings.Title(ss), nil +} + +// upper returns a copy of the input s with all Unicode letters mapped to their +// upper case. +func upper(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return strings.ToUpper(ss), nil +} + +// trim leading/trailing characters defined by b from a +func trim(a interface{}, b string) (string, error) { + aStr, err := cast.ToStringE(a) + if err != nil { + return "", err + } + return strings.Trim(aStr, b), nil +} + +// replace all occurrences of b with c in a +func replace(a, b, c interface{}) (string, error) { + aStr, err := cast.ToStringE(a) + if err != nil { + return "", err + } + bStr, err := cast.ToStringE(b) + if err != nil { + return "", err + } + cStr, err := cast.ToStringE(c) + if err != nil { + return "", err + } + return strings.Replace(aStr, bStr, cStr, -1), nil +} + +// partialCache represents a cache of partials protected by a mutex. +type partialCache struct { + sync.RWMutex + p map[string]template.HTML +} + +// Get retrieves partial output from the cache based upon the partial name. +// If the partial is not found in the cache, the partial is rendered and added +// to the cache. +func (t *templateFuncster) Get(key, name string, context interface{}) (p template.HTML) { + var ok bool + + t.cachedPartials.RLock() + p, ok = t.cachedPartials.p[key] + t.cachedPartials.RUnlock() + + if ok { + return p + } + + t.cachedPartials.Lock() + if p, ok = t.cachedPartials.p[key]; !ok { + t.cachedPartials.Unlock() + p = t.Tmpl.Partial(name, context) + + t.cachedPartials.Lock() + t.cachedPartials.p[key] = p + + } + t.cachedPartials.Unlock() + + return p +} + +// partialCached executes and caches partial templates. An optional variant +// string parameter (a string slice actually, but be only use a variadic +// argument to make it optional) can be passed so that a given partial can have +// multiple uses. The cache is created with name+variant as the key. +func (t *templateFuncster) partialCached(name string, context interface{}, variant ...string) template.HTML { + key := name + if len(variant) > 0 { + for i := 0; i < len(variant); i++ { + key += variant[i] + } + } + return t.Get(key, name, context) +} + +// regexpCache represents a cache of regexp objects protected by a mutex. +type regexpCache struct { + mu sync.RWMutex + re map[string]*regexp.Regexp +} + +// Get retrieves a regexp object from the cache based upon the pattern. +// If the pattern is not found in the cache, create one +func (rc *regexpCache) Get(pattern string) (re *regexp.Regexp, err error) { + var ok bool + + if re, ok = rc.get(pattern); !ok { + re, err = regexp.Compile(pattern) + if err != nil { + return nil, err + } + rc.set(pattern, re) + } + + return re, nil +} + +func (rc *regexpCache) get(key string) (re *regexp.Regexp, ok bool) { + rc.mu.RLock() + re, ok = rc.re[key] + rc.mu.RUnlock() + return +} + +func (rc *regexpCache) set(key string, re *regexp.Regexp) { + rc.mu.Lock() + rc.re[key] = re + rc.mu.Unlock() +} + +var reCache = regexpCache{re: make(map[string]*regexp.Regexp)} + +// replaceRE exposes a regular expression replacement function to the templates. +func replaceRE(pattern, repl, src interface{}) (_ string, err error) { + patternStr, err := cast.ToStringE(pattern) + if err != nil { + return + } + + replStr, err := cast.ToStringE(repl) + if err != nil { + return + } + + srcStr, err := cast.ToStringE(src) + if err != nil { + return + } + + re, err := reCache.Get(patternStr) + if err != nil { + return "", err + } + return re.ReplaceAllString(srcStr, replStr), nil +} + +// asTime converts the textual representation of the datetime string into +// a time.Time interface. +func asTime(v interface{}) (interface{}, error) { + t, err := cast.ToTimeE(v) + if err != nil { + return nil, err + } + return t, nil +} + +// dateFormat converts the textual representation of the datetime string into +// the other form or returns it of the time.Time value. These are formatted +// with the layout string +func dateFormat(layout string, v interface{}) (string, error) { + t, err := cast.ToTimeE(v) + if err != nil { + return "", err + } + return t.Format(layout), nil +} + +// dfault checks whether a given value is set and returns a default value if it +// is not. "Set" in this context means non-zero for numeric types and times; +// non-zero length for strings, arrays, slices, and maps; +// any boolean or struct value; or non-nil for any other types. +func dfault(dflt interface{}, given ...interface{}) (interface{}, error) { + // given is variadic because the following construct will not pass a piped + // argument when the key is missing: {{ index . "key" | default "foo" }} + // The Go template will complain that we got 1 argument when we expectd 2. + + if len(given) == 0 { + return dflt, nil + } + if len(given) != 1 { + return nil, fmt.Errorf("wrong number of args for default: want 2 got %d", len(given)+1) + } + + g := reflect.ValueOf(given[0]) + if !g.IsValid() { + return dflt, nil + } + + set := false + + switch g.Kind() { + case reflect.Bool: + set = true + case reflect.String, reflect.Array, reflect.Slice, reflect.Map: + set = g.Len() != 0 + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + set = g.Int() != 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + set = g.Uint() != 0 + case reflect.Float32, reflect.Float64: + set = g.Float() != 0 + case reflect.Complex64, reflect.Complex128: + set = g.Complex() != 0 + case reflect.Struct: + switch actual := given[0].(type) { + case time.Time: + set = !actual.IsZero() + default: + set = true + } + default: + set = !g.IsNil() + } + + if set { + return given[0], nil + } + + return dflt, nil +} + +// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero. +// +// Copied from Go stdlib src/text/template/exec.go. +func canBeNil(typ reflect.Type) bool { + switch typ.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return true + } + return false +} + +// prepareArg checks if value can be used as an argument of type argType, and +// converts an invalid value to appropriate zero if possible. +// +// Copied from Go stdlib src/text/template/funcs.go. +func prepareArg(value reflect.Value, argType reflect.Type) (reflect.Value, error) { + if !value.IsValid() { + if !canBeNil(argType) { + return reflect.Value{}, fmt.Errorf("value is nil; should be of type %s", argType) + } + value = reflect.Zero(argType) + } + if !value.Type().AssignableTo(argType) { + return reflect.Value{}, fmt.Errorf("value has type %s; should be %s", value.Type(), argType) + } + return value, nil +} + +// index returns the result of indexing its first argument by the following +// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each +// indexed item must be a map, slice, or array. +// +// Copied from Go stdlib src/text/template/funcs.go. +// Can hopefully be removed in Go 1.7, see https://github.com/golang/go/issues/14751 +func index(item interface{}, indices ...interface{}) (interface{}, error) { + v := reflect.ValueOf(item) + if !v.IsValid() { + return nil, errors.New("index of untyped nil") + } + for _, i := range indices { + index := reflect.ValueOf(i) + var isNil bool + if v, isNil = indirect(v); isNil { + return nil, errors.New("index of nil pointer") + } + switch v.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + var x int64 + switch index.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + x = index.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + x = int64(index.Uint()) + case reflect.Invalid: + return nil, errors.New("cannot index slice/array with nil") + default: + return nil, fmt.Errorf("cannot index slice/array with type %s", index.Type()) + } + if x < 0 || x >= int64(v.Len()) { + // We deviate from stdlib here. Don't return an error if the + // index is out of range. + return nil, nil + } + v = v.Index(int(x)) + case reflect.Map: + index, err := prepareArg(index, v.Type().Key()) + if err != nil { + return nil, err + } + if x := v.MapIndex(index); x.IsValid() { + v = x + } else { + v = reflect.Zero(v.Type().Elem()) + } + case reflect.Invalid: + // the loop holds invariant: v.IsValid() + panic("unreachable") + default: + return nil, fmt.Errorf("can't index item of type %s", v.Type()) + } + } + return v.Interface(), nil +} + +// readFile reads the file named by filename relative to the given basepath +// and returns the contents as a string. +// There is a upper size limit set at 1 megabytes. +func readFile(fs *afero.BasePathFs, filename string) (string, error) { + if filename == "" { + return "", errors.New("readFile needs a filename") + } + + if info, err := fs.Stat(filename); err == nil { + if info.Size() > 1000000 { + return "", fmt.Errorf("File %q is too big", filename) + } + } else { + return "", err + } + b, err := afero.ReadFile(fs, filename) + + if err != nil { + return "", err + } + + return string(b), nil +} + +// readFileFromWorkingDir reads the file named by filename relative to the +// configured WorkingDir. +// It returns the contents as a string. +// There is a upper size limit set at 1 megabytes. +func (t *templateFuncster) readFileFromWorkingDir(i interface{}) (string, error) { + s, err := cast.ToStringE(i) + if err != nil { + return "", err + } + return readFile(t.Fs.WorkingDir, s) +} + +// readDirFromWorkingDir listst the directory content relative to the +// configured WorkingDir. +func (t *templateFuncster) readDirFromWorkingDir(i interface{}) ([]os.FileInfo, error) { + path, err := cast.ToStringE(i) + if err != nil { + return nil, err + } + + list, err := afero.ReadDir(t.Fs.WorkingDir, path) + + if err != nil { + return nil, fmt.Errorf("Failed to read Directory %s with error message %s", path, err) + } + + return list, nil +} + +// safeHTMLAttr returns a given string as html/template HTMLAttr content. +func safeHTMLAttr(a interface{}) (template.HTMLAttr, error) { + s, err := cast.ToStringE(a) + return template.HTMLAttr(s), err +} + +// safeCSS returns a given string as html/template CSS content. +func safeCSS(a interface{}) (template.CSS, error) { + s, err := cast.ToStringE(a) + return template.CSS(s), err +} + +// safeURL returns a given string as html/template URL content. +func safeURL(a interface{}) (template.URL, error) { + s, err := cast.ToStringE(a) + return template.URL(s), err +} + +// safeHTML returns a given string as html/template HTML content. +func safeHTML(a interface{}) (template.HTML, error) { + s, err := cast.ToStringE(a) + return template.HTML(s), err +} + +// safeJS returns the given string as a html/template JS content. +func safeJS(a interface{}) (template.JS, error) { + s, err := cast.ToStringE(a) + return template.JS(s), err +} + +// mod returns a % b. +func mod(a, b interface{}) (int64, error) { + av := reflect.ValueOf(a) + bv := reflect.ValueOf(b) + var ai, bi int64 + + switch av.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + ai = av.Int() + default: + return 0, errors.New("Modulo operator can't be used with non integer value") + } + + switch bv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bi = bv.Int() + default: + return 0, errors.New("Modulo operator can't be used with non integer value") + } + + if bi == 0 { + return 0, errors.New("The number can't be divided by zero at modulo operation") + } + + return ai % bi, nil +} + +// modBool returns the boolean of a % b. If a % b == 0, return true. +func modBool(a, b interface{}) (bool, error) { + res, err := mod(a, b) + if err != nil { + return false, err + } + return res == int64(0), nil +} + +// base64Decode returns the base64 decoding of the given content. +func base64Decode(content interface{}) (string, error) { + conv, err := cast.ToStringE(content) + + if err != nil { + return "", err + } + + dec, err := base64.StdEncoding.DecodeString(conv) + + return string(dec), err +} + +// base64Encode returns the base64 encoding of the given content. +func base64Encode(content interface{}) (string, error) { + conv, err := cast.ToStringE(content) + + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString([]byte(conv)), nil +} + +// countWords returns the approximate word count of the given content. +func countWords(content interface{}) (int, error) { + conv, err := cast.ToStringE(content) + + if err != nil { + return 0, fmt.Errorf("Failed to convert content to string: %s", err.Error()) + } + + counter := 0 + for _, word := range strings.Fields(helpers.StripHTML(conv)) { + runeCount := utf8.RuneCountInString(word) + if len(word) == runeCount { + counter++ + } else { + counter += runeCount + } + } + + return counter, nil +} + +// countRunes returns the approximate rune count of the given content. +func countRunes(content interface{}) (int, error) { + conv, err := cast.ToStringE(content) + + if err != nil { + return 0, fmt.Errorf("Failed to convert content to string: %s", err.Error()) + } + + counter := 0 + for _, r := range helpers.StripHTML(conv) { + if !helpers.IsWhitespace(r) { + counter++ + } + } + + return counter, nil +} + +// humanize returns the humanized form of a single parameter. +// If the parameter is either an integer or a string containing an integer +// value, the behavior is to add the appropriate ordinal. +// Example: "my-first-post" -> "My first post" +// Example: "103" -> "103rd" +// Example: 52 -> "52nd" +func humanize(in interface{}) (string, error) { + word, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + if word == "" { + return "", nil + } + + _, ok := in.(int) // original param was literal int value + _, err = strconv.Atoi(word) // original param was string containing an int value + if ok || err == nil { + return inflect.Ordinalize(word), nil + } + return inflect.Humanize(word), nil +} + +// pluralize returns the plural form of a single word. +func pluralize(in interface{}) (string, error) { + word, err := cast.ToStringE(in) + if err != nil { + return "", err + } + return inflect.Pluralize(word), nil +} + +// singularize returns the singular form of a single word. +func singularize(in interface{}) (string, error) { + word, err := cast.ToStringE(in) + if err != nil { + return "", err + } + return inflect.Singularize(word), nil +} + +// md5 hashes the given input and returns its MD5 checksum +func md5(in interface{}) (string, error) { + conv, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + hash := _md5.Sum([]byte(conv)) + return hex.EncodeToString(hash[:]), nil +} + +// sha1 hashes the given input and returns its SHA1 checksum +func sha1(in interface{}) (string, error) { + conv, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + hash := _sha1.Sum([]byte(conv)) + return hex.EncodeToString(hash[:]), nil +} + +// sha256 hashes the given input and returns its SHA256 checksum +func sha256(in interface{}) (string, error) { + conv, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + hash := _sha256.Sum256([]byte(conv)) + return hex.EncodeToString(hash[:]), nil +} + +// querify encodes the given parameters “URL encoded” form ("bar=baz&foo=quux") sorted by key. +func querify(params ...interface{}) (string, error) { + qs := url.Values{} + vals, err := dictionary(params...) + if err != nil { + return "", errors.New("querify keys must be strings") + } + + for name, value := range vals { + qs.Add(name, fmt.Sprintf("%v", value)) + } + + return qs.Encode(), nil +} + +func htmlEscape(in interface{}) (string, error) { + conv, err := cast.ToStringE(in) + if err != nil { + return "", err + } + return html.EscapeString(conv), nil +} + +func htmlUnescape(in interface{}) (string, error) { + conv, err := cast.ToStringE(in) + if err != nil { + return "", err + } + return html.UnescapeString(conv), nil +} + +func (t *templateFuncster) absURL(a interface{}) (template.HTML, error) { + s, err := cast.ToStringE(a) + if err != nil { + return "", nil + } + return template.HTML(t.PathSpec.AbsURL(s, false)), nil +} + +func (t *templateFuncster) relURL(a interface{}) (template.HTML, error) { + s, err := cast.ToStringE(a) + if err != nil { + return "", nil + } + return template.HTML(t.PathSpec.RelURL(s, false)), nil +} + +// getenv retrieves the value of the environment variable named by the key. +// It returns the value, which will be empty if the variable is not present. +func getenv(key interface{}) (string, error) { + skey, err := cast.ToStringE(key) + if err != nil { + return "", nil + } + + return os.Getenv(skey), nil +} + +func (t *templateFuncster) initFuncMap() { + funcMap := template.FuncMap{ + "absURL": t.absURL, + "absLangURL": func(i interface{}) (template.HTML, error) { + s, err := cast.ToStringE(i) + if err != nil { + return "", err + } + return template.HTML(t.PathSpec.AbsURL(s, true)), nil + }, + "add": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '+') }, + "after": after, + "apply": t.apply, + "base64Decode": base64Decode, + "base64Encode": base64Encode, + "chomp": chomp, + "countrunes": countRunes, + "countwords": countWords, + "default": dfault, + "dateFormat": dateFormat, + "delimit": delimit, + "dict": dictionary, + "div": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '/') }, + "echoParam": returnWhenSet, + "emojify": emojify, + "eq": eq, + "findRE": findRE, + "first": first, + "ge": ge, + "getCSV": t.getCSV, + "getJSON": t.getJSON, + "getenv": getenv, + "gt": gt, + "hasPrefix": hasPrefix, + "highlight": t.highlight, + "htmlEscape": htmlEscape, + "htmlUnescape": htmlUnescape, + "humanize": humanize, + "imageConfig": t.imageConfig, + "in": in, + "index": index, + "int": func(v interface{}) (int, error) { return cast.ToIntE(v) }, + "intersect": intersect, + "isSet": isSet, + "isset": isSet, + "jsonify": jsonify, + "last": last, + "le": le, + "lower": lower, + "lt": lt, + "markdownify": t.markdownify, + "md5": md5, + "mod": mod, + "modBool": modBool, + "mul": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '*') }, + "ne": ne, + "now": func() time.Time { return time.Now() }, + "partial": t.Tmpl.Partial, + "partialCached": t.partialCached, + "plainify": plainify, + "pluralize": pluralize, + "querify": querify, + "readDir": t.readDirFromWorkingDir, + "readFile": t.readFileFromWorkingDir, + "ref": ref, + "relURL": t.relURL, + "relLangURL": func(i interface{}) (template.HTML, error) { + s, err := cast.ToStringE(i) + if err != nil { + return "", err + } + return template.HTML(t.PathSpec.RelURL(s, true)), nil + }, + "relref": relRef, + "replace": replace, + "replaceRE": replaceRE, + "safeCSS": safeCSS, + "safeHTML": safeHTML, + "safeHTMLAttr": safeHTMLAttr, + "safeJS": safeJS, + "safeURL": safeURL, + "sanitizeURL": helpers.SanitizeURL, + "sanitizeurl": helpers.SanitizeURL, + "seq": helpers.Seq, + "sha1": sha1, + "sha256": sha256, + "shuffle": shuffle, + "singularize": singularize, + "slice": slice, + "slicestr": slicestr, + "sort": sortSeq, + "split": split, + "string": func(v interface{}) (string, error) { return cast.ToStringE(v) }, + "sub": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '-') }, + "substr": substr, + "title": title, + "time": asTime, + "trim": trim, + "truncate": truncate, + "upper": upper, + "urlize": t.PathSpec.URLize, + "where": where, + "i18n": t.Translate, + "T": t.Translate, + } + + t.funcMap = funcMap + t.Tmpl.Funcs(funcMap) +} diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go new file mode 100644 index 00000000..0fba97bd --- /dev/null +++ b/tpl/tplimpl/template_funcs_test.go @@ -0,0 +1,2993 @@ +// Copyright 2016 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 tplimpl + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "html/template" + "image" + "image/color" + "image/png" + "math/rand" + "path" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + "time" + + "github.com/spf13/hugo/tpl" + + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/helpers" + + "io/ioutil" + "log" + "os" + + "github.com/spf13/afero" + "github.com/spf13/cast" + "github.com/spf13/hugo/config" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/hugo/i18n" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + logger = jww.NewNotepad(jww.LevelFatal, jww.LevelFatal, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) +) + +func newDepsConfig(cfg config.Provider) deps.DepsCfg { + l := helpers.NewLanguage("en", cfg) + l.Set("i18nDir", "i18n") + return deps.DepsCfg{ + Language: l, + Cfg: cfg, + Fs: hugofs.NewMem(l), + Logger: logger, + TemplateProvider: DefaultTemplateProvider, + TranslationProvider: i18n.NewTranslationProvider(), + } +} + +type tstNoStringer struct { +} + +type tstCompareType int + +const ( + tstEq tstCompareType = iota + tstNe + tstGt + tstGe + tstLt + tstLe +) + +func tstIsEq(tp tstCompareType) bool { + return tp == tstEq || tp == tstGe || tp == tstLe +} + +func tstIsGt(tp tstCompareType) bool { + return tp == tstGt || tp == tstGe +} + +func tstIsLt(tp tstCompareType) bool { + return tp == tstLt || tp == tstLe +} + +func TestFuncsInTemplate(t *testing.T) { + t.Parallel() + + workingDir := "/home/hugo" + + v := viper.New() + + v.Set("workingDir", workingDir) + v.Set("multilingual", true) + + fs := hugofs.NewMem(v) + + afero.WriteFile(fs.Source, filepath.Join(workingDir, "README.txt"), []byte("Hugo Rocks!"), 0755) + + // Add the examples from the docs: As a smoke test and to make sure the examples work. + // TODO(bep): docs: fix title example + in := + `absLangURL: {{ "index.html" | absLangURL }} +absURL: {{ "http://gohugo.io/" | absURL }} +absURL: {{ "mystyle.css" | absURL }} +absURL: {{ 42 | absURL }} +add: {{add 1 2}} +base64Decode 1: {{ "SGVsbG8gd29ybGQ=" | base64Decode }} +base64Decode 2: {{ 42 | base64Encode | base64Decode }} +base64Encode: {{ "Hello world" | base64Encode }} +chomp: {{chomp "

Blockhead

\n" }} +dateFormat: {{ dateFormat "Monday, Jan 2, 2006" "2015-01-21" }} +delimit: {{ delimit (slice "A" "B" "C") ", " " and " }} +div: {{div 6 3}} +echoParam: {{ echoParam .Params "langCode" }} +emojify: {{ "I :heart: Hugo" | emojify }} +eq: {{ if eq .Section "blog" }}current{{ end }} +findRE: {{ findRE "[G|g]o" "Hugo is a static side generator written in Go." "1" }} +hasPrefix 1: {{ hasPrefix "Hugo" "Hu" }} +hasPrefix 2: {{ hasPrefix "Hugo" "Fu" }} +htmlEscape 1: {{ htmlEscape "Cathal Garvey & The Sunshine Band " | safeHTML}} +htmlEscape 2: {{ htmlEscape "Cathal Garvey & The Sunshine Band "}} +htmlUnescape 1: {{htmlUnescape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" | safeHTML}} +htmlUnescape 2: {{"Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;" | htmlUnescape | htmlUnescape | safeHTML}} +htmlUnescape 3: {{"Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;" | htmlUnescape | htmlUnescape }} +htmlUnescape 4: {{ htmlEscape "Cathal Garvey & The Sunshine Band " | htmlUnescape | safeHTML }} +htmlUnescape 5: {{ htmlUnescape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" | htmlEscape | safeHTML }} +humanize 1: {{ humanize "my-first-post" }} +humanize 2: {{ humanize "myCamelPost" }} +humanize 3: {{ humanize "52" }} +humanize 4: {{ humanize 103 }} +in: {{ if in "this string contains a substring" "substring" }}Substring found!{{ end }} +jsonify: {{ (slice "A" "B" "C") | jsonify }} +lower: {{lower "BatMan"}} +markdownify: {{ .Title | markdownify}} +md5: {{ md5 "Hello world, gophers!" }} +mod: {{mod 15 3}} +modBool: {{modBool 15 3}} +mul: {{mul 2 3}} +plainify: {{ plainify "Hello world, gophers!" }} +pluralize: {{ "cat" | pluralize }} +querify 1: {{ (querify "foo" 1 "bar" 2 "baz" "with spaces" "qux" "this&that=those") | safeHTML }} +querify 2: Search +readDir: {{ range (readDir ".") }}{{ .Name }}{{ end }} +readFile: {{ readFile "README.txt" }} +relLangURL: {{ "index.html" | relLangURL }} +relURL 1: {{ "http://gohugo.io/" | relURL }} +relURL 2: {{ "mystyle.css" | relURL }} +relURL 3: {{ mul 2 21 | relURL }} +replace: {{ replace "Batman and Robin" "Robin" "Catwoman" }} +replaceRE: {{ "http://gohugo.io/docs" | replaceRE "^https?://([^/]+).*" "$1" }} +safeCSS: {{ "Bat&Man" | safeCSS | safeCSS }} +safeHTML: {{ "Bat&Man" | safeHTML | safeHTML }} +safeHTML: {{ "Bat&Man" | safeHTML }} +safeJS: {{ "(1*2)" | safeJS | safeJS }} +safeURL: {{ "http://gohugo.io" | safeURL | safeURL }} +seq: {{ seq 3 }} +sha1: {{ sha1 "Hello world, gophers!" }} +sha256: {{ sha256 "Hello world, gophers!" }} +singularize: {{ "cats" | singularize }} +slicestr: {{slicestr "BatMan" 0 3}} +slicestr: {{slicestr "BatMan" 3}} +sort: {{ slice "B" "C" "A" | sort }} +sub: {{sub 3 2}} +substr: {{substr "BatMan" 0 -3}} +substr: {{substr "BatMan" 3 3}} +title: {{title "Bat man"}} +time: {{ (time "2015-01-21").Year }} +trim: {{ trim "++Batman--" "+-" }} +truncate: {{ "this is a very long text" | truncate 10 " ..." }} +truncate: {{ "With [Markdown](/markdown) inside." | markdownify | truncate 14 }} +upper: {{upper "BatMan"}} +urlize: {{ "Bat Man" | urlize }} +` + + expected := `absLangURL: http://mysite.com/hugo/en/index.html +absURL: http://gohugo.io/ +absURL: http://mysite.com/hugo/mystyle.css +absURL: http://mysite.com/hugo/42 +add: 3 +base64Decode 1: Hello world +base64Decode 2: 42 +base64Encode: SGVsbG8gd29ybGQ= +chomp:

Blockhead

+dateFormat: Wednesday, Jan 21, 2015 +delimit: A, B and C +div: 2 +echoParam: en +emojify: I ❤️ Hugo +eq: current +findRE: [go] +hasPrefix 1: true +hasPrefix 2: false +htmlEscape 1: Cathal Garvey & The Sunshine Band <cathal@foo.bar> +htmlEscape 2: Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt; +htmlUnescape 1: Cathal Garvey & The Sunshine Band +htmlUnescape 2: Cathal Garvey & The Sunshine Band +htmlUnescape 3: Cathal Garvey & The Sunshine Band <cathal@foo.bar> +htmlUnescape 4: Cathal Garvey & The Sunshine Band +htmlUnescape 5: Cathal Garvey & The Sunshine Band <cathal@foo.bar> +humanize 1: My first post +humanize 2: My camel post +humanize 3: 52nd +humanize 4: 103rd +in: Substring found! +jsonify: ["A","B","C"] +lower: batman +markdownify: BatMan +md5: b3029f756f98f79e7f1b7f1d1f0dd53b +mod: 0 +modBool: true +mul: 6 +plainify: Hello world, gophers! +pluralize: cats +querify 1: bar=2&baz=with+spaces&foo=1&qux=this%26that%3Dthose +querify 2: Search +readDir: README.txt +readFile: Hugo Rocks! +relLangURL: /hugo/en/index.html +relURL 1: http://gohugo.io/ +relURL 2: /hugo/mystyle.css +relURL 3: /hugo/42 +replace: Batman and Catwoman +replaceRE: gohugo.io +safeCSS: Bat&Man +safeHTML: Bat&Man +safeHTML: Bat&Man +safeJS: (1*2) +safeURL: http://gohugo.io +seq: [1 2 3] +sha1: c8b5b0e33d408246e30f53e32b8f7627a7a649d4 +sha256: 6ec43b78da9669f50e4e422575c54bf87536954ccd58280219c393f2ce352b46 +singularize: cat +slicestr: Bat +slicestr: Man +sort: [A B C] +sub: 1 +substr: Bat +substr: Man +title: Bat Man +time: 2015 +trim: Batman +truncate: this is a ... +truncate: With Markdown … +upper: BATMAN +urlize: bat-man +` + + var b bytes.Buffer + + var data struct { + Title string + Section string + Params map[string]interface{} + } + + data.Title = "**BatMan**" + data.Section = "blog" + data.Params = map[string]interface{}{"langCode": "en"} + + v.Set("baseURL", "http://mysite.com/hugo/") + v.Set("CurrentContentLanguage", helpers.NewLanguage("en", v)) + + config := newDepsConfig(v) + config.WithTemplate = func(templ tpl.Template) error { + if _, err := templ.New("test").Parse(in); err != nil { + t.Fatal("Got error on parse", err) + } + return nil + } + config.Fs = fs + + d := deps.New(config) + if err := d.LoadResources(); err != nil { + t.Fatal(err) + } + + err := d.Tmpl.Lookup("test").Execute(&b, &data) + + if err != nil { + t.Fatal("Got error on execute", err) + } + + if b.String() != expected { + sl1 := strings.Split(b.String(), "\n") + sl2 := strings.Split(expected, "\n") + t.Errorf("Diff:\n%q", helpers.DiffStringSlices(sl1, sl2)) + } +} + +func TestCompare(t *testing.T) { + t.Parallel() + for _, this := range []struct { + tstCompareType + funcUnderTest func(a, b interface{}) bool + }{ + {tstGt, gt}, + {tstLt, lt}, + {tstGe, ge}, + {tstLe, le}, + {tstEq, eq}, + {tstNe, ne}, + } { + doTestCompare(t, this.tstCompareType, this.funcUnderTest) + } +} + +func doTestCompare(t *testing.T, tp tstCompareType, funcUnderTest func(a, b interface{}) bool) { + for i, this := range []struct { + left interface{} + right interface{} + expectIndicator int + }{ + {5, 8, -1}, + {8, 5, 1}, + {5, 5, 0}, + {int(5), int64(5), 0}, + {int32(5), int(5), 0}, + {int16(4), int(5), -1}, + {uint(15), uint64(15), 0}, + {-2, 1, -1}, + {2, -5, 1}, + {0.0, 1.23, -1}, + {1.1, 1.1, 0}, + {float32(1.0), float64(1.0), 0}, + {1.23, 0.0, 1}, + {"5", "5", 0}, + {"8", "5", 1}, + {"5", "0001", 1}, + {[]int{100, 99}, []int{1, 2, 3, 4}, -1}, + {cast.ToTime("2015-11-20"), cast.ToTime("2015-11-20"), 0}, + {cast.ToTime("2015-11-19"), cast.ToTime("2015-11-20"), -1}, + {cast.ToTime("2015-11-20"), cast.ToTime("2015-11-19"), 1}, + } { + result := funcUnderTest(this.left, this.right) + success := false + + if this.expectIndicator == 0 { + if tstIsEq(tp) { + success = result + } else { + success = !result + } + } + + if this.expectIndicator < 0 { + success = result && (tstIsLt(tp) || tp == tstNe) + success = success || (!result && !tstIsLt(tp)) + } + + if this.expectIndicator > 0 { + success = result && (tstIsGt(tp) || tp == tstNe) + success = success || (!result && (!tstIsGt(tp) || tp != tstNe)) + } + + if !success { + t.Errorf("[%d][%s] %v compared to %v: %t", i, path.Base(runtime.FuncForPC(reflect.ValueOf(funcUnderTest).Pointer()).Name()), this.left, this.right, result) + } + } +} + +func TestMod(t *testing.T) { + t.Parallel() + for i, this := range []struct { + a interface{} + b interface{} + expect interface{} + }{ + {3, 2, int64(1)}, + {3, 1, int64(0)}, + {3, 0, false}, + {0, 3, int64(0)}, + {3.1, 2, false}, + {3, 2.1, false}, + {3.1, 2.1, false}, + {int8(3), int8(2), int64(1)}, + {int16(3), int16(2), int64(1)}, + {int32(3), int32(2), int64(1)}, + {int64(3), int64(2), int64(1)}, + } { + result, err := mod(this.a, this.b) + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] modulo didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(result, this.expect) { + t.Errorf("[%d] modulo got %v but expected %v", i, result, this.expect) + } + } + } +} + +func TestModBool(t *testing.T) { + t.Parallel() + for i, this := range []struct { + a interface{} + b interface{} + expect interface{} + }{ + {3, 3, true}, + {3, 2, false}, + {3, 1, true}, + {3, 0, nil}, + {0, 3, true}, + {3.1, 2, nil}, + {3, 2.1, nil}, + {3.1, 2.1, nil}, + {int8(3), int8(3), true}, + {int8(3), int8(2), false}, + {int16(3), int16(3), true}, + {int16(3), int16(2), false}, + {int32(3), int32(3), true}, + {int32(3), int32(2), false}, + {int64(3), int64(3), true}, + {int64(3), int64(2), false}, + } { + result, err := modBool(this.a, this.b) + if this.expect == nil { + if err == nil { + t.Errorf("[%d] modulo didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(result, this.expect) { + t.Errorf("[%d] modulo got %v but expected %v", i, result, this.expect) + } + } + } +} + +func TestFirst(t *testing.T) { + t.Parallel() + for i, this := range []struct { + count interface{} + sequence interface{} + expect interface{} + }{ + {int(2), []string{"a", "b", "c"}, []string{"a", "b"}}, + {int32(3), []string{"a", "b"}, []string{"a", "b"}}, + {int64(2), []int{100, 200, 300}, []int{100, 200}}, + {100, []int{100, 200}, []int{100, 200}}, + {"1", []int{100, 200, 300}, []int{100}}, + {int64(-1), []int{100, 200, 300}, false}, + {"noint", []int{100, 200, 300}, false}, + {1, nil, false}, + {nil, []int{100}, false}, + {1, t, false}, + {1, (*string)(nil), false}, + } { + results, err := first(this.count, this.sequence) + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] First didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(results, this.expect) { + t.Errorf("[%d] First %d items, got %v but expected %v", i, this.count, results, this.expect) + } + } + } +} + +func TestLast(t *testing.T) { + t.Parallel() + for i, this := range []struct { + count interface{} + sequence interface{} + expect interface{} + }{ + {int(2), []string{"a", "b", "c"}, []string{"b", "c"}}, + {int32(3), []string{"a", "b"}, []string{"a", "b"}}, + {int64(2), []int{100, 200, 300}, []int{200, 300}}, + {100, []int{100, 200}, []int{100, 200}}, + {"1", []int{100, 200, 300}, []int{300}}, + {int64(-1), []int{100, 200, 300}, false}, + {"noint", []int{100, 200, 300}, false}, + {1, nil, false}, + {nil, []int{100}, false}, + {1, t, false}, + {1, (*string)(nil), false}, + } { + results, err := last(this.count, this.sequence) + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] First didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(results, this.expect) { + t.Errorf("[%d] First %d items, got %v but expected %v", i, this.count, results, this.expect) + } + } + } +} + +func TestAfter(t *testing.T) { + t.Parallel() + for i, this := range []struct { + count interface{} + sequence interface{} + expect interface{} + }{ + {int(2), []string{"a", "b", "c", "d"}, []string{"c", "d"}}, + {int32(3), []string{"a", "b"}, false}, + {int64(2), []int{100, 200, 300}, []int{300}}, + {100, []int{100, 200}, false}, + {"1", []int{100, 200, 300}, []int{200, 300}}, + {int64(-1), []int{100, 200, 300}, false}, + {"noint", []int{100, 200, 300}, false}, + {1, nil, false}, + {nil, []int{100}, false}, + {1, t, false}, + {1, (*string)(nil), false}, + } { + results, err := after(this.count, this.sequence) + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] First didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(results, this.expect) { + t.Errorf("[%d] First %d items, got %v but expected %v", i, this.count, results, this.expect) + } + } + } +} + +func TestShuffleInputAndOutputFormat(t *testing.T) { + t.Parallel() + for i, this := range []struct { + sequence interface{} + success bool + }{ + {[]string{"a", "b", "c", "d"}, true}, + {[]int{100, 200, 300}, true}, + {[]int{100, 200, 300}, true}, + {[]int{100, 200}, true}, + {[]string{"a", "b"}, true}, + {[]int{100, 200, 300}, true}, + {[]int{100, 200, 300}, true}, + {[]int{100}, true}, + {nil, false}, + {t, false}, + {(*string)(nil), false}, + } { + results, err := shuffle(this.sequence) + if !this.success { + if err == nil { + t.Errorf("[%d] First didn't return an expected error", i) + } + } else { + resultsv := reflect.ValueOf(results) + sequencev := reflect.ValueOf(this.sequence) + + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + + if resultsv.Len() != sequencev.Len() { + t.Errorf("Expected %d items, got %d items", sequencev.Len(), resultsv.Len()) + } + } + } +} + +func TestShuffleRandomising(t *testing.T) { + t.Parallel() + // Note that this test can fail with false negative result if the shuffle + // of the sequence happens to be the same as the original sequence. However + // the propability of the event is 10^-158 which is negligible. + sequenceLength := 100 + rand.Seed(time.Now().UTC().UnixNano()) + + for _, this := range []struct { + sequence []int + }{ + {rand.Perm(sequenceLength)}, + } { + results, _ := shuffle(this.sequence) + + resultsv := reflect.ValueOf(results) + + allSame := true + for index, value := range this.sequence { + allSame = allSame && (resultsv.Index(index).Interface() == value) + } + + if allSame { + t.Error("Expected sequence to be shuffled but was in the same order") + } + } +} + +func TestDictionary(t *testing.T) { + t.Parallel() + for i, this := range []struct { + v1 []interface{} + expecterr bool + expectedValue map[string]interface{} + }{ + {[]interface{}{"a", "b"}, false, map[string]interface{}{"a": "b"}}, + {[]interface{}{5, "b"}, true, nil}, + {[]interface{}{"a", 12, "b", []int{4}}, false, map[string]interface{}{"a": 12, "b": []int{4}}}, + {[]interface{}{"a", "b", "c"}, true, nil}, + } { + r, e := dictionary(this.v1...) + + if (this.expecterr && e == nil) || (!this.expecterr && e != nil) { + t.Errorf("[%d] got an unexpected error: %s", i, e) + } else if !this.expecterr { + if !reflect.DeepEqual(r, this.expectedValue) { + t.Errorf("[%d] got %v but expected %v", i, r, this.expectedValue) + } + } + } +} + +func blankImage(width, height int) []byte { + var buf bytes.Buffer + img := image.NewRGBA(image.Rect(0, 0, width, height)) + if err := png.Encode(&buf, img); err != nil { + panic(err) + } + return buf.Bytes() +} + +func TestImageConfig(t *testing.T) { + t.Parallel() + + workingDir := "/home/hugo" + + v := viper.New() + + v.Set("workingDir", workingDir) + + f := newTestFuncsterWithViper(v) + + for i, this := range []struct { + resetCache bool + path string + input []byte + expected image.Config + }{ + // Make sure that the cache is initialized by default. + { + resetCache: false, + path: "a.png", + input: blankImage(10, 10), + expected: image.Config{ + Width: 10, + Height: 10, + ColorModel: color.NRGBAModel, + }, + }, + { + resetCache: true, + path: "a.png", + input: blankImage(10, 10), + expected: image.Config{ + Width: 10, + Height: 10, + ColorModel: color.NRGBAModel, + }, + }, + { + resetCache: false, + path: "b.png", + input: blankImage(20, 15), + expected: image.Config{ + Width: 20, + Height: 15, + ColorModel: color.NRGBAModel, + }, + }, + { + resetCache: false, + path: "a.png", + input: blankImage(20, 15), + expected: image.Config{ + Width: 10, + Height: 10, + ColorModel: color.NRGBAModel, + }, + }, + { + resetCache: true, + path: "a.png", + input: blankImage(20, 15), + expected: image.Config{ + Width: 20, + Height: 15, + ColorModel: color.NRGBAModel, + }, + }, + } { + afero.WriteFile(f.Fs.Source, filepath.Join(workingDir, this.path), this.input, 0755) + + if this.resetCache { + resetImageConfigCache() + } + + result, err := f.imageConfig(this.path) + if err != nil { + t.Errorf("imageConfig returned error: %s", err) + } + + if !reflect.DeepEqual(result, this.expected) { + t.Errorf("[%d] imageConfig: expected '%v', got '%v'", i, this.expected, result) + } + + if len(defaultImageConfigCache.config) == 0 { + t.Error("defaultImageConfigCache should have at least 1 item") + } + } + + if _, err := f.imageConfig(t); err == nil { + t.Error("Expected error from imageConfig when passed invalid path") + } + + if _, err := f.imageConfig("non-existent.png"); err == nil { + t.Error("Expected error from imageConfig when passed non-existent file") + } + + if _, err := f.imageConfig(""); err == nil { + t.Error("Expected error from imageConfig when passed empty path") + } + + // test cache clearing + ResetCaches() + + if len(defaultImageConfigCache.config) != 0 { + t.Error("ResetCaches should have cleared defaultImageConfigCache") + } +} + +func TestIn(t *testing.T) { + t.Parallel() + for i, this := range []struct { + v1 interface{} + v2 interface{} + expect bool + }{ + {[]string{"a", "b", "c"}, "b", true}, + {[]interface{}{"a", "b", "c"}, "b", true}, + {[]interface{}{"a", "b", "c"}, "d", false}, + {[]string{"a", "b", "c"}, "d", false}, + {[]string{"a", "12", "c"}, 12, false}, + {[]int{1, 2, 4}, 2, true}, + {[]interface{}{1, 2, 4}, 2, true}, + {[]interface{}{1, 2, 4}, nil, false}, + {[]interface{}{nil}, nil, false}, + {[]int{1, 2, 4}, 3, false}, + {[]float64{1.23, 2.45, 4.67}, 1.23, true}, + {[]float64{1.234567, 2.45, 4.67}, 1.234568, false}, + {"this substring should be found", "substring", true}, + {"this substring should not be found", "subseastring", false}, + } { + result := in(this.v1, this.v2) + + if result != this.expect { + t.Errorf("[%d] got %v but expected %v", i, result, this.expect) + } + } +} + +func TestSlicestr(t *testing.T) { + t.Parallel() + var err error + for i, this := range []struct { + v1 interface{} + v2 interface{} + v3 interface{} + expect interface{} + }{ + {"abc", 1, 2, "b"}, + {"abc", 1, 3, "bc"}, + {"abcdef", 1, int8(3), "bc"}, + {"abcdef", 1, int16(3), "bc"}, + {"abcdef", 1, int32(3), "bc"}, + {"abcdef", 1, int64(3), "bc"}, + {"abc", 0, 1, "a"}, + {"abcdef", nil, nil, "abcdef"}, + {"abcdef", 0, 6, "abcdef"}, + {"abcdef", 0, 2, "ab"}, + {"abcdef", 2, nil, "cdef"}, + {"abcdef", int8(2), nil, "cdef"}, + {"abcdef", int16(2), nil, "cdef"}, + {"abcdef", int32(2), nil, "cdef"}, + {"abcdef", int64(2), nil, "cdef"}, + {123, 1, 3, "23"}, + {"abcdef", 6, nil, false}, + {"abcdef", 4, 7, false}, + {"abcdef", -1, nil, false}, + {"abcdef", -1, 7, false}, + {"abcdef", 1, -1, false}, + {tstNoStringer{}, 0, 1, false}, + {"ĀĀĀ", 0, 1, "Ā"}, // issue #1333 + {"a", t, nil, false}, + {"a", 1, t, false}, + } { + var result string + if this.v2 == nil { + result, err = slicestr(this.v1) + } else if this.v3 == nil { + result, err = slicestr(this.v1, this.v2) + } else { + result, err = slicestr(this.v1, this.v2, this.v3) + } + + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] Slice didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(result, this.expect) { + t.Errorf("[%d] got %s but expected %s", i, result, this.expect) + } + } + } + + // Too many arguments + _, err = slicestr("a", 1, 2, 3) + if err == nil { + t.Errorf("Should have errored") + } +} + +func TestHasPrefix(t *testing.T) { + t.Parallel() + cases := []struct { + s interface{} + prefix interface{} + want interface{} + isErr bool + }{ + {"abcd", "ab", true, false}, + {"abcd", "cd", false, false}, + {template.HTML("abcd"), "ab", true, false}, + {template.HTML("abcd"), "cd", false, false}, + {template.HTML("1234"), 12, true, false}, + {template.HTML("1234"), 34, false, false}, + {[]byte("abcd"), "ab", true, false}, + } + + for i, c := range cases { + res, err := hasPrefix(c.s, c.prefix) + if (err != nil) != c.isErr { + t.Fatalf("[%d] unexpected isErr state: want %v, got %v, err = %v", i, c.isErr, err != nil, err) + } + if res != c.want { + t.Errorf("[%d] want %v, got %v", i, c.want, res) + } + } +} + +func TestSubstr(t *testing.T) { + t.Parallel() + var err error + var n int + for i, this := range []struct { + v1 interface{} + v2 interface{} + v3 interface{} + expect interface{} + }{ + {"abc", 1, 2, "bc"}, + {"abc", 0, 1, "a"}, + {"abcdef", -1, 2, "ef"}, + {"abcdef", -3, 3, "bcd"}, + {"abcdef", 0, -1, "abcde"}, + {"abcdef", 2, -1, "cde"}, + {"abcdef", 4, -4, false}, + {"abcdef", 7, 1, false}, + {"abcdef", 1, 100, "bcdef"}, + {"abcdef", -100, 3, "abc"}, + {"abcdef", -3, -1, "de"}, + {"abcdef", 2, nil, "cdef"}, + {"abcdef", int8(2), nil, "cdef"}, + {"abcdef", int16(2), nil, "cdef"}, + {"abcdef", int32(2), nil, "cdef"}, + {"abcdef", int64(2), nil, "cdef"}, + {"abcdef", 2, int8(3), "cde"}, + {"abcdef", 2, int16(3), "cde"}, + {"abcdef", 2, int32(3), "cde"}, + {"abcdef", 2, int64(3), "cde"}, + {123, 1, 3, "23"}, + {1.2e3, 0, 4, "1200"}, + {tstNoStringer{}, 0, 1, false}, + {"abcdef", 2.0, nil, "cdef"}, + {"abcdef", 2.0, 2, "cd"}, + {"abcdef", 2, 2.0, "cd"}, + {"ĀĀĀ", 1, 2, "ĀĀ"}, // # issue 1333 + {"abcdef", "doo", nil, false}, + {"abcdef", "doo", "doo", false}, + {"abcdef", 1, "doo", false}, + } { + var result string + n = i + + if this.v3 == nil { + result, err = substr(this.v1, this.v2) + } else { + result, err = substr(this.v1, this.v2, this.v3) + } + + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] Substr didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(result, this.expect) { + t.Errorf("[%d] got %s but expected %s", i, result, this.expect) + } + } + } + + n++ + _, err = substr("abcdef") + if err == nil { + t.Errorf("[%d] Substr didn't return an expected error", n) + } + + n++ + _, err = substr("abcdef", 1, 2, 3) + if err == nil { + t.Errorf("[%d] Substr didn't return an expected error", n) + } +} + +func TestSplit(t *testing.T) { + t.Parallel() + for i, this := range []struct { + v1 interface{} + v2 string + expect interface{} + }{ + {"a, b", ", ", []string{"a", "b"}}, + {"a & b & c", " & ", []string{"a", "b", "c"}}, + {"http://example.com", "http://", []string{"", "example.com"}}, + {123, "2", []string{"1", "3"}}, + {tstNoStringer{}, ",", false}, + } { + result, err := split(this.v1, this.v2) + + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] Split didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(result, this.expect) { + t.Errorf("[%d] got %s but expected %s", i, result, this.expect) + } + } + } +} + +func TestIntersect(t *testing.T) { + t.Parallel() + for i, this := range []struct { + sequence1 interface{} + sequence2 interface{} + expect interface{} + }{ + {[]string{"a", "b", "c", "c"}, []string{"a", "b", "b"}, []string{"a", "b"}}, + {[]string{"a", "b"}, []string{"a", "b", "c"}, []string{"a", "b"}}, + {[]string{"a", "b", "c"}, []string{"d", "e"}, []string{}}, + {[]string{}, []string{}, []string{}}, + {[]string{"a", "b"}, nil, make([]interface{}, 0)}, + {nil, []string{"a", "b"}, make([]interface{}, 0)}, + {nil, nil, make([]interface{}, 0)}, + {[]string{"1", "2"}, []int{1, 2}, []string{}}, + {[]int{1, 2}, []string{"1", "2"}, []int{}}, + {[]int{1, 2, 4}, []int{2, 4}, []int{2, 4}}, + {[]int{2, 4}, []int{1, 2, 4}, []int{2, 4}}, + {[]int{1, 2, 4}, []int{3, 6}, []int{}}, + {[]float64{2.2, 4.4}, []float64{1.1, 2.2, 4.4}, []float64{2.2, 4.4}}, + } { + results, err := intersect(this.sequence1, this.sequence2) + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(results, this.expect) { + t.Errorf("[%d] got %v but expected %v", i, results, this.expect) + } + } + + _, err1 := intersect("not an array or slice", []string{"a"}) + + if err1 == nil { + t.Error("Expected error for non array as first arg") + } + + _, err2 := intersect([]string{"a"}, "not an array or slice") + + if err2 == nil { + t.Error("Expected error for non array as second arg") + } +} + +func TestIsSet(t *testing.T) { + t.Parallel() + aSlice := []interface{}{1, 2, 3, 5} + aMap := map[string]interface{}{"a": 1, "b": 2} + + assert.True(t, isSet(aSlice, 2)) + assert.True(t, isSet(aMap, "b")) + assert.False(t, isSet(aSlice, 22)) + assert.False(t, isSet(aMap, "bc")) +} + +func (x *TstX) TstRp() string { + return "r" + x.A +} + +func (x TstX) TstRv() string { + return "r" + x.B +} + +func (x TstX) unexportedMethod() string { + return x.unexported +} + +func (x TstX) MethodWithArg(s string) string { + return s +} + +func (x TstX) MethodReturnNothing() {} + +func (x TstX) MethodReturnErrorOnly() error { + return errors.New("some error occurred") +} + +func (x TstX) MethodReturnTwoValues() (string, string) { + return "foo", "bar" +} + +func (x TstX) MethodReturnValueWithError() (string, error) { + return "", errors.New("some error occurred") +} + +func (x TstX) String() string { + return fmt.Sprintf("A: %s, B: %s", x.A, x.B) +} + +type TstX struct { + A, B string + unexported string +} + +func TestTimeUnix(t *testing.T) { + t.Parallel() + var sec int64 = 1234567890 + tv := reflect.ValueOf(time.Unix(sec, 0)) + i := 1 + + res := toTimeUnix(tv) + if sec != res { + t.Errorf("[%d] timeUnix got %v but expected %v", i, res, sec) + } + + i++ + func(t *testing.T) { + defer func() { + if err := recover(); err == nil { + t.Errorf("[%d] timeUnix didn't return an expected error", i) + } + }() + iv := reflect.ValueOf(sec) + toTimeUnix(iv) + }(t) +} + +func TestEvaluateSubElem(t *testing.T) { + t.Parallel() + tstx := TstX{A: "foo", B: "bar"} + var inner struct { + S fmt.Stringer + } + inner.S = tstx + interfaceValue := reflect.ValueOf(&inner).Elem().Field(0) + + for i, this := range []struct { + value reflect.Value + key string + expect interface{} + }{ + {reflect.ValueOf(tstx), "A", "foo"}, + {reflect.ValueOf(&tstx), "TstRp", "rfoo"}, + {reflect.ValueOf(tstx), "TstRv", "rbar"}, + //{reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), 1, "foo"}, + {reflect.ValueOf(map[string]string{"key1": "foo", "key2": "bar"}), "key1", "foo"}, + {interfaceValue, "String", "A: foo, B: bar"}, + {reflect.Value{}, "foo", false}, + //{reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), 1.2, false}, + {reflect.ValueOf(tstx), "unexported", false}, + {reflect.ValueOf(tstx), "unexportedMethod", false}, + {reflect.ValueOf(tstx), "MethodWithArg", false}, + {reflect.ValueOf(tstx), "MethodReturnNothing", false}, + {reflect.ValueOf(tstx), "MethodReturnErrorOnly", false}, + {reflect.ValueOf(tstx), "MethodReturnTwoValues", false}, + {reflect.ValueOf(tstx), "MethodReturnValueWithError", false}, + {reflect.ValueOf((*TstX)(nil)), "A", false}, + {reflect.ValueOf(tstx), "C", false}, + {reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), "1", false}, + {reflect.ValueOf([]string{"foo", "bar"}), "1", false}, + } { + result, err := evaluateSubElem(this.value, this.key) + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] evaluateSubElem didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if result.Kind() != reflect.String || result.String() != this.expect { + t.Errorf("[%d] evaluateSubElem with %v got %v but expected %v", i, this.key, result, this.expect) + } + } + } +} + +func TestCheckCondition(t *testing.T) { + t.Parallel() + type expect struct { + result bool + isError bool + } + + for i, this := range []struct { + value reflect.Value + match reflect.Value + op string + expect + }{ + {reflect.ValueOf(123), reflect.ValueOf(123), "", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("foo"), "", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + "", + expect{true, false}, + }, + {reflect.ValueOf(true), reflect.ValueOf(true), "", expect{true, false}}, + {reflect.ValueOf(nil), reflect.ValueOf(nil), "", expect{true, false}}, + {reflect.ValueOf(123), reflect.ValueOf(456), "!=", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar"), "!=", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + "!=", + expect{true, false}, + }, + {reflect.ValueOf(true), reflect.ValueOf(false), "!=", expect{true, false}}, + {reflect.ValueOf(123), reflect.ValueOf(nil), "!=", expect{true, false}}, + {reflect.ValueOf(456), reflect.ValueOf(123), ">=", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar"), ">=", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + ">=", + expect{true, false}, + }, + {reflect.ValueOf(456), reflect.ValueOf(123), ">", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar"), ">", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + ">", + expect{true, false}, + }, + {reflect.ValueOf(123), reflect.ValueOf(456), "<=", expect{true, false}}, + {reflect.ValueOf("bar"), reflect.ValueOf("foo"), "<=", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + "<=", + expect{true, false}, + }, + {reflect.ValueOf(123), reflect.ValueOf(456), "<", expect{true, false}}, + {reflect.ValueOf("bar"), reflect.ValueOf("foo"), "<", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + "<", + expect{true, false}, + }, + {reflect.ValueOf(123), reflect.ValueOf([]int{123, 45, 678}), "in", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf([]string{"foo", "bar", "baz"}), "in", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf([]time.Time{ + time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC), + time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC), + time.Date(2015, time.June, 26, 19, 18, 56, 12345, time.UTC), + }), + "in", + expect{true, false}, + }, + {reflect.ValueOf(123), reflect.ValueOf([]int{45, 678}), "not in", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf([]string{"bar", "baz"}), "not in", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf([]time.Time{ + time.Date(2015, time.February, 26, 19, 18, 56, 12345, time.UTC), + time.Date(2015, time.March, 26, 19, 18, 56, 12345, time.UTC), + time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC), + }), + "not in", + expect{true, false}, + }, + {reflect.ValueOf("foo"), reflect.ValueOf("bar-foo-baz"), "in", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar--baz"), "not in", expect{true, false}}, + {reflect.Value{}, reflect.ValueOf("foo"), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.Value{}, "", expect{false, false}}, + {reflect.ValueOf((*TstX)(nil)), reflect.ValueOf("foo"), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf((*TstX)(nil)), "", expect{false, false}}, + {reflect.ValueOf(true), reflect.ValueOf("foo"), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf(true), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf(map[int]string{}), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf([]int{1, 2}), "", expect{false, false}}, + {reflect.ValueOf((*TstX)(nil)), reflect.ValueOf((*TstX)(nil)), ">", expect{false, false}}, + {reflect.ValueOf(true), reflect.ValueOf(false), ">", expect{false, false}}, + {reflect.ValueOf(123), reflect.ValueOf([]int{}), "in", expect{false, false}}, + {reflect.ValueOf(123), reflect.ValueOf(123), "op", expect{false, true}}, + } { + result, err := checkCondition(this.value, this.match, this.op) + if this.expect.isError { + if err == nil { + t.Errorf("[%d] checkCondition didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if result != this.expect.result { + t.Errorf("[%d] check condition %v %s %v, got %v but expected %v", i, this.value, this.op, this.match, result, this.expect.result) + } + } + } +} + +func TestWhere(t *testing.T) { + t.Parallel() + + type Mid struct { + Tst TstX + } + + d1 := time.Now() + d2 := d1.Add(1 * time.Hour) + d3 := d2.Add(1 * time.Hour) + d4 := d3.Add(1 * time.Hour) + d5 := d4.Add(1 * time.Hour) + d6 := d5.Add(1 * time.Hour) + + for i, this := range []struct { + sequence interface{} + key interface{} + op string + match interface{} + expect interface{} + }{ + { + sequence: []map[int]string{ + {1: "a", 2: "m"}, {1: "c", 2: "d"}, {1: "e", 3: "m"}, + }, + key: 2, match: "m", + expect: []map[int]string{ + {1: "a", 2: "m"}, + }, + }, + { + sequence: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "x": 4}, + }, + key: "b", match: 4, + expect: []map[string]int{ + {"a": 3, "b": 4}, + }, + }, + { + sequence: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", match: "f", + expect: []TstX{ + {A: "e", B: "f"}, + }, + }, + { + sequence: []*map[int]string{ + {1: "a", 2: "m"}, {1: "c", 2: "d"}, {1: "e", 3: "m"}, + }, + key: 2, match: "m", + expect: []*map[int]string{ + {1: "a", 2: "m"}, + }, + }, + { + sequence: []*TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", match: "f", + expect: []*TstX{ + {A: "e", B: "f"}, + }, + }, + { + sequence: []*TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "c"}, + }, + key: "TstRp", match: "rc", + expect: []*TstX{ + {A: "c", B: "d"}, + }, + }, + { + sequence: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "c"}, + }, + key: "TstRv", match: "rc", + expect: []TstX{ + {A: "e", B: "c"}, + }, + }, + { + sequence: []map[string]TstX{ + {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, + }, + key: "foo.B", match: "d", + expect: []map[string]TstX{ + {"foo": TstX{A: "c", B: "d"}}, + }, + }, + { + sequence: []map[string]TstX{ + {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, + }, + key: ".foo.B", match: "d", + expect: []map[string]TstX{ + {"foo": TstX{A: "c", B: "d"}}, + }, + }, + { + sequence: []map[string]TstX{ + {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, + }, + key: "foo.TstRv", match: "rd", + expect: []map[string]TstX{ + {"foo": TstX{A: "c", B: "d"}}, + }, + }, + { + sequence: []map[string]*TstX{ + {"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}, + }, + key: "foo.TstRp", match: "rc", + expect: []map[string]*TstX{ + {"foo": &TstX{A: "c", B: "d"}}, + }, + }, + { + sequence: []map[string]Mid{ + {"foo": Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": Mid{Tst: TstX{A: "e", B: "f"}}}, + }, + key: "foo.Tst.B", match: "d", + expect: []map[string]Mid{ + {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, + }, + }, + { + sequence: []map[string]Mid{ + {"foo": Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": Mid{Tst: TstX{A: "e", B: "f"}}}, + }, + key: "foo.Tst.TstRv", match: "rd", + expect: []map[string]Mid{ + {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, + }, + }, + { + sequence: []map[string]*Mid{ + {"foo": &Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": &Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": &Mid{Tst: TstX{A: "e", B: "f"}}}, + }, + key: "foo.Tst.TstRp", match: "rc", + expect: []map[string]*Mid{ + {"foo": &Mid{Tst: TstX{A: "c", B: "d"}}}, + }, + }, + { + sequence: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + key: "b", op: ">", match: 3, + expect: []map[string]int{ + {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + }, + { + sequence: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "!=", match: "f", + expect: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, + }, + }, + { + sequence: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + key: "b", op: "in", match: []int{3, 4, 5}, + expect: []map[string]int{ + {"a": 3, "b": 4}, + }, + }, + { + sequence: []map[string][]string{ + {"a": []string{"A", "B", "C"}, "b": []string{"D", "E", "F"}}, {"a": []string{"G", "H", "I"}, "b": []string{"J", "K", "L"}}, {"a": []string{"M", "N", "O"}, "b": []string{"P", "Q", "R"}}, + }, + key: "b", op: "intersect", match: []string{"D", "P", "Q"}, + expect: []map[string][]string{ + {"a": []string{"A", "B", "C"}, "b": []string{"D", "E", "F"}}, {"a": []string{"M", "N", "O"}, "b": []string{"P", "Q", "R"}}, + }, + }, + { + sequence: []map[string][]int{ + {"a": []int{1, 2, 3}, "b": []int{4, 5, 6}}, {"a": []int{7, 8, 9}, "b": []int{10, 11, 12}}, {"a": []int{13, 14, 15}, "b": []int{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int{4, 10, 12}, + expect: []map[string][]int{ + {"a": []int{1, 2, 3}, "b": []int{4, 5, 6}}, {"a": []int{7, 8, 9}, "b": []int{10, 11, 12}}, + }, + }, + { + sequence: []map[string][]int8{ + {"a": []int8{1, 2, 3}, "b": []int8{4, 5, 6}}, {"a": []int8{7, 8, 9}, "b": []int8{10, 11, 12}}, {"a": []int8{13, 14, 15}, "b": []int8{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int8{4, 10, 12}, + expect: []map[string][]int8{ + {"a": []int8{1, 2, 3}, "b": []int8{4, 5, 6}}, {"a": []int8{7, 8, 9}, "b": []int8{10, 11, 12}}, + }, + }, + { + sequence: []map[string][]int16{ + {"a": []int16{1, 2, 3}, "b": []int16{4, 5, 6}}, {"a": []int16{7, 8, 9}, "b": []int16{10, 11, 12}}, {"a": []int16{13, 14, 15}, "b": []int16{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int16{4, 10, 12}, + expect: []map[string][]int16{ + {"a": []int16{1, 2, 3}, "b": []int16{4, 5, 6}}, {"a": []int16{7, 8, 9}, "b": []int16{10, 11, 12}}, + }, + }, + { + sequence: []map[string][]int32{ + {"a": []int32{1, 2, 3}, "b": []int32{4, 5, 6}}, {"a": []int32{7, 8, 9}, "b": []int32{10, 11, 12}}, {"a": []int32{13, 14, 15}, "b": []int32{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int32{4, 10, 12}, + expect: []map[string][]int32{ + {"a": []int32{1, 2, 3}, "b": []int32{4, 5, 6}}, {"a": []int32{7, 8, 9}, "b": []int32{10, 11, 12}}, + }, + }, + { + sequence: []map[string][]int64{ + {"a": []int64{1, 2, 3}, "b": []int64{4, 5, 6}}, {"a": []int64{7, 8, 9}, "b": []int64{10, 11, 12}}, {"a": []int64{13, 14, 15}, "b": []int64{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int64{4, 10, 12}, + expect: []map[string][]int64{ + {"a": []int64{1, 2, 3}, "b": []int64{4, 5, 6}}, {"a": []int64{7, 8, 9}, "b": []int64{10, 11, 12}}, + }, + }, + { + sequence: []map[string][]float32{ + {"a": []float32{1.0, 2.0, 3.0}, "b": []float32{4.0, 5.0, 6.0}}, {"a": []float32{7.0, 8.0, 9.0}, "b": []float32{10.0, 11.0, 12.0}}, {"a": []float32{13.0, 14.0, 15.0}, "b": []float32{16.0, 17.0, 18.0}}, + }, + key: "b", op: "intersect", match: []float32{4, 10, 12}, + expect: []map[string][]float32{ + {"a": []float32{1.0, 2.0, 3.0}, "b": []float32{4.0, 5.0, 6.0}}, {"a": []float32{7.0, 8.0, 9.0}, "b": []float32{10.0, 11.0, 12.0}}, + }, + }, + { + sequence: []map[string][]float64{ + {"a": []float64{1.0, 2.0, 3.0}, "b": []float64{4.0, 5.0, 6.0}}, {"a": []float64{7.0, 8.0, 9.0}, "b": []float64{10.0, 11.0, 12.0}}, {"a": []float64{13.0, 14.0, 15.0}, "b": []float64{16.0, 17.0, 18.0}}, + }, + key: "b", op: "intersect", match: []float64{4, 10, 12}, + expect: []map[string][]float64{ + {"a": []float64{1.0, 2.0, 3.0}, "b": []float64{4.0, 5.0, 6.0}}, {"a": []float64{7.0, 8.0, 9.0}, "b": []float64{10.0, 11.0, 12.0}}, + }, + }, + { + sequence: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + key: "b", op: "in", match: slice(3, 4, 5), + expect: []map[string]int{ + {"a": 3, "b": 4}, + }, + }, + { + sequence: []map[string]time.Time{ + {"a": d1, "b": d2}, {"a": d3, "b": d4}, {"a": d5, "b": d6}, + }, + key: "b", op: "in", match: slice(d3, d4, d5), + expect: []map[string]time.Time{ + {"a": d3, "b": d4}, + }, + }, + { + sequence: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "not in", match: []string{"c", "d", "e"}, + expect: []TstX{ + {A: "a", B: "b"}, {A: "e", B: "f"}, + }, + }, + { + sequence: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "not in", match: slice("c", t, "d", "e"), + expect: []TstX{ + {A: "a", B: "b"}, {A: "e", B: "f"}, + }, + }, + { + sequence: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, + }, + key: "b", op: "", match: nil, + expect: []map[string]int{ + {"a": 3}, + }, + }, + { + sequence: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, + }, + key: "b", op: "!=", match: nil, + expect: []map[string]int{ + {"a": 1, "b": 2}, {"a": 5, "b": 6}, + }, + }, + { + sequence: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, + }, + key: "b", op: ">", match: nil, + expect: []map[string]int{}, + }, + { + sequence: []map[string]bool{ + {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, + }, + key: "b", op: "", match: true, + expect: []map[string]bool{ + {"c": true, "b": true}, + }, + }, + { + sequence: []map[string]bool{ + {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, + }, + key: "b", op: "!=", match: true, + expect: []map[string]bool{ + {"a": true, "b": false}, {"d": true, "b": false}, + }, + }, + { + sequence: []map[string]bool{ + {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, + }, + key: "b", op: ">", match: false, + expect: []map[string]bool{}, + }, + {sequence: (*[]TstX)(nil), key: "A", match: "a", expect: false}, + {sequence: TstX{A: "a", B: "b"}, key: "A", match: "a", expect: false}, + {sequence: []map[string]*TstX{{"foo": nil}}, key: "foo.B", match: "d", expect: false}, + { + sequence: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "op", match: "f", + expect: false, + }, + { + sequence: map[string]interface{}{ + "foo": []interface{}{map[interface{}]interface{}{"a": 1, "b": 2}}, + "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, + "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, + }, + key: "b", op: "in", match: slice(3, 4, 5), + expect: map[string]interface{}{ + "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, + }, + }, + { + sequence: map[string]interface{}{ + "foo": []interface{}{map[interface{}]interface{}{"a": 1, "b": 2}}, + "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, + "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, + }, + key: "b", op: ">", match: 3, + expect: map[string]interface{}{ + "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, + "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, + }, + }, + } { + var results interface{} + var err error + + if len(this.op) > 0 { + results, err = where(this.sequence, this.key, this.op, this.match) + } else { + results, err = where(this.sequence, this.key, this.match) + } + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] Where didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(results, this.expect) { + t.Errorf("[%d] Where clause matching %v with %v, got %v but expected %v", i, this.key, this.match, results, this.expect) + } + } + } + + var err error + _, err = where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1) + if err == nil { + t.Errorf("Where called with none string op value didn't return an expected error") + } + + _, err = where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1, 2) + if err == nil { + t.Errorf("Where called with more than two variable arguments didn't return an expected error") + } + + _, err = where(map[string]int{"a": 1, "b": 2}, "a") + if err == nil { + t.Errorf("Where called with no variable arguments didn't return an expected error") + } +} + +func TestDelimit(t *testing.T) { + t.Parallel() + for i, this := range []struct { + sequence interface{} + delimiter interface{} + last interface{} + expect template.HTML + }{ + {[]string{"class1", "class2", "class3"}, " ", nil, "class1 class2 class3"}, + {[]int{1, 2, 3, 4, 5}, ",", nil, "1,2,3,4,5"}, + {[]int{1, 2, 3, 4, 5}, ", ", nil, "1, 2, 3, 4, 5"}, + {[]string{"class1", "class2", "class3"}, " ", " and ", "class1 class2 and class3"}, + {[]int{1, 2, 3, 4, 5}, ",", ",", "1,2,3,4,5"}, + {[]int{1, 2, 3, 4, 5}, ", ", ", and ", "1, 2, 3, 4, and 5"}, + // test maps with and without sorting required + {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "--", nil, "10--20--30--40--50"}, + {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "--", nil, "30--20--10--40--50"}, + {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, "--", nil, "10--20--30--40--50"}, + {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, "--", nil, "30--20--10--40--50"}, + {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, "--", nil, "50--40--10--30--20"}, + {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, "--", nil, "10--20--30--40--50"}, + {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, "--", nil, "30--20--10--40--50"}, + {map[float64]string{3.3: "10", 2.3: "20", 1.3: "30", 4.3: "40", 5.3: "50"}, "--", nil, "30--20--10--40--50"}, + // test maps with a last delimiter + {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "--", "--and--", "10--20--30--40--and--50"}, + {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "--", "--and--", "30--20--10--40--and--50"}, + {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, "--", "--and--", "10--20--30--40--and--50"}, + {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, "--", "--and--", "30--20--10--40--and--50"}, + {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, "--", "--and--", "50--40--10--30--and--20"}, + {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, "--", "--and--", "10--20--30--40--and--50"}, + {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, "--", "--and--", "30--20--10--40--and--50"}, + {map[float64]string{3.5: "10", 2.5: "20", 1.5: "30", 4.5: "40", 5.5: "50"}, "--", "--and--", "30--20--10--40--and--50"}, + } { + var result template.HTML + var err error + if this.last == nil { + result, err = delimit(this.sequence, this.delimiter) + } else { + result, err = delimit(this.sequence, this.delimiter, this.last) + } + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(result, this.expect) { + t.Errorf("[%d] Delimit called on sequence: %v | delimiter: `%v` | last: `%v`, got %v but expected %v", i, this.sequence, this.delimiter, this.last, result, this.expect) + } + } +} + +func TestSort(t *testing.T) { + t.Parallel() + type ts struct { + MyInt int + MyFloat float64 + MyString string + } + type mid struct { + Tst TstX + } + + for i, this := range []struct { + sequence interface{} + sortByField interface{} + sortAsc string + expect interface{} + }{ + {[]string{"class1", "class2", "class3"}, nil, "asc", []string{"class1", "class2", "class3"}}, + {[]string{"class3", "class1", "class2"}, nil, "asc", []string{"class1", "class2", "class3"}}, + {[]int{1, 2, 3, 4, 5}, nil, "asc", []int{1, 2, 3, 4, 5}}, + {[]int{5, 4, 3, 1, 2}, nil, "asc", []int{1, 2, 3, 4, 5}}, + // test sort key parameter is focibly set empty + {[]string{"class3", "class1", "class2"}, map[int]string{1: "a"}, "asc", []string{"class1", "class2", "class3"}}, + // test map sorting by keys + {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, nil, "asc", []int{10, 20, 30, 40, 50}}, + {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, nil, "asc", []int{30, 20, 10, 40, 50}}, + {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, nil, "asc", []string{"10", "20", "30", "40", "50"}}, + {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, + {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, nil, "asc", []string{"50", "40", "10", "30", "20"}}, + {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, nil, "asc", []string{"10", "20", "30", "40", "50"}}, + {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, + {map[float64]string{3.3: "10", 2.3: "20", 1.3: "30", 4.3: "40", 5.3: "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, + // test map sorting by value + {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "value", "asc", []int{10, 20, 30, 40, 50}}, + {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "value", "asc", []int{10, 20, 30, 40, 50}}, + // test map sorting by field value + { + map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, + "MyInt", + "asc", + []ts{{10, 10.5, "ten"}, {20, 20.5, "twenty"}, {30, 30.5, "thirty"}, {40, 40.5, "forty"}, {50, 50.5, "fifty"}}, + }, + { + map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, + "MyFloat", + "asc", + []ts{{10, 10.5, "ten"}, {20, 20.5, "twenty"}, {30, 30.5, "thirty"}, {40, 40.5, "forty"}, {50, 50.5, "fifty"}}, + }, + { + map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, + "MyString", + "asc", + []ts{{50, 50.5, "fifty"}, {40, 40.5, "forty"}, {10, 10.5, "ten"}, {30, 30.5, "thirty"}, {20, 20.5, "twenty"}}, + }, + // test sort desc + {[]string{"class1", "class2", "class3"}, "value", "desc", []string{"class3", "class2", "class1"}}, + {[]string{"class3", "class1", "class2"}, "value", "desc", []string{"class3", "class2", "class1"}}, + // test sort by struct's method + { + []TstX{{A: "i", B: "j"}, {A: "e", B: "f"}, {A: "c", B: "d"}, {A: "g", B: "h"}, {A: "a", B: "b"}}, + "TstRv", + "asc", + []TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, + }, + { + []*TstX{{A: "i", B: "j"}, {A: "e", B: "f"}, {A: "c", B: "d"}, {A: "g", B: "h"}, {A: "a", B: "b"}}, + "TstRp", + "asc", + []*TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, + }, + // test map sorting by struct's method + { + map[string]TstX{"1": {A: "i", B: "j"}, "2": {A: "e", B: "f"}, "3": {A: "c", B: "d"}, "4": {A: "g", B: "h"}, "5": {A: "a", B: "b"}}, + "TstRv", + "asc", + []TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, + }, + { + map[string]*TstX{"1": {A: "i", B: "j"}, "2": {A: "e", B: "f"}, "3": {A: "c", B: "d"}, "4": {A: "g", B: "h"}, "5": {A: "a", B: "b"}}, + "TstRp", + "asc", + []*TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, + }, + // test sort by dot chaining key argument + { + []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, + "foo.A", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, + ".foo.A", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, + "foo.TstRv", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + []map[string]*TstX{{"foo": &TstX{A: "e", B: "f"}}, {"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}}, + "foo.TstRp", + "asc", + []map[string]*TstX{{"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}}, + }, + { + []map[string]mid{{"foo": mid{Tst: TstX{A: "e", B: "f"}}}, {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, + "foo.Tst.A", + "asc", + []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, + }, + { + []map[string]mid{{"foo": mid{Tst: TstX{A: "e", B: "f"}}}, {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, + "foo.Tst.TstRv", + "asc", + []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, + }, + // test map sorting by dot chaining key argument + { + map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, + "foo.A", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, + ".foo.A", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, + "foo.TstRv", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + map[string]map[string]*TstX{"1": {"foo": &TstX{A: "e", B: "f"}}, "2": {"foo": &TstX{A: "a", B: "b"}}, "3": {"foo": &TstX{A: "c", B: "d"}}}, + "foo.TstRp", + "asc", + []map[string]*TstX{{"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}}, + }, + { + map[string]map[string]mid{"1": {"foo": mid{Tst: TstX{A: "e", B: "f"}}}, "2": {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, "3": {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, + "foo.Tst.A", + "asc", + []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, + }, + { + map[string]map[string]mid{"1": {"foo": mid{Tst: TstX{A: "e", B: "f"}}}, "2": {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, "3": {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, + "foo.Tst.TstRv", + "asc", + []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, + }, + // interface slice with missing elements + { + []interface{}{ + map[interface{}]interface{}{"Title": "Foo", "Weight": 10}, + map[interface{}]interface{}{"Title": "Bar"}, + map[interface{}]interface{}{"Title": "Zap", "Weight": 5}, + }, + "Weight", + "asc", + []interface{}{ + map[interface{}]interface{}{"Title": "Bar"}, + map[interface{}]interface{}{"Title": "Zap", "Weight": 5}, + map[interface{}]interface{}{"Title": "Foo", "Weight": 10}, + }, + }, + // test error cases + {(*[]TstX)(nil), nil, "asc", false}, + {TstX{A: "a", B: "b"}, nil, "asc", false}, + { + []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, + "foo.NotAvailable", + "asc", + false, + }, + { + map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, + "foo.NotAvailable", + "asc", + false, + }, + {nil, nil, "asc", false}, + } { + var result interface{} + var err error + if this.sortByField == nil { + result, err = sortSeq(this.sequence) + } else { + result, err = sortSeq(this.sequence, this.sortByField, this.sortAsc) + } + + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] Sort didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(result, this.expect) { + t.Errorf("[%d] Sort called on sequence: %v | sortByField: `%v` | got %v but expected %v", i, this.sequence, this.sortByField, result, this.expect) + } + } + } +} + +func TestReturnWhenSet(t *testing.T) { + t.Parallel() + for i, this := range []struct { + data interface{} + key interface{} + expect interface{} + }{ + {[]int{1, 2, 3}, 1, int64(2)}, + {[]uint{1, 2, 3}, 1, uint64(2)}, + {[]float64{1.1, 2.2, 3.3}, 1, float64(2.2)}, + {[]string{"foo", "bar", "baz"}, 1, "bar"}, + {[]TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}}, 1, ""}, + {map[string]int{"foo": 1, "bar": 2, "baz": 3}, "bar", int64(2)}, + {map[string]uint{"foo": 1, "bar": 2, "baz": 3}, "bar", uint64(2)}, + {map[string]float64{"foo": 1.1, "bar": 2.2, "baz": 3.3}, "bar", float64(2.2)}, + {map[string]string{"foo": "FOO", "bar": "BAR", "baz": "BAZ"}, "bar", "BAR"}, + {map[string]TstX{"foo": {A: "a", B: "b"}, "bar": {A: "c", B: "d"}, "baz": {A: "e", B: "f"}}, "bar", ""}, + {(*[]string)(nil), "bar", ""}, + } { + result := returnWhenSet(this.data, this.key) + if !reflect.DeepEqual(result, this.expect) { + t.Errorf("[%d] ReturnWhenSet got %v (type %v) but expected %v (type %v)", i, result, reflect.TypeOf(result), this.expect, reflect.TypeOf(this.expect)) + } + } +} + +func TestMarkdownify(t *testing.T) { + t.Parallel() + v := viper.New() + + f := newTestFuncsterWithViper(v) + + for i, this := range []struct { + in interface{} + expect interface{} + }{ + {"Hello **World!**", template.HTML("Hello World!")}, + {[]byte("Hello Bytes **World!**"), template.HTML("Hello Bytes World!")}, + } { + result, err := f.markdownify(this.in) + if err != nil { + t.Fatalf("[%d] unexpected error in markdownify: %s", i, err) + } + if !reflect.DeepEqual(result, this.expect) { + t.Errorf("[%d] markdownify got %v (type %v) but expected %v (type %v)", i, result, reflect.TypeOf(result), this.expect, reflect.TypeOf(this.expect)) + } + } + + if _, err := f.markdownify(t); err == nil { + t.Fatalf("markdownify should have errored") + } +} + +func TestApply(t *testing.T) { + t.Parallel() + + f := newTestFuncster() + + strings := []interface{}{"a\n", "b\n"} + noStringers := []interface{}{tstNoStringer{}, tstNoStringer{}} + + chomped, _ := f.apply(strings, "chomp", ".") + assert.Equal(t, []interface{}{template.HTML("a"), template.HTML("b")}, chomped) + + chomped, _ = f.apply(strings, "chomp", "c\n") + assert.Equal(t, []interface{}{template.HTML("c"), template.HTML("c")}, chomped) + + chomped, _ = f.apply(nil, "chomp", ".") + assert.Equal(t, []interface{}{}, chomped) + + _, err := f.apply(strings, "apply", ".") + if err == nil { + t.Errorf("apply with apply should fail") + } + + var nilErr *error + _, err = f.apply(nilErr, "chomp", ".") + if err == nil { + t.Errorf("apply with nil in seq should fail") + } + + _, err = f.apply(strings, "dobedobedo", ".") + if err == nil { + t.Errorf("apply with unknown func should fail") + } + + _, err = f.apply(noStringers, "chomp", ".") + if err == nil { + t.Errorf("apply when func fails should fail") + } + + _, err = f.apply(tstNoStringer{}, "chomp", ".") + if err == nil { + t.Errorf("apply with non-sequence should fail") + } +} + +func TestChomp(t *testing.T) { + t.Parallel() + base := "\n This is\na story " + for i, item := range []string{ + "\n", "\n\n", + "\r", "\r\r", + "\r\n", "\r\n\r\n", + } { + c, _ := chomp(base + item) + chomped := string(c) + + if chomped != base { + t.Errorf("[%d] Chomp failed, got '%v'", i, chomped) + } + + _, err := chomp(tstNoStringer{}) + + if err == nil { + t.Errorf("Chomp should fail") + } + } +} + +func TestLower(t *testing.T) { + t.Parallel() + cases := []struct { + s interface{} + want string + isErr bool + }{ + {"TEST", "test", false}, + {template.HTML("LoWeR"), "lower", false}, + {[]byte("BYTES"), "bytes", false}, + } + + for i, c := range cases { + res, err := lower(c.s) + if (err != nil) != c.isErr { + t.Fatalf("[%d] unexpected isErr state: want %v, got %v, err = %v", i, c.want, (err != nil), err) + } + + if res != c.want { + t.Errorf("[%d] lower failed: want %v, got %v", i, c.want, res) + } + } +} + +func TestTitle(t *testing.T) { + t.Parallel() + cases := []struct { + s interface{} + want string + isErr bool + }{ + {"test", "Test", false}, + {template.HTML("hypertext"), "Hypertext", false}, + {[]byte("bytes"), "Bytes", false}, + } + + for i, c := range cases { + res, err := title(c.s) + if (err != nil) != c.isErr { + t.Fatalf("[%d] unexpected isErr state: want %v, got %v, err = %v", i, c.want, (err != nil), err) + } + + if res != c.want { + t.Errorf("[%d] title failed: want %v, got %v", i, c.want, res) + } + } +} + +func TestUpper(t *testing.T) { + t.Parallel() + cases := []struct { + s interface{} + want string + isErr bool + }{ + {"test", "TEST", false}, + {template.HTML("UpPeR"), "UPPER", false}, + {[]byte("bytes"), "BYTES", false}, + } + + for i, c := range cases { + res, err := upper(c.s) + if (err != nil) != c.isErr { + t.Fatalf("[%d] unexpected isErr state: want %v, got %v, err = %v", i, c.want, (err != nil), err) + } + + if res != c.want { + t.Errorf("[%d] upper failed: want %v, got %v", i, c.want, res) + } + } +} + +func TestHighlight(t *testing.T) { + t.Parallel() + code := "func boo() {}" + + f := newTestFuncster() + + highlighted, err := f.highlight(code, "go", "") + + if err != nil { + t.Fatal("Highlight returned error:", err) + } + + // this depends on a Pygments installation, but will always contain the function name. + if !strings.Contains(string(highlighted), "boo") { + t.Errorf("Highlight mismatch, got %v", highlighted) + } + + _, err = f.highlight(t, "go", "") + + if err == nil { + t.Error("Expected highlight error") + } +} + +func TestInflect(t *testing.T) { + t.Parallel() + for i, this := range []struct { + inflectFunc func(i interface{}) (string, error) + in interface{} + expected string + }{ + {humanize, "MyCamel", "My camel"}, + {humanize, "", ""}, + {humanize, "103", "103rd"}, + {humanize, "41", "41st"}, + {humanize, 103, "103rd"}, + {humanize, int64(92), "92nd"}, + {humanize, "5.5", "5.5"}, + {pluralize, "cat", "cats"}, + {pluralize, "", ""}, + {singularize, "cats", "cat"}, + {singularize, "", ""}, + } { + + result, err := this.inflectFunc(this.in) + + if err != nil { + t.Errorf("[%d] Unexpected Inflect error: %s", i, err) + } else if result != this.expected { + t.Errorf("[%d] Inflect method error, got %v expected %v", i, result, this.expected) + } + + _, err = this.inflectFunc(t) + if err == nil { + t.Errorf("[%d] Expected Inflect error", i) + } + } +} + +func TestCounterFuncs(t *testing.T) { + t.Parallel() + for i, this := range []struct { + countFunc func(i interface{}) (int, error) + in string + expected int + }{ + {countWords, "Do Be Do Be Do", 5}, + {countWords, "旁边", 2}, + {countRunes, "旁边", 2}, + } { + + result, err := this.countFunc(this.in) + + if err != nil { + t.Errorf("[%d] Unexpected counter error: %s", i, err) + } else if result != this.expected { + t.Errorf("[%d] Count method error, got %v expected %v", i, result, this.expected) + } + + _, err = this.countFunc(t) + if err == nil { + t.Errorf("[%d] Expected Count error", i) + } + } +} + +func TestReplace(t *testing.T) { + t.Parallel() + v, _ := replace("aab", "a", "b") + assert.Equal(t, "bbb", v) + v, _ = replace("11a11", 1, 2) + assert.Equal(t, "22a22", v) + v, _ = replace(12345, 1, 2) + assert.Equal(t, "22345", v) + _, e := replace(tstNoStringer{}, "a", "b") + assert.NotNil(t, e, "tstNoStringer isn't trimmable") + _, e = replace("a", tstNoStringer{}, "b") + assert.NotNil(t, e, "tstNoStringer cannot be converted to string") + _, e = replace("a", "b", tstNoStringer{}) + assert.NotNil(t, e, "tstNoStringer cannot be converted to string") +} + +func TestReplaceRE(t *testing.T) { + t.Parallel() + for i, val := range []struct { + pattern interface{} + repl interface{} + src interface{} + expect string + ok bool + }{ + {"^https?://([^/]+).*", "$1", "http://gohugo.io/docs", "gohugo.io", true}, + {"^https?://([^/]+).*", "$2", "http://gohugo.io/docs", "", true}, + {tstNoStringer{}, "$2", "http://gohugo.io/docs", "", false}, + {"^https?://([^/]+).*", tstNoStringer{}, "http://gohugo.io/docs", "", false}, + {"^https?://([^/]+).*", "$2", tstNoStringer{}, "", false}, + {"(ab)", "AB", "aabbaab", "aABbaAB", true}, + {"(ab", "AB", "aabb", "", false}, // invalid re + } { + v, err := replaceRE(val.pattern, val.repl, val.src) + if (err == nil) != val.ok { + t.Errorf("[%d] %s", i, err) + } + assert.Equal(t, val.expect, v) + } +} + +func TestFindRE(t *testing.T) { + t.Parallel() + for i, this := range []struct { + expr string + content interface{} + limit interface{} + expect []string + ok bool + }{ + {"[G|g]o", "Hugo is a static site generator written in Go.", 2, []string{"go", "Go"}, true}, + {"[G|g]o", "Hugo is a static site generator written in Go.", -1, []string{"go", "Go"}, true}, + {"[G|g]o", "Hugo is a static site generator written in Go.", 1, []string{"go"}, true}, + {"[G|g]o", "Hugo is a static site generator written in Go.", "1", []string{"go"}, true}, + {"[G|g]o", "Hugo is a static site generator written in Go.", nil, []string(nil), true}, + {"[G|go", "Hugo is a static site generator written in Go.", nil, []string(nil), false}, + {"[G|g]o", t, nil, []string(nil), false}, + } { + var ( + res []string + err error + ) + + res, err = findRE(this.expr, this.content, this.limit) + if err != nil && this.ok { + t.Errorf("[%d] returned an unexpected error: %s", i, err) + } + + assert.Equal(t, this.expect, res) + } +} + +func TestTrim(t *testing.T) { + t.Parallel() + + for i, this := range []struct { + v1 interface{} + v2 string + expect interface{} + }{ + {"1234 my way 13", "123 ", "4 my way"}, + {" my way ", " ", "my way"}, + {1234, "14", "23"}, + {tstNoStringer{}, " ", false}, + } { + result, err := trim(this.v1, this.v2) + + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] trim didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(result, this.expect) { + t.Errorf("[%d] got '%s' but expected %s", i, result, this.expect) + } + } + } +} + +func TestDateFormat(t *testing.T) { + t.Parallel() + for i, this := range []struct { + layout string + value interface{} + expect interface{} + }{ + {"Monday, Jan 2, 2006", "2015-01-21", "Wednesday, Jan 21, 2015"}, + {"Monday, Jan 2, 2006", time.Date(2015, time.January, 21, 0, 0, 0, 0, time.UTC), "Wednesday, Jan 21, 2015"}, + {"This isn't a date layout string", "2015-01-21", "This isn't a date layout string"}, + // The following test case gives either "Tuesday, Jan 20, 2015" or "Monday, Jan 19, 2015" depending on the local time zone + {"Monday, Jan 2, 2006", 1421733600, time.Unix(1421733600, 0).Format("Monday, Jan 2, 2006")}, + {"Monday, Jan 2, 2006", 1421733600.123, false}, + {time.RFC3339, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), "2016-03-03T04:05:00Z"}, + {time.RFC1123, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), "Thu, 03 Mar 2016 04:05:00 UTC"}, + {time.RFC3339, "Thu, 03 Mar 2016 04:05:00 UTC", "2016-03-03T04:05:00Z"}, + {time.RFC1123, "2016-03-03T04:05:00Z", "Thu, 03 Mar 2016 04:05:00 UTC"}, + } { + result, err := dateFormat(this.layout, this.value) + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] DateFormat didn't return an expected error, got %v", i, result) + } + } else { + if err != nil { + t.Errorf("[%d] DateFormat failed: %s", i, err) + continue + } + if result != this.expect { + t.Errorf("[%d] DateFormat got %v but expected %v", i, result, this.expect) + } + } + } +} + +func TestDefaultFunc(t *testing.T) { + t.Parallel() + then := time.Now() + now := time.Now() + + for i, this := range []struct { + dflt interface{} + given interface{} + expected interface{} + }{ + {true, false, false}, + {"5", 0, "5"}, + + {"test1", "set", "set"}, + {"test2", "", "test2"}, + {"test3", nil, "test3"}, + + {[2]int{10, 20}, [2]int{1, 2}, [2]int{1, 2}}, + {[2]int{10, 20}, [0]int{}, [2]int{10, 20}}, + {[2]int{100, 200}, nil, [2]int{100, 200}}, + + {[]string{"one"}, []string{"uno"}, []string{"uno"}}, + {[]string{"two"}, []string{}, []string{"two"}}, + {[]string{"three"}, nil, []string{"three"}}, + + {map[string]int{"one": 1}, map[string]int{"uno": 1}, map[string]int{"uno": 1}}, + {map[string]int{"one": 1}, map[string]int{}, map[string]int{"one": 1}}, + {map[string]int{"two": 2}, nil, map[string]int{"two": 2}}, + + {10, 1, 1}, + {10, 0, 10}, + {20, nil, 20}, + + {float32(10), float32(1), float32(1)}, + {float32(10), 0, float32(10)}, + {float32(20), nil, float32(20)}, + + {complex(2, -2), complex(1, -1), complex(1, -1)}, + {complex(2, -2), complex(0, 0), complex(2, -2)}, + {complex(3, -3), nil, complex(3, -3)}, + + {struct{ f string }{f: "one"}, struct{ f string }{}, struct{ f string }{}}, + {struct{ f string }{f: "two"}, nil, struct{ f string }{f: "two"}}, + + {then, now, now}, + {then, time.Time{}, then}, + } { + res, err := dfault(this.dflt, this.given) + if err != nil { + t.Errorf("[%d] default returned an error: %s", i, err) + continue + } + if !reflect.DeepEqual(this.expected, res) { + t.Errorf("[%d] default returned %v, but expected %v", i, res, this.expected) + } + } +} + +func TestDefault(t *testing.T) { + t.Parallel() + for i, this := range []struct { + input interface{} + tpl string + expected string + ok bool + }{ + {map[string]string{"foo": "bar"}, `{{ index . "foo" | default "nope" }}`, `bar`, true}, + {map[string]string{"foo": "pop"}, `{{ index . "bar" | default "nada" }}`, `nada`, true}, + {map[string]string{"foo": "cat"}, `{{ default "nope" .foo }}`, `cat`, true}, + {map[string]string{"foo": "dog"}, `{{ default "nope" .foo "extra" }}`, ``, false}, + {map[string]interface{}{"images": []string{}}, `{{ default "default.jpg" (index .images 0) }}`, `default.jpg`, true}, + } { + + tmpl := newTestTemplate(t, "test", this.tpl) + + buf := new(bytes.Buffer) + err := tmpl.Execute(buf, this.input) + if (err == nil) != this.ok { + t.Errorf("[%d] execute template returned unexpected error: %s", i, err) + continue + } + + if buf.String() != this.expected { + t.Errorf("[%d] execute template got %v, but expected %v", i, buf.String(), this.expected) + } + } +} + +func TestSafeHTML(t *testing.T) { + t.Parallel() + for i, this := range []struct { + str string + tmplStr string + expectWithoutEscape string + expectWithEscape string + }{ + {`
`, `{{ . }}`, `<div></div>`, `
`}, + } { + tmpl, err := template.New("test").Parse(this.tmplStr) + if err != nil { + t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) + continue + } + + buf := new(bytes.Buffer) + err = tmpl.Execute(buf, this.str) + if err != nil { + t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) + } + if buf.String() != this.expectWithoutEscape { + t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) + } + + buf.Reset() + v, err := safeHTML(this.str) + if err != nil { + t.Fatalf("[%d] unexpected error in safeHTML: %s", i, err) + } + + err = tmpl.Execute(buf, v) + if err != nil { + t.Errorf("[%d] execute template with an escaped string value by safeHTML returns unexpected error: %s", i, err) + } + if buf.String() != this.expectWithEscape { + t.Errorf("[%d] execute template with an escaped string value by safeHTML, got %v but expected %v", i, buf.String(), this.expectWithEscape) + } + } +} + +func TestSafeHTMLAttr(t *testing.T) { + t.Parallel() + for i, this := range []struct { + str string + tmplStr string + expectWithoutEscape string + expectWithEscape string + }{ + {`href="irc://irc.freenode.net/#golang"`, `irc`, `irc`, `irc`}, + } { + tmpl, err := template.New("test").Parse(this.tmplStr) + if err != nil { + t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) + continue + } + + buf := new(bytes.Buffer) + err = tmpl.Execute(buf, this.str) + if err != nil { + t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) + } + if buf.String() != this.expectWithoutEscape { + t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) + } + + buf.Reset() + v, err := safeHTMLAttr(this.str) + if err != nil { + t.Fatalf("[%d] unexpected error in safeHTMLAttr: %s", i, err) + } + + err = tmpl.Execute(buf, v) + if err != nil { + t.Errorf("[%d] execute template with an escaped string value by safeHTMLAttr returns unexpected error: %s", i, err) + } + if buf.String() != this.expectWithEscape { + t.Errorf("[%d] execute template with an escaped string value by safeHTMLAttr, got %v but expected %v", i, buf.String(), this.expectWithEscape) + } + } +} + +func TestSafeCSS(t *testing.T) { + t.Parallel() + for i, this := range []struct { + str string + tmplStr string + expectWithoutEscape string + expectWithEscape string + }{ + {`width: 60px;`, `
`, `
`, `
`}, + } { + tmpl, err := template.New("test").Parse(this.tmplStr) + if err != nil { + t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) + continue + } + + buf := new(bytes.Buffer) + err = tmpl.Execute(buf, this.str) + if err != nil { + t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) + } + if buf.String() != this.expectWithoutEscape { + t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) + } + + buf.Reset() + v, err := safeCSS(this.str) + if err != nil { + t.Fatalf("[%d] unexpected error in safeCSS: %s", i, err) + } + + err = tmpl.Execute(buf, v) + if err != nil { + t.Errorf("[%d] execute template with an escaped string value by safeCSS returns unexpected error: %s", i, err) + } + if buf.String() != this.expectWithEscape { + t.Errorf("[%d] execute template with an escaped string value by safeCSS, got %v but expected %v", i, buf.String(), this.expectWithEscape) + } + } +} + +// TODO(bep) what is this? Also look above. +func TestSafeJS(t *testing.T) { + t.Parallel() + for i, this := range []struct { + str string + tmplStr string + expectWithoutEscape string + expectWithEscape string + }{ + {`619c16f`, ``, ``, ``}, + } { + tmpl, err := template.New("test").Parse(this.tmplStr) + if err != nil { + t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) + continue + } + + buf := new(bytes.Buffer) + err = tmpl.Execute(buf, this.str) + if err != nil { + t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) + } + if buf.String() != this.expectWithoutEscape { + t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) + } + + buf.Reset() + v, err := safeJS(this.str) + if err != nil { + t.Fatalf("[%d] unexpected error in safeJS: %s", i, err) + } + + err = tmpl.Execute(buf, v) + if err != nil { + t.Errorf("[%d] execute template with an escaped string value by safeJS returns unexpected error: %s", i, err) + } + if buf.String() != this.expectWithEscape { + t.Errorf("[%d] execute template with an escaped string value by safeJS, got %v but expected %v", i, buf.String(), this.expectWithEscape) + } + } +} + +// TODO(bep) what is this? +func TestSafeURL(t *testing.T) { + t.Parallel() + for i, this := range []struct { + str string + tmplStr string + expectWithoutEscape string + expectWithEscape string + }{ + {`irc://irc.freenode.net/#golang`, `IRC`, `IRC`, `IRC`}, + } { + tmpl, err := template.New("test").Parse(this.tmplStr) + if err != nil { + t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) + continue + } + + buf := new(bytes.Buffer) + err = tmpl.Execute(buf, this.str) + if err != nil { + t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) + } + if buf.String() != this.expectWithoutEscape { + t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) + } + + buf.Reset() + v, err := safeURL(this.str) + if err != nil { + t.Fatalf("[%d] unexpected error in safeURL: %s", i, err) + } + + err = tmpl.Execute(buf, v) + if err != nil { + t.Errorf("[%d] execute template with an escaped string value by safeURL returns unexpected error: %s", i, err) + } + if buf.String() != this.expectWithEscape { + t.Errorf("[%d] execute template with an escaped string value by safeURL, got %v but expected %v", i, buf.String(), this.expectWithEscape) + } + } +} + +func TestBase64Decode(t *testing.T) { + t.Parallel() + testStr := "abc123!?$*&()'-=@~" + enc := base64.StdEncoding.EncodeToString([]byte(testStr)) + result, err := base64Decode(enc) + + if err != nil { + t.Error("base64Decode returned error:", err) + } + + if result != testStr { + t.Errorf("base64Decode: got '%s', expected '%s'", result, testStr) + } + + _, err = base64Decode(t) + if err == nil { + t.Error("Expected error from base64Decode") + } +} + +func TestBase64Encode(t *testing.T) { + t.Parallel() + testStr := "YWJjMTIzIT8kKiYoKSctPUB+" + dec, err := base64.StdEncoding.DecodeString(testStr) + + if err != nil { + t.Error("base64Encode: the DecodeString function of the base64 package returned an error:", err) + } + + result, err := base64Encode(string(dec)) + + if err != nil { + t.Errorf("base64Encode: Can't cast arg '%s' into a string:", testStr) + } + + if result != testStr { + t.Errorf("base64Encode: got '%s', expected '%s'", result, testStr) + } + + _, err = base64Encode(t) + if err == nil { + t.Error("Expected error from base64Encode") + } +} + +func TestMD5(t *testing.T) { + t.Parallel() + for i, this := range []struct { + input string + expectedHash string + }{ + {"Hello world, gophers!", "b3029f756f98f79e7f1b7f1d1f0dd53b"}, + {"Lorem ipsum dolor", "06ce65ac476fc656bea3fca5d02cfd81"}, + } { + result, err := md5(this.input) + if err != nil { + t.Errorf("md5 returned error: %s", err) + } + + if result != this.expectedHash { + t.Errorf("[%d] md5: expected '%s', got '%s'", i, this.expectedHash, result) + } + } + + _, err := md5(t) + if err == nil { + t.Error("Expected error from md5") + } +} + +func TestSHA1(t *testing.T) { + t.Parallel() + for i, this := range []struct { + input string + expectedHash string + }{ + {"Hello world, gophers!", "c8b5b0e33d408246e30f53e32b8f7627a7a649d4"}, + {"Lorem ipsum dolor", "45f75b844be4d17b3394c6701768daf39419c99b"}, + } { + result, err := sha1(this.input) + if err != nil { + t.Errorf("sha1 returned error: %s", err) + } + + if result != this.expectedHash { + t.Errorf("[%d] sha1: expected '%s', got '%s'", i, this.expectedHash, result) + } + } + + _, err := sha1(t) + if err == nil { + t.Error("Expected error from sha1") + } +} + +func TestSHA256(t *testing.T) { + t.Parallel() + for i, this := range []struct { + input string + expectedHash string + }{ + {"Hello world, gophers!", "6ec43b78da9669f50e4e422575c54bf87536954ccd58280219c393f2ce352b46"}, + {"Lorem ipsum dolor", "9b3e1beb7053e0f900a674dd1c99aca3355e1275e1b03d3cb1bc977f5154e196"}, + } { + result, err := sha256(this.input) + if err != nil { + t.Errorf("sha256 returned error: %s", err) + } + + if result != this.expectedHash { + t.Errorf("[%d] sha256: expected '%s', got '%s'", i, this.expectedHash, result) + } + } + + _, err := sha256(t) + if err == nil { + t.Error("Expected error from sha256") + } +} + +func TestReadFile(t *testing.T) { + t.Parallel() + + workingDir := "/home/hugo" + + v := viper.New() + + v.Set("workingDir", workingDir) + + f := newTestFuncsterWithViper(v) + + afero.WriteFile(f.Fs.Source, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755) + afero.WriteFile(f.Fs.Source, filepath.Join("/home", "f2.txt"), []byte("f2-content"), 0755) + + for i, this := range []struct { + filename string + expect interface{} + }{ + {"", false}, + {"b", false}, + {filepath.FromSlash("/f/f1.txt"), "f1-content"}, + {filepath.FromSlash("f/f1.txt"), "f1-content"}, + {filepath.FromSlash("../f2.txt"), false}, + } { + result, err := f.readFileFromWorkingDir(this.filename) + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] readFile didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] readFile failed: %s", i, err) + continue + } + if result != this.expect { + t.Errorf("[%d] readFile got %q but expected %q", i, result, this.expect) + } + } + } +} + +func TestPartialCached(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + partial string + tmpl string + variant string + }{ + // name and partial should match between test cases. + {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . }}`, ""}, + {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "header"}, + {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "footer"}, + {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "header"}, + } + + var data struct { + Title string + Section string + Params map[string]interface{} + } + + data.Title = "**BatMan**" + data.Section = "blog" + data.Params = map[string]interface{}{"langCode": "en"} + + for i, tc := range testCases { + var tmp string + if tc.variant != "" { + tmp = fmt.Sprintf(tc.tmpl, tc.variant) + } else { + tmp = tc.tmpl + } + + config := newDepsConfig(viper.New()) + + config.WithTemplate = func(templ tpl.Template) error { + err := templ.AddTemplate("testroot", tmp) + if err != nil { + return err + } + err = templ.AddTemplate("partials/"+tc.name, tc.partial) + if err != nil { + return err + } + + return nil + } + + de := deps.New(config) + require.NoError(t, de.LoadResources()) + + buf := new(bytes.Buffer) + templ := de.Tmpl.Lookup("testroot") + err := templ.Execute(buf, &data) + if err != nil { + t.Fatalf("[%d] error executing template: %s", i, err) + } + + for j := 0; j < 10; j++ { + buf2 := new(bytes.Buffer) + err := templ.Execute(buf2, nil) + if err != nil { + t.Fatalf("[%d] error executing template 2nd time: %s", i, err) + } + + if !reflect.DeepEqual(buf, buf2) { + t.Fatalf("[%d] cached results do not match:\nResult 1:\n%q\nResult 2:\n%q", i, buf, buf2) + } + } + } +} + +func BenchmarkPartial(b *testing.B) { + config := newDepsConfig(viper.New()) + config.WithTemplate = func(templ tpl.Template) error { + err := templ.AddTemplate("testroot", `{{ partial "bench1" . }}`) + if err != nil { + return err + } + err = templ.AddTemplate("partials/bench1", `{{ shuffle (seq 1 10) }}`) + if err != nil { + return err + } + + return nil + } + + de := deps.New(config) + require.NoError(b, de.LoadResources()) + + buf := new(bytes.Buffer) + tmpl := de.Tmpl.Lookup("testroot") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := tmpl.Execute(buf, nil); err != nil { + b.Fatalf("error executing template: %s", err) + } + buf.Reset() + } +} + +func BenchmarkPartialCached(b *testing.B) { + config := newDepsConfig(viper.New()) + config.WithTemplate = func(templ tpl.Template) error { + err := templ.AddTemplate("testroot", `{{ partialCached "bench1" . }}`) + if err != nil { + return err + } + err = templ.AddTemplate("partials/bench1", `{{ shuffle (seq 1 10) }}`) + if err != nil { + return err + } + + return nil + } + + de := deps.New(config) + require.NoError(b, de.LoadResources()) + + buf := new(bytes.Buffer) + tmpl := de.Tmpl.Lookup("testroot") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := tmpl.Execute(buf, nil); err != nil { + b.Fatalf("error executing template: %s", err) + } + buf.Reset() + } +} + +func newTestFuncster() *templateFuncster { + return newTestFuncsterWithViper(viper.New()) +} + +func newTestFuncsterWithViper(v *viper.Viper) *templateFuncster { + config := newDepsConfig(v) + d := deps.New(config) + + if err := d.LoadResources(); err != nil { + panic(err) + } + + return d.Tmpl.(*GoHTMLTemplate).funcster +} + +func newTestTemplate(t *testing.T, name, template string) *template.Template { + config := newDepsConfig(viper.New()) + config.WithTemplate = func(templ tpl.Template) error { + err := templ.AddTemplate(name, template) + if err != nil { + return err + } + return nil + } + + de := deps.New(config) + require.NoError(t, de.LoadResources()) + + return de.Tmpl.Lookup(name) +} diff --git a/tpl/tplimpl/template_resources.go b/tpl/tplimpl/template_resources.go new file mode 100644 index 00000000..2b3d7120 --- /dev/null +++ b/tpl/tplimpl/template_resources.go @@ -0,0 +1,253 @@ +// Copyright 2016 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 tplimpl + +import ( + "bytes" + "encoding/csv" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/url" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/spf13/afero" + "github.com/spf13/hugo/config" + "github.com/spf13/hugo/helpers" + jww "github.com/spf13/jwalterweatherman" +) + +var ( + remoteURLLock = &remoteLock{m: make(map[string]*sync.Mutex)} + resSleep = time.Second * 2 // if JSON decoding failed sleep for n seconds before retrying + resRetries = 1 // number of retries to load the JSON from URL or local file system +) + +type remoteLock struct { + sync.RWMutex + m map[string]*sync.Mutex +} + +// URLLock locks an URL during download +func (l *remoteLock) URLLock(url string) { + l.Lock() + if _, ok := l.m[url]; !ok { + l.m[url] = &sync.Mutex{} + } + l.Unlock() // call this Unlock before the next lock will be called. NFI why but defer doesn't work. + l.m[url].Lock() +} + +// URLUnlock unlocks an URL when the download has been finished. Use only in defer calls. +func (l *remoteLock) URLUnlock(url string) { + l.RLock() + defer l.RUnlock() + if um, ok := l.m[url]; ok { + um.Unlock() + } +} + +// getCacheFileID returns the cache ID for a string +func getCacheFileID(cfg config.Provider, id string) string { + return cfg.GetString("cacheDir") + url.QueryEscape(id) +} + +// resGetCache returns the content for an ID from the file cache or an error +// if the file is not found returns nil,nil +func resGetCache(id string, fs afero.Fs, cfg config.Provider, ignoreCache bool) ([]byte, error) { + if ignoreCache { + return nil, nil + } + fID := getCacheFileID(cfg, id) + isExists, err := helpers.Exists(fID, fs) + if err != nil { + return nil, err + } + if !isExists { + return nil, nil + } + + return afero.ReadFile(fs, fID) + +} + +// resWriteCache writes bytes to an ID into the file cache +func resWriteCache(id string, c []byte, fs afero.Fs, cfg config.Provider, ignoreCache bool) error { + if ignoreCache { + return nil + } + fID := getCacheFileID(cfg, id) + f, err := fs.Create(fID) + if err != nil { + return errors.New("Error: " + err.Error() + ". Failed to create file: " + fID) + } + defer f.Close() + n, err := f.Write(c) + if n == 0 { + return errors.New("No bytes written to file: " + fID) + } + if err != nil { + return errors.New("Error: " + err.Error() + ". Failed to write to file: " + fID) + } + return nil +} + +func resDeleteCache(id string, fs afero.Fs, cfg config.Provider) error { + return fs.Remove(getCacheFileID(cfg, id)) +} + +// resGetRemote loads the content of a remote file. This method is thread safe. +func resGetRemote(url string, fs afero.Fs, cfg config.Provider, hc *http.Client) ([]byte, error) { + c, err := resGetCache(url, fs, cfg, cfg.GetBool("ignoreCache")) + if c != nil && err == nil { + return c, nil + } + if err != nil { + return nil, err + } + + // avoid race condition with locks, block other goroutines if the current url is processing + remoteURLLock.URLLock(url) + defer func() { remoteURLLock.URLUnlock(url) }() + + // avoid multiple locks due to calling resGetCache twice + c, err = resGetCache(url, fs, cfg, cfg.GetBool("ignoreCache")) + if c != nil && err == nil { + return c, nil + } + if err != nil { + return nil, err + } + + jww.INFO.Printf("Downloading: %s ...", url) + res, err := hc.Get(url) + if err != nil { + return nil, err + } + c, err = ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + return nil, err + } + err = resWriteCache(url, c, fs, cfg, cfg.GetBool("ignoreCache")) + if err != nil { + return nil, err + } + jww.INFO.Printf("... and cached to: %s", getCacheFileID(cfg, url)) + return c, nil +} + +// resGetLocal loads the content of a local file +func resGetLocal(url string, fs afero.Fs, cfg config.Provider) ([]byte, error) { + filename := filepath.Join(cfg.GetString("workingDir"), url) + if e, err := helpers.Exists(filename, fs); !e { + return nil, err + } + + return afero.ReadFile(fs, filename) + +} + +// resGetResource loads the content of a local or remote file +func (t *templateFuncster) resGetResource(url string) ([]byte, error) { + if url == "" { + return nil, nil + } + if strings.Contains(url, "://") { + return resGetRemote(url, t.Fs.Source, t.Cfg, http.DefaultClient) + } + return resGetLocal(url, t.Fs.Source, t.Cfg) +} + +// getJSON expects one or n-parts of a URL to a resource which can either be a local or a remote one. +// If you provide multiple parts they will be joined together to the final URL. +// GetJSON returns nil or parsed JSON to use in a short code. +func (t *templateFuncster) getJSON(urlParts ...string) interface{} { + var v interface{} + url := strings.Join(urlParts, "") + + for i := 0; i <= resRetries; i++ { + c, err := t.resGetResource(url) + if err != nil { + jww.ERROR.Printf("Failed to get json resource %s with error message %s", url, err) + return nil + } + + err = json.Unmarshal(c, &v) + if err != nil { + jww.ERROR.Printf("Cannot read json from resource %s with error message %s", url, err) + jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep) + time.Sleep(resSleep) + resDeleteCache(url, t.Fs.Source, t.Cfg) + continue + } + break + } + return v +} + +// parseCSV parses bytes of CSV data into a slice slice string or an error +func parseCSV(c []byte, sep string) ([][]string, error) { + if len(sep) != 1 { + return nil, errors.New("Incorrect length of csv separator: " + sep) + } + b := bytes.NewReader(c) + r := csv.NewReader(b) + rSep := []rune(sep) + r.Comma = rSep[0] + r.FieldsPerRecord = 0 + return r.ReadAll() +} + +// getCSV expects a data separator and one or n-parts of a URL to a resource which +// can either be a local or a remote one. +// The data separator can be a comma, semi-colon, pipe, etc, but only one character. +// If you provide multiple parts for the URL they will be joined together to the final URL. +// GetCSV returns nil or a slice slice to use in a short code. +func (t *templateFuncster) getCSV(sep string, urlParts ...string) [][]string { + var d [][]string + url := strings.Join(urlParts, "") + + var clearCacheSleep = func(i int, u string) { + jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep) + time.Sleep(resSleep) + resDeleteCache(url, t.Fs.Source, t.Cfg) + } + + for i := 0; i <= resRetries; i++ { + c, err := t.resGetResource(url) + + if err == nil && !bytes.Contains(c, []byte(sep)) { + err = errors.New("Cannot find separator " + sep + " in CSV.") + } + + if err != nil { + jww.ERROR.Printf("Failed to read csv resource %s with error message %s", url, err) + clearCacheSleep(i, url) + continue + } + + if d, err = parseCSV(c, sep); err != nil { + jww.ERROR.Printf("Failed to parse csv file %s with error message %s", url, err) + clearCacheSleep(i, url) + continue + } + break + } + return d +} diff --git a/tpl/tplimpl/template_resources_test.go b/tpl/tplimpl/template_resources_test.go new file mode 100644 index 00000000..8a9f6265 --- /dev/null +++ b/tpl/tplimpl/template_resources_test.go @@ -0,0 +1,302 @@ +// Copyright 2016 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 tplimpl + +import ( + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/spf13/afero" + "github.com/spf13/hugo/helpers" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestScpCache(t *testing.T) { + t.Parallel() + + tests := []struct { + path string + content []byte + ignore bool + }{ + {"http://Foo.Bar/foo_Bar-Foo", []byte(`T€st Content 123`), false}, + {"fOO,bar:foo%bAR", []byte(`T€st Content 123 fOO,bar:foo%bAR`), false}, + {"FOo/BaR.html", []byte(`FOo/BaR.html T€st Content 123`), false}, + {"трям/трям", []byte(`T€st трям/трям Content 123`), false}, + {"은행", []byte(`T€st C은행ontent 123`), false}, + {"Банковский кассир", []byte(`Банковский кассир T€st Content 123`), false}, + {"Банковский кассир", []byte(`Банковский кассир T€st Content 456`), true}, + } + + fs := new(afero.MemMapFs) + + for _, test := range tests { + cfg := viper.New() + c, err := resGetCache(test.path, fs, cfg, test.ignore) + if err != nil { + t.Errorf("Error getting cache: %s", err) + } + if c != nil { + t.Errorf("There is content where there should not be anything: %s", string(c)) + } + + err = resWriteCache(test.path, test.content, fs, cfg, test.ignore) + if err != nil { + t.Errorf("Error writing cache: %s", err) + } + + c, err = resGetCache(test.path, fs, cfg, test.ignore) + if err != nil { + t.Errorf("Error getting cache after writing: %s", err) + } + if test.ignore { + if c != nil { + t.Errorf("Cache ignored but content is not nil: %s", string(c)) + } + } else { + if !bytes.Equal(c, test.content) { + t.Errorf("\nExpected: %s\nActual: %s\n", string(test.content), string(c)) + } + } + } +} + +func TestScpGetLocal(t *testing.T) { + t.Parallel() + v := viper.New() + fs := hugofs.NewMem(v) + ps := helpers.FilePathSeparator + + tests := []struct { + path string + content []byte + }{ + {"testpath" + ps + "test.txt", []byte(`T€st Content 123 fOO,bar:foo%bAR`)}, + {"FOo" + ps + "BaR.html", []byte(`FOo/BaR.html T€st Content 123`)}, + {"трям" + ps + "трям", []byte(`T€st трям/трям Content 123`)}, + {"은행", []byte(`T€st C은행ontent 123`)}, + {"Банковский кассир", []byte(`Банковский кассир T€st Content 123`)}, + } + + for _, test := range tests { + r := bytes.NewReader(test.content) + err := helpers.WriteToDisk(test.path, r, fs.Source) + if err != nil { + t.Error(err) + } + + c, err := resGetLocal(test.path, fs.Source, v) + if err != nil { + t.Errorf("Error getting resource content: %s", err) + } + if !bytes.Equal(c, test.content) { + t.Errorf("\nExpected: %s\nActual: %s\n", string(test.content), string(c)) + } + } + +} + +func getTestServer(handler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, *http.Client) { + testServer := httptest.NewServer(http.HandlerFunc(handler)) + client := &http.Client{ + Transport: &http.Transport{Proxy: func(r *http.Request) (*url.URL, error) { + // Remove when https://github.com/golang/go/issues/13686 is fixed + r.Host = "gohugo.io" + return url.Parse(testServer.URL) + }}, + } + return testServer, client +} + +func TestScpGetRemote(t *testing.T) { + t.Parallel() + fs := new(afero.MemMapFs) + + tests := []struct { + path string + content []byte + ignore bool + }{ + {"http://Foo.Bar/foo_Bar-Foo", []byte(`T€st Content 123`), false}, + {"http://Doppel.Gänger/foo_Bar-Foo", []byte(`T€st Cont€nt 123`), false}, + {"http://Doppel.Gänger/Fizz_Bazz-Foo", []byte(`T€st Банковский кассир Cont€nt 123`), false}, + {"http://Doppel.Gänger/Fizz_Bazz-Bar", []byte(`T€st Банковский кассир Cont€nt 456`), true}, + } + + for _, test := range tests { + + srv, cl := getTestServer(func(w http.ResponseWriter, r *http.Request) { + w.Write(test.content) + }) + defer func() { srv.Close() }() + + cfg := viper.New() + + c, err := resGetRemote(test.path, fs, cfg, cl) + if err != nil { + t.Errorf("Error getting resource content: %s", err) + } + if !bytes.Equal(c, test.content) { + t.Errorf("\nNet Expected: %s\nNet Actual: %s\n", string(test.content), string(c)) + } + cc, cErr := resGetCache(test.path, fs, cfg, test.ignore) + if cErr != nil { + t.Error(cErr) + } + if test.ignore { + if cc != nil { + t.Errorf("Cache ignored but content is not nil: %s", string(cc)) + } + } else { + if !bytes.Equal(cc, test.content) { + t.Errorf("\nCache Expected: %s\nCache Actual: %s\n", string(test.content), string(cc)) + } + } + } +} + +func TestParseCSV(t *testing.T) { + t.Parallel() + + tests := []struct { + csv []byte + sep string + exp string + err bool + }{ + {[]byte("a,b,c\nd,e,f\n"), "", "", true}, + {[]byte("a,b,c\nd,e,f\n"), "~/", "", true}, + {[]byte("a,b,c\nd,e,f"), "|", "a,b,cd,e,f", false}, + {[]byte("q,w,e\nd,e,f"), ",", "qwedef", false}, + {[]byte("a|b|c\nd|e|f|g"), "|", "abcdefg", true}, + {[]byte("z|y|c\nd|e|f"), "|", "zycdef", false}, + } + for _, test := range tests { + csv, err := parseCSV(test.csv, test.sep) + if test.err && err == nil { + t.Error("Expecting an error") + } + if test.err { + continue + } + if !test.err && err != nil { + t.Error(err) + } + + act := "" + for _, v := range csv { + act = act + strings.Join(v, "") + } + + if act != test.exp { + t.Errorf("\nExpected: %s\nActual: %s\n%#v\n", test.exp, act, csv) + } + + } +} + +func TestGetJSONFailParse(t *testing.T) { + t.Parallel() + + f := newTestFuncster() + + reqCount := 0 + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if reqCount > 0 { + w.Header().Add("Content-type", "application/json") + fmt.Fprintln(w, `{"gomeetup":["Sydney", "San Francisco", "Stockholm"]}`) + } else { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(w, `ERROR 500`) + } + reqCount++ + })) + defer ts.Close() + url := ts.URL + "/test.json" + + want := map[string]interface{}{"gomeetup": []interface{}{"Sydney", "San Francisco", "Stockholm"}} + have := f.getJSON(url) + assert.NotNil(t, have) + if have != nil { + assert.EqualValues(t, want, have) + } +} + +func TestGetCSVFailParseSep(t *testing.T) { + t.Parallel() + f := newTestFuncster() + + reqCount := 0 + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if reqCount > 0 { + w.Header().Add("Content-type", "application/json") + fmt.Fprintln(w, `gomeetup,city`) + fmt.Fprintln(w, `yes,Sydney`) + fmt.Fprintln(w, `yes,San Francisco`) + fmt.Fprintln(w, `yes,Stockholm`) + } else { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(w, `ERROR 500`) + } + reqCount++ + })) + defer ts.Close() + url := ts.URL + "/test.csv" + + want := [][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}} + have := f.getCSV(",", url) + assert.NotNil(t, have) + if have != nil { + assert.EqualValues(t, want, have) + } +} + +func TestGetCSVFailParse(t *testing.T) { + t.Parallel() + + f := newTestFuncster() + + reqCount := 0 + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-type", "application/json") + if reqCount > 0 { + fmt.Fprintln(w, `gomeetup,city`) + fmt.Fprintln(w, `yes,Sydney`) + fmt.Fprintln(w, `yes,San Francisco`) + fmt.Fprintln(w, `yes,Stockholm`) + } else { + fmt.Fprintln(w, `gomeetup,city`) + fmt.Fprintln(w, `yes,Sydney,Bondi,`) // wrong number of fields in line + fmt.Fprintln(w, `yes,San Francisco`) + fmt.Fprintln(w, `yes,Stockholm`) + } + reqCount++ + })) + defer ts.Close() + url := ts.URL + "/test.csv" + + want := [][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}} + have := f.getCSV(",", url) + assert.NotNil(t, have) + if have != nil { + assert.EqualValues(t, want, have) + } +} diff --git a/tpl/tplimpl/template_test.go b/tpl/tplimpl/template_test.go new file mode 100644 index 00000000..08bcab1a --- /dev/null +++ b/tpl/tplimpl/template_test.go @@ -0,0 +1,347 @@ +// Copyright 2016 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 tplimpl + +import ( + "bytes" + "errors" + "html/template" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/spf13/afero" + "github.com/spf13/hugo/deps" + + "github.com/spf13/hugo/tpl" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +// Some tests for Issue #1178 -- Ace +func TestAceTemplates(t *testing.T) { + t.Parallel() + + for i, this := range []struct { + basePath string + innerPath string + baseContent string + innerContent string + expect string + expectErr int + }{ + {"", filepath.FromSlash("_default/single.ace"), "", "{{ . }}", "DATA", 0}, + {filepath.FromSlash("_default/baseof.ace"), filepath.FromSlash("_default/single.ace"), + `= content main + h2 This is a content named "main" of an inner template. {{ . }}`, + `= doctype html +html lang=en + head + meta charset=utf-8 + title Base and Inner Template + body + h1 This is a base template {{ . }} + = yield main`, `Base and Inner Template

This is a base template DATA

`, 0}, + } { + + for _, root := range []string{"", os.TempDir()} { + + basePath := this.basePath + innerPath := this.innerPath + + if basePath != "" && root != "" { + basePath = filepath.Join(root, basePath) + } + + if innerPath != "" && root != "" { + innerPath = filepath.Join(root, innerPath) + } + + d := "DATA" + + config := newDepsConfig(viper.New()) + config.WithTemplate = func(templ tpl.Template) error { + return templ.AddAceTemplate("mytemplate.ace", basePath, innerPath, + []byte(this.baseContent), []byte(this.innerContent)) + } + + a := deps.New(config) + + if err := a.LoadResources(); err != nil { + t.Fatal(err) + } + + templ := a.Tmpl.(*GoHTMLTemplate) + + if len(templ.errors) > 0 && this.expectErr == 0 { + t.Errorf("Test %d with root '%s' errored: %v", i, root, templ.errors) + } else if len(templ.errors) == 0 && this.expectErr == 1 { + t.Errorf("#1 Test %d with root '%s' should have errored", i, root) + } + + var buff bytes.Buffer + err := a.Tmpl.ExecuteTemplate(&buff, "mytemplate.html", d) + + if err != nil && this.expectErr == 0 { + t.Errorf("Test %d with root '%s' errored: %s", i, root, err) + } else if err == nil && this.expectErr == 2 { + t.Errorf("#2 Test with root '%s' %d should have errored", root, i) + } else { + result := buff.String() + if result != this.expect { + t.Errorf("Test %d with root '%s' got\n%s\nexpected\n%s", i, root, result, this.expect) + } + } + + } + } + +} + +func isAtLeastGo16() bool { + version := runtime.Version() + return strings.Contains(version, "1.6") || strings.Contains(version, "1.7") +} + +func TestAddTemplateFileWithMaster(t *testing.T) { + t.Parallel() + + if !isAtLeastGo16() { + t.Skip("This test only runs on Go >= 1.6") + } + + for i, this := range []struct { + masterTplContent string + overlayTplContent string + writeSkipper int + expect interface{} + }{ + {`A{{block "main" .}}C{{end}}C`, `{{define "main"}}B{{end}}`, 0, "ABC"}, + {`A{{block "main" .}}C{{end}}C{{block "sub" .}}D{{end}}E`, `{{define "main"}}B{{end}}`, 0, "ABCDE"}, + {`A{{block "main" .}}C{{end}}C{{block "sub" .}}D{{end}}E`, `{{define "main"}}B{{end}}{{define "sub"}}Z{{end}}`, 0, "ABCZE"}, + {`tpl`, `tpl`, 1, false}, + {`tpl`, `tpl`, 2, false}, + {`{{.0.E}}`, `tpl`, 0, false}, + {`tpl`, `{{.0.E}}`, 0, false}, + } { + + overlayTplName := "ot" + masterTplName := "mt" + finalTplName := "tp" + + config := newDepsConfig(viper.New()) + config.WithTemplate = func(templ tpl.Template) error { + + err := templ.AddTemplateFileWithMaster(finalTplName, overlayTplName, masterTplName) + + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] AddTemplateFileWithMaster didn't return an expected error", i) + } + } else { + + if err != nil { + t.Errorf("[%d] AddTemplateFileWithMaster failed: %s", i, err) + return nil + } + + resultTpl := templ.Lookup(finalTplName) + + if resultTpl == nil { + t.Errorf("[%d] AddTemplateFileWithMaster: Result template not found", i) + return nil + } + + var b bytes.Buffer + err := resultTpl.Execute(&b, nil) + + if err != nil { + t.Errorf("[%d] AddTemplateFileWithMaster execute failed: %s", i, err) + return nil + } + resultContent := b.String() + + if resultContent != this.expect { + t.Errorf("[%d] AddTemplateFileWithMaster got \n%s but expected \n%v", i, resultContent, this.expect) + } + } + + return nil + } + + if this.writeSkipper != 1 { + afero.WriteFile(config.Fs.Source, masterTplName, []byte(this.masterTplContent), 0644) + } + if this.writeSkipper != 2 { + afero.WriteFile(config.Fs.Source, overlayTplName, []byte(this.overlayTplContent), 0644) + } + + deps.New(config) + + } + +} + +// A Go stdlib test for linux/arm. Will remove later. +// See #1771 +func TestBigIntegerFunc(t *testing.T) { + t.Parallel() + var func1 = func(v int64) error { + return nil + } + var funcs = map[string]interface{}{ + "A": func1, + } + + tpl, err := template.New("foo").Funcs(funcs).Parse("{{ A 3e80 }}") + if err != nil { + t.Fatal("Parse failed:", err) + } + err = tpl.Execute(ioutil.Discard, "foo") + + if err == nil { + t.Fatal("Execute should have failed") + } + + t.Log("Got expected error:", err) + +} + +// A Go stdlib test for linux/arm. Will remove later. +// See #1771 +type BI struct { +} + +func (b BI) A(v int64) error { + return nil +} +func TestBigIntegerMethod(t *testing.T) { + t.Parallel() + + data := &BI{} + + tpl, err := template.New("foo2").Parse("{{ .A 3e80 }}") + if err != nil { + t.Fatal("Parse failed:", err) + } + err = tpl.ExecuteTemplate(ioutil.Discard, "foo2", data) + + if err == nil { + t.Fatal("Execute should have failed") + } + + t.Log("Got expected error:", err) + +} + +// Test for bugs discovered by https://github.com/dvyukov/go-fuzz +func TestTplGoFuzzReports(t *testing.T) { + t.Parallel() + + // The following test case(s) also fail + // See https://github.com/golang/go/issues/10634 + //{"{{ seq 433937734937734969526500969526500 }}", 2}} + + for i, this := range []struct { + data string + expectErr int + }{ + // Issue #1089 + //{"{{apply .C \"first\" }}", 2}, + // Issue #1090 + {"{{ slicestr \"000000\" 10}}", 2}, + // Issue #1091 + //{"{{apply .C \"first\" 0 0 0}}", 2}, + {"{{seq 3e80}}", 2}, + // Issue #1095 + {"{{apply .C \"urlize\" " + + "\".\"}}", 2}} { + + d := &Data{ + A: 42, + B: "foo", + C: []int{1, 2, 3}, + D: map[int]string{1: "foo", 2: "bar"}, + E: Data1{42, "foo"}, + F: []string{"a", "b", "c"}, + G: []string{"a", "b", "c", "d", "e"}, + H: "a,b,c,d,e,f", + } + + config := newDepsConfig(viper.New()) + + config.WithTemplate = func(templ tpl.Template) error { + return templ.AddTemplate("fuzz", this.data) + } + + de := deps.New(config) + require.NoError(t, de.LoadResources()) + + templ := de.Tmpl.(*GoHTMLTemplate) + + if len(templ.errors) > 0 && this.expectErr == 0 { + t.Errorf("Test %d errored: %v", i, templ.errors) + } else if len(templ.errors) == 0 && this.expectErr == 1 { + t.Errorf("#1 Test %d should have errored", i) + } + + err := de.Tmpl.ExecuteTemplate(ioutil.Discard, "fuzz", d) + + if err != nil && this.expectErr == 0 { + t.Fatalf("Test %d errored: %s", i, err) + } else if err == nil && this.expectErr == 2 { + t.Fatalf("#2 Test %d should have errored", i) + } + + } +} + +type Data struct { + A int + B string + C []int + D map[int]string + E Data1 + F []string + G []string + H string +} + +type Data1 struct { + A int + B string +} + +func (Data1) Q() string { + return "foo" +} + +func (Data1) W() (string, error) { + return "foo", nil +} + +func (Data1) E() (string, error) { + return "foo", errors.New("Data.E error") +} + +func (Data1) R(v int) (string, error) { + return "foo", nil +} + +func (Data1) T(s string) (string, error) { + return s, nil +} diff --git a/tplapi/template.go b/tplapi/template.go deleted file mode 100644 index 58bc5ecf..00000000 --- a/tplapi/template.go +++ /dev/null @@ -1,28 +0,0 @@ -package tplapi - -import ( - "html/template" - "io" -) - -// TODO(bep) make smaller -// TODO(bep) consider putting this into /tpl and the implementation in /tpl/tplimpl or something -type Template interface { - ExecuteTemplate(wr io.Writer, name string, data interface{}) error - ExecuteTemplateToHTML(context interface{}, layouts ...string) template.HTML - Lookup(name string) *template.Template - Templates() []*template.Template - New(name string) *template.Template - GetClone() *template.Template - LoadTemplates(absPath string) - LoadTemplatesWithPrefix(absPath, prefix string) - AddTemplate(name, tpl string) error - AddTemplateFileWithMaster(name, overlayFilename, masterFilename string) error - AddAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error - AddInternalTemplate(prefix, name, tpl string) error - AddInternalShortcode(name, tpl string) error - Partial(name string, contextList ...interface{}) template.HTML - PrintErrors() - Funcs(funcMap template.FuncMap) - MarkReady() -}