~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.
cb14a77d
—
Peter John 3 years ago
new parser
- go.mod +8 -7
- go.sum +8 -4
- gsx/context.go +84 -0
- gsx/gsx.go +104 -280
- gsx/gsx_test.go +134 -134
- gsx/parser.go +173 -0
- gsx/parser_test.go +46 -0
- http.go +14 -2
go.mod
CHANGED
|
@@ -3,34 +3,35 @@ module github.com/pyros2097/gromer
|
|
|
3
3
|
go 1.18
|
|
4
4
|
|
|
5
5
|
require (
|
|
6
|
+
github.com/alecthomas/participle/v2 v2.0.0-beta.3
|
|
7
|
+
github.com/alecthomas/repr v0.1.0
|
|
8
|
+
github.com/felixge/httpsnoop v1.0.1
|
|
9
|
+
github.com/go-errors/errors v1.4.2
|
|
6
10
|
github.com/go-playground/validator/v10 v10.9.0
|
|
7
11
|
github.com/gobuffalo/velvet v0.0.0-20170320144106-d97471bf5d8f
|
|
12
|
+
github.com/goneric/stack v0.0.0-20220131052059-5990ae324dbd
|
|
8
13
|
github.com/google/uuid v1.3.0
|
|
14
|
+
github.com/gorilla/handlers v1.5.1
|
|
9
15
|
github.com/gorilla/mux v1.8.0
|
|
10
16
|
github.com/imdario/mergo v0.3.12
|
|
17
|
+
github.com/jinzhu/copier v0.3.5
|
|
11
18
|
github.com/rs/zerolog v1.26.1
|
|
12
19
|
github.com/segmentio/go-camelcase v0.0.0-20160726192923-7085f1e3c734
|
|
13
20
|
github.com/stretchr/testify v1.7.0
|
|
14
21
|
gocloud.dev v0.24.0
|
|
15
22
|
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57
|
|
16
|
-
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f
|
|
17
23
|
xojoc.pw/useragent v0.0.0-20200116211053-1ec61d55e8fe
|
|
18
24
|
)
|
|
19
25
|
|
|
20
26
|
require (
|
|
21
27
|
cloud.google.com/go v0.94.0 // indirect
|
|
22
28
|
cloud.google.com/go/firestore v1.5.0 // indirect
|
|
23
|
-
github.com/JoshVarga/svgparser v0.0.0-20200804023048-5eaba627a7d1 // indirect
|
|
24
|
-
github.com/alecthomas/repr v0.1.0 // indirect
|
|
25
29
|
github.com/aymerick/douceur v0.2.0 // indirect
|
|
26
30
|
github.com/aymerick/raymond v2.0.2+incompatible // indirect
|
|
27
31
|
github.com/blang/semver v3.5.1+incompatible // indirect
|
|
28
32
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
29
|
-
github.com/felixge/httpsnoop v1.0.1 // indirect
|
|
30
|
-
github.com/go-errors/errors v1.4.2 // indirect
|
|
31
33
|
github.com/go-playground/locales v0.14.0 // indirect
|
|
32
34
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
|
33
|
-
github.com/go-stack/stack v1.8.1 // indirect
|
|
34
35
|
github.com/gobuffalo/envy v1.6.5 // indirect
|
|
35
36
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
|
36
37
|
github.com/golang/protobuf v1.5.2 // indirect
|
|
@@ -38,7 +39,6 @@ require (
|
|
|
38
39
|
github.com/google/wire v0.5.0 // indirect
|
|
39
40
|
github.com/googleapis/gax-go/v2 v2.1.0 // indirect
|
|
40
41
|
github.com/gorilla/css v1.0.0 // indirect
|
|
41
|
-
github.com/gorilla/handlers v1.5.1 // indirect
|
|
42
42
|
github.com/joho/godotenv v1.3.0 // indirect
|
|
43
43
|
github.com/leodido/go-urn v1.2.1 // indirect
|
|
44
44
|
github.com/markbates/inflect v1.0.4 // indirect
|
|
@@ -58,6 +58,7 @@ require (
|
|
|
58
58
|
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect
|
|
59
59
|
go.opencensus.io v0.23.0 // indirect
|
|
60
60
|
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e // indirect
|
|
61
|
+
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
|
|
61
62
|
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
|
|
62
63
|
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect
|
|
63
64
|
golang.org/x/text v0.3.7 // indirect
|
go.sum
CHANGED
|
@@ -98,9 +98,10 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp
|
|
|
98
98
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
|
99
99
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
|
100
100
|
github.com/GoogleCloudPlatform/cloudsql-proxy v1.24.0/go.mod h1:3tx938GhY4FC+E1KT/jNjDw7Z5qxAEtIiERJ2sXjnII=
|
|
101
|
-
github.com/JoshVarga/svgparser v0.0.0-20200804023048-5eaba627a7d1 h1:RAQocNl+YQYGPt5yh4SR5zFUIHKrXnLhjIGhHO4Vwnc=
|
|
102
|
-
github.com/JoshVarga/svgparser v0.0.0-20200804023048-5eaba627a7d1/go.mod h1:tMmgUTWcco9d1ZmK7zjxuTv7XWZhyutXIsgu0uJ3gDw=
|
|
103
101
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
|
102
|
+
github.com/alecthomas/assert/v2 v2.0.3 h1:WKqJODfOiQG0nEJKFKzDIG3E29CN2/4zR9XGJzKIkbg=
|
|
103
|
+
github.com/alecthomas/participle/v2 v2.0.0-beta.3 h1:9HnyNuDsqOG8sl63Dz+KubqHhU8aWqsrjKdecim8GW0=
|
|
104
|
+
github.com/alecthomas/participle/v2 v2.0.0-beta.3/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM=
|
|
104
105
|
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
|
|
105
106
|
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
|
106
107
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
|
@@ -191,8 +192,6 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
|
|
|
191
192
|
github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A=
|
|
192
193
|
github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
|
193
194
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
|
194
|
-
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
|
195
|
-
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
|
196
195
|
github.com/gobuffalo/envy v1.6.5 h1:X3is06x7v0nW2xiy2yFbbIjwHz57CD6z6MkvqULTCm8=
|
|
197
196
|
github.com/gobuffalo/envy v1.6.5/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
|
|
198
197
|
github.com/gobuffalo/velvet v0.0.0-20170320144106-d97471bf5d8f h1:ddIdPdlkAgKMB0mbkft2LT3oxN1h3MN1fopCFrOgkhY=
|
|
@@ -237,6 +236,8 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx
|
|
|
237
236
|
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
|
238
237
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
239
238
|
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
239
|
+
github.com/goneric/stack v0.0.0-20220131052059-5990ae324dbd h1:Lk3pfbigof4g+Yet90PBZ9lLcSxL7+oi/mhRs6Jxeg0=
|
|
240
|
+
github.com/goneric/stack v0.0.0-20220131052059-5990ae324dbd/go.mod h1:mqMZDjOyTS46kiMNNDel6ZgPVru04QgAZr3wBmb+bnE=
|
|
240
241
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
241
242
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
242
243
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
|
@@ -302,10 +303,13 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
|
|
302
303
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
|
303
304
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
|
304
305
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
|
306
|
+
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
|
305
307
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
|
306
308
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
|
307
309
|
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
|
308
310
|
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
|
311
|
+
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
|
|
312
|
+
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
|
309
313
|
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
|
310
314
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
|
311
315
|
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
gsx/context.go
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package gsx
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
|
|
6
|
+
"github.com/jinzhu/copier"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
type HX struct {
|
|
10
|
+
Boosted bool
|
|
11
|
+
CurrentUrl string
|
|
12
|
+
Prompt string
|
|
13
|
+
Target string
|
|
14
|
+
TriggerName string
|
|
15
|
+
TriggerID string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Context struct {
|
|
19
|
+
context.Context
|
|
20
|
+
hx *HX
|
|
21
|
+
data M
|
|
22
|
+
meta M
|
|
23
|
+
links map[string]link
|
|
24
|
+
scripts map[string]bool
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func NewContext(c context.Context, hx *HX) *Context {
|
|
28
|
+
return &Context{
|
|
29
|
+
Context: c,
|
|
30
|
+
hx: hx,
|
|
31
|
+
data: M{},
|
|
32
|
+
meta: M{},
|
|
33
|
+
links: map[string]link{},
|
|
34
|
+
scripts: map[string]bool{},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func (c *Context) HX(k string) *HX {
|
|
39
|
+
return c.hx
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func (c *Context) Get(k string) interface{} {
|
|
43
|
+
return c.data[k]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func (c *Context) Set(k string, v interface{}) {
|
|
47
|
+
c.data[k] = v
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func (c *Context) Meta(meta M) {
|
|
51
|
+
c.meta = meta
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func (c *Context) AddMeta(k, v string) {
|
|
55
|
+
c.meta[k] = v
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func (c *Context) Link(rel, href, t, as string) {
|
|
59
|
+
c.links[href] = link{rel, href, t, as}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func (c *Context) Script(src string, sdefer bool) {
|
|
63
|
+
c.scripts[src] = sdefer
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func (c *Context) Data(data M) {
|
|
67
|
+
c.data = data
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func (c *Context) Render(tpl string) []*Tag {
|
|
71
|
+
name, _ := c.Get("funcName").(string)
|
|
72
|
+
tags := parse(name, tpl)
|
|
73
|
+
return populate(c, tags)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func (c *Context) Clone(name string) *Context {
|
|
77
|
+
clone := NewContext(c.Context, c.hx)
|
|
78
|
+
err := copier.Copy(clone, c)
|
|
79
|
+
if err != nil {
|
|
80
|
+
panic("Failed to copy")
|
|
81
|
+
}
|
|
82
|
+
c.Set("funcName", name)
|
|
83
|
+
return clone
|
|
84
|
+
}
|
gsx/gsx.go
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
package gsx
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"bytes"
|
|
5
|
-
"context"
|
|
6
4
|
"fmt"
|
|
7
5
|
"io"
|
|
8
6
|
"reflect"
|
|
@@ -10,22 +8,15 @@ import (
|
|
|
10
8
|
"runtime"
|
|
11
9
|
"strconv"
|
|
12
10
|
"strings"
|
|
13
|
-
|
|
14
|
-
"golang.org/x/net/html"
|
|
15
|
-
"golang.org/x/net/html/atom"
|
|
16
11
|
)
|
|
17
12
|
|
|
18
13
|
var (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
compMap = map[string]ComponentFunc{}
|
|
26
|
-
funcMap = map[string]interface{}{}
|
|
27
|
-
classesMap = map[string]M{}
|
|
28
|
-
refRegex = regexp.MustCompile(`{(.*?)}`)
|
|
14
|
+
htmlElements = []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"}
|
|
15
|
+
voidElements = []string{"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"}
|
|
16
|
+
compMap = map[string]ComponentFunc{}
|
|
17
|
+
funcMap = map[string]interface{}{}
|
|
18
|
+
classesMap = map[string]M{}
|
|
19
|
+
refRegex = regexp.MustCompile(`{(.*?)}`)
|
|
29
20
|
)
|
|
30
21
|
|
|
31
22
|
type (
|
|
@@ -43,70 +34,55 @@ type (
|
|
|
43
34
|
Type string
|
|
44
35
|
As string
|
|
45
36
|
}
|
|
46
|
-
Context struct {
|
|
47
|
-
context.Context
|
|
48
|
-
hxRequest bool
|
|
49
|
-
data M
|
|
50
|
-
meta M
|
|
51
|
-
links map[string]link
|
|
52
|
-
scripts map[string]bool
|
|
53
|
-
}
|
|
54
|
-
Node struct {
|
|
55
|
-
html.Node
|
|
56
|
-
}
|
|
57
37
|
)
|
|
58
38
|
|
|
59
|
-
func
|
|
39
|
+
func RegisterComponent(f interface{}, classes M, args ...string) {
|
|
40
|
+
name := getFunctionName(f)
|
|
60
|
-
|
|
41
|
+
compMap[name] = ComponentFunc{
|
|
61
|
-
|
|
42
|
+
Func: f,
|
|
62
|
-
hxRequest: hxRequest,
|
|
63
|
-
|
|
43
|
+
Args: args,
|
|
64
|
-
|
|
44
|
+
Classes: classes,
|
|
65
|
-
links: map[string]link{},
|
|
66
|
-
scripts: map[string]bool{},
|
|
67
45
|
}
|
|
68
46
|
}
|
|
69
47
|
|
|
70
|
-
func (
|
|
48
|
+
func RegisterFunc(f interface{}) {
|
|
71
|
-
return c.data[k]
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
49
|
+
name := getFunctionName(f)
|
|
75
|
-
|
|
50
|
+
funcMap[name] = f
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
func (c *Context) Meta(meta M) {
|
|
79
|
-
c.meta = meta
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
func (c *Context) AddMeta(k, v string) {
|
|
83
|
-
c.meta[k] = v
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
func (c *Context) Link(rel, href, t, as string) {
|
|
87
|
-
c.links[href] = link{rel, href, t, as}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
func (c *Context) Script(src string, sdefer bool) {
|
|
91
|
-
c.scripts[src] = sdefer
|
|
92
51
|
}
|
|
93
52
|
|
|
94
|
-
func (
|
|
53
|
+
func getFunctionName(temp interface{}) string {
|
|
54
|
+
strs := strings.Split((runtime.FuncForPC(reflect.ValueOf(temp).Pointer()).Name()), ".")
|
|
95
|
-
|
|
55
|
+
return strs[len(strs)-1]
|
|
96
56
|
}
|
|
97
57
|
|
|
98
|
-
func (
|
|
58
|
+
func (comp ComponentFunc) Render(c *Context, tag *Tag) []*Tag {
|
|
59
|
+
args := []reflect.Value{reflect.ValueOf(c)}
|
|
60
|
+
funcType := reflect.TypeOf(comp.Func)
|
|
61
|
+
for i, arg := range comp.Args {
|
|
62
|
+
if v, ok := c.data[arg]; ok {
|
|
63
|
+
args = append(args, reflect.ValueOf(v))
|
|
64
|
+
} else {
|
|
65
|
+
v := findAttribute(tag.Attributes, arg)
|
|
66
|
+
t := funcType.In(i + 1)
|
|
67
|
+
switch t.Kind() {
|
|
68
|
+
case reflect.Int:
|
|
99
|
-
|
|
69
|
+
value, _ := strconv.Atoi(*v.Value.Str)
|
|
100
|
-
|
|
70
|
+
args = append(args, reflect.ValueOf(value))
|
|
101
|
-
|
|
71
|
+
case reflect.Bool:
|
|
72
|
+
value, _ := strconv.ParseBool(*v.Value.Str)
|
|
73
|
+
args = append(args, reflect.ValueOf(value))
|
|
102
|
-
|
|
74
|
+
default:
|
|
75
|
+
args = append(args, reflect.ValueOf(v))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
103
78
|
}
|
|
79
|
+
result := reflect.ValueOf(comp.Func).Call(args)
|
|
80
|
+
tags := result[0].Interface().([]*Tag)
|
|
104
|
-
populate(c,
|
|
81
|
+
return populate(c, tags)
|
|
105
|
-
return &Node{*doc[0]}
|
|
106
82
|
}
|
|
107
83
|
|
|
108
|
-
func
|
|
84
|
+
func Write(c *Context, w io.Writer, tags []*Tag) {
|
|
109
|
-
if
|
|
85
|
+
if c.hx == nil {
|
|
110
86
|
w.Write([]byte(`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">`))
|
|
111
87
|
w.Write([]byte(`<meta http-equiv="Content-Type" content="text/html;charset=utf-8"><meta content="utf-8" http-equiv="encoding">`))
|
|
112
88
|
w.Write([]byte(`<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover">`))
|
|
@@ -134,30 +110,13 @@ func (n *Node) Write(c *Context, w io.Writer) {
|
|
|
134
110
|
}
|
|
135
111
|
w.Write([]byte(`</head><body>`))
|
|
136
112
|
}
|
|
137
|
-
|
|
113
|
+
out := renderString(tags)
|
|
114
|
+
w.Write([]byte(out))
|
|
138
|
-
if
|
|
115
|
+
if c.hx == nil {
|
|
139
116
|
w.Write([]byte(`</body></html>`))
|
|
140
117
|
}
|
|
141
118
|
}
|
|
142
119
|
|
|
143
|
-
func (n *Node) String() string {
|
|
144
|
-
b := bytes.NewBuffer(nil)
|
|
145
|
-
html.Render(b, &n.Node)
|
|
146
|
-
return b.String()
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
func stripWhitespace(s string) string {
|
|
150
|
-
return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(s, "\n", ""), "\r", ""), "\t", "")
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
func assertName(t, name string) {
|
|
154
|
-
for _, v := range htmlTags {
|
|
155
|
-
if name == v {
|
|
156
|
-
panic(fmt.Sprintf("%s '%s' name cannot be the same as a html tag", t, name))
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
120
|
func SetClasses(k string, m M) {
|
|
162
121
|
classesMap[k] = m
|
|
163
122
|
}
|
|
@@ -176,36 +135,6 @@ func GetComponentStyles() string {
|
|
|
176
135
|
return css
|
|
177
136
|
}
|
|
178
137
|
|
|
179
|
-
func RegisterComponent(f interface{}, classes M, args ...string) {
|
|
180
|
-
name := strings.ToLower(getFunctionName(f))
|
|
181
|
-
assertName("component", name)
|
|
182
|
-
compMap[name] = ComponentFunc{
|
|
183
|
-
Func: f,
|
|
184
|
-
Args: args,
|
|
185
|
-
Classes: classes,
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
func RegisterFunc(f interface{}) {
|
|
190
|
-
name := getFunctionName(f)
|
|
191
|
-
assertName("function", name)
|
|
192
|
-
funcMap[name] = f
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
func getFunctionName(temp interface{}) string {
|
|
196
|
-
strs := strings.Split((runtime.FuncForPC(reflect.ValueOf(temp).Pointer()).Name()), ".")
|
|
197
|
-
return strs[len(strs)-1]
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
func getAttribute(k string, kvs []html.Attribute) string {
|
|
201
|
-
for _, v := range kvs {
|
|
202
|
-
if v.Key == k {
|
|
203
|
-
return v.Val
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return ""
|
|
207
|
-
}
|
|
208
|
-
|
|
209
138
|
func convert(ref string, i interface{}) interface{} {
|
|
210
139
|
switch iv := i.(type) {
|
|
211
140
|
case bool:
|
|
@@ -248,193 +177,88 @@ func getRefValue(c *Context, ref string) interface{} {
|
|
|
248
177
|
}
|
|
249
178
|
}
|
|
250
179
|
|
|
251
|
-
func removeBrackets(v string) string {
|
|
252
|
-
return strings.ReplaceAll(strings.ReplaceAll(v, "{", ""), "}", "")
|
|
253
|
-
}
|
|
254
|
-
|
|
255
180
|
func substituteString(c *Context, v string) string {
|
|
256
181
|
found := refRegex.FindString(v)
|
|
257
182
|
if found != "" {
|
|
258
|
-
varName := removeBrackets(found)
|
|
259
|
-
varValue := fmt.Sprintf("%v", getRefValue(c,
|
|
183
|
+
varValue := fmt.Sprintf("%v", getRefValue(c, found))
|
|
260
184
|
return strings.ReplaceAll(v, found, varValue)
|
|
261
185
|
}
|
|
262
186
|
return v
|
|
263
187
|
}
|
|
264
188
|
|
|
265
|
-
func
|
|
189
|
+
func populate(c *Context, tags []*Tag) []*Tag {
|
|
266
|
-
if n.Data == "{children}" { // first
|
|
267
|
-
replaceNode1.NextSibling = &html.Node{}
|
|
268
|
-
*replaceNode1.NextSibling = *n.NextSibling
|
|
269
|
-
n.Parent.FirstChild = replaceNode1
|
|
270
|
-
return
|
|
271
|
-
}
|
|
272
|
-
|
|
190
|
+
for _, t := range tags {
|
|
273
|
-
if n.NextSibling.Data == "{children}" {
|
|
274
|
-
if n.NextSibling.NextSibling != nil { // middle
|
|
275
|
-
replaceNode1.NextSibling = &html.Node{}
|
|
276
|
-
*replaceNode1.NextSibling = *n.NextSibling.NextSibling
|
|
277
|
-
n.NextSibling = replaceNode1
|
|
278
|
-
}
|
|
279
|
-
if n.NextSibling.PrevSibling != nil { // last
|
|
280
|
-
replaceNode1.PrevSibling = &html.Node{}
|
|
281
|
-
*replaceNode1.PrevSibling = *n.NextSibling.PrevSibling
|
|
282
|
-
n.NextSibling = replaceNode1
|
|
283
|
-
}
|
|
284
|
-
} else {
|
|
285
|
-
populateChildren(n.NextSibling, replaceNode1)
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
if n.FirstChild != nil {
|
|
289
|
-
|
|
191
|
+
populateTag(c, t)
|
|
290
192
|
}
|
|
193
|
+
return tags
|
|
291
194
|
}
|
|
292
195
|
|
|
293
|
-
func cloneNode(n *html.Node) *html.Node {
|
|
294
|
-
attrs := []html.Attribute{}
|
|
295
|
-
for _, v := range n.Attr {
|
|
296
|
-
attrs = append(attrs, html.Attribute{
|
|
297
|
-
Key: v.Key,
|
|
298
|
-
Val: v.Val,
|
|
299
|
-
})
|
|
300
|
-
}
|
|
301
|
-
newNode := &html.Node{
|
|
302
|
-
Type: n.Type,
|
|
303
|
-
Data: n.Data,
|
|
304
|
-
DataAtom: n.DataAtom,
|
|
305
|
-
Attr: attrs,
|
|
306
|
-
}
|
|
307
|
-
if n.FirstChild != nil {
|
|
308
|
-
newNode.FirstChild = cloneNode(n.FirstChild)
|
|
309
|
-
}
|
|
310
|
-
if n.NextSibling != nil {
|
|
311
|
-
newNode.NextSibling = cloneNode(n.NextSibling)
|
|
312
|
-
}
|
|
313
|
-
return newNode
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
func
|
|
196
|
+
func populateTag(c *Context, tag *Tag) {
|
|
317
|
-
if
|
|
197
|
+
if tag.Name == "" {
|
|
318
|
-
if
|
|
198
|
+
if tag.Text.Ref != nil && *tag.Text.Ref != "children" {
|
|
319
|
-
|
|
199
|
+
*tag.Text.Ref = substituteString(c, *tag.Text.Ref)
|
|
320
200
|
}
|
|
321
|
-
} else
|
|
201
|
+
} else {
|
|
322
|
-
for
|
|
202
|
+
for _, a := range tag.Attributes {
|
|
323
|
-
if
|
|
203
|
+
if a.Key == "x-for" {
|
|
324
|
-
xfor := getAttribute("x-for", n.Attr)
|
|
325
|
-
arr := strings.Split(
|
|
204
|
+
arr := strings.Split(*a.Value.Str, " in ")
|
|
326
|
-
ctxItemKey := arr[0]
|
|
205
|
+
// ctxItemKey := arr[0]
|
|
327
206
|
ctxKey := arr[1]
|
|
328
207
|
data := c.data[ctxKey]
|
|
329
208
|
switch reflect.TypeOf(data).Kind() {
|
|
330
209
|
case reflect.Slice:
|
|
331
210
|
v := reflect.ValueOf(data)
|
|
332
|
-
if v.Len() == 0 {
|
|
333
|
-
if n.FirstChild != nil {
|
|
334
|
-
n.RemoveChild(n.FirstChild)
|
|
335
|
-
}
|
|
336
|
-
continue
|
|
337
|
-
}
|
|
338
|
-
if n.FirstChild == nil {
|
|
339
|
-
continue
|
|
340
|
-
}
|
|
341
|
-
firstChild := cloneNode(n.FirstChild)
|
|
342
|
-
n.RemoveChild(n.FirstChild)
|
|
343
211
|
for i := 0; i < v.Len(); i++ {
|
|
212
|
+
// ctx["_space"] = space + " "
|
|
213
|
+
// ctx[ctxName] = v.Index(i).Interface()
|
|
214
|
+
// s += render(x.Children[0], ctx) + "\n"
|
|
215
|
+
|
|
344
|
-
compCtx := &Context{
|
|
216
|
+
// compCtx := &Context{
|
|
345
|
-
|
|
217
|
+
// Context: c.Context,
|
|
346
|
-
|
|
218
|
+
// data: map[string]interface{}{
|
|
347
|
-
|
|
219
|
+
// ctxItemKey: v.Index(i).Interface(),
|
|
348
|
-
|
|
220
|
+
// },
|
|
349
|
-
}
|
|
221
|
+
// }
|
|
350
|
-
|
|
222
|
+
// tag.Children
|
|
351
|
-
itemChild.Parent = nil
|
|
352
|
-
if comp, ok := compMap[itemChild.Data]; ok {
|
|
223
|
+
// if comp, ok := compMap[itemChild.Data]; ok {
|
|
353
|
-
|
|
224
|
+
// newNode := populateComponent(compCtx, comp, itemChild, false)
|
|
354
|
-
|
|
225
|
+
// n.AppendChild(newNode)
|
|
355
|
-
} else {
|
|
226
|
+
// } else {
|
|
356
|
-
|
|
227
|
+
// n.AppendChild(itemChild)
|
|
357
|
-
|
|
228
|
+
// populate(compCtx, itemChild)
|
|
358
|
-
}
|
|
229
|
+
// }
|
|
359
230
|
}
|
|
360
231
|
}
|
|
361
|
-
} else if
|
|
232
|
+
} else if a.Value.Ref != nil {
|
|
362
|
-
if
|
|
233
|
+
if a.Key == "class" {
|
|
363
|
-
classes := []string{}
|
|
234
|
+
// classes := []string{}
|
|
364
|
-
kvstrings := strings.Split(strings.TrimSpace(
|
|
235
|
+
// kvstrings := strings.Split(strings.TrimSpace(at.Val), ",")
|
|
365
|
-
for _, kv := range kvstrings {
|
|
236
|
+
// for _, kv := range kvstrings {
|
|
366
|
-
|
|
237
|
+
// kvarray := strings.Split(kv, ":")
|
|
367
|
-
|
|
238
|
+
// k := strings.TrimSpace(kvarray[0])
|
|
368
|
-
|
|
239
|
+
// v := strings.TrimSpace(kvarray[1])
|
|
369
|
-
|
|
240
|
+
// varValue := getRefValue(c, v)
|
|
370
|
-
|
|
241
|
+
// if varValue.(bool) {
|
|
371
|
-
|
|
242
|
+
// classes = append(classes, k)
|
|
243
|
+
// }
|
|
372
|
-
|
|
244
|
+
// }
|
|
373
|
-
}
|
|
374
|
-
n.Attr[i] = html.Attribute{
|
|
245
|
+
// n.Attr[i] = html.Attribute{
|
|
375
|
-
|
|
246
|
+
// Namespace: at.Namespace,
|
|
376
|
-
|
|
247
|
+
// Key: at.Key,
|
|
377
|
-
|
|
248
|
+
// Val: strings.Join(classes, " "),
|
|
378
|
-
}
|
|
249
|
+
// }
|
|
379
250
|
} else {
|
|
380
|
-
n.Attr[i] = html.Attribute{
|
|
381
|
-
Namespace: at.Namespace,
|
|
382
|
-
Key: at.Key,
|
|
383
|
-
|
|
251
|
+
subs := substituteString(c, *a.Value.Ref)
|
|
384
|
-
}
|
|
252
|
+
a.Value = &Literal{Str: &subs}
|
|
385
253
|
}
|
|
386
254
|
}
|
|
387
255
|
}
|
|
388
|
-
for child := n.FirstChild; child != nil; child = child.NextSibling {
|
|
389
|
-
populate(c, child)
|
|
390
|
-
}
|
|
391
|
-
if comp, ok := compMap[
|
|
256
|
+
if comp, ok := compMap[tag.Name]; ok {
|
|
392
|
-
newNode := populateComponent(c, comp, n, true)
|
|
393
|
-
n.AppendChild(newNode)
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
func renderComponent(c *Context, comp ComponentFunc, n *html.Node) *Node {
|
|
399
|
-
args := []reflect.Value{reflect.ValueOf(c)}
|
|
400
|
-
funcType := reflect.TypeOf(comp.Func)
|
|
401
|
-
for i, arg := range comp.Args {
|
|
402
|
-
if v, ok := c.data[arg]; ok {
|
|
403
|
-
args = append(args, reflect.ValueOf(v))
|
|
404
|
-
} else {
|
|
405
|
-
v := getAttribute(arg, n.Attr)
|
|
406
|
-
t := funcType.In(i + 1)
|
|
407
|
-
switch t.Kind() {
|
|
408
|
-
case reflect.Int:
|
|
409
|
-
value, _ := strconv.Atoi(v)
|
|
410
|
-
args = append(args, reflect.ValueOf(value))
|
|
411
|
-
case reflect.Bool:
|
|
412
|
-
value, _ := strconv.ParseBool(v)
|
|
413
|
-
args = append(args, reflect.ValueOf(value))
|
|
414
|
-
default:
|
|
415
|
-
args = append(args, reflect.ValueOf(v))
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
result := reflect.ValueOf(comp.Func).Call(args)
|
|
420
|
-
|
|
257
|
+
compContext := c.Clone(tag.Name)
|
|
421
|
-
return compNode
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
func populateComponent(c *Context, comp ComponentFunc, n *html.Node, remove bool) *html.Node {
|
|
425
|
-
|
|
258
|
+
nodes := comp.Render(compContext, tag)
|
|
426
|
-
if n.FirstChild != nil {
|
|
427
|
-
|
|
259
|
+
// TODO: check if tag as Children already and {children} in defined in component
|
|
428
|
-
|
|
260
|
+
tag.Children = nodes
|
|
429
|
-
if n.FirstChild != nil && remove {
|
|
430
|
-
n.RemoveChild(n.FirstChild)
|
|
431
|
-
}
|
|
432
|
-
if !remove {
|
|
433
|
-
populate(c, newChild)
|
|
434
|
-
}
|
|
435
|
-
if compNode.FirstChild != nil {
|
|
436
|
-
populateChildren(compNode.FirstChild, newChild)
|
|
437
261
|
}
|
|
262
|
+
populate(c, tag.Children)
|
|
438
263
|
}
|
|
439
|
-
return &compNode.Node
|
|
440
264
|
}
|
gsx/gsx_test.go
CHANGED
|
@@ -12,8 +12,8 @@ type TodoData struct {
|
|
|
12
12
|
Completed bool
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
func Todo(
|
|
15
|
+
func Todo(c *Context, todo *TodoData) []*Tag {
|
|
16
|
-
return
|
|
16
|
+
return c.Render(`
|
|
17
17
|
<li id="todo-{todo.ID}" class="{ completed: todo.Completed }">
|
|
18
18
|
<div class="upper">
|
|
19
19
|
<span>{todo.Text}</span>
|
|
@@ -28,20 +28,20 @@ func Todo(h Context, todo *TodoData) *Node {
|
|
|
28
28
|
`)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
func TodoList(ctx Context, todos []*TodoData)
|
|
31
|
+
func TodoList(ctx Context, todos []*TodoData) []*Tag {
|
|
32
32
|
return ctx.Render(`
|
|
33
33
|
<ul id="todo-list" class="relative" x-for="todo in todos">
|
|
34
34
|
<Todo />
|
|
35
35
|
</ul>
|
|
36
|
-
`)
|
|
36
|
+
`)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
func TodoCount(ctx Context, count int)
|
|
39
|
+
func TodoCount(ctx Context, count int) []*Tag {
|
|
40
40
|
return ctx.Render(`
|
|
41
41
|
<span id="todo-count" class="todo-count" hx-swap-oob="true">
|
|
42
42
|
<strong>{count}</strong> item left
|
|
43
43
|
</span>
|
|
44
|
-
`)
|
|
44
|
+
`)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
func WebsiteName() string {
|
|
@@ -50,14 +50,14 @@ func WebsiteName() string {
|
|
|
50
50
|
|
|
51
51
|
func TestComponent(t *testing.T) {
|
|
52
52
|
r := require.New(t)
|
|
53
|
-
RegisterComponent(Todo, "todo")
|
|
53
|
+
RegisterComponent(Todo, nil, "todo")
|
|
54
54
|
RegisterFunc(WebsiteName)
|
|
55
55
|
h := Context{
|
|
56
56
|
data: map[string]interface{}{
|
|
57
57
|
"todo": &TodoData{ID: "4", Text: "My fourth todo", Completed: false},
|
|
58
58
|
},
|
|
59
59
|
}
|
|
60
|
-
actual := h.Render(`
|
|
60
|
+
actual := renderString(h.Render(`
|
|
61
61
|
<div>
|
|
62
62
|
<Todo>
|
|
63
63
|
<div class="todo-panel">
|
|
@@ -67,8 +67,8 @@ func TestComponent(t *testing.T) {
|
|
|
67
67
|
</Todo>
|
|
68
68
|
<Todo />
|
|
69
69
|
</div>
|
|
70
|
-
`)
|
|
70
|
+
`))
|
|
71
|
-
expected :=
|
|
71
|
+
expected := `
|
|
72
72
|
<div>
|
|
73
73
|
<todo>
|
|
74
74
|
<li id="todo-4" class="">
|
|
@@ -78,132 +78,132 @@ func TestComponent(t *testing.T) {
|
|
|
78
78
|
</li>
|
|
79
79
|
</todo>
|
|
80
80
|
</div>
|
|
81
|
-
`
|
|
81
|
+
`
|
|
82
82
|
r.Equal(expected, actual)
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
func TestFor(t *testing.T) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
85
|
+
// func TestFor(t *testing.T) {
|
|
86
|
+
// r := require.New(t)
|
|
87
|
+
// RegisterComponent(Todo, nil, "todo")
|
|
88
|
+
// RegisterFunc(WebsiteName)
|
|
89
|
+
// h := Context{
|
|
90
|
+
// data: map[string]interface{}{
|
|
91
|
+
// "todos": []*TodoData{
|
|
92
|
+
// {ID: "1", Text: "My first todo", Completed: true},
|
|
93
|
+
// {ID: "2", Text: "My second todo", Completed: false},
|
|
94
|
+
// {ID: "3", Text: "My third todo", Completed: false},
|
|
95
|
+
// },
|
|
96
|
+
// },
|
|
97
|
+
// }
|
|
98
|
+
// actual := h.Render(`
|
|
99
|
+
// <div>
|
|
100
|
+
// <ul x-for="todo in todos" class="relative">
|
|
101
|
+
// <li>
|
|
102
|
+
// <span>{todo.Text}</span>
|
|
103
|
+
// <span>{todo.Completed}</span>
|
|
104
|
+
// <a>link to {todo.ID}</a>
|
|
105
|
+
// </li>
|
|
106
|
+
// </ul>
|
|
107
|
+
// <ol x-for="todo in todos">
|
|
108
|
+
// <Todo>
|
|
109
|
+
// <div class="todo-panel">
|
|
110
|
+
// <span>{todo.Text}</span>
|
|
111
|
+
// <span>{todo.Completed}</span>
|
|
112
|
+
// </div>
|
|
113
|
+
// </Todo>
|
|
114
|
+
// </ol>
|
|
115
|
+
// </div>
|
|
116
|
+
// `).String()
|
|
117
|
+
// expected := stripWhitespace(`
|
|
118
|
+
// <div>
|
|
119
|
+
// <ul x-for="todo in todos" class="relative">
|
|
120
|
+
// <li><span>My first todo</span><span>true</span><a>link to 1</a></li>
|
|
121
|
+
// <li><span>My second todo</span><span>false</span><a>link to 2</a></li>
|
|
122
|
+
// <li><span>My third todo</span><span>false</span><a>link to 3</a></li>
|
|
123
|
+
// </ul>
|
|
124
|
+
// <ol x-for="todo in todos">
|
|
125
|
+
// <li id="todo-1" class="completed">
|
|
126
|
+
// <div class="view"><span>My first todo</span><span>My first todo</span></div>
|
|
127
|
+
// <div class="todo-panel"><span>My first todo</span><span>true</span></div>
|
|
128
|
+
// <div class="count"><span>true</span><span>true</span></div>
|
|
129
|
+
// </li>
|
|
130
|
+
// <li id="todo-2" class="">
|
|
131
|
+
// <div class="view"><span>My second todo</span><span>My second todo</span></div>
|
|
132
|
+
// <div class="todo-panel"><span>My second todo</span><span>false</span></div>
|
|
133
|
+
// <div class="count"><span>false</span><span>false</span></div>
|
|
134
|
+
// </li>
|
|
135
|
+
// <li id="todo-3" class="">
|
|
136
|
+
// <div class="view"><span>My third todo</span><span>My third todo</span></div>
|
|
137
|
+
// <div class="todo-panel"><span>My third todo</span><span>false</span></div>
|
|
138
|
+
// <div class="count"><span>false</span><span>false</span></div>
|
|
139
|
+
// </li>
|
|
140
|
+
// </ol>
|
|
141
|
+
// </div>
|
|
142
|
+
// `)
|
|
143
|
+
// r.Equal(expected, actual)
|
|
144
|
+
// }
|
|
145
145
|
|
|
146
|
-
func TestForComponent(t *testing.T) {
|
|
146
|
+
// func TestForComponent(t *testing.T) {
|
|
147
|
-
|
|
147
|
+
// r := require.New(t)
|
|
148
|
-
|
|
148
|
+
// RegisterComponent(Todo, nil, "todo")
|
|
149
|
-
|
|
149
|
+
// RegisterComponent(TodoList, nil, "todos")
|
|
150
|
-
|
|
150
|
+
// RegisterFunc(WebsiteName)
|
|
151
|
-
|
|
151
|
+
// h := Context{
|
|
152
|
-
|
|
152
|
+
// data: map[string]interface{}{
|
|
153
|
-
|
|
153
|
+
// "todos": []*TodoData{
|
|
154
|
-
|
|
154
|
+
// {ID: "1", Text: "My first todo", Completed: true},
|
|
155
|
-
|
|
155
|
+
// {ID: "2", Text: "My second todo", Completed: false},
|
|
156
|
-
|
|
156
|
+
// {ID: "3", Text: "My third todo", Completed: false},
|
|
157
|
+
// },
|
|
157
|
-
|
|
158
|
+
// },
|
|
158
|
-
},
|
|
159
|
-
|
|
159
|
+
// }
|
|
160
|
-
|
|
160
|
+
// actual := h.Render(`
|
|
161
|
-
<div>
|
|
162
|
-
<TodoList />
|
|
163
|
-
|
|
161
|
+
// <div>
|
|
162
|
+
// <TodoList />
|
|
163
|
+
// </div>
|
|
164
|
-
|
|
164
|
+
// `).String()
|
|
165
|
-
|
|
165
|
+
// expected := stripWhitespace(`
|
|
166
|
-
|
|
166
|
+
// <div>
|
|
167
|
-
|
|
167
|
+
// <ul id="todo-list" class="relative" x-for="todo in todos">
|
|
168
|
-
|
|
168
|
+
// <li id="todo-1" class="completed">
|
|
169
|
-
|
|
169
|
+
// <div class="view"><span>My first todo</span><span>My first todo</span></div>
|
|
170
|
-
|
|
170
|
+
// <div class="todo-panel"><span>My first todo</span><span>true</span></div>
|
|
171
|
-
|
|
171
|
+
// <div class="count"><span>true</span><span>true</span></div>
|
|
172
|
-
|
|
172
|
+
// </li>
|
|
173
|
-
|
|
173
|
+
// <li id="todo-2" class="">
|
|
174
|
-
|
|
174
|
+
// <div class="view"><span>My second todo</span><span>My second todo</span></div>
|
|
175
|
-
|
|
175
|
+
// <div class="todo-panel"><span>My second todo</span><span>false</span></div>
|
|
176
|
-
|
|
176
|
+
// <div class="count"><span>false</span><span>false</span></div>
|
|
177
|
-
|
|
177
|
+
// </li>
|
|
178
|
-
|
|
178
|
+
// <li id="todo-3" class="">
|
|
179
|
-
|
|
179
|
+
// <div class="view"><span>My third todo</span><span>My third todo</span></div>
|
|
180
|
-
|
|
180
|
+
// <div class="todo-panel"><span>My third todo</span><span>false</span></div>
|
|
181
|
-
|
|
181
|
+
// <div class="count"><span>false</span><span>false</span></div>
|
|
182
|
-
|
|
182
|
+
// </li>
|
|
183
|
-
|
|
183
|
+
// </ul>
|
|
184
|
-
|
|
184
|
+
// </div>
|
|
185
|
-
|
|
185
|
+
// `)
|
|
186
|
-
|
|
186
|
+
// r.Equal(expected, actual)
|
|
187
|
-
}
|
|
187
|
+
// }
|
|
188
188
|
|
|
189
|
-
func TestMultipleComonent(t *testing.T) {
|
|
189
|
+
// func TestMultipleComonent(t *testing.T) {
|
|
190
|
-
|
|
190
|
+
// r := require.New(t)
|
|
191
|
-
|
|
191
|
+
// RegisterComponent(Todo, nil, "todo")
|
|
192
|
-
|
|
192
|
+
// RegisterComponent(TodoCount, nil, "count")
|
|
193
|
-
|
|
193
|
+
// h := Context{
|
|
194
|
-
|
|
194
|
+
// data: map[string]interface{}{
|
|
195
|
-
|
|
195
|
+
// "todo": &TodoData{
|
|
196
|
-
|
|
196
|
+
// ID: "3",
|
|
197
|
-
|
|
197
|
+
// Text: "My third todo",
|
|
198
|
-
|
|
198
|
+
// Completed: false,
|
|
199
|
+
// },
|
|
199
|
-
|
|
200
|
+
// },
|
|
200
|
-
},
|
|
201
|
-
|
|
201
|
+
// }
|
|
202
|
-
|
|
202
|
+
// actual := h.Render(`
|
|
203
|
-
|
|
203
|
+
// <Todo />
|
|
204
|
-
|
|
204
|
+
// <TodoCount />
|
|
205
|
-
|
|
205
|
+
// `).String()
|
|
206
|
-
|
|
206
|
+
// expected := stripWhitespace(`
|
|
207
|
-
|
|
207
|
+
// `)
|
|
208
|
-
|
|
208
|
+
// r.Equal(expected, actual)
|
|
209
|
-
}
|
|
209
|
+
// }
|
gsx/parser.go
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
package gsx
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"strings"
|
|
6
|
+
|
|
7
|
+
"github.com/alecthomas/participle/v2"
|
|
8
|
+
"github.com/alecthomas/participle/v2/lexer"
|
|
9
|
+
"github.com/alecthomas/repr"
|
|
10
|
+
"github.com/goneric/stack"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
type Module struct {
|
|
14
|
+
Pos lexer.Position
|
|
15
|
+
AstNode []*AstNode `@@*`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type AstNode struct {
|
|
19
|
+
Pos lexer.Position
|
|
20
|
+
Open *Open `@@`
|
|
21
|
+
Close *Close `| @@`
|
|
22
|
+
Content *Literal `| @@`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type Open struct {
|
|
26
|
+
Pos lexer.Position
|
|
27
|
+
Name string `"<" @Ident`
|
|
28
|
+
Attributes []*Attribute `[ @@ { @@ } ]`
|
|
29
|
+
SelfClose string `@("/")? ">"`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type Close struct {
|
|
33
|
+
Pos lexer.Position
|
|
34
|
+
Name string `"<""/"@Ident">"`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Attribute struct {
|
|
38
|
+
Pos lexer.Position
|
|
39
|
+
Key string `@":"? @Ident ( @"-" @Ident )*`
|
|
40
|
+
Value *Literal `"=" @@`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type KV struct {
|
|
44
|
+
Pos lexer.Position
|
|
45
|
+
Key string `@String`
|
|
46
|
+
Value string `":" @"!"? @Ident ( @"." @Ident )*`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type Literal struct {
|
|
50
|
+
Pos lexer.Position
|
|
51
|
+
Str *string `@String`
|
|
52
|
+
Ref *string `| "{" @Ident ( @"." @Ident )* "}"`
|
|
53
|
+
KV []*KV `| "{" @@* "}"`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var htmlParser = participle.MustBuild[Module]()
|
|
57
|
+
|
|
58
|
+
type Tag struct {
|
|
59
|
+
Name string
|
|
60
|
+
Text *Literal
|
|
61
|
+
Attributes []*Attribute
|
|
62
|
+
Children []*Tag
|
|
63
|
+
SelfClosing bool
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func renderString(tags []*Tag) string {
|
|
67
|
+
s := ""
|
|
68
|
+
for _, t := range tags {
|
|
69
|
+
s += renderTagString(t, "") + "\n"
|
|
70
|
+
}
|
|
71
|
+
return s
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func renderTagString(x *Tag, space string) string {
|
|
75
|
+
if x.Name == "" {
|
|
76
|
+
if x.Text != nil && x.Text.Str != nil {
|
|
77
|
+
return space + strings.ReplaceAll(*x.Text.Str, `"`, "")
|
|
78
|
+
}
|
|
79
|
+
if x.Text != nil && x.Text.Ref != nil {
|
|
80
|
+
return space + "{" + *x.Text.Ref + "}"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
s := space + "<" + x.Name
|
|
84
|
+
if len(x.Attributes) > 0 {
|
|
85
|
+
s += " "
|
|
86
|
+
}
|
|
87
|
+
for _, a := range x.Attributes {
|
|
88
|
+
if a.Value.Str != nil {
|
|
89
|
+
s += a.Key + "=" + *a.Value.Str
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if x.SelfClosing {
|
|
93
|
+
s += " />"
|
|
94
|
+
} else {
|
|
95
|
+
s += ">\n"
|
|
96
|
+
}
|
|
97
|
+
if !x.SelfClosing {
|
|
98
|
+
for _, c := range x.Children {
|
|
99
|
+
s += renderTagString(c, space+" ") + "\n"
|
|
100
|
+
}
|
|
101
|
+
s += space + "</" + x.Name + ">"
|
|
102
|
+
}
|
|
103
|
+
return s
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func findAttribute(items []*Attribute, key string) *Attribute {
|
|
107
|
+
for _, a := range items {
|
|
108
|
+
if a.Key == key {
|
|
109
|
+
return a
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return nil
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func sliceContains(slice []string, s string) bool {
|
|
116
|
+
for _, v := range slice {
|
|
117
|
+
if s == v {
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func processTree(module *Module) []*Tag {
|
|
125
|
+
tags := []*Tag{}
|
|
126
|
+
var prevTag *Tag
|
|
127
|
+
stack := stack.New[*Tag]()
|
|
128
|
+
for _, n := range module.AstNode {
|
|
129
|
+
if n.Open != nil {
|
|
130
|
+
newTag := &Tag{
|
|
131
|
+
Name: n.Open.Name,
|
|
132
|
+
Attributes: n.Open.Attributes,
|
|
133
|
+
SelfClosing: n.Open.SelfClose == "/",
|
|
134
|
+
}
|
|
135
|
+
if prevTag != nil {
|
|
136
|
+
prevTag.Children = append(prevTag.Children, newTag)
|
|
137
|
+
if !newTag.SelfClosing {
|
|
138
|
+
stack.Push(prevTag)
|
|
139
|
+
prevTag = newTag
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
tags = append(tags, newTag)
|
|
143
|
+
prevTag = newTag
|
|
144
|
+
}
|
|
145
|
+
} else if n.Close != nil {
|
|
146
|
+
repr.Println("close", n.Close.Name, prevTag.Name)
|
|
147
|
+
if n.Close.Name == prevTag.Name {
|
|
148
|
+
prevTag, _ = stack.Pop()
|
|
149
|
+
} else {
|
|
150
|
+
panic(fmt.Errorf("Brackets not matching in line %d:%d", n.Close.Pos.Line, n.Close.Pos.Column))
|
|
151
|
+
}
|
|
152
|
+
} else if n.Content != nil {
|
|
153
|
+
newTag := &Tag{
|
|
154
|
+
Name: "",
|
|
155
|
+
Text: n.Content,
|
|
156
|
+
}
|
|
157
|
+
if prevTag != nil {
|
|
158
|
+
prevTag.Children = append(prevTag.Children, newTag)
|
|
159
|
+
} else {
|
|
160
|
+
tags = append(tags, newTag)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return tags
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func parse(name, s string) []*Tag {
|
|
168
|
+
ast, err := htmlParser.ParseString(name, s)
|
|
169
|
+
if err != nil {
|
|
170
|
+
panic(err)
|
|
171
|
+
}
|
|
172
|
+
return processTree(ast)
|
|
173
|
+
}
|
gsx/parser_test.go
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package gsx
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/stretchr/testify/require"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestParse(t *testing.T) {
|
|
11
|
+
r := require.New(t)
|
|
12
|
+
actual := renderString(parse("test", `
|
|
13
|
+
<ul id="todo-list" class="relative">
|
|
14
|
+
<Todo todo={v}>
|
|
15
|
+
<div>"Todo123"</div>
|
|
16
|
+
</Todo>
|
|
17
|
+
<img src="123" />
|
|
18
|
+
<span>{WebsiteName}</span>
|
|
19
|
+
</ul>
|
|
20
|
+
<div>
|
|
21
|
+
<p>
|
|
22
|
+
"Done"
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
`))
|
|
26
|
+
println(actual)
|
|
27
|
+
expected := strings.TrimLeft(`
|
|
28
|
+
<ul id="todo-list"class="relative">
|
|
29
|
+
<Todo >
|
|
30
|
+
<div>
|
|
31
|
+
Todo123
|
|
32
|
+
</div>
|
|
33
|
+
</Todo>
|
|
34
|
+
<img src="123" />
|
|
35
|
+
<span>
|
|
36
|
+
{WebsiteName}
|
|
37
|
+
</span>
|
|
38
|
+
</ul>
|
|
39
|
+
<div>
|
|
40
|
+
<p>
|
|
41
|
+
Done
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
`, "\n")
|
|
45
|
+
r.Equal(expected, actual)
|
|
46
|
+
}
|
http.go
CHANGED
|
@@ -307,13 +307,25 @@ func Handle(router *mux.Router, method, route string, h interface{}, meta, style
|
|
|
307
307
|
gsx.SetClasses(key, styles)
|
|
308
308
|
router.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
|
|
309
309
|
newCtx := context.WithValue(context.WithValue(r.Context(), "url", r.URL), "header", r.Header)
|
|
310
|
+
var hx *gsx.HX
|
|
310
|
-
|
|
311
|
+
if r.Header.Get("HX-Request") == "true" {
|
|
312
|
+
hx = &gsx.HX{
|
|
313
|
+
Boosted: r.Header.Get("HX-Boosted") == "true",
|
|
314
|
+
CurrentUrl: r.Header.Get("HX-Current-URL"),
|
|
315
|
+
Prompt: r.Header.Get("HX-Prompt"),
|
|
316
|
+
Target: r.Header.Get("HX-Target"),
|
|
317
|
+
TriggerName: r.Header.Get("HX-Trigger-Name"),
|
|
318
|
+
TriggerID: r.Header.Get("HX-Trigger"),
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
c := gsx.NewContext(newCtx, hx)
|
|
322
|
+
c.Set("funcName", route)
|
|
311
323
|
c.Set("requestId", uuid.NewString())
|
|
312
324
|
c.Link("stylesheet", GetPageStylesUrl(key), "", "")
|
|
313
325
|
c.Link("stylesheet", GetComponentsStylesUrl(), "", "")
|
|
314
326
|
c.Link("icon", "/assets/favicon.ico", "image/x-icon", "image")
|
|
315
327
|
c.Script("/gromer/js/htmx@1.7.0.js", false)
|
|
316
|
-
c.Script("/gromer/js/alpinejs@3.9.6.js", true)
|
|
328
|
+
// c.Script("/gromer/js/alpinejs@3.9.6.js", true)
|
|
317
329
|
c.Meta(meta)
|
|
318
330
|
PerformRequest(route, h, c, w, r)
|
|
319
331
|
}).Methods(method)
|