~repos /gromer

#golang#htmx#ssr

git clone https://pyrossh.dev/repos/gromer.git

gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.


cb14a77d Peter John

3 years ago
new parser
Files changed (8) hide show
  1. go.mod +8 -7
  2. go.sum +8 -4
  3. gsx/context.go +84 -0
  4. gsx/gsx.go +104 -280
  5. gsx/gsx_test.go +134 -134
  6. gsx/parser.go +173 -0
  7. gsx/parser_test.go +46 -0
  8. 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
- contextNode = &html.Node{
20
- Type: html.ElementNode,
21
- Data: "div",
22
- DataAtom: atom.Lookup([]byte("div")),
23
- }
24
- 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"}
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 NewContext(c context.Context, hxRequest bool) *Context {
39
+ func RegisterComponent(f interface{}, classes M, args ...string) {
40
+ name := getFunctionName(f)
60
- return &Context{
41
+ compMap[name] = ComponentFunc{
61
- Context: c,
42
+ Func: f,
62
- hxRequest: hxRequest,
63
- data: M{},
43
+ Args: args,
64
- meta: M{},
44
+ Classes: classes,
65
- links: map[string]link{},
66
- scripts: map[string]bool{},
67
45
  }
68
46
  }
69
47
 
70
- func (c *Context) Get(k string) interface{} {
48
+ func RegisterFunc(f interface{}) {
71
- return c.data[k]
72
- }
73
-
74
- func (c *Context) Set(k string, v interface{}) {
49
+ name := getFunctionName(f)
75
- c.data[k] = v
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 (c *Context) Data(data M) {
53
+ func getFunctionName(temp interface{}) string {
54
+ strs := strings.Split((runtime.FuncForPC(reflect.ValueOf(temp).Pointer()).Name()), ".")
95
- c.data = data
55
+ return strs[len(strs)-1]
96
56
  }
97
57
 
98
- func (c *Context) Render(tpl string) *Node {
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
- newTpl := stripWhitespace(tpl)
69
+ value, _ := strconv.Atoi(*v.Value.Str)
100
- doc, err := html.ParseFragment(bytes.NewBuffer([]byte(newTpl)), contextNode)
70
+ args = append(args, reflect.ValueOf(value))
101
- if err != nil {
71
+ case reflect.Bool:
72
+ value, _ := strconv.ParseBool(*v.Value.Str)
73
+ args = append(args, reflect.ValueOf(value))
102
- panic(err)
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, doc[0])
81
+ return populate(c, tags)
105
- return &Node{*doc[0]}
106
82
  }
107
83
 
108
- func (n *Node) Write(c *Context, w io.Writer) {
84
+ func Write(c *Context, w io.Writer, tags []*Tag) {
109
- if !c.hxRequest {
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
- html.Render(w, &n.Node)
113
+ out := renderString(tags)
114
+ w.Write([]byte(out))
138
- if !c.hxRequest {
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, varName))
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 populateChildren(n, replaceNode1 *html.Node) {
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
- if n.NextSibling != nil {
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
- populateChildren(n.FirstChild, replaceNode1)
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 populate(c *Context, n *html.Node) {
196
+ func populateTag(c *Context, tag *Tag) {
317
- if n.Type == html.TextNode {
197
+ if tag.Name == "" {
318
- if n.Data != "" && strings.Contains(n.Data, "{") && n.Data != "{children}" {
198
+ if tag.Text.Ref != nil && *tag.Text.Ref != "children" {
319
- n.Data = substituteString(c, n.Data)
199
+ *tag.Text.Ref = substituteString(c, *tag.Text.Ref)
320
200
  }
321
- } else if n.Type == html.ElementNode {
201
+ } else {
322
- for i, at := range n.Attr {
202
+ for _, a := range tag.Attributes {
323
- if at.Key == "x-for" {
203
+ if a.Key == "x-for" {
324
- xfor := getAttribute("x-for", n.Attr)
325
- arr := strings.Split(xfor, " in ")
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
- Context: c.Context,
217
+ // Context: c.Context,
346
- data: map[string]interface{}{
218
+ // data: map[string]interface{}{
347
- ctxItemKey: v.Index(i).Interface(),
219
+ // ctxItemKey: v.Index(i).Interface(),
348
- },
220
+ // },
349
- }
221
+ // }
350
- itemChild := cloneNode(firstChild)
222
+ // tag.Children
351
- itemChild.Parent = nil
352
- if comp, ok := compMap[itemChild.Data]; ok {
223
+ // if comp, ok := compMap[itemChild.Data]; ok {
353
- newNode := populateComponent(compCtx, comp, itemChild, false)
224
+ // newNode := populateComponent(compCtx, comp, itemChild, false)
354
- n.AppendChild(newNode)
225
+ // n.AppendChild(newNode)
355
- } else {
226
+ // } else {
356
- n.AppendChild(itemChild)
227
+ // n.AppendChild(itemChild)
357
- populate(compCtx, itemChild)
228
+ // populate(compCtx, itemChild)
358
- }
229
+ // }
359
230
  }
360
231
  }
361
- } else if at.Val != "" && strings.Contains(at.Val, "{") {
232
+ } else if a.Value.Ref != nil {
362
- if at.Key == "class" {
233
+ if a.Key == "class" {
363
- classes := []string{}
234
+ // classes := []string{}
364
- kvstrings := strings.Split(strings.TrimSpace(removeBrackets(at.Val)), ",")
235
+ // kvstrings := strings.Split(strings.TrimSpace(at.Val), ",")
365
- for _, kv := range kvstrings {
236
+ // for _, kv := range kvstrings {
366
- kvarray := strings.Split(kv, ":")
237
+ // kvarray := strings.Split(kv, ":")
367
- k := strings.TrimSpace(kvarray[0])
238
+ // k := strings.TrimSpace(kvarray[0])
368
- v := strings.TrimSpace(kvarray[1])
239
+ // v := strings.TrimSpace(kvarray[1])
369
- varValue := getRefValue(c, v)
240
+ // varValue := getRefValue(c, v)
370
- if varValue.(bool) {
241
+ // if varValue.(bool) {
371
- classes = append(classes, k)
242
+ // classes = append(classes, k)
243
+ // }
372
- }
244
+ // }
373
- }
374
- n.Attr[i] = html.Attribute{
245
+ // n.Attr[i] = html.Attribute{
375
- Namespace: at.Namespace,
246
+ // Namespace: at.Namespace,
376
- Key: at.Key,
247
+ // Key: at.Key,
377
- Val: strings.Join(classes, " "),
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
- Val: substituteString(c, at.Val),
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[n.Data]; ok {
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
- compNode := result[0].Interface().(*Node)
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
- compNode := renderComponent(c, comp, n)
258
+ nodes := comp.Render(compContext, tag)
426
- if n.FirstChild != nil {
427
- newChild := cloneNode(n.FirstChild)
259
+ // TODO: check if tag as Children already and {children} in defined in component
428
- newChild.Parent = nil
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(h Context, todo *TodoData) *Node {
15
+ func Todo(c *Context, todo *TodoData) []*Tag {
16
- return h.Render(`
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) (*Node, error) {
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
- `), nil
36
+ `)
37
37
  }
38
38
 
39
- func TodoCount(ctx Context, count int) (*Node, error) {
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
- `), nil
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
- `).String()
70
+ `))
71
- expected := stripWhitespace(`
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
- r := require.New(t)
87
- RegisterComponent(Todo, "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
- }
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
- r := require.New(t)
147
+ // r := require.New(t)
148
- RegisterComponent(Todo, "todo")
148
+ // RegisterComponent(Todo, nil, "todo")
149
- RegisterComponent(TodoList, "todos")
149
+ // RegisterComponent(TodoList, nil, "todos")
150
- RegisterFunc(WebsiteName)
150
+ // RegisterFunc(WebsiteName)
151
- h := Context{
151
+ // h := Context{
152
- data: map[string]interface{}{
152
+ // data: map[string]interface{}{
153
- "todos": []*TodoData{
153
+ // "todos": []*TodoData{
154
- {ID: "1", Text: "My first todo", Completed: true},
154
+ // {ID: "1", Text: "My first todo", Completed: true},
155
- {ID: "2", Text: "My second todo", Completed: false},
155
+ // {ID: "2", Text: "My second todo", Completed: false},
156
- {ID: "3", Text: "My third todo", Completed: false},
156
+ // {ID: "3", Text: "My third todo", Completed: false},
157
+ // },
157
- },
158
+ // },
158
- },
159
- }
159
+ // }
160
- actual := h.Render(`
160
+ // actual := h.Render(`
161
- <div>
162
- <TodoList />
163
- </div>
161
+ // <div>
162
+ // <TodoList />
163
+ // </div>
164
- `).String()
164
+ // `).String()
165
- expected := stripWhitespace(`
165
+ // expected := stripWhitespace(`
166
- <div>
166
+ // <div>
167
- <ul id="todo-list" class="relative" x-for="todo in todos">
167
+ // <ul id="todo-list" class="relative" x-for="todo in todos">
168
- <li id="todo-1" class="completed">
168
+ // <li id="todo-1" class="completed">
169
- <div class="view"><span>My first todo</span><span>My first todo</span></div>
169
+ // <div class="view"><span>My first todo</span><span>My first todo</span></div>
170
- <div class="todo-panel"><span>My first todo</span><span>true</span></div>
170
+ // <div class="todo-panel"><span>My first todo</span><span>true</span></div>
171
- <div class="count"><span>true</span><span>true</span></div>
171
+ // <div class="count"><span>true</span><span>true</span></div>
172
- </li>
172
+ // </li>
173
- <li id="todo-2" class="">
173
+ // <li id="todo-2" class="">
174
- <div class="view"><span>My second todo</span><span>My second todo</span></div>
174
+ // <div class="view"><span>My second todo</span><span>My second todo</span></div>
175
- <div class="todo-panel"><span>My second todo</span><span>false</span></div>
175
+ // <div class="todo-panel"><span>My second todo</span><span>false</span></div>
176
- <div class="count"><span>false</span><span>false</span></div>
176
+ // <div class="count"><span>false</span><span>false</span></div>
177
- </li>
177
+ // </li>
178
- <li id="todo-3" class="">
178
+ // <li id="todo-3" class="">
179
- <div class="view"><span>My third todo</span><span>My third todo</span></div>
179
+ // <div class="view"><span>My third todo</span><span>My third todo</span></div>
180
- <div class="todo-panel"><span>My third todo</span><span>false</span></div>
180
+ // <div class="todo-panel"><span>My third todo</span><span>false</span></div>
181
- <div class="count"><span>false</span><span>false</span></div>
181
+ // <div class="count"><span>false</span><span>false</span></div>
182
- </li>
182
+ // </li>
183
- </ul>
183
+ // </ul>
184
- </div>
184
+ // </div>
185
- `)
185
+ // `)
186
- r.Equal(expected, actual)
186
+ // r.Equal(expected, actual)
187
- }
187
+ // }
188
188
 
189
- func TestMultipleComonent(t *testing.T) {
189
+ // func TestMultipleComonent(t *testing.T) {
190
- r := require.New(t)
190
+ // r := require.New(t)
191
- RegisterComponent(Todo, "todo")
191
+ // RegisterComponent(Todo, nil, "todo")
192
- RegisterComponent(TodoCount, "count")
192
+ // RegisterComponent(TodoCount, nil, "count")
193
- h := Context{
193
+ // h := Context{
194
- data: map[string]interface{}{
194
+ // data: map[string]interface{}{
195
- "todo": &TodoData{
195
+ // "todo": &TodoData{
196
- ID: "3",
196
+ // ID: "3",
197
- Text: "My third todo",
197
+ // Text: "My third todo",
198
- Completed: false,
198
+ // Completed: false,
199
+ // },
199
- },
200
+ // },
200
- },
201
- }
201
+ // }
202
- actual := h.Render(`
202
+ // actual := h.Render(`
203
- <Todo />
203
+ // <Todo />
204
- <TodoCount />
204
+ // <TodoCount />
205
- `).String()
205
+ // `).String()
206
- expected := stripWhitespace(`
206
+ // expected := stripWhitespace(`
207
- `)
207
+ // `)
208
- r.Equal(expected, actual)
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
- c := gsx.NewContext(newCtx, r.Header.Get("HX-Request") == "true")
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)