~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.


58731858 Peter John

3 years ago
implement for
Files changed (4) hide show
  1. gsx/gsx.go +54 -47
  2. gsx/gsx_test.go +180 -64
  3. gsx/parser.go +87 -8
  4. gsx/parser_test.go +27 -2
gsx/gsx.go CHANGED
@@ -26,6 +26,7 @@ type (
26
26
  MS map[string]string
27
27
  Arr []interface{}
28
28
  ComponentFunc struct {
29
+ Name string
29
30
  Func interface{}
30
31
  Args []string
31
32
  Classes M
@@ -41,6 +42,7 @@ type (
41
42
  func RegisterComponent(f interface{}, classes M, args ...string) {
42
43
  name := getFunctionName(f)
43
44
  compMap[name] = ComponentFunc{
45
+ Name: name,
44
46
  Func: f,
45
47
  Args: args,
46
48
  Classes: classes,
@@ -64,19 +66,36 @@ func (comp ComponentFunc) Render(c *Context, tag *Tag) []*Tag {
64
66
  if v, ok := c.data[arg]; ok {
65
67
  args = append(args, reflect.ValueOf(v))
66
68
  } else {
69
+ t := funcType.In(i + 1)
67
70
  v, _ := lo.Find(tag.Attributes, func(a *Attribute) bool {
68
71
  return a.Key == arg
69
72
  })
73
+ var data interface{}
74
+ if v.Value.Ref != nil {
75
+ data = c.data[*v.Value.Ref]
70
- t := funcType.In(i + 1)
76
+ } else if v.Value.Str != nil {
77
+ data = *v.Value.Str
78
+ }
71
79
  switch t.Kind() {
72
80
  case reflect.Int:
81
+ s, ok := data.(string)
82
+ if !ok {
83
+ panic(fmt.Errorf("expected component %s: prop %s to be of type string but got %+v ", comp.Name, arg, data))
84
+ }
73
- value, _ := strconv.Atoi(*v.Value.Str)
85
+ value, _ := strconv.Atoi(s)
86
+ c.Set(arg, value)
74
87
  args = append(args, reflect.ValueOf(value))
75
88
  case reflect.Bool:
89
+ s, ok := data.(string)
90
+ if !ok {
91
+ panic(fmt.Errorf("expected component %s: prop %s to be of type string but got %+v ", comp.Name, arg, data))
92
+ }
76
- value, _ := strconv.ParseBool(*v.Value.Str)
93
+ value, _ := strconv.ParseBool(s)
94
+ c.Set(arg, value)
77
95
  args = append(args, reflect.ValueOf(value))
78
96
  default:
97
+ c.Set(arg, data)
79
- args = append(args, reflect.ValueOf(v))
98
+ args = append(args, reflect.ValueOf(data))
80
99
  }
81
100
  }
82
101
  }
@@ -219,39 +238,40 @@ func populateTag(c *Context, tag *Tag) {
219
238
  sValue := fmt.Sprintf("%+v", value)
220
239
  tag.Text.Str = &sValue
221
240
  }
241
+ } else if loop := tag.Text.For; loop != nil {
242
+ tag.Name = "fragment"
243
+ data := c.data[loop.Reference]
244
+ statement := loop.Statements[0].ReturnStatement
245
+ switch reflect.TypeOf(data).Kind() {
246
+ case reflect.Slice:
247
+ v := reflect.ValueOf(data)
248
+ for i := 0; i < v.Len(); i++ {
249
+ compContext := c.Clone(tag.Name)
250
+ compContext.data[loop.Index] = i
251
+ compContext.data[loop.Key] = v.Index(i).Interface()
252
+ newTags := populate(compContext, cloneTags(statement.Tags))
253
+ for _, t := range newTags {
254
+ tag.Children = append(tag.Children, t)
255
+ }
256
+ }
257
+ }
222
258
  }
223
259
  } else {
260
+ if comp, ok := compMap[tag.Name]; ok {
261
+ if tag.SelfClosing {
262
+ tag.SelfClosing = false
263
+ }
264
+ compContext := c.Clone(tag.Name)
265
+ nodes := comp.Render(compContext, tag)
266
+ populate(compContext, tag.Children)
267
+ compContext.Set("children", tag.Children)
268
+ tag.Children = nodes
269
+ populate(compContext, tag.Children)
270
+ } else {
271
+ populate(c, tag.Children)
272
+ }
224
273
  for _, a := range tag.Attributes {
225
- if a.Key == "x-for" {
226
- arr := strings.Split(*a.Value.Str, " in ")
227
- // ctxItemKey := arr[0]
228
- ctxKey := arr[1]
229
- data := c.data[ctxKey]
230
- switch reflect.TypeOf(data).Kind() {
231
- case reflect.Slice:
232
- v := reflect.ValueOf(data)
233
- for i := 0; i < v.Len(); i++ {
234
- // ctx["_space"] = space + " "
235
- // ctx[ctxName] = v.Index(i).Interface()
236
- // s += render(x.Children[0], ctx) + "\n"
237
-
238
- // compCtx := &Context{
239
- // Context: c.Context,
240
- // data: map[string]interface{}{
241
- // ctxItemKey: v.Index(i).Interface(),
242
- // },
243
- // }
244
- // tag.Children
245
- // if comp, ok := compMap[itemChild.Data]; ok {
246
- // newNode := populateComponent(compCtx, comp, itemChild, false)
247
- // n.AppendChild(newNode)
248
- // } else {
249
- // n.AppendChild(itemChild)
250
- // populate(compCtx, itemChild)
251
- // }
252
- }
253
- }
254
- } else if a.Value.Str != nil {
274
+ if a.Value.Str != nil {
255
275
  if strings.Contains(*a.Value.Str, "{") {
256
276
  subs := substituteString(c, removeQuotes(*a.Value.Str))
257
277
  a.Value = &Literal{Str: &subs}
@@ -273,18 +293,5 @@ func populateTag(c *Context, tag *Tag) {
273
293
  a.Value.Str = &result
274
294
  }
275
295
  }
276
- if comp, ok := compMap[tag.Name]; ok {
277
- if tag.SelfClosing {
278
- tag.SelfClosing = false
279
- }
280
- compContext := c.Clone(tag.Name)
281
- nodes := comp.Render(compContext, tag)
282
- populate(compContext, tag.Children)
283
- compContext.Set("children", tag.Children)
284
- tag.Children = nodes
285
- populate(compContext, tag.Children)
286
- } else {
287
- populate(c, tag.Children)
288
- }
289
296
  }
290
297
  }
gsx/gsx_test.go CHANGED
@@ -49,6 +49,10 @@ func WebsiteName() string {
49
49
  return "My Website"
50
50
  }
51
51
 
52
+ func trimLeft(s string) string {
53
+ return strings.TrimLeft(s, "\n")
54
+ }
55
+
52
56
  func TestComponent(t *testing.T) {
53
57
  r := require.New(t)
54
58
  RegisterComponent(Todo, nil, "todo")
@@ -66,7 +70,7 @@ func TestComponent(t *testing.T) {
66
70
  <Todo />
67
71
  `)
68
72
  actual := renderString(nodes)
69
- expected := strings.TrimLeft(`
73
+ expected := trimLeft(`
70
74
  <Todo>
71
75
  <li id="todo-4" class="completed">
72
76
  <div class="upper">
@@ -115,7 +119,7 @@ func TestComponent(t *testing.T) {
115
119
  </div>
116
120
  </li>
117
121
  </Todo>
118
- `, "\n")
122
+ `)
119
123
  r.Equal(expected, actual)
120
124
  }
121
125
 
@@ -134,7 +138,7 @@ func TestMultipleComponent(t *testing.T) {
134
138
  <TodoCount />
135
139
  `)
136
140
  actual := renderString(nodes)
137
- expected := strings.TrimLeft(`
141
+ expected := trimLeft(`
138
142
  <Todo>
139
143
  <li id="todo-4" class="completed">
140
144
  <div class="upper">
@@ -164,70 +168,182 @@ func TestMultipleComponent(t *testing.T) {
164
168
  item left
165
169
  </span>
166
170
  </TodoCount>
167
- `, "\n")
171
+ `)
168
172
  r.Equal(expected, actual)
169
173
  }
170
174
 
171
- // func TestFor(t *testing.T) {
172
- // r := require.New(t)
173
- // RegisterComponent(Todo, nil, "todo")
174
- // RegisterFunc(WebsiteName)
175
- // h := Context{
176
- // data: map[string]interface{}{
177
- // "todos": []*TodoData{
178
- // {ID: "1", Text: "My first todo", Completed: true},
179
- // {ID: "2", Text: "My second todo", Completed: false},
180
- // {ID: "3", Text: "My third todo", Completed: false},
181
- // },
182
- // },
183
- // }
184
- // actual := h.Render(`
185
- // <div>
186
- // <ul x-for="todo in todos" class="relative">
187
- // <li>
188
- // <span>{todo.Text}</span>
189
- // <span>{todo.Completed}</span>
190
- // <a>link to {todo.ID}</a>
191
- // </li>
192
- // </ul>
193
- // <ol x-for="todo in todos">
194
- // <Todo>
195
- // <div class="todo-panel">
196
- // <span>{todo.Text}</span>
197
- // <span>{todo.Completed}</span>
198
- // </div>
199
- // </Todo>
200
- // </ol>
201
- // </div>
202
- // `).String()
203
- // expected := stripWhitespace(`
204
- // <div>
205
- // <ul x-for="todo in todos" class="relative">
206
- // <li><span>My first todo</span><span>true</span><a>link to 1</a></li>
207
- // <li><span>My second todo</span><span>false</span><a>link to 2</a></li>
208
- // <li><span>My third todo</span><span>false</span><a>link to 3</a></li>
209
- // </ul>
210
- // <ol x-for="todo in todos">
211
- // <li id="todo-1" class="completed">
212
- // <div class="view"><span>My first todo</span><span>My first todo</span></div>
213
- // <div class="todo-panel"><span>My first todo</span><span>true</span></div>
214
- // <div class="count"><span>true</span><span>true</span></div>
215
- // </li>
216
- // <li id="todo-2" class="">
217
- // <div class="view"><span>My second todo</span><span>My second todo</span></div>
218
- // <div class="todo-panel"><span>My second todo</span><span>false</span></div>
219
- // <div class="count"><span>false</span><span>false</span></div>
220
- // </li>
221
- // <li id="todo-3" class="">
222
- // <div class="view"><span>My third todo</span><span>My third todo</span></div>
223
- // <div class="todo-panel"><span>My third todo</span><span>false</span></div>
224
- // <div class="count"><span>false</span><span>false</span></div>
225
- // </li>
226
- // </ol>
227
- // </div>
228
- // `)
229
- // r.Equal(expected, actual)
230
- // }
175
+ func TestFor(t *testing.T) {
176
+ r := require.New(t)
177
+ RegisterComponent(Todo, nil, "todo")
178
+ RegisterFunc(WebsiteName)
179
+ h := Context{
180
+ data: map[string]interface{}{
181
+ "todos": []*TodoData{
182
+ {ID: "1", Text: "My first todo", Completed: true},
183
+ {ID: "2", Text: "My second todo", Completed: false},
184
+ {ID: "3", Text: "My third todo", Completed: false},
185
+ },
186
+ },
187
+ }
188
+ nodes := h.Render(`
189
+ <ul class="relative">
190
+ for i, v := range todos {
191
+ return (
192
+ <li>
193
+ <span>{v.Text}</span>
194
+ <span>{v.Completed}</span>
195
+ <a>"link to" {v.ID}</a>
196
+ </li>
197
+ )
198
+ }
199
+ </ul>
200
+ <ol>
201
+ for i, v := range todos {
202
+ return (
203
+ <Todo todo={v}>
204
+ <div class="todo-panel">
205
+ <span>{v.Text}</span>
206
+ <span>{v.Completed}</span>
207
+ </div>
208
+ </Todo>
209
+ )
210
+ }
211
+ </ol>
212
+ `)
213
+ actual := renderString(nodes)
214
+ expected := trimLeft(`
215
+ <ul class="relative">
216
+ <li>
217
+ <span>
218
+ My first todo
219
+ </span>
220
+ <span>
221
+ true
222
+ </span>
223
+ <a>
224
+ link to
225
+ 1
226
+ </a>
227
+ </li>
228
+ <li>
229
+ <span>
230
+ My second todo
231
+ </span>
232
+ <span>
233
+ false
234
+ </span>
235
+ <a>
236
+ link to
237
+ 2
238
+ </a>
239
+ </li>
240
+ <li>
241
+ <span>
242
+ My third todo
243
+ </span>
244
+ <span>
245
+ false
246
+ </span>
247
+ <a>
248
+ link to
249
+ 3
250
+ </a>
251
+ </li>
252
+
253
+ </ul>
254
+ <ol>
255
+ <Todo todo="v">
256
+ <li id="todo-1" class="completed">
257
+ <div class="upper">
258
+ <span>
259
+ My first todo
260
+ </span>
261
+ <span>
262
+ My first todo
263
+ </span>
264
+ </div>
265
+ <div class="todo-panel">
266
+ <span>
267
+ My first todo
268
+ </span>
269
+ <span>
270
+ true
271
+ </span>
272
+ </div>
273
+
274
+ <div class="bottom">
275
+ <span>
276
+ true
277
+ </span>
278
+ <span>
279
+ true
280
+ </span>
281
+ </div>
282
+ </li>
283
+ </Todo>
284
+ <Todo todo="v">
285
+ <li id="todo-2">
286
+ <div class="upper">
287
+ <span>
288
+ My second todo
289
+ </span>
290
+ <span>
291
+ My second todo
292
+ </span>
293
+ </div>
294
+ <div class="todo-panel">
295
+ <span>
296
+ My second todo
297
+ </span>
298
+ <span>
299
+ false
300
+ </span>
301
+ </div>
302
+
303
+ <div class="bottom">
304
+ <span>
305
+ false
306
+ </span>
307
+ <span>
308
+ false
309
+ </span>
310
+ </div>
311
+ </li>
312
+ </Todo>
313
+ <Todo todo="v">
314
+ <li id="todo-3">
315
+ <div class="upper">
316
+ <span>
317
+ My third todo
318
+ </span>
319
+ <span>
320
+ My third todo
321
+ </span>
322
+ </div>
323
+ <div class="todo-panel">
324
+ <span>
325
+ My third todo
326
+ </span>
327
+ <span>
328
+ false
329
+ </span>
330
+ </div>
331
+
332
+ <div class="bottom">
333
+ <span>
334
+ false
335
+ </span>
336
+ <span>
337
+ false
338
+ </span>
339
+ </div>
340
+ </li>
341
+ </Todo>
342
+
343
+ </ol>
344
+ `)
345
+ r.Equal(expected, actual)
346
+ }
231
347
 
232
348
  // func TestForComponent(t *testing.T) {
233
349
  // r := require.New(t)
gsx/parser.go CHANGED
@@ -10,8 +10,8 @@ import (
10
10
  )
11
11
 
12
12
  type Module struct {
13
- Pos lexer.Position
13
+ Pos lexer.Position
14
- AstNode []*AstNode `@@*`
14
+ Nodes []*AstNode `@@*`
15
15
  }
16
16
 
17
17
  type AstNode struct {
@@ -33,6 +33,23 @@ type Close struct {
33
33
  Name string `"<""/"@Ident">"`
34
34
  }
35
35
 
36
+ type ForStatement struct {
37
+ Pos lexer.Position `"for"`
38
+ Index string `@Ident ","`
39
+ Key string `@Ident`
40
+ Reference string `":""=""range" @Ident`
41
+ Statements []*Statement `"{" @@* "}"`
42
+ }
43
+
44
+ type Statement struct {
45
+ ReturnStatement *ReturnStatement `@@`
46
+ }
47
+
48
+ type ReturnStatement struct {
49
+ Nodes []*AstNode `"return" "(" @@* ")"`
50
+ Tags []*Tag
51
+ }
52
+
36
53
  type Attribute struct {
37
54
  Pos lexer.Position
38
55
  Key string `@":"? @Ident ( @"-" @Ident )*`
@@ -47,9 +64,36 @@ type KV struct {
47
64
 
48
65
  type Literal struct {
49
66
  Pos lexer.Position
50
- Str *string `@String`
67
+ Str *string `@String`
51
- Ref *string `| "{" @Ident ( @"." @Ident )* "}"`
68
+ Ref *string `| "{" @Ident ( @"." @Ident )* "}"`
52
- KV []*KV `| "{" @@* "}"`
69
+ KV []*KV `| "{" @@* "}"`
70
+ For *ForStatement `| @@`
71
+ }
72
+
73
+ func (l *Literal) Clone() *Literal {
74
+ if l == nil {
75
+ return nil
76
+ }
77
+ newLiteral := &Literal{}
78
+ if l.Str != nil {
79
+ v := "" + *l.Str
80
+ newLiteral.Str = &v
81
+ }
82
+ if l.Ref != nil {
83
+ v := "" + *l.Ref
84
+ newLiteral.Ref = &v
85
+ }
86
+ if l.KV != nil {
87
+ newLiteral.KV = []*KV{}
88
+ for _, kv := range l.KV {
89
+ newLiteral.KV = append(newLiteral.KV, &KV{
90
+ Key: "" + kv.Key,
91
+ Value: "" + kv.Value,
92
+ })
93
+ }
94
+ }
95
+ // TODO copy for
96
+ return newLiteral
53
97
  }
54
98
 
55
99
  var htmlParser = participle.MustBuild[Module]()
@@ -62,6 +106,34 @@ type Tag struct {
62
106
  SelfClosing bool
63
107
  }
64
108
 
109
+ func (t *Tag) Clone() *Tag {
110
+ newTag := &Tag{
111
+ Name: t.Name,
112
+ Text: t.Text.Clone(),
113
+ Attributes: []*Attribute{},
114
+ SelfClosing: t.SelfClosing,
115
+ Children: []*Tag{},
116
+ }
117
+ for _, v := range t.Attributes {
118
+ newTag.Attributes = append(newTag.Attributes, &Attribute{
119
+ Key: v.Key,
120
+ Value: v.Value.Clone(),
121
+ })
122
+ }
123
+ for _, child := range t.Children {
124
+ newTag.Children = append(newTag.Children, child.Clone())
125
+ }
126
+ return newTag
127
+ }
128
+
129
+ func cloneTags(tags []*Tag) []*Tag {
130
+ newTags := []*Tag{}
131
+ for _, v := range tags {
132
+ newTags = append(newTags, v.Clone())
133
+ }
134
+ return newTags
135
+ }
136
+
65
137
  func renderString(tags []*Tag) string {
66
138
  s := ""
67
139
  for _, t := range tags {
@@ -106,11 +178,11 @@ func renderTagString(x *Tag, space string) string {
106
178
  return s
107
179
  }
108
180
 
109
- func processTree(module *Module) []*Tag {
181
+ func processTree(nodes []*AstNode) []*Tag {
110
182
  tags := []*Tag{}
111
183
  var prevTag *Tag
112
184
  stack := stack.New[*Tag]()
113
- for _, n := range module.AstNode {
185
+ for _, n := range nodes {
114
186
  if n.Open != nil {
115
187
  newTag := &Tag{
116
188
  Name: n.Open.Name,
@@ -140,6 +212,13 @@ func processTree(module *Module) []*Tag {
140
212
  Name: "",
141
213
  Text: n.Content,
142
214
  }
215
+ if n.Content.For != nil {
216
+ for _, s := range n.Content.For.Statements {
217
+ if s.ReturnStatement != nil {
218
+ s.ReturnStatement.Tags = processTree(s.ReturnStatement.Nodes)
219
+ }
220
+ }
221
+ }
143
222
  if prevTag != nil {
144
223
  prevTag.Children = append(prevTag.Children, newTag)
145
224
  } else {
@@ -155,5 +234,5 @@ func parse(name, s string) []*Tag {
155
234
  if err != nil {
156
235
  panic(err)
157
236
  }
158
- return processTree(ast)
237
+ return processTree(ast.Nodes)
159
238
  }
gsx/parser_test.go CHANGED
@@ -23,7 +23,6 @@ func TestParse(t *testing.T) {
23
23
  </p>
24
24
  </div>
25
25
  `))
26
- println(actual)
27
26
  expected := strings.TrimLeft(`
28
27
  <ul id=""todo-list"" class=""relative"">
29
28
  <Todo>
@@ -51,10 +50,36 @@ func TestSelfClose(t *testing.T) {
51
50
  <Todo />
52
51
  <TodoCount />
53
52
  `))
54
- println(actual)
55
53
  expected := strings.TrimLeft(`
56
54
  <Todo />
57
55
  <TodoCount />
58
56
  `, "\n")
59
57
  r.Equal(expected, actual)
60
58
  }
59
+
60
+ func TestForLoop(t *testing.T) {
61
+ r := require.New(t)
62
+ actual := renderString(parse("test", `
63
+ <ul>
64
+ for k, v := range todos {
65
+ return (
66
+ <li>
67
+ "data"
68
+ </li>
69
+ <div>
70
+ <span>
71
+ {name}
72
+ </span>
73
+ </div>
74
+ )
75
+ }
76
+ </ul>
77
+ `))
78
+ expected := strings.TrimLeft(`
79
+ <ul>
80
+ <>
81
+ </>
82
+ </ul>
83
+ `, "\n")
84
+ r.Equal(expected, actual)
85
+ }