Add support for Go 1.6 block keyword in templates

NOTE: Needs Go 1.6 to use the new feature.

Fixes #1832
This commit is contained in:
Bjørn Erik Pedersen 2016-02-09 18:39:17 +01:00
parent 924028a9be
commit a2abad9677
3 changed files with 192 additions and 28 deletions

View file

@ -462,6 +462,11 @@ func FileContains(filename string, subslice []byte, fs afero.Fs) (bool, error) {
return afero.FileContainsBytes(fs, filename, subslice) 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. // Check if a file or directory exists.
func Exists(path string, fs afero.Fs) (bool, error) { func Exists(path string, fs afero.Fs) (bool, error) {
return afero.Exists(fs, path) return afero.Exists(fs, path)

View file

@ -16,6 +16,7 @@ package tpl
import ( import (
"fmt" "fmt"
"github.com/eknkc/amber" "github.com/eknkc/amber"
"github.com/spf13/afero"
bp "github.com/spf13/hugo/bufferpool" bp "github.com/spf13/hugo/bufferpool"
"github.com/spf13/hugo/helpers" "github.com/spf13/hugo/helpers"
"github.com/spf13/hugo/hugofs" "github.com/spf13/hugo/hugofs"
@ -23,7 +24,6 @@ import (
"github.com/yosssi/ace" "github.com/yosssi/ace"
"html/template" "html/template"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -32,6 +32,8 @@ import (
var localTemplates *template.Template var localTemplates *template.Template
var tmpl Template var tmpl Template
// TODO(bep) an interface with hundreds of methods ... remove it.
// And unexport most of these methods.
type Template interface { type Template interface {
ExecuteTemplate(wr io.Writer, name string, data interface{}) error ExecuteTemplate(wr io.Writer, name string, data interface{}) error
Lookup(name string) *template.Template Lookup(name string) *template.Template
@ -42,6 +44,7 @@ type Template interface {
LoadTemplatesWithPrefix(absPath, prefix string) LoadTemplatesWithPrefix(absPath, prefix string)
MarkReady() MarkReady()
AddTemplate(name, tpl string) error AddTemplate(name, tpl string) error
AddTemplateFileWithMaster(name, overlayFilename, masterFilename string) error
AddAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error AddAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error
AddInternalTemplate(prefix, name, tpl string) error AddInternalTemplate(prefix, name, tpl string) error
AddInternalShortcode(name, tpl string) error AddInternalShortcode(name, tpl string) error
@ -55,7 +58,12 @@ type templateErr struct {
type GoHTMLTemplate struct { type GoHTMLTemplate struct {
template.Template 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 errors []*templateErr
} }
@ -79,6 +87,7 @@ func InitializeT() Template {
func New() Template { func New() Template {
var templates = &GoHTMLTemplate{ var templates = &GoHTMLTemplate{
Template: *template.New(""), Template: *template.New(""),
overlays: make(map[string]*template.Template),
errors: make([]*templateErr, 0), errors: make([]*templateErr, 0),
} }
@ -144,14 +153,20 @@ func Lookup(name string) *template.Template {
func (t *GoHTMLTemplate) Lookup(name string) *template.Template { func (t *GoHTMLTemplate) Lookup(name string) *template.Template {
templ := localTemplates.Lookup(name) if templ := localTemplates.Lookup(name); templ != nil {
if templ != nil {
return templ return templ
} }
if t.overlays != nil {
if templ, ok := t.overlays[name]; ok {
return templ
}
}
if t.clone != nil { if t.clone != nil {
return t.clone.Lookup(name) if templ := t.clone.Lookup(name); templ != nil {
return templ
}
} }
return nil return nil
@ -202,6 +217,53 @@ func (t *GoHTMLTemplate) AddTemplate(name, tpl string) error {
return err 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 { func (t *GoHTMLTemplate) AddAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error {
t.checkState() t.checkState()
var base, inner *ace.File var base, inner *ace.File
@ -248,14 +310,14 @@ func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) er
} }
case ".ace": case ".ace":
var innerContent, baseContent []byte var innerContent, baseContent []byte
innerContent, err := ioutil.ReadFile(path) innerContent, err := afero.ReadFile(hugofs.SourceFs, path)
if err != nil { if err != nil {
return err return err
} }
if baseTemplatePath != "" { if baseTemplatePath != "" {
baseContent, err = ioutil.ReadFile(baseTemplatePath) baseContent, err = afero.ReadFile(hugofs.SourceFs, baseTemplatePath)
if err != nil { if err != nil {
return err return err
} }
@ -263,7 +325,13 @@ func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) er
return t.AddAceTemplate(name, baseTemplatePath, path, baseContent, innerContent) return t.AddAceTemplate(name, baseTemplatePath, path, baseContent, innerContent)
default: default:
b, err := ioutil.ReadFile(path)
if baseTemplatePath != "" {
return t.AddTemplateFileWithMaster(name, path, baseTemplatePath)
}
b, err := afero.ReadFile(hugofs.SourceFs, path)
if err != nil { if err != nil {
return err return err
} }
@ -288,12 +356,13 @@ func isBackupFile(path string) bool {
return path[len(path)-1] == '~' 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 { func isBaseTemplate(path string) bool {
return strings.HasSuffix(path, baseAceFilename) return strings.Contains(path, baseFileBase)
} }
func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {
@ -332,35 +401,44 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {
var baseTemplatePath string var baseTemplatePath string
// ACE templates may have both a base and inner template. // Ace and Go templates may have both a base and inner template.
if filepath.Ext(path) == ".ace" && !strings.HasSuffix(filepath.Dir(path), "partials") { 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 // This may be a view that shouldn't have base template
// Have to look inside it to make sure // 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 { if err != nil {
return err return err
} }
if needsBase { if needsBase {
// Look for base template in the follwing order: // Look for base template in the follwing order:
// 1. <current-path>/<template-name>-baseof.ace, e.g. list-baseof.ace. // 1. <current-path>/<template-name>-baseof.<suffix>, e.g. list-baseof.<suffix>.
// 2. <current-path>/baseof.ace // 2. <current-path>/baseof.<suffix>
// 3. _default/<template-name>-baseof.ace, e.g. list-baseof.ace. // 3. _default/<template-name>-baseof.<suffix>, e.g. list-baseof.<suffix>.
// 4. _default/baseof.ace // 4. _default/baseof.<suffix>
// 5. <themedir>/layouts/_default/<template-name>-baseof.ace // 5. <themedir>/layouts/_default/<template-name>-baseof.<suffix>
// 6. <themedir>/layouts/_default/baseof.ace // 6. <themedir>/layouts/_default/baseof.<suffix>
currBaseAceFilename := fmt.Sprintf("%s-%s", helpers.Filename(path), baseAceFilename) currBaseFilename := fmt.Sprintf("%s-%s", helpers.Filename(path), baseFileName)
templateDir := filepath.Dir(path) templateDir := filepath.Dir(path)
themeDir := helpers.GetThemeDir() themeDir := helpers.GetThemeDir()
pathsToCheck := []string{ pathsToCheck := []string{
filepath.Join(templateDir, currBaseAceFilename), filepath.Join(templateDir, currBaseFilename),
filepath.Join(templateDir, baseAceFilename), filepath.Join(templateDir, baseFileName),
filepath.Join(absPath, "_default", currBaseAceFilename), filepath.Join(absPath, "_default", currBaseFilename),
filepath.Join(absPath, "_default", baseAceFilename), filepath.Join(absPath, "_default", baseFileName),
filepath.Join(themeDir, "layouts", "_default", currBaseAceFilename), filepath.Join(themeDir, "layouts", "_default", currBaseFilename),
filepath.Join(themeDir, "layouts", "_default", baseAceFilename), filepath.Join(themeDir, "layouts", "_default", baseFileName),
} }
for _, pathToCheck := range pathsToCheck { for _, pathToCheck := range pathsToCheck {

View file

@ -16,10 +16,14 @@ package tpl
import ( import (
"bytes" "bytes"
"errors" "errors"
"github.com/spf13/afero"
"github.com/spf13/hugo/hugofs"
"html/template" "html/template"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings"
"testing" "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. // A Go stdlib test for linux/arm. Will remove later.
// See #1771 // See #1771
func TestBigIntegerFunc(t *testing.T) { func TestBigIntegerFunc(t *testing.T) {