// Copyright 2017-present The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tplimpl import ( "html/template" "strings" texttemplate "text/template" "github.com/eknkc/amber" "os" "github.com/spf13/hugo/output" "path/filepath" "sync" "github.com/spf13/afero" "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/tpl" ) const ( textTmplNamePrefix = "_text/" ) var ( _ tpl.TemplateHandler = (*templateHandler)(nil) _ tpl.TemplateTestMocker = (*templateHandler)(nil) _ tpl.TemplateFinder = (*htmlTemplates)(nil) _ tpl.TemplateFinder = (*textTemplates)(nil) _ templateLoader = (*htmlTemplates)(nil) _ templateLoader = (*textTemplates)(nil) _ templateLoader = (*templateHandler)(nil) _ templateFuncsterTemplater = (*htmlTemplates)(nil) _ templateFuncsterTemplater = (*textTemplates)(nil) ) // Protecting global map access (Amber) var amberMu sync.Mutex type templateErr struct { name string err error } type templateLoader interface { handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error addTemplate(name, tpl string) error addLateTemplate(name, tpl string) error } type templateFuncsterTemplater interface { tpl.TemplateFinder setFuncs(funcMap map[string]interface{}) setTemplateFuncster(f *templateFuncster) } // templateHandler holds the templates in play. // It implements the templateLoader and tpl.TemplateHandler interfaces. type templateHandler struct { // text holds all the pure text templates. text *textTemplates html *htmlTemplates amberFuncMap template.FuncMap errors []*templateErr *deps.Deps } func (t *templateHandler) addError(name string, err error) { t.errors = append(t.errors, &templateErr{name, err}) } // PrintErrors prints the accumulated errors as ERROR to the log. func (t *templateHandler) PrintErrors() { for _, e := range t.errors { t.Log.ERROR.Println(e.name, ":", e.err) } } // Lookup tries to find a template with the given name in both template // collections: First HTML, then the plain text template collection. func (t *templateHandler) Lookup(name string) *tpl.TemplateAdapter { var te *tpl.TemplateAdapter isTextTemplate := strings.HasPrefix(name, textTmplNamePrefix) if isTextTemplate { // The templates are stored without the prefix identificator. name = strings.TrimPrefix(name, textTmplNamePrefix) te = t.text.Lookup(name) } else { te = t.html.Lookup(name) } if te == nil { return nil } return te } func (t *templateHandler) clone(d *deps.Deps) *templateHandler { c := &templateHandler{ Deps: d, html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template)}, text: &textTemplates{t: texttemplate.Must(t.text.t.Clone()), overlays: make(map[string]*texttemplate.Template)}, errors: make([]*templateErr, 0), } d.Tmpl = c c.initFuncs() for k, v := range t.html.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(c.html.funcster.funcMap) c.html.overlays[k] = vc } for k, v := range t.text.overlays { vc := texttemplate.Must(v.Clone()) vc = vc.Lookup(vc.Name()) vc.Funcs(texttemplate.FuncMap(c.text.funcster.funcMap)) c.text.overlays[k] = vc } return c } func newTemplateAdapter(deps *deps.Deps) *templateHandler { htmlT := &htmlTemplates{ t: template.New(""), overlays: make(map[string]*template.Template), } textT := &textTemplates{ t: texttemplate.New(""), overlays: make(map[string]*texttemplate.Template), } return &templateHandler{ Deps: deps, html: htmlT, text: textT, errors: make([]*templateErr, 0), } } type htmlTemplates struct { funcster *templateFuncster t *template.Template // This looks, and is, strange. // The clone is used by non-renderable content pages, and these need to be // re-parsed on content change, and to avoid the // "cannot Parse after Execute" error, we need to re-clone it from the original clone. clone *template.Template cloneClone *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 } func (t *htmlTemplates) setTemplateFuncster(f *templateFuncster) { t.funcster = f } func (t *htmlTemplates) Lookup(name string) *tpl.TemplateAdapter { templ := t.lookup(name) if templ == nil { return nil } return &tpl.TemplateAdapter{Template: templ} } func (t *htmlTemplates) lookup(name string) *template.Template { if templ := t.t.Lookup(name); templ != nil { return templ } if t.overlays != nil { if templ, ok := t.overlays[name]; ok { return templ } } if t.clone != nil { return t.clone.Lookup(name) } return nil } type textTemplates struct { funcster *templateFuncster t *texttemplate.Template clone *texttemplate.Template cloneClone *texttemplate.Template overlays map[string]*texttemplate.Template } func (t *textTemplates) setTemplateFuncster(f *templateFuncster) { t.funcster = f } func (t *textTemplates) Lookup(name string) *tpl.TemplateAdapter { templ := t.lookup(name) if templ == nil { return nil } return &tpl.TemplateAdapter{Template: templ} } func (t *textTemplates) lookup(name string) *texttemplate.Template { if templ := t.t.Lookup(name); templ != nil { return templ } if t.overlays != nil { if templ, ok := t.overlays[name]; ok { return templ } } if t.clone != nil { return t.clone.Lookup(name) } return nil } func (t *templateHandler) setFuncs(funcMap map[string]interface{}) { t.html.setFuncs(funcMap) t.text.setFuncs(funcMap) } // SetFuncs replaces the funcs in the func maps with new definitions. // This is only used in tests. func (t *templateHandler) SetFuncs(funcMap map[string]interface{}) { t.setFuncs(funcMap) } func (t *htmlTemplates) setFuncs(funcMap map[string]interface{}) { t.t.Funcs(funcMap) } func (t *textTemplates) setFuncs(funcMap map[string]interface{}) { t.t.Funcs(funcMap) } // LoadTemplates loads the templates, starting from the given absolute path. // A prefix can be given to indicate a template namespace to load the templates // into, i.e. "_internal" etc. func (t *templateHandler) LoadTemplates(absPath, prefix string) { // TODO(bep) output formats. Will have to get to complete list when that is ready. t.loadTemplates(absPath, prefix, output.Formats{output.HTMLFormat, output.RSSFormat, output.CalendarFormat, output.AMPFormat, output.JSONFormat}) } func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) error { templ, err := tt.New(name).Parse(tpl) if err != nil { return err } if err := applyTemplateTransformersToHMLTTemplate(templ); err != nil { return err } return nil } func (t *htmlTemplates) addTemplate(name, tpl string) error { return t.addTemplateIn(t.t, name, tpl) } func (t *htmlTemplates) addLateTemplate(name, tpl string) error { return t.addTemplateIn(t.clone, name, tpl) } func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) error { name = strings.TrimPrefix(name, textTmplNamePrefix) templ, err := tt.New(name).Parse(tpl) if err != nil { return err } if err := applyTemplateTransformersToTextTemplate(templ); err != nil { return err } return nil } func (t *textTemplates) addTemplate(name, tpl string) error { return t.addTemplateIn(t.t, name, tpl) } func (t *textTemplates) addLateTemplate(name, tpl string) error { return t.addTemplateIn(t.clone, name, tpl) } func (t *templateHandler) addTemplate(name, tpl string) error { return t.AddTemplate(name, tpl) } func (t *templateHandler) addLateTemplate(name, tpl string) error { return t.AddLateTemplate(name, tpl) } // AddLateTemplate is used to add a template late, i.e. after the // regular templates have started its execution. func (t *templateHandler) AddLateTemplate(name, tpl string) error { h := t.getTemplateHandler(name) if err := h.addLateTemplate(name, tpl); err != nil { t.addError(name, err) return err } return nil } // AddTemplate parses and adds a template to the collection. // Templates with name prefixed with "_text" will be handled as plain // text templates. func (t *templateHandler) AddTemplate(name, tpl string) error { h := t.getTemplateHandler(name) if err := h.addTemplate(name, tpl); err != nil { t.addError(name, err) return err } return nil } // MarkReady marks the templates as "ready for execution". No changes allowed // after this is set. // TODO(bep) if this proves to be resource heavy, we could detect // earlier if we really need this, or make it lazy. func (t *templateHandler) MarkReady() { if t.html.clone == nil { t.html.clone = template.Must(t.html.t.Clone()) t.html.cloneClone = template.Must(t.html.clone.Clone()) } if t.text.clone == nil { t.text.clone = texttemplate.Must(t.text.t.Clone()) t.text.cloneClone = texttemplate.Must(t.text.clone.Clone()) } } // RebuildClone rebuilds the cloned templates. Used for live-reloads. func (t *templateHandler) RebuildClone() { t.html.clone = template.Must(t.html.cloneClone.Clone()) t.text.clone = texttemplate.Must(t.text.cloneClone.Clone()) } func (t *templateHandler) loadTemplates(absPath string, prefix string, formats output.Formats) { 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 } var ( workingDir = t.PathSpec.WorkingDir() themeDir = t.PathSpec.GetThemeDir() layoutDir = t.PathSpec.LayoutDir() ) if themeDir != "" && strings.HasPrefix(absPath, themeDir) { workingDir = themeDir layoutDir = "layouts" } li := strings.LastIndex(path, layoutDir) + len(layoutDir) + 1 relPath := path[li:] descriptor := output.TemplateLookupDescriptor{ WorkingDir: workingDir, LayoutDir: layoutDir, RelPath: relPath, Prefix: prefix, Theme: t.PathSpec.Theme(), OutputFormats: formats, FileExists: func(filename string) (bool, error) { return helpers.Exists(filename, t.Fs.Source) }, ContainsAny: func(filename string, subslices [][]byte) (bool, error) { return helpers.FileContainsAny(filename, subslices, t.Fs.Source) }, } tplID, err := output.CreateTemplateNames(descriptor) if err != nil { t.Log.ERROR.Printf("Failed to resolve template in path %q: %s", path, err) return nil } if err := t.addTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil { t.Log.ERROR.Printf("Failed to add template %q in path %q: %s", tplID.Name, 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 (t *templateHandler) initFuncs() { // The template funcs need separation between text and html templates. for _, funcsterHolder := range []templateFuncsterTemplater{t.html, t.text} { funcster := newTemplateFuncster(t.Deps, funcsterHolder) // The URL funcs in the funcMap is somewhat language dependent, // so we need to wait until the language and site config is loaded. funcster.initFuncMap() funcsterHolder.setTemplateFuncster(funcster) } // Amber is HTML only. t.amberFuncMap = template.FuncMap{} amberMu.Lock() for k, v := range amber.FuncMap { t.amberFuncMap[k] = v } for k, v := range t.html.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 *templateHandler) getTemplateHandler(name string) templateLoader { if strings.HasPrefix(name, textTmplNamePrefix) { return t.text } return t.html } func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { h := t.getTemplateHandler(name) return h.handleMaster(name, overlayFilename, masterFilename, onMissing) } func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { masterTpl := t.lookup(masterFilename) if masterTpl == nil { templ, err := onMissing(masterFilename) if err != nil { return err } masterTpl, err = t.t.New(overlayFilename).Parse(templ) if err != nil { return err } } templ, err := onMissing(overlayFilename) if err != nil { return err } overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ) if err != nil { return err } // 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 := applyTemplateTransformersToHMLTTemplate(overlayTpl); err != nil { return err } t.overlays[name] = overlayTpl return err } func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { name = strings.TrimPrefix(name, textTmplNamePrefix) masterTpl := t.lookup(masterFilename) if masterTpl == nil { templ, err := onMissing(masterFilename) if err != nil { return err } masterTpl, err = t.t.New(overlayFilename).Parse(templ) if err != nil { return err } } templ, err := onMissing(overlayFilename) if err != nil { return err } overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ) if err != nil { return err } overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) if err := applyTemplateTransformersToTextTemplate(overlayTpl); err != nil { return err } t.overlays[name] = overlayTpl return err } func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) error { t.checkState() getTemplate := func(filename string) (string, error) { b, err := afero.ReadFile(t.Fs.Source, filename) if err != nil { return "", err } return string(b), nil } // get the suffix and switch on that ext := filepath.Ext(path) switch ext { case ".amber": // Only HTML support for 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.html.t.New(templateName)) amberMu.Unlock() if err != nil { return err } return applyTemplateTransformersToHMLTTemplate(templ) case ".ace": // Only HTML support for 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.handleMaster(name, path, baseTemplatePath, getTemplate) } templ, err := getTemplate(path) if err != nil { return err } t.Log.DEBUG.Printf("Add template file from path %s", path) return t.AddTemplate(name, templ) } } func (t *templateHandler) loadEmbedded() { t.embedShortcodes() t.embedTemplates() } func (t *templateHandler) addInternalTemplate(prefix, name, tpl string) error { if prefix != "" { return t.AddTemplate("_internal/"+prefix+"/"+name, tpl) } return t.AddTemplate("_internal/"+name, tpl) } func (t *templateHandler) addInternalShortcode(name, content string) error { return t.addInternalTemplate("shortcodes", name, content) } func (t *templateHandler) checkState() { if t.html.clone != nil || t.text.clone != nil { panic("template is cloned and cannot be modfified") } } func isDotFile(path string) bool { return filepath.Base(path)[0] == '.' } func isBackupFile(path string) bool { return path[len(path)-1] == '~' } const baseFileBase = "baseof" func isBaseTemplate(path string) bool { return strings.Contains(path, baseFileBase) }