~repos /gromer
git clone https://pyrossh.dev/repos/gromer.git
gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.
2caf3db0
—
Peter John 4 years ago
improve more
- LICENSE +0 -43
- attributes.go +0 -132
- attributes_test.go +0 -48
- element.go +0 -139
- element_test.go +0 -461
- example/assets/icon.png +0 -0
- example/assets/icon2.png +0 -0
- example/components/header.go +1 -1
- example/main.go +31 -31
- example/makefile +12 -16
- example/readme.md +0 -11
- example/samconfig.toml +0 -10
- example/template.yml +11 -58
- go.mod +2 -1
- go.sum +18 -0
- html.go +219 -6
- readme.md +40 -105
- router.go +0 -968
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://
|
|
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
|
-
/
|
|
6
|
+
"net/http"
|
|
7
|
+
"os"
|
|
8
|
+
"time"
|
|
6
9
|
|
|
7
|
-
|
|
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
|
-
|
|
27
|
+
isLambda := os.Getenv("_LAMBDA_SERVER_PORT") != ""
|
|
28
|
+
r := mux.NewRouter()
|
|
18
|
-
//
|
|
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
|
-
/
|
|
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
|
-
|
|
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
|
-
|
|
42
|
+
log.Print("running in lambda mode")
|
|
44
|
-
|
|
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
|
-
|
|
4
|
+
local:
|
|
5
|
-
|
|
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
|
-
|
|
17
|
+
make css
|
|
15
|
-
|
|
18
|
+
make build
|
|
16
19
|
sam deploy
|
|
17
|
-
make wasm
|
|
18
|
-
aws s3 sync ./assets s3://wapp
|
|
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
|
|
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:
|
|
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
|
-
|
|
8
|
+
AppUrl:
|
|
19
9
|
Value: !GetAtt CloudFrontDistribution.DomainName
|
|
20
10
|
ApiUrl:
|
|
21
|
-
Value: !Sub https://${
|
|
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:
|
|
17
|
+
BucketName: wapp-example
|
|
28
18
|
|
|
29
19
|
CloudFrontOriginAccessIdentity:
|
|
30
20
|
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
|
|
31
21
|
Properties:
|
|
32
22
|
CloudFrontOriginAccessIdentityConfig:
|
|
33
|
-
Comment:
|
|
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 [
|
|
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
|
|
68
|
+
OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}'
|
|
88
69
|
- Id: app
|
|
89
|
-
DomainName: !Sub ${
|
|
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: [
|
|
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:
|
|
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:
|
|
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/
|
|
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
|
-
|
|
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("
|
|
180
|
+
return NewElement("h2", false, uis...)
|
|
78
181
|
}
|
|
79
182
|
func H3(uis ...interface{}) *Element {
|
|
80
|
-
return NewElement("
|
|
183
|
+
return NewElement("h3", false, uis...)
|
|
81
184
|
}
|
|
82
185
|
func H4(uis ...interface{}) *Element {
|
|
83
|
-
return NewElement("
|
|
186
|
+
return NewElement("h4", false, uis...)
|
|
84
187
|
}
|
|
85
188
|
func H5(uis ...interface{}) *Element {
|
|
86
|
-
return NewElement("
|
|
189
|
+
return NewElement("h5", false, uis...)
|
|
87
190
|
}
|
|
88
191
|
func H6(uis ...interface{}) *Element {
|
|
89
|
-
return NewElement("
|
|
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
|
|
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.
|
|
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
|
-
[
|
|
23
|
+
[Demo](https://github.com/pyros2097/wapp-example)
|
|
45
24
|
|
|
46
|
-
**
|
|
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
|
|
34
|
+
func Header() *Element {
|
|
56
|
-
count, setCount := c.UseInt(0)
|
|
57
|
-
inc := func() { setCount(count() + 1) }
|
|
58
|
-
|
|
35
|
+
return Row(Css("w-full mb-20 font-bold text-xl text-gray-700 p-4"),
|
|
59
|
-
return Col(
|
|
60
|
-
Row(
|
|
61
|
-
|
|
36
|
+
Div(Css("text-blue-700"),
|
|
62
|
-
|
|
37
|
+
A(Href("https://wapp.pyros2097.dev"), Text("wapp.pyros2097.dev")),
|
|
63
|
-
),
|
|
64
38
|
),
|
|
65
|
-
|
|
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
|
-
|
|
42
|
+
Div(Css("link mr-4"), A(Href("/clock"), Text("Clock"))),
|
|
67
|
-
Text("-"),
|
|
68
|
-
),
|
|
69
|
-
|
|
43
|
+
Div(Css("link mr-4"), A(Href("/about"), Text("About"))),
|
|
70
|
-
|
|
44
|
+
Div(Css("link mr-4"), A(Href("/container"), Text("Container"))),
|
|
71
|
-
),
|
|
72
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
58
|
+
Row(
|
|
118
|
-
|
|
59
|
+
Row(Css("underline"),
|
|
119
|
-
|
|
60
|
+
Text("Counter"),
|
|
120
|
-
|
|
61
|
+
),
|
|
121
|
-
|
|
62
|
+
),
|
|
122
|
-
|
|
63
|
+
Row(
|
|
123
|
-
|
|
64
|
+
Button(Css("btn m-20"),
|
|
124
|
-
|
|
65
|
+
Text("-"),
|
|
125
|
-
|
|
66
|
+
),
|
|
67
|
+
Row(Css("m-20"),
|
|
68
|
+
Text(strconv.Itoa(1)),
|
|
126
|
-
|
|
69
|
+
),
|
|
127
|
-
Row(
|
|
128
|
-
|
|
70
|
+
Button(Css("btn m-20"),
|
|
129
|
-
|
|
71
|
+
Text("+"),
|
|
130
|
-
|
|
72
|
+
),
|
|
131
|
-
Div(Css("text-6xl m-20 cursor-pointer select-none"), OnClick(stopTimer),
|
|
132
|
-
|
|
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
|
-
}
|