Skip to content

Commit

Permalink
Add support for Go 1.6 block keyword in templates
Browse files Browse the repository at this point in the history
NOTE: Needs Go 1.6 to use the new feature.

Fixes gohugoio#1832
  • Loading branch information
bep authored and tychoish committed Aug 13, 2017
1 parent 1076b0d commit d6c9c13
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 29 deletions.
5 changes: 5 additions & 0 deletions helpers/path.go
Expand Up @@ -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)
Expand Down
136 changes: 107 additions & 29 deletions tpl/template.go
Expand Up @@ -16,14 +16,14 @@ 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"
jww "github.com/spf13/jwalterweatherman"
"github.com/yosssi/ace"
"html/template"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}

Expand All @@ -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),
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -248,22 +310,28 @@ 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
}
}

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
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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. <current-path>/<template-name>-baseof.ace, e.g. list-baseof.ace.
// 2. <current-path>/baseof.ace
// 3. _default/<template-name>-baseof.ace, e.g. list-baseof.ace.
// 4. _default/baseof.ace
// 5. <themedir>/layouts/_default/<template-name>-baseof.ace
// 6. <themedir>/layouts/_default/baseof.ace

currBaseAceFilename := fmt.Sprintf("%s-%s", helpers.Filename(path), baseAceFilename)
// 1. <current-path>/<template-name>-baseof.<suffix>, e.g. list-baseof.<suffix>.
// 2. <current-path>/baseof.<suffix>
// 3. _default/<template-name>-baseof.<suffix>, e.g. list-baseof.<suffix>.
// 4. _default/baseof.<suffix>
// 5. <themedir>/layouts/_default/<template-name>-baseof.<suffix>
// 6. <themedir>/layouts/_default/baseof.<suffix>

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 {
Expand Down
81 changes: 81 additions & 0 deletions tpl/template_test.go
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit d6c9c13

Please sign in to comment.