From c507e2717df7dd4b870478033bc5ece0b039a8c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Fri, 17 Feb 2017 13:30:50 +0100 Subject: [PATCH] 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 --- deps/deps.go | 8 +- hugolib/embedded_shortcodes_test.go | 6 +- hugolib/hugo_sites.go | 12 +- hugolib/shortcode.go | 4 +- hugolib/shortcode_test.go | 38 +- hugolib/site.go | 10 +- hugolib/sitemap_test.go | 4 +- hugolib/testhelpers_test.go | 6 +- tpl/template.go | 586 +----------------- tpl/{ => tplimpl}/amber_compiler.go | 2 +- tpl/{ => tplimpl}/reflect_helpers.go | 4 +- tpl/tplimpl/template.go | 575 +++++++++++++++++ .../template_ast_transformers.go | 2 +- .../template_ast_transformers_test.go | 2 +- tpl/{ => tplimpl}/template_embedded.go | 2 +- tpl/{ => tplimpl}/template_func_truncate.go | 2 +- .../template_func_truncate_test.go | 2 +- tpl/{ => tplimpl}/template_funcs.go | 2 +- tpl/{ => tplimpl}/template_funcs_test.go | 14 +- tpl/{ => tplimpl}/template_resources.go | 2 +- tpl/{ => tplimpl}/template_resources_test.go | 2 +- tpl/{ => tplimpl}/template_test.go | 10 +- tplapi/template.go | 28 - 23 files changed, 661 insertions(+), 662 deletions(-) rename tpl/{ => tplimpl}/amber_compiler.go (98%) rename tpl/{ => tplimpl}/reflect_helpers.go (96%) create mode 100644 tpl/tplimpl/template.go rename tpl/{ => tplimpl}/template_ast_transformers.go (99%) rename tpl/{ => tplimpl}/template_ast_transformers_test.go (99%) rename tpl/{ => tplimpl}/template_embedded.go (99%) rename tpl/{ => tplimpl}/template_func_truncate.go (99%) rename tpl/{ => tplimpl}/template_func_truncate_test.go (99%) rename tpl/{ => tplimpl}/template_funcs.go (99%) rename tpl/{ => tplimpl}/template_funcs_test.go (99%) rename tpl/{ => tplimpl}/template_resources.go (99%) rename tpl/{ => tplimpl}/template_resources_test.go (99%) rename tpl/{ => tplimpl}/template_test.go (97%) delete mode 100644 tplapi/template.go diff --git a/deps/deps.go b/deps/deps.go index 39a3d31a4..de1b955cb 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 513767eb4..a98ca1369 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 f0feec23e..3cbe4fa90 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 4ce6df7e9..775c57e8c 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 b1f28d53b..665e3a944 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 bd0156849..a5555d0e4 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 8bbcb487b..daefde524 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 33e78e121..f0fcd9530 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/template.go b/tpl/template.go index 9a6364d5a..aaf7fc8c7 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/amber_compiler.go b/tpl/tplimpl/amber_compiler.go similarity index 98% rename from tpl/amber_compiler.go rename to tpl/tplimpl/amber_compiler.go index 4477f6ac0..252c39ffb 100644 --- a/tpl/amber_compiler.go +++ b/tpl/tplimpl/amber_compiler.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tpl +package tplimpl import ( "html/template" diff --git a/tpl/reflect_helpers.go b/tpl/tplimpl/reflect_helpers.go similarity index 96% rename from tpl/reflect_helpers.go rename to tpl/tplimpl/reflect_helpers.go index f2ce722a2..7463683fc 100644 --- a/tpl/reflect_helpers.go +++ b/tpl/tplimpl/reflect_helpers.go @@ -1,4 +1,4 @@ -// Copyright 2016 The Hugo Authors. All rights reserved. +// 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. @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tpl +package tplimpl import ( "reflect" diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go new file mode 100644 index 000000000..cf1fc5620 --- /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/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go similarity index 99% rename from tpl/template_ast_transformers.go rename to tpl/tplimpl/template_ast_transformers.go index 19b772add..68090497b 100644 --- a/tpl/template_ast_transformers.go +++ b/tpl/tplimpl/template_ast_transformers.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tpl +package tplimpl import ( "errors" diff --git a/tpl/template_ast_transformers_test.go b/tpl/tplimpl/template_ast_transformers_test.go similarity index 99% rename from tpl/template_ast_transformers_test.go rename to tpl/tplimpl/template_ast_transformers_test.go index 43d78284c..048d52fee 100644 --- a/tpl/template_ast_transformers_test.go +++ b/tpl/tplimpl/template_ast_transformers_test.go @@ -10,7 +10,7 @@ // 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 +package tplimpl import ( "bytes" diff --git a/tpl/template_embedded.go b/tpl/tplimpl/template_embedded.go similarity index 99% rename from tpl/template_embedded.go rename to tpl/tplimpl/template_embedded.go index f782a31e9..50397b28c 100644 --- a/tpl/template_embedded.go +++ b/tpl/tplimpl/template_embedded.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tpl +package tplimpl type Tmpl struct { Name string diff --git a/tpl/template_func_truncate.go b/tpl/tplimpl/template_func_truncate.go similarity index 99% rename from tpl/template_func_truncate.go rename to tpl/tplimpl/template_func_truncate.go index b5886edae..d4bb63272 100644 --- a/tpl/template_func_truncate.go +++ b/tpl/tplimpl/template_func_truncate.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tpl +package tplimpl import ( "errors" diff --git a/tpl/template_func_truncate_test.go b/tpl/tplimpl/template_func_truncate_test.go similarity index 99% rename from tpl/template_func_truncate_test.go rename to tpl/tplimpl/template_func_truncate_test.go index 9213c6faa..9c4beecff 100644 --- a/tpl/template_func_truncate_test.go +++ b/tpl/tplimpl/template_func_truncate_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tpl +package tplimpl import ( "html/template" diff --git a/tpl/template_funcs.go b/tpl/tplimpl/template_funcs.go similarity index 99% rename from tpl/template_funcs.go rename to tpl/tplimpl/template_funcs.go index 9777bf619..dae621ac3 100644 --- a/tpl/template_funcs.go +++ b/tpl/tplimpl/template_funcs.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tpl +package tplimpl import ( "bytes" diff --git a/tpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go similarity index 99% rename from tpl/template_funcs_test.go rename to tpl/tplimpl/template_funcs_test.go index 35214b649..0fba97bd3 100644 --- a/tpl/template_funcs_test.go +++ b/tpl/tplimpl/template_funcs_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tpl +package tplimpl import ( "bytes" @@ -31,7 +31,7 @@ import ( "testing" "time" - "github.com/spf13/hugo/tplapi" + "github.com/spf13/hugo/tpl" "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" @@ -273,7 +273,7 @@ urlize: bat-man v.Set("CurrentContentLanguage", helpers.NewLanguage("en", v)) config := newDepsConfig(v) - config.WithTemplate = func(templ tplapi.Template) error { + config.WithTemplate = func(templ tpl.Template) error { if _, err := templ.New("test").Parse(in); err != nil { t.Fatal("Got error on parse", err) } @@ -2862,7 +2862,7 @@ func TestPartialCached(t *testing.T) { config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tplapi.Template) error { + config.WithTemplate = func(templ tpl.Template) error { err := templ.AddTemplate("testroot", tmp) if err != nil { return err @@ -2901,7 +2901,7 @@ func TestPartialCached(t *testing.T) { func BenchmarkPartial(b *testing.B) { config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tplapi.Template) error { + config.WithTemplate = func(templ tpl.Template) error { err := templ.AddTemplate("testroot", `{{ partial "bench1" . }}`) if err != nil { return err @@ -2932,7 +2932,7 @@ func BenchmarkPartial(b *testing.B) { func BenchmarkPartialCached(b *testing.B) { config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tplapi.Template) error { + config.WithTemplate = func(templ tpl.Template) error { err := templ.AddTemplate("testroot", `{{ partialCached "bench1" . }}`) if err != nil { return err @@ -2978,7 +2978,7 @@ func newTestFuncsterWithViper(v *viper.Viper) *templateFuncster { func newTestTemplate(t *testing.T, name, template string) *template.Template { config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tplapi.Template) error { + config.WithTemplate = func(templ tpl.Template) error { err := templ.AddTemplate(name, template) if err != nil { return err diff --git a/tpl/template_resources.go b/tpl/tplimpl/template_resources.go similarity index 99% rename from tpl/template_resources.go rename to tpl/tplimpl/template_resources.go index bb1b11eb3..2b3d7120c 100644 --- a/tpl/template_resources.go +++ b/tpl/tplimpl/template_resources.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tpl +package tplimpl import ( "bytes" diff --git a/tpl/template_resources_test.go b/tpl/tplimpl/template_resources_test.go similarity index 99% rename from tpl/template_resources_test.go rename to tpl/tplimpl/template_resources_test.go index 82764977f..8a9f62659 100644 --- a/tpl/template_resources_test.go +++ b/tpl/tplimpl/template_resources_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tpl +package tplimpl import ( "bytes" diff --git a/tpl/template_test.go b/tpl/tplimpl/template_test.go similarity index 97% rename from tpl/template_test.go rename to tpl/tplimpl/template_test.go index 5bb6d89d3..08bcab1a7 100644 --- a/tpl/template_test.go +++ b/tpl/tplimpl/template_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tpl +package tplimpl import ( "bytes" @@ -27,7 +27,7 @@ import ( "github.com/spf13/afero" "github.com/spf13/hugo/deps" - "github.com/spf13/hugo/tplapi" + "github.com/spf13/hugo/tpl" "github.com/spf13/viper" "github.com/stretchr/testify/require" ) @@ -74,7 +74,7 @@ html lang=en d := "DATA" config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tplapi.Template) error { + config.WithTemplate = func(templ tpl.Template) error { return templ.AddAceTemplate("mytemplate.ace", basePath, innerPath, []byte(this.baseContent), []byte(this.innerContent)) } @@ -144,7 +144,7 @@ func TestAddTemplateFileWithMaster(t *testing.T) { finalTplName := "tp" config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tplapi.Template) error { + config.WithTemplate = func(templ tpl.Template) error { err := templ.AddTemplateFileWithMaster(finalTplName, overlayTplName, masterTplName) @@ -284,7 +284,7 @@ func TestTplGoFuzzReports(t *testing.T) { config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tplapi.Template) error { + config.WithTemplate = func(templ tpl.Template) error { return templ.AddTemplate("fuzz", this.data) } diff --git a/tplapi/template.go b/tplapi/template.go deleted file mode 100644 index 58bc5ecf9..000000000 --- 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() -}