diff --git a/.travis.yml b/.travis.yml index e1e04f3..b15be07 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,11 +11,14 @@ branches: go: - 1.9 - - "1.10" + - 1.x - tip go_import_path: aahframework.org/view.v0 +before_install: + - bash <(curl -s https://aahframework.org/base-before-install) "vfs forge config essentials log" + install: - go get -t -v ./... diff --git a/README.md b/README.md index c8dd29b..b74f283 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,21 @@ -# view - aah framework -[![Build Status](https://travis-ci.org/go-aah/view.svg?branch=master)](https://travis-ci.org/go-aah/view) [![codecov](https://codecov.io/gh/go-aah/view/branch/master/graph/badge.svg)](https://codecov.io/gh/go-aah/view/branch/master) [![Go Report Card](https://goreportcard.com/badge/aahframework.org/view.v0)](https://goreportcard.com/report/aahframework.org/view.v0) [![Version](https://img.shields.io/badge/version-0.8.2-blue.svg)](https://github.com/go-aah/view/releases/latest) [![GoDoc](https://godoc.org/aahframework.org/view.v0?status.svg)](https://godoc.org/aahframework.org/view.v0) [![License](https://img.shields.io/github/license/go-aah/view.svg)](LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@aahframework-55acee.svg)](https://twitter.com/aahframework) +

+ +

View Engine library by aah framework

+

+

+

Build Status Code Coverage Go Report Card Release Version Godoc Twitter @aahframework

+

-***v0.8.2 [released](https://github.com/go-aah/view/releases/latest) and tagged on Apr 25, 2018*** +View Engine library provides enhanced Go template engine which supports partial template inheritance, imports, etc. -Go HTML template library which supports partial template inheritance, imports, etc. +### News -*`view` developed for aah framework. However, it's an independent library, can be used separately with any `Go` language project. Feel free to use it.* + * `v0.9.0` [released](https://github.com/go-aah/view/releases/latest) and tagged on Jul 06, 2018. + +## Installation -# Installation -#### Stable Version - Production Ready ```bash -# install the library go get -u aahframework.org/view.v0 ``` -Visit official website https://aahframework.org to learn more. +Visit official website https://aahframework.org to learn more about `aah` framework. diff --git a/anti_csrf_field.go b/anti_csrf_field.go deleted file mode 100644 index 8f000a6..0000000 --- a/anti_csrf_field.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/view source code and usage is governed by a MIT style -// license that can be found in the LICENSE file. - -package view - -import ( - "fmt" - "io/ioutil" - "path/filepath" - "strings" - - "aahframework.org/essentials.v0" - "aahframework.org/log.v0" -) - -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// type AntiCSRFField and methods -//________________________________________ - -// AntiCSRFField is used to insert Anti-CSRF HTML field dynamically -// while parsing templates on view engine. -type AntiCSRFField struct { - engineName string - field string - inserter *strings.Replacer - leftDelim string - rightDelim string -} - -// NewAntiCSRFField method creates new instance of Anti-CSRF HTML field -// parser. -func NewAntiCSRFField(engineName, leftDelim, rightDelim string) *AntiCSRFField { - csft := &AntiCSRFField{engineName: engineName, leftDelim: leftDelim, rightDelim: rightDelim} - - csft.field = fmt.Sprintf(` - `, csft.leftDelim, csft.rightDelim) - csft.inserter = strings.NewReplacer("", csft.field) - - return csft -} - -// InsertOnFile method inserts the Anti-CSRF HTML field for given HTML file and -// writes a processed file into temp directory then return the new file path. -func (ft *AntiCSRFField) InsertOnFiles(files ...string) []string { - var ofiles []string - - for _, f := range files { - fpath, err := ft.InsertOnFile(f) - if err != nil { - log.Errorf("anitcsrffield: unable to insert Anti-CSRF field for file: %s", f) - ofiles = append(ofiles, f) - continue - } - ofiles = append(ofiles, fpath) - } - - return ofiles -} - -// InsertOnFile method inserts the Anti-CSRF HTML filed for given HTML file and -// writes a processed file into temp directory then return the new file path. -func (ft *AntiCSRFField) InsertOnFile(file string) (string, error) { - tmpDir, _ := ioutil.TempDir("", ft.engineName+"_anti_csrf") - - fileBytes, err := ioutil.ReadFile(file) - if err != nil { - return "", err - } - - fileStr := string(fileBytes) - f := StripPathPrefixAt(file, "views") - fpath := filepath.Join(tmpDir, f) - if strings.Contains(fileStr, "") { - log.Tracef("Inserting Anti-CSRF field for file: %s", filepath.Join("views", f)) - fileStr = ft.InsertOnString(fileStr) - if err = ess.MkDirAll(filepath.Dir(fpath), 0755); err != nil { - return "", err - } - - if err = ioutil.WriteFile(fpath, []byte(fileStr), 0755); err != nil { - return "", err - } - - return fpath, nil - } - - return file, nil -} - -// InsertOnString method inserts the Anti-CSRF HTML field on -// given HTML string and returns the processed HTML string. -func (ft *AntiCSRFField) InsertOnString(str string) string { - return ft.inserter.Replace(str) -} diff --git a/anti_csrf_field_test.go b/anti_csrf_field_test.go deleted file mode 100644 index 5c88d40..0000000 --- a/anti_csrf_field_test.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/view source code and usage is governed by a MIT style -// license that can be found in the LICENSE file. - -package view - -import ( - "io/ioutil" - "path/filepath" - "strings" - "testing" - - "aahframework.org/test.v0/assert" -) - -func TestAntiCSRFFieldNoFormTag(t *testing.T) { - acsrf := NewAntiCSRFField("go", "{{", "}}") - fpath := filepath.Join(getTestdataPath(), "anti-csrf-field", "testhtml-noform.html") - - files := acsrf.InsertOnFiles(fpath) - bytes, err := ioutil.ReadFile(files[0]) - assert.Nil(t, err) - assert.False(t, strings.Contains(string(bytes), "{{ anti_csrf_token . }}")) -} - -func TestAntiCSRFFieldFormTag(t *testing.T) { - acsrf := NewAntiCSRFField("go", "%%", "%%") - fpath := filepath.Join(getTestdataPath(), "anti-csrf-field", "testhtml-form.html") - - files := acsrf.InsertOnFiles(fpath) - bytes, err := ioutil.ReadFile(files[0]) - assert.Nil(t, err) - assert.True(t, strings.Contains(string(bytes), "%% anitcsrftoken . %%")) -} - -func TestAntiCSRFFieldFormTagDelim(t *testing.T) { - acsrf := NewAntiCSRFField("go", "[[", "]]") - fpath := filepath.Join(getTestdataPath(), "anti-csrf-field", "not-exists.html") - - files := acsrf.InsertOnFiles(fpath) - assert.NotNil(t, files) - assert.Equal(t, fpath, files[0]) -} diff --git a/funcs.go b/funcs.go index 5fa22fa..b372173 100644 --- a/funcs.go +++ b/funcs.go @@ -1,5 +1,5 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/view source code and usage is governed by a MIT style +// aahframework.org/view source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package view @@ -13,30 +13,40 @@ import ( ) // tmplSafeHTML method outputs given HTML as-is, use it with care. -func tmplSafeHTML(str string) template.HTML { +func (e *GoViewEngine) tmplSafeHTML(str string) template.HTML { return template.HTML(str) } // tmplInclude method renders given template with View Args and imports into // current template. -func tmplInclude(name string, viewArgs map[string]interface{}) template.HTML { +func (e *GoViewEngine) tmplInclude(name string, viewArgs map[string]interface{}) template.HTML { if !strings.HasPrefix(name, "common") { name = "common/" + name } + name = filepath.ToSlash(name) + var err error + var tmpl *template.Template + if e.hotReload { + if tmpl, err = e.ParseFile(name); err != nil { + log.Errorf("goviewengine: %s", err) + return e.tmplSafeHTML("") + } + } else { + tmpl = commonTemplates.Lookup(name) + } - tmpl := commonTemplates.Lookup(name) if tmpl == nil { log.Warnf("goviewengine: common template not found: %s", name) - return tmplSafeHTML("") + return e.tmplSafeHTML("") } buf := acquireBuffer() defer releaseBuffer(buf) - if err := tmpl.Execute(buf, viewArgs); err != nil { - log.Error(err) - return template.HTML("") + if err = tmpl.Execute(buf, viewArgs); err != nil { + log.Errorf("goviewengine: %s", err) + return e.tmplSafeHTML("") } - return tmplSafeHTML(buf.String()) + return e.tmplSafeHTML(buf.String()) } diff --git a/go_engine.go b/go_engine.go index 477b130..c6ed1cc 100644 --- a/go_engine.go +++ b/go_engine.go @@ -1,5 +1,5 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/view source code and usage is governed by a MIT style +// aahframework.org/view source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package view @@ -7,14 +7,14 @@ package view import ( "bytes" "html/template" - "io/ioutil" + "path" "path/filepath" "strings" "sync" "aahframework.org/config.v0" - "aahframework.org/essentials.v0" "aahframework.org/log.v0" + "aahframework.org/vfs.v0" ) const noLayout = "nolayout" @@ -24,9 +24,9 @@ var ( bufPool *sync.Pool ) -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // type GoViewEngine and its method -//___________________________________ +//______________________________________________________________________________ // GoViewEngine implements the partial inheritance support with Go templates. type GoViewEngine struct { @@ -35,19 +35,20 @@ type GoViewEngine struct { // Init method initialize a template engine with given aah application config // and application views base path. -func (e *GoViewEngine) Init(appCfg *config.Config, baseDir string) error { +func (e *GoViewEngine) Init(fs *vfs.VFS, appCfg *config.Config, baseDir string) error { if e.EngineBase == nil { - e.EngineBase = &EngineBase{} + e.EngineBase = new(EngineBase) } - if err := e.EngineBase.Init(appCfg, baseDir, "go", ".html"); err != nil { + if err := e.EngineBase.Init(fs, appCfg, baseDir, "go", ".html"); err != nil { return err } // Add template func AddTemplateFunc(template.FuncMap{ - "import": tmplInclude, - "include": tmplInclude, // alias for import + "safeHTML": e.tmplSafeHTML, + "import": e.tmplInclude, + "include": e.tmplInclude, // alias for import }) // load common templates @@ -71,7 +72,7 @@ func (e *GoViewEngine) Init(appCfg *config.Config, baseDir string) error { _ = e.loadNonLayoutTemplates("pages") } - if ess.IsFileExists(filepath.Join(e.BaseDir, "errors")) { + if e.VFS.IsExists(filepath.Join(e.BaseDir, "errors")) { if err = e.loadNonLayoutTemplates("errors"); err != nil { return err } @@ -80,9 +81,9 @@ func (e *GoViewEngine) Init(appCfg *config.Config, baseDir string) error { return nil } -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // GoViewEngine unexported methods -//___________________________________ +//______________________________________________________________________________ func (e *GoViewEngine) loadCommonTemplates() error { commons, err := e.FilesPath("common") @@ -92,27 +93,19 @@ func (e *GoViewEngine) loadCommonTemplates() error { commonTemplates = &Templates{} bufPool = &sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} - prefix := filepath.Dir(e.BaseDir) + prefix := path.Dir(e.BaseDir) for _, file := range commons { if !strings.HasSuffix(file, e.FileExt) { log.Warnf("goviewengine: not a valid template extension[%s]: %s", e.FileExt, TrimPathPrefix(prefix, file)) continue } - tmplKey := StripPathPrefixAt(filepath.ToSlash(file), "views/") - tmpl := e.NewTemplate(tmplKey) - - tbytes, err := ioutil.ReadFile(file) + log.Tracef("Parsing file: %s", TrimPathPrefix(prefix, file)) + tmpl, err := e.ParseFile(file) if err != nil { return err } - - tstr := e.AntiCSRFField.InsertOnString(string(tbytes)) - if tmpl, err = tmpl.Parse(tstr); err != nil { - return err - } - - if err = commonTemplates.Add(tmplKey, tmpl); err != nil { + if err = commonTemplates.Add(tmpl.Name(), tmpl); err != nil { return err } } @@ -126,30 +119,28 @@ func (e *GoViewEngine) loadLayoutTemplates(layouts []string) error { return err } - prefix := filepath.Dir(e.BaseDir) + prefix := path.Dir(e.BaseDir) var errs []error for _, layout := range layouts { - layoutKey := strings.ToLower(filepath.Base(layout)) + layoutKey := strings.ToLower(path.Base(layout)) for _, dir := range dirs { - files, err := filepath.Glob(filepath.Join(dir, "*"+e.FileExt)) + files, err := e.VFS.Glob(path.Join(dir, "*"+e.FileExt)) if err != nil { errs = append(errs, err) continue } for _, file := range files { - tfiles := []string{layout, file} tmplKey := StripPathPrefixAt(filepath.ToSlash(file), "views/") tmpl := e.NewTemplate(tmplKey) - tmplfiles := e.AntiCSRFField.InsertOnFiles(tfiles...) + tfiles := []string{layout, file} log.Tracef("Parsing files: %s", TrimPathPrefix(prefix, tfiles...)) - if tmpl, err = tmpl.ParseFiles(tmplfiles...); err != nil { + if _, err = e.ParseFiles(tmpl, tfiles...); err != nil { errs = append(errs, err) continue } - if err = e.AddTemplate(layoutKey, tmplKey, tmpl); err != nil { errs = append(errs, err) continue @@ -167,10 +158,10 @@ func (e *GoViewEngine) loadNonLayoutTemplates(scope string) error { return err } - prefix := filepath.Dir(e.BaseDir) + prefix := path.Dir(e.BaseDir) var errs []error for _, dir := range dirs { - files, err := filepath.Glob(filepath.Join(dir, "*"+e.FileExt)) + files, err := e.VFS.Glob(path.Join(dir, "*"+e.FileExt)) if err != nil { errs = append(errs, err) continue @@ -179,11 +170,13 @@ func (e *GoViewEngine) loadNonLayoutTemplates(scope string) error { for _, file := range files { tmplKey := noLayout + "-" + StripPathPrefixAt(filepath.ToSlash(file), "views/") tmpl := e.NewTemplate(tmplKey) - fileBytes, _ := ioutil.ReadFile(file) - fileStr := e.AntiCSRFField.InsertOnString(string(fileBytes)) log.Tracef("Parsing file: %s", TrimPathPrefix(prefix, file)) - if tmpl, err = tmpl.Parse(fileStr); err != nil { + tstr, err := e.Open(file) + if err != nil { + return err + } + if tmpl, err = tmpl.Parse(tstr); err != nil { errs = append(errs, err) continue } @@ -200,9 +193,4 @@ func (e *GoViewEngine) loadNonLayoutTemplates(scope string) error { func init() { _ = AddEngine("go", &GoViewEngine{}) - - // Add template func - AddTemplateFunc(template.FuncMap{ - "safeHTML": tmplSafeHTML, - }) } diff --git a/go_engine_test.go b/go_engine_test.go index e7be7f4..8b5c7b1 100644 --- a/go_engine_test.go +++ b/go_engine_test.go @@ -8,7 +8,7 @@ import ( "bytes" "errors" "html/template" - "path/filepath" + "io/ioutil" "strings" "testing" @@ -18,9 +18,10 @@ import ( ) func TestViewAppPages(t *testing.T) { - _ = log.SetLevel("trace") + // _ = log.SetLevel("trace") + log.SetWriter(ioutil.Discard) cfg, _ := config.ParseString(`view { }`) - ge := loadGoViewEngine(t, cfg, "views") + ge := loadGoViewEngine(t, cfg, "views", false) data := map[string]interface{}{ "GreetName": "aah framework", @@ -46,11 +47,12 @@ func TestViewAppPages(t *testing.T) { } func TestViewUserPages(t *testing.T) { - _ = log.SetLevel("trace") + // _ = log.SetLevel("trace") + log.SetWriter(ioutil.Discard) cfg, _ := config.ParseString(`view { delimiters = "{{.}}" }`) - ge := loadGoViewEngine(t, cfg, "views") + ge := loadGoViewEngine(t, cfg, "views", true) data := map[string]interface{}{ "GreetName": "aah framework", @@ -79,12 +81,13 @@ func TestViewUserPages(t *testing.T) { } func TestViewUserPagesNoLayout(t *testing.T) { - _ = log.SetLevel("trace") + // _ = log.SetLevel("trace") + log.SetWriter(ioutil.Discard) cfg, _ := config.ParseString(`view { delimiters = "{{.}}" default_layout = false }`) - ge := loadGoViewEngine(t, cfg, "views") + ge := loadGoViewEngine(t, cfg, "views", false) data := map[string]interface{}{ "GreetName": "aah framework", @@ -105,51 +108,54 @@ func TestViewUserPagesNoLayout(t *testing.T) { } func TestViewBaseDirNotExists(t *testing.T) { - viewsDir := filepath.Join(getTestdataPath(), "views1") + viewsDir := join("testdata", "views1") ge := &GoViewEngine{} cfg, _ := config.ParseString(`view { }`) - err := ge.Init(cfg, viewsDir) + err := ge.Init(newVFS(), cfg, viewsDir) assert.NotNil(t, err) assert.True(t, strings.HasPrefix(err.Error(), "goviewengine: views base dir is not exists:")) } func TestViewDelimitersError(t *testing.T) { - viewsDir := filepath.Join(getTestdataPath(), "views") + viewsDir := join("testdata", "views") ge := &GoViewEngine{} cfg, _ := config.ParseString(`view { delimiters = "{{." }`) - err := ge.Init(cfg, viewsDir) + err := ge.Init(newVFS(), cfg, viewsDir) assert.NotNil(t, err) assert.Equal(t, "goviewengine: config 'view.delimiters' value is invalid", err.Error()) } func TestViewErrors(t *testing.T) { - _ = log.SetLevel("trace") + // _ = log.SetLevel("trace") + log.SetWriter(ioutil.Discard) cfg, _ := config.ParseString(`view { default_layout = false }`) + fs := newVFS() + // No layout directiry - viewsDir := filepath.Join(getTestdataPath(), "views-no-layouts-dir") + viewsDir := join("testdata", "views-no-layouts-dir") ge := &GoViewEngine{} - err := ge.Init(cfg, viewsDir) + err := ge.Init(fs, cfg, viewsDir) assert.NotNil(t, err) assert.True(t, strings.HasPrefix(err.Error(), "goviewengine: layouts base dir is not exists:")) // No Common directory - viewsDir = filepath.Join(getTestdataPath(), "views-no-common-dir") + viewsDir = join("testdata", "views-no-common-dir") ge = &GoViewEngine{} - err = ge.Init(cfg, viewsDir) + err = ge.Init(fs, cfg, viewsDir) assert.NotNil(t, err) assert.True(t, strings.HasPrefix(err.Error(), "goviewengine: common base dir is not exists:")) // No Pages directory - viewsDir = filepath.Join(getTestdataPath(), "views-no-pages-dir") + viewsDir = join("testdata", "views-no-pages-dir") ge = &GoViewEngine{} - err = ge.Init(cfg, viewsDir) + err = ge.Init(fs, cfg, viewsDir) assert.NotNil(t, err) assert.True(t, strings.HasPrefix(err.Error(), "goviewengine: pages base dir is not exists:")) @@ -159,25 +165,34 @@ func TestViewErrors(t *testing.T) { assert.Equal(t, "goviewengine: error processing templates, please check the log", err.Error()) } -func loadGoViewEngine(t *testing.T, cfg *config.Config, dir string) *GoViewEngine { +func loadGoViewEngine(t *testing.T, cfg *config.Config, dir string, hotreload bool) *GoViewEngine { // dummy func for test AddTemplateFunc(template.FuncMap{ - "anitcsrftoken": func(arg interface{}) string { + "anticsrftoken": func(arg interface{}) string { return "" }, + "rurl": func(args map[string]interface{}, key string) string { + return "//localhost:8080/login" + }, + "qparam": func(args map[string]interface{}, key string) string { + return "/index" + }, }) - viewsDir := filepath.Join(getTestdataPath(), dir) + viewsDir := join("testdata", dir) ge := &GoViewEngine{} - err := ge.Init(cfg, viewsDir) + err := ge.Init(newVFS(), cfg, viewsDir) assert.FailNowOnError(t, err, "") + ge.hotReload = hotreload assert.Equal(t, viewsDir, ge.BaseDir) assert.NotNil(t, ge.AppConfig) assert.NotNil(t, ge.Templates) - assert.NotNil(t, (&EngineBase{}).Init(nil, "", "", "")) + assert.NotNil(t, (&EngineBase{}).Init(nil, nil, "", "", "")) + + log.SetWriter(ioutil.Discard) return ge } diff --git a/testdata/views/pages/app/testhtml-form.html b/testdata/views/pages/app/testhtml-form.html new file mode 100644 index 0000000..188cc69 --- /dev/null +++ b/testdata/views/pages/app/testhtml-form.html @@ -0,0 +1,28 @@ + + + + pageTitle + + + +

node template engine

+
+
+ {{ if .Welcome }} +

You are amazing

+ {{ else }} +

Get on it!

+ {{ end }} +

+ one is terse and simple + templating language with a + focus on performance + and powerful features. +

+
+ + diff --git a/testdata/views/pages/user/testhtml-form.html b/testdata/views/pages/user/testhtml-form.html new file mode 100644 index 0000000..bf5f2fb --- /dev/null +++ b/testdata/views/pages/user/testhtml-form.html @@ -0,0 +1,28 @@ + + + + pageTitle + + + +

node template engine

+
+
+ {{ if .Welcome }} +

You are amazing

+ {{ else }} +

Get on it!

+ {{ end }} +

+ one is terse and simple + templating language with a + focus on performance + and powerful features. +

+
+ + diff --git a/version.go b/version.go index f8863b2..cacaec6 100644 --- a/version.go +++ b/version.go @@ -1,8 +1,8 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/view source code and usage is governed by a MIT style +// aahframework.org/view source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package view // Version no. of aah framework view library -const Version = "0.8.2" +const Version = "0.9.0" diff --git a/view.go b/view.go index 4a4fd0a..70ae7d1 100644 --- a/view.go +++ b/view.go @@ -1,5 +1,5 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/view source code and usage is governed by a MIT style +// aahframework.org/view source code and usage is governed by a MIT style // license that can be found in the LICENSE file. // Package view is implementation of aah framework view engine using Go @@ -11,13 +11,15 @@ import ( "errors" "fmt" "html/template" + "path" "path/filepath" - "reflect" + "regexp" "strings" "aahframework.org/config.v0" "aahframework.org/essentials.v0" "aahframework.org/log.v0" + "aahframework.org/vfs.v0" ) var ( @@ -39,13 +41,13 @@ var ( // Enginer interface defines a methods for pluggable view engine. type Enginer interface { - Init(appCfg *config.Config, baseDir string) error + Init(fs *vfs.VFS, appCfg *config.Config, baseDir string) error Get(layout, path, tmplName string) (*template.Template, error) } -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // Package methods -//___________________________________ +//______________________________________________________________________________ // AddTemplateFunc method adds given Go template funcs into function map. func AddTemplateFunc(funcMap template.FuncMap) { @@ -72,16 +74,13 @@ func AddEngine(name string, engine Enginer) error { // GetEngine method returns the view engine from store by name otherwise nil. func GetEngine(name string) (Enginer, bool) { - if engine, found := viewEngines[name]; found { - ty := reflect.TypeOf(engine) - return reflect.New(ty.Elem()).Interface().(Enginer), found - } - return nil, false + engine, found := viewEngines[name] + return engine, found } -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // type Templates, methods -//___________________________________ +//______________________________________________________________________________ // Templates hold template reference of lowercase key and case sensitive key // with reference to compliled template. @@ -89,7 +88,7 @@ type Templates struct { set map[string]*template.Template } -// Get method return the template for given key. +// Lookup method return the template for given key. func (t *Templates) Lookup(key string) *template.Template { return t.set[filepath.ToSlash(key)] } @@ -125,35 +124,38 @@ func (t *Templates) Keys() []string { return keys } -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// type EngineBase, methods -//___________________________________ +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// type EngineBase, its methods +//______________________________________________________________________________ // EngineBase struct is to create common and repurpose the implementation. -// Could used for custom view implementation. +// Could be used for custom view engine implementation. type EngineBase struct { + CaseSensitive bool + IsLayoutEnabled bool + hotReload bool Name string - AppConfig *config.Config BaseDir string - Templates map[string]*Templates FileExt string - CaseSensitive bool - IsLayoutEnabled bool LeftDelim string RightDelim string - AntiCSRFField *AntiCSRFField + AppConfig *config.Config + Templates map[string]*Templates + VFS *vfs.VFS + loginFormRegex *regexp.Regexp } // Init method is to initialize the base fields values. -func (eb *EngineBase) Init(appCfg *config.Config, baseDir, defaultEngineName, defaultFileExt string) error { +func (eb *EngineBase) Init(fs *vfs.VFS, appCfg *config.Config, baseDir, defaultEngineName, defaultFileExt string) error { if appCfg == nil { return fmt.Errorf("view: app config is nil") } + eb.VFS = fs eb.Name = appCfg.StringDefault("view.engine", defaultEngineName) // check base directory - if !ess.IsFileExists(baseDir) { + if !eb.VFS.IsExists(baseDir) { return fmt.Errorf("%sviewengine: views base dir is not exists: %s", eb.Name, baseDir) } @@ -170,19 +172,107 @@ func (eb *EngineBase) Init(appCfg *config.Config, baseDir, defaultEngineName, de } eb.LeftDelim, eb.RightDelim = delimiter[0], delimiter[1] - // Anti CSRF - eb.AntiCSRFField = NewAntiCSRFField("go", eb.LeftDelim, eb.RightDelim) + eb.loginFormRegex = regexp.MustCompile(`()`) + return nil } +// Open method reads template from VFS if not found resolve from physical +// file system. Also does auto field insertion such as +// Anti-CSRF(anti_csrf_token) and requested page URL (_rt). +func (eb *EngineBase) Open(filename string) (string, error) { + b, err := vfs.ReadFile(eb.VFS, filename) + if err != nil { + return "", err + } + return eb.AutoFieldInsertion(filename, string(b)), nil +} + +// AutoFieldInsertion method processes the aah view's to auto insert the field. +func (eb *EngineBase) AutoFieldInsertion(name, v string) string { + // process auto field insertion, if form tag exists + // anti_csrf_token field + if strings.Contains(v, "") { + log.Tracef("Adding field 'anti_csrf_token' into all forms: %s", name) + fieldName := eb.AppConfig.StringDefault("security.anti_csrf.form_field_name", "anti_csrf_token") + v = strings.Replace(v, "", fmt.Sprintf(` + `, fieldName, eb.LeftDelim, eb.RightDelim), -1) + } + + // _rt field + if matches := eb.loginFormRegex.FindAllStringIndex(v, -1); len(matches) > 0 { + log.Tracef("Adding field '_rt' into login form: %s", name) + for _, m := range matches { + ts := v[m[0]:m[1]] + v = strings.Replace(v, ts, fmt.Sprintf(`%s + `, ts), 1) + } + } + + return v +} + +// ParseFile method parses given single file. +func (eb *EngineBase) ParseFile(filename string) (*template.Template, error) { + if !strings.HasPrefix(filename, eb.BaseDir) { + filename = path.Join(eb.BaseDir, filename) + } + tmpl := eb.NewTemplate(StripPathPrefixAt(filepath.ToSlash(filename), "views/")) + tstr, err := eb.Open(filename) + if err != nil { + return nil, err + } + return tmpl.Parse(tstr) +} + +// ParseFiles method parses given files with given template instance. +func (eb *EngineBase) ParseFiles(t *template.Template, filenames ...string) (*template.Template, error) { + for _, filename := range filenames { + s, err := eb.Open(filename) + if err != nil { + return nil, err + } + + name := filepath.Base(filename) + var tmpl *template.Template + if t == nil { + t = eb.NewTemplate(name) + } + if name == t.Name() { + tmpl = t + } else { + tmpl = t.New(name) + } + if _, err = tmpl.Parse(s); err != nil { + return nil, err + } + } + + return t, nil +} + // Get method returns the template based given name if found, otherwise nil. -func (eb *EngineBase) Get(layout, path, tmplName string) (*template.Template, error) { +func (eb *EngineBase) Get(layout, tpath, tmplName string) (*template.Template, error) { + if eb.hotReload && eb.Name == "go" { + key := path.Join(tpath, tmplName) + if !eb.CaseSensitive { + key = strings.ToLower(key) + } + + if ess.IsStrEmpty(layout) { + return eb.ParseFile(path.Join(eb.BaseDir, key)) + } + return eb.ParseFiles(eb.NewTemplate(key), + path.Join(eb.BaseDir, "layouts", layout), + path.Join(eb.BaseDir, key)) + } + if ess.IsStrEmpty(layout) { layout = noLayout } if tmpls, found := eb.Templates[layout]; found { - key := filepath.Join(path, tmplName) + key := path.Join(tpath, tmplName) if layout == noLayout { key = noLayout + "-" + key } @@ -199,6 +289,11 @@ func (eb *EngineBase) Get(layout, path, tmplName string) (*template.Template, er return nil, ErrTemplateNotFound } +// SetHotReload method set teh view engine mode into hot reload without watcher. +func (eb *EngineBase) SetHotReload(r bool) { + eb.hotReload = r +} + // AddTemplate method adds the given template for layout and key. func (eb *EngineBase) AddTemplate(layout, key string, tmpl *template.Template) error { if eb.Templates[layout] == nil { @@ -223,34 +318,32 @@ func (eb *EngineBase) ParseErrors(errs []error) error { // LayoutFiles method returns the all layout files from `/layouts`. // If layout directory doesn't exists it returns error. func (eb *EngineBase) LayoutFiles() ([]string, error) { - baseDir := filepath.Join(eb.BaseDir, "layouts") - if !ess.IsFileExists(baseDir) { + baseDir := path.Join(eb.BaseDir, "layouts") + if !eb.VFS.IsExists(baseDir) { return nil, fmt.Errorf("%sviewengine: layouts base dir is not exists: %s", eb.Name, baseDir) } - return filepath.Glob(filepath.Join(baseDir, "*"+eb.FileExt)) + return eb.VFS.Glob(path.Join(baseDir, "*"+eb.FileExt)) } // DirsPath method returns all sub directories from `/`. // if it not exists returns error. func (eb *EngineBase) DirsPath(subDir string) ([]string, error) { - baseDir := filepath.Join(eb.BaseDir, subDir) - if !ess.IsFileExists(baseDir) { + baseDir := path.Join(eb.BaseDir, subDir) + if !eb.VFS.IsExists(baseDir) { return nil, fmt.Errorf("%sviewengine: %s base dir is not exists: %s", eb.Name, subDir, baseDir) } - - return ess.DirsPath(baseDir, true) + return eb.VFS.Dirs(baseDir) } // FilesPath method returns all file path from `/`. // if it not exists returns error. func (eb *EngineBase) FilesPath(subDir string) ([]string, error) { - baseDir := filepath.Join(eb.BaseDir, subDir) - if !ess.IsFileExists(baseDir) { + baseDir := path.Join(eb.BaseDir, subDir) + if !eb.VFS.IsExists(baseDir) { return nil, fmt.Errorf("%sviewengine: %s base dir is not exists: %s", eb.Name, subDir, baseDir) } - - return ess.FilesPath(baseDir, true) + return eb.VFS.Files(baseDir) } // NewTemplate method return new instance on `template.Template` initialized with diff --git a/view_test.go b/view_test.go index f116af5..b9b2c7a 100644 --- a/view_test.go +++ b/view_test.go @@ -6,12 +6,17 @@ package view import ( "html/template" + "io/ioutil" "os" + "path" "path/filepath" "strings" "testing" + "aahframework.org/config.v0" + "aahframework.org/log.v0" "aahframework.org/test.v0/assert" + "aahframework.org/vfs.v0" ) func TestViewAddTemplateFunc(t *testing.T) { @@ -61,7 +66,23 @@ func TestViewTemplates(t *testing.T) { assert.False(t, tmpls.IsExists("not-exixts")) } -func getTestdataPath() string { +func testdataBaseDir() string { wd, _ := os.Getwd() return filepath.Join(wd, "testdata") } + +func newVFS() *vfs.VFS { + fs := new(vfs.VFS) + fs.AddMount("/testdata", testdataBaseDir()) + return fs +} + +func join(s ...string) string { + return "/" + path.Join(s...) +} + +func newLog() log.Loggerer { + l, _ := log.New(config.NewEmpty()) + l.SetWriter(ioutil.Discard) + return l +}