├── .gitignore ├── _fixture └── templates │ ├── share │ ├── footer.html │ └── header.html │ ├── index3.volt │ ├── index.html │ └── index2.html ├── render_test.go ├── notify.go ├── README.md └── render.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store -------------------------------------------------------------------------------- /_fixture/templates/share/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_fixture/templates/share/header.html: -------------------------------------------------------------------------------- 1 |
this is header.
-------------------------------------------------------------------------------- /_fixture/templates/index3.volt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Baa 7 | 8 | 9 | 10 | Hello, {{ name }}, Volt. 11 | 12 | 13 | -------------------------------------------------------------------------------- /_fixture/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Baa 7 | 8 | 9 | 10 | 11 | Hello, {{ name }} 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /_fixture/templates/index2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Baa 7 | 8 | 9 | 10 | 11 | {% include "share/header.html" %} 12 | 13 | Hello, {{ name }} 14 | 15 | {% include "share/footer.html" %} 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /render_test.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | import ( 4 | "html/template" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/go-baa/baa" 12 | . "github.com/smartystreets/goconvey/convey" 13 | ) 14 | 15 | var b = baa.New() 16 | 17 | func TestRender1(t *testing.T) { 18 | Convey("render", t, func() { 19 | b.SetDI("render", New(Options{ 20 | Baa: b, 21 | Root: "_fixture/templates/", 22 | Extensions: []string{".html"}, 23 | Functions: template.FuncMap{}, 24 | })) 25 | 26 | Convey("normal render", func() { 27 | b.Get("/", func(c *baa.Context) { 28 | c.HTML(200, "index") 29 | }) 30 | w := request("GET", "/") 31 | So(w.Code, ShouldEqual, http.StatusOK) 32 | }) 33 | 34 | Convey("embed render", func() { 35 | b.Get("/i2", func(c *baa.Context) { 36 | body, err := c.Fetch("index2") 37 | So(err, ShouldBeNil) 38 | So(strings.Contains(string(body), "header"), ShouldBeTrue) 39 | }) 40 | w := request("GET", "/i2") 41 | So(w.Code, ShouldEqual, http.StatusOK) 42 | }) 43 | 44 | Convey("change file", func() { 45 | file := "_fixture/templates/index.html" 46 | body, err := ioutil.ReadFile(file) 47 | So(err, ShouldBeNil) 48 | err = ioutil.WriteFile(file, body, 0664) 49 | So(err, ShouldBeNil) 50 | }) 51 | }) 52 | } 53 | 54 | func request(method, uri string) *httptest.ResponseRecorder { 55 | req, _ := http.NewRequest(method, uri, nil) 56 | w := httptest.NewRecorder() 57 | b.ServeHTTP(w, req) 58 | return w 59 | } 60 | -------------------------------------------------------------------------------- /notify.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/fsnotify/fsnotify" 7 | ) 8 | 9 | const ( 10 | Create fsnotify.Op = 1 << iota 11 | Write 12 | Remove 13 | Rename 14 | Chmod 15 | ) 16 | 17 | type notifyItem struct { 18 | event fsnotify.Op 19 | path string 20 | } 21 | 22 | func (r *Render) notify() { 23 | watcher, err := fsnotify.NewWatcher() 24 | if err != nil { 25 | r.Error(err) 26 | return 27 | } 28 | defer watcher.Close() 29 | 30 | done := make(chan bool) 31 | go func() { 32 | for { 33 | select { 34 | case event := <-watcher.Events: 35 | if event.Op&fsnotify.Write == fsnotify.Write { 36 | r.fileChanges <- notifyItem{fsnotify.Write, event.Name} 37 | } else if event.Op&fsnotify.Create == fsnotify.Create { 38 | r.fileChanges <- notifyItem{fsnotify.Create, event.Name} 39 | } else if event.Op&fsnotify.Remove == fsnotify.Remove { 40 | r.fileChanges <- notifyItem{fsnotify.Remove, event.Name} 41 | } 42 | case err = <-watcher.Errors: 43 | r.Error(err) 44 | } 45 | } 46 | }() 47 | 48 | var l []string 49 | l = append(l, r.Root) 50 | err = recursiveDir(r.Root, &l) 51 | if err != nil { 52 | r.Error(err) 53 | return 54 | } 55 | for _, d := range l { 56 | err = watcher.Add(d) 57 | if err != nil { 58 | r.Error(err) 59 | return 60 | } 61 | } 62 | 63 | <-done 64 | } 65 | 66 | func recursiveDir(dir string, l *[]string) error { 67 | dl, err := readDir(dir) 68 | if err != nil { 69 | return err 70 | } 71 | for _, d := range dl { 72 | if d.IsDir() { 73 | _dir := dir + "/" + d.Name() 74 | *l = append(*l, _dir) 75 | err = recursiveDir(_dir, l) 76 | if err != nil { 77 | return err 78 | } 79 | } 80 | } 81 | return err 82 | } 83 | 84 | func readDir(dirname string) ([]os.FileInfo, error) { 85 | f, err := os.Open(dirname) 86 | if err != nil { 87 | return nil, err 88 | } 89 | list, err := f.Readdir(-1) 90 | f.Close() 91 | if err != nil { 92 | return nil, err 93 | } 94 | return list, nil 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pongo2 2 | A helper for using [Pongo2](https://github.com/micate/pongo2) with Baa. 3 | 4 | ## getting started 5 | 6 | ```go 7 | package main 8 | 9 | import ( 10 | "github.com/go-baa/baa" 11 | "github.com/go-baa/pongo2" 12 | ) 13 | 14 | func main() { 15 | // new app 16 | app := baa.New() 17 | 18 | // register pongo2 render 19 | // render is template DI for baa, must be this name. 20 | app.SetDI("render", pongo2.New(pongo2.Options{ 21 | Baa: b, 22 | Root: "templates/", 23 | Extensions: []string{".html"}, 24 | Functions: map[string]interface{}{}, 25 | Context: map[string]interface{}{ 26 | "SITE_NAME": "Yet another website", 27 | }, 28 | })) 29 | 30 | // router 31 | app.Get("/", func(c *baa.Context) { 32 | c.HTML(200, "index") 33 | }) 34 | 35 | // run app 36 | app.Run(":1323") 37 | } 38 | ``` 39 | 40 | ## usage 41 | 42 | ### common 43 | 44 | #### output 45 | 46 | ```html 47 | This is {{ name }}. 48 | ``` 49 | 50 | #### include template 51 | 52 | ```html 53 | {% include "path/to/tpl.html" %} 54 | ``` 55 | 56 | with params: 57 | 58 | ```html 59 | {% include "relative/path/to/tpl.html" with foo=var %} 60 | {% include "relative/path/to/tpl.html" with foo="bar" %} 61 | ``` 62 | 63 | **note**: nested template reveived param as string type. 64 | 65 | #### if / elif / else / endif 66 | 67 | ```html 68 | {% if vara %} 69 | {% elif varb %} 70 | {% else %} 71 | {% endif %} 72 | ``` 73 | 74 | #### for 75 | 76 | ```html 77 | {% for item in items %} 78 | {{ forloop.Counter }} {{ forloop.Counter0 }} {{ forloop.First }} {{ forloop.Last }} {{ forloop.Revcounter }} {{ forloop.Revcounter0 }} 79 | {{ item }} 80 | {% endfor %} 81 | ``` 82 | 83 | #### extends / block / macro and so on ... 84 | see [document](https://docs.djangoproject.com/en/dev/ref/templates/language/). 85 | 86 | ### builtin filters 87 | 88 | * escape 89 | * safe 90 | * escapejs 91 | * add 92 | * addslashes 93 | * capfirst 94 | * center 95 | * cut 96 | * date 97 | * default 98 | * default_if_none 99 | * divisibleby 100 | * first 101 | * floatformat 102 | * get_digit 103 | * iriencode 104 | * join 105 | * last 106 | * length 107 | * length_is 108 | * linebreaks 109 | * linebreaksbr 110 | * linenumbers 111 | * ljust 112 | * lower 113 | * make_list 114 | * phone2numeric 115 | * pluralize 116 | * random 117 | * removetags 118 | * rjust 119 | * slice 120 | * stringformat 121 | * striptags 122 | * time 123 | * title 124 | * truncatechars 125 | * truncatechars_html 126 | * truncatewords 127 | * truncatewords_html 128 | * upper 129 | * urlencode 130 | * urlize 131 | * urlizetrunc 132 | * wordcount 133 | * wordwrap 134 | * yesno 135 | * float 136 | * integer -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | // Package pongo2 providers the pongo2 template engine for baa. 2 | package pongo2 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/go-baa/baa" 13 | "github.com/safeie/pongo2" 14 | ) 15 | 16 | // Render the pongo2 template engine 17 | type Render struct { 18 | Options 19 | fileChanges chan notifyItem // notify file changes 20 | } 21 | 22 | // Options render options 23 | type Options struct { 24 | Baa *baa.Baa // baa 25 | Root string // template root dir 26 | Extensions []string // template file extensions 27 | Filters map[string]pongo2.FilterFunction // template filters 28 | Functions map[string]interface{} // template functions 29 | Context map[string]interface{} // template global context 30 | } 31 | 32 | // tplIndexes template name path indexes 33 | var tplIndexes map[string]string 34 | 35 | // New create a template engine 36 | func New(o Options) *Render { 37 | // init indexes map 38 | tplIndexes = map[string]string{} 39 | 40 | r := new(Render) 41 | r.Baa = o.Baa 42 | r.Root = o.Root 43 | r.Extensions = o.Extensions 44 | r.Context = o.Context 45 | 46 | // check template dir 47 | if r.Root == "" { 48 | panic("pongo2.New: template dir is empty!") 49 | } 50 | r.Root, _ = filepath.Abs(r.Root) 51 | slash := "/" 52 | if runtime.GOOS == "windows" { 53 | slash = "\\" 54 | } 55 | if r.Root[len(r.Root)-1] != slash[0] { 56 | r.Root += slash 57 | } 58 | if f, err := os.Stat(r.Root); err != nil { 59 | panic("pongo2.New: template dir[" + r.Root + "] open error: " + err.Error()) 60 | } else { 61 | if !f.IsDir() { 62 | panic("pongo2.New: template dir[" + r.Root + "] is not s directory!") 63 | } 64 | } 65 | 66 | // check extension 67 | if r.Extensions == nil { 68 | r.Extensions = []string{".html"} 69 | } 70 | 71 | // register filter 72 | for name, filter := range o.Filters { 73 | pongo2.RegisterOrReplaceFilter(name, filter) 74 | } 75 | 76 | // merge function into context 77 | for k, v := range o.Functions { 78 | if _, ok := r.Context[k]; ok { 79 | panic("pongo2.New: context key[" + k + "] already exists in functions") 80 | } 81 | r.Context[k] = v 82 | } 83 | 84 | if baa.Env != baa.PROD { 85 | // enable debug mode 86 | pongo2.DefaultSet.Debug = true 87 | 88 | r.fileChanges = make(chan notifyItem, 8) 89 | go r.notify() 90 | go func() { 91 | for item := range r.fileChanges { 92 | if r.Baa != nil && r.Baa.Debug() { 93 | r.Error("filechanges Receive -> " + item.path) 94 | } 95 | if item.event == Create || item.event == Write { 96 | r.parseFile(item.path) 97 | } 98 | } 99 | }() 100 | } 101 | 102 | // load templates 103 | r.loadTpls() 104 | 105 | return r 106 | } 107 | 108 | // Render template 109 | func (r *Render) Render(w io.Writer, tpl string, data interface{}) error { 110 | path, ok := tplIndexes[tpl] 111 | if !ok { 112 | return fmt.Errorf("pongo2.Render: tpl [%s] not found", tpl) 113 | } 114 | 115 | t, err := pongo2.FromCache(path) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | ctx, err := r.buildContext(data) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | return t.ExecuteWriter(ctx, w) 126 | } 127 | 128 | // buildContext build pongo2 render context 129 | func (r *Render) buildContext(in interface{}) (pongo2.Context, error) { 130 | // check data type 131 | data, ok := in.(map[string]interface{}) 132 | if !ok { 133 | return nil, fmt.Errorf("pongo2.buildContext: unsupported render data type [%v]", in) 134 | } 135 | 136 | // copy from global context 137 | ctx := map[string]interface{}{} 138 | for k, v := range r.Context { 139 | ctx[k] = v 140 | } 141 | 142 | // fill with render data 143 | for k, v := range data { 144 | if _, ok := ctx[k]; ok { 145 | return nil, fmt.Errorf("pongo2.buildContext: render data key [%s] already exists", k) 146 | } 147 | ctx[k] = v 148 | } 149 | 150 | return pongo2.Context(ctx), nil 151 | } 152 | 153 | // loadTpls load all template files 154 | func (r *Render) loadTpls() { 155 | paths, err := r.readDir(r.Root) 156 | if err != nil { 157 | r.Error(err) 158 | return 159 | } 160 | for _, path := range paths { 161 | err = r.parseFile(path) 162 | if err != nil { 163 | r.Error(err) 164 | } 165 | } 166 | } 167 | 168 | // readDir scan dir load all template files 169 | func (r *Render) readDir(path string) ([]string, error) { 170 | var paths []string 171 | f, err := os.Open(path) 172 | if err != nil { 173 | return nil, err 174 | } 175 | defer f.Close() 176 | fs, err := f.Readdir(-1) 177 | if err != nil { 178 | return nil, err 179 | } 180 | 181 | var p string 182 | for _, f := range fs { 183 | p = filepath.Clean(path + "/" + f.Name()) 184 | if f.IsDir() { 185 | fs, err := r.readDir(p) 186 | if err != nil { 187 | continue 188 | } 189 | for _, f := range fs { 190 | paths = append(paths, f) 191 | } 192 | } else { 193 | if r.checkExt(p) { 194 | paths = append(paths, p) 195 | } 196 | } 197 | } 198 | return paths, nil 199 | } 200 | 201 | // tplName get template alias from a template file path 202 | func (r *Render) tplName(path string) string { 203 | if len(path) > len(r.Root) && path[:len(r.Root)] == r.Root { 204 | path = path[len(r.Root):] 205 | } 206 | ext := filepath.Ext(path) 207 | path = path[:len(path)-len(ext)] 208 | if runtime.GOOS == "windows" { 209 | path = strings.Replace(path, "\\", "/", -1) 210 | } 211 | return path 212 | } 213 | 214 | // checkExt check path extension allow use 215 | func (r *Render) checkExt(path string) bool { 216 | ext := filepath.Ext(path) 217 | if ext == "" { 218 | return false 219 | } 220 | for i := range r.Extensions { 221 | if r.Extensions[i] == ext { 222 | return true 223 | } 224 | } 225 | return false 226 | } 227 | 228 | // parseFile load file and parse to template 229 | func (r *Render) parseFile(path string) error { 230 | // parse template 231 | _, err := pongo2.FromCache(path) 232 | if err != nil { 233 | return err 234 | } 235 | 236 | // update indexes 237 | tpl := r.tplName(path) 238 | tplIndexes[tpl] = path 239 | 240 | return nil 241 | } 242 | 243 | // Error log error 244 | func (r *Render) Error(v interface{}) { 245 | if r.Baa != nil { 246 | r.Baa.Logger().Println(v) 247 | } 248 | } 249 | --------------------------------------------------------------------------------