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: 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 --- tpl/amber_compiler.go | 42 - tpl/reflect_helpers.go | 70 - tpl/template.go | 586 +---- tpl/template_ast_transformers.go | 259 --- tpl/template_ast_transformers_test.go | 269 --- tpl/template_embedded.go | 266 --- tpl/template_func_truncate.go | 156 -- tpl/template_func_truncate_test.go | 83 - tpl/template_funcs.go | 2217 ------------------ tpl/template_funcs_test.go | 2993 ------------------------- tpl/template_resources.go | 253 --- tpl/template_resources_test.go | 302 --- tpl/template_test.go | 347 --- tpl/tplimpl/amber_compiler.go | 42 + tpl/tplimpl/reflect_helpers.go | 70 + tpl/tplimpl/template.go | 575 +++++ tpl/tplimpl/template_ast_transformers.go | 259 +++ tpl/tplimpl/template_ast_transformers_test.go | 269 +++ tpl/tplimpl/template_embedded.go | 266 +++ tpl/tplimpl/template_func_truncate.go | 156 ++ tpl/tplimpl/template_func_truncate_test.go | 83 + tpl/tplimpl/template_funcs.go | 2217 ++++++++++++++++++ tpl/tplimpl/template_funcs_test.go | 2993 +++++++++++++++++++++++++ tpl/tplimpl/template_resources.go | 253 +++ tpl/tplimpl/template_resources_test.go | 302 +++ tpl/tplimpl/template_test.go | 347 +++ 26 files changed, 7851 insertions(+), 7824 deletions(-) delete mode 100644 tpl/amber_compiler.go delete mode 100644 tpl/reflect_helpers.go delete mode 100644 tpl/template_ast_transformers.go delete mode 100644 tpl/template_ast_transformers_test.go delete mode 100644 tpl/template_embedded.go delete mode 100644 tpl/template_func_truncate.go delete mode 100644 tpl/template_func_truncate_test.go delete mode 100644 tpl/template_funcs.go delete mode 100644 tpl/template_funcs_test.go delete mode 100644 tpl/template_resources.go delete mode 100644 tpl/template_resources_test.go delete mode 100644 tpl/template_test.go create mode 100644 tpl/tplimpl/amber_compiler.go create mode 100644 tpl/tplimpl/reflect_helpers.go create mode 100644 tpl/tplimpl/template.go create mode 100644 tpl/tplimpl/template_ast_transformers.go create mode 100644 tpl/tplimpl/template_ast_transformers_test.go create mode 100644 tpl/tplimpl/template_embedded.go create mode 100644 tpl/tplimpl/template_func_truncate.go create mode 100644 tpl/tplimpl/template_func_truncate_test.go create mode 100644 tpl/tplimpl/template_funcs.go create mode 100644 tpl/tplimpl/template_funcs_test.go create mode 100644 tpl/tplimpl/template_resources.go create mode 100644 tpl/tplimpl/template_resources_test.go create mode 100644 tpl/tplimpl/template_test.go (limited to 'tpl') diff --git a/tpl/amber_compiler.go b/tpl/amber_compiler.go deleted file mode 100644 index 4477f6ac0..000000000 --- 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 f2ce722a2..000000000 --- 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 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/template_ast_transformers.go b/tpl/template_ast_transformers.go deleted file mode 100644 index 19b772add..000000000 --- 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 43d78284c..000000000 --- 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 f782a31e9..000000000 --- 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 b5886edae..000000000 --- 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 9213c6faa..000000000 --- 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 9777bf619..000000000 --- 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 slicest