~repos /gromer

#golang#htmx#ssr

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

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


fee5b082 Peter John

3 years ago
add jsx templating
template/parser.go ADDED
@@ -0,0 +1,33 @@
1
+ package template
2
+
3
+ import (
4
+ "github.com/alecthomas/participle/v2"
5
+ "github.com/alecthomas/participle/v2/lexer"
6
+ )
7
+
8
+ type Module struct {
9
+ Pos lexer.Position
10
+ Nodes []*Xml `parser:"{ @@ }"`
11
+ }
12
+
13
+ type KeyValue struct {
14
+ Pos lexer.Position
15
+ Key string `parser:"@\":\"? @Ident ( @\"-\" @Ident )*"`
16
+ Value *Literal `parser:"\"=\" @@"`
17
+ }
18
+
19
+ type Literal struct {
20
+ Str string `parser:"@String"`
21
+ Ref string `parser:"| @\"{\" @Ident ( @\".\" @Ident )* @\"}\""`
22
+ }
23
+
24
+ type Xml struct {
25
+ Pos lexer.Position
26
+ Name string `parser:"\"<\"@Ident"`
27
+ Parameters []*KeyValue `parser:"[ @@ { @@ } ] \">\""`
28
+ Children []*Xml `parser:"{ @@ }"`
29
+ Value *Literal `parser:"{ @@ }"` // Todo make this match with @String or Literal
30
+ Close string `parser:"\"<\"\"/\"@Ident\">\""`
31
+ }
32
+
33
+ var xmlParser = participle.MustBuild(&Module{})
template/template.go ADDED
@@ -0,0 +1,131 @@
1
+ package template
2
+
3
+ import (
4
+ "fmt"
5
+ "reflect"
6
+ "runtime"
7
+ "strings"
8
+ )
9
+
10
+ type Component func(map[string]interface{}) string
11
+
12
+ var htmlTags = []string{"ul", "li", "span", "div"}
13
+ var compMap = map[string]interface{}{}
14
+ var funcMap = map[string]interface{}{}
15
+
16
+ func getFunctionName(temp interface{}) string {
17
+ strs := strings.Split((runtime.FuncForPC(reflect.ValueOf(temp).Pointer()).Name()), ".")
18
+ return strs[len(strs)-1]
19
+ }
20
+
21
+ func RegisterComponent(f interface{}) {
22
+ name := getFunctionName(f)
23
+ compMap[name] = f
24
+ }
25
+
26
+ func RegisterFunc(f interface{}) {
27
+ name := getFunctionName(f)
28
+ funcMap[name] = f
29
+ }
30
+
31
+ func getAttribute(k string, kvs []*KeyValue) string {
32
+ for _, param := range kvs {
33
+ if param.Key == k {
34
+ return strings.ReplaceAll(param.Value.Str, `"`, "")
35
+ }
36
+ }
37
+ return ""
38
+ }
39
+
40
+ func Render(x *Xml, ctx map[string]interface{}) string {
41
+ space, _ := ctx["_space"].(string)
42
+ s := space + "<" + x.Name
43
+ if len(x.Parameters) > 0 {
44
+ s += " "
45
+ }
46
+ for i, param := range x.Parameters {
47
+ s += param.Key + "=" + param.Value.Str
48
+ if i < len(x.Parameters)-1 {
49
+ s += " "
50
+ }
51
+ }
52
+ s += ">\n"
53
+ if x.Value != nil {
54
+ if x.Value.Ref != "" {
55
+ key := strings.ReplaceAll(strings.ReplaceAll(x.Value.Ref, "{", ""), "}", "")
56
+ if f, ok := funcMap[key]; ok {
57
+ s += f.(func() string)()
58
+ } else {
59
+ parts := strings.Split(key, ".")
60
+ if len(parts) == 2 {
61
+ if v, ok := ctx[parts[0]]; ok {
62
+ s += reflect.ValueOf(v).Elem().FieldByName(parts[1]).Interface().(string)
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ if x.Name == "For" {
69
+ ctxKey := getAttribute("key", x.Parameters)
70
+ ctxName := getAttribute("name", x.Parameters)
71
+ data := ctx[ctxKey]
72
+ switch reflect.TypeOf(data).Kind() {
73
+ case reflect.Slice:
74
+ v := reflect.ValueOf(data)
75
+ for i := 0; i < v.Len(); i++ {
76
+ ctx["_space"] = space + " "
77
+ ctx[ctxName] = v.Index(i).Interface()
78
+ s += Render(x.Children[0], ctx)
79
+ }
80
+ }
81
+ } else {
82
+ if comp, ok := compMap[x.Name]; ok {
83
+ ctxKey := getAttribute("key", x.Parameters)
84
+ result := reflect.ValueOf(comp).Call([]reflect.Value{reflect.ValueOf(ctx[ctxKey])})
85
+ s += result[0].Interface().(string)
86
+ } else {
87
+ found := false
88
+ for _, t := range htmlTags {
89
+ if t == x.Name {
90
+ found = true
91
+ }
92
+ }
93
+ if !found {
94
+ panic(fmt.Errorf("Comp not found %s", x.Name))
95
+ }
96
+ }
97
+ for _, c := range x.Children {
98
+ ctx["_space"] = space + " "
99
+ s += Render(c, ctx) + "\n"
100
+ }
101
+ }
102
+ s += space + "</" + x.Name + ">"
103
+ return s
104
+ }
105
+
106
+ func Html(ctx map[string]interface{}, tpl string) string {
107
+ tree := &Module{}
108
+ err := xmlParser.ParseBytes("filename", []byte(tpl), tree)
109
+ if err != nil {
110
+ panic(err)
111
+ }
112
+ o := ""
113
+ for _, n := range tree.Nodes {
114
+ v := Render(n, ctx)
115
+ o += v
116
+ }
117
+ return o
118
+ }
119
+
120
+ // <script>
121
+ // document.addEventListener('alpine:init', () => {
122
+ // Alpine.store('todos', {
123
+ // list: [],
124
+ // count: 0,
125
+ // })
126
+ // })
127
+ // </script>
128
+
129
+ // patch: {
130
+ // { "op": "add", "path": "/todos/list", "value": { "id": "123", "text": "123" } },
131
+ // }
template/template_test.go ADDED
@@ -0,0 +1,52 @@
1
+ package template
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/stretchr/testify/require"
7
+ )
8
+
9
+ type TodoData struct {
10
+ ID string
11
+ Text string
12
+ }
13
+
14
+ func Todo(data *TodoData) string {
15
+ ctx := map[string]interface{}{
16
+ "todo": data,
17
+ }
18
+ return Html(ctx, `
19
+ <li id={todo.ID} :class="{ 'completed': todo.Completed }">
20
+ <div class="view">
21
+ <span>{todo.Text}</span>
22
+ </div>
23
+ </li>
24
+ `)
25
+ }
26
+
27
+ func WebsiteName() string {
28
+ return "My Website"
29
+ }
30
+
31
+ func TestHtml(t *testing.T) {
32
+ r := require.New(t)
33
+ RegisterComponent(Todo)
34
+ RegisterFunc(WebsiteName)
35
+ ctx := map[string]interface{}{
36
+ "_space": "",
37
+ "todos": []*TodoData{
38
+ {ID: "b1a7359c-ebb4-11ec-8ea0-0242ac120002", Text: "My first todo"},
39
+ },
40
+ }
41
+ actual := Html(ctx, `
42
+ <ul id="todo-list" class="relative">
43
+ <For key="todos" name="todo">
44
+ <Todo key="todo"></Todo>
45
+ </For>
46
+ <span>{WebsiteName}</span>
47
+ </ul>
48
+ `)
49
+ expected := `<ul id="todo-list" class="relative">
50
+ </ul>`
51
+ r.Equal(expected, actual)
52
+ }