diff --git a/helpers/path.go b/helpers/path.go index 0fce5690fdc..5536241e52a 100644 --- a/helpers/path.go +++ b/helpers/path.go @@ -462,6 +462,11 @@ func FileContains(filename string, subslice []byte, fs afero.Fs) (bool, error) { return afero.FileContainsBytes(fs, filename, subslice) } +// Check if a file contains any of the specified strings. +func FileContainsAny(filename string, subslices [][]byte, fs afero.Fs) (bool, error) { + return afero.FileContainsAnyBytes(fs, filename, subslices) +} + // Check if a file or directory exists. func Exists(path string, fs afero.Fs) (bool, error) { return afero.Exists(fs, path) diff --git a/tpl/template.go b/tpl/template.go index b8405d580f4..253fee2f40a 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -16,6 +16,7 @@ package tpl import ( "fmt" "github.com/eknkc/amber" + "github.com/spf13/afero" bp "github.com/spf13/hugo/bufferpool" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/hugofs" @@ -23,7 +24,6 @@ import ( "github.com/yosssi/ace" "html/template" "io" - "io/ioutil" "os" "path/filepath" "strings" @@ -32,6 +32,8 @@ import ( var localTemplates *template.Template var tmpl Template +// TODO(bep) an interface with hundreds of methods ... remove it. +// And unexport most of these methods. type Template interface { ExecuteTemplate(wr io.Writer, name string, data interface{}) error Lookup(name string) *template.Template @@ -42,6 +44,7 @@ type Template interface { LoadTemplatesWithPrefix(absPath, prefix string) MarkReady() 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 @@ -55,7 +58,12 @@ type templateErr struct { type GoHTMLTemplate struct { template.Template - clone *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 } @@ -79,6 +87,7 @@ func InitializeT() Template { func New() Template { var templates = &GoHTMLTemplate{ Template: *template.New(""), + overlays: make(map[string]*template.Template), errors: make([]*templateErr, 0), } @@ -144,14 +153,20 @@ func Lookup(name string) *template.Template { func (t *GoHTMLTemplate) Lookup(name string) *template.Template { - templ := localTemplates.Lookup(name) - - if templ != nil { + if templ := localTemplates.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) + if templ := t.clone.Lookup(name); templ != nil { + return templ + } } return nil @@ -202,6 +217,53 @@ func (t *GoHTMLTemplate) AddTemplate(name, tpl string) error { return err } +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(hugofs.SourceFs, 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(hugofs.SourceFs, 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 { + 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 @@ -248,14 +310,14 @@ func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) er } case ".ace": var innerContent, baseContent []byte - innerContent, err := ioutil.ReadFile(path) + innerContent, err := afero.ReadFile(hugofs.SourceFs, path) if err != nil { return err } if baseTemplatePath != "" { - baseContent, err = ioutil.ReadFile(baseTemplatePath) + baseContent, err = afero.ReadFile(hugofs.SourceFs, baseTemplatePath) if err != nil { return err } @@ -263,7 +325,13 @@ func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) er return t.AddAceTemplate(name, baseTemplatePath, path, baseContent, innerContent) default: - b, err := ioutil.ReadFile(path) + + if baseTemplatePath != "" { + return t.AddTemplateFileWithMaster(name, path, baseTemplatePath) + } + + b, err := afero.ReadFile(hugofs.SourceFs, path) + if err != nil { return err } @@ -288,12 +356,13 @@ func isBackupFile(path string) bool { return path[len(path)-1] == '~' } -const baseAceFilename = "baseof.ace" +const baseFileBase = "baseof" -var aceTemplateInnerMarker = []byte("= content") +var aceTemplateInnerMarkers = [][]byte{[]byte("= content")} +var goTemplateInnerMarkers = [][]byte{[]byte("{{define"), []byte("{{ define")} func isBaseTemplate(path string) bool { - return strings.HasSuffix(path, baseAceFilename) + return strings.Contains(path, baseFileBase) } func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { @@ -332,35 +401,44 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { var baseTemplatePath string - // ACE templates may have both a base and inner template. - if filepath.Ext(path) == ".ace" && !strings.HasSuffix(filepath.Dir(path), "partials") { + // Ace and Go templates may have both a base and inner template. + if filepath.Ext(path) != ".amber" && !strings.HasSuffix(filepath.Dir(path), "partials") { + + 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.FileContains(path, aceTemplateInnerMarker, hugofs.OsFs) + needsBase, err := helpers.FileContainsAny(path, innerMarkers, hugofs.OsFs) if err != nil { return err } if needsBase { // Look for base template in the follwing order: - // 1. /-baseof.ace, e.g. list-baseof.ace. - // 2. /baseof.ace - // 3. _default/-baseof.ace, e.g. list-baseof.ace. - // 4. _default/baseof.ace - // 5. /layouts/_default/-baseof.ace - // 6. /layouts/_default/baseof.ace - - currBaseAceFilename := fmt.Sprintf("%s-%s", helpers.Filename(path), baseAceFilename) + // 1. /-baseof., e.g. list-baseof.. + // 2. /baseof. + // 3. _default/-baseof., e.g. list-baseof.. + // 4. _default/baseof. + // 5. /layouts/_default/-baseof. + // 6. /layouts/_default/baseof. + + currBaseFilename := fmt.Sprintf("%s-%s", helpers.Filename(path), baseFileName) templateDir := filepath.Dir(path) themeDir := helpers.GetThemeDir() pathsToCheck := []string{ - filepath.Join(templateDir, currBaseAceFilename), - filepath.Join(templateDir, baseAceFilename), - filepath.Join(absPath, "_default", currBaseAceFilename), - filepath.Join(absPath, "_default", baseAceFilename), - filepath.Join(themeDir, "layouts", "_default", currBaseAceFilename), - filepath.Join(themeDir, "layouts", "_default", baseAceFilename), + filepath.Join(templateDir, currBaseFilename), + filepath.Join(templateDir, baseFileName), + filepath.Join(absPath, "_default", currBaseFilename), + filepath.Join(absPath, "_default", baseFileName), + filepath.Join(themeDir, "layouts", "_default", currBaseFilename), + filepath.Join(themeDir, "layouts", "_default", baseFileName), } for _, pathToCheck := range pathsToCheck { diff --git a/tpl/template_test.go b/tpl/template_test.go index fbc088dc768..073029fee3a 100644 --- a/tpl/template_test.go +++ b/tpl/template_test.go @@ -16,10 +16,14 @@ package tpl import ( "bytes" "errors" + "github.com/spf13/afero" + "github.com/spf13/hugo/hugofs" "html/template" "io/ioutil" "os" "path/filepath" + "runtime" + "strings" "testing" ) @@ -92,6 +96,83 @@ html lang=en } +func isAtLeastGo16() bool { + version := runtime.Version() + return strings.Contains(version, "1.6") || strings.Contains(version, "1.7") +} + +func TestAddTemplateFileWithMaster(t *testing.T) { + + if !isAtLeastGo16() { + t.Skip("This test only runs on Go >= 1.6") + } + + for i, this := range []struct { + masterTplContent string + overlayTplContent string + writeSkipper int + expect interface{} + }{ + {`A{{block "main" .}}C{{end}}C`, `{{define "main"}}B{{end}}`, 0, "ABC"}, + {`A{{block "main" .}}C{{end}}C{{block "sub" .}}D{{end}}E`, `{{define "main"}}B{{end}}`, 0, "ABCDE"}, + {`A{{block "main" .}}C{{end}}C{{block "sub" .}}D{{end}}E`, `{{define "main"}}B{{end}}{{define "sub"}}Z{{end}}`, 0, "ABCZE"}, + {`tpl`, `tpl`, 1, false}, + {`tpl`, `tpl`, 2, false}, + {`{{.0.E}}`, `tpl`, 0, false}, + {`tpl`, `{{.0.E}}`, 0, false}, + } { + + hugofs.SourceFs = afero.NewMemMapFs() + templ := New() + overlayTplName := "ot" + masterTplName := "mt" + finalTplName := "tp" + + if this.writeSkipper != 1 { + afero.WriteFile(hugofs.SourceFs, masterTplName, []byte(this.masterTplContent), 0644) + } + if this.writeSkipper != 2 { + afero.WriteFile(hugofs.SourceFs, overlayTplName, []byte(this.overlayTplContent), 0644) + } + + err := templ.AddTemplateFileWithMaster(finalTplName, overlayTplName, masterTplName) + + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] AddTemplateFileWithMaster didn't return an expected error", i) + } + } else { + + if err != nil { + t.Errorf("[%d] AddTemplateFileWithMaster failed: %s", i, err) + continue + } + + resultTpl := templ.Lookup(finalTplName) + + if resultTpl == nil { + t.Errorf("[%d] AddTemplateFileWithMaster: Result teamplate not found") + continue + } + + var b bytes.Buffer + err := resultTpl.Execute(&b, nil) + + if err != nil { + t.Errorf("[%d] AddTemplateFileWithMaster execute failed: %s", i, err) + continue + } + resultContent := b.String() + + if resultContent != this.expect { + t.Errorf("[%d] AddTemplateFileWithMaster got \n%s but expected \n%v", i, resultContent, this.expect) + } + } + + } + +} + // A Go stdlib test for linux/arm. Will remove later. // See #1771 func TestBigIntegerFunc(t *testing.T) {