~repos /gromer
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 +4 -4
- example/config/config.go +3 -1
- example/pages/about/get.go +1 -1
- go.mod +0 -1
- go.sum +0 -2
- handlebars/README.md +0 -0
- handlebars/context.go +79 -0
- handlebars/context_test.go +47 -0
- handlebars/eval.go +385 -0
- handlebars/eval_test.go +72 -0
- handlebars/helpers.go +312 -0
- handlebars/helpers_test.go +466 -0
- handlebars/template.go +95 -0
- handlebars/template_test.go +89 -0
- http.go +6 -6
- readme.md +135 -3
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 =
|
|
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 :=
|
|
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 :=
|
|
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
|
-
|
|
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 "
|
|
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>|<b>unsafe</b>", 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 *
|
|
45
|
+
ctx *handlebars.Context
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
func Html(tpl string) *HandlersTemplate {
|
|
49
|
-
return &HandlersTemplate{text: tpl, ctx:
|
|
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 :=
|
|
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
|
-
|
|
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 :=
|
|
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
|
[](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
|
|
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
|
+
```
|