~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.
ac320ee3
—
Peter John 3 years ago
use gsx
- gsx/gsx.go +270 -0
- gsx/gsx_test.go +92 -0
- gsx/parser.go +0 -41
- gsx/template.go +0 -196
- gsx/template_test.go +0 -70
- lib/main.go +0 -133
gsx/gsx.go
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
package gsx
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"io"
|
|
6
|
+
"reflect"
|
|
7
|
+
"regexp"
|
|
8
|
+
"runtime"
|
|
9
|
+
"strings"
|
|
10
|
+
|
|
11
|
+
"github.com/alecthomas/repr"
|
|
12
|
+
"golang.org/x/net/html"
|
|
13
|
+
"golang.org/x/net/html/atom"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
var (
|
|
17
|
+
contextNode = &html.Node{
|
|
18
|
+
Type: html.ElementNode,
|
|
19
|
+
Data: "div",
|
|
20
|
+
DataAtom: atom.Lookup([]byte("div")),
|
|
21
|
+
}
|
|
22
|
+
htmlTags = []string{"a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "base", "basefont", "bb", "bdo", "big", "blockquote", "body", "br /", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "command", "datagrid", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "eventsource", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame", "frameset", "h1 to <h6>", "head", "header", "hgroup", "hr /", "html", "i", "iframe", "img", "input", "ins", "isindex", "kbd", "keygen", "label", "legend", "li", "link", "map", "mark", "menu", "meta", "meter", "nav", "noframes", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "script", "section", "select", "small", "source", "span", "strike", "strong", "style", "sub", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"}
|
|
23
|
+
compMap = map[string]ComponentFunc{}
|
|
24
|
+
funcMap = map[string]interface{}{}
|
|
25
|
+
styles = ""
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
type (
|
|
29
|
+
ComponentFunc struct {
|
|
30
|
+
Func interface{}
|
|
31
|
+
Args []string
|
|
32
|
+
}
|
|
33
|
+
Html map[string]interface{}
|
|
34
|
+
Node struct {
|
|
35
|
+
*html.Node
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
func (h Html) Render(tpl string) Node {
|
|
40
|
+
newTpl := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(tpl, "\n", ""), "\r", ""), "\t", "")
|
|
41
|
+
doc, err := html.ParseFragment(bytes.NewBuffer([]byte(newTpl)), contextNode)
|
|
42
|
+
if err != nil {
|
|
43
|
+
panic(err)
|
|
44
|
+
}
|
|
45
|
+
populate(h, doc[0])
|
|
46
|
+
return Node{doc[0]}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func (n Node) Write(w io.Writer) error {
|
|
50
|
+
return html.Render(w, n.Node)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func (n Node) String() string {
|
|
54
|
+
b := bytes.NewBuffer(nil)
|
|
55
|
+
html.Render(b, n.Node)
|
|
56
|
+
return b.String()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func RegisterComponent(name string, f interface{}, args ...string) {
|
|
60
|
+
compMap[name] = ComponentFunc{
|
|
61
|
+
Func: f,
|
|
62
|
+
Args: args,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func RegisterFunc(f interface{}) {
|
|
67
|
+
name := getFunctionName(f)
|
|
68
|
+
funcMap[name] = f
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func Css(v string) string {
|
|
72
|
+
styles += v
|
|
73
|
+
return v
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func GetStyles() string {
|
|
77
|
+
return styles
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func getFunctionName(temp interface{}) string {
|
|
81
|
+
strs := strings.Split((runtime.FuncForPC(reflect.ValueOf(temp).Pointer()).Name()), ".")
|
|
82
|
+
return strs[len(strs)-1]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func getAttribute(k string, kvs []html.Attribute) string {
|
|
86
|
+
for _, v := range kvs {
|
|
87
|
+
if v.Key == k {
|
|
88
|
+
return v.Val
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return ""
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func convert(ref string, i interface{}) interface{} {
|
|
95
|
+
switch iv := i.(type) {
|
|
96
|
+
case bool:
|
|
97
|
+
if strings.Contains(ref, "!") {
|
|
98
|
+
return !iv
|
|
99
|
+
} else {
|
|
100
|
+
return iv
|
|
101
|
+
}
|
|
102
|
+
case string:
|
|
103
|
+
return iv
|
|
104
|
+
}
|
|
105
|
+
return nil
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func subsRef(ctx map[string]interface{}, ref string) interface{} {
|
|
109
|
+
if f, ok := funcMap[ref]; ok {
|
|
110
|
+
return f.(func() string)()
|
|
111
|
+
} else {
|
|
112
|
+
parts := strings.Split(strings.ReplaceAll(ref, "!", ""), ".")
|
|
113
|
+
if len(parts) == 2 {
|
|
114
|
+
if v, ok := ctx[parts[0]]; ok {
|
|
115
|
+
i := reflect.ValueOf(v).Elem().FieldByName(parts[1]).Interface()
|
|
116
|
+
return convert(ref, i)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return convert(ref, ctx[ref])
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func populateChildren(n, replaceNode1 *html.Node) {
|
|
124
|
+
if n.Data == "{children}" { // first
|
|
125
|
+
replaceNode1.NextSibling = &html.Node{}
|
|
126
|
+
*replaceNode1.NextSibling = *n.NextSibling
|
|
127
|
+
n.Parent.FirstChild = replaceNode1
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
if n.NextSibling != nil {
|
|
131
|
+
if n.NextSibling.Data == "{children}" {
|
|
132
|
+
if n.NextSibling.NextSibling != nil { // middle
|
|
133
|
+
replaceNode1.NextSibling = &html.Node{}
|
|
134
|
+
*replaceNode1.NextSibling = *n.NextSibling.NextSibling
|
|
135
|
+
n.NextSibling = replaceNode1
|
|
136
|
+
}
|
|
137
|
+
if n.NextSibling.PrevSibling != nil { // last
|
|
138
|
+
replaceNode1.PrevSibling = &html.Node{}
|
|
139
|
+
*replaceNode1.PrevSibling = *n.NextSibling.PrevSibling
|
|
140
|
+
n.NextSibling = replaceNode1
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
populateChildren(n.NextSibling, replaceNode1)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if n.FirstChild != nil {
|
|
147
|
+
populateChildren(n.FirstChild, replaceNode1)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func populate(ctx Html, n *html.Node) {
|
|
152
|
+
if n.Type == html.TextNode {
|
|
153
|
+
// if n.Data != "" {
|
|
154
|
+
// }
|
|
155
|
+
} else if n.Type == html.ElementNode {
|
|
156
|
+
repr.Println("dd", n.Data)
|
|
157
|
+
for i, at := range n.Attr {
|
|
158
|
+
// if len(param.Value.KV) != 0 {
|
|
159
|
+
// values := []string{}
|
|
160
|
+
// for _, kv := range param.Value.KV {
|
|
161
|
+
// if subsRef(ctx, kv.Value) == true {
|
|
162
|
+
// values = append(values, kv.Key)
|
|
163
|
+
// }
|
|
164
|
+
// }
|
|
165
|
+
if at.Val != "" && strings.Contains(at.Val, "{") {
|
|
166
|
+
if at.Key == "class" {
|
|
167
|
+
repr.Println(at)
|
|
168
|
+
} else {
|
|
169
|
+
re := regexp.MustCompile(`{(.*?)}`)
|
|
170
|
+
found := re.FindString(at.Val)
|
|
171
|
+
if found != "" {
|
|
172
|
+
varName := strings.ReplaceAll(strings.ReplaceAll(found, "{", ""), "}", "")
|
|
173
|
+
varValue := subsRef(ctx, varName).(string)
|
|
174
|
+
n.Attr[i] = html.Attribute{
|
|
175
|
+
Namespace: at.Namespace,
|
|
176
|
+
Key: at.Key,
|
|
177
|
+
Val: strings.ReplaceAll(at.Val, found, varValue),
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if comp, ok := compMap[n.Data]; ok {
|
|
184
|
+
h := Html(ctx)
|
|
185
|
+
args := []reflect.Value{reflect.ValueOf(h)}
|
|
186
|
+
for _, arg := range comp.Args {
|
|
187
|
+
if v, ok := ctx[arg]; ok {
|
|
188
|
+
args = append(args, reflect.ValueOf(v))
|
|
189
|
+
} else {
|
|
190
|
+
v := getAttribute(arg, n.Attr)
|
|
191
|
+
args = append(args, reflect.ValueOf(v))
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
result := reflect.ValueOf(comp.Func).Call(args)
|
|
195
|
+
compNode := result[0].Interface().(Node)
|
|
196
|
+
// html.Render(os.Stdout, componentNode)
|
|
197
|
+
// println("")
|
|
198
|
+
// html.Render(os.Stdout, n)
|
|
199
|
+
// println("")
|
|
200
|
+
if n.FirstChild != nil {
|
|
201
|
+
newChild := &html.Node{}
|
|
202
|
+
*newChild = *n.FirstChild
|
|
203
|
+
newChild.Parent = nil
|
|
204
|
+
n.RemoveChild(n.FirstChild)
|
|
205
|
+
populateChildren(compNode.FirstChild, newChild)
|
|
206
|
+
n.AppendChild(compNode.Node)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
210
|
+
populate(ctx, c)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// func render(x *Xml, ctx map[string]interface{}) string {
|
|
216
|
+
// if x.Value != nil {
|
|
217
|
+
// if x.Value.Ref != "" {
|
|
218
|
+
// s += space + " " + subsRef(ctx, x.Value.Ref).(string) + "\n"
|
|
219
|
+
// } else if x.Value.Str != "" {
|
|
220
|
+
// s += space + " " + strings.ReplaceAll(x.Value.Str, `"`, "") + "\n"
|
|
221
|
+
// }
|
|
222
|
+
// }
|
|
223
|
+
// if x.Name == "For" {
|
|
224
|
+
// ctxKey := getAttribute("key", x.Attributes)
|
|
225
|
+
// ctxName := getAttribute("itemKey", x.Attributes)
|
|
226
|
+
// data := ctx[ctxKey]
|
|
227
|
+
// switch reflect.TypeOf(data).Kind() {
|
|
228
|
+
// case reflect.Slice:
|
|
229
|
+
// v := reflect.ValueOf(data)
|
|
230
|
+
// for i := 0; i < v.Len(); i++ {
|
|
231
|
+
// ctx["_space"] = space + " "
|
|
232
|
+
// ctx[ctxName] = v.Index(i).Interface()
|
|
233
|
+
// s += render(x.Children[0], ctx) + "\n"
|
|
234
|
+
// }
|
|
235
|
+
// }
|
|
236
|
+
// } else {
|
|
237
|
+
// if comp, ok := compMap[x.Name]; ok {
|
|
238
|
+
// } else {
|
|
239
|
+
// found := false
|
|
240
|
+
// for _, t := range htmlTags {
|
|
241
|
+
// if t == x.Name {
|
|
242
|
+
// found = true
|
|
243
|
+
// }
|
|
244
|
+
// }
|
|
245
|
+
// if !found {
|
|
246
|
+
// panic(fmt.Errorf("Comp not found %s", x.Name))
|
|
247
|
+
// }
|
|
248
|
+
// for _, c := range x.Children {
|
|
249
|
+
// ctx["_space"] = space + " "
|
|
250
|
+
// s += render(c, ctx) + "\n"
|
|
251
|
+
// }
|
|
252
|
+
// }
|
|
253
|
+
// }
|
|
254
|
+
// s += space + "</" + x.Name + ">"
|
|
255
|
+
// return s
|
|
256
|
+
// }
|
|
257
|
+
|
|
258
|
+
// <script>
|
|
259
|
+
// document.addEventListener('alpine:init', () => {
|
|
260
|
+
// Alpine.store('todos', {
|
|
261
|
+
// list: [],
|
|
262
|
+
// count: 0,
|
|
263
|
+
// })
|
|
264
|
+
// })
|
|
265
|
+
// </script>
|
|
266
|
+
|
|
267
|
+
// patch: {
|
|
268
|
+
// { "op": "add", "path": "/todos/list", "value": { "id": "123", "text": "123" } },
|
|
269
|
+
// }
|
|
270
|
+
//
|
gsx/gsx_test.go
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
package gsx
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
|
|
6
|
+
"github.com/stretchr/testify/require"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
type TodoData struct {
|
|
10
|
+
ID string
|
|
11
|
+
Text string
|
|
12
|
+
Completed bool
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func Todo(h Html, todo *TodoData) Node {
|
|
16
|
+
return h.Render(`
|
|
17
|
+
<li id="todo-{todo.ID}" class="{ completed: todo.Completed }">
|
|
18
|
+
<div class="view">
|
|
19
|
+
<span>{todo.Text}</span>
|
|
20
|
+
</div>
|
|
21
|
+
{children}
|
|
22
|
+
<div class="count">
|
|
23
|
+
<span>{todo.Completed}</span>
|
|
24
|
+
</div>
|
|
25
|
+
</li>
|
|
26
|
+
`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func WebsiteName() string {
|
|
30
|
+
return "My Website"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func TestHtml(t *testing.T) {
|
|
34
|
+
r := require.New(t)
|
|
35
|
+
RegisterComponent("todo", Todo, "todo")
|
|
36
|
+
RegisterFunc(WebsiteName)
|
|
37
|
+
h := Html(map[string]interface{}{
|
|
38
|
+
"todos": []*TodoData{
|
|
39
|
+
{ID: "b1a7359c-ebb4-11ec-8ea0-0242ac120002", Text: "My first todo", Completed: true},
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
h["todo"] = &TodoData{ID: "b1a7359c-ebb4-11ec-8ea0-0242ac120002", Text: "My first todo", Completed: true}
|
|
43
|
+
// <template x-for="todo in todos">
|
|
44
|
+
actual := h.Render(`
|
|
45
|
+
<div>
|
|
46
|
+
<div>
|
|
47
|
+
123
|
|
48
|
+
<Todo key="todo">
|
|
49
|
+
<div class="container">
|
|
50
|
+
<h2>Title</h2>
|
|
51
|
+
<h3>Sub title</h3>
|
|
52
|
+
</div>
|
|
53
|
+
</Todo>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
Test
|
|
57
|
+
<button>click</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
`).String()
|
|
61
|
+
expected := "<div><div>123<todo key=\"todo\"><li id=\"todo-b1a7359c-ebb4-11ec-8ea0-0242ac120002\" class=\"{ completed: todo.Completed }\"><div class=\"view\"><span>{todo.Text}</span></div><div class=\"container\"><h2>Title</h2><h3>Sub title</h3></div><div class=\"count\"><span>{todo.Completed}</span></div></li></todo></div><div>Test<button>click</button></div></div>"
|
|
62
|
+
// actual := Html(ctx).Render(`
|
|
63
|
+
// <ul id="todo-list" class="relative">
|
|
64
|
+
// <For key="todos" itemKey="todo">
|
|
65
|
+
// <Todo key="todo">
|
|
66
|
+
// <div>"Todo123"</div>
|
|
67
|
+
// </Todo>
|
|
68
|
+
// </For>
|
|
69
|
+
// <span>{WebsiteName}</span>
|
|
70
|
+
// </ul>
|
|
71
|
+
// `)
|
|
72
|
+
// expected := `<ul id="todo-list" class="relative">
|
|
73
|
+
// <For key="todos" itemKey="todo">
|
|
74
|
+
// <Todo key="todo">
|
|
75
|
+
// <li id="b1a7359c-ebb4-11ec-8ea0-0242ac120002" class="completed">
|
|
76
|
+
// <div>
|
|
77
|
+
// Todo123
|
|
78
|
+
// </div>
|
|
79
|
+
// <div class="view">
|
|
80
|
+
// <span>
|
|
81
|
+
// My first todo
|
|
82
|
+
// </span>
|
|
83
|
+
// </div>
|
|
84
|
+
// </li>
|
|
85
|
+
// </Todo>
|
|
86
|
+
// </For>
|
|
87
|
+
// <span>
|
|
88
|
+
// My Website
|
|
89
|
+
// </span>
|
|
90
|
+
// </ul>`
|
|
91
|
+
r.Equal(expected, actual)
|
|
92
|
+
}
|
gsx/parser.go
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
package gsx
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"github.com/alecthomas/participle/v2"
|
|
5
|
-
"github.com/alecthomas/participle/v2/lexer"
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
type Module struct {
|
|
9
|
-
Pos lexer.Position
|
|
10
|
-
Nodes []*Xml `@@`
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
type Attribute struct {
|
|
14
|
-
Pos lexer.Position
|
|
15
|
-
Key string `@":"? @Ident ( @"-" @Ident )*`
|
|
16
|
-
Value *Literal `"=" @@`
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
type KV struct {
|
|
20
|
-
Pos lexer.Position
|
|
21
|
-
Key string `@Ident`
|
|
22
|
-
Value string `":" @"!"? @Ident ( @"." @Ident )*`
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type Literal struct {
|
|
26
|
-
Pos lexer.Position
|
|
27
|
-
Str string `@String`
|
|
28
|
-
Ref string `| "{" @Ident ( @"." @Ident )* "}"`
|
|
29
|
-
KV []*KV `| "{""{" @@* "}""}"`
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
type Xml struct {
|
|
33
|
-
Pos lexer.Position
|
|
34
|
-
Name string `"<" @Ident`
|
|
35
|
-
Attributes []*Attribute `[ @@ { @@ } ]">"`
|
|
36
|
-
Children []*Xml `{ @@ }`
|
|
37
|
-
Value *Literal `{ @@ }` // Todo make this match with @String or Literal
|
|
38
|
-
Close string `("<""/"@Ident">")?`
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
var xmlParser = participle.MustBuild(&Module{})
|
gsx/template.go
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
package gsx
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"fmt"
|
|
5
|
-
"reflect"
|
|
6
|
-
"runtime"
|
|
7
|
-
"strings"
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
type ComponentFunc struct {
|
|
11
|
-
Func interface{}
|
|
12
|
-
Args []string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type Html map[string]interface{}
|
|
16
|
-
|
|
17
|
-
func (h Html) Render(tpl string) string {
|
|
18
|
-
tree := &Module{}
|
|
19
|
-
err := xmlParser.ParseBytes(tpl[0:10], []byte(tpl), tree)
|
|
20
|
-
if err != nil {
|
|
21
|
-
panic(err)
|
|
22
|
-
}
|
|
23
|
-
o := ""
|
|
24
|
-
for _, n := range tree.Nodes {
|
|
25
|
-
v := render(n, h)
|
|
26
|
-
o += v
|
|
27
|
-
}
|
|
28
|
-
return o
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
var htmlTags = []string{"a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "base", "basefont", "bb", "bdo", "big", "blockquote", "body", "br /", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "command", "datagrid", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "eventsource", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame", "frameset", "h1 to <h6>", "head", "header", "hgroup", "hr /", "html", "i", "iframe", "img", "input", "ins", "isindex", "kbd", "keygen", "label", "legend", "li", "link", "map", "mark", "menu", "meta", "meter", "nav", "noframes", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "script", "section", "select", "small", "source", "span", "strike", "strong", "style", "sub", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"}
|
|
32
|
-
var compMap = map[string]ComponentFunc{}
|
|
33
|
-
var funcMap = map[string]interface{}{}
|
|
34
|
-
|
|
35
|
-
func getFunctionName(temp interface{}) string {
|
|
36
|
-
strs := strings.Split((runtime.FuncForPC(reflect.ValueOf(temp).Pointer()).Name()), ".")
|
|
37
|
-
return strs[len(strs)-1]
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
func RegisterComponent(f interface{}, args ...string) {
|
|
41
|
-
name := getFunctionName(f)
|
|
42
|
-
compMap[name] = ComponentFunc{
|
|
43
|
-
Func: f,
|
|
44
|
-
Args: args,
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
func RegisterFunc(f interface{}) {
|
|
49
|
-
name := getFunctionName(f)
|
|
50
|
-
funcMap[name] = f
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
var styles = ""
|
|
54
|
-
|
|
55
|
-
func Css(v string) string {
|
|
56
|
-
styles += v
|
|
57
|
-
return v
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
func GetStyles() string {
|
|
61
|
-
return styles
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
func getAttribute(k string, kvs []*Attribute) string {
|
|
65
|
-
for _, param := range kvs {
|
|
66
|
-
if param.Key == k {
|
|
67
|
-
return strings.ReplaceAll(param.Value.Str, `"`, "")
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return ""
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
func convert(ref string, i interface{}) interface{} {
|
|
74
|
-
switch iv := i.(type) {
|
|
75
|
-
case bool:
|
|
76
|
-
if strings.Contains(ref, "!") {
|
|
77
|
-
return !iv
|
|
78
|
-
} else {
|
|
79
|
-
return iv
|
|
80
|
-
}
|
|
81
|
-
case string:
|
|
82
|
-
return iv
|
|
83
|
-
}
|
|
84
|
-
return nil
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
func subsRef(ctx map[string]interface{}, ref string) interface{} {
|
|
88
|
-
if f, ok := funcMap[ref]; ok {
|
|
89
|
-
return f.(func() string)()
|
|
90
|
-
} else {
|
|
91
|
-
parts := strings.Split(strings.ReplaceAll(ref, "!", ""), ".")
|
|
92
|
-
if len(parts) == 2 {
|
|
93
|
-
if v, ok := ctx[parts[0]]; ok {
|
|
94
|
-
i := reflect.ValueOf(v).Elem().FieldByName(parts[1]).Interface()
|
|
95
|
-
return convert(ref, i)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return convert(ref, ctx[ref])
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
func render(x *Xml, ctx map[string]interface{}) string {
|
|
103
|
-
space, _ := ctx["_space"].(string)
|
|
104
|
-
s := space + "<" + x.Name
|
|
105
|
-
if len(x.Attributes) > 0 {
|
|
106
|
-
s += " "
|
|
107
|
-
}
|
|
108
|
-
for i, param := range x.Attributes {
|
|
109
|
-
if len(param.Value.KV) != 0 {
|
|
110
|
-
values := []string{}
|
|
111
|
-
for _, kv := range param.Value.KV {
|
|
112
|
-
if subsRef(ctx, kv.Value) == true {
|
|
113
|
-
values = append(values, kv.Key)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
s += param.Key + `="` + strings.Join(values, "") + `"`
|
|
117
|
-
} else if param.Value.Ref != "" {
|
|
118
|
-
s += param.Key + `="` + subsRef(ctx, param.Value.Ref).(string) + `"`
|
|
119
|
-
} else {
|
|
120
|
-
s += param.Key + "=" + param.Value.Str
|
|
121
|
-
}
|
|
122
|
-
if i < len(x.Attributes)-1 {
|
|
123
|
-
s += " "
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
s += ">\n"
|
|
127
|
-
if x.Value != nil {
|
|
128
|
-
if x.Value.Ref != "" {
|
|
129
|
-
s += space + " " + subsRef(ctx, x.Value.Ref).(string) + "\n"
|
|
130
|
-
} else if x.Value.Str != "" {
|
|
131
|
-
s += space + " " + strings.ReplaceAll(x.Value.Str, `"`, "") + "\n"
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if x.Name == "For" {
|
|
135
|
-
ctxKey := getAttribute("key", x.Attributes)
|
|
136
|
-
ctxName := getAttribute("itemKey", x.Attributes)
|
|
137
|
-
data := ctx[ctxKey]
|
|
138
|
-
switch reflect.TypeOf(data).Kind() {
|
|
139
|
-
case reflect.Slice:
|
|
140
|
-
v := reflect.ValueOf(data)
|
|
141
|
-
for i := 0; i < v.Len(); i++ {
|
|
142
|
-
ctx["_space"] = space + " "
|
|
143
|
-
ctx[ctxName] = v.Index(i).Interface()
|
|
144
|
-
s += render(x.Children[0], ctx) + "\n"
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
if comp, ok := compMap[x.Name]; ok {
|
|
149
|
-
ctx["_space"] = space + " "
|
|
150
|
-
h := Html(ctx)
|
|
151
|
-
args := []reflect.Value{reflect.ValueOf(h)}
|
|
152
|
-
for _, k := range comp.Args {
|
|
153
|
-
if v, ok := ctx[k]; ok {
|
|
154
|
-
args = append(args, reflect.ValueOf(v))
|
|
155
|
-
} else {
|
|
156
|
-
v := getAttribute(k, x.Attributes)
|
|
157
|
-
args = append(args, reflect.ValueOf(v))
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if len(x.Children) > 0 {
|
|
161
|
-
h["children"] = render(x.Children[0], h)
|
|
162
|
-
}
|
|
163
|
-
result := reflect.ValueOf(comp.Func).Call(args)
|
|
164
|
-
s += result[0].Interface().(string) + "\n"
|
|
165
|
-
} else {
|
|
166
|
-
found := false
|
|
167
|
-
for _, t := range htmlTags {
|
|
168
|
-
if t == x.Name {
|
|
169
|
-
found = true
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if !found {
|
|
173
|
-
panic(fmt.Errorf("Comp not found %s", x.Name))
|
|
174
|
-
}
|
|
175
|
-
for _, c := range x.Children {
|
|
176
|
-
ctx["_space"] = space + " "
|
|
177
|
-
s += render(c, ctx) + "\n"
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
s += space + "</" + x.Name + ">"
|
|
182
|
-
return s
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// <script>
|
|
186
|
-
// document.addEventListener('alpine:init', () => {
|
|
187
|
-
// Alpine.store('todos', {
|
|
188
|
-
// list: [],
|
|
189
|
-
// count: 0,
|
|
190
|
-
// })
|
|
191
|
-
// })
|
|
192
|
-
// </script>
|
|
193
|
-
|
|
194
|
-
// patch: {
|
|
195
|
-
// { "op": "add", "path": "/todos/list", "value": { "id": "123", "text": "123" } },
|
|
196
|
-
// }
|
gsx/template_test.go
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
package gsx
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"testing"
|
|
5
|
-
|
|
6
|
-
"github.com/stretchr/testify/require"
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
type TodoData struct {
|
|
10
|
-
ID string
|
|
11
|
-
Text string
|
|
12
|
-
Completed bool
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
func Todo(h Html, todo *TodoData) string {
|
|
16
|
-
return h.Render(`
|
|
17
|
-
<li id={todo.ID} class={{ completed: todo.Completed }}>
|
|
18
|
-
<div class="view">
|
|
19
|
-
<span>{todo.Text}</span>
|
|
20
|
-
</div>
|
|
21
|
-
{children}
|
|
22
|
-
</li>
|
|
23
|
-
`)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
func WebsiteName() string {
|
|
27
|
-
return "My Website"
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
func TestHtml(t *testing.T) {
|
|
31
|
-
r := require.New(t)
|
|
32
|
-
RegisterComponent(Todo, "todo")
|
|
33
|
-
RegisterFunc(WebsiteName)
|
|
34
|
-
ctx := map[string]interface{}{
|
|
35
|
-
"_space": "",
|
|
36
|
-
"todos": []*TodoData{
|
|
37
|
-
{ID: "b1a7359c-ebb4-11ec-8ea0-0242ac120002", Text: "My first todo", Completed: true},
|
|
38
|
-
},
|
|
39
|
-
}
|
|
40
|
-
actual := Html(ctx).Render(`
|
|
41
|
-
<ul id="todo-list" class="relative">
|
|
42
|
-
<For key="todos" itemKey="todo">
|
|
43
|
-
<Todo key="todo">
|
|
44
|
-
<div>"Todo123"</div>
|
|
45
|
-
</Todo>
|
|
46
|
-
</For>
|
|
47
|
-
<span>{WebsiteName}</span>
|
|
48
|
-
</ul>
|
|
49
|
-
`)
|
|
50
|
-
expected := `<ul id="todo-list" class="relative">
|
|
51
|
-
<For key="todos" itemKey="todo">
|
|
52
|
-
<Todo key="todo">
|
|
53
|
-
<li id="b1a7359c-ebb4-11ec-8ea0-0242ac120002" class="completed">
|
|
54
|
-
<div>
|
|
55
|
-
Todo123
|
|
56
|
-
</div>
|
|
57
|
-
<div class="view">
|
|
58
|
-
<span>
|
|
59
|
-
My first todo
|
|
60
|
-
</span>
|
|
61
|
-
</div>
|
|
62
|
-
</li>
|
|
63
|
-
</Todo>
|
|
64
|
-
</For>
|
|
65
|
-
<span>
|
|
66
|
-
My Website
|
|
67
|
-
</span>
|
|
68
|
-
</ul>`
|
|
69
|
-
r.Equal(expected, actual)
|
|
70
|
-
}
|
lib/main.go
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"bytes"
|
|
5
|
-
"os"
|
|
6
|
-
"strings"
|
|
7
|
-
|
|
8
|
-
"golang.org/x/net/html"
|
|
9
|
-
"golang.org/x/net/html/atom"
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
type HtmlTag struct {
|
|
13
|
-
Tag string
|
|
14
|
-
Text string
|
|
15
|
-
Attr []html.Attribute
|
|
16
|
-
Children []*HtmlTag
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
var contextNode = &html.Node{
|
|
20
|
-
Type: html.ElementNode,
|
|
21
|
-
Data: "div",
|
|
22
|
-
DataAtom: atom.Lookup([]byte("div")),
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
func Html(tpl string) *html.Node {
|
|
26
|
-
newTpl := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(tpl, "\n", ""), "\r", ""), "\t", "")
|
|
27
|
-
doc, err := html.ParseFragment(bytes.NewBuffer([]byte(newTpl)), contextNode)
|
|
28
|
-
if err != nil {
|
|
29
|
-
panic(err)
|
|
30
|
-
}
|
|
31
|
-
return doc[0]
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
func Todo() *html.Node {
|
|
35
|
-
return Html(`
|
|
36
|
-
<li id="{todo.ID}" class="{ completed: todo.Completed }">
|
|
37
|
-
<div class="view">
|
|
38
|
-
<span>{todo.Text}</span>
|
|
39
|
-
</div>
|
|
40
|
-
{children}
|
|
41
|
-
<div class="count">
|
|
42
|
-
<span>{todo.Count}</span>
|
|
43
|
-
</div>
|
|
44
|
-
</li>
|
|
45
|
-
`)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
func walkChildren(n, replaceNode1 *html.Node) {
|
|
49
|
-
if n.Data == "{children}" { // first
|
|
50
|
-
replaceNode1.NextSibling = &html.Node{}
|
|
51
|
-
*replaceNode1.NextSibling = *n.NextSibling
|
|
52
|
-
n.Parent.FirstChild = replaceNode1
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
if n.NextSibling != nil {
|
|
56
|
-
if n.NextSibling.Data == "{children}" {
|
|
57
|
-
if n.NextSibling.NextSibling != nil { // middle
|
|
58
|
-
replaceNode1.NextSibling = &html.Node{}
|
|
59
|
-
*replaceNode1.NextSibling = *n.NextSibling.NextSibling
|
|
60
|
-
n.NextSibling = replaceNode1
|
|
61
|
-
}
|
|
62
|
-
if n.NextSibling.PrevSibling != nil { // last
|
|
63
|
-
replaceNode1.PrevSibling = &html.Node{}
|
|
64
|
-
*replaceNode1.PrevSibling = *n.NextSibling.PrevSibling
|
|
65
|
-
n.NextSibling = replaceNode1
|
|
66
|
-
}
|
|
67
|
-
} else {
|
|
68
|
-
walkChildren(n.NextSibling, replaceNode1)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if n.FirstChild != nil {
|
|
72
|
-
walkChildren(n.FirstChild, replaceNode1)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
func main() {
|
|
77
|
-
// ctx := map[string]interface{}{}
|
|
78
|
-
doc := Html(`
|
|
79
|
-
<div>
|
|
80
|
-
<div>
|
|
81
|
-
123
|
|
82
|
-
<Todo id={todo.ID} class="{ completed: todo.Completed }">
|
|
83
|
-
<div class="container">
|
|
84
|
-
<h2>Title</h2>
|
|
85
|
-
<h3>Sub title</h3>
|
|
86
|
-
</div>
|
|
87
|
-
</Todo>
|
|
88
|
-
</div>
|
|
89
|
-
<div>
|
|
90
|
-
Test
|
|
91
|
-
<button>click</button>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
`)
|
|
95
|
-
top := &HtmlTag{}
|
|
96
|
-
var f func(parent *HtmlTag, n *html.Node)
|
|
97
|
-
f = func(parent *HtmlTag, n *html.Node) {
|
|
98
|
-
if n.Type == html.TextNode {
|
|
99
|
-
data := strings.TrimSpace(n.Data)
|
|
100
|
-
if data != "" {
|
|
101
|
-
// data = "{}"
|
|
102
|
-
}
|
|
103
|
-
} else if n.Type == html.ElementNode {
|
|
104
|
-
// repr.Println(n.Data)
|
|
105
|
-
if n.Data == "todo" {
|
|
106
|
-
componentNode := Todo()
|
|
107
|
-
html.Render(os.Stdout, componentNode)
|
|
108
|
-
println("")
|
|
109
|
-
html.Render(os.Stdout, n)
|
|
110
|
-
println("")
|
|
111
|
-
if n.FirstChild != nil {
|
|
112
|
-
newChild := &html.Node{}
|
|
113
|
-
*newChild = *n.FirstChild
|
|
114
|
-
newChild.Parent = nil
|
|
115
|
-
n.RemoveChild(n.FirstChild)
|
|
116
|
-
walkChildren(componentNode.FirstChild, newChild)
|
|
117
|
-
n.AppendChild(componentNode)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// newParent := &HtmlTag{
|
|
121
|
-
// Tag: n.Data,
|
|
122
|
-
// Attr: n.Attr,
|
|
123
|
-
// }
|
|
124
|
-
// parent.Children = append(parent.Children, newParent)
|
|
125
|
-
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
126
|
-
f(nil, c)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
f(top, doc)
|
|
131
|
-
// repr.Println(top)
|
|
132
|
-
html.Render(os.Stdout, doc)
|
|
133
|
-
}
|