~repos /gromer

#golang#htmx#ssr

git clone https://pyrossh.dev/repos/gromer.git

gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.


e62ff686 Peter John

3 years ago
import velvet
cmd/gromer/main.go CHANGED
@@ -12,8 +12,8 @@ import (
12
12
  "strings"
13
13
  "unicode"
14
14
 
15
- "github.com/gobuffalo/velvet"
16
15
  "github.com/pyros2097/gromer"
16
+ "github.com/pyros2097/gromer/handlebars"
17
17
  "golang.org/x/mod/modfile"
18
18
  )
19
19
 
@@ -127,7 +127,7 @@ func main() {
127
127
  for _, r := range gromer.RouteDefs {
128
128
  fmt.Printf("%-6s %s %-6s\n", r.Method, r.Path, r.PkgPath)
129
129
  }
130
- err = velvet.Helpers.Add("title", func(v string) string {
130
+ err = handlebars.GlobalHelpers.Add("title", func(v string) string {
131
131
  return strings.Title(strings.ToLower(v))
132
132
  })
133
133
  if err != nil {
@@ -156,13 +156,13 @@ func main() {
156
156
  if err != nil {
157
157
  log.Fatal(err)
158
158
  }
159
- ctx := velvet.NewContext()
159
+ ctx := handlebars.NewContext()
160
160
  ctx.Set("moduleName", moduleName)
161
161
  ctx.Set("routes", gromer.RouteDefs)
162
162
  ctx.Set("routeImports", routeImports)
163
163
  ctx.Set("componentNames", componentNames)
164
164
  ctx.Set("tick", "`")
165
- s, err := velvet.Render(`// Code generated by gromer. DO NOT EDIT.
165
+ s, err := handlebars.Render(`// Code generated by gromer. DO NOT EDIT.
166
166
  package main
167
167
 
168
168
  import (
example/config/config.go CHANGED
@@ -1,6 +1,8 @@
1
1
  package config
2
2
 
3
+ import (
3
- import "os"
4
+ "os"
5
+ )
4
6
 
5
7
  var DATABASE_URL string
6
8
 
example/pages/about/get.go CHANGED
@@ -8,7 +8,7 @@ import (
8
8
 
9
9
  func GET(c context.Context) (HtmlContent, int, error) {
10
10
  return Html(`
11
- {{#Page "gromer example"}}
11
+ {{#Page "About me"}}
12
12
  <div class="flex flex-col justify-center items-center">
13
13
  {{#Header "123"}}
14
14
  A new link is here
go.mod CHANGED
@@ -6,7 +6,6 @@ require (
6
6
  github.com/bradleyjkemp/cupaloy v2.3.0+incompatible
7
7
  github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf
8
8
  github.com/go-playground/validator/v10 v10.9.0
9
- github.com/gobuffalo/velvet v0.0.0-20170320144106-d97471bf5d8f
10
9
  github.com/google/uuid v1.3.0
11
10
  github.com/gorilla/mux v1.8.0
12
11
  github.com/imdario/mergo v0.3.12
go.sum CHANGED
@@ -187,8 +187,6 @@ github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSG
187
187
  github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
188
188
  github.com/gobuffalo/envy v1.6.5 h1:X3is06x7v0nW2xiy2yFbbIjwHz57CD6z6MkvqULTCm8=
189
189
  github.com/gobuffalo/envy v1.6.5/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
190
- github.com/gobuffalo/velvet v0.0.0-20170320144106-d97471bf5d8f h1:ddIdPdlkAgKMB0mbkft2LT3oxN1h3MN1fopCFrOgkhY=
191
- github.com/gobuffalo/velvet v0.0.0-20170320144106-d97471bf5d8f/go.mod h1:m9x1vDSQYrGiEhEiu0c4XuE0SZzw31Ms8ULjGdhaA54=
192
190
  github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
193
191
  github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
194
192
  github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
handlebars/README.md ADDED
File without changes
handlebars/context.go ADDED
@@ -0,0 +1,79 @@
1
+ package handlebars
2
+
3
+ // Context holds all of the data for the template that is being rendered.
4
+ type Context struct {
5
+ data map[string]interface{}
6
+ options map[string]interface{}
7
+ outer *Context
8
+ }
9
+
10
+ func (c *Context) export() map[string]interface{} {
11
+ m := map[string]interface{}{}
12
+ if c.outer != nil {
13
+ for k, v := range c.outer.export() {
14
+ m[k] = v
15
+ }
16
+ }
17
+ for k, v := range c.data {
18
+ m[k] = v
19
+ }
20
+ if c.options != nil {
21
+ for k, v := range c.options {
22
+ m[k] = v
23
+ }
24
+ }
25
+
26
+ return m
27
+ }
28
+
29
+ // New context containing the current context. Values set on the new context
30
+ // will not be set onto the original context, however, the original context's
31
+ // values will be available to the new context.
32
+ func (c *Context) New() *Context {
33
+ cc := NewContext()
34
+ cc.outer = c
35
+ return cc
36
+ }
37
+
38
+ // Set a value onto the context
39
+ func (c *Context) Set(key string, value interface{}) {
40
+ c.data[key] = value
41
+ }
42
+
43
+ // Get a value from the context, or it's parent's context if one exists.
44
+ func (c *Context) Get(key string) interface{} {
45
+ if v, ok := c.data[key]; ok {
46
+ return v
47
+ }
48
+ if c.outer != nil {
49
+ return c.outer.Get(key)
50
+ }
51
+ return nil
52
+ }
53
+
54
+ // Has checks the existence of the key in the context.
55
+ func (c *Context) Has(key string) bool {
56
+ return c.Get(key) != nil
57
+ }
58
+
59
+ // Options are the values passed into a helper.
60
+ func (c *Context) Options() map[string]interface{} {
61
+ return c.options
62
+ }
63
+
64
+ // NewContext returns a fully formed context ready to go
65
+ func NewContext() *Context {
66
+ return &Context{
67
+ data: map[string]interface{}{},
68
+ options: map[string]interface{}{},
69
+ outer: nil,
70
+ }
71
+ }
72
+
73
+ // NewContextWith returns a fully formed context using the data
74
+ // provided.
75
+ func NewContextWith(data map[string]interface{}) *Context {
76
+ c := NewContext()
77
+ c.data = data
78
+ return c
79
+ }
handlebars/context_test.go ADDED
@@ -0,0 +1,47 @@
1
+ package handlebars
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/stretchr/testify/require"
7
+ )
8
+
9
+ func Test_Context_Set(t *testing.T) {
10
+ r := require.New(t)
11
+ c := NewContext()
12
+ r.Nil(c.Get("foo"))
13
+ c.Set("foo", "bar")
14
+ r.NotNil(c.Get("foo"))
15
+ }
16
+
17
+ func Test_Context_Get(t *testing.T) {
18
+ r := require.New(t)
19
+ c := NewContext()
20
+ r.Nil(c.Get("foo"))
21
+ c.Set("foo", "bar")
22
+ r.Equal("bar", c.Get("foo"))
23
+ }
24
+
25
+ func Test_NewSubContext_Set(t *testing.T) {
26
+ r := require.New(t)
27
+
28
+ c := NewContext()
29
+ r.Nil(c.Get("foo"))
30
+
31
+ sc := c.New()
32
+ r.Nil(sc.Get("foo"))
33
+ sc.Set("foo", "bar")
34
+ r.Equal("bar", sc.Get("foo"))
35
+
36
+ r.Nil(c.Get("foo"))
37
+ }
38
+
39
+ func Test_NewSubContext_Get(t *testing.T) {
40
+ r := require.New(t)
41
+
42
+ c := NewContext()
43
+ c.Set("foo", "bar")
44
+
45
+ sc := c.New()
46
+ r.Equal("bar", sc.Get("foo"))
47
+ }
handlebars/eval.go ADDED
@@ -0,0 +1,385 @@
1
+ package handlebars
2
+
3
+ import (
4
+ "bytes"
5
+ "fmt"
6
+ "html/template"
7
+ "reflect"
8
+ "strconv"
9
+ "strings"
10
+
11
+ "github.com/aymerick/raymond/ast"
12
+ "github.com/pkg/errors"
13
+ )
14
+
15
+ // HTMLer generates HTML source
16
+ type HTMLer interface {
17
+ HTML() template.HTML
18
+ }
19
+
20
+ type interfacer interface {
21
+ Interface() interface{}
22
+ }
23
+
24
+ type blockParams struct {
25
+ current []string
26
+ stack [][]string
27
+ }
28
+
29
+ func NewBlockParams() *blockParams {
30
+ return &blockParams{
31
+ current: []string{},
32
+ stack: [][]string{},
33
+ }
34
+ }
35
+
36
+ func (bp *blockParams) push(params []string) {
37
+ bp.current = params
38
+ bp.stack = append(bp.stack, params)
39
+ }
40
+
41
+ func (bp *blockParams) pop() []string {
42
+ l := len(bp.stack)
43
+ if l == 0 {
44
+ return bp.current
45
+ }
46
+ p := bp.stack[l-1]
47
+ bp.stack = bp.stack[0:(l - 1)]
48
+ l = len(bp.stack)
49
+ if l == 0 {
50
+ bp.current = []string{}
51
+ } else {
52
+ bp.current = bp.stack[l-1]
53
+ }
54
+ return p
55
+ }
56
+
57
+ var helperContextKind = "HelperContext"
58
+
59
+ type evalVisitor struct {
60
+ template *Template
61
+ context *Context
62
+ curBlock *ast.BlockStatement
63
+ blockParams *blockParams
64
+ }
65
+
66
+ func newEvalVisitor(t *Template, c *Context) *evalVisitor {
67
+ return &evalVisitor{
68
+ template: t,
69
+ context: c,
70
+ blockParams: NewBlockParams(),
71
+ }
72
+ }
73
+
74
+ func (ev *evalVisitor) VisitProgram(p *ast.Program) interface{} {
75
+ // fmt.Println("VisitProgram")
76
+ defer ev.blockParams.pop()
77
+ out := &bytes.Buffer{}
78
+ ev.blockParams.push(p.BlockParams)
79
+ for _, b := range p.Body {
80
+ ev.context = ev.context.New()
81
+ var value interface{}
82
+ value = b.Accept(ev)
83
+ switch vp := value.(type) {
84
+ case error:
85
+ return vp
86
+ case template.HTML:
87
+ out.Write([]byte(vp))
88
+ case HTMLer:
89
+ out.Write([]byte(vp.HTML()))
90
+ case string:
91
+ out.WriteString(template.HTMLEscapeString(vp))
92
+ case []string:
93
+ out.WriteString(template.HTMLEscapeString(strings.Join(vp, " ")))
94
+ case int:
95
+ out.WriteString(strconv.Itoa(vp))
96
+ case fmt.Stringer:
97
+ out.WriteString(template.HTMLEscapeString(vp.String()))
98
+ case interfacer:
99
+ out.WriteString(template.HTMLEscaper(vp.Interface()))
100
+ case nil:
101
+ default:
102
+ return errors.WithStack(errors.Errorf("unsupport eval return format %T: %+v", value, value))
103
+ }
104
+
105
+ }
106
+ return out.String()
107
+ }
108
+ func (ev *evalVisitor) VisitMustache(m *ast.MustacheStatement) interface{} {
109
+ // fmt.Println("VisitMustache")
110
+ expr := m.Expression.Accept(ev)
111
+ return expr
112
+ }
113
+ func (ev *evalVisitor) VisitBlock(node *ast.BlockStatement) interface{} {
114
+ // fmt.Println("VisitBlock")
115
+ defer func() {
116
+ ev.curBlock = nil
117
+ }()
118
+ ev.curBlock = node
119
+ expr := node.Expression.Accept(ev)
120
+ return expr
121
+ }
122
+
123
+ func (ev *evalVisitor) VisitPartial(*ast.PartialStatement) interface{} {
124
+ // fmt.Println("VisitPartial")
125
+ return ""
126
+ }
127
+
128
+ func (ev *evalVisitor) VisitContent(c *ast.ContentStatement) interface{} {
129
+ // fmt.Println("VisitContent")
130
+ return template.HTML(c.Original)
131
+ }
132
+
133
+ func (ev *evalVisitor) VisitComment(*ast.CommentStatement) interface{} {
134
+ return ""
135
+ }
136
+
137
+ func (ev *evalVisitor) VisitExpression(e *ast.Expression) interface{} {
138
+ // fmt.Println("VisitExpression")
139
+ if e.Hash != nil {
140
+ e.Hash.Accept(ev)
141
+ }
142
+ h := ev.helperName(e.HelperName())
143
+ if h != "" {
144
+ if helper, ok := GlobalHelpers.helpers[h]; ok {
145
+ return ev.evalHelper(e, helper)
146
+ }
147
+ if ev.context.Has(h) {
148
+ x := ev.context.Get(h)
149
+ if x != nil && h == "partial" {
150
+ return ev.evalHelper(e, x)
151
+ }
152
+ return x
153
+ }
154
+ return errors.WithStack(errors.Errorf("could not find value for %s [line %d:%d]", h, e.Line, e.Pos))
155
+ }
156
+ parts := strings.Split(e.Canonical(), ".")
157
+ if len(parts) > 1 && ev.context.Has(parts[0]) {
158
+ rv := reflect.ValueOf(ev.context.Get(parts[0]))
159
+ if rv.Kind() == reflect.Ptr {
160
+ rv = rv.Elem()
161
+ }
162
+ m := rv.MethodByName(parts[1])
163
+ if m.IsValid() {
164
+ return ev.evalHelper(e, m.Interface())
165
+ }
166
+ }
167
+ if fp := e.FieldPath(); fp != nil {
168
+ return ev.VisitPath(fp)
169
+ }
170
+ if e.Path != nil {
171
+ return e.Path.Accept(ev)
172
+ }
173
+ return nil
174
+ }
175
+
176
+ func (ev *evalVisitor) VisitSubExpression(*ast.SubExpression) interface{} {
177
+ // fmt.Println("VisitSubExpression")
178
+ return nil
179
+ }
180
+
181
+ func (ev *evalVisitor) VisitPath(node *ast.PathExpression) interface{} {
182
+ // fmt.Println("VisitPath")
183
+ // fmt.Printf("### node -> %+v\n", node)
184
+ // fmt.Printf("### node -> %T\n", node)
185
+ // fmt.Printf("### node.IsDataRoot() -> %+v\n", node.IsDataRoot())
186
+ // fmt.Printf("### node.Loc() -> %+v\n", node.Location())
187
+ // fmt.Printf("### node.String() -> %+v\n", node.String())
188
+ // fmt.Printf("### node.Type() -> %+v\n", node.Type())
189
+ // fmt.Printf("### node.Data -> %+v\n", node.Data)
190
+ // fmt.Printf("### node.Depth -> %+v\n", node.Depth)
191
+ // fmt.Printf("### node.Original -> %+v\n", node.Original)
192
+ // fmt.Printf("### node.Parts -> %+v\n", node.Parts)
193
+ // fmt.Printf("### node.Scoped -> %+v\n", node.Scoped)
194
+ var v interface{}
195
+ var h string
196
+ if node.Data || len(node.Parts) == 0 {
197
+ h = ev.helperName(node.Original)
198
+ } else {
199
+ h = ev.helperName(node.Parts[0])
200
+ }
201
+ if ev.context.Get(h) != nil {
202
+ v = ev.context.Get(h)
203
+ }
204
+ if v == nil {
205
+ return ""
206
+ // return errors.WithStack(errors.Errorf("could not find value for %s [line %d:%d]", h, node.Line, node.Pos))
207
+ }
208
+
209
+ for i := 1; i < len(node.Parts); i++ {
210
+ rv := reflect.ValueOf(v)
211
+ if rv.Kind() == reflect.Ptr {
212
+ rv = rv.Elem()
213
+ }
214
+ p := node.Parts[i]
215
+ m := rv.MethodByName(p)
216
+ if m.IsValid() {
217
+
218
+ args := []reflect.Value{}
219
+ rt := m.Type()
220
+ if rt.NumIn() > 0 {
221
+ last := rt.In(rt.NumIn() - 1)
222
+ if last.Name() == helperContextKind {
223
+ hargs := HelperContext{
224
+ Context: ev.context,
225
+ Args: []interface{}{},
226
+ evalVisitor: ev,
227
+ }
228
+ args = append(args, reflect.ValueOf(hargs))
229
+ } else if last.Kind() == reflect.Map {
230
+ args = append(args, reflect.ValueOf(ev.context.Options()))
231
+ }
232
+ if len(args) > rt.NumIn() {
233
+ err := errors.Errorf("Incorrect number of arguments being passed to %s (%d for %d)", p, len(args), rt.NumIn())
234
+ return errors.WithStack(err)
235
+ }
236
+ }
237
+ vv := m.Call(args)
238
+
239
+ if len(vv) >= 1 {
240
+ v = vv[0].Interface()
241
+ }
242
+ continue
243
+ }
244
+ switch rv.Kind() {
245
+ case reflect.Map:
246
+ pv := reflect.ValueOf(p)
247
+ keys := rv.MapKeys()
248
+ for i := 0; i < len(keys); i++ {
249
+ k := keys[i]
250
+ if k.Interface() == pv.Interface() {
251
+ return rv.MapIndex(k).Interface()
252
+ }
253
+ }
254
+ return errors.WithStack(errors.Errorf("could not find value for %s [line %d:%d]", node.Original, node.Line, node.Pos))
255
+ default:
256
+ f := rv.FieldByName(p)
257
+ v = f.Interface()
258
+ }
259
+ }
260
+ return v
261
+ }
262
+
263
+ func (ev *evalVisitor) VisitString(node *ast.StringLiteral) interface{} {
264
+ // fmt.Println("VisitString")
265
+ return node.Value
266
+ }
267
+
268
+ func (ev *evalVisitor) VisitBoolean(node *ast.BooleanLiteral) interface{} {
269
+ // fmt.Println("VisitBoolean")
270
+ return node.Value
271
+ }
272
+
273
+ func (ev *evalVisitor) VisitNumber(node *ast.NumberLiteral) interface{} {
274
+ // fmt.Println("VisitNumber")
275
+ return node.Number()
276
+ }
277
+
278
+ func (ev *evalVisitor) VisitHash(node *ast.Hash) interface{} {
279
+ // fmt.Println("VisitHash")
280
+ ctx := ev.context.New()
281
+ for _, h := range node.Pairs {
282
+ val := h.Accept(ev).(map[string]interface{})
283
+ for k, v := range val {
284
+ ctx.Set(k, v)
285
+ ctx.Options()[k] = v
286
+ }
287
+ }
288
+ ev.context = ctx
289
+ return nil
290
+ }
291
+
292
+ func (ev *evalVisitor) VisitHashPair(node *ast.HashPair) interface{} {
293
+ // fmt.Println("VisitHashPair")
294
+ return map[string]interface{}{
295
+ node.Key: node.Val.Accept(ev),
296
+ }
297
+ }
298
+
299
+ func (ev *evalVisitor) evalHelper(node *ast.Expression, helper interface{}) (ret interface{}) {
300
+ // fmt.Println("evalHelper")
301
+ defer func() {
302
+ if r := recover(); r != nil {
303
+ switch rp := r.(type) {
304
+ case error:
305
+ ret = errors.WithStack(rp)
306
+ case string:
307
+ ret = errors.WithStack(errors.New(rp))
308
+ }
309
+ }
310
+ }()
311
+
312
+ hargs := HelperContext{
313
+ Context: ev.context,
314
+ Args: []interface{}{},
315
+ evalVisitor: ev,
316
+ }
317
+
318
+ rv := reflect.ValueOf(helper)
319
+ if rv.Kind() == reflect.Ptr {
320
+ rv = rv.Elem()
321
+ }
322
+ rt := rv.Type()
323
+
324
+ args := []reflect.Value{}
325
+
326
+ if rt.NumIn() > 0 {
327
+ for _, p := range node.Params {
328
+ v := p.Accept(ev)
329
+ vv := reflect.ValueOf(v)
330
+ hargs.Args = append(hargs.Args, v)
331
+ args = append(args, vv)
332
+ }
333
+
334
+ last := rt.In(rt.NumIn() - 1)
335
+ if last.Name() == helperContextKind {
336
+ args = append(args, reflect.ValueOf(hargs))
337
+ } else if last.Kind() == reflect.Map {
338
+ if node.Canonical() == "partial" {
339
+ args = append(args, reflect.ValueOf(ev.context.export()))
340
+ } else {
341
+ args = append(args, reflect.ValueOf(ev.context.Options()))
342
+ }
343
+ }
344
+ if len(args) > rt.NumIn() {
345
+ err := errors.Errorf("Incorrect number of arguments being passed to %s (%d for %d)", node.Canonical(), len(args), rt.NumIn())
346
+ return errors.WithStack(err)
347
+ }
348
+ }
349
+ vv := rv.Call(args)
350
+
351
+ if len(vv) >= 1 {
352
+ v := vv[0].Interface()
353
+ if len(vv) >= 2 {
354
+ if !vv[1].IsNil() {
355
+ return errors.WithStack(vv[1].Interface().(error))
356
+ }
357
+ }
358
+ return v
359
+ }
360
+
361
+ return ""
362
+ }
363
+
364
+ func (ev *evalVisitor) helperName(h string) string {
365
+ if h != "" {
366
+ bp := ev.blockParams.current
367
+ if len(bp) == 1 {
368
+ if t := ev.context.Get("@value"); t != nil {
369
+ ev.context.Set(bp[0], t)
370
+ }
371
+ }
372
+ if len(bp) >= 2 {
373
+ if t := ev.context.Get("@value"); t != nil {
374
+ ev.context.Set(bp[1], t)
375
+ }
376
+ for _, k := range []string{"@index", "@key"} {
377
+ if t := ev.context.Get(k); t != nil {
378
+ ev.context.Set(bp[0], t)
379
+ }
380
+ }
381
+ }
382
+ return h
383
+ }
384
+ return ""
385
+ }
handlebars/eval_test.go ADDED
@@ -0,0 +1,72 @@
1
+ package handlebars
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+
7
+ "github.com/stretchr/testify/require"
8
+ )
9
+
10
+ func Test_blockParams(t *testing.T) {
11
+ r := require.New(t)
12
+ bp := NewBlockParams()
13
+ r.Equal([]string{}, bp.current)
14
+ r.Len(bp.stack, 0)
15
+
16
+ bp.push([]string{"mark"})
17
+ r.Equal([]string{"mark"}, bp.current)
18
+ r.Len(bp.stack, 1)
19
+
20
+ bp.push([]string{"bates"})
21
+ r.Equal([]string{"bates"}, bp.current)
22
+ r.Len(bp.stack, 2)
23
+ r.Equal([][]string{
24
+ []string{"mark"},
25
+ []string{"bates"},
26
+ }, bp.stack)
27
+
28
+ b := bp.pop()
29
+ r.Equal([]string{"bates"}, b)
30
+ r.Equal([]string{"mark"}, bp.current)
31
+ r.Len(bp.stack, 1)
32
+
33
+ b = bp.pop()
34
+ r.Equal([]string{"mark"}, b)
35
+ r.Len(bp.stack, 0)
36
+ r.Equal([]string{}, bp.current)
37
+ }
38
+
39
+ func newBlockParams() {
40
+ panic("unimplemented")
41
+ }
42
+
43
+ func Test_Eval_Map_Call_Key(t *testing.T) {
44
+ r := require.New(t)
45
+ ctx := NewContext()
46
+ data := map[string]string{
47
+ "a": "A",
48
+ "b": "B",
49
+ }
50
+ ctx.Set("letters", data)
51
+ input := `
52
+ {{letters.a}}|{{letters.b}}
53
+ `
54
+
55
+ s, err := Render(input, ctx)
56
+ r.NoError(err)
57
+ r.Equal("A|B", strings.TrimSpace(s))
58
+ }
59
+
60
+ func Test_Eval_Calls_on_Pointers(t *testing.T) {
61
+ r := require.New(t)
62
+ type user struct {
63
+ Name string
64
+ }
65
+ u := &user{Name: "Mark"}
66
+ ctx := NewContext()
67
+ ctx.Set("user", u)
68
+
69
+ s, err := Render("{{user.Name}}", ctx)
70
+ r.NoError(err)
71
+ r.Equal("Mark", s)
72
+ }
handlebars/helpers.go ADDED
@@ -0,0 +1,312 @@
1
+ package handlebars
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "fmt"
7
+ "html/template"
8
+ "reflect"
9
+ "strconv"
10
+ "strings"
11
+ "sync"
12
+
13
+ "github.com/pkg/errors"
14
+ )
15
+
16
+ // GlobalHelpers contains all of the default helpers for handlebars.
17
+ // These will be available to all templates. You should add
18
+ // any custom global helpers to this list.
19
+ var GlobalHelpers = HelperMap{
20
+ moot: &sync.Mutex{},
21
+ helpers: map[string]interface{}{},
22
+ }
23
+
24
+ func init() {
25
+ GlobalHelpers.Add("if", ifHelper)
26
+ GlobalHelpers.Add("unless", unlessHelper)
27
+ GlobalHelpers.Add("each", eachHelper)
28
+ GlobalHelpers.Add("eq", equalHelper)
29
+ GlobalHelpers.Add("equal", equalHelper)
30
+ GlobalHelpers.Add("neq", notEqualHelper)
31
+ GlobalHelpers.Add("notequal", notEqualHelper)
32
+ GlobalHelpers.Add("json", toJSONHelper)
33
+ GlobalHelpers.Add("js_escape", template.JSEscapeString)
34
+ GlobalHelpers.Add("html_escape", template.HTMLEscapeString)
35
+ GlobalHelpers.Add("upcase", strings.ToUpper)
36
+ GlobalHelpers.Add("downcase", strings.ToLower)
37
+ GlobalHelpers.Add("len", lenHelper)
38
+ GlobalHelpers.Add("debug", debugHelper)
39
+ GlobalHelpers.Add("inspect", inspectHelper)
40
+ }
41
+
42
+ // HelperContext is an optional context that can be passed
43
+ // as the last argument to helper functions.
44
+ type HelperContext struct {
45
+ Context *Context
46
+ Args []interface{}
47
+ evalVisitor *evalVisitor
48
+ }
49
+
50
+ // Block executes the block of template associated with
51
+ // the helper, think the block inside of an "if" or "each"
52
+ // statement.
53
+ func (h HelperContext) Block() (string, error) {
54
+ return h.BlockWith(h.Context)
55
+ }
56
+
57
+ // BlockWith executes the block of template associated with
58
+ // the helper, think the block inside of an "if" or "each"
59
+ // statement. It takes a new context with which to evaluate
60
+ // the block.
61
+ func (h HelperContext) BlockWith(ctx *Context) (string, error) {
62
+ nev := newEvalVisitor(h.evalVisitor.template, ctx)
63
+ nev.blockParams = h.evalVisitor.blockParams
64
+ dd := nev.VisitProgram(h.evalVisitor.curBlock.Program)
65
+ switch tp := dd.(type) {
66
+ case string:
67
+ return tp, nil
68
+ case error:
69
+ return "", errors.WithStack(tp)
70
+ case nil:
71
+ return "", nil
72
+ default:
73
+ return "", errors.WithStack(errors.Errorf("unknown return value %T %+v", dd, dd))
74
+ }
75
+ }
76
+
77
+ // ElseBlock executes the "inverse" block of template associated with
78
+ // the helper, think the "else" block of an "if" or "each"
79
+ // statement.
80
+ func (h HelperContext) ElseBlock() (string, error) {
81
+ return h.ElseBlockWith(h.Context)
82
+ }
83
+
84
+ // ElseBlockWith executes the "inverse" block of template associated with
85
+ // the helper, think the "else" block of an "if" or "each"
86
+ // statement. It takes a new context with which to evaluate
87
+ // the block.
88
+ func (h HelperContext) ElseBlockWith(ctx *Context) (string, error) {
89
+ if h.evalVisitor.curBlock.Inverse == nil {
90
+ return "", nil
91
+ }
92
+ nev := newEvalVisitor(h.evalVisitor.template, ctx)
93
+ nev.blockParams = h.evalVisitor.blockParams
94
+ dd := nev.VisitProgram(h.evalVisitor.curBlock.Inverse)
95
+ switch tp := dd.(type) {
96
+ case string:
97
+ return tp, nil
98
+ case error:
99
+ return "", errors.WithStack(tp)
100
+ case nil:
101
+ return "", nil
102
+ default:
103
+ return "", errors.WithStack(errors.Errorf("unknown return value %T %+v", dd, dd))
104
+ }
105
+ }
106
+
107
+ // Get is a convenience method that calls the underlying Context.
108
+ func (h HelperContext) Get(key string) interface{} {
109
+ return h.Context.Get(key)
110
+ }
111
+
112
+ // toJSONHelper converts an interface into a string.
113
+ func toJSONHelper(v interface{}) (template.HTML, error) {
114
+ b, err := json.Marshal(v)
115
+ if err != nil {
116
+ return "", errors.WithStack(err)
117
+ }
118
+ return template.HTML(b), nil
119
+ }
120
+
121
+ func lenHelper(v interface{}) string {
122
+ rv := reflect.ValueOf(v)
123
+ if rv.Kind() == reflect.Ptr {
124
+ rv = rv.Elem()
125
+ }
126
+ return strconv.Itoa(rv.Len())
127
+ }
128
+
129
+ // Debug by verbosely printing out using 'pre' tags.
130
+ func debugHelper(v interface{}) template.HTML {
131
+ return template.HTML(fmt.Sprintf("<pre>%+v</pre>", v))
132
+ }
133
+
134
+ func inspectHelper(v interface{}) string {
135
+ return fmt.Sprintf("%+v", v)
136
+ }
137
+
138
+ func eachHelper(collection interface{}, help HelperContext) (template.HTML, error) {
139
+ out := bytes.Buffer{}
140
+ val := reflect.ValueOf(collection)
141
+ if val.Kind() == reflect.Ptr {
142
+ val = val.Elem()
143
+ }
144
+ if val.Kind() == reflect.Struct || val.Len() == 0 {
145
+ s, err := help.ElseBlock()
146
+ return template.HTML(s), err
147
+ }
148
+ switch val.Kind() {
149
+ case reflect.Array, reflect.Slice:
150
+ for i := 0; i < val.Len(); i++ {
151
+ v := val.Index(i).Interface()
152
+ ctx := help.Context.New()
153
+ ctx.Set("@first", i == 0)
154
+ ctx.Set("@last", i == val.Len()-1)
155
+ ctx.Set("@index", i)
156
+ ctx.Set("@value", v)
157
+ s, err := help.BlockWith(ctx)
158
+ if err != nil {
159
+ return "", errors.WithStack(err)
160
+ }
161
+ out.WriteString(s)
162
+ }
163
+ case reflect.Map:
164
+ keys := val.MapKeys()
165
+ for i := 0; i < len(keys); i++ {
166
+ key := keys[i].Interface()
167
+ v := val.MapIndex(keys[i]).Interface()
168
+ ctx := help.Context.New()
169
+ ctx.Set("@first", i == 0)
170
+ ctx.Set("@last", i == len(keys)-1)
171
+ ctx.Set("@key", key)
172
+ ctx.Set("@value", v)
173
+ s, err := help.BlockWith(ctx)
174
+ if err != nil {
175
+ return "", errors.WithStack(err)
176
+ }
177
+ out.WriteString(s)
178
+ }
179
+ }
180
+ return template.HTML(out.String()), nil
181
+ }
182
+
183
+ func equalHelper(a, b interface{}, help HelperContext) (template.HTML, error) {
184
+ if a == b {
185
+ s, err := help.Block()
186
+ if err != nil {
187
+ return "", err
188
+ }
189
+ return template.HTML(s), nil
190
+ }
191
+ s, err := help.ElseBlock()
192
+ if err != nil {
193
+ return "", err
194
+ }
195
+ return template.HTML(s), nil
196
+ }
197
+
198
+ func notEqualHelper(a, b interface{}, help HelperContext) (template.HTML, error) {
199
+ if a != b {
200
+ s, err := help.Block()
201
+ if err != nil {
202
+ return "", err
203
+ }
204
+ return template.HTML(s), nil
205
+ }
206
+ s, err := help.ElseBlock()
207
+ if err != nil {
208
+ return "", err
209
+ }
210
+ return template.HTML(s), nil
211
+ }
212
+
213
+ func ifHelper(conditional interface{}, help HelperContext) (template.HTML, error) {
214
+ if IsTrue(conditional) {
215
+ s, err := help.Block()
216
+ return template.HTML(s), err
217
+ }
218
+ s, err := help.ElseBlock()
219
+ return template.HTML(s), err
220
+ }
221
+
222
+ // IsTrue returns true if obj is a truthy value.
223
+ func IsTrue(obj interface{}) bool {
224
+ thruth, ok := isTrueValue(reflect.ValueOf(obj))
225
+ if !ok {
226
+ return false
227
+ }
228
+ return thruth
229
+ }
230
+
231
+ // isTrueValue reports whether the value is 'true', in the sense of not the zero of its type,
232
+ // and whether the value has a meaningful truth value
233
+ //
234
+ // NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
235
+ func isTrueValue(val reflect.Value) (truth, ok bool) {
236
+ if !val.IsValid() {
237
+ // Something like var x interface{}, never set. It's a form of nil.
238
+ return false, true
239
+ }
240
+ switch val.Kind() {
241
+ case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
242
+ truth = val.Len() > 0
243
+ case reflect.Bool:
244
+ truth = val.Bool()
245
+ case reflect.Complex64, reflect.Complex128:
246
+ truth = val.Complex() != 0
247
+ case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
248
+ truth = !val.IsNil()
249
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
250
+ truth = val.Int() != 0
251
+ case reflect.Float32, reflect.Float64:
252
+ truth = val.Float() != 0
253
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
254
+ truth = val.Uint() != 0
255
+ case reflect.Struct:
256
+ truth = true // Struct values are always true.
257
+ default:
258
+ return
259
+ }
260
+ return truth, true
261
+ }
262
+
263
+ func unlessHelper(conditional bool, help HelperContext) (template.HTML, error) {
264
+ return ifHelper(!conditional, help)
265
+ }
266
+
267
+ // HelperMap holds onto helpers and validates they are properly formed.
268
+ type HelperMap struct {
269
+ moot *sync.Mutex
270
+ helpers map[string]interface{}
271
+ }
272
+
273
+ // Add a new helper to the map. New Helpers will be validated to ensure they
274
+ // meet the requirements for a helper:
275
+ /*
276
+ func(...) (string) {}
277
+ func(...) (string, error) {}
278
+ func(...) (template.HTML) {}
279
+ func(...) (template.HTML, error) {}
280
+ */
281
+ func (h *HelperMap) Add(key string, helper interface{}) error {
282
+ h.moot.Lock()
283
+ defer h.moot.Unlock()
284
+ err := h.validateHelper(key, helper)
285
+ if err != nil {
286
+ return errors.WithStack(err)
287
+ }
288
+ h.helpers[key] = helper
289
+ return nil
290
+ }
291
+
292
+ func (h *HelperMap) validateHelper(key string, helper interface{}) error {
293
+ ht := reflect.ValueOf(helper).Type()
294
+
295
+ if ht.NumOut() < 1 {
296
+ return errors.WithStack(errors.Errorf("%s must return at least one value ([string|template.HTML], [error])", key))
297
+ }
298
+ so := ht.Out(0).Kind().String()
299
+ if ht.NumOut() > 1 {
300
+ et := ht.Out(1)
301
+ ev := reflect.ValueOf(et)
302
+ ek := fmt.Sprintf("%s", ev.Interface())
303
+ if (so != "string" && so != "template.HTML") || (ek != "error") {
304
+ return errors.WithStack(errors.Errorf("%s must return ([string|template.HTML], [error]), not (%s, %s)", key, so, et.Kind()))
305
+ }
306
+ } else {
307
+ if so != "string" && so != "template.HTML" {
308
+ return errors.WithStack(errors.Errorf("%s must return ([string|template.HTML], [error]), not (%s)", key, so))
309
+ }
310
+ }
311
+ return nil
312
+ }
handlebars/helpers_test.go ADDED
@@ -0,0 +1,466 @@
1
+ package handlebars
2
+
3
+ import (
4
+ "fmt"
5
+ "html/template"
6
+ "strings"
7
+ "testing"
8
+
9
+ "github.com/stretchr/testify/require"
10
+ )
11
+
12
+ func Test_CustomGlobalHelper(t *testing.T) {
13
+ r := require.New(t)
14
+ err := GlobalHelpers.Add("say", func(name string) (string, error) {
15
+ return fmt.Sprintf("say: %s", name), nil
16
+ })
17
+ r.NoError(err)
18
+
19
+ input := `{{say "mark"}}`
20
+ ctx := NewContext()
21
+ s, err := Render(input, ctx)
22
+ r.NoError(err)
23
+ r.Equal("say: mark", s)
24
+ }
25
+
26
+ func Test_CustomGlobalBlockHelper(t *testing.T) {
27
+ r := require.New(t)
28
+ GlobalHelpers.Add("say", func(name string, help HelperContext) (template.HTML, error) {
29
+ ctx := help.Context
30
+ ctx.Set("name", strings.ToUpper(name))
31
+ s, err := help.BlockWith(ctx)
32
+ return template.HTML(s), err
33
+ })
34
+
35
+ input := `
36
+ {{#say "mark"}}
37
+ <h1>{{name}}</h1>
38
+ {{/say}}
39
+ `
40
+ ctx := NewContext()
41
+ s, err := Render(input, ctx)
42
+ r.NoError(err)
43
+ r.Contains(s, "<h1>MARK</h1>")
44
+ }
45
+
46
+ func Test_Helper_Hash_Options(t *testing.T) {
47
+ r := require.New(t)
48
+ GlobalHelpers.Add("say", func(help HelperContext) string {
49
+ return help.Context.Get("name").(string)
50
+ })
51
+
52
+ input := `{{say name="mark"}}`
53
+ ctx := NewContext()
54
+ s, err := Render(input, ctx)
55
+ r.NoError(err)
56
+ r.Equal("mark", s)
57
+ }
58
+
59
+ func Test_Helper_Hash_Options_Many(t *testing.T) {
60
+ r := require.New(t)
61
+ GlobalHelpers.Add("say", func(help HelperContext) string {
62
+ return help.Context.Get("first").(string) + help.Context.Get("last").(string)
63
+ })
64
+
65
+ input := `{{say first=first_name last=last_name}}`
66
+ ctx := NewContext()
67
+ ctx.Set("first_name", "Mark")
68
+ ctx.Set("last_name", "Bates")
69
+ s, err := Render(input, ctx)
70
+ r.NoError(err)
71
+ r.Equal("MarkBates", s)
72
+ }
73
+
74
+ func Test_Helper_Santize_Output(t *testing.T) {
75
+ r := require.New(t)
76
+
77
+ GlobalHelpers.Add("safe", func(help HelperContext) template.HTML {
78
+ return template.HTML("<p>safe</p>")
79
+ })
80
+ GlobalHelpers.Add("unsafe", func(help HelperContext) string {
81
+ return "<b>unsafe</b>"
82
+ })
83
+
84
+ input := `{{safe}}|{{unsafe}}`
85
+ s, err := Render(input, NewContext())
86
+ r.NoError(err)
87
+ r.Equal("<p>safe</p>|&lt;b&gt;unsafe&lt;/b&gt;", s)
88
+ }
89
+
90
+ func Test_JSON_Helper(t *testing.T) {
91
+ r := require.New(t)
92
+
93
+ input := `{{json names}}`
94
+ ctx := NewContext()
95
+ ctx.Set("names", []string{"mark", "bates"})
96
+ s, err := Render(input, ctx)
97
+ r.NoError(err)
98
+ r.Equal(`["mark","bates"]`, s)
99
+ }
100
+
101
+ func Test_If_Helper(t *testing.T) {
102
+ r := require.New(t)
103
+ ctx := NewContext()
104
+ input := `{{#if true}}hi{{/if}}`
105
+
106
+ s, err := Render(input, ctx)
107
+ r.NoError(err)
108
+ r.Equal("hi", s)
109
+ }
110
+
111
+ func Test_If_Helper_false(t *testing.T) {
112
+ r := require.New(t)
113
+ ctx := NewContext()
114
+ input := `{{#if false}}hi{{/if}}`
115
+
116
+ s, err := Render(input, ctx)
117
+ r.NoError(err)
118
+ r.Equal("", s)
119
+ }
120
+
121
+ func Test_If_Helper_NoArgs(t *testing.T) {
122
+ r := require.New(t)
123
+ ctx := NewContext()
124
+ input := `{{#if }}hi{{/if}}`
125
+
126
+ _, err := Render(input, ctx)
127
+ r.Error(err)
128
+ }
129
+
130
+ func Test_If_Helper_Else(t *testing.T) {
131
+ r := require.New(t)
132
+ ctx := NewContext()
133
+ input := `
134
+ {{#if false}}
135
+ hi
136
+ {{ else }}
137
+ bye
138
+ {{/if}}`
139
+
140
+ s, err := Render(input, ctx)
141
+ r.NoError(err)
142
+ r.Contains(s, "bye")
143
+ }
144
+
145
+ func Test_Unless_Helper(t *testing.T) {
146
+ r := require.New(t)
147
+ ctx := NewContext()
148
+ input := `{{#unless false}}hi{{/unless}}`
149
+
150
+ s, err := Render(input, ctx)
151
+ r.NoError(err)
152
+ r.Equal("hi", s)
153
+ }
154
+
155
+ func Test_EqualHelper_True(t *testing.T) {
156
+ r := require.New(t)
157
+ input := `
158
+ {{#eq 1 1}}
159
+ it was true
160
+ {{else}}
161
+ it was false
162
+ {{/eq}}
163
+ `
164
+ s, err := Render(input, NewContext())
165
+ r.NoError(err)
166
+ r.Contains(s, "it was true")
167
+ }
168
+
169
+ func Test_EqualHelper_False(t *testing.T) {
170
+ r := require.New(t)
171
+ input := `
172
+ {{#eq 1 2}}
173
+ it was true
174
+ {{else}}
175
+ it was false
176
+ {{/eq}}
177
+ `
178
+ s, err := Render(input, NewContext())
179
+ r.NoError(err)
180
+ r.Contains(s, "it was false")
181
+ }
182
+
183
+ func Test_EqualHelper_DifferentTypes(t *testing.T) {
184
+ r := require.New(t)
185
+ input := `
186
+ {{#eq 1 "1"}}
187
+ it was true
188
+ {{else}}
189
+ it was false
190
+ {{/eq}}
191
+ `
192
+ s, err := Render(input, NewContext())
193
+ r.NoError(err)
194
+ r.Contains(s, "it was false")
195
+ }
196
+
197
+ func Test_NotEqualHelper_True(t *testing.T) {
198
+ r := require.New(t)
199
+ input := `
200
+ {{#neq 1 1}}
201
+ it was true
202
+ {{else}}
203
+ it was false
204
+ {{/neq}}
205
+ `
206
+ s, err := Render(input, NewContext())
207
+ r.NoError(err)
208
+ r.Contains(s, "it was false")
209
+ }
210
+
211
+ func Test_NotEqualHelper_False(t *testing.T) {
212
+ r := require.New(t)
213
+ input := `
214
+ {{#neq 1 2}}
215
+ it was true
216
+ {{else}}
217
+ it was false
218
+ {{/neq}}
219
+ `
220
+ s, err := Render(input, NewContext())
221
+ r.NoError(err)
222
+ r.Contains(s, "it was true")
223
+ }
224
+
225
+ func Test_NotEqualHelper_DifferentTypes(t *testing.T) {
226
+ r := require.New(t)
227
+ input := `
228
+ {{#neq 1 "1"}}
229
+ it was true
230
+ {{else}}
231
+ it was false
232
+ {{/neq}}
233
+ `
234
+ s, err := Render(input, NewContext())
235
+ r.NoError(err)
236
+ r.Contains(s, "it was true")
237
+ }
238
+
239
+ func Test_Each_Helper_NoArgs(t *testing.T) {
240
+ r := require.New(t)
241
+ ctx := NewContext()
242
+ input := `{{#each }}{{@value}}{{/each}}`
243
+
244
+ _, err := Render(input, ctx)
245
+ r.Error(err)
246
+ }
247
+
248
+ func Test_Each_Helper(t *testing.T) {
249
+ r := require.New(t)
250
+ ctx := NewContext()
251
+ ctx.Set("names", []string{"mark", "bates"})
252
+ input := `{{#each names }}<p>{{@value}}</p>{{/each}}`
253
+
254
+ s, err := Render(input, ctx)
255
+ r.NoError(err)
256
+ r.Equal("<p>mark</p><p>bates</p>", s)
257
+ }
258
+
259
+ func Test_Each_Helper_Index(t *testing.T) {
260
+ r := require.New(t)
261
+ ctx := NewContext()
262
+ ctx.Set("names", []string{"mark", "bates"})
263
+ input := `{{#each names }}<p>{{@index}}</p>{{/each}}`
264
+
265
+ s, err := Render(input, ctx)
266
+ r.NoError(err)
267
+ r.Equal("<p>0</p><p>1</p>", s)
268
+ }
269
+
270
+ func Test_Each_Helper_As(t *testing.T) {
271
+ r := require.New(t)
272
+ ctx := NewContext()
273
+ ctx.Set("names", []string{"mark", "bates"})
274
+ input := `{{#each names as |ind name| }}<p>{{ind}}-{{name}}</p>{{/each}}`
275
+
276
+ s, err := Render(input, ctx)
277
+ r.NoError(err)
278
+ r.Equal("<p>0-mark</p><p>1-bates</p>", s)
279
+ }
280
+
281
+ func Test_Each_Helper_As_Nested(t *testing.T) {
282
+ r := require.New(t)
283
+ ctx := NewContext()
284
+ users := []struct {
285
+ Name string
286
+ Initials []string
287
+ }{
288
+ {Name: "Mark", Initials: []string{"M", "F", "B"}},
289
+ {Name: "Rachel", Initials: []string{"R", "A", "B"}},
290
+ }
291
+ ctx.Set("users", users)
292
+ input := `
293
+ {{#each users as |user|}}
294
+ <h1>{{user.Name}}</h1>
295
+ {{#each user.Initials as |i|}}
296
+ {{user.Name}}: {{i}}
297
+ {{/each}}
298
+ {{/each}}
299
+ `
300
+
301
+ s, err := Render(input, ctx)
302
+ r.NoError(err)
303
+ r.Contains(s, "<h1>Mark</h1>")
304
+ r.Contains(s, "Mark: M")
305
+ r.Contains(s, "Mark: F")
306
+ r.Contains(s, "Mark: B")
307
+ r.Contains(s, "<h1>Rachel</h1>")
308
+ r.Contains(s, "Rachel: R")
309
+ r.Contains(s, "Rachel: A")
310
+ r.Contains(s, "Rachel: B")
311
+ }
312
+
313
+ func Test_Each_Helper_SlicePtr(t *testing.T) {
314
+ r := require.New(t)
315
+ type user struct {
316
+ Name string
317
+ }
318
+ type users []user
319
+
320
+ us := &users{
321
+ {Name: "Mark"},
322
+ {Name: "Rachel"},
323
+ }
324
+
325
+ ctx := NewContext()
326
+ ctx.Set("users", us)
327
+
328
+ input := `
329
+ {{#each users as |user|}}
330
+ {{user.Name}}
331
+ {{/each}}
332
+ `
333
+ s, err := Render(input, ctx)
334
+ r.NoError(err)
335
+ r.Contains(s, "Mark")
336
+ r.Contains(s, "Rachel")
337
+ }
338
+
339
+ func Test_Each_Helper_Map(t *testing.T) {
340
+ r := require.New(t)
341
+ ctx := NewContext()
342
+ data := map[string]string{
343
+ "a": "A",
344
+ "b": "B",
345
+ }
346
+ ctx.Set("letters", data)
347
+ input := `
348
+ {{#each letters}}
349
+ {{@key}}:{{@value}}
350
+ {{/each}}
351
+ `
352
+
353
+ s, err := Render(input, ctx)
354
+ r.NoError(err)
355
+ for k, v := range data {
356
+ r.Contains(s, fmt.Sprintf("%s:%s", k, v))
357
+ }
358
+ }
359
+
360
+ func Test_Each_Helper_Map_As(t *testing.T) {
361
+ r := require.New(t)
362
+ ctx := NewContext()
363
+ data := map[string]string{
364
+ "a": "A",
365
+ "b": "B",
366
+ }
367
+ ctx.Set("letters", data)
368
+ input := `
369
+ {{#each letters as |k v|}}
370
+ {{k}}:{{v}}
371
+ {{/each}}
372
+ `
373
+
374
+ s, err := Render(input, ctx)
375
+ r.NoError(err)
376
+ for k, v := range data {
377
+ r.Contains(s, fmt.Sprintf("%s:%s", k, v))
378
+ }
379
+ }
380
+
381
+ func Test_Each_Helper_Else(t *testing.T) {
382
+ r := require.New(t)
383
+ ctx := NewContext()
384
+ data := map[string]string{}
385
+ ctx.Set("letters", data)
386
+ input := `
387
+ {{#each letters as |k v|}}
388
+ {{k}}:{{v}}
389
+ {{else}}
390
+ no letters
391
+ {{/each}}
392
+ `
393
+
394
+ s, err := Render(input, ctx)
395
+ r.NoError(err)
396
+ r.Contains(s, "no letters")
397
+ }
398
+
399
+ func Test_Each_Helper_Else_Collection(t *testing.T) {
400
+ r := require.New(t)
401
+ ctx := NewContext()
402
+ data := map[string][]string{}
403
+ ctx.Set("collection", data)
404
+
405
+ input := `
406
+ {{#each collection.emptykey as |k v|}}
407
+ {{k}}:{{v}}
408
+ {{else}}
409
+ no letters
410
+ {{/each}}
411
+ `
412
+
413
+ s, err := Render(input, ctx)
414
+ r.NoError(err)
415
+ r.Contains(s, "no letters")
416
+ }
417
+
418
+ func Test_Each_Helper_Else_CollectionMap(t *testing.T) {
419
+ r := require.New(t)
420
+ ctx := NewContext()
421
+ data := map[string]map[string]string{
422
+ "emptykey": map[string]string{},
423
+ }
424
+
425
+ ctx.Set("collection", data)
426
+
427
+ input := `
428
+ {{#each collection.emptykey.something as |k v|}}
429
+ {{k}}:{{v}}
430
+ {{else}}
431
+ no letters
432
+ {{/each}}
433
+ `
434
+
435
+ s, err := Render(input, ctx)
436
+ r.NoError(err)
437
+ r.Contains(s, "no letters")
438
+ }
439
+
440
+ func Test_HelperMap_Add(t *testing.T) {
441
+ r := require.New(t)
442
+ err := GlobalHelpers.Add("foo", func(help HelperContext) (string, error) {
443
+ return "", nil
444
+ })
445
+ r.NoError(err)
446
+ }
447
+
448
+ func Test_HelperMap_Add_Invalid_NoReturn(t *testing.T) {
449
+ r := require.New(t)
450
+ err := GlobalHelpers.Add("foo", func(help HelperContext) {})
451
+ r.Error(err)
452
+ r.Contains(err.Error(), "must return at least one")
453
+ }
454
+
455
+ func Test_HelperMap_Add_Invalid_ReturnTypes(t *testing.T) {
456
+ r := require.New(t)
457
+ err := GlobalHelpers.Add("foo", func(help HelperContext) (string, string) {
458
+ return "", ""
459
+ })
460
+ r.Error(err)
461
+ r.Contains(err.Error(), "foo must return ([string|template.HTML], [error]), not (string, string)")
462
+
463
+ err = GlobalHelpers.Add("foo", func(help HelperContext) int { return 1 })
464
+ r.Error(err)
465
+ r.Contains(err.Error(), "foo must return ([string|template.HTML], [error]), not (int)")
466
+ }
handlebars/template.go ADDED
@@ -0,0 +1,95 @@
1
+ package handlebars
2
+
3
+ import (
4
+ "sync"
5
+
6
+ "github.com/aymerick/raymond/ast"
7
+ "github.com/aymerick/raymond/parser"
8
+ "github.com/pkg/errors"
9
+ )
10
+
11
+ // Template represents an input and helpers to be used
12
+ // to evaluate and render the input.
13
+ type Template struct {
14
+ Input string
15
+ program *ast.Program
16
+ }
17
+
18
+ // NewTemplate from the input string.
19
+ func NewTemplate(input string) (*Template, error) {
20
+ t := &Template{
21
+ Input: input,
22
+ }
23
+ err := t.Parse()
24
+ if err != nil {
25
+ return t, errors.WithStack(err)
26
+ }
27
+ return t, nil
28
+ }
29
+
30
+ // Parse the template this can be called many times
31
+ // as a successful result is cached and is used on subsequent
32
+ // uses.
33
+ func (t *Template) Parse() error {
34
+ if t.program != nil {
35
+ return nil
36
+ }
37
+ program, err := parser.Parse(t.Input)
38
+ if err != nil {
39
+ return errors.WithStack(err)
40
+ }
41
+ t.program = program
42
+ return nil
43
+ }
44
+
45
+ // Exec the template using the content and return the results
46
+ func (t *Template) Exec(ctx *Context) (string, error) {
47
+ err := t.Parse()
48
+ if err != nil {
49
+ return "", errors.WithStack(err)
50
+ }
51
+ v := newEvalVisitor(t, ctx)
52
+ r := t.program.Accept(v)
53
+ switch rp := r.(type) {
54
+ case string:
55
+ return rp, nil
56
+ case error:
57
+ return "", rp
58
+ case nil:
59
+ return "", nil
60
+ default:
61
+ return "", errors.WithStack(errors.Errorf("unsupport eval return format %T: %+v", r, r))
62
+ }
63
+ }
64
+
65
+ var cache = map[string]*Template{}
66
+ var moot = &sync.Mutex{}
67
+
68
+ // Parse an input string and return a Template.
69
+ func Parse(input string) (*Template, error) {
70
+ moot.Lock()
71
+ defer moot.Unlock()
72
+ if t, ok := cache[input]; ok {
73
+ return t, nil
74
+ }
75
+ t, err := NewTemplate(input)
76
+
77
+ if err == nil {
78
+ cache[input] = t
79
+ }
80
+
81
+ if err != nil {
82
+ return t, errors.WithStack(err)
83
+ }
84
+
85
+ return t, nil
86
+ }
87
+
88
+ // Render a string using the given the context.
89
+ func Render(input string, ctx *Context) (string, error) {
90
+ t, err := Parse(input)
91
+ if err != nil {
92
+ return "", errors.WithStack(err)
93
+ }
94
+ return t.Exec(ctx)
95
+ }
handlebars/template_test.go ADDED
@@ -0,0 +1,89 @@
1
+ package handlebars
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/stretchr/testify/require"
7
+ )
8
+
9
+ func Test_Render(t *testing.T) {
10
+ r := require.New(t)
11
+
12
+ ctx := NewContext()
13
+ ctx.Set("name", "Tim")
14
+ s, err := Render("{{name}}", ctx)
15
+ r.NoError(err)
16
+ r.Equal("Tim", s)
17
+ }
18
+
19
+ func Test_Render_with_Content(t *testing.T) {
20
+ r := require.New(t)
21
+
22
+ ctx := NewContext()
23
+ ctx.Set("name", "Tim")
24
+ s, err := Render("<p>{{name}}</p>", ctx)
25
+ r.NoError(err)
26
+ r.Equal("<p>Tim</p>", s)
27
+ }
28
+
29
+ func Test_Render_Unknown_Value(t *testing.T) {
30
+ r := require.New(t)
31
+
32
+ ctx := NewContext()
33
+ _, err := Render("<p>{{name}}</p>", ctx)
34
+ r.Error(err)
35
+ r.Equal("could not find value for name [line 1:3]", err.Error())
36
+ }
37
+
38
+ func Test_Render_with_String(t *testing.T) {
39
+ r := require.New(t)
40
+
41
+ ctx := NewContext()
42
+ s, err := Render(`<p>{{"Tim"}}</p>`, ctx)
43
+ r.NoError(err)
44
+ r.Equal("<p>Tim</p>", s)
45
+ }
46
+
47
+ func Test_Render_with_Math(t *testing.T) {
48
+ r := require.New(t)
49
+
50
+ ctx := NewContext()
51
+ _, err := Render(`<p>{{2 + 1}}</p>`, ctx)
52
+ r.Error(err)
53
+ }
54
+
55
+ func Test_Render_with_Comments(t *testing.T) {
56
+ r := require.New(t)
57
+ ctx := NewContext()
58
+ s, err := Render(`<p><!-- comment --></p>`, ctx)
59
+ r.NoError(err)
60
+ r.Equal("<p><!-- comment --></p>", s)
61
+ }
62
+
63
+ func Test_Render_with_Func(t *testing.T) {
64
+ r := require.New(t)
65
+ ctx := NewContext()
66
+ ctx.Set("user", user{First: "Mark", Last: "Bates"})
67
+ s, err := Render("{{user.FullName}}", ctx)
68
+ r.NoError(err)
69
+ r.Equal("Mark Bates", s)
70
+ }
71
+
72
+ func Test_Render_Array(t *testing.T) {
73
+ r := require.New(t)
74
+
75
+ ctx := NewContext()
76
+ ctx.Set("names", []string{"mark", "bates"})
77
+ s, err := Render("{{names}}", ctx)
78
+ r.NoError(err)
79
+ r.Equal("mark bates", s)
80
+ }
81
+
82
+ type user struct {
83
+ First string
84
+ Last string
85
+ }
86
+
87
+ func (u user) FullName() string {
88
+ return u.First + " " + u.Last
89
+ }
http.go CHANGED
@@ -18,8 +18,8 @@ import (
18
18
  "time"
19
19
 
20
20
  "github.com/go-playground/validator/v10"
21
- "github.com/gobuffalo/velvet"
22
21
  "github.com/gorilla/mux"
22
+ "github.com/pyros2097/gromer/handlebars"
23
23
  "github.com/rs/zerolog"
24
24
  "github.com/rs/zerolog/log"
25
25
  "github.com/rs/zerolog/pkgerrors"
@@ -42,11 +42,11 @@ type RouteDefinition struct {
42
42
  type HtmlContent string
43
43
  type HandlersTemplate struct {
44
44
  text string
45
- ctx *velvet.Context
45
+ ctx *handlebars.Context
46
46
  }
47
47
 
48
48
  func Html(tpl string) *HandlersTemplate {
49
- return &HandlersTemplate{text: tpl, ctx: velvet.NewContext()}
49
+ return &HandlersTemplate{text: tpl, ctx: handlebars.NewContext()}
50
50
  }
51
51
 
52
52
  func (t *HandlersTemplate) Prop(key string, v any) *HandlersTemplate {
@@ -63,7 +63,7 @@ func (t *HandlersTemplate) Props(args ...any) *HandlersTemplate {
63
63
  }
64
64
 
65
65
  func (t *HandlersTemplate) Render(args ...any) (HtmlContent, int, error) {
66
- s, err := velvet.Render(t.text, t.ctx)
66
+ s, err := handlebars.Render(t.text, t.ctx)
67
67
  if err != nil {
68
68
  return HtmlContent("Server Erorr"), 500, err
69
69
  }
@@ -83,7 +83,7 @@ func RegisterComponent(fn interface{}, props ...string) {
83
83
  name := GetFunctionName(fn)
84
84
  fnType := reflect.TypeOf(fn)
85
85
  fnValue := reflect.ValueOf(fn)
86
- velvet.Helpers.Add(name, func(title string, c velvet.HelperContext) (template.HTML, error) {
86
+ handlebars.GlobalHelpers.Add(name, func(title string, c handlebars.HelperContext) (template.HTML, error) {
87
87
  s, err := c.Block()
88
88
  if err != nil {
89
89
  return "", err
@@ -97,7 +97,7 @@ func RegisterComponent(fn interface{}, props ...string) {
97
97
  }
98
98
  res := fnValue.Call(args)
99
99
  tpl := res[0].Interface().(*HandlersTemplate)
100
- comp, err := velvet.Render(tpl.text, tpl.ctx)
100
+ comp, err := handlebars.Render(tpl.text, tpl.ctx)
101
101
  if err != nil {
102
102
  return "", err
103
103
  }
readme.md CHANGED
@@ -3,9 +3,10 @@
3
3
  [![Version](https://badge.fury.io/gh/pyros2097%2Fgromer.svg)](https://github.com/pyros2097/gromer)
4
4
 
5
5
  **gromer** is a framework and cli to build web apps in golang.
6
- It uses a declarative syntax using funcs that allows creating and dealing with HTML elements only by using Go, and without writing any HTML markup.
6
+ It uses a declarative syntax using funcs that allows creating and dealing with HTML components and pages using templates.
7
7
 
8
8
  It also generates http handlers for your routes which follow a particular folder structure. Similar to other frameworks like nextjs, sveltekit.
9
+ These handlers are also normal functions and can be imported in other packages directly ((inspired by [Encore](https://encore.dev/)).
9
10
 
10
11
  # Install
11
12
 
@@ -13,8 +14,6 @@ It also generates http handlers for your routes which follow a particular folder
13
14
  go get -u -v github.com/pyros2097/gromer/cmd/gromer
14
15
  ```
15
16
 
16
- You can install this plugin https://marketplace.visualstudio.com/items?itemName=pyros2097.vscode-go-inline-html for syntax highlighting html templates in golang.
17
-
18
17
  # Using
19
18
 
20
19
  You need to follow this directory structure similar to nextjs for the api route handlers to be generated
@@ -22,3 +21,136 @@ You need to follow this directory structure similar to nextjs for the api route
22
21
  Take a look at the example for now,
23
22
 
24
23
  [Example](https://github.com/pyros2097/gromer/tree/master/example)
24
+
25
+ # Templating
26
+
27
+ Gromer uses a handlebars like templating language for components and pages. This is a modified version of this package [velvet](https://github.com/gobuffalo/velvet)
28
+ If you know handlebars, you basically know how to use it.
29
+
30
+ You can install this plugin [VSCode Go inline html plugin](https://marketplace.visualstudio.com/items?itemName=pyros2097.vscode-go-inline-html) for syntax highlighting the templates.
31
+
32
+ Let's assume you have a template (a string of some kind):
33
+
34
+ ```handlebars
35
+ <!-- some input -->
36
+ <h1>{{ name }}</h1>
37
+ <ul>
38
+ {{#each names}}
39
+ <li>{{ @value }}</li>
40
+ {{/each}}
41
+ </ul>
42
+ ```
43
+
44
+ Given that string, you can render the template like such:
45
+
46
+ ```html
47
+ <h1>Mark</h1>
48
+ <ul>
49
+ <li>John</li>
50
+ <li>Paul</li>
51
+ <li>George</li>
52
+ <li>Ringo</li>
53
+ </ul>
54
+ ```
55
+
56
+ ### If Statements
57
+
58
+ ```handlebars
59
+ {{#if true }}
60
+ render this
61
+ {{/if}}
62
+ ```
63
+
64
+ #### Else Statements
65
+
66
+ ```handlebars
67
+ {{#if false }}
68
+ won't render this
69
+ {{ else }}
70
+ render this
71
+ {{/if}}
72
+ ```
73
+
74
+ #### Unless Statements
75
+
76
+ ```handlebars
77
+ {{#unless true }}
78
+ won't render this
79
+ {{/unless}}
80
+ ```
81
+
82
+ ### Each Statements
83
+
84
+ #### Arrays
85
+
86
+ When looping through `arrays` or `slices`, the block being looped through will be access to the "global" context, as well as have four new variables available within that block:
87
+
88
+ * `@first` [`bool`] - is this the first pass through the iteration?
89
+ * `@last` [`bool`] - is this the last pass through the iteration?
90
+ * `@index` [`int`] - the counter of where in the loop you are, starting with `0`.
91
+ * `@value` - the current element in the array or slice that is being iterated over.
92
+
93
+ ```handlebars
94
+ <ul>
95
+ {{#each names}}
96
+ <li>{{ @index }} - {{ @value }}</li>
97
+ {{/each}}
98
+ </ul>
99
+ ```
100
+
101
+ By using "block parameters" you can change the "key" of the element being accessed from `@value` to a key of your choosing.
102
+
103
+ ```handlebars
104
+ <ul>
105
+ {{#each names as |name|}}
106
+ <li>{{ name }}</li>
107
+ {{/each}}
108
+ </ul>
109
+ ```
110
+
111
+ To change both the key and the index name you can pass two "block parameters"; the first being the new name for the index and the second being the name for the element.
112
+
113
+ ```handlebars
114
+ <ul>
115
+ {{#each names as |index, name|}}
116
+ <li>{{ index }} - {{ name }}</li>
117
+ {{/each}}
118
+ </ul>
119
+ ```
120
+
121
+ #### Maps
122
+
123
+ Looping through `maps` using the `each` helper is also supported, and follows very similar guidelines to looping through `arrays`.
124
+
125
+ * `@first` [`bool`] - is this the first pass through the iteration?
126
+ * `@last` [`bool`] - is this the last pass through the iteration?
127
+ * `@key` - the key of the pair being accessed.
128
+ * `@value` - the value of the pair being accessed.
129
+
130
+ ```handlebars
131
+ <ul>
132
+ {{#each users}}
133
+ <li>{{ @key }} - {{ @value }}</li>
134
+ {{/each}}
135
+ </ul>
136
+ ```
137
+
138
+ By using "block parameters" you can change the "key" of the element being accessed from `@value` to a key of your choosing.
139
+
140
+ ```handlebars
141
+ <ul>
142
+ {{#each users as |user|}}
143
+ <li>{{ @key }} - {{ user }}</li>
144
+ {{/each}}
145
+ </ul>
146
+ ```
147
+
148
+ To change both the key and the value name you can pass two "block parameters"; the first being the new name for the key and the second being the name for the value.
149
+
150
+ ```handlebars
151
+ <ul>
152
+ {{#each users as |key, user|}}
153
+ <li>{{ key }} - {{ user }}</li>
154
+ {{/each}}
155
+ </ul>
156
+ ```