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


2caf3db0 Peter John

4 years ago
improve more
LICENSE DELETED
@@ -1,43 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2016 Maxence Charriere
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
22
-
23
- MIT License
24
-
25
- Copyright (c) 2020 pyros2097
26
-
27
- Permission is hereby granted, free of charge, to any person obtaining a copy
28
- of this software and associated documentation files (the "Software"), to deal
29
- in the Software without restriction, including without limitation the rights
30
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31
- copies of the Software, and to permit persons to whom the Software is
32
- furnished to do so, subject to the following conditions:
33
-
34
- The above copyright notice and this permission notice shall be included in all
35
- copies or substantial portions of the Software.
36
-
37
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
43
- SOFTWARE.
attributes.go DELETED
@@ -1,132 +0,0 @@
1
- package app
2
-
3
- import (
4
- "strconv"
5
- )
6
-
7
- type Attribute struct {
8
- Key string
9
- Value string
10
- }
11
-
12
- func ID(v string) Attribute {
13
- return Attribute{"id", v}
14
- }
15
-
16
- func Style(v string) Attribute {
17
- return Attribute{"style", v}
18
- }
19
-
20
- func Accept(v string) Attribute {
21
- return Attribute{"accept", v}
22
- }
23
-
24
- func AutoComplete(v bool) Attribute {
25
- return Attribute{"autocomplete", strconv.FormatBool(v)}
26
- }
27
-
28
- func Checked(v bool) Attribute {
29
- return Attribute{"checked", strconv.FormatBool(v)}
30
- }
31
-
32
- func Disabled(v bool) Attribute {
33
- return Attribute{"disabled", strconv.FormatBool(v)}
34
- }
35
-
36
- func Name(v string) Attribute {
37
- return Attribute{"name", v}
38
- }
39
-
40
- func Type(v string) Attribute {
41
- return Attribute{"type", v}
42
- }
43
-
44
- func Value(v string) Attribute {
45
- return Attribute{"value", v}
46
- }
47
-
48
- func Placeholder(v string) Attribute {
49
- return Attribute{"placeholder", v}
50
- }
51
-
52
- func Src(v string) Attribute {
53
- return Attribute{"src", v}
54
- }
55
-
56
- func Defer() Attribute {
57
- return Attribute{"defer", "true"}
58
- }
59
-
60
- func ViewBox(v string) Attribute {
61
- return Attribute{"viewBox", v}
62
- }
63
-
64
- func X(v string) Attribute {
65
- return Attribute{"x", v}
66
- }
67
-
68
- func Y(v string) Attribute {
69
- return Attribute{"y", v}
70
- }
71
-
72
- func Href(v string) Attribute {
73
- return Attribute{"href", v}
74
- }
75
-
76
- func Target(v string) Attribute {
77
- return Attribute{"target", v}
78
- }
79
-
80
- func Rel(v string) Attribute {
81
- return Attribute{"rel", v}
82
- }
83
-
84
- type CssAttribute struct {
85
- classes string
86
- }
87
-
88
- func Css(d string) CssAttribute {
89
- return CssAttribute{classes: d}
90
- }
91
-
92
- func CssIf(v bool, d string) CssAttribute {
93
- if v {
94
- return CssAttribute{classes: d}
95
- }
96
- return CssAttribute{}
97
- }
98
-
99
- func XData(v string) Attribute {
100
- return Attribute{"x-data", v}
101
- }
102
-
103
- func XText(v string) Attribute {
104
- return Attribute{"x-text", v}
105
- }
106
-
107
- func MergeAttributes(parent *Element, uis ...interface{}) *Element {
108
- elems := []*Element{}
109
- for _, v := range uis {
110
- switch c := v.(type) {
111
- case Attribute:
112
- parent.setAttr(c.Key, c.Value)
113
- case CssAttribute:
114
- if vv, ok := parent.attrs["classes"]; ok {
115
- parent.setAttr("class", vv+" "+c.classes)
116
- } else {
117
- parent.setAttr("class", c.classes)
118
- }
119
- case *Element:
120
- elems = append(elems, c)
121
- case nil:
122
- // dont need to add nil items
123
- default:
124
- // fmt.Printf("%v\n", v)
125
- panic("unknown type in render")
126
- }
127
- }
128
- if !parent.selfClosing {
129
- parent.body = elems
130
- }
131
- return parent
132
- }
attributes_test.go DELETED
@@ -1,48 +0,0 @@
1
- package app
2
-
3
- import (
4
- "bytes"
5
- "strconv"
6
- "testing"
7
-
8
- "github.com/stretchr/testify/assert"
9
- )
10
-
11
- func Counter(c *RenderContext) UI {
12
- count, _ := c.UseInt(0)
13
- return Col(
14
- Row(Css("yellow"),
15
- Text("Counter"),
16
- ),
17
- Row(
18
- Div(
19
- Text("-"),
20
- ),
21
- Div(
22
- Text(strconv.Itoa(count())),
23
- ),
24
- Div(
25
- Text("+"),
26
- ),
27
- ),
28
- )
29
- }
30
-
31
- func HomeRoute(c *RenderContext) UI {
32
- return Div(
33
- Div(),
34
- Counter(c),
35
- )
36
- }
37
-
38
- func TestCreatePage(t *testing.T) {
39
- page := bytes.NewBuffer(nil)
40
- page.WriteString("<!DOCTYPE html>\n")
41
- Html(
42
- Head(
43
- Title("Title"),
44
- ),
45
- Body(HomeRoute(NewRenderContext())),
46
- ).Html(page)
47
- assert.Equal(t, "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\">\n <meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\">\n <meta http-equiv=\"encoding\" content=\"utf-8\">\n <title>\n Title\n </title>\n </head>\n <body>\n <div>\n <div></div>\n <div class=\"flex flex-col justify-center items-center\">\n <div class=\"flex flex-row justify-center items-center yellow\">\n Counter\n </div>\n <div class=\"flex flex-row justify-center items-center\">\n <div>\n -\n </div>\n <div>\n 0\n </div>\n <div>\n +\n </div>\n </div>\n </div>\n </div>\n </body>\n</html>", page.String())
48
- }
element.go DELETED
@@ -1,139 +0,0 @@
1
- package app
2
-
3
- import (
4
- "io"
5
- "unsafe"
6
- )
7
-
8
- type Element struct {
9
- tag string
10
- attrs map[string]string
11
- body []*Element
12
- selfClosing bool
13
- text string
14
- }
15
-
16
- func NewElement(tag string, selfClosing bool, uis ...interface{}) *Element {
17
- return MergeAttributes(&Element{tag: tag, selfClosing: selfClosing}, uis...)
18
- }
19
-
20
- func (e *Element) updateAttrs(attrs map[string]string) {
21
- for k := range e.attrs {
22
- if _, exists := attrs[k]; !exists {
23
- e.delAttr(k)
24
- }
25
- }
26
-
27
- if e.attrs == nil && len(attrs) != 0 {
28
- e.attrs = make(map[string]string, len(attrs))
29
- }
30
-
31
- for k, v := range attrs {
32
- if curval, exists := e.attrs[k]; !exists || curval != v {
33
- e.attrs[k] = v
34
- }
35
- }
36
- }
37
-
38
- func (e *Element) setAttr(k string, v string) {
39
- if e.attrs == nil {
40
- e.attrs = make(map[string]string)
41
- }
42
-
43
- switch k {
44
- case "style", "allow":
45
- s := e.attrs[k] + v + ";"
46
- e.attrs[k] = s
47
- return
48
-
49
- case "class":
50
- s := e.attrs[k]
51
- if s != "" {
52
- s += " "
53
- }
54
- s += v
55
- e.attrs[k] = s
56
- return
57
- }
58
- if v == "false" {
59
- delete(e.attrs, k)
60
- return
61
- } else if v == "true" {
62
- e.attrs[k] = ""
63
- } else {
64
- e.attrs[k] = v
65
- }
66
- }
67
-
68
- func (e *Element) delAttr(k string) {
69
- delete(e.attrs, k)
70
- }
71
-
72
- func writeIndent(w io.Writer, indent int) {
73
- for i := 0; i < indent*4; i++ {
74
- w.Write(stob(" "))
75
- }
76
- }
77
-
78
- func ln() []byte {
79
- return stob("\n")
80
- }
81
-
82
- func btos(b []byte) string {
83
- return *(*string)(unsafe.Pointer(&b))
84
- }
85
-
86
- func stob(s string) []byte {
87
- return *(*[]byte)(unsafe.Pointer(&s))
88
- }
89
-
90
- func (e *Element) Html(w io.Writer) {
91
- e.HtmlWithIndent(w, 0)
92
- }
93
-
94
- func (e *Element) HtmlWithIndent(w io.Writer, indent int) {
95
- writeIndent(w, indent)
96
- if e.tag == "html" {
97
- w.Write(stob("<!DOCTYPE html>\n"))
98
- }
99
- if e.tag == "text" {
100
- writeIndent(w, indent)
101
- w.Write(stob(e.text))
102
- return
103
- }
104
- w.Write(stob("<"))
105
- w.Write(stob(e.tag))
106
-
107
- for k, v := range e.attrs {
108
- w.Write(stob(" "))
109
- w.Write(stob(k))
110
-
111
- if v != "" {
112
- w.Write(stob(`="`))
113
- w.Write(stob(v))
114
- w.Write(stob(`"`))
115
- }
116
- }
117
-
118
- w.Write(stob(">"))
119
-
120
- if e.selfClosing {
121
- return
122
- }
123
-
124
- for _, c := range e.body {
125
- w.Write(ln())
126
- if c != nil {
127
- c.HtmlWithIndent(w, indent+1)
128
- }
129
- }
130
-
131
- if len(e.body) != 0 {
132
- w.Write(ln())
133
- writeIndent(w, indent)
134
- }
135
-
136
- w.Write(stob("</"))
137
- w.Write(stob(e.tag))
138
- w.Write(stob(">"))
139
- }
element_test.go DELETED
@@ -1,461 +0,0 @@
1
- package app
2
-
3
- // func TestElemSetAttr(t *testing.T) {
4
- // utests := []struct {
5
- // scenario string
6
- // key string
7
- // value interface{}
8
- // explectedValue string
9
- // valueNotSet bool
10
- // }{
11
- // {
12
- // scenario: "string",
13
- // key: "title",
14
- // value: "test",
15
- // explectedValue: "test",
16
- // },
17
- // {
18
- // scenario: "int",
19
- // key: "max",
20
- // value: 42,
21
- // explectedValue: "42",
22
- // },
23
- // {
24
- // scenario: "bool true",
25
- // key: "hidden",
26
- // value: true,
27
- // explectedValue: "",
28
- // },
29
- // {
30
- // scenario: "bool false",
31
- // key: "hidden",
32
- // value: false,
33
- // valueNotSet: true,
34
- // },
35
- // {
36
- // scenario: "style",
37
- // key: "style",
38
- // value: "margin:42",
39
- // explectedValue: "margin:42;",
40
- // },
41
- // {
42
- // scenario: "set successive styles",
43
- // key: "style",
44
- // value: "padding:42",
45
- // explectedValue: "margin:42;padding:42;",
46
- // },
47
- // {
48
- // scenario: "class",
49
- // key: "class",
50
- // value: "hello",
51
- // explectedValue: "hello",
52
- // },
53
- // {
54
- // scenario: "set successive classes",
55
- // key: "class",
56
- // value: "world",
57
- // explectedValue: "hello world",
58
- // },
59
- // }
60
-
61
- // e := &elem{}
62
-
63
- // for _, u := range utests {
64
- // t.Run(u.scenario, func(t *testing.T) {
65
- // e.setAttr(u.key, u.value)
66
- // v, exists := e.attrs[u.key]
67
- // require.Equal(t, u.explectedValue, v)
68
- // require.Equal(t, u.valueNotSet, !exists)
69
- // })
70
- // }
71
- // }
72
-
73
- // func TestElemUpdateAttrs(t *testing.T) {
74
- // utests := []struct {
75
- // scenario string
76
- // current map[string]string
77
- // incoming map[string]string
78
- // }{
79
- // {
80
- // scenario: "attributes are removed",
81
- // current: map[string]string{
82
- // "foo": "bar",
83
- // "hello": "world",
84
- // },
85
- // incoming: nil,
86
- // },
87
- // {
88
- // scenario: "attributes are added",
89
- // current: nil,
90
- // incoming: map[string]string{
91
- // "foo": "bar",
92
- // "hello": "world",
93
- // },
94
- // },
95
- // {
96
- // scenario: "attributes are updated",
97
- // current: map[string]string{
98
- // "foo": "bar",
99
- // "hello": "world",
100
- // },
101
- // incoming: map[string]string{
102
- // "foo": "boo",
103
- // "hello": "there",
104
- // },
105
- // },
106
- // {
107
- // scenario: "attributes are synced",
108
- // current: map[string]string{
109
- // "foo": "bar",
110
- // "hello": "world",
111
- // },
112
- // incoming: map[string]string{
113
- // "foo": "boo",
114
- // "goodbye": "world",
115
- // },
116
- // },
117
- // }
118
-
119
- // for _, u := range utests {
120
- // t.Run(u.scenario, func(t *testing.T) {
121
- // testSkipNonWasm(t)
122
-
123
- // n := Div().(*htmlDiv)
124
- // err := mount(n)
125
- // require.NoError(t, err)
126
- // defer dismount(n)
127
-
128
- // n.attrs = u.current
129
- // n.updateAttrs(u.incoming)
130
-
131
- // if len(u.incoming) == 0 {
132
- // require.Empty(t, n.attributes())
133
- // return
134
- // }
135
-
136
- // require.Equal(t, u.incoming, n.attributes())
137
- // })
138
- // }
139
- // }
140
-
141
- // func TestElemSetEventHandler(t *testing.T) {
142
- // e := &elem{}
143
- // h := func(Context, Event) {}
144
- // e.setEventHandler("click", h)
145
-
146
- // expectedHandler := eventHandler{
147
- // event: "click",
148
- // value: h,
149
- // }
150
-
151
- // registeredHandler := e.events["click"]
152
- // require.True(t, expectedHandler.equal(registeredHandler))
153
- // }
154
-
155
- // func TestElemUpdateEventHandlers(t *testing.T) {
156
- // utests := []struct {
157
- // scenario string
158
- // current EventHandler
159
- // incoming EventHandler
160
- // }{
161
- // {
162
- // scenario: "handler is removed",
163
- // current: func(Context, Event) {},
164
- // incoming: nil,
165
- // },
166
- // {
167
- // scenario: "handler is added",
168
- // current: nil,
169
- // incoming: func(Context, Event) {},
170
- // },
171
- // {
172
- // scenario: "handler is updated",
173
- // current: func(Context, Event) {},
174
- // incoming: func(Context, Event) {},
175
- // },
176
- // }
177
-
178
- // for _, u := range utests {
179
- // t.Run(u.scenario, func(t *testing.T) {
180
- // testSkipNonWasm(t)
181
-
182
- // var current map[string]eventHandler
183
- // var incoming map[string]eventHandler
184
-
185
- // if u.current != nil {
186
- // current = map[string]eventHandler{
187
- // "click": {
188
- // event: "click",
189
- // value: u.current,
190
- // },
191
- // }
192
- // }
193
-
194
- // if u.incoming != nil {
195
- // incoming = map[string]eventHandler{
196
- // "click": {
197
- // event: "click",
198
- // value: u.incoming,
199
- // },
200
- // }
201
- // }
202
-
203
- // n := Div().(*htmlDiv)
204
- // n.events = current
205
- // err := mount(n)
206
- // require.NoError(t, err)
207
- // defer dismount(n)
208
-
209
- // n.updateEventHandler(incoming)
210
-
211
- // if len(incoming) == 0 {
212
- // require.Empty(t, n.attributes())
213
- // return
214
- // }
215
-
216
- // h := n.eventHandlers()["click"]
217
- // require.True(t, h.equal(incoming["click"]))
218
- // })
219
- // }
220
- // }
221
-
222
- // func TestElemMountDismount(t *testing.T) {
223
- // testMountDismount(t, []mountTest{
224
- // {
225
- // scenario: "html element",
226
- // node: Div().
227
- // Class("hello").
228
- // OnClick(func(Context, Event) {}),
229
- // },
230
- // })
231
- // }
232
-
233
- // // func TestElemUpdate(t *testing.T) {
234
- // // testUpdate(t, []updateTest{
235
- // // {
236
- // // scenario: "html element returns replace error when updated with a non html-element",
237
- // // a: Div(),
238
- // // b: Text("hello"),
239
- // // replaceErr: true,
240
- // // },
241
- // // {
242
- // // scenario: "html element attributes are updated",
243
- // // a: Div().
244
- // // ID("max").
245
- // // Class("foo").
246
- // // AccessKey("test"),
247
- // // b: Div().
248
- // // ID("max").
249
- // // Class("bar").
250
- // // Lang("fr"),
251
- // // matches: []TestUIDescriptor{
252
- // // {
253
- // // Expected: Div().
254
- // // ID("max").
255
- // // Class("bar").
256
- // // Lang("fr"),
257
- // // },
258
- // // },
259
- // // },
260
- // // {
261
- // // scenario: "html element event handlers are updated",
262
- // // a: Div().
263
- // // OnClick(func(Context, Event) {}).
264
- // // OnBlur(func(Context, Event) {}),
265
- // // b: Div().
266
- // // OnClick(func(Context, Event) {}).
267
- // // OnChange(func(Context, Event) {}),
268
- // // matches: []TestUIDescriptor{
269
- // // {
270
- // // Expected: Div().
271
- // // OnClick(nil).
272
- // // OnChange(nil),
273
- // // },
274
- // // },
275
- // // },
276
- // // {
277
- // // scenario: "html element is replaced by a text",
278
- // // a: Div().Body(
279
- // // H2().Text("hello"),
280
- // // ),
281
- // // b: Div().Body(
282
- // // Text("hello"),
283
- // // ),
284
- // // matches: []TestUIDescriptor{
285
- // // {
286
- // // Path: TestPath(),
287
- // // Expected: Div(),
288
- // // },
289
- // // {
290
- // // Path: TestPath(0),
291
- // // Expected: Text("hello"),
292
- // // },
293
- // // },
294
- // // },
295
- // // {
296
- // // scenario: "html element is replaced by a component",
297
- // // a: Div().Body(
298
- // // H2().Text("hello"),
299
- // // ),
300
- // // b: Div().Body(
301
- // // &hello{},
302
- // // ),
303
- // // matches: []TestUIDescriptor{
304
- // // {
305
- // // Path: TestPath(),
306
- // // Expected: Div(),
307
- // // },
308
- // // {
309
- // // Path: TestPath(0),
310
- // // Expected: &hello{},
311
- // // },
312
- // // {
313
- // // Path: TestPath(0, 0, 0),
314
- // // Expected: H1(),
315
- // // },
316
- // // {
317
- // // Path: TestPath(0, 0, 0, 0),
318
- // // Expected: Text("hello, "),
319
- // // },
320
- // // },
321
- // // },
322
- // // {
323
- // // scenario: "html element is replaced by another html element",
324
- // // a: Div().Body(
325
- // // H2(),
326
- // // ),
327
- // // b: Div().Body(
328
- // // H1(),
329
- // // ),
330
- // // matches: []TestUIDescriptor{
331
- // // {
332
- // // Path: TestPath(),
333
- // // Expected: Div(),
334
- // // },
335
- // // {
336
- // // Path: TestPath(0),
337
- // // Expected: H1(),
338
- // // },
339
- // // },
340
- // // },
341
- // // {
342
- // // scenario: "html element is replaced by raw html element",
343
- // // a: Div().Body(
344
- // // H2().Text("hello"),
345
- // // ),
346
- // // b: Div().Body(
347
- // // Raw("<svg></svg>"),
348
- // // ),
349
- // // matches: []TestUIDescriptor{
350
- // // {
351
- // // Path: TestPath(),
352
- // // Expected: Div(),
353
- // // },
354
- // // {
355
- // // Path: TestPath(0),
356
- // // Expected: Raw("<svg></svg>"),
357
- // // },
358
- // // },
359
- // // },
360
- // // })
361
- // // }
362
-
363
- // func TestTextMountDismout(t *testing.T) {
364
- // testMountDismount(t, []mountTest{
365
- // {
366
- // scenario: "text",
367
- // node: Text("hello"),
368
- // },
369
- // })
370
- // }
371
-
372
- // func TestTextUpdate(t *testing.T) {
373
- // testUpdate(t, []updateTest{
374
- // {
375
- // scenario: "text element returns replace error when updated with a non text-element",
376
- // a: Text("hello"),
377
- // b: Div(),
378
- // replaceErr: true,
379
- // },
380
- // {
381
- // scenario: "text element is updated",
382
- // a: Text("hello"),
383
- // b: Text("world"),
384
- // matches: []TestUIDescriptor{
385
- // {
386
- // Expected: Text("world"),
387
- // },
388
- // },
389
- // },
390
-
391
- // {
392
- // scenario: "text is replaced by a html elem",
393
- // a: Div().Body(
394
- // Text("hello"),
395
- // ),
396
- // b: Div().Body(
397
- // H2().Text("hello"),
398
- // ),
399
- // matches: []TestUIDescriptor{
400
- // {
401
- // Path: TestPath(),
402
- // Expected: Div(),
403
- // },
404
- // {
405
- // Path: TestPath(0),
406
- // Expected: H2(),
407
- // },
408
- // {
409
- // Path: TestPath(0, 0),
410
- // Expected: Text("hello"),
411
- // },
412
- // },
413
- // },
414
- // {
415
- // scenario: "text is replaced by a component",
416
- // a: Div().Body(
417
- // Text("hello"),
418
- // ),
419
- // // b: Div().Body(
420
- // // &hello{},
421
- // // ),
422
- // matches: []TestUIDescriptor{
423
- // {
424
- // Path: TestPath(),
425
- // Expected: Div(),
426
- // },
427
- // // {
428
- // // Path: TestPath(0),
429
- // // Expected: &hello{},
430
- // // },
431
- // {
432
- // Path: TestPath(0, 0, 0),
433
- // Expected: H1(),
434
- // },
435
- // {
436
- // Path: TestPath(0, 0, 0, 0),
437
- // Expected: Text("hello, "),
438
- // },
439
- // },
440
- // },
441
- // {
442
- // scenario: "text is replaced by a raw html element",
443
- // a: Div().Body(
444
- // Text("hello"),
445
- // ),
446
- // b: Div().Body(
447
- // Raw("<svg></svg>"),
448
- // ),
449
- // matches: []TestUIDescriptor{
450
- // {
451
- // Path: TestPath(),
452
- // Expected: Div(),
453
- // },
454
- // {
455
- // Path: TestPath(0),
456
- // Expected: Raw("<svg></svg>"),
457
- // },
458
- // },
459
- // },
460
- // })
461
- // }
example/assets/icon.png CHANGED
Binary file
example/assets/icon2.png DELETED
Binary file
example/components/header.go CHANGED
@@ -7,7 +7,7 @@ import (
7
7
  func Header() *Element {
8
8
  return Row(Css("w-full mb-20 font-bold text-xl text-gray-700 p-4"),
9
9
  Div(Css("text-blue-700"),
10
- A(Href("https://wapp.pyros2097.dev"), Text("wapp.pyros2097.dev")),
10
+ A(Href("https://pyros.sh"), Text("pyros.sh")),
11
11
  ),
12
12
  Div(Css("flex flex-row flex-1 justify-end items-end p-2"),
13
13
  Div(Css("border-b-2 border-white text-lg text-blue-700 mr-4"), Text("Examples: ")),
example/main.go CHANGED
@@ -2,44 +2,44 @@ package main
2
2
 
3
3
  import (
4
4
  "embed"
5
+ "log"
5
- // "log"
6
+ "net/http"
7
+ "os"
8
+ "time"
6
9
 
7
- // sshd "github.com/jpillora/sshd-lite/server"
10
+ "github.com/apex/gateway/v2"
11
+ "github.com/gorilla/mux"
8
12
  . "github.com/pyros2097/wapp"
9
-
10
13
  "github.com/pyros2097/wapp/example/pages"
11
14
  )
12
15
 
13
16
  //go:embed assets/*
14
17
  var assetsFS embed.FS
15
18
 
19
+ func wrap(f func(http.ResponseWriter, *http.Request) *Element) func(http.ResponseWriter, *http.Request) {
20
+ return func(w http.ResponseWriter, r *http.Request) {
21
+ w.Header().Set("Content-Type", "text/html")
22
+ f(w, r).WriteHtml(w)
23
+ }
24
+ }
25
+
16
26
  func main() {
17
- // os.Setenv("HAS_WASM", "false")
27
+ isLambda := os.Getenv("_LAMBDA_SERVER_PORT") != ""
28
+ r := mux.NewRouter()
18
- // SetErrorHandler(func(w *RenderContext, err error) UI {
29
+ r.PathPrefix("/assets/").Handler(http.FileServer(http.FS(assetsFS)))
19
- // return Col(Css("text-4xl text-gray-700"),
20
- // Header(c),
21
- // Row(
22
- // Text("Oops something went wrong"),
23
- // ),
24
- // Row(Css("mt-6"),
25
- // Text("Please check back again"),
26
- // ),
27
- // )
28
- // })
29
- // go func() {
30
- // s, err := sshd.NewServer(&sshd.Config{
31
- // Port: "2223",
32
- // AuthType: "peter:pass",
30
+ r.HandleFunc("/", wrap(pages.Index))
33
- // })
34
- // if err != nil {
35
- // log.Fatal(err)
36
- // }
37
- // err = s.Start()
38
- // if err != nil {
39
- // log.Fatal(err)
40
- // }
41
- // }()
42
- Route("/about", pages.About)
31
+ r.HandleFunc("/about", wrap(pages.About))
32
+ if !isLambda {
33
+ println("http listening on http://localhost:1234")
34
+ srv := &http.Server{
35
+ Handler: r,
36
+ Addr: "127.0.0.1:1234",
37
+ WriteTimeout: 30 * time.Second,
38
+ ReadTimeout: 30 * time.Second,
39
+ }
40
+ log.Fatal(srv.ListenAndServe())
41
+ } else {
43
- Route("/", pages.Index)
42
+ log.Print("running in lambda mode")
44
- Run(assetsFS)
43
+ log.Fatal(gateway.ListenAndServe(":3000", r))
44
+ }
45
45
  }
example/makefile CHANGED
@@ -1,25 +1,21 @@
1
1
  run:
2
2
  go run *.go
3
3
 
4
- wasm: export GOOS=js
4
+ local:
5
- wasm: export GOARCH=wasm
5
+ sam local start-api
6
- wasm:
7
- go build -ldflags='-s -w' -o assets/main.wasm
8
6
 
9
7
  css:
10
- npx tailwindcss-cli@latest build assets/config.css -o assets/styles.css
8
+ npx tailwindcss-cli@latest build -i assets/config.css -o assets/styles.css
9
+
10
+ build: export GOOS=linux
11
+ build: export GOARCH=amd64
12
+ build:
13
+ go build -o main
11
14
 
12
15
  deploy: export NODE_ENV=production
13
16
  deploy:
14
- npx tailwindcss-cli@latest build assets/config.css -o assets/styles.css
17
+ make css
15
- go build -o main
18
+ make build
16
19
  sam deploy
17
- make wasm
18
- aws s3 sync ./assets s3://wapp.pyros2097.dev/assets --delete --exclude main.wasm
20
+ aws s3 sync ./assets s3://wapp-example/assets --delete
19
- brotli -Z -j assets/main.wasm
20
- mv assets/main.wasm.br assets/main.wasm
21
- aws s3 cp assets/main.wasm s3://wapp.pyros2097.dev/assets/main.wasm --content-encoding br --content-type application/wasm
22
- aws cloudfront create-invalidation --distribution-id E17XVZYYZ1JXEU --paths "/*"
21
+ aws cloudfront create-invalidation --distribution-id E3C93YR5O58WG6 --paths "/*"
23
-
24
- local:
25
- sam local start-api
example/readme.md DELETED
@@ -1,11 +0,0 @@
1
- # wapp-example
2
-
3
- An example to demostrate the wapp framework
4
-
5
- 1. Compile wasm frontend,
6
- `GOOS=js GOARCH=wasm go build -o assets/main.wasm`
7
-
8
- 2. Run the server,
9
- `go run *.go`
10
-
11
- These commands are also available in the makefile for convenience
example/samconfig.toml DELETED
@@ -1,10 +0,0 @@
1
- version = 0.1
2
- [default]
3
- [default.deploy]
4
- [default.deploy.parameters]
5
- stack_name = "wapp-pyros2097-dev"
6
- s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-grb3hle00qk9"
7
- s3_prefix = "wapp-pyros2097-dev"
8
- region = "us-east-1"
9
- capabilities = "CAPABILITY_IAM"
10
- parameter_overrides = "DomainName=\"wapp.pyros2097.dev\" HostedZoneId=\"Z0469276792OXT6RNZF5\" AcmCertificateArn=\"arn:aws:acm:us-east-1:719747015987:certificate/16acd03e-b59a-459c-8a3f-5caa50df7079\""
example/template.yml CHANGED
@@ -1,36 +1,26 @@
1
1
  ---
2
- AWSTemplateFormatVersion: "2010-09-09"
2
+ AWSTemplateFormatVersion: '2010-09-09'
3
3
  Transform: AWS::Serverless-2016-10-31
4
- Parameters:
5
- DomainName:
6
- Type: String
7
- Default: wapp.pyros2097.dev
8
- HostedZoneId:
9
- Type: String
10
- Default: Z0469276792OXT6RNZF5
11
- AcmCertificateArn:
12
- Type: String
13
- Default: arn:aws:acm:us-east-1:719747015987:certificate/16acd03e-b59a-459c-8a3f-5caa50df7079
14
4
 
15
5
  Outputs:
16
6
  DistributionId:
17
7
  Value: !Ref CloudFrontDistribution
18
- DistributionDomain:
8
+ AppUrl:
19
9
  Value: !GetAtt CloudFrontDistribution.DomainName
20
10
  ApiUrl:
21
- Value: !Sub https://${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/Prod/
11
+ Value: !Sub https://${ServerlessHttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/
22
12
 
23
13
  Resources:
24
14
  WebBucket:
25
15
  Type: AWS::S3::Bucket
26
16
  Properties:
27
- BucketName: !Ref DomainName
17
+ BucketName: wapp-example
28
18
 
29
19
  CloudFrontOriginAccessIdentity:
30
20
  Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
31
21
  Properties:
32
22
  CloudFrontOriginAccessIdentityConfig:
33
- Comment: "CloudFront OAI"
23
+ Comment: 'CloudFront OAI'
34
24
 
35
25
  WebBucketPolicy:
36
26
  Type: AWS::S3::BucketPolicy
@@ -42,18 +32,13 @@ Resources:
42
32
  Principal:
43
33
  CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
44
34
  Action: s3:GetObject
45
- Resource: !Join ["/", [!GetAtt WebBucket.Arn, "*"]]
35
+ Resource: !Join ['/', [!GetAtt WebBucket.Arn, '*']]
46
36
  - Effect: Allow
47
37
  Principal:
48
38
  CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
49
39
  Action: s3:ListBucket
50
40
  Resource: !GetAtt WebBucket.Arn
51
41
 
52
- HttpApi:
53
- Type: AWS::Serverless::HttpApi
54
- Properties:
55
- StageName: Prod
56
-
57
42
  HttpApiLambda:
58
43
  Type: AWS::Serverless::Function
59
44
  Properties:
@@ -65,8 +50,6 @@ Resources:
65
50
  ApiEvent:
66
51
  Type: HttpApi
67
52
  Properties:
68
- ApiId:
69
- Ref: HttpApi
70
53
  Method: ANY
71
54
  Path: '$default'
72
55
 
@@ -74,8 +57,6 @@ Resources:
74
57
  Type: AWS::CloudFront::Distribution
75
58
  Properties:
76
59
  DistributionConfig:
77
- Aliases:
78
- - !Ref DomainName
79
60
  HttpVersion: http2
80
61
  Enabled: true
81
62
  IPV6Enabled: true
@@ -84,20 +65,18 @@ Resources:
84
65
  - Id: s3
85
66
  DomainName: !GetAtt WebBucket.DomainName
86
67
  S3OriginConfig:
87
- OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}"
68
+ OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}'
88
69
  - Id: app
89
- DomainName: !Sub ${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}
70
+ DomainName: !Sub ${ServerlessHttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}
90
- OriginPath: /Prod
91
71
  CustomOriginConfig:
92
72
  OriginProtocolPolicy: https-only
93
73
  DefaultCacheBehavior:
94
74
  TargetOriginId: app
95
- AllowedMethods: [HEAD, GET]
75
+ AllowedMethods: [DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT]
96
- CachedMethods: [HEAD, GET]
97
76
  ViewerProtocolPolicy: https-only
98
77
  Compress: true
99
78
  ForwardedValues:
100
- QueryString: true
79
+ QueryString: true
101
80
  CacheBehaviors:
102
81
  - TargetOriginId: s3
103
82
  PathPattern: /assets/*
@@ -106,30 +85,4 @@ Resources:
106
85
  ViewerProtocolPolicy: https-only
107
86
  Compress: true
108
87
  ForwardedValues:
109
- QueryString: false
88
+ QueryString: false
110
- ViewerCertificate:
111
- AcmCertificateArn: !Ref AcmCertificateArn
112
- MinimumProtocolVersion: TLSv1.2_2018
113
- SslSupportMethod: sni-only
114
-
115
- DNSRecordSet:
116
- Type: AWS::Route53::RecordSet
117
- Properties:
118
- AliasTarget:
119
- DNSName: !GetAtt CloudFrontDistribution.DomainName
120
- EvaluateTargetHealth: false
121
- HostedZoneId: Z2FDTNDATAQYW2 # CloudFront hosted zone ID
122
- HostedZoneId: !Ref HostedZoneId
123
- Name: !Ref DomainName
124
- Type: A
125
-
126
- DNSIPV6RecordSet:
127
- Type: AWS::Route53::RecordSet
128
- Properties:
129
- AliasTarget:
130
- DNSName: !GetAtt CloudFrontDistribution.DomainName
131
- EvaluateTargetHealth: false
132
- HostedZoneId: Z2FDTNDATAQYW2 # CloudFront hosted zone ID
133
- HostedZoneId: !Ref HostedZoneId
134
- Name: !Ref DomainName
135
- Type: AAAA
go.mod CHANGED
@@ -3,7 +3,8 @@ module github.com/pyros2097/wapp
3
3
  go 1.16
4
4
 
5
5
  require (
6
- github.com/davecgh/go-spew v1.1.1 // indirect
6
+ github.com/apex/gateway/v2 v2.0.0
7
+ github.com/gorilla/mux v1.8.0
7
8
  github.com/kr/pretty v0.1.0 // indirect
8
9
  github.com/stretchr/testify v1.6.1
9
10
  gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
go.sum CHANGED
@@ -1,19 +1,37 @@
1
+ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2
+ github.com/apex/gateway/v2 v2.0.0 h1:tJwKiB7ObbXuF3yoqTf/CfmaZRhHB+GfilTNSCf1Wnc=
3
+ github.com/apex/gateway/v2 v2.0.0/go.mod h1:y+uuK0JxdvTHZeVns501/7qklBhnDHtGU0hfUQ6QIfI=
4
+ github.com/aws/aws-lambda-go v1.17.0 h1:Ogihmi8BnpmCNktKAGpNwSiILNNING1MiosnKUfU8m0=
5
+ github.com/aws/aws-lambda-go v1.17.0/go.mod h1:FEwgPLE6+8wcGBTe5cJN3JWurd1Ztm9zN4jsXsjzKKw=
6
+ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
1
7
  github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2
8
  github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3
9
  github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10
+ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
11
+ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
4
12
  github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
5
13
  github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
6
14
  github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
7
15
  github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
8
16
  github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
17
+ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
18
+ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
9
19
  github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
10
20
  github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
21
+ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
22
+ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
11
23
  github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
24
+ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
12
25
  github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
13
26
  github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
27
+ github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
28
+ github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
29
+ github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
14
30
  gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
15
31
  gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
16
32
  gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
33
+ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
17
34
  gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
35
+ gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
18
36
  gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
19
37
  gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
html.go CHANGED
@@ -1,6 +1,109 @@
1
1
  package app
2
2
 
3
+ import (
4
+ "io"
3
- import "reflect"
5
+ "reflect"
6
+ "strconv"
7
+ )
8
+
9
+ type Element struct {
10
+ tag string
11
+ attrs map[string]string
12
+ body []*Element
13
+ selfClosing bool
14
+ text string
15
+ }
16
+
17
+ func NewElement(tag string, selfClosing bool, uis ...interface{}) *Element {
18
+ return MergeAttributes(&Element{tag: tag, selfClosing: selfClosing}, uis...)
19
+ }
20
+
21
+ func (e *Element) setAttr(k string, v string) {
22
+ if e.attrs == nil {
23
+ e.attrs = make(map[string]string)
24
+ }
25
+
26
+ switch k {
27
+ case "style", "allow":
28
+ s := e.attrs[k] + v + ";"
29
+ e.attrs[k] = s
30
+ return
31
+
32
+ case "class":
33
+ s := e.attrs[k]
34
+ if s != "" {
35
+ s += " "
36
+ }
37
+ s += v
38
+ e.attrs[k] = s
39
+ return
40
+ }
41
+ if v == "false" {
42
+ delete(e.attrs, k)
43
+ return
44
+ } else if v == "true" {
45
+ e.attrs[k] = ""
46
+ } else {
47
+ e.attrs[k] = v
48
+ }
49
+ }
50
+
51
+ func (e *Element) WriteHtml(w io.Writer) {
52
+ e.writeHtmlIndent(w, 0)
53
+ }
54
+
55
+ func (e *Element) writeHtmlIndent(w io.Writer, indent int) {
56
+ e.writeIndent(w, indent)
57
+ if e.tag == "html" {
58
+ w.Write([]byte("<!DOCTYPE html>\n"))
59
+ }
60
+ if e.tag == "text" {
61
+ e.writeIndent(w, indent)
62
+ w.Write([]byte(e.text))
63
+ return
64
+ }
65
+ w.Write([]byte("<"))
66
+ w.Write([]byte(e.tag))
67
+
68
+ for k, v := range e.attrs {
69
+ w.Write([]byte(" "))
70
+ w.Write([]byte(k))
71
+
72
+ if v != "" {
73
+ w.Write([]byte(`="`))
74
+ w.Write([]byte(v))
75
+ w.Write([]byte(`"`))
76
+ }
77
+ }
78
+
79
+ w.Write([]byte(">"))
80
+
81
+ if e.selfClosing {
82
+ return
83
+ }
84
+
85
+ for _, c := range e.body {
86
+ w.Write([]byte("\n"))
87
+ if c != nil {
88
+ c.writeHtmlIndent(w, indent+1)
89
+ }
90
+ }
91
+
92
+ if len(e.body) != 0 {
93
+ w.Write([]byte("\n"))
94
+ e.writeIndent(w, indent)
95
+ }
96
+
97
+ w.Write([]byte("</"))
98
+ w.Write([]byte(e.tag))
99
+ w.Write([]byte(">"))
100
+ }
101
+
102
+ func (e *Element) writeIndent(w io.Writer, indent int) {
103
+ for i := 0; i < indent*4; i++ {
104
+ w.Write([]byte(" "))
105
+ }
106
+ }
4
107
 
5
108
  func Html(elems ...*Element) *Element {
6
109
  return &Element{tag: "html", body: elems}
@@ -74,19 +177,19 @@ func H1(uis ...interface{}) *Element {
74
177
  return NewElement("h1", false, uis...)
75
178
  }
76
179
  func H2(uis ...interface{}) *Element {
77
- return NewElement("h1", false, uis...)
180
+ return NewElement("h2", false, uis...)
78
181
  }
79
182
  func H3(uis ...interface{}) *Element {
80
- return NewElement("h1", false, uis...)
183
+ return NewElement("h3", false, uis...)
81
184
  }
82
185
  func H4(uis ...interface{}) *Element {
83
- return NewElement("h1", false, uis...)
186
+ return NewElement("h4", false, uis...)
84
187
  }
85
188
  func H5(uis ...interface{}) *Element {
86
- return NewElement("h1", false, uis...)
189
+ return NewElement("h5", false, uis...)
87
190
  }
88
191
  func H6(uis ...interface{}) *Element {
89
- return NewElement("h1", false, uis...)
192
+ return NewElement("h6", false, uis...)
90
193
  }
91
194
 
92
195
  func Span(uis ...interface{}) *Element {
@@ -166,3 +269,113 @@ func Map2(source interface{}, f func(v interface{}, i int) *Element) []*Element
166
269
  }
167
270
  return body
168
271
  }
272
+
273
+ type Attribute struct {
274
+ Key string
275
+ Value string
276
+ }
277
+
278
+ func ID(v string) Attribute {
279
+ return Attribute{"id", v}
280
+ }
281
+
282
+ func Style(v string) Attribute {
283
+ return Attribute{"style", v}
284
+ }
285
+
286
+ func Accept(v string) Attribute {
287
+ return Attribute{"accept", v}
288
+ }
289
+
290
+ func AutoComplete(v bool) Attribute {
291
+ return Attribute{"autocomplete", strconv.FormatBool(v)}
292
+ }
293
+
294
+ func Checked(v bool) Attribute {
295
+ return Attribute{"checked", strconv.FormatBool(v)}
296
+ }
297
+
298
+ func Disabled(v bool) Attribute {
299
+ return Attribute{"disabled", strconv.FormatBool(v)}
300
+ }
301
+
302
+ func Name(v string) Attribute {
303
+ return Attribute{"name", v}
304
+ }
305
+
306
+ func Type(v string) Attribute {
307
+ return Attribute{"type", v}
308
+ }
309
+
310
+ func Value(v string) Attribute {
311
+ return Attribute{"value", v}
312
+ }
313
+
314
+ func Placeholder(v string) Attribute {
315
+ return Attribute{"placeholder", v}
316
+ }
317
+
318
+ func Src(v string) Attribute {
319
+ return Attribute{"src", v}
320
+ }
321
+
322
+ func Defer() Attribute {
323
+ return Attribute{"defer", "true"}
324
+ }
325
+
326
+ func ViewBox(v string) Attribute {
327
+ return Attribute{"viewBox", v}
328
+ }
329
+
330
+ func X(v string) Attribute {
331
+ return Attribute{"x", v}
332
+ }
333
+
334
+ func Y(v string) Attribute {
335
+ return Attribute{"y", v}
336
+ }
337
+
338
+ func Href(v string) Attribute {
339
+ return Attribute{"href", v}
340
+ }
341
+
342
+ func Target(v string) Attribute {
343
+ return Attribute{"target", v}
344
+ }
345
+
346
+ func Rel(v string) Attribute {
347
+ return Attribute{"rel", v}
348
+ }
349
+
350
+ func Css(v string) Attribute {
351
+ return Attribute{"class", v}
352
+ }
353
+
354
+ func XData(v string) Attribute {
355
+ return Attribute{"x-data", v}
356
+ }
357
+
358
+ func XText(v string) Attribute {
359
+ return Attribute{"x-text", v}
360
+ }
361
+
362
+ func MergeAttributes(parent *Element, uis ...interface{}) *Element {
363
+ elems := []*Element{}
364
+ for _, v := range uis {
365
+ switch c := v.(type) {
366
+ case Attribute:
367
+ parent.setAttr(c.Key, c.Value)
368
+ case *Element:
369
+ elems = append(elems, c)
370
+ case nil:
371
+ // dont need to add nil items
372
+ default:
373
+ // fmt.Printf("%v\n", v)
374
+ panic("unknown type in render")
375
+ }
376
+ }
377
+ if !parent.selfClosing {
378
+ parent.body = elems
379
+ }
380
+ return parent
381
+ }
readme.md CHANGED
@@ -6,44 +6,23 @@
6
6
 
7
7
  # wapp
8
8
 
9
- **wapp** is a framework to build isomorphic web apps in golang.
10
-
11
- It uses a declarative syntax using funcs that allows creating and dealing with HTML elements only by using Go, and without writing any HTML markup. The syntax is inspired by react and its awesome hooks and functional component features. It is highly opioninated and integrates very well with tailwind css for now.
12
-
13
- This originally started out as of fork of this awesome go-app PWA framework. All credits goes to Maxence Charriere for majority of the work.
14
-
15
- Inspired by:
16
- * [go-app](https://github.com/maxence-charriere/go-app)
17
- * [react](https://reactjs.org/docs/components-and-props.html)
18
- * [reacct-hooks](https://reactjs.org/docs/hooks-intro.html)
19
- * [jotai](https://github.com/pmndrs/jotai)
20
- * [klyva](https://github.com/merisbahti/klyva)
21
-
9
+ **wapp** is a framework to build web apps in golang.
10
+ It uses a declarative syntax using funcs that allows creating and dealing with HTML elements only by using Go, and without writing any HTML markup.ZZZIt is highly opioninated and integrates uses tailwind css and alpinejs.
22
11
 
23
12
  # Install
24
13
 
25
14
  **wapp** requirements:
26
15
 
27
- - [Go 1.15](https://golang.org/doc/go1.15)
16
+ - [Go 1.16](https://golang.org/doc/go1.16)
28
17
 
29
18
  ```sh
30
19
  go mod init
31
20
  go get -u -v github.com/pyros2097/wapp
32
21
  ```
33
22
 
34
- # Demos
35
-
36
- [Demo 1](https://wapp.pyros2097.dev/)
37
-
38
- [Demo 2](https://timer.pyros2097.dev/)
39
-
40
- # Examples
41
-
42
- [Example 1](https://github.com/pyros2097/wapp/tree/master/example)
43
-
44
- [Example 2](https://github.com/pyros2097/wapp-timer)
23
+ [Demo](https://github.com/pyros2097/wapp-example)
45
24
 
46
- **Counter**
25
+ **Example**
47
26
 
48
27
  ```go
49
28
  package main
@@ -52,92 +31,48 @@ import (
52
31
  . "github.com/pyros2097/wapp"
53
32
  )
54
33
 
55
- func Counter(c *RenderContext) UI {
34
+ func Header() *Element {
56
- count, setCount := c.UseInt(0)
57
- inc := func() { setCount(count() + 1) }
58
- dec := func() { setCount(count() - 1) }
35
+ return Row(Css("w-full mb-20 font-bold text-xl text-gray-700 p-4"),
59
- return Col(
60
- Row(
61
- Row(Css("text-6xl"),
36
+ Div(Css("text-blue-700"),
62
- Text("Counter"),
37
+ A(Href("https://wapp.pyros2097.dev"), Text("wapp.pyros2097.dev")),
63
- ),
64
38
  ),
65
- Row(
39
+ Div(Css("flex flex-row flex-1 justify-end items-end p-2"),
40
+ Div(Css("border-b-2 border-white text-lg text-blue-700 mr-4"), Text("Examples: ")),
41
+ Div(Css("link mr-4"), A(Href("/"), Text("Home"))),
66
- Row(Css("text-6xl m-20 cursor-pointer select-none"), OnClick(dec),
42
+ Div(Css("link mr-4"), A(Href("/clock"), Text("Clock"))),
67
- Text("-"),
68
- ),
69
- Row(Css("text-6xl m-20"),
43
+ Div(Css("link mr-4"), A(Href("/about"), Text("About"))),
70
- Text(count()),
44
+ Div(Css("link mr-4"), A(Href("/container"), Text("Container"))),
71
- ),
72
- Row(Css("text-6xl m-20 cursor-pointer select-none"), OnClick(inc),
45
+ Div(Css("link mr-4"), A(Href("/panic"), Text("Panic"))),
73
- Text("+"),
74
- ),
75
46
  ),
76
47
  )
77
48
  }
78
49
 
79
- func main() {
80
- Route("/", Counter)
81
- Run()
82
- }
83
- ```
84
-
85
- **Clock**
86
-
87
- ```go
88
- package main
89
-
90
- import (
91
- "time"
92
-
93
- . "github.com/pyros2097/wapp"
94
- )
95
-
96
- func Route(c *RenderContext, title string) UI {
50
+ func Index(w http.ResponseWriter, r *http.Request) *Element {
97
- timeValue, setTime := c.UseState(time.Now())
98
- running, setRunning := c.UseState(false)
99
- startTimer := func() {
100
- setRunning(true)
101
- go func() {
102
- for running().(bool) {
103
- setTime(time.Now())
104
- time.Sleep(time.Second * 1)
105
- }
106
- }()
107
- }
108
- stopTimer := func() {
109
- setRunning(false)
110
- }
111
- c.UseEffect(func() func() {
112
- startTimer()
113
- return stopTimer
114
- })
115
-
116
- return Col(
51
+ return Page(
52
+ Col(
53
+ Header(),
54
+ H1(Text("Hello this is a h1")),
55
+ H2(Text("Hello this is a h2")),
56
+ H2(XData("{ message: 'I ❤️ Alpine' }"), XText("message"), Text("")),
57
+ Col(Css("text-3xl text-gray-700"),
117
- Row(
58
+ Row(
118
- Div(Css("text-6xl"),
59
+ Row(Css("underline"),
119
- Text(title),
60
+ Text("Counter"),
120
- ),
61
+ ),
121
- ),
62
+ ),
122
- Row(
63
+ Row(
123
- Div(Css("mt-10"),
64
+ Button(Css("btn m-20"),
124
- Text(timeValue().(time.Time).Format("15:04:05")),
65
+ Text("-"),
125
- ),
66
+ ),
67
+ Row(Css("m-20"),
68
+ Text(strconv.Itoa(1)),
126
- ),
69
+ ),
127
- Row(
128
- Div(Css("text-6xl m-20 cursor-pointer select-none"), OnClick(startTimer),
70
+ Button(Css("btn m-20"),
129
- Text("Start"),
71
+ Text("+"),
130
- ),
72
+ ),
131
- Div(Css("text-6xl m-20 cursor-pointer select-none"), OnClick(stopTimer),
132
- Text("Stop"),
73
+ ),
133
74
  ),
134
75
  ),
135
76
  )
136
77
  }
137
-
138
- func main() {
139
- Route("/", Clock)
140
- Run()
141
- }
142
-
143
78
  ```
router.go DELETED
@@ -1,968 +0,0 @@
1
- package app
2
-
3
- import (
4
- "context"
5
- "embed"
6
- "errors"
7
- "net/http"
8
- "os"
9
- "strings"
10
- "unicode"
11
- "unicode/utf8"
12
- // "github.com/aws/aws-lambda-go/events"
13
- )
14
-
15
- func min(a, b int) int {
16
- if a <= b {
17
- return a
18
- }
19
- return b
20
- }
21
-
22
- func longestCommonPrefix(a, b string) int {
23
- i := 0
24
- max := min(len(a), len(b))
25
- for i < max && a[i] == b[i] {
26
- i++
27
- }
28
- return i
29
- }
30
-
31
- // Search for a wildcard segment and check the name for invalid characters.
32
- // Returns -1 as index, if no wildcard was found.
33
- func findWildcard(path string) (wilcard string, i int, valid bool) {
34
- // Find start
35
- for start, c := range []byte(path) {
36
- // A wildcard starts with ':' (param) or '*' (catch-all)
37
- if c != ':' && c != '*' {
38
- continue
39
- }
40
-
41
- // Find end and check for invalid characters
42
- valid = true
43
- for end, c := range []byte(path[start+1:]) {
44
- switch c {
45
- case '/':
46
- return path[start : start+1+end], start, valid
47
- case ':', '*':
48
- valid = false
49
- }
50
- }
51
- return path[start:], start, valid
52
- }
53
- return "", -1, false
54
- }
55
-
56
- func countParams(path string) uint16 {
57
- var n uint
58
- for i := range []byte(path) {
59
- switch path[i] {
60
- case ':', '*':
61
- n++
62
- }
63
- }
64
- return uint16(n)
65
- }
66
-
67
- type nodeType uint8
68
-
69
- const (
70
- static nodeType = iota // default
71
- root
72
- param
73
- catchAll
74
- )
75
-
76
- type node struct {
77
- path string
78
- indices string
79
- wildChild bool
80
- nType nodeType
81
- priority uint32
82
- children []*node
83
- handle RouteFn
84
- }
85
-
86
- // Increments priority of the given child and reorders if necessary
87
- func (n *node) incrementChildPrio(pos int) int {
88
- cs := n.children
89
- cs[pos].priority++
90
- prio := cs[pos].priority
91
-
92
- // Adjust position (move to front)
93
- newPos := pos
94
- for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
95
- // Swap node positions
96
- cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
97
- }
98
-
99
- // Build new index char string
100
- if newPos != pos {
101
- n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
102
- n.indices[pos:pos+1] + // The index char we move
103
- n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
104
- }
105
-
106
- return newPos
107
- }
108
-
109
- // addRoute adds a node with the given handle to the path.
110
- // Not concurrency-safe!
111
- func (n *node) addRoute(path string, handle RouteFn) {
112
- fullPath := path
113
- n.priority++
114
-
115
- // Empty tree
116
- if n.path == "" && n.indices == "" {
117
- n.insertChild(path, fullPath, handle)
118
- n.nType = root
119
- return
120
- }
121
-
122
- walk:
123
- for {
124
- // Find the longest common prefix.
125
- // This also implies that the common prefix contains no ':' or '*'
126
- // since the existing key can't contain those chars.
127
- i := longestCommonPrefix(path, n.path)
128
-
129
- // Split edge
130
- if i < len(n.path) {
131
- child := node{
132
- path: n.path[i:],
133
- wildChild: n.wildChild,
134
- nType: static,
135
- indices: n.indices,
136
- children: n.children,
137
- handle: n.handle,
138
- priority: n.priority - 1,
139
- }
140
-
141
- n.children = []*node{&child}
142
- // []byte for proper unicode char conversion, see #65
143
- n.indices = string([]byte{n.path[i]})
144
- n.path = path[:i]
145
- n.handle = nil
146
- n.wildChild = false
147
- }
148
-
149
- // Make new node a child of this node
150
- if i < len(path) {
151
- path = path[i:]
152
-
153
- if n.wildChild {
154
- n = n.children[0]
155
- n.priority++
156
-
157
- // Check if the wildcard matches
158
- if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
159
- // Adding a child to a catchAll is not possible
160
- n.nType != catchAll &&
161
- // Check for longer wildcard, e.g. :name and :names
162
- (len(n.path) >= len(path) || path[len(n.path)] == '/') {
163
- continue walk
164
- } else {
165
- // Wildcard conflict
166
- pathSeg := path
167
- if n.nType != catchAll {
168
- pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
169
- }
170
- prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
171
- panic("'" + pathSeg +
172
- "' in new path '" + fullPath +
173
- "' conflicts with existing wildcard '" + n.path +
174
- "' in existing prefix '" + prefix +
175
- "'")
176
- }
177
- }
178
-
179
- idxc := path[0]
180
-
181
- // '/' after param
182
- if n.nType == param && idxc == '/' && len(n.children) == 1 {
183
- n = n.children[0]
184
- n.priority++
185
- continue walk
186
- }
187
-
188
- // Check if a child with the next path byte exists
189
- for i, c := range []byte(n.indices) {
190
- if c == idxc {
191
- i = n.incrementChildPrio(i)
192
- n = n.children[i]
193
- continue walk
194
- }
195
- }
196
-
197
- // Otherwise insert it
198
- if idxc != ':' && idxc != '*' {
199
- // []byte for proper unicode char conversion, see #65
200
- n.indices += string([]byte{idxc})
201
- child := &node{}
202
- n.children = append(n.children, child)
203
- n.incrementChildPrio(len(n.indices) - 1)
204
- n = child
205
- }
206
- n.insertChild(path, fullPath, handle)
207
- return
208
- }
209
-
210
- // Otherwise add handle to current node
211
- if n.handle != nil {
212
- panic("a handle is already registered for path '" + fullPath + "'")
213
- }
214
- n.handle = handle
215
- return
216
- }
217
- }
218
-
219
- func (n *node) insertChild(path, fullPath string, handle RouteFn) {
220
- for {
221
- // Find prefix until first wildcard
222
- wildcard, i, valid := findWildcard(path)
223
- if i < 0 { // No wilcard found
224
- break
225
- }
226
-
227
- // The wildcard name must not contain ':' and '*'
228
- if !valid {
229
- panic("only one wildcard per path segment is allowed, has: '" +
230
- wildcard + "' in path '" + fullPath + "'")
231
- }
232
-
233
- // Check if the wildcard has a name
234
- if len(wildcard) < 2 {
235
- panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
236
- }
237
-
238
- // Check if this node has existing children which would be
239
- // unreachable if we insert the wildcard here
240
- if len(n.children) > 0 {
241
- panic("wildcard segment '" + wildcard +
242
- "' conflicts with existing children in path '" + fullPath + "'")
243
- }
244
-
245
- // param
246
- if wildcard[0] == ':' {
247
- if i > 0 {
248
- // Insert prefix before the current wildcard
249
- n.path = path[:i]
250
- path = path[i:]
251
- }
252
-
253
- n.wildChild = true
254
- child := &node{
255
- nType: param,
256
- path: wildcard,
257
- }
258
- n.children = []*node{child}
259
- n = child
260
- n.priority++
261
-
262
- // If the path doesn't end with the wildcard, then there
263
- // will be another non-wildcard subpath starting with '/'
264
- if len(wildcard) < len(path) {
265
- path = path[len(wildcard):]
266
- child := &node{
267
- priority: 1,
268
- }
269
- n.children = []*node{child}
270
- n = child
271
- continue
272
- }
273
-
274
- // Otherwise we're done. Insert the handle in the new leaf
275
- n.handle = handle
276
- return
277
- }
278
-
279
- // catchAll
280
- if i+len(wildcard) != len(path) {
281
- panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
282
- }
283
-
284
- if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
285
- panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
286
- }
287
-
288
- // Currently fixed width 1 for '/'
289
- i--
290
- if path[i] != '/' {
291
- panic("no / before catch-all in path '" + fullPath + "'")
292
- }
293
-
294
- n.path = path[:i]
295
-
296
- // First node: catchAll node with empty path
297
- child := &node{
298
- wildChild: true,
299
- nType: catchAll,
300
- }
301
- n.children = []*node{child}
302
- n.indices = string('/')
303
- n = child
304
- n.priority++
305
-
306
- // Second node: node holding the variable
307
- child = &node{
308
- path: path[i:],
309
- nType: catchAll,
310
- handle: handle,
311
- priority: 1,
312
- }
313
- n.children = []*node{child}
314
-
315
- return
316
- }
317
-
318
- // If no wildcard was found, simply insert the path and handle
319
- n.path = path
320
- n.handle = handle
321
- }
322
-
323
- // Returns the handle registered with the given path (key). The values of
324
- // wildcards are saved to a map.
325
- // If no handle can be found, a TSR (trailing slash redirect) recommendation is
326
- // made if a handle exists with an extra (without the) trailing slash for the
327
- // given path.
328
- func (n *node) getValue(path string, params func() *Params) (handle RouteFn, ps *Params, tsr bool) {
329
- walk: // Outer loop for walking the tree
330
- for {
331
- prefix := n.path
332
- if len(path) > len(prefix) {
333
- if path[:len(prefix)] == prefix {
334
- path = path[len(prefix):]
335
-
336
- // If this node does not have a wildcard (param or catchAll)
337
- // child, we can just look up the next child node and continue
338
- // to walk down the tree
339
- if !n.wildChild {
340
- idxc := path[0]
341
- for i, c := range []byte(n.indices) {
342
- if c == idxc {
343
- n = n.children[i]
344
- continue walk
345
- }
346
- }
347
-
348
- // Nothing found.
349
- // We can recommend to redirect to the same URL without a
350
- // trailing slash if a leaf exists for that path.
351
- tsr = (path == "/" && n.handle != nil)
352
- return
353
- }
354
-
355
- // Handle wildcard child
356
- n = n.children[0]
357
- switch n.nType {
358
- case param:
359
- // Find param end (either '/' or path end)
360
- end := 0
361
- for end < len(path) && path[end] != '/' {
362
- end++
363
- }
364
-
365
- // Save param value
366
- if params != nil {
367
- if ps == nil {
368
- ps = params()
369
- }
370
- // Expand slice within preallocated capacity
371
- i := len(*ps)
372
- *ps = (*ps)[:i+1]
373
- (*ps)[i] = Param{
374
- Key: n.path[1:],
375
- Value: path[:end],
376
- }
377
- }
378
-
379
- // We need to go deeper!
380
- if end < len(path) {
381
- if len(n.children) > 0 {
382
- path = path[end:]
383
- n = n.children[0]
384
- continue walk
385
- }
386
-
387
- // ... but we can't
388
- tsr = (len(path) == end+1)
389
- return
390
- }
391
-
392
- if handle = n.handle; handle != nil {
393
- return
394
- } else if len(n.children) == 1 {
395
- // No handle found. Check if a handle for this path + a
396
- // trailing slash exists for TSR recommendation
397
- n = n.children[0]
398
- tsr = (n.path == "/" && n.handle != nil) || (n.path == "" && n.indices == "/")
399
- }
400
-
401
- return
402
-
403
- case catchAll:
404
- // Save param value
405
- if params != nil {
406
- if ps == nil {
407
- ps = params()
408
- }
409
- // Expand slice within preallocated capacity
410
- i := len(*ps)
411
- *ps = (*ps)[:i+1]
412
- (*ps)[i] = Param{
413
- Key: n.path[2:],
414
- Value: path,
415
- }
416
- }
417
-
418
- handle = n.handle
419
- return
420
-
421
- default:
422
- panic("invalid node type")
423
- }
424
- }
425
- } else if path == prefix {
426
- // We should have reached the node containing the handle.
427
- // Check if this node has a handle registered.
428
- if handle = n.handle; handle != nil {
429
- return
430
- }
431
-
432
- // If there is no handle for this route, but this route has a
433
- // wildcard child, there must be a handle for this path with an
434
- // additional trailing slash
435
- if path == "/" && n.wildChild && n.nType != root {
436
- tsr = true
437
- return
438
- }
439
-
440
- // No handle found. Check if a handle for this path + a
441
- // trailing slash exists for trailing slash recommendation
442
- for i, c := range []byte(n.indices) {
443
- if c == '/' {
444
- n = n.children[i]
445
- tsr = (len(n.path) == 1 && n.handle != nil) ||
446
- (n.nType == catchAll && n.children[0].handle != nil)
447
- return
448
- }
449
- }
450
- return
451
- }
452
-
453
- // Nothing found. We can recommend to redirect to the same URL with an
454
- // extra trailing slash if a leaf exists for that path
455
- tsr = (path == "/") ||
456
- (len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
457
- path == prefix[:len(prefix)-1] && n.handle != nil)
458
- return
459
- }
460
- }
461
-
462
- // Makes a case-insensitive lookup of the given path and tries to find a handler.
463
- // It can optionally also fix trailing slashes.
464
- // It returns the case-corrected path and a bool indicating whether the lookup
465
- // was successful.
466
- func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (fixedPath string, found bool) {
467
- const stackBufSize = 128
468
-
469
- // Use a static sized buffer on the stack in the common case.
470
- // If the path is too long, allocate a buffer on the heap instead.
471
- buf := make([]byte, 0, stackBufSize)
472
- if l := len(path) + 1; l > stackBufSize {
473
- buf = make([]byte, 0, l)
474
- }
475
-
476
- ciPath := n.findCaseInsensitivePathRec(
477
- path,
478
- buf, // Preallocate enough memory for new path
479
- [4]byte{}, // Empty rune buffer
480
- fixTrailingSlash,
481
- )
482
-
483
- return string(ciPath), ciPath != nil
484
- }
485
-
486
- // Shift bytes in array by n bytes left
487
- func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
488
- switch n {
489
- case 0:
490
- return rb
491
- case 1:
492
- return [4]byte{rb[1], rb[2], rb[3], 0}
493
- case 2:
494
- return [4]byte{rb[2], rb[3]}
495
- case 3:
496
- return [4]byte{rb[3]}
497
- default:
498
- return [4]byte{}
499
- }
500
- }
501
-
502
- // Recursive case-insensitive lookup function used by n.findCaseInsensitivePath
503
- func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte {
504
- npLen := len(n.path)
505
-
506
- walk: // Outer loop for walking the tree
507
- for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) {
508
- // Add common prefix to result
509
- oldPath := path
510
- path = path[npLen:]
511
- ciPath = append(ciPath, n.path...)
512
-
513
- if len(path) > 0 {
514
- // If this node does not have a wildcard (param or catchAll) child,
515
- // we can just look up the next child node and continue to walk down
516
- // the tree
517
- if !n.wildChild {
518
- // Skip rune bytes already processed
519
- rb = shiftNRuneBytes(rb, npLen)
520
-
521
- if rb[0] != 0 {
522
- // Old rune not finished
523
- idxc := rb[0]
524
- for i, c := range []byte(n.indices) {
525
- if c == idxc {
526
- // continue with child node
527
- n = n.children[i]
528
- npLen = len(n.path)
529
- continue walk
530
- }
531
- }
532
- } else {
533
- // Process a new rune
534
- var rv rune
535
-
536
- // Find rune start.
537
- // Runes are up to 4 byte long,
538
- // -4 would definitely be another rune.
539
- var off int
540
- for max := min(npLen, 3); off < max; off++ {
541
- if i := npLen - off; utf8.RuneStart(oldPath[i]) {
542
- // read rune from cached path
543
- rv, _ = utf8.DecodeRuneInString(oldPath[i:])
544
- break
545
- }
546
- }
547
-
548
- // Calculate lowercase bytes of current rune
549
- lo := unicode.ToLower(rv)
550
- utf8.EncodeRune(rb[:], lo)
551
-
552
- // Skip already processed bytes
553
- rb = shiftNRuneBytes(rb, off)
554
-
555
- idxc := rb[0]
556
- for i, c := range []byte(n.indices) {
557
- // Lowercase matches
558
- if c == idxc {
559
- // must use a recursive approach since both the
560
- // uppercase byte and the lowercase byte might exist
561
- // as an index
562
- if out := n.children[i].findCaseInsensitivePathRec(
563
- path, ciPath, rb, fixTrailingSlash,
564
- ); out != nil {
565
- return out
566
- }
567
- break
568
- }
569
- }
570
-
571
- // If we found no match, the same for the uppercase rune,
572
- // if it differs
573
- if up := unicode.ToUpper(rv); up != lo {
574
- utf8.EncodeRune(rb[:], up)
575
- rb = shiftNRuneBytes(rb, off)
576
-
577
- idxc := rb[0]
578
- for i, c := range []byte(n.indices) {
579
- // Uppercase matches
580
- if c == idxc {
581
- // Continue with child node
582
- n = n.children[i]
583
- npLen = len(n.path)
584
- continue walk
585
- }
586
- }
587
- }
588
- }
589
-
590
- // Nothing found. We can recommend to redirect to the same URL
591
- // without a trailing slash if a leaf exists for that path
592
- if fixTrailingSlash && path == "/" && n.handle != nil {
593
- return ciPath
594
- }
595
- return nil
596
- }
597
-
598
- n = n.children[0]
599
- switch n.nType {
600
- case param:
601
- // Find param end (either '/' or path end)
602
- end := 0
603
- for end < len(path) && path[end] != '/' {
604
- end++
605
- }
606
-
607
- // Add param value to case insensitive path
608
- ciPath = append(ciPath, path[:end]...)
609
-
610
- // We need to go deeper!
611
- if end < len(path) {
612
- if len(n.children) > 0 {
613
- // Continue with child node
614
- n = n.children[0]
615
- npLen = len(n.path)
616
- path = path[end:]
617
- continue
618
- }
619
-
620
- // ... but we can't
621
- if fixTrailingSlash && len(path) == end+1 {
622
- return ciPath
623
- }
624
- return nil
625
- }
626
-
627
- if n.handle != nil {
628
- return ciPath
629
- } else if fixTrailingSlash && len(n.children) == 1 {
630
- // No handle found. Check if a handle for this path + a
631
- // trailing slash exists
632
- n = n.children[0]
633
- if n.path == "/" && n.handle != nil {
634
- return append(ciPath, '/')
635
- }
636
- }
637
- return nil
638
-
639
- case catchAll:
640
- return append(ciPath, path...)
641
-
642
- default:
643
- panic("invalid node type")
644
- }
645
- } else {
646
- // We should have reached the node containing the handle.
647
- // Check if this node has a handle registered.
648
- if n.handle != nil {
649
- return ciPath
650
- }
651
-
652
- // No handle found.
653
- // Try to fix the path by adding a trailing slash
654
- if fixTrailingSlash {
655
- for i, c := range []byte(n.indices) {
656
- if c == '/' {
657
- n = n.children[i]
658
- if (len(n.path) == 1 && n.handle != nil) ||
659
- (n.nType == catchAll && n.children[0].handle != nil) {
660
- return append(ciPath, '/')
661
- }
662
- return nil
663
- }
664
- }
665
- }
666
- return nil
667
- }
668
- }
669
-
670
- // Nothing found.
671
- // Try to fix the path by adding / removing a trailing slash
672
- if fixTrailingSlash {
673
- if path == "/" {
674
- return ciPath
675
- }
676
- if len(path)+1 == npLen && n.path[len(path)] == '/' &&
677
- strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handle != nil {
678
- return append(ciPath, n.path...)
679
- }
680
- }
681
- return nil
682
- }
683
-
684
- // Param is a single URL parameter, consisting of a key and a value.
685
- type Param struct {
686
- Key string
687
- Value string
688
- }
689
-
690
- // Params is a Param-slice, as returned by the router.
691
- // The slice is ordered, the first URL parameter is also the first slice value.
692
- // It is therefore safe to read values by the index.
693
- type Params []Param
694
-
695
- // ByName returns the value of the first Param which key matches the given name.
696
- // If no matching Param is found, an empty string is returned.
697
- func (ps Params) ByName(name string) string {
698
- for _, p := range ps {
699
- if p.Key == name {
700
- return p.Value
701
- }
702
- }
703
- return ""
704
- }
705
-
706
- type paramsKey struct{}
707
-
708
- // ParamsKey is the request context key under which URL params are stored.
709
- var ParamsKey = paramsKey{}
710
-
711
- // ParamsFromContext pulls the URL parameters from a request context,
712
- // or returns nil if none are present.
713
- func ParamsFromContext(ctx context.Context) Params {
714
- p, _ := ctx.Value(ParamsKey).(Params)
715
- return p
716
- }
717
-
718
- type RouteFn func(w http.ResponseWriter, r *http.Request) *Element
719
-
720
- // Router is a http.Handler which can be used to dispatch requests to different
721
- // handler functions via configurable routes
722
- type Router struct {
723
- trees map[string]*node
724
- maxParams uint16
725
- // Enables automatic redirection if the current route can't be matched but a
726
- // handler for the path with (without) the trailing slash exists.
727
- // For example if /foo/ is requested but a route only exists for /foo, the
728
- // client is redirected to /foo with http status code 301 for GET requests
729
- // and 308 for all other request methods.
730
- RedirectTrailingSlash bool
731
-
732
- // Configurable handler which is called when no matching route is found.
733
- NotFound RouteFn
734
-
735
- // Configurable handler which is called when an error occurs.
736
- Error RouteFn
737
- }
738
-
739
- var globalRouter = &Router{
740
- RedirectTrailingSlash: true,
741
- NotFound: func(w http.ResponseWriter, r *http.Request) *Element {
742
- return Col(
743
- Row(
744
- Text("This is the default 404 - Not Found Route handler"),
745
- ),
746
- Row(
747
- Text("SetNotFoundHandler(func(c *RenderContext) UI {}) to override it"),
748
- ),
749
- )
750
- },
751
- Error: func(w http.ResponseWriter, r *http.Request) *Element {
752
- // err.Error()s
753
- return Col(
754
- Row(
755
- Text("This is the default 500 - Internal Server Error Route handler"),
756
- ),
757
- Row(
758
- Text("Error: "),
759
- ),
760
- Row(
761
- Text("SetErrorHandler(func(r *http.Request, w http.ResponseWriter) *Element {}) to override it"),
762
- ),
763
- )
764
- },
765
- }
766
-
767
- func SetNotFoundHandler(b RouteFn) {
768
- globalRouter.NotFound = b
769
- }
770
-
771
- func SetErrorHandler(b RouteFn) {
772
- globalRouter.Error = b
773
- }
774
-
775
- // GET route
776
- func (r *Router) GET(path string, handle RouteFn) {
777
- r.Handle("GET", path, handle)
778
- }
779
-
780
- // POST route
781
- func (r *Router) POST(path string, handle RouteFn) {
782
- r.Handle("POST", path, handle)
783
- }
784
-
785
- // PUT route
786
- func (r *Router) PUT(path string, handle RouteFn) {
787
- r.Handle("PUT", path, handle)
788
- }
789
-
790
- // DELETE route
791
- func (r *Router) DELETE(path string, handle RouteFn) {
792
- r.Handle("DELETE", path, handle)
793
- }
794
-
795
- // Handle registers a new request handle with the given path and method.
796
- //
797
- // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
798
- // functions can be used.
799
- //
800
- // This function is intended for bulk loading and to allow the usage of less
801
- // frequently used, non-standardized or custom methods (e.g. for internal
802
- // communication with a proxy).
803
- func (r *Router) Handle(method, path string, handle RouteFn) {
804
- varsCount := uint16(0)
805
-
806
- if method == "" {
807
- panic("method must not be empty")
808
- }
809
- if len(path) < 1 || path[0] != '/' {
810
- panic("path must begin with '/' in path '" + path + "'")
811
- }
812
- if handle == nil {
813
- panic("handle must not be nil")
814
- }
815
-
816
- if r.trees == nil {
817
- r.trees = make(map[string]*node)
818
- }
819
-
820
- root := r.trees[method]
821
- if root == nil {
822
- root = new(node)
823
- r.trees[method] = root
824
- }
825
-
826
- root.addRoute(path, handle)
827
-
828
- // Update maxParams
829
- if paramsCount := countParams(path); paramsCount+varsCount > r.maxParams {
830
- r.maxParams = paramsCount + varsCount
831
- }
832
- }
833
-
834
- // Lookup allows the manual lookup of a method + path combo.
835
- // This is e.g. useful to build a framework around this router.
836
- // If the path was found, it returns the handle function and the path parameter
837
- // values. Otherwise the third return value indicates whether a redirection to
838
- // the same path with an extra / without the trailing slash should be performed.
839
- func (r *Router) Lookup(method, path string) (interface{}, Params, bool) {
840
- if root := r.trees[method]; root != nil {
841
- handle, ps, tsr := root.getValue(path, nil)
842
- if handle == nil {
843
- return nil, nil, tsr
844
- }
845
- if ps == nil {
846
- return handle, nil, tsr
847
- }
848
- return handle, *ps, tsr
849
- }
850
- return nil, nil, false
851
- }
852
-
853
- // ServeHTTP makes the router implement the http.Handler interface.
854
- func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
855
- // Handle errors
856
- defer func() {
857
- if rcv := recover(); rcv != nil {
858
- var err error
859
- switch x := rcv.(type) {
860
- case string:
861
- err = errors.New(x)
862
- case error:
863
- err = x
864
- default:
865
- err = errors.New("unknown panic")
866
- }
867
- w.Header().Set("Content-Type", "text/html")
868
- w.WriteHeader(http.StatusInternalServerError)
869
- w.Write([]byte(err.Error()))
870
- // r.Error(w, req)
871
- }
872
- }()
873
-
874
- path := req.URL.Path
875
- println("route: " + req.URL.Path)
876
-
877
- if root := r.trees[req.Method]; root != nil {
878
- // TODO: use _ ps save it to context for useParam()
879
- if handle, _, tsr := root.getValue(path, nil); handle != nil {
880
- // w.Header().Set("Content-Type", "text/html")
881
- // w.WriteHeader(http.StatusOK)
882
- elm := handle(w, req)
883
- if elm != nil {
884
- elm.HtmlWithIndent(w, 2)
885
- }
886
- return
887
- } else if req.Method != http.MethodConnect && path != "/" {
888
- // Moved Permanently, request with GET method
889
- code := http.StatusMovedPermanently
890
- if req.Method != http.MethodGet {
891
- // Permanent Redirect, request with same method
892
- code = http.StatusPermanentRedirect
893
- }
894
-
895
- if tsr && r.RedirectTrailingSlash {
896
- if len(path) > 1 && path[len(path)-1] == '/' {
897
- req.URL.Path = path[:len(path)-1]
898
- } else {
899
- req.URL.Path = path + "/"
900
- }
901
- http.Redirect(w, req, req.URL.String(), code)
902
- return
903
- }
904
- }
905
- }
906
-
907
- // Handle 404
908
- w.Header().Set("Content-Type", "text/html")
909
- w.WriteHeader(http.StatusNotFound)
910
- r.NotFound(w, req)
911
- }
912
-
913
- // func (r *Router) Lambda(ctx context.Context, e events.APIGatewayV2HTTPRequest) (res events.APIGatewayV2HTTPResponse, err error) {
914
- // res.StatusCode = 200
915
- // res.Headers = map[string]string{
916
- // "Content-Type": "text/html",
917
- // }
918
- // // Handle errors
919
- // defer func() {
920
- // if rcv := recover(); rcv != nil {
921
- // switch x := rcv.(type) {
922
- // case string:
923
- // err = errors.New(x)
924
- // case error:
925
- // err = x
926
- // default:
927
- // err = errors.New("unknown panic")
928
- // }
929
- // res.Body = r.getPage(r.Error(err))
930
- // }
931
- // }()
932
-
933
- // println("route: " + e.RawPath)
934
- // path := strings.Replace(e.RawPath, "/Prod/", "/", 1)
935
- // if root := r.trees[e.RequestContext.HTTP.Method]; root != nil {
936
- // if handle, _, _ := root.getValue(path, nil); handle != nil {
937
- // res.Body = r.getPage(handle)
938
- // return
939
- // }
940
- // }
941
-
942
- // // Handle 404
943
- // res.StatusCode = http.StatusNotFound
944
- // res.Body = r.getPage(r.NotFound(NewRenderContext()))
945
- // return
946
- // }
947
-
948
- func Run(assetsFS embed.FS) {
949
- isLambda := os.Getenv("_LAMBDA_SERVER_PORT") != ""
950
- if !isLambda {
951
- assetsHandler := http.FileServer(http.FS(assetsFS))
952
- globalRouter.GET("/assets/*filepath", func(w http.ResponseWriter, r *http.Request) *Element {
953
- assetsHandler.ServeHTTP(w, r)
954
- return nil
955
- })
956
- println("Serving on http://localhost:1234")
957
- http.ListenAndServe(":1234", globalRouter)
958
- } else {
959
- // println("running in lambda mode")
960
- // lambda.Start(globalRouter.Lambda)
961
- }
962
-
963
- }
964
-
965
- func Route(path string, render RouteFn) {
966
- println("registering route: " + path)
967
- globalRouter.GET(path, render)
968
- }