├── .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 |
--------------------------------------------------------------------------------
/_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 |
--------------------------------------------------------------------------------