~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.
5050bbb1
—
Peter John 3 years ago
remove handlebars
- _example/components/page.go +10 -17
- _example/components/todo.go +12 -15
- _example/containers/TodoCount.go +9 -15
- _example/containers/TodoList.go +9 -8
- _example/main.go +6 -5
- _example/pages/404/get.go +4 -4
- _example/pages/about/get.go +4 -4
- _example/pages/get.go +12 -13
- _example/pages/post.go +22 -24
- api_explorer.go +0 -275
- gsx/template.go +11 -0
- handlebars/README.md +0 -132
- handlebars/context.go +0 -79
- handlebars/context_test.go +0 -47
- handlebars/eval.go +0 -385
- handlebars/eval_test.go +0 -73
- handlebars/helpers.go +0 -312
- handlebars/helpers_test.go +0 -466
- handlebars/template.go +0 -125
- handlebars/template_test.go +0 -89
- http.go +119 -117
- readme.md +4 -4
_example/components/page.go
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
package components
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"html/template"
|
|
5
|
-
|
|
6
|
-
. "github.com/pyros2097/gromer/
|
|
4
|
+
. "github.com/pyros2097/gromer/gsx"
|
|
7
5
|
)
|
|
8
6
|
|
|
9
7
|
var _ = Css(`
|
|
@@ -219,31 +217,26 @@ var _ = Css(`
|
|
|
219
217
|
}
|
|
220
218
|
`)
|
|
221
219
|
|
|
222
|
-
type PageProps struct {
|
|
223
|
-
Title string `json:"title"`
|
|
224
|
-
Children template.HTML `json:"children"`
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
func Page(
|
|
220
|
+
func Page(h Html, title string) string {
|
|
228
|
-
return
|
|
221
|
+
return h.Render(`
|
|
229
222
|
<!DOCTYPE html>
|
|
230
223
|
<html lang="en">
|
|
231
224
|
<head>
|
|
232
225
|
<meta charset="UTF-8" />
|
|
233
226
|
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
|
234
227
|
<meta content="utf-8" http-equiv="encoding" />
|
|
235
|
-
<title>{
|
|
228
|
+
<title>{title}</title>
|
|
236
|
-
<meta name="description" content="{
|
|
229
|
+
<meta name="description" content="{title}" />
|
|
237
230
|
<meta name="author" content="pyrossh" />
|
|
238
231
|
<meta name="keywords" content="pyros.sh, pyrossh, gromer" />
|
|
239
232
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover" />
|
|
240
|
-
<link rel="icon" href="{
|
|
233
|
+
<link rel="icon" href="{GetAssetUrl "images/icon.png"}" />
|
|
241
|
-
<link rel="stylesheet" href="{
|
|
234
|
+
<link rel="stylesheet" href="{GetStylesUrl}" />
|
|
242
|
-
<script src="{
|
|
235
|
+
<script src="{GetAlpineJsUrl}"></script>
|
|
243
|
-
<script src="{
|
|
236
|
+
<script src="{GetHtmxJsUrl}" defer=""></script>
|
|
244
237
|
</head>
|
|
245
238
|
<body>
|
|
246
|
-
{
|
|
239
|
+
{children}
|
|
247
240
|
</body>
|
|
248
241
|
</html>
|
|
249
242
|
`)
|
_example/components/todo.go
CHANGED
|
@@ -2,33 +2,30 @@ package components
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"github.com/pyros2097/gromer/_example/services/todos"
|
|
5
|
-
. "github.com/pyros2097/gromer/
|
|
5
|
+
. "github.com/pyros2097/gromer/gsx"
|
|
6
6
|
)
|
|
7
7
|
|
|
8
8
|
var _ = Css(`
|
|
9
9
|
`)
|
|
10
10
|
|
|
11
|
-
type TodoProps struct {
|
|
12
|
-
Todo *todos.Todo `json:"todo"`
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
func Todo(
|
|
11
|
+
func Todo(h Html, todo *todos.Todo) string {
|
|
16
|
-
return
|
|
12
|
+
return h.Render(`
|
|
17
|
-
<li id="todo-{
|
|
13
|
+
<li id="todo-{todo.ID}" class={{ completed: todo.Completed }}>
|
|
18
14
|
<div class="view">
|
|
19
|
-
<form hx-target="#todo-{
|
|
15
|
+
<form hx-target="#todo-{todo.ID}" hx-swap="outerHTML">
|
|
20
16
|
<input type="hidden" name="intent" value="complete" />
|
|
21
|
-
<input type="hidden" name="id" value="{
|
|
17
|
+
<input type="hidden" name="id" value="{todo.ID}" />
|
|
22
|
-
<input hx-post="/" class="checkbox" type="checkbox" {{
|
|
18
|
+
<input hx-post="/" class="checkbox" type="checkbox" checked={{ completed: todo.Completed }} />
|
|
23
19
|
</form>
|
|
24
|
-
<label>{
|
|
20
|
+
<label>{todo.Text}</label>
|
|
25
|
-
<!-- <label hx-get="/todos/edit/{{ props.Todo.ID }}" hx-target="#todo-{{ props.Todo.ID }}" hx-swap="outerHTML">{{ props.Todo.Text }}</label> -->
|
|
26
|
-
<form hx-post="/" hx-target="#todo-{
|
|
21
|
+
<form hx-post="/" hx-target="#todo-{todo.ID}" hx-swap="delete">
|
|
27
22
|
<input type="hidden" name="intent" value="delete" />
|
|
28
|
-
<input type="hidden" name="id" value="{
|
|
23
|
+
<input type="hidden" name="id" value="{todo.ID}" />
|
|
29
24
|
<button class="destroy"></button>
|
|
30
25
|
</form>
|
|
31
26
|
</div>
|
|
32
27
|
</li>
|
|
33
28
|
`)
|
|
34
29
|
}
|
|
30
|
+
|
|
31
|
+
// <!-- <label hx-get="/todos/edit/{todo.ID}" hx-target="#todo-{todo.ID}" hx-swap="outerHTML">{{ props.Todo.Text }}</label> -->
|
_example/containers/TodoCount.go
CHANGED
|
@@ -3,9 +3,8 @@ package containers
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
|
|
6
|
-
. "github.com/pyros2097/gromer"
|
|
7
6
|
"github.com/pyros2097/gromer/_example/services/todos"
|
|
8
|
-
. "github.com/pyros2097/gromer/
|
|
7
|
+
. "github.com/pyros2097/gromer/gsx"
|
|
9
8
|
)
|
|
10
9
|
|
|
11
10
|
var _ = Css(`
|
|
@@ -19,23 +18,18 @@ var _ = Css(`
|
|
|
19
18
|
}
|
|
20
19
|
`)
|
|
21
20
|
|
|
22
|
-
type TodoCountProps struct {
|
|
23
|
-
Page int `json:"page"`
|
|
24
|
-
Filter string `json:"filter"`
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
func TodoCount(ctx context.Context,
|
|
21
|
+
func TodoCount(h Html, ctx context.Context, filter string) (string, error) {
|
|
28
|
-
index := Default(props.Page, 1)
|
|
29
22
|
todos, err := todos.GetAllTodo(ctx, todos.GetAllTodoParams{
|
|
30
|
-
Filter:
|
|
23
|
+
Filter: filter,
|
|
31
|
-
Limit:
|
|
24
|
+
Limit: 1000,
|
|
32
25
|
})
|
|
33
26
|
if err != nil {
|
|
34
|
-
return
|
|
27
|
+
return "", err
|
|
35
28
|
}
|
|
29
|
+
h["count"] = len(todos)
|
|
36
|
-
return
|
|
30
|
+
return h.Render(`
|
|
37
31
|
<span class="todo-count" id="todo-count" hx-swap-oob="true">
|
|
38
|
-
<strong>{
|
|
32
|
+
<strong>{count}</strong> item left
|
|
39
33
|
</span>
|
|
40
|
-
`)
|
|
34
|
+
`), nil
|
|
41
35
|
}
|
_example/containers/TodoList.go
CHANGED
|
@@ -5,7 +5,7 @@ import (
|
|
|
5
5
|
|
|
6
6
|
. "github.com/pyros2097/gromer"
|
|
7
7
|
"github.com/pyros2097/gromer/_example/services/todos"
|
|
8
|
-
. "github.com/pyros2097/gromer/
|
|
8
|
+
. "github.com/pyros2097/gromer/gsx"
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
var _ = Css(`
|
|
@@ -135,20 +135,21 @@ type TodoListProps struct {
|
|
|
135
135
|
Filter string `json:"filter"`
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
func TodoList(ctx context.Context, props TodoListProps) (
|
|
138
|
+
func TodoList(h Html, ctx context.Context, props TodoListProps) (string, error) {
|
|
139
139
|
index := Default(props.Page, 1)
|
|
140
140
|
todos, err := todos.GetAllTodo(ctx, todos.GetAllTodoParams{
|
|
141
141
|
Filter: props.Filter,
|
|
142
142
|
Limit: index,
|
|
143
143
|
})
|
|
144
144
|
if err != nil {
|
|
145
|
-
return
|
|
145
|
+
return "", err
|
|
146
146
|
}
|
|
147
|
+
h["todos"] = todos
|
|
147
|
-
return
|
|
148
|
+
return h.Render(`
|
|
148
149
|
<ul id="todo-list" class="relative">
|
|
149
|
-
|
|
150
|
+
<For key="todos" itemKey="todo">
|
|
150
|
-
|
|
151
|
+
<Todo key="todo"></Todo>
|
|
151
|
-
|
|
152
|
+
</For>
|
|
152
153
|
</ul>
|
|
153
|
-
`)
|
|
154
|
+
`), nil
|
|
154
155
|
}
|
_example/main.go
CHANGED
|
@@ -13,15 +13,16 @@ import (
|
|
|
13
13
|
"github.com/pyros2097/gromer/_example/pages/404"
|
|
14
14
|
"github.com/pyros2097/gromer/_example/pages"
|
|
15
15
|
"github.com/pyros2097/gromer/_example/pages/about"
|
|
16
|
+
"github.com/pyros2097/gromer/gsx"
|
|
16
17
|
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
func init() {
|
|
20
|
-
|
|
21
|
+
gsx.RegisterComponent(components.Page)
|
|
21
|
-
|
|
22
|
+
gsx.RegisterComponent(components.Todo)
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
gsx.RegisterComponent(containers.TodoCount)
|
|
24
|
-
|
|
25
|
+
gsx.RegisterComponent(containers.TodoList)
|
|
25
26
|
gromer.RegisterAssets(assets.FS)
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -38,7 +39,7 @@ func main() {
|
|
|
38
39
|
gromer.StylesRoute(staticRouter, "/styles.css")
|
|
39
40
|
|
|
40
41
|
pageRouter := baseRouter.NewRoute().Subrouter()
|
|
41
|
-
gromer.ApiExplorerRoute(pageRouter, "/explorer")
|
|
42
|
+
// gromer.ApiExplorerRoute(pageRouter, "/explorer")
|
|
42
43
|
gromer.Handle(pageRouter, "GET", "/", pages.GET)
|
|
43
44
|
gromer.Handle(pageRouter, "POST", "/", pages.POST)
|
|
44
45
|
gromer.Handle(pageRouter, "GET", "/about", about.GET)
|
_example/pages/404/get.go
CHANGED
|
@@ -3,11 +3,11 @@ package not_found_404
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
|
|
6
|
-
. "github.com/pyros2097/gromer/
|
|
6
|
+
. "github.com/pyros2097/gromer/gsx"
|
|
7
7
|
)
|
|
8
8
|
|
|
9
|
-
func GET(c context.Context) (
|
|
9
|
+
func GET(h Html, c context.Context) (string, int, error) {
|
|
10
|
-
return
|
|
10
|
+
return h.Render(`
|
|
11
11
|
{{#Page title="Page Not Found"}}
|
|
12
12
|
{{#Header}}{{/Header}}
|
|
13
13
|
<main class="box center">
|
|
@@ -17,5 +17,5 @@ func GET(c context.Context) (HtmlContent, int, error) {
|
|
|
17
17
|
</h1>
|
|
18
18
|
</main>
|
|
19
19
|
{{/Page}}
|
|
20
|
-
|
|
20
|
+
`), 404, nil
|
|
21
21
|
}
|
_example/pages/about/get.go
CHANGED
|
@@ -3,11 +3,11 @@ package about
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
|
|
6
|
-
. "github.com/pyros2097/gromer/
|
|
6
|
+
. "github.com/pyros2097/gromer/gsx"
|
|
7
7
|
)
|
|
8
8
|
|
|
9
|
-
func GET(c context.Context) (
|
|
9
|
+
func GET(h Html, c context.Context) (string, int, error) {
|
|
10
|
-
return
|
|
10
|
+
return h.Render(`
|
|
11
11
|
{{#Page title="About me"}}
|
|
12
12
|
<div class="flex flex-col justify-center items-center">
|
|
13
13
|
{{#Header}}
|
|
@@ -16,5 +16,5 @@ func GET(c context.Context) (HtmlContent, int, error) {
|
|
|
16
16
|
<h1>About Me</h1>
|
|
17
17
|
</div>
|
|
18
18
|
{{/Page}}
|
|
19
|
-
|
|
19
|
+
`), 200, nil
|
|
20
20
|
}
|
_example/pages/get.go
CHANGED
|
@@ -4,7 +4,7 @@ import (
|
|
|
4
4
|
"context"
|
|
5
5
|
|
|
6
6
|
_ "github.com/pyros2097/gromer/_example/components"
|
|
7
|
-
. "github.com/pyros2097/gromer/
|
|
7
|
+
. "github.com/pyros2097/gromer/gsx"
|
|
8
8
|
)
|
|
9
9
|
|
|
10
10
|
var _ = Css(`
|
|
@@ -61,24 +61,26 @@ type GetParams struct {
|
|
|
61
61
|
Filter string `json:"filter"`
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
func GET(ctx context.Context, params GetParams) (
|
|
64
|
+
func GET(h Html, ctx context.Context, params GetParams) (string, int, error) {
|
|
65
|
+
h["page"] = params.Page
|
|
66
|
+
h["filter"] = params.Filter
|
|
65
|
-
return
|
|
67
|
+
return h.Render(`
|
|
66
|
-
|
|
68
|
+
<Page title="gromer example">
|
|
67
69
|
<section class="todoapp">
|
|
68
70
|
<header class="header">
|
|
69
71
|
<h1>todos</h1>
|
|
70
72
|
<form hx-post="/" hx-target="#todo-list" hx-swap="afterbegin" _="on htmx:afterOnLoad set #text.value to ''">
|
|
71
|
-
<input type="hidden" name="intent" value="create"
|
|
73
|
+
<input type="hidden" name="intent" value="create"></input>
|
|
72
|
-
<input class="new-todo" id="text" name="text" placeholder="What needs to be done?" autofocus="false" autocomplete="off">
|
|
74
|
+
<input class="new-todo" id="text" name="text" placeholder="What needs to be done?" autofocus="false" autocomplete="off"></input>
|
|
73
75
|
</form>
|
|
74
76
|
</header>
|
|
75
77
|
<section class="main">
|
|
76
78
|
<input class="toggle-all" id="toggle-all" type="checkbox">
|
|
77
79
|
<label for="toggle-all">Mark all as complete</label>
|
|
78
|
-
|
|
80
|
+
<TodoList id="todo-list" page={page} filter={filter}></TodoList>
|
|
79
81
|
</section>
|
|
80
82
|
<footer class="footer">
|
|
81
|
-
|
|
83
|
+
<TodoCount filter={filter}></TodoCount>
|
|
82
84
|
<ul class="filters">
|
|
83
85
|
<li>
|
|
84
86
|
<a href="?filter=all">All</a>
|
|
@@ -96,9 +98,6 @@ func GET(ctx context.Context, params GetParams) (HtmlContent, int, error) {
|
|
|
96
98
|
</form>
|
|
97
99
|
</footer>
|
|
98
100
|
</section>
|
|
99
|
-
|
|
101
|
+
</Page>
|
|
100
|
-
`).
|
|
101
|
-
Prop("page", params.Page).
|
|
102
|
-
Prop("filter", params.Filter).
|
|
103
|
-
|
|
102
|
+
`), 200, nil
|
|
104
103
|
}
|
_example/pages/post.go
CHANGED
|
@@ -6,7 +6,7 @@ import (
|
|
|
6
6
|
|
|
7
7
|
_ "github.com/pyros2097/gromer/_example/components"
|
|
8
8
|
"github.com/pyros2097/gromer/_example/services/todos"
|
|
9
|
-
. "github.com/pyros2097/gromer/
|
|
9
|
+
. "github.com/pyros2097/gromer/gsx"
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
type PostParams struct {
|
|
@@ -15,61 +15,59 @@ type PostParams struct {
|
|
|
15
15
|
Text string `json:"text"`
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
func POST(ctx context.Context, params PostParams) (
|
|
18
|
+
func POST(h Html, ctx context.Context, params PostParams) (string, int, error) {
|
|
19
19
|
if params.Intent == "clear_completed" {
|
|
20
20
|
allTodos, err := todos.GetAllTodo(ctx, todos.GetAllTodoParams{
|
|
21
21
|
Filter: "all",
|
|
22
22
|
Limit: 1000,
|
|
23
23
|
})
|
|
24
24
|
if err != nil {
|
|
25
|
-
return
|
|
25
|
+
return "", 500, err
|
|
26
26
|
}
|
|
27
27
|
for _, t := range allTodos {
|
|
28
28
|
if t.Completed {
|
|
29
29
|
_, err := todos.DeleteTodo(ctx, t.ID)
|
|
30
30
|
if err != nil {
|
|
31
|
-
return
|
|
31
|
+
return "", 500, err
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
return
|
|
35
|
+
return h.Render(`
|
|
36
|
-
|
|
36
|
+
<TodoList id="todo-list" filter="all" page="1"></TodoList>
|
|
37
|
-
|
|
37
|
+
<TodoCount filter="all" page="1"></TodoCount>
|
|
38
|
-
`)
|
|
38
|
+
`), 200, nil
|
|
39
39
|
} else if params.Intent == "create" {
|
|
40
40
|
todo, err := todos.CreateTodo(ctx, params.Text)
|
|
41
41
|
if err != nil {
|
|
42
|
-
return
|
|
42
|
+
return "", 500, err
|
|
43
43
|
}
|
|
44
|
+
h["todo"] = todo
|
|
44
|
-
return
|
|
45
|
+
return h.Render(`
|
|
45
|
-
|
|
46
|
+
<Todo todo=todo></Todo>
|
|
46
|
-
|
|
47
|
+
<TodoCount filter="all" page=1></TodoCount>
|
|
47
|
-
`).
|
|
48
|
-
Prop("todo", todo).
|
|
49
|
-
|
|
48
|
+
`), 200, nil
|
|
50
49
|
} else if params.Intent == "delete" {
|
|
51
50
|
_, err := todos.DeleteTodo(ctx, params.ID)
|
|
52
51
|
if err != nil {
|
|
53
|
-
return
|
|
52
|
+
return "", 500, err
|
|
54
53
|
}
|
|
55
|
-
return
|
|
54
|
+
return "", 200, nil
|
|
56
55
|
} else if params.Intent == "complete" {
|
|
57
56
|
todo, err := todos.GetTodo(ctx, params.ID)
|
|
58
57
|
if err != nil {
|
|
59
|
-
return
|
|
58
|
+
return "", 500, err
|
|
60
59
|
}
|
|
61
60
|
_, err = todos.UpdateTodo(ctx, params.ID, todos.UpdateTodoParams{
|
|
62
61
|
Text: todo.Text,
|
|
63
62
|
Completed: !todo.Completed,
|
|
64
63
|
})
|
|
65
64
|
if err != nil {
|
|
66
|
-
return
|
|
65
|
+
return "", 500, err
|
|
67
66
|
}
|
|
67
|
+
h["todo"] = todo
|
|
68
|
-
return
|
|
68
|
+
return h.Render(`
|
|
69
69
|
{{#Todo todo=todo}}{{/Todo}}
|
|
70
|
-
`).
|
|
71
|
-
Prop("todo", todo).
|
|
72
|
-
|
|
70
|
+
`), 200, nil
|
|
73
71
|
}
|
|
74
|
-
return
|
|
72
|
+
return "", 404, fmt.Errorf("Intent not specified: %s", params.Intent)
|
|
75
73
|
}
|
api_explorer.go
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
package gromer
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"encoding/json"
|
|
5
|
-
"html/template"
|
|
6
|
-
"net/http"
|
|
7
|
-
"strings"
|
|
8
|
-
|
|
9
|
-
"github.com/carlmjohnson/versioninfo"
|
|
10
|
-
"github.com/gorilla/mux"
|
|
11
|
-
"github.com/pyros2097/gromer/assets"
|
|
12
|
-
. "github.com/pyros2097/gromer/handlebars"
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
func ApiExplorerRoute(router *mux.Router, path string) {
|
|
16
|
-
router.Path(path).Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
17
|
-
cmcss, _ := assets.FS.ReadFile("css/codemirror@5.63.1.css")
|
|
18
|
-
stylescss, _ := assets.FS.ReadFile("css/styles.css")
|
|
19
|
-
cmjs, _ := assets.FS.ReadFile("js/codemirror@5.63.1.min.js")
|
|
20
|
-
cmjsjs, _ := assets.FS.ReadFile("js/codemirror-javascript@5.63.1.js")
|
|
21
|
-
apiRoutes := []RouteDefinition{}
|
|
22
|
-
for _, v := range RouteDefs {
|
|
23
|
-
if strings.Contains(v.Path, "/api/") {
|
|
24
|
-
apiRoutes = append(apiRoutes, v)
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
apiData, err := json.Marshal(apiRoutes)
|
|
28
|
-
if err != nil {
|
|
29
|
-
RespondError(w, 500, err)
|
|
30
|
-
}
|
|
31
|
-
status, err := Html(`
|
|
32
|
-
<!DOCTYPE html>
|
|
33
|
-
<html lang="en">
|
|
34
|
-
<head>
|
|
35
|
-
<meta charset="UTF-8">
|
|
36
|
-
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
|
37
|
-
<meta http-equiv="encoding" content="utf-8">
|
|
38
|
-
<title> API Explorer </title>
|
|
39
|
-
<meta name="description" content="API Explorer">
|
|
40
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover">
|
|
41
|
-
<style>
|
|
42
|
-
{{ css }}
|
|
43
|
-
</style>
|
|
44
|
-
<script>
|
|
45
|
-
{{ js }}
|
|
46
|
-
</script>
|
|
47
|
-
</head>
|
|
48
|
-
<body>
|
|
49
|
-
<div class="flex flex-col">
|
|
50
|
-
<div class="flex w-full p-2 bg-gray-50 border-b border-gray-200 items-center justify-start">
|
|
51
|
-
<div class="flex mr-4 text-gray-700 text-2xl font-bold"> API Explorer</div>
|
|
52
|
-
<div class="text-xl">
|
|
53
|
-
<select id="api-select" class="form-select block">
|
|
54
|
-
{{#each routes as |route|}}
|
|
55
|
-
<option value="{{ @index }}">
|
|
56
|
-
<div> {{ route.Method }} {{ route.Path }} </div>
|
|
57
|
-
</option>
|
|
58
|
-
{{/each}}
|
|
59
|
-
</select>
|
|
60
|
-
</div>
|
|
61
|
-
<div class="flex ml-3 mr-3">
|
|
62
|
-
<button id="run" class="bg-gray-200 border border-gray-400 hover:bg-gray-200 focus:outline-none rounded-md text-gray-700 text-md font-bold pt-2 pb-2 pl-6 pr-6"> RUN </button>
|
|
63
|
-
</div>
|
|
64
|
-
<div class="flex flex-1 justify-end">
|
|
65
|
-
(commit {{ commit }})
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
<div class="flex">
|
|
69
|
-
<div class="flex flex-row" style="width: 50%;;">
|
|
70
|
-
<div class="pr-8 border-r border-gray-300" style="background: #f7f7f7;;"></div>
|
|
71
|
-
<div class="w-full">
|
|
72
|
-
<div class="text-gray-700 text-sm font-bold uppercase pl-2 pt-2 pb-2 bg-gray-50 border-b border-gray-200"> Headers </div>
|
|
73
|
-
<table id="headersTable">
|
|
74
|
-
<tr>
|
|
75
|
-
<td>
|
|
76
|
-
<input class="w-full p-1" value="Authorization">
|
|
77
|
-
</td>
|
|
78
|
-
<td>
|
|
79
|
-
<input class="w-full p-1">
|
|
80
|
-
</td>
|
|
81
|
-
</tr>
|
|
82
|
-
</table>
|
|
83
|
-
<div class="text-gray-700 text-sm font-bold uppercase pl-2 pt-2 pb-2 bg-gray-50 border-b border-gray-200"> Path Params </div>
|
|
84
|
-
<table id="pathParamsTable">
|
|
85
|
-
<tr>
|
|
86
|
-
<td class="text-gray-700" style="width: 50%;;">
|
|
87
|
-
<div class="p-1"> 123 </div>
|
|
88
|
-
</td>
|
|
89
|
-
<td style="width: 50%;;">
|
|
90
|
-
<input class="w-full p-1">
|
|
91
|
-
</td>
|
|
92
|
-
</tr>
|
|
93
|
-
</table>
|
|
94
|
-
<div class="text-gray-700 text-sm font-bold uppercase pl-2 pt-2 pb-2 bg-gray-50 border-b border-gray-200"> Query Params </div>
|
|
95
|
-
<table id="queryParamsTable">
|
|
96
|
-
<tr>
|
|
97
|
-
<td class="text-gray-700" style="width: 50%;;">
|
|
98
|
-
<div class="p-1"> 123 </div>
|
|
99
|
-
</td>
|
|
100
|
-
<td style="width: 50%;;">
|
|
101
|
-
<input class="w-full p-1">
|
|
102
|
-
</td>
|
|
103
|
-
</tr>
|
|
104
|
-
</table>
|
|
105
|
-
<div class="text-gray-700 text-sm font-bold uppercase pl-2 pt-2 pb-2 bg-gray-50 border-b border-gray-200"> Body </div>
|
|
106
|
-
<div id="left" class="border-b border-gray-200 text-md"></div>
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
<div class="flex flex-row" style="width: 50%;;">
|
|
110
|
-
<div id="right" class="w-full border-l border-l-gray-200 text-md"></div>
|
|
111
|
-
<div class="pr-8 border-l border-gray-300" style="background: #f7f7f7;;"></div>
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
<script>
|
|
116
|
-
window.apiDefs = {{ apiData }}
|
|
117
|
-
</script>
|
|
118
|
-
<script>
|
|
119
|
-
window.codeLeft = CodeMirror(document.getElementById('left'), {
|
|
120
|
-
value: '{}',
|
|
121
|
-
mode: 'javascript'
|
|
122
|
-
})
|
|
123
|
-
</script>
|
|
124
|
-
<script>
|
|
125
|
-
window.codeRight = CodeMirror(document.getElementById('right'), {
|
|
126
|
-
value: '',
|
|
127
|
-
mode: 'javascript',
|
|
128
|
-
lineNumbers: true,
|
|
129
|
-
readOnly: true,
|
|
130
|
-
lineWrapping: true
|
|
131
|
-
})
|
|
132
|
-
</script>
|
|
133
|
-
<script>
|
|
134
|
-
const getCurrentApiCall = () => {
|
|
135
|
-
const index = document.getElementById("api-select").value;
|
|
136
|
-
return window.apiDefs[index];
|
|
137
|
-
}
|
|
138
|
-
const updatePathParams = (apiCall) => {
|
|
139
|
-
const table = document.getElementById("pathParamsTable");
|
|
140
|
-
if (apiCall.pathParams.length === 0) {
|
|
141
|
-
table.innerHTML = " <div style='background-color: rgb(245, 245, 245); padding: 0.25rem; text-align: center; color: gray;'>NONE</div>";
|
|
142
|
-
} else {
|
|
143
|
-
table.innerHTML = "";
|
|
144
|
-
}
|
|
145
|
-
for (const param of apiCall.pathParams.reverse()) {
|
|
146
|
-
const row = table.insertRow(0);
|
|
147
|
-
const cell1 = row.insertCell(0);
|
|
148
|
-
const cell2 = row.insertCell(1);
|
|
149
|
-
cell1.style = "width: 30%; border-left: 0px;";
|
|
150
|
-
cell1.class = "text-gray-700";
|
|
151
|
-
cell2.style = "width: 70%;";
|
|
152
|
-
cell1.innerHTML = " <div class='p-1'> " + param + " </div>";
|
|
153
|
-
cell2.innerHTML = " <input id='path-param-" + param + " class='w-full p-1'>";
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const updateParams = (apiCall) => {
|
|
157
|
-
const table = document.getElementById("queryParamsTable");
|
|
158
|
-
if (!apiCall.params) {
|
|
159
|
-
table.innerHTML = " <div style='background-color: rgb(245, 245, 245); padding: 0.25rem; text-align: center; color: gray;'> NONE </div>";
|
|
160
|
-
} else {
|
|
161
|
-
table.innerHTML = "";
|
|
162
|
-
}
|
|
163
|
-
if (apiCall.method === "GET" || apiCall.method === "DELETE") {
|
|
164
|
-
for (const key of Object.keys(apiCall.params)) {
|
|
165
|
-
const row = table.insertRow(0);
|
|
166
|
-
const cell1 = row.insertCell(0);
|
|
167
|
-
const cell2 = row.insertCell(1);
|
|
168
|
-
cell1.style = "width: 30%; border-left: 0px;";
|
|
169
|
-
cell1.class = "text-gray-700";
|
|
170
|
-
cell2.style = "width: 70%;";
|
|
171
|
-
cell1.innerHTML = " <div class='p-1'> " + key + " </div>";
|
|
172
|
-
cell2.innerHTML = " <input id='query-param-" + key + "' class='w-full p-1'>";
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
const updateBody = (apiCall) => {
|
|
177
|
-
if (apiCall.method !== "GET" && apiCall.method !== "DELETE") {
|
|
178
|
-
window.codeLeft.setValue(JSON.stringify(apiCall.params, 2, 2));
|
|
179
|
-
} else {
|
|
180
|
-
window.codeLeft.setValue("");
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
const init = () => {
|
|
184
|
-
updatePathParams(window.apiDefs[0]);
|
|
185
|
-
updateParams(window.apiDefs[0]);
|
|
186
|
-
updateBody(window.apiDefs[0]);
|
|
187
|
-
const headersJson = localStorage.getItem("headers");
|
|
188
|
-
if (headersJson) {
|
|
189
|
-
const table = document.getElementById("headersTable");
|
|
190
|
-
const headers = JSON.parse(headersJson);
|
|
191
|
-
table.innerHTML = "";
|
|
192
|
-
for (const key of Object.keys(headers)) {
|
|
193
|
-
const value = headers[key];
|
|
194
|
-
const row = table.insertRow(0);
|
|
195
|
-
const cell1 = row.insertCell(0);
|
|
196
|
-
const cell2 = row.insertCell(1);
|
|
197
|
-
cell1.style = "width: 30%; border-left: 0px;";
|
|
198
|
-
cell2.style = "width: 70%;";
|
|
199
|
-
cell1.innerHTML = "<input value = '" + key + "' class='w-full p-1'>";
|
|
200
|
-
cell2.innerHTML = "<input value = '" + value + "' class='w-full p-1'>";
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
window.onload = () => {
|
|
205
|
-
init();
|
|
206
|
-
}
|
|
207
|
-
document.getElementById("api-select").onchange = () => {
|
|
208
|
-
const apiCall = getCurrentApiCall();
|
|
209
|
-
updatePathParams(apiCall);
|
|
210
|
-
updateParams(apiCall);
|
|
211
|
-
updateBody(apiCall);
|
|
212
|
-
}
|
|
213
|
-
const run = document.getElementById("run");
|
|
214
|
-
run.onclick = async () => {
|
|
215
|
-
run.innerHTML = "<svg class='spinner' viewBox='0 0 50 50'><circle class='path' cx='25' cy='25' r='20' fill='none' stroke-width='5'></circle></svg>";
|
|
216
|
-
const table = document.getElementById("headersTable");
|
|
217
|
-
const headers = {};
|
|
218
|
-
for (const row of table.rows) {
|
|
219
|
-
const key = row.cells[0].children[0].value;
|
|
220
|
-
const value = row.cells[1].children[0].value;
|
|
221
|
-
headers[key] = value;
|
|
222
|
-
}
|
|
223
|
-
const apiCall = getCurrentApiCall();
|
|
224
|
-
let path = apiCall.path;
|
|
225
|
-
const bodyParams = {};
|
|
226
|
-
if (apiCall.method !== "GET" && apiCall.method != "DELETE") {
|
|
227
|
-
bodyParams["body"] = window.codeLeft.getValue();
|
|
228
|
-
} else {
|
|
229
|
-
for (const param of apiCall.pathParams) {
|
|
230
|
-
const value = document.getElementById('path-param-' + param).value;
|
|
231
|
-
path = path.replace('{' + param + '}', value);
|
|
232
|
-
}
|
|
233
|
-
const paramsKeys = Object.keys(apiCall.params);
|
|
234
|
-
if (paramsKeys.length > 0) {
|
|
235
|
-
path += "?";
|
|
236
|
-
paramsKeys.forEach((key, i) => {
|
|
237
|
-
const value = document.getElementById('query-param-' + key).value;
|
|
238
|
-
path += key + "=" + value;
|
|
239
|
-
if (i !== paramsKeys.length - 1) {
|
|
240
|
-
path += "&";
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
localStorage.setItem("headers", JSON.stringify(headers));
|
|
246
|
-
try {
|
|
247
|
-
const res = await fetch(path, {
|
|
248
|
-
method: apiCall.method,
|
|
249
|
-
headers,
|
|
250
|
-
...bodyParams
|
|
251
|
-
});
|
|
252
|
-
const json = await res.json();
|
|
253
|
-
window.codeRight.setValue(JSON.stringify(json, 2, 2));
|
|
254
|
-
} catch (err) {
|
|
255
|
-
window.codeRight.setValue(JSON.stringify({
|
|
256
|
-
error: err.message
|
|
257
|
-
}, 2, 2));
|
|
258
|
-
}
|
|
259
|
-
run.innerHTML = "RUN";
|
|
260
|
-
}
|
|
261
|
-
</script>
|
|
262
|
-
</body>
|
|
263
|
-
</html>
|
|
264
|
-
`).Props(
|
|
265
|
-
"commit", versioninfo.Revision[0:7],
|
|
266
|
-
"routes", apiRoutes,
|
|
267
|
-
"apiData", template.HTML(string(apiData)),
|
|
268
|
-
"css", template.HTML(string(cmcss)+"\n\n"+string(stylescss)),
|
|
269
|
-
"js", template.HTML(string(cmjs)+"\n\n"+string(cmjsjs)),
|
|
270
|
-
).RenderWriter(w)
|
|
271
|
-
if err != nil {
|
|
272
|
-
RespondError(w, status, err)
|
|
273
|
-
}
|
|
274
|
-
})
|
|
275
|
-
}
|
gsx/template.go
CHANGED
|
@@ -50,6 +50,17 @@ func RegisterFunc(f interface{}) {
|
|
|
50
50
|
funcMap[name] = f
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
var styles = ""
|
|
54
|
+
|
|
55
|
+
func Css(v string) string {
|
|
56
|
+
styles += v
|
|
57
|
+
return v
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func GetStyles() string {
|
|
61
|
+
return styles
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
func getAttribute(k string, kvs []*Attribute) string {
|
|
54
65
|
for _, param := range kvs {
|
|
55
66
|
if param.Key == k {
|
handlebars/README.md
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
# Templating
|
|
2
|
-
|
|
3
|
-
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)
|
|
4
|
-
If you know handlebars, you basically know how to use it.
|
|
5
|
-
|
|
6
|
-
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.
|
|
7
|
-
|
|
8
|
-
Let's assume you have a template (a string of some kind):
|
|
9
|
-
|
|
10
|
-
```handlebars
|
|
11
|
-
<!-- some input -->
|
|
12
|
-
<h1>{{name}}</h1>
|
|
13
|
-
<ul>
|
|
14
|
-
{{#each names}}
|
|
15
|
-
<li>{{@value}}</li>
|
|
16
|
-
{{/each}}
|
|
17
|
-
</ul>
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Given that string, you can render the template like such:
|
|
21
|
-
|
|
22
|
-
```html
|
|
23
|
-
<h1>Mark</h1>
|
|
24
|
-
<ul>
|
|
25
|
-
<li>John</li>
|
|
26
|
-
<li>Paul</li>
|
|
27
|
-
<li>George</li>
|
|
28
|
-
<li>Ringo</li>
|
|
29
|
-
</ul>
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### If Statements
|
|
33
|
-
|
|
34
|
-
```handlebars
|
|
35
|
-
{{#if true}}
|
|
36
|
-
render this
|
|
37
|
-
{{/if}}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
#### Else Statements
|
|
41
|
-
|
|
42
|
-
```handlebars
|
|
43
|
-
{{#if false}}
|
|
44
|
-
won't render this
|
|
45
|
-
{{else}}
|
|
46
|
-
render this
|
|
47
|
-
{{/if}}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
#### Unless Statements
|
|
51
|
-
|
|
52
|
-
```handlebars
|
|
53
|
-
{{#unless true}}
|
|
54
|
-
won't render this
|
|
55
|
-
{{/unless}}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Each Statements
|
|
59
|
-
|
|
60
|
-
#### Arrays
|
|
61
|
-
|
|
62
|
-
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:
|
|
63
|
-
|
|
64
|
-
- `@first` [`bool`] - is this the first pass through the iteration?
|
|
65
|
-
- `@last` [`bool`] - is this the last pass through the iteration?
|
|
66
|
-
- `@index` [`int`] - the counter of where in the loop you are, starting with `0`.
|
|
67
|
-
- `@value` - the current element in the array or slice that is being iterated over.
|
|
68
|
-
|
|
69
|
-
```handlebars
|
|
70
|
-
<ul>
|
|
71
|
-
{{#each names}}
|
|
72
|
-
<li>{{@index}} - {{@value}}</li>
|
|
73
|
-
{{/each}}
|
|
74
|
-
</ul>
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
By using "block parameters" you can change the "key" of the element being accessed from `@value` to a key of your choosing.
|
|
78
|
-
|
|
79
|
-
```handlebars
|
|
80
|
-
<ul>
|
|
81
|
-
{{#each names as |name|}}
|
|
82
|
-
<li>{{name}}</li>
|
|
83
|
-
{{/each}}
|
|
84
|
-
</ul>
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
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.
|
|
88
|
-
|
|
89
|
-
```handlebars
|
|
90
|
-
<ul>
|
|
91
|
-
{{#each names as |index, name|}}
|
|
92
|
-
<li>{{ index }} - {{ name }}</li>
|
|
93
|
-
{{/each}}
|
|
94
|
-
</ul>
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
#### Maps
|
|
98
|
-
|
|
99
|
-
Looping through `maps` using the `each` helper is also supported, and follows very similar guidelines to looping through `arrays`.
|
|
100
|
-
|
|
101
|
-
- `@first` [`bool`] - is this the first pass through the iteration?
|
|
102
|
-
- `@last` [`bool`] - is this the last pass through the iteration?
|
|
103
|
-
- `@key` - the key of the pair being accessed.
|
|
104
|
-
- `@value` - the value of the pair being accessed.
|
|
105
|
-
|
|
106
|
-
```handlebars
|
|
107
|
-
<ul>
|
|
108
|
-
{{#each users}}
|
|
109
|
-
<li>{{@key}} - {{@value}}</li>
|
|
110
|
-
{{/each}}
|
|
111
|
-
</ul>
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
By using "block parameters" you can change the "key" of the element being accessed from `@value` to a key of your choosing.
|
|
115
|
-
|
|
116
|
-
```handlebars
|
|
117
|
-
<ul>
|
|
118
|
-
{{#each users as |user|}}
|
|
119
|
-
<li>{{@key}} - {{user}}</li>
|
|
120
|
-
{{/each}}
|
|
121
|
-
</ul>
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
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.
|
|
125
|
-
|
|
126
|
-
```handlebars
|
|
127
|
-
<ul>
|
|
128
|
-
{{#each users as |key, user|}}
|
|
129
|
-
<li>{{ key }} - {{ user }}</li>
|
|
130
|
-
{{/each}}
|
|
131
|
-
</ul>
|
|
132
|
-
```
|
handlebars/context.go
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,385 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
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
|
-
s, _, err := Html(`
|
|
52
|
-
{{letters.a}}|{{letters.b}}
|
|
53
|
-
`).Props(
|
|
54
|
-
"a", "A",
|
|
55
|
-
"b", "B",
|
|
56
|
-
).Render()
|
|
57
|
-
r.NoError(err)
|
|
58
|
-
r.Equal("A|B", strings.TrimSpace(string(s)))
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
func Test_Eval_Calls_on_Pointers(t *testing.T) {
|
|
62
|
-
r := require.New(t)
|
|
63
|
-
type user struct {
|
|
64
|
-
Name string
|
|
65
|
-
}
|
|
66
|
-
u := &user{Name: "Mark"}
|
|
67
|
-
ctx := NewContext()
|
|
68
|
-
ctx.Set("user", u)
|
|
69
|
-
|
|
70
|
-
s, err := Render("{{user.Name}}", ctx)
|
|
71
|
-
r.NoError(err)
|
|
72
|
-
r.Equal("Mark", s)
|
|
73
|
-
}
|
handlebars/helpers.go
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,466 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
package handlebars
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"fmt"
|
|
5
|
-
"io"
|
|
6
|
-
"net/http"
|
|
7
|
-
|
|
8
|
-
"github.com/aymerick/raymond/ast"
|
|
9
|
-
"github.com/aymerick/raymond/parser"
|
|
10
|
-
"github.com/pkg/errors"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
var stylesCss = CssContent("")
|
|
14
|
-
|
|
15
|
-
type CssContent string
|
|
16
|
-
type HtmlContent string
|
|
17
|
-
|
|
18
|
-
// Template represents an input and helpers to be used
|
|
19
|
-
// to evaluate and render the input.
|
|
20
|
-
type Template struct {
|
|
21
|
-
Input string
|
|
22
|
-
Context *Context
|
|
23
|
-
program *ast.Program
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// NewTemplate from the input string.
|
|
27
|
-
func NewTemplate(input string) (*Template, error) {
|
|
28
|
-
t := &Template{
|
|
29
|
-
Input: input,
|
|
30
|
-
Context: NewContext(),
|
|
31
|
-
}
|
|
32
|
-
err := t.Parse()
|
|
33
|
-
if err != nil {
|
|
34
|
-
return t, errors.WithStack(err)
|
|
35
|
-
}
|
|
36
|
-
return t, nil
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Parse the template this can be called many times
|
|
40
|
-
// as a successful result is cached and is used on subsequent
|
|
41
|
-
// uses.
|
|
42
|
-
func (t *Template) Parse() error {
|
|
43
|
-
if t.program != nil {
|
|
44
|
-
return nil
|
|
45
|
-
}
|
|
46
|
-
program, err := parser.Parse(t.Input)
|
|
47
|
-
if err != nil {
|
|
48
|
-
return errors.WithStack(err)
|
|
49
|
-
}
|
|
50
|
-
t.program = program
|
|
51
|
-
return nil
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Exec the template using the content and return the results
|
|
55
|
-
func (t *Template) RenderWithStatus(status int) (HtmlContent, int, error) {
|
|
56
|
-
err := t.Parse()
|
|
57
|
-
if err != nil {
|
|
58
|
-
return HtmlContent("Server Erorr"), 500, errors.WithStack(err)
|
|
59
|
-
}
|
|
60
|
-
v := newEvalVisitor(t, t.Context)
|
|
61
|
-
r := t.program.Accept(v)
|
|
62
|
-
switch rp := r.(type) {
|
|
63
|
-
case string:
|
|
64
|
-
return HtmlContent(rp), status, nil
|
|
65
|
-
case error:
|
|
66
|
-
return HtmlContent("Server Erorr"), 500, rp
|
|
67
|
-
case nil:
|
|
68
|
-
return HtmlContent(""), 200, nil
|
|
69
|
-
default:
|
|
70
|
-
return HtmlContent("Server Erorr"), 500, errors.WithStack(errors.Errorf("unsupport eval return format %T: %+v", r, r))
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
func (t *Template) Render() (HtmlContent, int, error) {
|
|
75
|
-
return t.RenderWithStatus(200)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
func (t *Template) RenderWriter(w io.Writer) (int, error) {
|
|
79
|
-
s, status, err := t.Render()
|
|
80
|
-
if err != nil {
|
|
81
|
-
return status, err
|
|
82
|
-
}
|
|
83
|
-
if v, ok := w.(http.ResponseWriter); ok {
|
|
84
|
-
v.WriteHeader(status)
|
|
85
|
-
}
|
|
86
|
-
w.Write([]byte(s))
|
|
87
|
-
return 200, nil
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
func (t *Template) Prop(key string, v any) *Template {
|
|
91
|
-
t.Context.Set(key, v)
|
|
92
|
-
return t
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
func (t *Template) Props(args ...any) *Template {
|
|
96
|
-
for i := 0; i < len(args); i += 2 {
|
|
97
|
-
key := fmt.Sprintf("%s", args[i])
|
|
98
|
-
t.Context.Set(key, args[i+1])
|
|
99
|
-
}
|
|
100
|
-
return t
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
func Html(tpl string) *Template {
|
|
104
|
-
return &Template{
|
|
105
|
-
Input: tpl,
|
|
106
|
-
Context: NewContext(),
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
func HtmlErr(status int, err error) (HtmlContent, int, error) {
|
|
111
|
-
return HtmlContent("ErrorPage/AccessDeniedPage/NotFoundPage based on status code"), status, err
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
func HtmlEmpty() (HtmlContent, int, error) {
|
|
115
|
-
return Html("").Render()
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
func Css(v string) CssContent {
|
|
119
|
-
stylesCss += CssContent(v)
|
|
120
|
-
return CssContent(v)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
func GetStyles() CssContent {
|
|
124
|
-
return stylesCss
|
|
125
|
-
}
|
handlebars/template_test.go
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
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
|
@@ -6,7 +6,6 @@ import (
|
|
|
6
6
|
"embed"
|
|
7
7
|
"encoding/json"
|
|
8
8
|
"fmt"
|
|
9
|
-
"html/template"
|
|
10
9
|
"net"
|
|
11
10
|
"net/http"
|
|
12
11
|
"net/url"
|
|
@@ -20,10 +19,11 @@ import (
|
|
|
20
19
|
"sync"
|
|
21
20
|
"time"
|
|
22
21
|
|
|
22
|
+
"github.com/alecthomas/repr"
|
|
23
23
|
"github.com/go-playground/validator/v10"
|
|
24
24
|
"github.com/gorilla/mux"
|
|
25
25
|
"github.com/pyros2097/gromer/assets"
|
|
26
|
-
"github.com/pyros2097/gromer/
|
|
26
|
+
"github.com/pyros2097/gromer/gsx"
|
|
27
27
|
"github.com/rs/zerolog"
|
|
28
28
|
"github.com/rs/zerolog/log"
|
|
29
29
|
"github.com/rs/zerolog/pkgerrors"
|
|
@@ -49,10 +49,10 @@ func init() {
|
|
|
49
49
|
PartsExclude: []string{zerolog.TimestampFieldName},
|
|
50
50
|
})
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
gsx.RegisterFunc(GetStylesUrl)
|
|
53
|
-
|
|
53
|
+
gsx.RegisterFunc(GetAssetUrl)
|
|
54
|
-
|
|
54
|
+
gsx.RegisterFunc(GetAlpineJsUrl)
|
|
55
|
-
|
|
55
|
+
gsx.RegisterFunc(GetHtmxJsUrl)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
var RouteDefs []RouteDefinition
|
|
@@ -76,104 +76,104 @@ func RegisterAssets(fs embed.FS) {
|
|
|
76
76
|
appAssets = fs
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
func RegisterComponent(fn any, props ...string) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
func RegisterContainer(fn any, props ...string) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
79
|
+
// func RegisterComponent(fn any, props ...string) {
|
|
80
|
+
// name := getFunctionName(fn)
|
|
81
|
+
// fnType := reflect.TypeOf(fn)
|
|
82
|
+
// fnValue := reflect.ValueOf(fn)
|
|
83
|
+
// handlebars.GlobalHelpers.Add(name, func(help handlebars.HelperContext) (template.HTML, error) {
|
|
84
|
+
// args := []reflect.Value{}
|
|
85
|
+
// var props any
|
|
86
|
+
// if fnType.NumIn() > 0 {
|
|
87
|
+
// structType := fnType.In(0)
|
|
88
|
+
// instance := reflect.New(structType)
|
|
89
|
+
// if structType.Kind() != reflect.Struct {
|
|
90
|
+
// log.Fatal().Msgf("component '%s' props should be a struct", name)
|
|
91
|
+
// }
|
|
92
|
+
// rv := instance.Elem()
|
|
93
|
+
// for i := 0; i < structType.NumField(); i++ {
|
|
94
|
+
// if f := rv.Field(i); f.CanSet() {
|
|
95
|
+
// jsonName := structType.Field(i).Tag.Get("json")
|
|
96
|
+
// defaultValue := structType.Field(i).Tag.Get("default")
|
|
97
|
+
// if jsonName == "children" {
|
|
98
|
+
// s, err := help.Block()
|
|
99
|
+
// if err != nil {
|
|
100
|
+
// return "", err
|
|
101
|
+
// }
|
|
102
|
+
// f.Set(reflect.ValueOf(template.HTML(s)))
|
|
103
|
+
// } else {
|
|
104
|
+
// v := help.Context.Get(jsonName)
|
|
105
|
+
// if v == nil {
|
|
106
|
+
// f.Set(reflect.ValueOf(defaultValue))
|
|
107
|
+
// } else {
|
|
108
|
+
// f.Set(reflect.ValueOf(v))
|
|
109
|
+
// }
|
|
110
|
+
// }
|
|
111
|
+
// }
|
|
112
|
+
// }
|
|
113
|
+
// args = append(args, rv)
|
|
114
|
+
// props = rv.Interface()
|
|
115
|
+
// }
|
|
116
|
+
// res := fnValue.Call(args)
|
|
117
|
+
// tpl := res[0].Interface().(*handlebars.Template)
|
|
118
|
+
// tpl.Context.Set("props", props)
|
|
119
|
+
// s, _, err := tpl.Render()
|
|
120
|
+
// if err != nil {
|
|
121
|
+
// return "", err
|
|
122
|
+
// }
|
|
123
|
+
// return template.HTML(s), nil
|
|
124
|
+
// })
|
|
125
|
+
// }
|
|
126
|
+
|
|
127
|
+
// func RegisterContainer(fn any, props ...string) {
|
|
128
|
+
// name := getFunctionName(fn)
|
|
129
|
+
// fnType := reflect.TypeOf(fn)
|
|
130
|
+
// fnValue := reflect.ValueOf(fn)
|
|
131
|
+
// // shandlebars.GlobalHelpers.Add(name, func(help handlebars.HelperContext) (template.HTML, error) {
|
|
132
|
+
// args := []reflect.Value{reflect.ValueOf(context.TODO())}
|
|
133
|
+
// var props any
|
|
134
|
+
// if fnType.NumIn() > 1 {
|
|
135
|
+
// structType := fnType.In(1)
|
|
136
|
+
// instance := reflect.New(structType)
|
|
137
|
+
// if structType.Kind() != reflect.Struct {
|
|
138
|
+
// log.Fatal().Msgf("component '%s' props should be a struct", name)
|
|
139
|
+
// }
|
|
140
|
+
// rv := instance.Elem()
|
|
141
|
+
// for i := 0; i < structType.NumField(); i++ {
|
|
142
|
+
// if f := rv.Field(i); f.CanSet() {
|
|
143
|
+
// jsonName := structType.Field(i).Tag.Get("json")
|
|
144
|
+
// defaultValue := structType.Field(i).Tag.Get("default")
|
|
145
|
+
// if jsonName == "children" {
|
|
146
|
+
// s, err := help.Block()
|
|
147
|
+
// if err != nil {
|
|
148
|
+
// return "", err
|
|
149
|
+
// }
|
|
150
|
+
// f.Set(reflect.ValueOf(template.HTML(s)))
|
|
151
|
+
// } else {
|
|
152
|
+
// v := help.Context.Get(jsonName)
|
|
153
|
+
// if v == nil {
|
|
154
|
+
// f.Set(reflect.ValueOf(defaultValue))
|
|
155
|
+
// } else {
|
|
156
|
+
// f.Set(reflect.ValueOf(v))
|
|
157
|
+
// }
|
|
158
|
+
// }
|
|
159
|
+
// }
|
|
160
|
+
// }
|
|
161
|
+
// args = append(args, rv)
|
|
162
|
+
// props = rv.Interface()
|
|
163
|
+
// }
|
|
164
|
+
// res := fnValue.Call(args)
|
|
165
|
+
// tpl := res[0].Interface().(*handlebars.Template)
|
|
166
|
+
// // if res[1].Interface() != nil {
|
|
167
|
+
// // show error in component
|
|
168
|
+
// // }
|
|
169
|
+
// tpl.Context.Set("props", props)
|
|
170
|
+
// s, _, err := tpl.Render()
|
|
171
|
+
// if err != nil {
|
|
172
|
+
// return "", err
|
|
173
|
+
// }
|
|
174
|
+
// return template.HTML(s), nil
|
|
175
|
+
// })
|
|
176
|
+
// }
|
|
177
177
|
|
|
178
178
|
func RespondError(w http.ResponseWriter, status int, err error) {
|
|
179
179
|
w.Header().Set("Content-Type", "application/json")
|
|
@@ -208,7 +208,7 @@ func addRouteDef(method, route string, h interface{}) {
|
|
|
208
208
|
pathParams := GetRouteParams(route)
|
|
209
209
|
var body any = nil
|
|
210
210
|
funcType := reflect.TypeOf(h)
|
|
211
|
-
if funcType.NumIn() > len(pathParams)+
|
|
211
|
+
if funcType.NumIn() > len(pathParams)+2 {
|
|
212
212
|
structType := funcType.In(funcType.NumIn() - 1)
|
|
213
213
|
instance := reflect.New(structType)
|
|
214
214
|
if structType.Kind() != reflect.Struct {
|
|
@@ -226,13 +226,15 @@ func addRouteDef(method, route string, h interface{}) {
|
|
|
226
226
|
|
|
227
227
|
func PerformRequest(route string, h interface{}, ctx interface{}, w http.ResponseWriter, r *http.Request) {
|
|
228
228
|
params := GetRouteParams(route)
|
|
229
|
+
htmlTemplate := gsx.Html(map[string]interface{}{})
|
|
229
|
-
args := []reflect.Value{reflect.ValueOf(ctx)}
|
|
230
|
+
args := []reflect.Value{reflect.ValueOf(htmlTemplate), reflect.ValueOf(ctx)}
|
|
230
231
|
funcType := reflect.TypeOf(h)
|
|
231
232
|
icount := funcType.NumIn()
|
|
232
233
|
vars := mux.Vars(r)
|
|
233
234
|
for _, k := range params {
|
|
234
235
|
args = append(args, reflect.ValueOf(vars[k]))
|
|
235
236
|
}
|
|
237
|
+
repr.Println(len(args), icount)
|
|
236
238
|
if len(args) != icount {
|
|
237
239
|
structType := funcType.In(icount - 1)
|
|
238
240
|
instance := reflect.New(structType)
|
|
@@ -310,20 +312,20 @@ func PerformRequest(route string, h interface{}, ctx interface{}, w http.Respons
|
|
|
310
312
|
RespondError(w, responseStatus, responseError.(error))
|
|
311
313
|
return
|
|
312
314
|
}
|
|
313
|
-
if v, ok := response.(
|
|
315
|
+
if v, ok := response.(string); ok {
|
|
314
316
|
w.Header().Set("Content-Type", "text/html")
|
|
315
317
|
// This has to be at end always
|
|
316
318
|
w.WriteHeader(responseStatus)
|
|
317
319
|
w.Write([]byte(v))
|
|
318
320
|
return
|
|
319
321
|
}
|
|
320
|
-
if v, ok := response.(handlebars.CssContent); ok {
|
|
322
|
+
// if v, ok := response.(handlebars.CssContent); ok {
|
|
321
|
-
|
|
323
|
+
// w.Header().Set("Content-Type", "text/css")
|
|
322
|
-
|
|
324
|
+
// // This has to be at end always
|
|
323
|
-
|
|
325
|
+
// w.WriteHeader(responseStatus)
|
|
324
|
-
|
|
326
|
+
// w.Write([]byte(v))
|
|
325
|
-
|
|
327
|
+
// return
|
|
326
|
-
}
|
|
328
|
+
// }
|
|
327
329
|
w.Header().Set("Content-Type", "application/json")
|
|
328
330
|
// This has to be at end always
|
|
329
331
|
w.WriteHeader(responseStatus)
|
|
@@ -455,7 +457,7 @@ func StatusHandler(h interface{}) http.Handler {
|
|
|
455
457
|
|
|
456
458
|
// This has to be at end always after headers are set
|
|
457
459
|
w.WriteHeader(responseStatus)
|
|
458
|
-
w.Write([]byte(response.(
|
|
460
|
+
w.Write([]byte(response.(string)))
|
|
459
461
|
})).(http.Handler)
|
|
460
462
|
}
|
|
461
463
|
|
|
@@ -471,7 +473,7 @@ func StylesRoute(router *mux.Router, path string) {
|
|
|
471
473
|
router.Path(path).Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
472
474
|
w.Header().Set("Content-Type", "text/css")
|
|
473
475
|
w.WriteHeader(200)
|
|
474
|
-
w.Write([]byte(
|
|
476
|
+
w.Write([]byte(gsx.GetStyles()))
|
|
475
477
|
})
|
|
476
478
|
}
|
|
477
479
|
|
|
@@ -523,7 +525,7 @@ func GetAlpineJsUrl() string {
|
|
|
523
525
|
|
|
524
526
|
func GetStylesUrl() string {
|
|
525
527
|
sum := getSum("styles.css", func() [16]byte {
|
|
526
|
-
return md5.Sum([]byte(
|
|
528
|
+
return md5.Sum([]byte(gsx.GetStyles()))
|
|
527
529
|
})
|
|
528
530
|
return fmt.Sprintf("/styles.css?hash=%s", sum)
|
|
529
531
|
}
|
readme.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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 inline
|
|
6
|
+
It uses a declarative syntax using inline templates for components and pages.
|
|
7
7
|
It also generates http handlers for your routes which follow a particular folder structure. Similar to other frameworks like nextjs, sveltekit.
|
|
8
8
|
These handlers are also normal functions and can be imported in other packages directly. ((inspired by [Encore](https://encore.dev/)).
|
|
9
9
|
More information on the templating syntax is given [here](https://github.com/pyrossh/gromer/blob/master/handlebars/README.md),
|
|
@@ -56,8 +56,8 @@ func GET(ctx context.Context, params GetParams) (HtmlContent, int, error) {
|
|
|
56
56
|
return HtmlErr(status, err)
|
|
57
57
|
}
|
|
58
58
|
return Html(`
|
|
59
|
-
|
|
59
|
+
<Page title="gromer example">
|
|
60
|
-
|
|
60
|
+
<Header></Header>
|
|
61
61
|
<section class="todoapp">
|
|
62
62
|
<section class="main">
|
|
63
63
|
<ul class="todo-list" id="todo-list">
|
|
@@ -68,7 +68,7 @@ func GET(ctx context.Context, params GetParams) (HtmlContent, int, error) {
|
|
|
68
68
|
</section>
|
|
69
69
|
{{/if}}
|
|
70
70
|
</section>
|
|
71
|
-
|
|
71
|
+
</Page>
|
|
72
72
|
`).
|
|
73
73
|
Prop("todos", todos).
|
|
74
74
|
Render()
|