~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.


c9c1d70a Peter John

3 years ago
improve styling
_example/assets/favicon.ico CHANGED
Binary file
_example/assets/images/icon.png DELETED
Binary file
_example/components/Checkbox.go CHANGED
@@ -4,37 +4,9 @@ import (
4
4
  . "github.com/pyros2097/gromer/gsx"
5
5
  )
6
6
 
7
- var _ = Css(`
8
- .checkbox {
9
- text-align: center;
10
- width: 40px;
11
- /* auto, since non-WebKit browsers doesn't support input styling */
12
- height: auto;
13
- position: absolute;
14
- top: 0;
15
- bottom: 0;
16
- margin: auto 0;
17
- border: none; /* Mobile Safari */
7
+ // var CheckboxStyles = M{}
18
- -webkit-appearance: none;
19
- appearance: none;
20
- }
21
8
 
22
- .checkbox {
23
- opacity: 0;
24
- }
25
-
26
- .checkbox + label {
27
- background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
28
- background-repeat: no-repeat;
29
- background-position: center left;
30
- }
31
-
32
- .checkbox:checked + label {
33
- background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
34
- }
35
- `)
36
-
37
- func Checkbox(c Context, value bool) *Node {
9
+ func Checkbox(c *Context, value bool) *Node {
38
10
  return c.Render(`
39
11
  <input class="checkbox" type="checkbox" checked="{value}" />
40
12
  `)
_example/components/todo.go CHANGED
@@ -5,7 +5,7 @@ import (
5
5
  . "github.com/pyros2097/gromer/gsx"
6
6
  )
7
7
 
8
- func Todo(c Context, todo *todos.Todo) *Node {
8
+ func Todo(c *Context, todo *todos.Todo) *Node {
9
9
  return c.Render(`
10
10
  <li id="todo-{todo.ID}" class="{ completed: todo.Completed }">
11
11
  <div class="view">
_example/containers/Error.go CHANGED
@@ -4,7 +4,7 @@ import (
4
4
  . "github.com/pyros2097/gromer/gsx"
5
5
  )
6
6
 
7
- func Error(c Context, err error) *Node {
7
+ func Error(c *Context, err error) *Node {
8
8
  c.Set("err", err.Error())
9
9
  return c.Render(`
10
10
  <span class="error">
@@ -12,13 +12,3 @@ func Error(c Context, err error) *Node {
12
12
  </span>
13
13
  `)
14
14
  }
15
-
16
- var _ = Css(`
17
- .error {
18
- color: red;
19
- font-size: 18px;
20
- font-weight: 500;
21
- padding: 24px;
22
- border: red 1px solid;
23
- }
24
- `)
_example/containers/TodoCount.go CHANGED
@@ -5,7 +5,7 @@ import (
5
5
  . "github.com/pyros2097/gromer/gsx"
6
6
  )
7
7
 
8
- func TodoCount(c Context, filter string) *Node {
8
+ func TodoCount(c *Context, filter string) *Node {
9
9
  todos, err := todos.GetAllTodo(c, todos.GetAllTodoParams{
10
10
  Filter: filter,
11
11
  Limit: 1000,
@@ -20,14 +20,3 @@ func TodoCount(c Context, filter string) *Node {
20
20
  </span>
21
21
  `)
22
22
  }
23
-
24
- var _ = Css(`
25
- .todo-count {
26
- float: left;
27
- text-align: left;
28
- }
29
-
30
- .todo-count strong {
31
- font-weight: 300;
32
- }
33
- `)
_example/containers/TodoList.go CHANGED
@@ -6,7 +6,7 @@ import (
6
6
  . "github.com/pyros2097/gromer/gsx"
7
7
  )
8
8
 
9
- func TodoList(c Context, page int, filter string) *Node {
9
+ func TodoList(c *Context, page int, filter string) *Node {
10
10
  index := Default(page, 1)
11
11
  todos, err := todos.GetAllTodo(c, todos.GetAllTodoParams{
12
12
  Filter: filter,
@@ -22,95 +22,3 @@ func TodoList(c Context, page int, filter string) *Node {
22
22
  </ul>
23
23
  `)
24
24
  }
25
-
26
- var _ = Css(`
27
- .todo-list {
28
- margin: 0;
29
- padding: 0;
30
- list-style: none;
31
- }
32
-
33
- .todo-list li {
34
- position: relative;
35
- font-size: 24px;
36
- border-bottom: 1px solid #ededed;
37
- }
38
-
39
- .todo-list li:last-child {
40
- border-bottom: none;
41
- }
42
-
43
- .todo-list li.editing {
44
- border-bottom: none;
45
- padding: 0;
46
- }
47
-
48
- .todo-list li.editing .edit {
49
- display: block;
50
- width: 506px;
51
- padding: 12px 16px;
52
- margin: 0 0 0 43px;
53
- }
54
-
55
- .todo-list li.editing .view {
56
- display: none;
57
- }
58
-
59
- .todo-list li label {
60
- word-break: break-all;
61
- padding: 15px 15px 15px 60px;
62
- display: block;
63
- line-height: 1.2;
64
- transition: color 0.4s;
65
- }
66
-
67
- .todo-list li.completed label {
68
- color: #d9d9d9;
69
- text-decoration: line-through;
70
- }
71
-
72
- .todo-list li .destroy {
73
- display: none;
74
- position: absolute;
75
- top: 0;
76
- right: 10px;
77
- bottom: 0;
78
- width: 40px;
79
- height: 40px;
80
- margin: auto 0;
81
- font-size: 30px;
82
- color: #cc9a9a;
83
- margin-bottom: 11px;
84
- transition: color 0.2s ease-out;
85
- }
86
-
87
- .todo-list li .destroy:hover {
88
- color: #af5b5e;
89
- }
90
-
91
- .todo-list li .destroy:after {
92
- content: '×';
93
- }
94
-
95
- .todo-list li:hover .destroy {
96
- display: block;
97
- }
98
-
99
- .todo-list li .edit {
100
- display: none;
101
- }
102
-
103
- .todo-list li.editing:last-child {
104
- margin-bottom: -1px;
105
- }
106
-
107
- @media screen and (-webkit-min-device-pixel-ratio: 0) {
108
- .todo-list li .toggle {
109
- background: none;
110
- }
111
-
112
- .todo-list li .toggle {
113
- height: 40px;
114
- }
115
- }
116
- `)
_example/main.go CHANGED
@@ -38,9 +38,9 @@ func main() {
38
38
  gromer.StylesRoute(staticRouter, "/styles.css")
39
39
 
40
40
  pageRouter := baseRouter.NewRoute().Subrouter()
41
- gromer.Handle(pageRouter, "GET", "/", routes.GET)
41
+ gromer.Handle(pageRouter, "GET", "/", routes.GET, routes.Meta, routes.Styles)
42
- gromer.Handle(pageRouter, "POST", "/", routes.POST)
42
+ gromer.Handle(pageRouter, "POST", "/", routes.POST, routes.Meta, routes.Styles)
43
- gromer.Handle(pageRouter, "GET", "/about", about.GET)
43
+ gromer.Handle(pageRouter, "GET", "/about", about.GET, about.Meta, about.Styles)
44
44
 
45
45
  log.Info().Msg("http server listening on http://localhost:3000")
46
46
  srv := server.New(baseRouter, nil)
_example/routes/404/get.go CHANGED
@@ -4,8 +4,14 @@ import (
4
4
  . "github.com/pyros2097/gromer/gsx"
5
5
  )
6
6
 
7
+ var (
8
+ Meta = M{
9
+ "title": "Page Not Found",
10
+ }
11
+ Styles = M{}
12
+ )
13
+
7
- func GET(c Context) (*Node, int, error) {
14
+ func GET(c *Context) (*Node, int, error) {
8
- c.Meta("title", "Page Not Found")
9
15
  return c.Render(`
10
16
  <main class="box center">
11
17
  <h1>Page Not Found</h1>
_example/routes/about/get.go CHANGED
@@ -4,9 +4,15 @@ import (
4
4
  . "github.com/pyros2097/gromer/gsx"
5
5
  )
6
6
 
7
+ var (
8
+ Meta = M{
9
+ "title": "About Gromer",
10
+ "description": "About Gromer",
11
+ }
12
+ Styles = M{}
13
+ )
14
+
7
- func GET(c Context) (*Node, int, error) {
15
+ func GET(c *Context) (*Node, int, error) {
8
- c.Meta("title", "About Gromer")
9
- c.Meta("description", "About Gromer")
10
16
  return c.Render(`
11
17
  <div class="flex flex-col justify-center items-center">
12
18
  A new link is here
_example/routes/get.go CHANGED
@@ -5,130 +5,96 @@ import (
5
5
  . "github.com/pyros2097/gromer/gsx"
6
6
  )
7
7
 
8
- var _ = Css(`
8
+ var (
9
- .container {
9
+ Meta = M{
10
- background: #fff;
11
- margin: 130px 0 40px 0;
12
- position: relative;
13
- box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
10
+ "title": "Gromer Todos",
11
+ "description": "Gromer Todos",
12
+ "author": "gromer",
13
+ "keywords": "gromer",
14
14
  }
15
15
 
16
- .container h1 {
16
+ Styles = M{
17
+ "container": "bg-gray-50 min-h-screen font-sans",
18
+ "todos-container": "container mx-auto flex flex-col items-center",
19
+ "title": "text-opacity-20 text-red-900 text-8xl text-center",
20
+ "main": "mt-8 shadow-xl w-full max-w-prose bg-white",
17
- position: absolute;
21
+ "bottom": M{
18
- top: -155px;
19
- width: 100%;
20
- font-size: 100px;
21
- font-weight: 100;
22
- text-align: center;
22
+ "container": "flex flex-row items-center flex-wrap sm:flex-nowrap p-2 font-light border-t-2 border-gray-100",
23
- color: rgba(175, 47, 47, 0.15);
23
+ "row": "flex-1 flex flex-row",
24
+ "section-1": "flex-1 flex flex-row order-1 justify-start",
25
+ "section-2": "flex-1 flex flex-row order-2 sm:order-3 justify-end",
26
+ "section-3": "flex-1 flex flex-row order-3 sm:order-2 min-w-full sm:min-w-min justify-center",
27
+ "link": "rounded border px-1 mx-2 hover:border-red-100",
24
- -webkit-text-rendering: optimizeLegibility;
28
+ "active": "border-red-900",
29
+ "clear": "font-light hover:underline",
25
- -moz-text-rendering: optimizeLegibility;
30
+ "disabled": "invisible disabled",
31
+ },
32
+ "footer": M{
33
+ "container": "mt-16 p-4 flex flex-col",
26
- text-rendering: optimizeLegibility;
34
+ "link": "hover:underline",
35
+ "subtitle": "m-0.5 text-xs text-center text-gray-500",
36
+ },
27
37
  }
28
-
29
- input::-webkit-input-placeholder {
30
- font-style: italic;
31
- font-weight: 300;
32
- color: #e6e6e6;
33
- }
34
-
35
- input::-moz-placeholder {
36
- font-style: italic;
37
- font-weight: 300;
38
- color: #e6e6e6;
39
- }
40
-
41
- input::input-placeholder {
42
- font-style: italic;
43
- font-weight: 300;
44
- color: #e6e6e6;
45
- }
46
-
47
- .clear-completed, .clear-completed:active {
48
- float: right;
49
- position: relative;
50
- line-height: 20px;
51
- text-decoration: none;
52
- cursor: pointer;
53
- }
54
-
55
- .clear-completed:hover {
56
- text-decoration: underline;
57
- }
58
-
59
- .filters {
60
- margin: 0;
61
- padding: 0;
62
- list-style: none;
63
- position: absolute;
64
- right: 0;
65
- left: 0;
66
- }
67
-
68
- .filters li {
69
- display: inline;
70
- }
71
-
72
- .filters li a {
73
- color: inherit;
74
- margin: 3px;
75
- padding: 3px 7px;
76
- text-decoration: none;
77
- border: 1px solid transparent;
78
- border-radius: 3px;
79
- }
80
-
81
- .filters li a:hover {
82
- border-color: rgba(175, 47, 47, 0.1);
83
- }
84
-
85
- .filters li a.selected {
86
- border-color: rgba(175, 47, 47, 0.2);
87
- }
88
- `)
38
+ )
89
39
 
90
40
  type GetParams struct {
91
41
  Page int `json:"page"`
92
42
  Filter string `json:"filter"`
93
43
  }
94
44
 
95
- func GET(c Context, params GetParams) (*Node, int, error) {
45
+ func GET(c *Context, params GetParams) (*Node, int, error) {
96
- c.Meta("title", "Gromer Todos")
97
- c.Meta("description", "Gromer Todos")
98
- c.Meta("author", "gromer")
99
- c.Meta("keywords", "gromer")
100
46
  return c.Render(`
101
47
  <div class="container">
102
- <header class="header">
48
+ <div class="todos-container">
49
+ <header>
103
- <h1>todos</h1>
50
+ <h1 class="title">todos</h1>
104
- <form hx-post="/" hx-target="#todo-list" hx-swap="afterbegin" _="on htmx:afterOnLoad set #text.value to ''">
51
+ <form hx-post="/" hx-target="#todo-list" hx-swap="afterbegin" _="on htmx:afterOnLoad set #text.value to ''">
105
- <input type="hidden" name="intent" value="create" />
52
+ <input type="hidden" name="intent" value="create" />
106
- <input class="new-todo" id="text" name="text" placeholder="What needs to be done?" autofocus="false" autocomplete="off" />
53
+ <input class="new-todo" id="text" name="text" placeholder="What needs to be done?" autofocus="false" autocomplete="off" />
107
- </form>
54
+ </form>
108
- </header>
55
+ </header>
109
- <section class="main">
56
+ <main class="main">
110
- <input class="toggle-all" id="toggle-all" type="checkbox" />
57
+ <input class="toggle-all" id="toggle-all" type="checkbox" />
111
- <label for="toggle-all">Mark all as complete</label>
58
+ <label for="toggle-all">Mark all as complete</label>
112
- <TodoList id="todo-list" page="{params.Page}" filter="{params.Filter}" />
59
+ <TodoList id="todo-list" page="{params.Page}" filter="{params.Filter}"></TodoList>
60
+ <div class="bottom">
61
+ <div class="section-1">
62
+ <TodoCount filter="{params.Filter}"></TodoCount>
63
+ </div>
64
+ <ul class="section-2">
65
+ <li>
66
+ <a href="?filter=all" class="link active">All</a>
67
+ </li>
68
+ <li>
69
+ <a href="?filter=active" class="link">Active</a>
70
+ </li>
71
+ <li>
72
+ <a href="?filter=completed" class="link">Completed</a>
73
+ </li>
74
+ </ul>
75
+ <div class="section-3">
76
+ <form hx-target="#todo-list" hx-post="/">
77
+ <input type="hidden" name="intent" value="clear_completed" />
78
+ <button type="submit" class="bottom-clear">Clear completed</button>
79
+ </form>
80
+ </div>
81
+ </div>
113
- </section>
82
+ </main>
114
- <footer class="footer">
83
+ <footer class="footer">
115
- <TodoCount filter="{params.Filter}" />
116
- <ul class="filters">
84
+ <span class="subtitle">Written by
117
- <li>
118
- <a href="?filter=all">All</a>
119
- </li>
120
- <li>
121
- <a href="?filter=active">Active</a>
122
- </li>
123
- <li>
124
- <a href="?filter=completed">Completed</a>
125
- </li>
126
- </ul>
127
- <form hx-target="#todo-list" hx-post="/">
128
- <input type="hidden" name="intent" value="clear_completed" />
85
+ <a class="link" href="https://github.com/pyrossh/">pyrossh</a>
129
- <button type="submit" class="clear-completed" >Clear completed</button>
130
- </form>
86
+ </span>
87
+ <span class="subtitle">using
88
+ <a class="link" href="https://github.com/pyrossh/gromer">Gromer</a>
89
+ </span>
90
+ <span class="subtitle">thanks to
91
+ <a class="link" href="https://github.com/wishawa/">Wisha Wa</a>
92
+ </span>
93
+ <span class="subtitle">according to the spec
94
+ <a class="link" href="https://todomvc.com/">TodoMVC</a>
95
+ </span>
131
- </footer>
96
+ </footer>
97
+ </div>
132
98
  </div>
133
99
  `), 200, nil
134
100
  }
_example/routes/post.go CHANGED
@@ -14,7 +14,7 @@ type PostParams struct {
14
14
  Text string `json:"text"`
15
15
  }
16
16
 
17
- func POST(c Context, params PostParams) (*Node, int, error) {
17
+ func POST(c *Context, params PostParams) (*Node, int, error) {
18
18
  if params.Intent == "clear_completed" {
19
19
  allTodos, err := todos.GetAllTodo(c, todos.GetAllTodoParams{
20
20
  Filter: "all",
_example/routes/styles.go DELETED
@@ -1,153 +0,0 @@
1
- package routes
2
-
3
- import (
4
- . "github.com/pyros2097/gromer/gsx"
5
- )
6
-
7
- var _ = Css(`
8
- hr {
9
- margin: 20px 0;
10
- border: 0;
11
- border-top: 1px dashed #c5c5c5;
12
- border-bottom: 1px dashed #f7f7f7;
13
- }
14
-
15
- html,
16
- body {
17
- margin: 0;
18
- padding: 0;
19
- }
20
-
21
- button {
22
- margin: 0;
23
- padding: 0;
24
- border: 0;
25
- background: none;
26
- font-size: 100%;
27
- vertical-align: baseline;
28
- font-family: inherit;
29
- font-weight: inherit;
30
- color: inherit;
31
- -webkit-appearance: none;
32
- appearance: none;
33
- -webkit-font-smoothing: antialiased;
34
- -moz-osx-font-smoothing: grayscale;
35
- }
36
-
37
- body {
38
- font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
39
- line-height: 1.4em;
40
- background: #f5f5f5;
41
- color: #4d4d4d;
42
- min-width: 230px;
43
- max-width: 550px;
44
- margin: 0 auto;
45
- -webkit-font-smoothing: antialiased;
46
- -moz-osx-font-smoothing: grayscale;
47
- font-weight: 300;
48
- }
49
-
50
- :focus {
51
- outline: 0;
52
- }
53
-
54
- .hidden {
55
- display: none;
56
- }
57
-
58
- .new-todo,
59
- .edit {
60
- position: relative;
61
- margin: 0;
62
- width: 100%;
63
- font-size: 24px;
64
- font-family: inherit;
65
- font-weight: inherit;
66
- line-height: 1.4em;
67
- border: 0;
68
- color: inherit;
69
- padding: 6px;
70
- border: 1px solid #999;
71
- box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
72
- box-sizing: border-box;
73
- -webkit-font-smoothing: antialiased;
74
- -moz-osx-font-smoothing: grayscale;
75
- }
76
-
77
- .new-todo {
78
- padding: 16px 16px 16px 60px;
79
- border: none;
80
- background: rgba(0, 0, 0, 0.003);
81
- box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
82
- }
83
-
84
- .main {
85
- position: relative;
86
- z-index: 2;
87
- border-top: 1px solid #e6e6e6;
88
- }
89
-
90
- .toggle-all {
91
- text-align: center;
92
- border: none; /* Mobile Safari */
93
- opacity: 0;
94
- position: absolute;
95
- }
96
-
97
- .toggle-all + label {
98
- width: 60px;
99
- height: 34px;
100
- font-size: 0;
101
- position: absolute;
102
- top: -52px;
103
- left: -13px;
104
- -webkit-transform: rotate(90deg);
105
- transform: rotate(90deg);
106
- }
107
-
108
- .toggle-all + label:before {
109
- content: '❯';
110
- font-size: 22px;
111
- color: #e6e6e6;
112
- padding: 10px 27px 10px 27px;
113
- }
114
-
115
- .toggle-all:checked + label:before {
116
- color: #737373;
117
- }
118
-
119
- .footer {
120
- color: #777;
121
- padding: 10px 15px;
122
- height: 20px;
123
- text-align: center;
124
- border-top: 1px solid #e6e6e6;
125
- }
126
-
127
- .footer:before {
128
- content: '';
129
- position: absolute;
130
- right: 0;
131
- bottom: 0;
132
- left: 0;
133
- height: 50px;
134
- overflow: hidden;
135
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
136
- }
137
-
138
- @media screen and (-webkit-min-device-pixel-ratio: 0) {
139
- .toggle-all {
140
- background: none;
141
- }
142
- }
143
-
144
- @media (max-width: 430px) {
145
- .footer {
146
- height: 50px;
147
- }
148
-
149
- .filters {
150
- bottom: 10px;
151
- }
152
- }
153
- `)
go.mod CHANGED
@@ -20,13 +20,16 @@ require (
20
20
  require (
21
21
  cloud.google.com/go v0.94.0 // indirect
22
22
  cloud.google.com/go/firestore v1.5.0 // indirect
23
+ github.com/alecthomas/repr v0.1.0 // indirect
23
24
  github.com/aymerick/douceur v0.2.0 // indirect
24
25
  github.com/aymerick/raymond v2.0.2+incompatible // indirect
25
26
  github.com/blang/semver v3.5.1+incompatible // indirect
26
27
  github.com/davecgh/go-spew v1.1.1 // indirect
27
28
  github.com/felixge/httpsnoop v1.0.1 // indirect
29
+ github.com/go-errors/errors v1.4.2 // indirect
28
30
  github.com/go-playground/locales v0.14.0 // indirect
29
31
  github.com/go-playground/universal-translator v0.18.0 // indirect
32
+ github.com/go-stack/stack v1.8.1 // indirect
30
33
  github.com/gobuffalo/envy v1.6.5 // indirect
31
34
  github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
32
35
  github.com/golang/protobuf v1.5.2 // indirect
go.sum CHANGED
@@ -99,6 +99,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
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
101
  github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
102
+ github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
103
+ github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
102
104
  github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
103
105
  github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
104
106
  github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
@@ -169,6 +171,8 @@ github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5
169
171
  github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
170
172
  github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
171
173
  github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
174
+ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
175
+ github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
172
176
  github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
173
177
  github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
174
178
  github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -185,6 +189,8 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
185
189
  github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A=
186
190
  github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
187
191
  github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
192
+ github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
193
+ github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
188
194
  github.com/gobuffalo/envy v1.6.5 h1:X3is06x7v0nW2xiy2yFbbIjwHz57CD6z6MkvqULTCm8=
189
195
  github.com/gobuffalo/envy v1.6.5/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
190
196
  github.com/gobuffalo/velvet v0.0.0-20170320144106-d97471bf5d8f h1:ddIdPdlkAgKMB0mbkft2LT3oxN1h3MN1fopCFrOgkhY=
gsx/gsx.go CHANGED
@@ -21,14 +21,17 @@ var (
21
21
  Data: "div",
22
22
  DataAtom: atom.Lookup([]byte("div")),
23
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
- styles = ""
28
- refRegex = regexp.MustCompile(`{(.*?)}`)
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(`{(.*?)}`)
29
29
  )
30
30
 
31
31
  type (
32
+ M map[string]interface{}
33
+ MS map[string]string
34
+ Arr []interface{}
32
35
  ComponentFunc struct {
33
36
  Func interface{}
34
37
  Args []string
@@ -42,8 +45,8 @@ type (
42
45
  Context struct {
43
46
  context.Context
44
47
  hxRequest bool
45
- data map[string]interface{}
48
+ data M
46
- metas map[string]string
49
+ meta M
47
50
  links map[string]link
48
51
  scripts map[string]bool
49
52
  }
@@ -52,61 +55,76 @@ type (
52
55
  }
53
56
  )
54
57
 
55
- func NewContext(c context.Context, hxRequest bool) Context {
58
+ func NewContext(c context.Context, hxRequest bool) *Context {
59
+ return &Context{
60
+ Context: c,
61
+ hxRequest: hxRequest,
62
+ data: M{},
63
+ meta: M{},
64
+ links: map[string]link{},
65
+ scripts: map[string]bool{},
66
+ }
67
+ }
68
+
56
- return Context{Context: c, hxRequest: hxRequest, data: map[string]interface{}{}, metas: map[string]string{}, links: map[string]link{}, scripts: map[string]bool{}}
69
+ func (c *Context) Get(k string) interface{} {
70
+ return c.data[k]
57
71
  }
58
72
 
59
- func (h Context) Get(k string) interface{} {
73
+ func (c *Context) Set(k string, v interface{}) {
60
- return h.data[k]
74
+ c.data[k] = v
61
75
  }
62
76
 
63
- func (h Context) Set(k string, v interface{}) {
77
+ func (c *Context) Meta(meta M) {
64
- h.data[k] = v
78
+ c.meta = meta
65
79
  }
66
80
 
67
- func (h Context) Meta(k, v string) {
81
+ func (c *Context) AddMeta(k, v string) {
68
- h.metas[k] = v
82
+ c.meta[k] = v
69
83
  }
70
84
 
71
- func (h Context) Link(rel, href, t, as string) {
85
+ func (c *Context) Link(rel, href, t, as string) {
72
- h.links[href] = link{rel, href, t, as}
86
+ c.links[href] = link{rel, href, t, as}
73
87
  }
74
88
 
75
- func (h Context) Script(src string, sdefer bool) {
89
+ func (c *Context) Script(src string, sdefer bool) {
76
- h.scripts[src] = sdefer
90
+ c.scripts[src] = sdefer
77
91
  }
78
92
 
93
+ func (c *Context) Data(data M) {
94
+ c.data = data
95
+ }
96
+
79
- func (h Context) Render(tpl string) *Node {
97
+ func (c *Context) Render(tpl string) *Node {
80
98
  newTpl := stripWhitespace(tpl)
81
99
  doc, err := html.ParseFragment(bytes.NewBuffer([]byte(newTpl)), contextNode)
82
100
  if err != nil {
83
101
  panic(err)
84
102
  }
85
- populate(h, doc[0])
103
+ populate(c, doc[0])
86
104
  return &Node{*doc[0]}
87
105
  }
88
106
 
89
- func (n *Node) Write(ctx Context, w io.Writer) {
107
+ func (n *Node) Write(c *Context, w io.Writer) {
90
- if !ctx.hxRequest {
108
+ if !c.hxRequest {
91
109
  w.Write([]byte(`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">`))
92
110
  w.Write([]byte(`<meta http-equiv="Content-Type" content="text/html;charset=utf-8"><meta content="utf-8" http-equiv="encoding">`))
93
111
  w.Write([]byte(`<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover">`))
94
- for k, v := range ctx.metas {
112
+ for k, v := range c.meta {
95
113
  w.Write([]byte(fmt.Sprintf(`<meta name="%s" content="%s">`, k, v)))
96
114
  }
97
- for k, v := range ctx.metas {
115
+ for k, v := range c.meta {
98
116
  if k == "title" {
99
117
  w.Write([]byte(fmt.Sprintf(`<title>%s</title>`, v)))
100
118
  }
101
119
  }
102
- for _, v := range ctx.links {
120
+ for _, v := range c.links {
103
121
  if v.Type != "" || v.As != "" {
104
122
  w.Write([]byte(fmt.Sprintf(`<link rel="%s" href="%s" type="%s" as="%s">`, v.Rel, v.Href, v.Type, v.As)))
105
123
  } else {
106
124
  w.Write([]byte(fmt.Sprintf(`<link rel="%s" href="%s">`, v.Rel, v.Href)))
107
125
  }
108
126
  }
109
- for src, sdefer := range ctx.scripts {
127
+ for src, sdefer := range c.scripts {
110
128
  if sdefer {
111
129
  w.Write([]byte(fmt.Sprintf(`<script src="%s" defer="true"></script>`, src)))
112
130
  } else {
@@ -116,7 +134,7 @@ func (n *Node) Write(ctx Context, w io.Writer) {
116
134
  w.Write([]byte(`</head><body>`))
117
135
  }
118
136
  html.Render(w, &n.Node)
119
- if !ctx.hxRequest {
137
+ if !c.hxRequest {
120
138
  w.Write([]byte(`</body></html>`))
121
139
  }
122
140
  }
@@ -139,6 +157,14 @@ func assertName(t, name string) {
139
157
  }
140
158
  }
141
159
 
160
+ func SetClasses(k string, m M) {
161
+ classesMap[k] = m
162
+ }
163
+
164
+ func GetStyles(k string) string {
165
+ return normalizeCss + "\n" + computeCss(classesMap[k], k)
166
+ }
167
+
142
168
  func RegisterComponent(f interface{}, args ...string) {
143
169
  name := strings.ToLower(getFunctionName(f))
144
170
  assertName("component", name)
@@ -154,15 +180,6 @@ func RegisterFunc(f interface{}) {
154
180
  funcMap[name] = f
155
181
  }
156
182
 
157
- func Css(v string) string {
158
- styles += v
159
- return v
160
- }
161
-
162
- func GetStyles() string {
163
- return styles
164
- }
165
-
166
183
  func getFunctionName(temp interface{}) string {
167
184
  strs := strings.Split((runtime.FuncForPC(reflect.ValueOf(temp).Pointer()).Name()), ".")
168
185
  return strs[len(strs)-1]
@@ -194,13 +211,13 @@ func convert(ref string, i interface{}) interface{} {
194
211
  }
195
212
  }
196
213
 
197
- func getRefValue(ctx Context, ref string) interface{} {
214
+ func getRefValue(c *Context, ref string) interface{} {
198
215
  if f, ok := funcMap[ref]; ok {
199
216
  return f.(func() string)()
200
217
  } else {
201
218
  parts := strings.Split(strings.ReplaceAll(ref, "!", ""), ".")
202
219
  if len(parts) == 2 {
203
- if v, ok := ctx.data[parts[0]]; ok {
220
+ if v, ok := c.data[parts[0]]; ok {
204
221
  a := reflect.ValueOf(v)
205
222
  if a.Kind() == reflect.Ptr {
206
223
  i := a.Elem().FieldByName(parts[1]).Interface()
@@ -211,7 +228,7 @@ func getRefValue(ctx Context, ref string) interface{} {
211
228
  }
212
229
  }
213
230
  }
214
- return convert(ref, ctx.data[ref])
231
+ return convert(ref, c.data[ref])
215
232
  }
216
233
  }
217
234
 
@@ -219,11 +236,11 @@ func removeBrackets(v string) string {
219
236
  return strings.ReplaceAll(strings.ReplaceAll(v, "{", ""), "}", "")
220
237
  }
221
238
 
222
- func substituteString(ctx Context, v string) string {
239
+ func substituteString(c *Context, v string) string {
223
240
  found := refRegex.FindString(v)
224
241
  if found != "" {
225
242
  varName := removeBrackets(found)
226
- varValue := fmt.Sprintf("%v", getRefValue(ctx, varName))
243
+ varValue := fmt.Sprintf("%v", getRefValue(c, varName))
227
244
  return strings.ReplaceAll(v, found, varValue)
228
245
  }
229
246
  return v
@@ -280,10 +297,10 @@ func cloneNode(n *html.Node) *html.Node {
280
297
  return newNode
281
298
  }
282
299
 
283
- func populate(ctx Context, n *html.Node) {
300
+ func populate(c *Context, n *html.Node) {
284
301
  if n.Type == html.TextNode {
285
302
  if n.Data != "" && strings.Contains(n.Data, "{") && n.Data != "{children}" {
286
- n.Data = substituteString(ctx, n.Data)
303
+ n.Data = substituteString(c, n.Data)
287
304
  }
288
305
  } else if n.Type == html.ElementNode {
289
306
  for i, at := range n.Attr {
@@ -292,7 +309,7 @@ func populate(ctx Context, n *html.Node) {
292
309
  arr := strings.Split(xfor, " in ")
293
310
  ctxItemKey := arr[0]
294
311
  ctxKey := arr[1]
295
- data := ctx.data[ctxKey]
312
+ data := c.data[ctxKey]
296
313
  switch reflect.TypeOf(data).Kind() {
297
314
  case reflect.Slice:
298
315
  v := reflect.ValueOf(data)
@@ -308,8 +325,8 @@ func populate(ctx Context, n *html.Node) {
308
325
  firstChild := cloneNode(n.FirstChild)
309
326
  n.RemoveChild(n.FirstChild)
310
327
  for i := 0; i < v.Len(); i++ {
311
- compCtx := Context{
328
+ compCtx := &Context{
312
- Context: ctx.Context,
329
+ Context: c.Context,
313
330
  data: map[string]interface{}{
314
331
  ctxItemKey: v.Index(i).Interface(),
315
332
  },
@@ -333,7 +350,7 @@ func populate(ctx Context, n *html.Node) {
333
350
  kvarray := strings.Split(kv, ":")
334
351
  k := strings.TrimSpace(kvarray[0])
335
352
  v := strings.TrimSpace(kvarray[1])
336
- varValue := getRefValue(ctx, v)
353
+ varValue := getRefValue(c, v)
337
354
  if varValue.(bool) {
338
355
  classes += k
339
356
  }
@@ -347,26 +364,26 @@ func populate(ctx Context, n *html.Node) {
347
364
  n.Attr[i] = html.Attribute{
348
365
  Namespace: at.Namespace,
349
366
  Key: at.Key,
350
- Val: substituteString(ctx, at.Val),
367
+ Val: substituteString(c, at.Val),
351
368
  }
352
369
  }
353
370
  }
354
371
  }
355
- for c := n.FirstChild; c != nil; c = c.NextSibling {
372
+ for child := n.FirstChild; child != nil; child = child.NextSibling {
356
- populate(ctx, c)
373
+ populate(c, child)
357
374
  }
358
375
  if comp, ok := compMap[n.Data]; ok {
359
- newNode := populateComponent(ctx, comp, n, true)
376
+ newNode := populateComponent(c, comp, n, true)
360
377
  n.AppendChild(newNode)
361
378
  }
362
379
  }
363
380
  }
364
381
 
365
- func renderComponent(ctx Context, comp ComponentFunc, n *html.Node) *Node {
382
+ func renderComponent(c *Context, comp ComponentFunc, n *html.Node) *Node {
366
- args := []reflect.Value{reflect.ValueOf(ctx)}
383
+ args := []reflect.Value{reflect.ValueOf(c)}
367
384
  funcType := reflect.TypeOf(comp.Func)
368
385
  for i, arg := range comp.Args {
369
- if v, ok := ctx.data[arg]; ok {
386
+ if v, ok := c.data[arg]; ok {
370
387
  args = append(args, reflect.ValueOf(v))
371
388
  } else {
372
389
  v := getAttribute(arg, n.Attr)
@@ -388,8 +405,8 @@ func renderComponent(ctx Context, comp ComponentFunc, n *html.Node) *Node {
388
405
  return compNode
389
406
  }
390
407
 
391
- func populateComponent(ctx Context, comp ComponentFunc, n *html.Node, remove bool) *html.Node {
408
+ func populateComponent(c *Context, comp ComponentFunc, n *html.Node, remove bool) *html.Node {
392
- compNode := renderComponent(ctx, comp, n)
409
+ compNode := renderComponent(c, comp, n)
393
410
  if n.FirstChild != nil {
394
411
  newChild := cloneNode(n.FirstChild)
395
412
  newChild.Parent = nil
@@ -397,7 +414,7 @@ func populateComponent(ctx Context, comp ComponentFunc, n *html.Node, remove boo
397
414
  n.RemoveChild(n.FirstChild)
398
415
  }
399
416
  if !remove {
400
- populate(ctx, newChild)
417
+ populate(c, newChild)
401
418
  }
402
419
  if compNode.FirstChild != nil {
403
420
  populateChildren(compNode.FirstChild, newChild)
gsx/twx.go ADDED
@@ -0,0 +1,552 @@
1
+ package gsx
2
+
3
+ import "strings"
4
+
5
+ type KeyValues struct {
6
+ Keys M
7
+ Values MS
8
+ }
9
+
10
+ var colors = KeyValues{
11
+ Keys: M{
12
+ "bg": "background-color",
13
+ "text": "color",
14
+ "divide": "border-color",
15
+ "border": "border-color",
16
+ "ring": "--tw-ring-color",
17
+ "border-l": "border-left-color",
18
+ "border-r": "border-right-color",
19
+ "border-t": "border-top-color",
20
+ "border-b": "border-bottom-color",
21
+ },
22
+ Values: MS{
23
+ "transparent": "transparent",
24
+ "current": "currentColor",
25
+ "black": "rgba(0, 0, 0, 1)",
26
+ "white": "rgba(255, 255, 255, 1)",
27
+ "gray-50": "rgba(249, 250, 251, 1)",
28
+ "gray-100": "rgba(243, 244, 246, 1)",
29
+ "gray-200": "rgba(229, 231, 235, 1)",
30
+ "gray-300": "rgba(209, 213, 219, 1)",
31
+ "gray-400": "rgba(156, 163, 175, 1)",
32
+ "gray-500": "rgba(107, 114, 128, 1)",
33
+ "gray-600": "rgba(75, 85, 99, 1)",
34
+ "gray-700": "rgba(55, 65, 81, 1)",
35
+ "gray-800": "rgba(31, 41, 55, 1)",
36
+ "gray-900": "rgba(17, 24, 39, 1)",
37
+ "red-50": "rgba(254, 242, 242, 1)",
38
+ "red-100": "rgba(254, 226, 226, 1)",
39
+ "red-200": "rgba(254, 202, 202, 1)",
40
+ "red-300": "rgba(252, 165, 165, 1)",
41
+ "red-400": "rgba(248, 113, 113, 1)",
42
+ "red-500": "rgba(239, 68, 68, 1)",
43
+ "red-600": "rgba(220, 38, 38, 1)",
44
+ "red-700": "rgba(185, 28, 28, 1)",
45
+ "red-800": "rgba(153, 27, 27, 1)",
46
+ "red-900": "rgba(127, 29, 29, 1)",
47
+ "yellow-50": "rgba(255, 251, 235, 1)",
48
+ "yellow-100": "rgba(254, 243, 199, 1)",
49
+ "yellow-200": "rgba(253, 230, 138, 1)",
50
+ "yellow-300": "rgba(252, 211, 77, 1)",
51
+ "yellow-400": "rgba(251, 191, 36, 1)",
52
+ "yellow-500": "rgba(245, 158, 11, 1)",
53
+ "yellow-600": "rgba(217, 119, 6, 1)",
54
+ "yellow-700": "rgba(180, 83, 9, 1)",
55
+ "yellow-800": "rgba(146, 64, 14, 1)",
56
+ "yellow-900": "rgba(120, 53, 15, 1)",
57
+ "green-50": "rgba(236, 253, 245, 1)",
58
+ "green-100": "rgba(209, 250, 229, 1)",
59
+ "green-200": "rgba(167, 243, 208, 1)",
60
+ "green-300": "rgba(110, 231, 183, 1)",
61
+ "green-400": "rgba(52, 211, 153, 1)",
62
+ "green-500": "rgba(16, 185, 129, 1)",
63
+ "green-600": "rgba(5, 150, 105, 1)",
64
+ "green-700": "rgba(4, 120, 87, 1)",
65
+ "green-800": "rgba(6, 95, 70, 1)",
66
+ "green-900": "rgba(6, 78, 59, 1)",
67
+ "blue-50": "rgba(239, 246, 255, 1)",
68
+ "blue-100": "rgba(219, 234, 254, 1)",
69
+ "blue-200": "rgba(191, 219, 254, 1)",
70
+ "blue-300": "rgba(147, 197, 253, 1)",
71
+ "blue-400": "rgba(96, 165, 250, 1)",
72
+ "blue-500": "rgba(59, 130, 246, 1)",
73
+ "blue-600": "rgba(37, 99, 235, 1)",
74
+ "blue-700": "rgba(29, 78, 216, 1)",
75
+ "blue-800": "rgba(30, 64, 175, 1)",
76
+ "blue-900": "rgba(30, 58, 138, 1)",
77
+ "indigo-50": "rgba(238, 242, 255, 1)",
78
+ "indigo-100": "rgba(224, 231, 255, 1)",
79
+ "indigo-200": "rgba(199, 210, 254, 1)",
80
+ "indigo-300": "rgba(165, 180, 252, 1)",
81
+ "indigo-400": "rgba(129, 140, 248, 1)",
82
+ "indigo-500": "rgba(99, 102, 241, 1)",
83
+ "indigo-600": "rgba(79, 70, 229, 1)",
84
+ "indigo-700": "rgba(67, 56, 202, 1)",
85
+ "indigo-800": "rgba(55, 48, 163, 1)",
86
+ "indigo-900": "rgba(49, 46, 129, 1)",
87
+ "purple-50": "rgba(245, 243, 255, 1)",
88
+ "purple-100": "rgba(237, 233, 254, 1)",
89
+ "purple-200": "rgba(221, 214, 254, 1)",
90
+ "purple-300": "rgba(196, 181, 253, 1)",
91
+ "purple-400": "rgba(167, 139, 250, 1)",
92
+ "purple-500": "rgba(139, 92, 246, 1)",
93
+ "purple-600": "rgba(124, 58, 237, 1)",
94
+ "purple-700": "rgba(109, 40, 217, 1)",
95
+ "purple-800": "rgba(91, 33, 182, 1)",
96
+ "purple-900": "rgba(76, 29, 149, 1)",
97
+ "pink-50": "rgba(253, 242, 248, 1)",
98
+ "pink-100": "rgba(252, 231, 243, 1)",
99
+ "pink-200": "rgba(251, 207, 232, 1)",
100
+ "pink-300": "rgba(249, 168, 212, 1)",
101
+ "pink-400": "rgba(244, 114, 182, 1)",
102
+ "pink-500": "rgba(236, 72, 153, 1)",
103
+ "pink-600": "rgba(219, 39, 119, 1)",
104
+ "pink-700": "rgba(190, 24, 93, 1)",
105
+ "pink-800": "rgba(157, 23, 77, 1)",
106
+ "pink-900": "rgba(131, 24, 67, 1)",
107
+ },
108
+ }
109
+
110
+ var spacing = KeyValues{
111
+ Keys: M{
112
+ "mr": "margin-right",
113
+ "ml": "margin-left",
114
+ "mt": "margin-top",
115
+ "mb": "margin-bottom",
116
+ "mx": Arr{
117
+ "margin-left",
118
+ "margin-right",
119
+ },
120
+ "my": Arr{
121
+ "margin-top",
122
+ "margin-bottom",
123
+ },
124
+ "m": "margin",
125
+ "pr": "padding-right",
126
+ "pl": "padding-left",
127
+ "pt": "padding-top",
128
+ "pb": "padding-bottom",
129
+ "px": Arr{
130
+ "padding-left",
131
+ "padding-right",
132
+ },
133
+ "py": Arr{
134
+ "padding-top",
135
+ "padding-bottom",
136
+ },
137
+ "p": "padding",
138
+ },
139
+ Values: MS{
140
+ "0": "0px",
141
+ "1": "0.25rem",
142
+ "2": "0.5rem",
143
+ "3": "0.75rem",
144
+ "4": "1rem",
145
+ "5": "1.25rem",
146
+ "6": "1.5rem",
147
+ "7": "1.75rem",
148
+ "8": "2rem",
149
+ "9": "2.25rem",
150
+ "10": "2.5rem",
151
+ "11": "2.75rem",
152
+ "12": "3rem",
153
+ "14": "3.5rem",
154
+ "16": "4rem",
155
+ "20": "5rem",
156
+ "24": "6rem",
157
+ "28": "7rem",
158
+ "32": "8rem",
159
+ "36": "9rem",
160
+ "40": "10rem",
161
+ "44": "11rem",
162
+ "48": "12rem",
163
+ "52": "13rem",
164
+ "56": "14rem",
165
+ "60": "15rem",
166
+ "64": "16rem",
167
+ "72": "18rem",
168
+ "80": "20rem",
169
+ "96": "24rem",
170
+ "auto": "auto",
171
+ "px": "1px",
172
+ "0.5": "0.125rem",
173
+ "1.5": "0.375rem",
174
+ "2.5": "0.625rem",
175
+ "3.5": "0.875rem",
176
+ },
177
+ }
178
+
179
+ var radius = KeyValues{
180
+ Keys: M{
181
+ "rounded": "border-radius",
182
+ "rounded-t": "border-top-radius",
183
+ "rounded-r": "border-right-radius",
184
+ "rounded-l": "border-left-radius",
185
+ "rounded-b": "border-bottom-radius",
186
+ "rounded-tl": Arr{
187
+ "border-top-radius",
188
+ "border-left-radius",
189
+ },
190
+ "rounded-tr": Arr{
191
+ "border-top-radius",
192
+ "border-right-radius",
193
+ },
194
+ "rounded-bl": Arr{
195
+ "border-bottom-radius",
196
+ "border-left-radius",
197
+ },
198
+ "rounded-br": Arr{
199
+ "border-bottom-radius",
200
+ "border-right-radius",
201
+ },
202
+ },
203
+ Values: MS{
204
+ "none": "0px",
205
+ "sm": "0.125rem",
206
+ "": "0.25rem",
207
+ "md": "0.375rem",
208
+ "lg": "0.5rem",
209
+ "xl": "0.75rem",
210
+ "2xl": "1rem",
211
+ "3xl": "1.5rem",
212
+ "full": "9999px",
213
+ },
214
+ }
215
+
216
+ var borders = KeyValues{
217
+ Keys: M{
218
+ "border": "border-width",
219
+ "border-l": "border-left-width",
220
+ "border-r": "border-right-width",
221
+ "border-t": "border-top-width",
222
+ "border-b": "border-bottom-width",
223
+ },
224
+ Values: MS{
225
+ "0": "0px",
226
+ "2": "2px",
227
+ "4": "4px",
228
+ "8": "8px",
229
+ "": "1px",
230
+ },
231
+ }
232
+
233
+ var sizes = KeyValues{
234
+ Keys: M{
235
+ "h": "height",
236
+ "w": "width",
237
+ "top": "top",
238
+ "left": "left",
239
+ "bottom": "bottom",
240
+ "right": "right",
241
+ "minh": "min-height",
242
+ "minw": "min-width",
243
+ "maxh": "max-height",
244
+ "maxw": "max-width",
245
+ },
246
+ Values: MS{
247
+ "auto": "auto",
248
+ "min": "min-content",
249
+ "max": "max-content",
250
+ "0": "0px",
251
+ "1": "0.25rem",
252
+ "2": "0.5rem",
253
+ "3": "0.75rem",
254
+ "4": "1rem",
255
+ "5": "1.25rem",
256
+ "6": "1.5rem",
257
+ "7": "1.75rem",
258
+ "8": "2rem",
259
+ "9": "2.25rem",
260
+ "10": "2.5rem",
261
+ "11": "2.75rem",
262
+ "12": "3rem",
263
+ "14": "3.5rem",
264
+ "16": "4rem",
265
+ "20": "5rem",
266
+ "24": "6rem",
267
+ "28": "7rem",
268
+ "32": "8rem",
269
+ "36": "9rem",
270
+ "40": "10rem",
271
+ "44": "11rem",
272
+ "48": "12rem",
273
+ "52": "13rem",
274
+ "56": "14rem",
275
+ "60": "15rem",
276
+ "64": "16rem",
277
+ "72": "18rem",
278
+ "80": "20rem",
279
+ "96": "24rem",
280
+ "px": "1px",
281
+ "0.5": "0.125rem",
282
+ "1.5": "0.375rem",
283
+ "2.5": "0.625rem",
284
+ "3.5": "0.875rem",
285
+ "1/2": "50%",
286
+ "1/3": "33.33%",
287
+ "2/3": "66.66%",
288
+ "1/4": "25%",
289
+ "2/4": "50%",
290
+ "3/4": "75%",
291
+ "1/5": "20%",
292
+ "2/5": "40%",
293
+ "3/5": "60%",
294
+ "4/5": "80%",
295
+ "1/6": "16.66%",
296
+ "2/6": "33.33%",
297
+ "3/6": "50%",
298
+ "4/6": "66.66%",
299
+ "5/6": "83.33%",
300
+ "1/12": "8.33%",
301
+ "2/12": "16.66%",
302
+ "3/12": "25%",
303
+ "4/12": "33.33%",
304
+ "5/12": "41.66%",
305
+ "6/12": "50%",
306
+ "7/12": "58.33%",
307
+ "8/12": "66.66%",
308
+ "9/12": "75%",
309
+ "10/12": "83.33%",
310
+ "11/12": "91.66%",
311
+ "full": "100%",
312
+ },
313
+ }
314
+
315
+ var twClassLookup = MS{
316
+ "flex": "display: flex;",
317
+ "inline-flex": "display: inline-flex;",
318
+ "block": "display: block;",
319
+ "inline-block": "display: inline-block;",
320
+ "inline": "display: inline;",
321
+ "table": "display: table;",
322
+ "inline-table": "display: inline-table;",
323
+ "grid": "display: grid;",
324
+ "inline-grid": "display: inline-grid;",
325
+ "contents": "display: contents;",
326
+ "list-item": "display: list-item;",
327
+ "hidden": "display: none;",
328
+ "flex-1": "flex: 1;",
329
+ "flex-row": "flex-direction: row;",
330
+ "flex-col": "flex-direction: column;",
331
+ "flex-wrap": "flex-wrap: wrap;",
332
+ "flex-nowrap": "flex-wrap: nowrap;",
333
+ "flex-wrap-reverse": "flex-wrap: wrap-reverse;",
334
+ "items-baseline": "align-items: baseline;",
335
+ "items-start": "align-items: flex-start;",
336
+ "items-center": "align-items: center;",
337
+ "items-end": "align-items: flex-end;",
338
+ "items-stretch": "align-items: stretch;",
339
+ "justify-start": "justify-content: flex-start;",
340
+ "justify-end": "justify-content: flex-end;",
341
+ "justify-center": "justify-content: center;",
342
+ "justify-between": "justify-content: space-between;",
343
+ "justify-around": "justify-content: space-around;",
344
+ "justify-evenly": "justify-content: space-evenly;",
345
+ "uppercase": "text-transform: uppercase",
346
+ "lowercase": "text-transform: lowercase",
347
+ "capitalize": "text-transform: capitalize",
348
+ "normal-case": "text-transform: normal-case",
349
+ "text-left": "text-align: left;",
350
+ "text-center": "text-align: center;",
351
+ "text-right": "text-align: right;",
352
+ "text-justify": "text-align: justify;",
353
+ "underline": "text-decoration: underline;",
354
+ "line-through": "text-decoration: line-through;",
355
+ "no-underline": "text-decoration: none;",
356
+ "whitespace-normal": "white-space: normal;",
357
+ "whitespace-nowrap": "white-space: nowrap;",
358
+ "whitespace-pre": "white-space: pre;",
359
+ "whitespace-pre-line": "white-space: pre-line;",
360
+ "whitespace-pre-wrap": "white-space: pre-wrap;",
361
+ "break-normal": "word-break: normal; overflow-wrap: normal;",
362
+ "break-words": "word-break: break-word;",
363
+ "break-all": "word-break: break-all;",
364
+ "font-sans": "font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";",
365
+ "font-serif": "font-family: ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif;",
366
+ "font-mono": "font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;",
367
+ "font-thin": "font-weight: 100;",
368
+ "font-extralight": "font-weight: 200;",
369
+ "font-light": "font-weight: 300;",
370
+ "font-normal": "font-weight: 400;",
371
+ "font-medium": "font-weight: 500;",
372
+ "font-semibold": "font-weight: 600;",
373
+ "font-bold": "font-weight: 700;",
374
+ "font-extrabold": "font-weight: 800;",
375
+ "font-black": "font-weight: 900;",
376
+ "text-xs": "font-size: 0.75rem; line-height: 1rem;",
377
+ "text-sm": "font-size: 0.875rem; line-height: 1.25rem;",
378
+ "text-base": "font-size: 1rem; line-height: 1.5rem;",
379
+ "text-lg": "font-size: 1.125rem; line-height: 1.75rem;",
380
+ "text-xl": "font-size: 1.25rem; line-height: 1.75rem;",
381
+ "text-2xl": "font-size: 1.5rem; line-height: 2rem;",
382
+ "text-3xl": "font-size: 1.875rem; line-height: 2.25rem;",
383
+ "text-4xl": "font-size: 2.25rem; line-height: 2.5rem;",
384
+ "text-5xl": "font-size: 3rem; line-height: 1;",
385
+ "text-6xl": "font-size: 3.75rem;; line-height: 1;",
386
+ "text-7xl": "font-size: 4.5rem; line-height: 1;",
387
+ "text-8xl": "font-size: 6rem; line-height: 1;",
388
+ "text-9xl": "font-size: 8rem; line-height: 1;",
389
+ "cursor-auto": "cursor: auto;",
390
+ "cursor-default": "cursor: default;",
391
+ "cursor-pointer": "cursor: pointer;",
392
+ "cursor-wait": "cursor: wait;",
393
+ "cursor-text": "cursor: text;",
394
+ "cursor-move": "cursor: move;",
395
+ "cursor-help": "cursor: help;",
396
+ "cursor-not-allowed": "cursor: not-allowed;",
397
+ "pointer-events-none": "pointer-events: none;",
398
+ "pointer-events-auto": "pointer-events: auto;",
399
+ "select-none": "user-select: none;",
400
+ "select-text": "user-select: text;",
401
+ "select-all": "user-select: all;",
402
+ "select-auto": "user-select: auto;",
403
+ "w-screen": "100vw",
404
+ "h-screen": "100vh",
405
+ "static": "position: static;",
406
+ "fixed": "position: fixed;",
407
+ "absolute": "position: absolute;",
408
+ "relative": "position: relative;",
409
+ "sticky": "position: sticky;",
410
+ "overflow-auto": "overflow: auto;",
411
+ "overflow-hidden": "overflow: hidden;",
412
+ "overflow-visible": "overflow: visible;",
413
+ "overflow-scroll": "overflow: scroll;",
414
+ "overflow-x-auto": "overflow-x: auto;",
415
+ "overflow-y-auto": "overflow-y: auto;",
416
+ "overflow-x-hidden": "overflow-x: hidden;",
417
+ "overflow-y-hidden": "overflow-y: hidden;",
418
+ "overflow-x-visible": "overflow-x: visible;",
419
+ "overflow-y-visible": "overflow-y: visible;",
420
+ "overflow-x-scroll": "overflow-x: scroll;",
421
+ "overflow-y-scroll": "overflow-y: scroll;",
422
+ "origin-center": "transform-origin: center;",
423
+ "origin-top": "transform-origin: top;",
424
+ "origin-top-right": "transform-origin: top right;",
425
+ "origin-right": "transform-origin: right;",
426
+ "origin-bottom-right": "transform-origin: bottom right;",
427
+ "origin-bottom": "transform-origin: bottom;",
428
+ "origin-bottom-left": "transform-origin: bottom left;",
429
+ "origin-left": "transform-origin: left;",
430
+ "origin-top-left": "transform-origin: top left;",
431
+ "shadow-sm": "box-shadow: 0 0 #0000, 0 0 #0000, 0 1px 2px 0 rgba(0, 0, 0, 0.05);",
432
+ "shadow": "box-shadow: 0 0 #0000, 0 0 #0000, 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);",
433
+ "shadow-md": "box-shadow: 0 0 #0000, 0 0 #0000, 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);",
434
+ "shadow-lg": "box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);",
435
+ "shadow-xl": "box-shadow: 0 0 #0000, 0 0 #0000, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);",
436
+ "shadow-2xl": "box-shadow: 0 0 #0000, 0 0 #0000, 0 25px 50px -12px rgba(0, 0, 0, 0.25);",
437
+ "shadow-inner": "box-shadow: 0 0 #0000, 0 0 #0000, inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);",
438
+ "shadow-none": "box-shadow: 0 0 #0000, 0 0 #0000, 0 0 #0000;",
439
+ "ring-inset": "--tw-ring-inset: insest;",
440
+ "ring-0": "box-shadow: 0 0 0 calc(0px + 0px) rgba(59, 130, 246, 0.5);",
441
+ "ring-1": "box-shadow: 0 0 0 calc(1px + 0px) rgba(59, 130, 246, 0.5);",
442
+ "ring-2": "box-shadow: 0 0 0 calc(2px + 0px) rgba(59, 130, 246, 0.5);",
443
+ "ring-4": "box-shadow: 0 0 0 calc(4px + 0px) rgba(59, 130, 246, 0.5);",
444
+ "ring-8": "box-shadow: 0 0 0 calc(8px + 0px) rgba(59, 130, 246, 0.5);",
445
+ "ring": "box-shadow: 0 0 0 calc(3px + 0px) rgba(59, 130, 246, 0.5);",
446
+ }
447
+
448
+ func init() {
449
+ mapApply(sizes)
450
+ mapApply(spacing)
451
+ mapApply(colors)
452
+ mapApply(borders)
453
+ mapApply(radius)
454
+ }
455
+
456
+ func mapApply(obj KeyValues) {
457
+ for key, v := range obj.Keys {
458
+ for vkey, vv := range obj.Values {
459
+ suffix := ""
460
+ if vkey != "" {
461
+ suffix = "-" + vkey
462
+ }
463
+ className := key + suffix
464
+ if vstring, ok := v.(string); ok {
465
+ twClassLookup[className] = vstring + ": " + vv + ";"
466
+ }
467
+ if varr, ok := v.(Arr); ok {
468
+ twClassLookup[className] = ""
469
+ for _, kk := range varr {
470
+ twClassLookup[className] += kk.(string) + ": " + vv + ";"
471
+ }
472
+ }
473
+ }
474
+ }
475
+ }
476
+
477
+ var normalizeCss = `*, ::before, ::after { box-sizing: border-box; }
478
+ html { -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; line-height: 1.15; -webkit-text-size-adjust: 100%; }
479
+ body { margin: 0; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; }
480
+ hr { height: 0; color: inherit; }
481
+ abbr[title] { -webkit-text-decoration: underline dotted; text-decoration: underline dotted; }
482
+ b, strong { font-weight: bolder; }
483
+ code, kbd, samp, pre { font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 1em; }
484
+ small { font-size: 80%; }
485
+ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
486
+ sub { bottom: -0.25em; }
487
+ sup { top: -0.5em; }
488
+ table { text-indent: 0; border-color: inherit; }
489
+ button, input, optgroup, select, textarea { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; }
490
+ button, select { text-transform: none; }
491
+ button, [type='button'], [type='reset'], [type='submit'] { -webkit-appearance: button; }
492
+ ::-moz-focus-inner { border-style: none; padding: 0; }
493
+ :-moz-focusring { outline: 1px dotted ButtonText; outline: auto; }
494
+ :-moz-ui-invalid { box-shadow: none; }
495
+ legend { padding: 0; }
496
+ progress { vertical-align: baseline; }
497
+ ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; }
498
+ [type='search'] { -webkit-appearance: textfield; outline-offset: -2px; }
499
+ ::-webkit-search-decoration { -webkit-appearance: none; }
500
+ ::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; }
501
+ summary { display: list-item; }
502
+ blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre { margin: 0; }
503
+ button { background-color: transparent; background-image: none; }
504
+ fieldset { margin: 0; padding: 0; }
505
+ ol, ul { list-style: none; margin: 0; padding: 0; }
506
+ html { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; line-height: 1.5; }
507
+ body { font-family: inherit; line-height: inherit; }
508
+ *, ::before, ::after { box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor; }
509
+ hr { border-top-width: 1px; }
510
+ img { border-style: solid; }
511
+ textarea { resize: vertical; }
512
+ input::-moz-placeholder, textarea::-moz-placeholder { opacity: 1; color: #9ca3af; }
513
+ input:-ms-input-placeholder, textarea:-ms-input-placeholder { opacity: 1; color: #9ca3af; }
514
+ input::placeholder, textarea::placeholder { opacity: 1; color: #9ca3af; }
515
+ button, [role="button"] { cursor: pointer; }
516
+ table { border-collapse: collapse; }
517
+ h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; }
518
+ a { color: inherit; text-decoration: inherit; }
519
+ button, input, optgroup, select, textarea { padding: 0; line-height: inherit; color: inherit; }
520
+ pre, code, kbd, samp { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
521
+ img, svg, video, canvas, audio, iframe, embed, object { display: block; vertical-align: middle; }
522
+ img, video { max-width: 100%; height: auto; }
523
+ [hidden] { display: none; }
524
+ *, ::before, ::after { --tw-border-opacity: 1; border-color: rgba(229, 231, 235, var(--tw-border-opacity)); }`
525
+
526
+ func computeCss(classMap M, parent string) string {
527
+ p := "\n"
528
+ for k, v := range classMap {
529
+ switch it := v.(type) {
530
+ case string:
531
+ if parent != "" {
532
+ if k == "container" {
533
+ p += "." + parent
534
+ } else {
535
+ p += "." + parent + " ." + k
536
+ }
537
+ } else {
538
+ p += "." + k
539
+ }
540
+ p += " {\n"
541
+ for _, c := range strings.Split(it, " ") {
542
+ if s, ok := twClassLookup[c]; ok {
543
+ p += " " + s + "\n"
544
+ }
545
+ }
546
+ p += "}\n"
547
+ case M:
548
+ p += computeCss(it, k)
549
+ }
550
+ }
551
+ return p
552
+ }
http.go CHANGED
@@ -20,6 +20,7 @@ import (
20
20
  "time"
21
21
 
22
22
  "github.com/felixge/httpsnoop"
23
+ "github.com/go-errors/errors"
23
24
  "github.com/go-playground/validator/v10"
24
25
  "github.com/google/uuid"
25
26
  "github.com/gorilla/handlers"
@@ -69,7 +70,6 @@ func init() {
69
70
  PartsExclude: []string{zerolog.TimestampFieldName},
70
71
  })
71
72
  }
72
- gsx.RegisterFunc(GetStylesUrl)
73
73
  gsx.RegisterFunc(GetAssetUrl)
74
74
  }
75
75
 
@@ -86,9 +86,8 @@ func RespondError(w http.ResponseWriter, status int, err error) {
86
86
  }
87
87
  if status >= 500 {
88
88
  merror["error"] = "Internal Server Error"
89
- stack := string(debug.Stack())
89
+ sterr, _ := err.(*errors.Error)
90
- println(stack)
91
- log.WithLevel(zerolog.ErrorLevel).Err(err).Bool("panic", status == 599).Str("stack", stack).Stack()
90
+ log.Error().Msg(err.Error() + "\n" + sterr.ErrorStack())
92
91
  }
93
92
  validationErrors, ok := err.(validator.ValidationErrors)
94
93
  if ok {
@@ -107,15 +106,9 @@ func GetRouteParams(route string) []string {
107
106
  return params
108
107
  }
109
108
 
110
- func PerformRequest(route string, h interface{}, ctx context.Context, w http.ResponseWriter, r *http.Request) {
109
+ func PerformRequest(route string, h interface{}, c *gsx.Context, w http.ResponseWriter, r *http.Request) {
111
110
  params := GetRouteParams(route)
112
- renderContext := gsx.NewContext(ctx, r.Header.Get("HX-Request") == "true")
113
- renderContext.Set("requestId", uuid.NewString())
114
- renderContext.Link("stylesheet", GetStylesUrl(), "", "")
115
- renderContext.Link("icon", "/assets/favicon.ico", "image/x-icon", "image")
116
- renderContext.Script("/gromer/js/htmx@1.7.0.js", false)
117
- renderContext.Script("/gromer/js/alpinejs@3.9.6.js", true)
118
- args := []reflect.Value{reflect.ValueOf(renderContext)}
111
+ args := []reflect.Value{reflect.ValueOf(c)}
119
112
  funcType := reflect.TypeOf(h)
120
113
  icount := funcType.NumIn()
121
114
  vars := mux.Vars(r)
@@ -189,7 +182,7 @@ func PerformRequest(route string, h interface{}, ctx context.Context, w http.Res
189
182
  RespondError(w, 400, fmt.Errorf("Illegal Content-Type tag found %s", contentType))
190
183
  return
191
184
  }
192
- renderContext.Set("params", instance.Elem().Interface())
185
+ c.Set("params", instance.Elem().Interface())
193
186
  args = append(args, instance.Elem())
194
187
  }
195
188
  values := reflect.ValueOf(h).Call(args)
@@ -200,25 +193,18 @@ func PerformRequest(route string, h interface{}, ctx context.Context, w http.Res
200
193
  RespondError(w, responseStatus, responseError.(error))
201
194
  return
202
195
  }
203
- if v, ok := response.(*gsx.Node); ok {
204
- w.Header().Set("Content-Type", "text/html")
196
+ w.Header().Set("Content-Type", "text/html")
205
- // This has to be at end always
206
- w.WriteHeader(responseStatus)
207
- v.Write(renderContext, w)
208
- return
209
- }
210
- w.Header().Set("Content-Type", "application/json")
211
197
  // This has to be at end always
212
198
  w.WriteHeader(responseStatus)
213
- data, _ := json.Marshal(response)
199
+ response.(*gsx.Node).Write(c, w)
214
- w.Write(data)
215
200
  }
216
201
 
217
202
  func LogMiddleware(next http.Handler) http.Handler {
218
203
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
219
204
  defer func() {
220
205
  if err := recover(); err != nil {
206
+ log.Error().Msgf("%s %d %s %s", r.Method, 599, useragent.Parse(r.UserAgent()).Name, r.URL.Path)
221
- RespondError(w, 599, fmt.Errorf("%+v", err))
207
+ RespondError(w, 599, errors.Errorf(fmt.Sprintf("%+v", err)))
222
208
  }
223
209
  }()
224
210
  m := httpsnoop.CaptureMetrics(next, w, r)
@@ -226,10 +212,7 @@ func LogMiddleware(next http.Handler) http.Handler {
226
212
  if len(ip) > 0 && ip[0] == '[' {
227
213
  ip = ip[1 : len(ip)-1]
228
214
  }
229
- log.WithLevel(zerolog.InfoLevel).Msgf("%s %d %.2fkb %s %s %s", r.Method,
215
+ log.Info().Msgf("%s %d %.2fkb %s %s %s", r.Method, m.Code, float64(m.Written)/1024.0, m.Duration.Round(time.Millisecond).String(),
230
- m.Code,
231
- float64(m.Written)/1024.0,
232
- m.Duration.Round(time.Millisecond).String(),
233
216
  useragent.Parse(r.UserAgent()).Name,
234
217
  r.URL.Path,
235
218
  )
@@ -273,16 +256,33 @@ func StaticRoute(router *mux.Router, path string, fs embed.FS) {
273
256
 
274
257
  func StylesRoute(router *mux.Router, path string) {
275
258
  router.Path(path).Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
259
+ err := r.ParseForm()
260
+ if err != nil {
261
+ RespondError(w, 400, err)
262
+ return
263
+ }
264
+ key := r.Form.Get("key")
276
265
  w.Header().Set("Content-Type", "text/css")
277
266
  w.WriteHeader(200)
278
- w.Write([]byte(gsx.GetStyles()))
267
+ w.Write([]byte(gsx.GetStyles(key)))
279
268
  })
280
269
  }
281
270
 
282
- func Handle(router *mux.Router, method, route string, h interface{}) {
271
+ func Handle(router *mux.Router, method, route string, h interface{}, meta, styles gsx.M) {
272
+ key := getSum(route, func() [16]byte {
273
+ return md5.Sum([]byte(route))
274
+ })
275
+ gsx.SetClasses(key, styles)
283
276
  router.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
284
- ctx := context.WithValue(context.WithValue(r.Context(), "url", r.URL), "header", r.Header)
277
+ newCtx := context.WithValue(context.WithValue(r.Context(), "url", r.URL), "header", r.Header)
278
+ c := gsx.NewContext(newCtx, r.Header.Get("HX-Request") == "true")
279
+ c.Set("requestId", uuid.NewString())
280
+ c.Link("stylesheet", GetStylesUrl(key), "", "")
281
+ c.Link("icon", "/assets/favicon.ico", "image/x-icon", "image")
282
+ c.Script("/gromer/js/htmx@1.7.0.js", false)
283
+ c.Script("/gromer/js/alpinejs@3.9.6.js", true)
284
+ c.Meta(meta)
285
- PerformRequest(route, h, ctx, w, r)
285
+ PerformRequest(route, h, c, w, r)
286
286
  }).Methods(method, "OPTIONS")
287
287
  }
288
288
 
@@ -316,9 +316,9 @@ func GetAssetUrl(fs embed.FS, path string) string {
316
316
  return fmt.Sprintf("/assets/%s?hash=%s", path, sum)
317
317
  }
318
318
 
319
- func GetStylesUrl() string {
319
+ func GetStylesUrl(k string) string {
320
320
  sum := getSum("styles.css", func() [16]byte {
321
- return md5.Sum([]byte(gsx.GetStyles()))
321
+ return md5.Sum([]byte(gsx.GetStyles(k)))
322
322
  })
323
- return fmt.Sprintf("/styles.css?hash=%s", sum)
323
+ return fmt.Sprintf("/styles.css?key=%s&hash=%s", k, sum)
324
324
  }
utils.go CHANGED
@@ -11,10 +11,6 @@ import (
11
11
  "github.com/segmentio/go-camelcase"
12
12
  )
13
13
 
14
- type M map[string]interface{}
15
- type MS map[string]string
16
- type Arr []interface{}
17
-
18
14
  var Validator = validator.New()
19
15
  var ValidatorErrorMap = map[string]string{
20
16
  "required": "is required",