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


3671d4a4 Peter John

3 years ago
use handlebars template
cmd/gromer/main.go CHANGED
@@ -228,7 +228,6 @@ import (
228
228
  "github.com/rs/zerolog/log"
229
229
  "gocloud.dev/server"
230
230
 
231
- "{{ moduleName }}/context"
232
231
  {{#each allPkgs }}"{{ moduleName }}/pages{{ @key }}"
233
232
  {{/each}}
234
233
  )
@@ -239,7 +238,9 @@ var assetsFS embed.FS
239
238
  func main() {
240
239
  port := os.Getenv("PORT")
241
240
  r := mux.NewRouter()
241
+ r.Use(gromer.CorsMiddleware)
242
+ r.Use(gromer.LogMiddleware)
242
- r.NotFoundHandler = http.HandlerFunc(notFound)
243
+ r.NotFoundHandler = gromer.NotFoundHandler
243
244
  r.PathPrefix("/assets/").Handler(wrapCache(http.FileServer(http.FS(assetsFS))))
244
245
  handle(r, "GET", "/api", gromer.ApiExplorer(apiDefinitions()))
245
246
  {{#each routes as |route| }}handle(r, "{{ route.Method }}", "{{ route.Path }}", {{ route.Pkg }}.{{ route.Method }})
@@ -258,26 +259,15 @@ func wrapCache(h http.Handler) http.Handler {
258
259
  })
259
260
  }
260
261
 
261
- func notFound(w http.ResponseWriter, r *http.Request) {
262
- gromer.LogReq(404, r)
263
- }
264
-
265
262
  func handle(router *mux.Router, method, route string, h interface{}) {
266
263
  router.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
267
- var status int
268
- defer func() {
269
- gromer.LogReq(status, r)
270
- }()
271
264
  ctx := c.WithValue(
272
265
  c.WithValue(
273
266
  c.WithValue(r.Context(), "assetsFS", assetsFS),
274
- "url", r.URL),
267
+ "url", r.URL),
275
268
  "header", r.Header)
276
- status, err = gromer.PerformRequest(route, h, ctx, w, r)
269
+ gromer.PerformRequest(route, h, ctx, w, r)
277
- if err != nil {
278
- log.Error().Stack().Err(err).Msg("")
279
- }
280
- }).Methods(method)
270
+ }).Methods(method, "OPTIONS")
281
271
  }
282
272
 
283
273
  func apiDefinitions() []gromer.ApiDefinition {
example/components/button.go DELETED
@@ -1,13 +0,0 @@
1
- package components
2
-
3
- import (
4
- . "github.com/pyros2097/gromer"
5
- )
6
-
7
- func Button2(title, clickHandler string) *Element {
8
- return Button(Css("bg-gray-300 border-b-2 border-white hover:bg-gray-200 focus:outline-none rounded text-gray-700"), OnClick(clickHandler),
9
- Div(Css("flex flex-row flex-1 justify-center items-center py-4 px-6"),
10
- Text(title),
11
- ),
12
- )
13
- }
example/components/counter.go DELETED
@@ -1,52 +0,0 @@
1
- package components
2
-
3
- import (
4
- "context"
5
- "strconv"
6
-
7
- . "github.com/pyros2097/gromer"
8
- )
9
-
10
- type S map[string]interface{}
11
-
12
- type CC func(nodes ...interface{}) *Element
13
-
14
- func Styled2(s S) CC {
15
- return func(nodes ...interface{}) *Element {
16
- return Div(nodes...)
17
- }
18
- }
19
-
20
- var Container = Styled2(S{
21
- "border-left": "2px",
22
- "border-right": "2px",
23
- })
24
-
25
- func Counter(c context.Context, start int) *Element {
26
- count, setCount := UseState(c, start)
27
- increment := func() {
28
- setCount(count().(int) + 1)
29
- }
30
- decrement := func() {
31
- setCount(count().(int) + 1)
32
- }
33
- return Container("123", Css("text-3xl text-gray-700"),
34
- Row(
35
- Row(Css("underline"),
36
- Text("Counter"),
37
- ),
38
- ),
39
- Row(
40
- Button2("-", "decrement"),
41
- Row(Css("m-20 text-5xl"), XText("count"),
42
- Text("count"),
43
- ),
44
- Button2("+", "increment"),
45
- ),
46
- M{
47
- "count": strconv.Itoa(count().(int)),
48
- "increment": increment,
49
- "decrement": decrement,
50
- },
51
- )
52
- }
example/components/grid.go DELETED
@@ -1,13 +0,0 @@
1
- package components
2
-
3
- import (
4
- . "github.com/pyros2097/gromer"
5
- )
6
-
7
- func Row(uis ...interface{}) *Element {
8
- return NewElement("div", false, append([]interface{}{Css("flex flex-row justify-center items-center")}, uis...)...)
9
- }
10
-
11
- func Col(uis ...interface{}) *Element {
12
- return NewElement("div", false, append([]interface{}{Css("flex flex-col justify-center items-center")}, uis...)...)
13
- }
example/components/header.go CHANGED
@@ -1,22 +1,30 @@
1
1
  package components
2
2
 
3
- import (
4
- . "github.com/pyros2097/gromer"
5
- )
6
-
7
- func Header() *Element {
3
+ func Header() string {
8
- link := "border-b-2 border-white hover:border-red-700 mr-4"
4
+ return `
9
- return Row(Css("w-full mb-20 font-bold text-xl text-gray-700 p-4"),
5
+ <div class="flex flex-row justify-center items-center w-full mb-20 font-bold text-xl text-gray-700 p-4">
10
- Div(Css("text-blue-700"),
6
+ <div class="text-blue-700">
11
- A(Href("https://pyros.sh"), Text("pyros.sh")),
7
+ <a href="https://pyros.sh"> pyros.sh </a>
12
- ),
8
+ </div>
13
- Div(Css("flex flex-row flex-1 justify-end items-end p-2"),
9
+ <div class="flex flex-row flex-1 justify-end items-end p-2">
14
- Div(Css("border-b-2 border-white text-lg text-blue-700 mr-4"), Text("Examples: ")),
10
+ <div class="border-b-2 border-white text-lg text-blue-700 mr-4">Examples:</div>
11
+ <div class="border-b-2 border-white hover:border-red-700 mr-4">
15
- Div(Css(link), A(Href("/"), Text("Home"))),
12
+ <a href="/"> Home </a>
13
+ </div>
14
+ <div class="border-b-2 border-white hover:border-red-700 mr-4">
16
- Div(Css(link), A(Href("/clock"), Text("Clock"))),
15
+ <a href="/clock"> Clock </a>
16
+ </div>
17
+ <div class="border-b-2 border-white hover:border-red-700 mr-4">
17
- Div(Css(link), A(Href("/about"), Text("About"))),
18
+ <a href="/about"> About </a>
19
+ </div>
20
+ <div class="border-b-2 border-white hover:border-red-700 mr-4">
18
- Div(Css(link), A(Href("/container"), Text("Container"))),
21
+ <a href="/container"> Container </a>
22
+ </div>
23
+ <div class="border-b-2 border-white hover:border-red-700 mr-4">
19
- Div(Css(link), A(Href("/panic"), Text("Panic"))),
24
+ <a href="/panic"> Panic </a>
25
+ </div>
26
+ </div>
20
- ),
27
+ {{ children }}
28
+ </div>
21
- )
29
+ `
22
30
  }
example/components/init.go ADDED
@@ -0,0 +1,10 @@
1
+ package components
2
+
3
+ import (
4
+ . "github.com/pyros2097/gromer"
5
+ )
6
+
7
+ func init() {
8
+ RegisterComponent(Page)
9
+ RegisterComponent(Header)
10
+ }
example/components/page.go ADDED
@@ -0,0 +1,26 @@
1
+ package components
2
+
3
+ func Page() string {
4
+ return `
5
+ <!DOCTYPE html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="UTF-8" />
9
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
10
+ <meta content="utf-8" http-equiv="encoding" />
11
+ <title>{{ title }}</title>
12
+ <meta name="description" content="{{ title }}" />
13
+ <meta name="author" content="pyros.sh" />
14
+ <meta content="pyros.sh, gromer" name="keywords" />
15
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover" />
16
+ <link rel="icon" href="/assets/icon.png" />
17
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css" />
18
+ <script src="https://unpkg.com/htmx.org@1.7.0"></script>
19
+ <script src="/assets/alpine.js" defer=""></script>
20
+ </head>
21
+ <body>
22
+ {{ children }}
23
+ </body>
24
+ </html>
25
+ `
26
+ }
example/db/schema.sql ADDED
@@ -0,0 +1,64 @@
1
+ SET statement_timeout = 0;
2
+ SET lock_timeout = 0;
3
+ SET idle_in_transaction_session_timeout = 0;
4
+ SET client_encoding = 'UTF8';
5
+ SET standard_conforming_strings = on;
6
+ SELECT pg_catalog.set_config('search_path', '', false);
7
+ SET check_function_bodies = false;
8
+ SET xmloption = content;
9
+ SET client_min_messages = warning;
10
+ SET row_security = off;
11
+
12
+ SET default_tablespace = '';
13
+
14
+ SET default_table_access_method = heap;
15
+
16
+ --
17
+ -- Name: schema_migrations; Type: TABLE; Schema: public; Owner: -
18
+ --
19
+
20
+ CREATE TABLE public.schema_migrations (
21
+ version character varying(255) NOT NULL
22
+ );
23
+
24
+
25
+ --
26
+ -- Name: todos; Type: TABLE; Schema: public; Owner: -
27
+ --
28
+
29
+ CREATE TABLE public.todos (
30
+ id text NOT NULL,
31
+ text text NOT NULL,
32
+ completed boolean NOT NULL,
33
+ created_at timestamp without time zone NOT NULL,
34
+ updated_at timestamp without time zone NOT NULL
35
+ );
36
+
37
+
38
+ --
39
+ -- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: -
40
+ --
41
+
42
+ ALTER TABLE ONLY public.schema_migrations
43
+ ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);
44
+
45
+
46
+ --
47
+ -- Name: todos todos_pkey; Type: CONSTRAINT; Schema: public; Owner: -
48
+ --
49
+
50
+ ALTER TABLE ONLY public.todos
51
+ ADD CONSTRAINT todos_pkey PRIMARY KEY (id);
52
+
53
+
54
+ --
55
+ -- PostgreSQL database dump complete
56
+ --
57
+
58
+
59
+ --
60
+ -- Dbmate schema migrations
61
+ --
62
+
63
+ INSERT INTO public.schema_migrations (version) VALUES
64
+ ('20211128110219');
example/main.go CHANGED
@@ -12,34 +12,37 @@ import (
12
12
  "github.com/rs/zerolog/log"
13
13
  "gocloud.dev/server"
14
14
 
15
- "github.com/pyros2097/gromer/example/context"
16
15
  "github.com/pyros2097/gromer/example/pages/api/todos"
17
16
  "github.com/pyros2097/gromer/example/pages"
18
17
  "github.com/pyros2097/gromer/example/pages/about"
19
18
  "github.com/pyros2097/gromer/example/pages/api/recover"
20
19
  "github.com/pyros2097/gromer/example/pages/api/todos/_todoId_"
20
+
21
21
  )
22
22
 
23
23
  //go:embed assets/*
24
24
  var assetsFS embed.FS
25
25
 
26
26
  func main() {
27
+ port := os.Getenv("PORT")
27
28
  r := mux.NewRouter()
29
+ r.Use(gromer.CorsMiddleware)
28
30
  r.Use(gromer.LogMiddleware)
29
31
  r.NotFoundHandler = gromer.NotFoundHandler
30
32
  r.PathPrefix("/assets/").Handler(wrapCache(http.FileServer(http.FS(assetsFS))))
31
33
  handle(r, "GET", "/api", gromer.ApiExplorer(apiDefinitions()))
32
34
  handle(r, "GET", "/about", about.GET)
35
+ handle(r, "GET", "/api/recover", recover.GET)
33
36
  handle(r, "DELETE", "/api/todos/{todoId}", todos_todoId_.DELETE)
34
37
  handle(r, "GET", "/api/todos/{todoId}", todos_todoId_.GET)
35
38
  handle(r, "PUT", "/api/todos/{todoId}", todos_todoId_.PUT)
36
39
  handle(r, "GET", "/api/todos", todos.GET)
37
40
  handle(r, "POST", "/api/todos", todos.POST)
38
- handle(r, "GET", "/api/recover", recover.GET)
39
41
  handle(r, "GET", "/", pages.GET)
42
+
40
- println("http server listening on http://localhost:3000")
43
+ println("http server listening on http://localhost:"+port)
41
44
  srv := server.New(r, nil)
42
- if err := srv.ListenAndServe(":3000"); err != nil {
45
+ if err := srv.ListenAndServe(":"+port); err != nil {
43
46
  log.Fatal().Stack().Err(err).Msg("failed to listen")
44
47
  }
45
48
  }
@@ -53,22 +56,26 @@ func wrapCache(h http.Handler) http.Handler {
53
56
 
54
57
  func handle(router *mux.Router, method, route string, h interface{}) {
55
58
  router.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
56
- ctx, err := context.WithContext(c.WithValue(
59
+ ctx := c.WithValue(
57
60
  c.WithValue(
58
61
  c.WithValue(r.Context(), "assetsFS", assetsFS),
59
- "url", r.URL),
62
+ "url", r.URL),
60
- "header", r.Header))
63
+ "header", r.Header)
61
- if err != nil {
62
- gromer.RespondError(w, 500, err)
63
- return
64
- }
65
64
  gromer.PerformRequest(route, h, ctx, w, r)
66
- }).Methods(method)
65
+ }).Methods(method, "OPTIONS")
67
66
  }
68
67
 
69
68
  func apiDefinitions() []gromer.ApiDefinition {
70
69
  return []gromer.ApiDefinition{
71
70
 
71
+ {
72
+ Method: "GET",
73
+ Path: "/api/recover",
74
+ PathParams: []string{ },
75
+ Params: map[string]interface{}{
76
+
77
+ },
78
+ },
72
79
  {
73
80
  Method: "DELETE",
74
81
  Path: "/api/todos/{todoId}",
@@ -82,7 +89,7 @@ func apiDefinitions() []gromer.ApiDefinition {
82
89
  Path: "/api/todos/{todoId}",
83
90
  PathParams: []string{ "todoId", },
84
91
  Params: map[string]interface{}{
85
- "show": "string",
92
+
86
93
  },
87
94
  },
88
95
  {
@@ -90,7 +97,7 @@ func apiDefinitions() []gromer.ApiDefinition {
90
97
  Path: "/api/todos/{todoId}",
91
98
  PathParams: []string{ "todoId", },
92
99
  Params: map[string]interface{}{
93
- "completed": "bool",
100
+
94
101
  },
95
102
  },
96
103
  {
@@ -98,7 +105,7 @@ func apiDefinitions() []gromer.ApiDefinition {
98
105
  Path: "/api/todos",
99
106
  PathParams: []string{ },
100
107
  Params: map[string]interface{}{
101
- "limit": "int", "offset": "int",
108
+
102
109
  },
103
110
  },
104
111
  {
@@ -106,7 +113,7 @@ func apiDefinitions() []gromer.ApiDefinition {
106
113
  Path: "/api/todos",
107
114
  PathParams: []string{ },
108
115
  Params: map[string]interface{}{
109
- "text": "string",
116
+
110
117
  },
111
118
  },
112
119
  }
example/makefile CHANGED
@@ -1,11 +1,9 @@
1
1
  setup:
2
- go install github.com/kyleconroy/sqlc/cmd/sqlc@v1.11.0
2
+ go install github.com/kyleconroy/sqlc/cmd/sqlc@v1.13.0
3
3
  go install github.com/amacneil/dbmate@v1.12.1
4
4
  go install github.com/mitranim/gow@latest
5
5
  go install github.com/pyros2097/gromer/cmd/gromer@v0.9.1
6
6
  docker pull postgres:14.1
7
-
8
- start-db:
9
7
  docker run --name postgres141 -p 5432:5432 -e POSTGRES_PASSWORD=demo -d postgres:14.1
10
8
 
11
9
  generate: export DATABASE_URL=postgres://postgres:demo@127.0.0.1:5432/postgres?sslmode=disable
example/pages/about/get.go CHANGED
@@ -4,27 +4,11 @@ import (
4
4
  "context"
5
5
 
6
6
  . "github.com/pyros2097/gromer"
7
- . "github.com/pyros2097/gromer/example/components"
8
7
  )
9
8
 
10
- func GET(c context.Context) (HtmlPage, int, error) {
9
+ func GET(c context.Context) (HtmlContent, int, error) {
11
- return Html(
10
+ return Html(`
12
- Head(
13
- Title("Example"),
11
+ <!DOCTYPE html>
14
- Meta("description", "Example"),
15
- Meta("author", "pyros.sh"),
12
+ <html lang="en">
16
- Meta("keywords", "pyros.sh, gromer"),
17
- Meta("viewport", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover"),
18
- Link("icon", "/assets/icon.png"),
19
- Script(Src("/assets/alpine.js"), Defer()),
20
- ),
21
- Body(
22
- Col(
23
- Header(),
24
- Row(Css("text-5xl text-gray-700"),
25
- Text("About Me"),
26
- ),
27
- ),
28
- ),
29
- ), 200, nil
13
+ <head>`, nil)
30
14
  }
example/pages/api/todos/get.go CHANGED
@@ -3,6 +3,7 @@ package todos
3
3
  import (
4
4
  "context"
5
5
 
6
+ . "github.com/pyros2097/gromer"
6
7
  "github.com/pyros2097/gromer/example/db"
7
8
  )
8
9
 
@@ -12,10 +13,7 @@ type GetParams struct {
12
13
  }
13
14
 
14
15
  func GET(ctx context.Context, params GetParams) ([]*db.Todo, int, error) {
15
- limit := params.Limit
16
+ limit := Default(params.Limit, 10)
16
- if limit == 0 {
17
- limit = 10
18
- }
19
17
  todos, err := db.Query.ListTodos(ctx, db.ListTodosParams{
20
18
  Limit: int32(limit),
21
19
  Offset: int32(params.Offset),
example/pages/get.go CHANGED
@@ -4,34 +4,57 @@ import (
4
4
  "context"
5
5
 
6
6
  . "github.com/pyros2097/gromer"
7
- . "github.com/pyros2097/gromer/example/components"
7
+ _ "github.com/pyros2097/gromer/example/components"
8
- . "github.com/pyros2097/gromer/example/context"
8
+ "github.com/pyros2097/gromer/example/pages/api/todos"
9
9
  )
10
10
 
11
+ type GetParams struct {
12
+ Page int `json:"limit"`
13
+ }
14
+
11
- func GET(c context.Context) (HtmlPage, int, error) {
15
+ func GET(ctx context.Context, params GetParams) (HtmlContent, int, error) {
16
+ page := Default(params.Page, 1)
17
+ todos, status, err := todos.GET(ctx, todos.GetParams{
18
+ Limit: 10,
19
+ Offset: 10 * (page - 1),
20
+ })
21
+ if err != nil {
12
- ctx := WithState(c)
22
+ return HtmlErr(status, err)
13
- userID := GetUserID(c)
23
+ }
14
- return Html(
24
+ return Html(`
15
- Head(
16
- Title("Example"),
25
+ {{#Page "gromer example"}}
17
- Meta("description", "Example"),
18
- Meta("author", "pyros.sh"),
19
- Meta("keywords", "pyros.sh, gromer"),
26
+ <div class="flex flex-col justify-center items-center">
20
- Meta("viewport", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover"),
21
- Link("icon", "/assets/icon.png"),
22
- Script(Src("/assets/alpine.js"), Defer()),
23
- ),
24
- Body(
25
- Col(
26
- Header(),
27
- H1(Text("Hello "+userID)),
27
+ {{#Header "123"}}
28
+ A new link is here
29
+ {{/Header}}
28
- H1(Text("Hello this is a h1")),
30
+ <h1>Hello this is a h1</h1>
29
- H2(Text("Hello this is a h2")),
31
+ <h2>Hello this is a h2</h2>
30
- H3(XData("{ message: 'I ❤️ Alpine' }"), XText("message"), Text("")),
32
+ <h3 x-data="{ message: 'I ❤️ Alpine' }" x-text="message">I ❤️ Alpine</h3>
31
- Div(Css("mt-10"),
33
+ <table class="table">
34
+ <thead>
32
- Counter(ctx, 4),
35
+ <tr>
36
+ <th>ID</th>
37
+ <th>Title</th>
38
+ <th>Author</th>
39
+ </tr>
40
+ </thead>
41
+ <tbody id="new-book" hx-target="closest tr" hx-swap="outerHTML swap:0.5s">
42
+ {{#each todos as |todo|}}
33
- ),
43
+ <tr>
44
+ <td>{{todo.ID}}</td>
45
+ <td>Book1</td>
46
+ <td>Author1</td>
34
- ),
47
+ <td>
48
+ <button class="button is-primary">Edit</button>
49
+ </td>
35
- ),
50
+ <td>
51
+ <button hx-swap="delete" class="button is-danger" hx-delete="/api/todos/{{todo.ID}}">Delete</button>
52
+ </td>
53
+ </tr>
36
- ), 200, nil
54
+ {{/each}}
55
+ </tbody>
56
+ </table>
57
+ </div>
58
+ {{/Page}}
59
+ `, M{"todos": todos})
37
60
  }
go.mod CHANGED
@@ -15,7 +15,7 @@ require (
15
15
  github.com/segmentio/go-camelcase v0.0.0-20160726192923-7085f1e3c734
16
16
  github.com/stretchr/testify v1.7.0
17
17
  gocloud.dev v0.24.0
18
- golang.org/x/mod v0.5.1
18
+ golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57
19
19
  )
20
20
 
21
21
  require (
@@ -52,9 +52,10 @@ require (
52
52
  github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect
53
53
  go.opencensus.io v0.23.0 // indirect
54
54
  golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e // indirect
55
+ golang.org/x/exp v0.0.0-20220328175248-053ad81199eb // indirect
55
56
  golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
56
57
  golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
57
- golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e // indirect
58
+ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect
58
59
  golang.org/x/text v0.3.7 // indirect
59
60
  golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
60
61
  google.golang.org/api v0.56.0 // indirect
go.sum CHANGED
@@ -434,6 +434,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
434
434
  golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
435
435
  golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
436
436
  golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
437
+ golang.org/x/exp v0.0.0-20220328175248-053ad81199eb h1:pC9Okm6BVmxEw76PUu0XUbOTQ92JX11hfvqTjAV3qxM=
438
+ golang.org/x/exp v0.0.0-20220328175248-053ad81199eb/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
437
439
  golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
438
440
  golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
439
441
  golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -462,6 +464,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
462
464
  golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
463
465
  golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
464
466
  golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
467
+ golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
468
+ golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
465
469
  golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
466
470
  golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
467
471
  golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -588,6 +592,7 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
588
592
  golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
589
593
  golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e h1:XMgFehsDnnLGtjvjOfqWSUzt0alpTR1RSEuznObga2c=
590
594
  golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
595
+ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
591
596
  golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
592
597
  golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
593
598
  golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
html.go DELETED
@@ -1,403 +0,0 @@
1
- package gromer
2
-
3
- import (
4
- "bytes"
5
- "context"
6
- "fmt"
7
- "io"
8
- "strconv"
9
- "strings"
10
- )
11
-
12
- func writeIndent(w io.Writer, indent int) {
13
- for i := 0; i < indent*2; i++ {
14
- w.Write([]byte(" "))
15
- }
16
- }
17
-
18
- func mergeAttributes(parent *Element, uis ...interface{}) *Element {
19
- elems := []*Element{}
20
- for _, v := range uis {
21
- switch c := v.(type) {
22
- case Attribute:
23
- parent.setAttr(c.Key, c.Value)
24
- case M:
25
- for k, v := range c {
26
- if a, ok := v.(string); ok {
27
- parent.setAttr(k, a)
28
- } else {
29
- // store some server state on the component ctx
30
- // parent.setAttr(k, a)
31
- }
32
-
33
- }
34
- case *Element:
35
- elems = append(elems, c)
36
- case nil:
37
- // dont need to add nil items
38
- default:
39
- panic(fmt.Sprintf("unknown type in render %+v", v))
40
- }
41
- }
42
- if !parent.selfClosing {
43
- parent.children = elems
44
- }
45
- return parent
46
- }
47
-
48
- type HtmlPage struct {
49
- ctx context.Context
50
- classLookup map[string]bool
51
- css *bytes.Buffer
52
- js *bytes.Buffer
53
- Head *Element
54
- Body *Element
55
- }
56
-
57
- func (p *HtmlPage) computeCss(elems []*Element) {
58
- for _, el := range elems {
59
- if v, ok := el.attrs["class"]; ok {
60
- classes := strings.Split(v, " ")
61
- for _, c := range classes {
62
- if s, ok := twClassLookup[c]; ok {
63
- if _, ok2 := p.classLookup[c]; !ok2 {
64
- p.classLookup[c] = true
65
- p.css.WriteString(" ." + c + " { " + s + " }\n")
66
- }
67
- }
68
- }
69
- }
70
- if len(el.children) > 0 {
71
- p.computeCss(el.children)
72
- }
73
- }
74
- }
75
-
76
- func (p *HtmlPage) WriteHtml(w io.Writer) {
77
- w.Write([]byte("<!DOCTYPE html>\n"))
78
- w.Write([]byte("<html lang=\"en\">\n"))
79
- p.computeCss(p.Body.children)
80
- p.Head.children = append(p.Head.children, StyleTag(Text(normalizeStyles+p.css.String())))
81
- p.Head.writeHtmlIndent(w, 1)
82
- // p.Body.children = append(p.Body.children, Script(Text(fmt.Sprintf(`
83
- // document.addEventListener('alpine:init', () => {%s
84
- // });
85
- // `, p.js.String()))))
86
- p.Body.writeHtmlIndent(w, 1)
87
- w.Write([]byte("\n</html>"))
88
- }
89
-
90
- func Html(h *Element, b *Element) HtmlPage {
91
- return HtmlPage{
92
- // ctx: context.WithValue(ctx, "state", &StateContext{}),
93
- classLookup: map[string]bool{},
94
- js: bytes.NewBuffer(nil),
95
- css: bytes.NewBuffer(nil),
96
- Head: h,
97
- Body: b,
98
- }
99
- }
100
-
101
- func Head(elems ...*Element) *Element {
102
- basic := []*Element{
103
- {tag: "meta", selfClosing: true, attrs: map[string]string{"charset": "UTF-8"}},
104
- {tag: "meta", selfClosing: true, attrs: map[string]string{"http-equiv": "Content-Type", "content": "text/html;charset=utf-8"}},
105
- {tag: "meta", selfClosing: true, attrs: map[string]string{"http-equiv": "encoding", "content": "utf-8"}},
106
- }
107
- return &Element{tag: "head", children: append(basic, elems...)}
108
- }
109
-
110
- func Body(elems ...*Element) *Element {
111
- return &Element{tag: "body", children: elems}
112
- }
113
-
114
- type Element struct {
115
- tag string
116
- attrs map[string]string
117
- children []*Element
118
- selfClosing bool
119
- text string
120
- }
121
-
122
- func NewElement(tag string, selfClosing bool, uis ...interface{}) *Element {
123
- return mergeAttributes(&Element{tag: tag, selfClosing: selfClosing}, uis...)
124
- }
125
-
126
- func (e *Element) setAttr(k string, v string) {
127
- if e.attrs == nil {
128
- e.attrs = make(map[string]string)
129
- }
130
-
131
- switch k {
132
- case "style", "allow":
133
- s := e.attrs[k] + v + ";"
134
- e.attrs[k] = s
135
- return
136
-
137
- case "class":
138
- s := e.attrs[k]
139
- if s != "" {
140
- s += " "
141
- }
142
- s += v
143
- e.attrs[k] = s
144
- return
145
- }
146
- if v == "false" {
147
- delete(e.attrs, k)
148
- return
149
- } else if v == "true" {
150
- e.attrs[k] = ""
151
- } else {
152
- e.attrs[k] = v
153
- }
154
- }
155
-
156
- func (e *Element) writeHtmlIndent(w io.Writer, indent int) {
157
- writeIndent(w, indent)
158
- if e.tag == "text" {
159
- writeIndent(w, indent)
160
- w.Write([]byte(e.text))
161
- return
162
- }
163
- w.Write([]byte("<"))
164
- w.Write([]byte(e.tag))
165
-
166
- for k, v := range e.attrs {
167
- w.Write([]byte(" "))
168
- w.Write([]byte(k))
169
-
170
- if v != "" {
171
- w.Write([]byte(`="`))
172
- w.Write([]byte(v))
173
- w.Write([]byte(`"`))
174
- }
175
- }
176
-
177
- w.Write([]byte(">"))
178
-
179
- if e.selfClosing {
180
- return
181
- }
182
-
183
- for _, c := range e.children {
184
- if len(e.children) > 1 {
185
- w.Write([]byte("\n"))
186
- }
187
- if c != nil {
188
- c.writeHtmlIndent(w, indent+1)
189
- }
190
- }
191
-
192
- if len(e.children) != 0 {
193
- // w.Write([]byte("\n"))
194
- writeIndent(w, indent)
195
- }
196
-
197
- w.Write([]byte("</"))
198
- w.Write([]byte(e.tag))
199
- w.Write([]byte(">\n"))
200
- }
201
-
202
- func Title(v string) *Element {
203
- return &Element{
204
- tag: "title",
205
- children: []*Element{Text(v)},
206
- }
207
- }
208
- func Text(v string) *Element {
209
- return &Element{
210
- tag: "text",
211
- text: v,
212
- }
213
- }
214
-
215
- func Meta(name, content string) *Element {
216
- return &Element{
217
- tag: "meta",
218
- selfClosing: true,
219
- attrs: map[string]string{
220
- "name": name,
221
- "content": content,
222
- },
223
- }
224
- }
225
-
226
- func Link(rel, href string) *Element {
227
- return &Element{
228
- tag: "link",
229
- selfClosing: true,
230
- attrs: map[string]string{
231
- "rel": rel,
232
- "href": href,
233
- },
234
- }
235
- }
236
-
237
- func Script(uis ...interface{}) *Element {
238
- return NewElement("script", false, uis...)
239
- }
240
-
241
- func StyleTag(uis ...interface{}) *Element {
242
- return NewElement("style", false, uis...)
243
- }
244
-
245
- func Div(uis ...interface{}) *Element {
246
- return NewElement("div", false, uis...)
247
- }
248
-
249
- func A(uis ...interface{}) *Element {
250
- return NewElement("a", false, uis...)
251
- }
252
-
253
- func P(uis ...interface{}) *Element {
254
- return NewElement("p", false, uis...)
255
- }
256
-
257
- func H1(uis ...interface{}) *Element {
258
- return NewElement("h1", false, uis...)
259
- }
260
- func H2(uis ...interface{}) *Element {
261
- return NewElement("h2", false, uis...)
262
- }
263
- func H3(uis ...interface{}) *Element {
264
- return NewElement("h3", false, uis...)
265
- }
266
- func H4(uis ...interface{}) *Element {
267
- return NewElement("h4", false, uis...)
268
- }
269
- func H5(uis ...interface{}) *Element {
270
- return NewElement("h5", false, uis...)
271
- }
272
- func H6(uis ...interface{}) *Element {
273
- return NewElement("h6", false, uis...)
274
- }
275
-
276
- func Span(uis ...interface{}) *Element {
277
- return NewElement("span", false, uis...)
278
- }
279
-
280
- func Input(uis ...interface{}) *Element {
281
- return NewElement("input", true, uis...)
282
- }
283
-
284
- func Image(uis ...interface{}) *Element {
285
- return NewElement("image", true, uis...)
286
- }
287
-
288
- func Button(uis ...interface{}) *Element {
289
- return NewElement("button", false, uis...)
290
- }
291
-
292
- func Svg(uis ...interface{}) *Element {
293
- return NewElement("svg", false, uis...)
294
- }
295
-
296
- func SvgText(uis ...interface{}) *Element {
297
- return NewElement("text", false, uis...)
298
- }
299
-
300
- func Ul(uis ...interface{}) *Element {
301
- return NewElement("ul", false, uis...)
302
- }
303
-
304
- func Li(uis ...interface{}) *Element {
305
- return NewElement("li", false, uis...)
306
- }
307
-
308
- type Attribute struct {
309
- Key string
310
- Value string
311
- }
312
-
313
- func Attr(k, v string) Attribute {
314
- return Attribute{k, v}
315
- }
316
-
317
- func OnClick(v string) Attribute {
318
- return Attribute{"@click", v}
319
- }
320
-
321
- func ID(v string) Attribute {
322
- return Attribute{"id", v}
323
- }
324
-
325
- func Style(v string) Attribute {
326
- return Attribute{"style", v}
327
- }
328
-
329
- func Accept(v string) Attribute {
330
- return Attribute{"accept", v}
331
- }
332
-
333
- func AutoComplete(v bool) Attribute {
334
- return Attribute{"autocomplete", strconv.FormatBool(v)}
335
- }
336
-
337
- func Checked(v bool) Attribute {
338
- return Attribute{"checked", strconv.FormatBool(v)}
339
- }
340
-
341
- func Disabled(v bool) Attribute {
342
- return Attribute{"disabled", strconv.FormatBool(v)}
343
- }
344
-
345
- func Name(v string) Attribute {
346
- return Attribute{"name", v}
347
- }
348
-
349
- func Type(v string) Attribute {
350
- return Attribute{"type", v}
351
- }
352
-
353
- func Value(v string) Attribute {
354
- return Attribute{"value", v}
355
- }
356
-
357
- func Placeholder(v string) Attribute {
358
- return Attribute{"placeholder", v}
359
- }
360
-
361
- func Src(v string) Attribute {
362
- return Attribute{"src", v}
363
- }
364
-
365
- func Defer() Attribute {
366
- return Attribute{"defer", "true"}
367
- }
368
-
369
- func ViewBox(v string) Attribute {
370
- return Attribute{"viewBox", v}
371
- }
372
-
373
- func X(v string) Attribute {
374
- return Attribute{"x", v}
375
- }
376
-
377
- func Y(v string) Attribute {
378
- return Attribute{"y", v}
379
- }
380
-
381
- func Href(v string) Attribute {
382
- return Attribute{"href", v}
383
- }
384
-
385
- func Target(v string) Attribute {
386
- return Attribute{"target", v}
387
- }
388
-
389
- func Rel(v string) Attribute {
390
- return Attribute{"rel", v}
391
- }
392
-
393
- func Css(v string) Attribute {
394
- return Attribute{"class", v}
395
- }
396
-
397
- func XData(v string) Attribute {
398
- return Attribute{"x-data", v}
399
- }
400
-
401
- func XText(v string) Attribute {
402
- return Attribute{"x-text", v}
403
- }
html_test.go DELETED
@@ -1,111 +0,0 @@
1
- package gromer
2
-
3
- import (
4
- "bytes"
5
- "context"
6
- "strconv"
7
- "testing"
8
-
9
- "github.com/bradleyjkemp/cupaloy"
10
- . "github.com/franela/goblin"
11
- )
12
-
13
- func TestGetRouteParams(t *testing.T) {
14
- g := Goblin(t)
15
- g.Describe("GetRouteParams", func() {
16
- g.It("should return all the right params", func() {
17
- params := GetRouteParams("/api/todos/{id}/update/{action}")
18
- g.Assert(params).Equal([]string{"id", "action"})
19
- })
20
- })
21
- }
22
-
23
- func Row(uis ...interface{}) *Element {
24
- return NewElement("div", false, append([]interface{}{Css("flex flex-row justify-center items-center")}, uis...)...)
25
- }
26
-
27
- func Col(uis ...interface{}) *Element {
28
- return NewElement("div", false, append([]interface{}{Css("flex flex-col justify-center items-center")}, uis...)...)
29
- }
30
-
31
- func Counter(c context.Context, start int) *Element {
32
- count, setCount := UseState(c, start)
33
- increment := func() {
34
- setCount(count().(int) + 1)
35
- }
36
- decrement := func() {
37
- setCount(count().(int) + 1)
38
- }
39
- return Col(Css("text-3xl text-gray-700"),
40
- Row(
41
- Row(Css("underline"),
42
- Text("Counter"),
43
- ),
44
- ),
45
- Row(
46
- Button(OnClick("decrement"), Text("-")),
47
- Row(Css("m-20 text-5xl"), XText("count"),
48
- Text(strconv.Itoa(count().(int))),
49
- ),
50
- Button(OnClick("increment"), Text("+")),
51
- ),
52
- M{
53
- "count": count,
54
- "increment": increment,
55
- "decrement": decrement,
56
- },
57
- )
58
- // return `
59
- // <div class="flex flex-col justify-center items-center text-3xl text-gray-700">
60
- // <div class="flex flex-row justify-center items-center">
61
- // <div class="flex flex-row justify-center items-center underline">
62
- // Counter
63
- // </div>
64
- // </div>
65
- // <div class="flex flex-row justify-center items-center">
66
- // <button class="btn m-20" @click="Increment">
67
- // -
68
- // </button>
69
- // <div class="flex flex-row justify-center items-center m-20 text-8xl">
70
- // {{ count }}
71
- // </div>
72
- // <button class="btn m-20" @click="Decrement">
73
- // +
74
- // </button>
75
- // </div>
76
- // </div>
77
- // `
78
- }
79
-
80
- func TestHtml(t *testing.T) {
81
- g := Goblin(t)
82
- g.Describe("Html", func() {
83
- g.It("should match snapshot", func() {
84
- ctx := WithState(context.Background())
85
- b := bytes.NewBuffer(nil)
86
- p := Html(
87
- Head(
88
- Title("123"),
89
- Meta("description", "123"),
90
- Meta("author", "123"),
91
- Meta("keywords", "123"),
92
- Meta("viewport", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover"),
93
- Link("icon", "/assets/icon.png"),
94
- Link("apple-touch-icon", "/assets/icon.png"),
95
- Link("stylesheet", "/assets/styles.css"),
96
- Script(Src("/assets/alpine.js"), Defer()),
97
- Meta("title", "title"),
98
- ),
99
- Body(
100
- H1(Text("Hello this is a h1")),
101
- H2(Text("Hello this is a h2")),
102
- H3(XData("{ message: 'I ❤️ Alpine' }"), XText("message"), Text("")),
103
- Counter(ctx, 4),
104
- ),
105
- )
106
- p.WriteHtml(b)
107
- c := cupaloy.New(cupaloy.SnapshotFileExtension(".html"))
108
- c.SnapshotT(t, b.String())
109
- })
110
- })
111
- }
http.go CHANGED
@@ -3,17 +3,20 @@ package gromer
3
3
  import (
4
4
  "encoding/json"
5
5
  "fmt"
6
+ "html/template"
6
7
  "net"
7
8
  "net/http"
8
9
  "os"
9
10
  "reflect"
10
11
  "regexp"
12
+ "runtime"
11
13
  "runtime/debug"
12
14
  "strconv"
13
15
  "strings"
14
16
  "time"
15
17
 
16
18
  "github.com/go-playground/validator/v10"
19
+ "github.com/gobuffalo/velvet"
17
20
  "github.com/gorilla/mux"
18
21
  "github.com/rs/zerolog"
19
22
  "github.com/rs/zerolog/log"
@@ -23,6 +26,49 @@ import (
23
26
  var info *debug.BuildInfo
24
27
  var IsCloundRun bool
25
28
 
29
+ type HtmlContent string
30
+
31
+ func Html(tpl string, params map[string]interface{}) (HtmlContent, int, error) {
32
+ ctx := velvet.NewContext()
33
+ for k, v := range params {
34
+ ctx.Set(k, v)
35
+ }
36
+ s, err := velvet.Render(tpl, ctx)
37
+ if err != nil {
38
+ return HtmlContent(""), 500, err
39
+ }
40
+ return HtmlContent(s), 200, nil
41
+ }
42
+
43
+ func HtmlErr(status int, err error) (HtmlContent, int, error) {
44
+ return HtmlContent("ErrorPage/AccessDeniedPage/NotFoundPage based on status code"), status, err
45
+ }
46
+
47
+ func GetFunctionName(temp interface{}) string {
48
+ strs := strings.Split((runtime.FuncForPC(reflect.ValueOf(temp).Pointer()).Name()), ".")
49
+ return strs[len(strs)-1]
50
+ }
51
+
52
+ func RegisterComponent(fn interface{}) {
53
+ name := GetFunctionName(fn)
54
+ // reflect.New(reflect.FuncOf())
55
+ velvet.Helpers.Add(name, func(title string, c velvet.HelperContext) (template.HTML, error) {
56
+ s, err := c.Block()
57
+ if err != nil {
58
+ return "", err
59
+ }
60
+ ctx := velvet.NewContext()
61
+ ctx.Set("title", title)
62
+ ctx.Set("children", template.HTML(s))
63
+ res := reflect.ValueOf(fn).Call([]reflect.Value{})
64
+ comp, err := velvet.Render(res[0].Interface().(string), ctx)
65
+ if err != nil {
66
+ return "", err
67
+ }
68
+ return template.HTML(comp), nil
69
+ })
70
+ }
71
+
26
72
  func init() {
27
73
  IsCloundRun = os.Getenv("K_REVISION") != ""
28
74
  info, _ = debug.ReadBuildInfo()
@@ -135,11 +181,11 @@ func PerformRequest(route string, h interface{}, ctx interface{}, w http.Respons
135
181
  RespondError(w, responseStatus, responseError.(error))
136
182
  return
137
183
  }
138
- if v, ok := response.(HtmlPage); ok {
184
+ if v, ok := response.(HtmlContent); ok {
139
185
  w.Header().Set("Content-Type", "text/html")
140
186
  // This has to be at end always
141
187
  w.WriteHeader(responseStatus)
142
- v.WriteHtml(w)
188
+ w.Write([]byte(v))
143
189
  return
144
190
  }
145
191
  w.Header().Set("Content-Type", "application/json")
@@ -217,6 +263,19 @@ var LogMiddleware = mux.MiddlewareFunc(func(next http.Handler) http.Handler {
217
263
  })
218
264
  })
219
265
 
266
+ func CorsMiddleware(next http.Handler) http.Handler {
267
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
268
+ w.Header().Set("Access-Control-Allow-Origin", "*")
269
+ w.Header().Set("Access-Control-Allow-Methods", "*")
270
+ w.Header().Set("Access-Control-Allow-Headers", "*")
271
+ if r.Method == "OPTIONS" {
272
+ w.WriteHeader(200)
273
+ return
274
+ }
275
+ next.ServeHTTP(w, r)
276
+ })
277
+ }
278
+
220
279
  var NotFoundHandler = LogMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
221
280
  RespondError(w, 404, fmt.Errorf("path '%s' not found", r.URL.String()))
222
281
  }))
utils.go CHANGED
@@ -1,6 +1,7 @@
1
1
  package gromer
2
2
 
3
3
  import (
4
+ "fmt"
4
5
  "reflect"
5
6
  "strings"
6
7
  "time"
@@ -68,3 +69,16 @@ func GetValidationError(err validator.ValidationErrors) map[string]string {
68
69
  }
69
70
  return emap
70
71
  }
72
+
73
+ func Zero[T any](s ...T) T {
74
+ var zero T
75
+ return zero
76
+ }
77
+
78
+ func Default[S any](a, b S) S {
79
+ va := fmt.Sprintf("%v", a)
80
+ if va == "" || va == "0" {
81
+ return b
82
+ }
83
+ return a
84
+ }