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) +
+ +
+
+ -***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 @@ + + + +