~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.
17cb1d3e
—
Peter John 4 years ago
fix issues
- .snapshots/TestHtmlPage +78 -14
- cmd/{main.go → wapp/main.go} +0 -0
- context.go +0 -71
- css.go +51 -0
- html.go +84 -13
- html_test.go +9 -0
- readme.md +8 -0
.snapshots/TestHtmlPage
CHANGED
|
@@ -4,8 +4,67 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
|
6
6
|
<meta http-equiv="encoding" content="utf-8">
|
|
7
|
+
<title> 123 </title>
|
|
8
|
+
|
|
9
|
+
<meta name="description" content="123">
|
|
10
|
+
<meta name="author" content="123">
|
|
11
|
+
<meta name="keywords" content="123">
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover">
|
|
13
|
+
<link rel="icon" href="/assets/icon.png">
|
|
14
|
+
<link rel="apple-touch-icon" href="/assets/icon.png">
|
|
15
|
+
<link rel="stylesheet" href="/assets/styles.css">
|
|
16
|
+
<script src="/assets/alpine.js" defer></script>
|
|
17
|
+
|
|
7
18
|
<meta name="title" content="title">
|
|
8
19
|
<style>
|
|
20
|
+
*, ::before, ::after { box-sizing: border-box; }
|
|
21
|
+
html { -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; line-height: 1.15; -webkit-text-size-adjust: 100%; }
|
|
22
|
+
body { margin: 0; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; }
|
|
23
|
+
hr { height: 0; color: inherit; }
|
|
24
|
+
abbr[title] { -webkit-text-decoration: underline dotted; text-decoration: underline dotted; }
|
|
25
|
+
b, strong { font-weight: bolder; }
|
|
26
|
+
code, kbd, samp, pre { font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 1em; }
|
|
27
|
+
small { font-size: 80%; }
|
|
28
|
+
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
|
|
29
|
+
sub { bottom: -0.25em; }
|
|
30
|
+
sup { top: -0.5em; }
|
|
31
|
+
table { text-indent: 0; border-color: inherit; }
|
|
32
|
+
button, input, optgroup, select, textarea { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; }
|
|
33
|
+
button, select { text-transform: none; }
|
|
34
|
+
button, [type='button'], [type='reset'], [type='submit'] { -webkit-appearance: button; }
|
|
35
|
+
::-moz-focus-inner { border-style: none; padding: 0; }
|
|
36
|
+
:-moz-focusring { outline: 1px dotted ButtonText; outline: auto; }
|
|
37
|
+
:-moz-ui-invalid { box-shadow: none; }
|
|
38
|
+
legend { padding: 0; }
|
|
39
|
+
progress { vertical-align: baseline; }
|
|
40
|
+
::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; }
|
|
41
|
+
[type='search'] { -webkit-appearance: textfield; outline-offset: -2px; }
|
|
42
|
+
::-webkit-search-decoration { -webkit-appearance: none; }
|
|
43
|
+
::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; }
|
|
44
|
+
summary { display: list-item; }
|
|
45
|
+
blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre { margin: 0; }
|
|
46
|
+
button { background-color: transparent; background-image: none; }
|
|
47
|
+
fieldset { margin: 0; padding: 0; }
|
|
48
|
+
ol, ul { list-style: none; margin: 0; padding: 0; }
|
|
49
|
+
html { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; line-height: 1.5; }
|
|
50
|
+
body { font-family: inherit; line-height: inherit; }
|
|
51
|
+
*, ::before, ::after { box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor; }
|
|
52
|
+
hr { border-top-width: 1px; }
|
|
53
|
+
img { border-style: solid; }
|
|
54
|
+
textarea { resize: vertical; }
|
|
55
|
+
input::-moz-placeholder, textarea::-moz-placeholder { opacity: 1; color: #9ca3af; }
|
|
56
|
+
input:-ms-input-placeholder, textarea:-ms-input-placeholder { opacity: 1; color: #9ca3af; }
|
|
57
|
+
input::placeholder, textarea::placeholder { opacity: 1; color: #9ca3af; }
|
|
58
|
+
button, [role="button"] { cursor: pointer; }
|
|
59
|
+
table { border-collapse: collapse; }
|
|
60
|
+
h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; }
|
|
61
|
+
a { color: inherit; text-decoration: inherit; }
|
|
62
|
+
button, input, optgroup, select, textarea { padding: 0; line-height: inherit; color: inherit; }
|
|
63
|
+
pre, code, kbd, samp { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
|
64
|
+
img, svg, video, canvas, audio, iframe, embed, object { display: block; vertical-align: middle; }
|
|
65
|
+
img, video { max-width: 100%; height: auto; }
|
|
66
|
+
[hidden] { display: none; }
|
|
67
|
+
*, ::before, ::after { --tw-border-opacity: 1; border-color: rgba(229, 231, 235, var(--tw-border-opacity)); }
|
|
9
68
|
.flex { display: flex; }
|
|
10
69
|
.flex-col { flex-direction: column; }
|
|
11
70
|
.justify-center { justify-content: center; }
|
|
@@ -15,7 +74,8 @@
|
|
|
15
74
|
.flex-row { flex-direction: row; }
|
|
16
75
|
.underline { text-decoration: underline; }
|
|
17
76
|
.m-20 { margin: 5rem; }
|
|
18
|
-
.text-8xl { font-size: 6rem; line-height: 1; }
|
|
77
|
+
.text-8xl { font-size: 6rem; line-height: 1; }
|
|
78
|
+
</style>
|
|
19
79
|
</head>
|
|
20
80
|
<body>
|
|
21
81
|
<h1> Hello this is a h1 </h1>
|
|
@@ -24,14 +84,14 @@
|
|
|
24
84
|
|
|
25
85
|
<h3 x-data="{ message: 'I ❤️ Alpine' }" x-text="message"> </h3>
|
|
26
86
|
|
|
27
|
-
<counter> <div class="flex flex-col justify-center items-center text-3xl text-gray-700">
|
|
87
|
+
<counter x-data="counter"> <div class="flex flex-col justify-center items-center text-3xl text-gray-700">
|
|
28
88
|
<div class="flex flex-row justify-center items-center"> <div class="flex flex-row justify-center items-center underline"> Counter </div>
|
|
29
89
|
</div>
|
|
30
90
|
|
|
31
|
-
<div
|
|
91
|
+
<div class="flex flex-row justify-center items-center" x-data="counter">
|
|
32
92
|
<button class="btn m-20" @click="decrement"> - </button>
|
|
33
93
|
|
|
34
|
-
<div
|
|
94
|
+
<div class="flex flex-row justify-center items-center m-20 text-8xl" x-text="state.count"> 4 </div>
|
|
35
95
|
|
|
36
96
|
<button class="btn m-20" @click="increment"> + </button>
|
|
37
97
|
</div>
|
|
@@ -39,17 +99,21 @@
|
|
|
39
99
|
</counter>
|
|
40
100
|
|
|
41
101
|
<script>
|
|
102
|
+
document.addEventListener('alpine:init', () => {
|
|
103
|
+
|
|
42
|
-
|
|
104
|
+
Alpine.data('counter', () => ({
|
|
43
|
-
|
|
105
|
+
state: {
|
|
44
|
-
|
|
106
|
+
count:1,
|
|
45
|
-
|
|
107
|
+
|
|
46
|
-
|
|
108
|
+
},
|
|
47
|
-
|
|
109
|
+
increment() {this.state.count += 1;},
|
|
48
|
-
|
|
110
|
+
decrement() {this.state.count -= 1;},
|
|
49
|
-
|
|
111
|
+
|
|
50
|
-
|
|
112
|
+
}));
|
|
51
113
|
|
|
114
|
+
|
|
115
|
+
});
|
|
52
|
-
|
|
116
|
+
</script>
|
|
53
117
|
</body>
|
|
54
118
|
|
|
55
119
|
</html>
|
cmd/{main.go → wapp/main.go}
RENAMED
|
File without changes
|
context.go
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
package wapp
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"encoding/json"
|
|
5
|
-
"net/http"
|
|
6
|
-
"reflect"
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
type State map[string]interface{}
|
|
10
|
-
type Actions map[string]func() string
|
|
11
|
-
|
|
12
|
-
type Reducer struct {
|
|
13
|
-
Name string
|
|
14
|
-
State
|
|
15
|
-
Actions
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
func RespondError(w http.ResponseWriter, status int, err error) {
|
|
19
|
-
w.WriteHeader(status)
|
|
20
|
-
w.Header().Set("Content-Type", "application/json")
|
|
21
|
-
data, _ := json.Marshal(map[string]string{
|
|
22
|
-
"error": err.Error(),
|
|
23
|
-
})
|
|
24
|
-
w.Write(data)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
func PerformRequest(h interface{}, ctx interface{}, w http.ResponseWriter, r *http.Request) error {
|
|
28
|
-
args := []reflect.Value{reflect.ValueOf(ctx)}
|
|
29
|
-
funcType := reflect.TypeOf(h)
|
|
30
|
-
icount := funcType.NumIn()
|
|
31
|
-
if icount == 2 {
|
|
32
|
-
structType := funcType.In(1)
|
|
33
|
-
instance := reflect.New(structType)
|
|
34
|
-
if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
|
|
35
|
-
err := json.NewDecoder(r.Body).Decode(instance.Interface())
|
|
36
|
-
if err != nil {
|
|
37
|
-
RespondError(w, 500, err)
|
|
38
|
-
return err
|
|
39
|
-
}
|
|
40
|
-
} else if r.Method == "GET" {
|
|
41
|
-
rv := instance.Elem()
|
|
42
|
-
for i := 0; i < structType.NumField(); i++ {
|
|
43
|
-
if f := rv.Field(i); f.CanSet() {
|
|
44
|
-
jsonName := structType.Field(i).Tag.Get("json")
|
|
45
|
-
jsonValue := r.URL.Query().Get(jsonName)
|
|
46
|
-
f.SetString(jsonValue)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
args = append(args, instance.Elem())
|
|
51
|
-
}
|
|
52
|
-
values := reflect.ValueOf(h).Call(args)
|
|
53
|
-
response := values[0].Interface()
|
|
54
|
-
responseStatus := values[1].Interface().(int)
|
|
55
|
-
responseError := values[2].Interface()
|
|
56
|
-
if responseError != nil {
|
|
57
|
-
RespondError(w, responseStatus, responseError.(error))
|
|
58
|
-
return responseError.(error)
|
|
59
|
-
}
|
|
60
|
-
if v, ok := response.(*HtmlPage); ok {
|
|
61
|
-
w.WriteHeader(responseStatus)
|
|
62
|
-
w.Header().Set("Content-Type", "text/html")
|
|
63
|
-
v.WriteHtml(w)
|
|
64
|
-
return nil
|
|
65
|
-
}
|
|
66
|
-
w.WriteHeader(responseStatus)
|
|
67
|
-
w.Header().Set("Content-Type", "application/json")
|
|
68
|
-
data, _ := json.Marshal(response)
|
|
69
|
-
w.Write(data)
|
|
70
|
-
return nil
|
|
71
|
-
}
|
css.go
CHANGED
|
@@ -469,3 +469,54 @@ func mapApply(obj KeyValues) {
|
|
|
469
469
|
}
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
|
+
|
|
473
|
+
var normalizeStyles = `
|
|
474
|
+
*, ::before, ::after { box-sizing: border-box; }
|
|
475
|
+
html { -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; line-height: 1.15; -webkit-text-size-adjust: 100%; }
|
|
476
|
+
body { margin: 0; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; }
|
|
477
|
+
hr { height: 0; color: inherit; }
|
|
478
|
+
abbr[title] { -webkit-text-decoration: underline dotted; text-decoration: underline dotted; }
|
|
479
|
+
b, strong { font-weight: bolder; }
|
|
480
|
+
code, kbd, samp, pre { font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 1em; }
|
|
481
|
+
small { font-size: 80%; }
|
|
482
|
+
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
|
|
483
|
+
sub { bottom: -0.25em; }
|
|
484
|
+
sup { top: -0.5em; }
|
|
485
|
+
table { text-indent: 0; border-color: inherit; }
|
|
486
|
+
button, input, optgroup, select, textarea { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; }
|
|
487
|
+
button, select { text-transform: none; }
|
|
488
|
+
button, [type='button'], [type='reset'], [type='submit'] { -webkit-appearance: button; }
|
|
489
|
+
::-moz-focus-inner { border-style: none; padding: 0; }
|
|
490
|
+
:-moz-focusring { outline: 1px dotted ButtonText; outline: auto; }
|
|
491
|
+
:-moz-ui-invalid { box-shadow: none; }
|
|
492
|
+
legend { padding: 0; }
|
|
493
|
+
progress { vertical-align: baseline; }
|
|
494
|
+
::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; }
|
|
495
|
+
[type='search'] { -webkit-appearance: textfield; outline-offset: -2px; }
|
|
496
|
+
::-webkit-search-decoration { -webkit-appearance: none; }
|
|
497
|
+
::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; }
|
|
498
|
+
summary { display: list-item; }
|
|
499
|
+
blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre { margin: 0; }
|
|
500
|
+
button { background-color: transparent; background-image: none; }
|
|
501
|
+
fieldset { margin: 0; padding: 0; }
|
|
502
|
+
ol, ul { list-style: none; margin: 0; padding: 0; }
|
|
503
|
+
html { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; line-height: 1.5; }
|
|
504
|
+
body { font-family: inherit; line-height: inherit; }
|
|
505
|
+
*, ::before, ::after { box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor; }
|
|
506
|
+
hr { border-top-width: 1px; }
|
|
507
|
+
img { border-style: solid; }
|
|
508
|
+
textarea { resize: vertical; }
|
|
509
|
+
input::-moz-placeholder, textarea::-moz-placeholder { opacity: 1; color: #9ca3af; }
|
|
510
|
+
input:-ms-input-placeholder, textarea:-ms-input-placeholder { opacity: 1; color: #9ca3af; }
|
|
511
|
+
input::placeholder, textarea::placeholder { opacity: 1; color: #9ca3af; }
|
|
512
|
+
button, [role="button"] { cursor: pointer; }
|
|
513
|
+
table { border-collapse: collapse; }
|
|
514
|
+
h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; }
|
|
515
|
+
a { color: inherit; text-decoration: inherit; }
|
|
516
|
+
button, input, optgroup, select, textarea { padding: 0; line-height: inherit; color: inherit; }
|
|
517
|
+
pre, code, kbd, samp { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
|
518
|
+
img, svg, video, canvas, audio, iframe, embed, object { display: block; vertical-align: middle; }
|
|
519
|
+
img, video { max-width: 100%; height: auto; }
|
|
520
|
+
[hidden] { display: none; }
|
|
521
|
+
*, ::before, ::after { --tw-border-opacity: 1; border-color: rgba(229, 231, 235, var(--tw-border-opacity)); }
|
|
522
|
+
`
|
html.go
CHANGED
|
@@ -2,8 +2,11 @@ package wapp
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"bytes"
|
|
5
|
+
"encoding/json"
|
|
5
6
|
"fmt"
|
|
6
7
|
"io"
|
|
8
|
+
"net/http"
|
|
9
|
+
"reflect"
|
|
7
10
|
"strconv"
|
|
8
11
|
"strings"
|
|
9
12
|
|
|
@@ -52,7 +55,7 @@ func (p *HtmlPage) computeCss(elems []*Element) {
|
|
|
52
55
|
if s, ok := twClassLookup[c]; ok {
|
|
53
56
|
if _, ok2 := p.classLookup[c]; !ok2 {
|
|
54
57
|
p.classLookup[c] = true
|
|
55
|
-
p.css.WriteString("
|
|
58
|
+
p.css.WriteString(" ." + c + " { " + s + " } \n")
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -79,9 +82,13 @@ func (p *HtmlPage) WriteHtml(w io.Writer) {
|
|
|
79
82
|
w.Write([]byte("<html>\n"))
|
|
80
83
|
p.computeCss(p.Body.body)
|
|
81
84
|
p.computeJs(p.Body.body)
|
|
82
|
-
p.Head.body = append(p.Head.body, StyleTag(Text(p.css.String())))
|
|
85
|
+
p.Head.body = append(p.Head.body, StyleTag(Text(normalizeStyles+p.css.String())))
|
|
83
86
|
p.Head.writeHtmlIndent(w, 1)
|
|
84
|
-
p.Body.body = append(p.Body.body, Script(Text(
|
|
87
|
+
p.Body.body = append(p.Body.body, Script(Text(fmt.Sprintf(`
|
|
88
|
+
document.addEventListener('alpine:init', () => {
|
|
89
|
+
%s
|
|
90
|
+
});
|
|
91
|
+
`, p.js.String()))))
|
|
85
92
|
p.Body.writeHtmlIndent(w, 1)
|
|
86
93
|
w.Write([]byte("\n</html>"))
|
|
87
94
|
}
|
|
@@ -109,7 +116,7 @@ func Body(elems ...*Element) *Element {
|
|
|
109
116
|
return &Element{tag: "body", body: elems}
|
|
110
117
|
}
|
|
111
118
|
|
|
112
|
-
func Component(r Reducer,
|
|
119
|
+
func Component(r Reducer, uis ...interface{}) *Element {
|
|
113
120
|
v := velvet.NewContext()
|
|
114
121
|
stateMap := map[string]interface{}{}
|
|
115
122
|
actionsMap := map[string]interface{}{}
|
|
@@ -124,19 +131,19 @@ func Component(r Reducer, elems ...*Element) *Element {
|
|
|
124
131
|
v.Set("state", stateMap)
|
|
125
132
|
v.Set("actions", actionsMap)
|
|
126
133
|
s, err := velvet.Render(`
|
|
127
|
-
|
|
134
|
+
Alpine.data('{{ name }}', () => ({
|
|
128
|
-
|
|
135
|
+
state: {
|
|
129
|
-
|
|
136
|
+
{{#each state}}{{ @key }}{{ @value }},
|
|
130
|
-
|
|
137
|
+
{{/each}}
|
|
131
|
-
|
|
138
|
+
},
|
|
132
|
-
|
|
139
|
+
{{#each actions}}{{ @key }}{{ @value }},
|
|
133
|
-
|
|
140
|
+
{{/each}}
|
|
134
|
-
|
|
141
|
+
}));
|
|
135
142
|
`, v)
|
|
136
143
|
if err != nil {
|
|
137
144
|
panic(err)
|
|
138
145
|
}
|
|
139
|
-
return &Element{tag: r.Name, text: "wapp_js|" + s,
|
|
146
|
+
return mergeAttributes(&Element{tag: r.Name, text: "wapp_js|" + s}, append([]interface{}{XData(r.Name)}, uis...)...)
|
|
140
147
|
}
|
|
141
148
|
|
|
142
149
|
type Element struct {
|
|
@@ -429,3 +436,67 @@ func XData(v string) Attribute {
|
|
|
429
436
|
func XText(v string) Attribute {
|
|
430
437
|
return Attribute{"x-text", v}
|
|
431
438
|
}
|
|
439
|
+
|
|
440
|
+
type State map[string]interface{}
|
|
441
|
+
type Actions map[string]func() string
|
|
442
|
+
|
|
443
|
+
type Reducer struct {
|
|
444
|
+
Name string
|
|
445
|
+
State
|
|
446
|
+
Actions
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
func RespondError(w http.ResponseWriter, status int, err error) {
|
|
450
|
+
w.WriteHeader(status)
|
|
451
|
+
w.Header().Set("Content-Type", "application/json")
|
|
452
|
+
data, _ := json.Marshal(map[string]string{
|
|
453
|
+
"error": err.Error(),
|
|
454
|
+
})
|
|
455
|
+
w.Write(data)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
func PerformRequest(h interface{}, ctx interface{}, w http.ResponseWriter, r *http.Request) error {
|
|
459
|
+
args := []reflect.Value{reflect.ValueOf(ctx)}
|
|
460
|
+
funcType := reflect.TypeOf(h)
|
|
461
|
+
icount := funcType.NumIn()
|
|
462
|
+
if icount == 2 {
|
|
463
|
+
structType := funcType.In(1)
|
|
464
|
+
instance := reflect.New(structType)
|
|
465
|
+
if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
|
|
466
|
+
err := json.NewDecoder(r.Body).Decode(instance.Interface())
|
|
467
|
+
if err != nil {
|
|
468
|
+
RespondError(w, 500, err)
|
|
469
|
+
return err
|
|
470
|
+
}
|
|
471
|
+
} else if r.Method == "GET" {
|
|
472
|
+
rv := instance.Elem()
|
|
473
|
+
for i := 0; i < structType.NumField(); i++ {
|
|
474
|
+
if f := rv.Field(i); f.CanSet() {
|
|
475
|
+
jsonName := structType.Field(i).Tag.Get("json")
|
|
476
|
+
jsonValue := r.URL.Query().Get(jsonName)
|
|
477
|
+
f.SetString(jsonValue)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
args = append(args, instance.Elem())
|
|
482
|
+
}
|
|
483
|
+
values := reflect.ValueOf(h).Call(args)
|
|
484
|
+
response := values[0].Interface()
|
|
485
|
+
responseStatus := values[1].Interface().(int)
|
|
486
|
+
responseError := values[2].Interface()
|
|
487
|
+
if responseError != nil {
|
|
488
|
+
RespondError(w, responseStatus, responseError.(error))
|
|
489
|
+
return responseError.(error)
|
|
490
|
+
}
|
|
491
|
+
if v, ok := response.(HtmlPage); ok {
|
|
492
|
+
w.WriteHeader(responseStatus)
|
|
493
|
+
w.Header().Set("Content-Type", "text/html")
|
|
494
|
+
v.WriteHtml(w)
|
|
495
|
+
return nil
|
|
496
|
+
}
|
|
497
|
+
w.WriteHeader(responseStatus)
|
|
498
|
+
w.Header().Set("Content-Type", "application/json")
|
|
499
|
+
data, _ := json.Marshal(response)
|
|
500
|
+
w.Write(data)
|
|
501
|
+
return nil
|
|
502
|
+
}
|
html_test.go
CHANGED
|
@@ -98,6 +98,15 @@ func TestHtmlPage(t *testing.T) {
|
|
|
98
98
|
b := bytes.NewBuffer(nil)
|
|
99
99
|
p := Html(
|
|
100
100
|
Head(
|
|
101
|
+
Title("123"),
|
|
102
|
+
Meta("description", "123"),
|
|
103
|
+
Meta("author", "123"),
|
|
104
|
+
Meta("keywords", "123"),
|
|
105
|
+
Meta("viewport", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover"),
|
|
106
|
+
Link("icon", "/assets/icon.png"),
|
|
107
|
+
Link("apple-touch-icon", "/assets/icon.png"),
|
|
108
|
+
Link("stylesheet", "/assets/styles.css"),
|
|
109
|
+
Script(Src("/assets/alpine.js"), Defer()),
|
|
101
110
|
Meta("title", "title"),
|
|
102
111
|
),
|
|
103
112
|
Body(
|
readme.md
CHANGED
|
@@ -15,6 +15,14 @@ It uses a declarative syntax using funcs that allows creating and dealing with H
|
|
|
15
15
|
go mod init
|
|
16
16
|
go get -u -v github.com/pyros2097/wapp
|
|
17
17
|
```
|
|
18
|
+
|
|
19
|
+
# Install Cli
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
go mod init
|
|
23
|
+
go get -u -v github.com/pyros2097/wapp/cmd/wapp
|
|
24
|
+
```
|
|
25
|
+
|
|
18
26
|
# Example
|
|
19
27
|
|
|
20
28
|
https://github.com/pyros2097/wapp-example
|