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


564b4fef Peter John

3 years ago
improve codegen
api_explorer.go CHANGED
@@ -1,348 +1,13 @@
1
1
  package gromer
2
2
 
3
3
  import (
4
- "context"
5
4
  "encoding/json"
6
- "fmt"
7
5
  )
8
6
 
9
- func Select(uis ...interface{}) *Element {
10
- return NewElement("select", false, uis...)
11
- }
12
-
13
- func Option(uis ...interface{}) *Element {
14
- return NewElement("option", false, uis...)
15
- }
16
-
17
- func Table(uis ...interface{}) *Element {
18
- return NewElement("table", false, uis...)
19
- }
20
-
21
- func TR(uis ...interface{}) *Element {
22
- return NewElement("tr", false, uis...)
23
- }
24
-
25
- func TD(uis ...interface{}) *Element {
26
- return NewElement("td", false, uis...)
27
- }
28
-
29
- func Section(title string) *Element {
30
- return Div(Css("text-gray-700 text-sm font-bold uppercase pl-2 pt-2 pb-2 bg-gray-50 border-b border-gray-200"), Text(title))
31
- }
32
-
33
- type ApiDefinition struct {
34
- Method string `json:"method"`
35
- Path string `json:"path"`
36
- PathParams []string `json:"pathParams"`
37
- Params map[string]interface{} `json:"params"`
38
- }
39
-
40
- func ApiExplorer(apiDefs []ApiDefinition) func(c context.Context) (HtmlPage, int, error) {
41
- return func(c context.Context) (HtmlPage, int, error) {
42
- data, err := json.Marshal(apiDefs)
43
- if err != nil {
44
- return Html(nil, nil), 500, err
45
- }
46
- options := []interface{}{ID("api-select"), Css("form-select block")}
47
- for i, c := range apiDefs {
48
- options = append(options, Option(Attr("value", fmt.Sprintf("%d", i)), Div(Text(fmt.Sprintf("%0.6s %s", c.Method, c.Path)))))
49
- }
50
- return Html(
51
- Head(
52
- Title("Example"),
53
- Meta("description", "Example"),
54
- Meta("author", "pyros.sh"),
55
- Meta("keywords", "pyros.sh, gromer"),
56
- Meta("viewport", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover"),
57
- Link("icon", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEU0OkArMjhobHEoPUPFEBIuO0L+AAC2FBZ2JyuNICOfGx7xAwTjCAlCNTvVDA1aLzQ3COjMAAAAVUlEQVQI12NgwAaCDSA0888GCItjn0szWGBJTVoGSCjWs8TleQCQYV95evdxkFT8Kpe0PLDi5WfKd4LUsN5zS1sKFolt8bwAZrCaGqNYJAgFDEpQAAAzmxafI4vZWwAAAABJRU5ErkJggg=="),
58
- Link("stylesheet", "https://cdn.jsdelivr.net/npm/codemirror@5.63.1/lib/codemirror.css"),
59
- StyleTag(Text(`
60
- html, body {
61
- height: 100vh;
62
- }
63
-
64
- #left .CodeMirror {
65
- height: 400px;
66
- }
67
-
68
- #right .CodeMirror {
69
- height: calc(100vh - 60px);
70
- }
71
-
72
- .form-select {
73
- background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23a0aec0'%3e%3cpath d='M15.3 9.3a1 1 0 0 1 1.4 1.4l-4 4a1 1 0 0 1-1.4 0l-4-4a1 1 0 0 1 1.4-1.4l3.3 3.29 3.3-3.3z'/%3e%3c/svg%3e");
74
- -webkit-appearance: none;
75
- -moz-appearance: none;
76
- appearance: none;
77
- -webkit-print-color-adjust: exact;
78
- color-adjust: exact;
79
- background-repeat: no-repeat;
80
- background-color: #fff;
81
- border-color: #e2e8f0;
82
- border-width: 1px;
83
- border-radius: 0.25rem;
84
- padding-top: 0.5rem;
85
- padding-right: 2.5rem;
86
- padding-bottom: 0.5rem;
87
- padding-left: 0.75rem;
88
- font-size: 1rem;
89
- line-height: 1.5;
90
- background-position: right 0.5rem center;
91
- background-size: 1.5em 1.5em;
92
- }
93
-
94
- .form-select:focus {
95
- outline: none;
96
- box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
97
- border-color: #63b3ed;
98
- }
99
-
100
- table {
101
- width: 100%;
102
- }
103
-
104
- tr {
105
- width: 100%;
106
- }
107
-
108
- td, th {
109
- border-bottom: 1px solid rgb(204, 204, 204);
110
- border-left: 1px solid rgb(204, 204, 204);
111
- text-align: left;
112
- }
113
-
114
- textarea:focus, input:focus{
115
- outline: none;
116
- box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
117
- border-color: #63b3ed;
118
- }
119
-
120
- *:focus {
121
- outline: none;
122
- box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
123
- border-color: #63b3ed;
124
- }
125
-
126
- .spinner {
127
- animation: rotate 2s linear infinite;
128
- width: 24px;
129
- height: 24px;
130
- }
131
-
132
- .spinner .path {
133
- stroke: rgba(249, 250, 251, 1);
134
- stroke-linecap: round;
135
- animation: dash 1.5s ease-in-out infinite;
136
- }
137
-
138
- @keyframes rotate {
139
- 100% {
140
- transform: rotate(360deg);
141
- }
142
- }
143
-
144
- @keyframes dash {
145
- 0% {
146
- stroke-dasharray: 1, 150;
147
- stroke-dashoffset: 0;
148
- }
149
- 50% {
150
- stroke-dasharray: 90, 150;
151
- stroke-dashoffset: -35;
152
- }
153
- 100% {
154
- stroke-dasharray: 90, 150;
155
- stroke-dashoffset: -124;
156
- }
157
- }
158
- `)),
159
- Script(Src("https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.1/codemirror.min.js")),
160
- Script(Src("https://cdn.jsdelivr.net/npm/codemirror@5.63.1/mode/javascript/javascript.js")),
161
- ),
162
- Body(
163
- Div(Css("flex flex-col"),
164
- Div(Css("flex w-full p-2 bg-gray-50 border-b border-gray-200 items-center justify-start"),
165
- Div(Css("flex mr-4 text-gray-700 text-2xl font-bold"), Text("API Explorer")),
166
- Div(Css("text-xl"),
167
- Select(options...),
168
- ),
169
- Div(Css("flex ml-3 mr-3"), Button(ID("run"), Css("bg-gray-200 border border-gray-400 hover:bg-gray-200 focus:outline-none rounded-md text-gray-700 text-md font-bold pt-2 pb-2 pl-6 pr-6"),
170
- Text("RUN"),
171
- )),
172
- ),
173
- Div(Css("flex"),
174
- Div(Css("flex flex-row"), Style("width: 50%;"),
175
- Div(Css("pr-8 border-r border-gray-300"), Style("background: #f7f7f7;")),
176
- Div(Css("w-full"),
177
- Section("Headers"),
178
- Table(ID("headersTable"),
179
- TR(
180
- TD(Input(Css("w-full p-1"), Attr("value", "Authorization"))),
181
- TD(Input(Css("w-full p-1"))),
182
- ),
183
- ),
184
- Section("Path Params"),
185
- Table(ID("pathParamsTable"),
186
- TR(
187
- TD(Css("text-gray-700"), Style("width: 50%;"), Div(Css("p-1"), Text("123"))),
188
- TD(Style("width: 50%;"), Input(Css("w-full p-1"))),
189
- ),
190
- ),
191
- Section("Query Params"),
192
- Table(ID("queryParamsTable"),
193
- TR(
194
- TD(Css("text-gray-700"), Style("width: 50%;"), Div(Css("p-1"), Text("123"))),
195
- TD(Style("width: 50%;"), Input(Css("w-full p-1"))),
196
- ),
197
- ),
198
- Section("Body"),
199
- Div(ID("left"), Css("border-b border-gray-200 text-md")),
200
- ),
201
- ),
202
- Div(Css("flex flex-row"), Style("width: 50%;"),
203
- Div(ID("right"), Css("w-full border-l border-l-gray-200 text-md")),
204
- Div(Css("pr-8 border-l border-gray-300"), Style("background: #f7f7f7;")),
205
- ),
206
- ),
207
- ),
208
- Script(Text("window.apiDefs = "+string(data))),
209
- Script(Text("window.codeLeft = CodeMirror(document.getElementById('left'), {value: '{}',mode: 'javascript' })")),
210
- Script(Text("window.codeRight = CodeMirror(document.getElementById('right'), {value: '',mode: 'javascript', lineNumbers: true, readOnly: true, lineWrapping: true })")),
211
- Script(Text(`
212
- const getCurrentApiCall = () => {
213
- const index = document.getElementById("api-select").value;
214
- return window.apiDefs[index];
215
- }
216
-
217
- const updatePathParams = (apiCall) => {
218
- const table = document.getElementById("pathParamsTable");
219
- if (apiCall.pathParams.length === 0) {
220
- table.innerHTML = "<div style='background-color: rgb(245, 245, 245); padding: 0.25rem; text-align: center; color: gray;'>NONE</div>";
221
- } else {
222
- table.innerHTML = "";
223
- }
224
- for(const param of apiCall.pathParams.reverse()) {
225
- const row = table.insertRow(0);
226
- const cell1 = row.insertCell(0);
227
- const cell2 = row.insertCell(1);
228
- cell1.style = "width: 30%; border-left: 0px;";
229
- cell1.class = "text-gray-700";
230
- cell2.style = "width: 70%;";
231
- cell1.innerHTML = "<div class='p-1'>" + param + "</div>";
232
- cell2.innerHTML = "<input id='path-param-" + param + "' class='w-full p-1'>";
233
- }
234
- }
235
-
236
- const updateParams = (apiCall) => {
237
- const table = document.getElementById("queryParamsTable");
238
- if (!apiCall.params) {
239
- table.innerHTML = "<div style='background-color: rgb(245, 245, 245); padding: 0.25rem; text-align: center; color: gray;'>NONE</div>";
240
- } else {
241
- table.innerHTML = "";
242
- }
243
- if (apiCall.method === "GET" || apiCall.method === "DELETE") {
244
- for(const key of Object.keys(apiCall.params)) {
245
- const row = table.insertRow(0);
246
- const cell1 = row.insertCell(0);
247
- const cell2 = row.insertCell(1);
248
- cell1.style = "width: 30%; border-left: 0px;";
249
- cell1.class = "text-gray-700";
250
- cell2.style = "width: 70%;";
251
- cell1.innerHTML = "<div class='p-1'>" + key + "</div>";
252
- cell2.innerHTML = "<input id='query-param-" + key + "' class='w-full p-1'>";
253
- }
254
- }
255
- }
256
-
257
- const updateBody = (apiCall) => {
258
- if (apiCall.method !== "GET" && apiCall.method !== "DELETE") {
259
- window.codeLeft.setValue(JSON.stringify(apiCall.params, 2, 2));
260
- } else {
261
- window.codeLeft.setValue("");
262
- }
263
- }
264
-
265
- const init = () => {
266
- updatePathParams(window.apiDefs[0]);
267
- updateParams(window.apiDefs[0]);
268
- updateBody(window.apiDefs[0]);
269
- const headersJson = localStorage.getItem("headers");
270
- if (headersJson) {
271
- const table = document.getElementById("headersTable");
272
- const headers = JSON.parse(headersJson);
273
- table.innerHTML = "";
274
- for(const key of Object.keys(headers)) {
275
- const value = headers[key];
276
- const row = table.insertRow(0);
277
- const cell1 = row.insertCell(0);
278
- const cell2 = row.insertCell(1);
279
- cell1.style = "width: 30%; border-left: 0px;";
280
- cell2.style = "width: 70%;";
281
- cell1.innerHTML = "<input value='" + key + "' class='w-full p-1'>";
282
- cell2.innerHTML = "<input value='" + value + "' class='w-full p-1'>";
283
- }
284
- }
285
- }
286
-
287
- window.onload = () => {
288
- init();
289
- }
290
-
291
- document.getElementById("api-select").onchange = () => {
292
- const apiCall = getCurrentApiCall();
293
- updatePathParams(apiCall);
294
- updateParams(apiCall);
295
- updateBody(apiCall);
296
- }
297
-
298
- const run = document.getElementById("run");
299
- run.onclick = async () => {
300
- run.innerHTML = "<svg class='spinner' viewBox='0 0 50 50'><circle class='path' cx='25' cy='25' r='20' fill='none' stroke-width='5'></circle></svg>";
301
- const table = document.getElementById("headersTable");
302
- const headers = {};
303
- for(const row of table.rows) {
304
- const key = row.cells[0].children[0].value;
305
- const value = row.cells[1].children[0].value;
306
- headers[key] = value;
307
- }
308
- const apiCall = getCurrentApiCall();
309
- let path = apiCall.path;
310
- const bodyParams = {};
311
- if (apiCall.method !== "GET" && apiCall.method != "DELETE") {
312
- bodyParams["body"] = window.codeLeft.getValue();
313
- } else {
314
- for(const param of apiCall.pathParams) {
315
- const value = document.getElementById('path-param-' + param).value;
316
- path = path.replace('{' + param + '}', value);
317
- }
318
- const paramsKeys = Object.keys(apiCall.params);
319
- if (paramsKeys.length > 0) {
320
- path += "?";
321
- paramsKeys.forEach((key, i) => {
322
- const value = document.getElementById('query-param-' + key).value;
323
- path += key+"="+value;
324
- if (i !== paramsKeys.length - 1) {
325
- path += "&";
326
- }
327
- });
328
- }
329
- }
330
- localStorage.setItem("headers", JSON.stringify(headers));
331
- try {
332
- const res = await fetch(path, {
333
- method: apiCall.method,
334
- headers,
335
- ...bodyParams
336
- });
337
- const json = await res.json();
338
- window.codeRight.setValue(JSON.stringify(json, 2, 2));
339
- } catch (err) {
340
- window.codeRight.setValue(JSON.stringify({ error: err.message }, 2, 2));
341
- }
342
- run.innerHTML = "RUN";
343
- }
344
- `)),
345
- ),
346
- ), 200, nil
7
+ func ApiExplorer() (HtmlContent, int, error) {
8
+ _, err := json.Marshal(RouteDefs)
9
+ if err != nil {
10
+ return HtmlErr(500, err)
347
11
  }
12
+ return Html("", nil)
348
13
  }
cmd/gromer/main.go CHANGED
@@ -4,9 +4,6 @@ import (
4
4
  "bytes"
5
5
  "flag"
6
6
  "fmt"
7
- "go/ast"
8
- "go/parser"
9
- "go/token"
10
7
  "io/ioutil"
11
8
  "log"
12
9
  "os"
@@ -83,28 +80,6 @@ func rewritePkg(pkg string) string {
83
80
  return lastItem
84
81
  }
85
82
 
86
- func getApiFunc(method, route string, pathParams []string, params map[string]interface{}) gromer.ApiDefinition {
87
- muxRoute := bytes.NewBuffer(nil)
88
- foundStart := false
89
- for _, v := range route {
90
- if string(v) == "_" && !foundStart {
91
- foundStart = true
92
- muxRoute.WriteString("{")
93
- } else if string(v) == "_" && foundStart {
94
- foundStart = false
95
- muxRoute.WriteString("}")
96
- } else {
97
- muxRoute.WriteString(string(v))
98
- }
99
- }
100
- return gromer.ApiDefinition{
101
- Method: method,
102
- Path: muxRoute.String(),
103
- PathParams: pathParams,
104
- Params: params,
105
- }
106
- }
107
-
108
83
  func lowerFirst(s string) string {
109
84
  for i, v := range s {
110
85
  return string(unicode.ToLower(v)) + s[i+1:]
@@ -129,8 +104,6 @@ func main() {
129
104
  } else {
130
105
  moduleName = *pkgFlag
131
106
  }
132
- routes := []*Route{}
133
- apiDefs := []gromer.ApiDefinition{}
134
107
  allPkgs := map[string]string{}
135
108
  err := filepath.Walk("pages",
136
109
  func(filesrc string, info os.FileInfo, err error) error {
@@ -147,80 +120,35 @@ func main() {
147
120
  path = "/"
148
121
  pkg = "pages"
149
122
  }
150
- routePath := rewritePath(path)
151
- pathParams := gromer.GetRouteParams(routePath)
152
- routes = append(routes, &Route{
123
+ gromer.RouteDefs = append(gromer.RouteDefs, gromer.RouteDefinition{
153
124
  Method: method,
154
- Path: routePath,
125
+ Path: rewritePath(path),
155
126
  Pkg: rewritePkg(pkg),
156
127
  })
157
- if strings.Contains(path, "/api/") {
158
- data, err := ioutil.ReadFile(filesrc)
159
- if err != nil {
160
- panic(err)
161
- }
162
- fset := token.NewFileSet()
163
- f, err := parser.ParseFile(fset, "", string(data), parser.AllErrors)
164
- if err != nil {
165
- panic(err)
166
- }
167
- var params map[string]interface{}
168
- mapsOfInputParams := map[string]map[string]interface{}{}
169
- for _, d := range f.Decls {
170
- if decl, ok := d.(*ast.GenDecl); ok {
171
- for _, spec := range decl.Specs {
172
- if typeSpec, ok := spec.(*ast.TypeSpec); ok {
173
- if st, ok := typeSpec.Type.(*ast.StructType); ok {
174
- mapsOfInputParams[typeSpec.Name.Name] = map[string]interface{}{}
175
- for _, f := range st.Fields.List {
176
- if len(f.Names) > 0 {
177
- fieldName := lowerFirst(f.Names[0].Name)
178
- mapsOfInputParams[typeSpec.Name.Name][fieldName] = fmt.Sprintf("%+s", f.Type)
179
- }
180
- }
181
- }
182
- }
183
- }
184
- }
185
- if decl, ok := d.(*ast.FuncDecl); ok {
186
- if decl.Name.Name == method {
187
- list := decl.Type.Params.List
188
- lastParam := list[len(list)-1]
189
- if spec, ok := lastParam.Type.(*ast.Ident); ok {
190
- if spec.IsExported() {
191
- // TODO: need to read from imported files and pick up the struct and
192
- // convert it to json
193
- } else if v, ok := mapsOfInputParams[spec.Name]; ok {
194
- params = v
195
- }
196
- }
197
- }
198
- }
199
- }
200
- apiDefs = append(apiDefs, getApiFunc(method, path, pathParams, params))
201
- }
202
128
  }
203
129
  return nil
204
130
  })
205
131
  if err != nil {
206
132
  log.Fatal(err)
207
133
  }
208
- for _, r := range routes {
134
+ for _, r := range gromer.RouteDefs {
209
135
  fmt.Printf("%-6s %s\n", r.Method, r.Path)
210
136
  }
137
+ err = velvet.Helpers.Add("title", func(v string) string {
138
+ return strings.Title(strings.ToLower(v))
139
+ })
140
+ if err != nil {
141
+ log.Fatal(err)
142
+ }
211
143
  ctx := velvet.NewContext()
212
144
  ctx.Set("moduleName", moduleName)
213
145
  ctx.Set("allPkgs", allPkgs)
214
- ctx.Set("routes", routes)
146
+ ctx.Set("routes", gromer.RouteDefs)
215
- ctx.Set("apiDefs", apiDefs)
216
147
  ctx.Set("tick", "`")
217
148
  s, err := velvet.Render(`// Code generated by gromer. DO NOT EDIT.
218
149
  package main
219
150
 
220
151
  import (
221
- c "context"
222
- "embed"
223
- "net/http"
224
152
  "os"
225
153
 
226
154
  "github.com/gorilla/mux"
@@ -228,22 +156,20 @@ import (
228
156
  "github.com/rs/zerolog/log"
229
157
  "gocloud.dev/server"
230
158
 
159
+ "{{ moduleName }}/assets"
231
160
  {{#each allPkgs }}"{{ moduleName }}/pages{{ @key }}"
232
161
  {{/each}}
233
162
  )
234
163
 
235
- //go:embed assets/*
236
- var assetsFS embed.FS
237
-
238
164
  func main() {
239
165
  port := os.Getenv("PORT")
240
166
  r := mux.NewRouter()
241
167
  r.Use(gromer.CorsMiddleware)
242
168
  r.Use(gromer.LogMiddleware)
243
169
  r.NotFoundHandler = gromer.NotFoundHandler
244
- r.PathPrefix("/assets/").Handler(wrapCache(http.FileServer(http.FS(assetsFS))))
170
+ gromer.Static(r, "/assets/", assets.FS)
245
- handle(r, "GET", "/api", gromer.ApiExplorer(apiDefinitions()))
171
+ gromer.Handle(r, "GET", "/api", gromer.ApiExplorer, nil)
246
- {{#each routes as |route| }}handle(r, "{{ route.Method }}", "{{ route.Path }}", {{ route.Pkg }}.{{ route.Method }})
172
+ {{#each routes as |route| }}gromer.Handle(r, "{{ route.Method }}", "{{ route.Path }}", {{ route.Pkg }}.{{ route.Method }}, {{ route.Pkg }}.{{ title route.Method }}Params{})
247
173
  {{/each}}
248
174
  println("http server listening on http://localhost:"+port)
249
175
  srv := server.New(r, nil)
@@ -251,38 +177,6 @@ func main() {
251
177
  log.Fatal().Stack().Err(err).Msg("failed to listen")
252
178
  }
253
179
  }
254
-
255
- func wrapCache(h http.Handler) http.Handler {
256
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
257
- w.Header().Set("Cache-Control", "public, max-age=2592000")
258
- h.ServeHTTP(w, r)
259
- })
260
- }
261
-
262
- func handle(router *mux.Router, method, route string, h interface{}) {
263
- router.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
264
- ctx := c.WithValue(
265
- c.WithValue(
266
- c.WithValue(r.Context(), "assetsFS", assetsFS),
267
- "url", r.URL),
268
- "header", r.Header)
269
- gromer.PerformRequest(route, h, ctx, w, r)
270
- }).Methods(method, "OPTIONS")
271
- }
272
-
273
- func apiDefinitions() []gromer.ApiDefinition {
274
- return []gromer.ApiDefinition{
275
- {{#each apiDefs as |api| }}
276
- {
277
- Method: "{{api.Method}}",
278
- Path: "{{api.Path}}",
279
- PathParams: []string{ {{#each api.PathParams as |param| }}"{{param}}", {{/each}} },
280
- Params: map[string]interface{}{
281
- {{#each api.Params }}"{{ @key }}": "{{ @value }}", {{/each}}
282
- },
283
- },{{/each}}
284
- }
285
- }
286
180
  `, ctx)
287
181
  if err != nil {
288
182
  panic(err)
css.go DELETED
@@ -1,536 +0,0 @@
1
- package gromer
2
-
3
- type M map[string]interface{}
4
- type MS map[string]string
5
- type Arr []interface{}
6
-
7
- type KeyValues struct {
8
- Keys M
9
- Values MS
10
- }
11
-
12
- var colors = KeyValues{
13
- Keys: M{
14
- "bg": "background-color",
15
- "text": "color",
16
- "divide": "border-color",
17
- "border": "border-color",
18
- "ring": "--tw-ring-color",
19
- "border-l": "border-left-color",
20
- "border-r": "border-right-color",
21
- "border-t": "border-top-color",
22
- "border-b": "border-bottom-color",
23
- },
24
- Values: MS{
25
- "transparent": "transparent",
26
- "current": "currentColor",
27
- "black": "rgba(0, 0, 0, 1)",
28
- "white": "rgba(255, 255, 255, 1)",
29
- "gray-50": "rgba(249, 250, 251, 1)",
30
- "gray-100": "rgba(243, 244, 246, 1)",
31
- "gray-200": "rgba(229, 231, 235, 1)",
32
- "gray-300": "rgba(209, 213, 219, 1)",
33
- "gray-400": "rgba(156, 163, 175, 1)",
34
- "gray-500": "rgba(107, 114, 128, 1)",
35
- "gray-600": "rgba(75, 85, 99, 1)",
36
- "gray-700": "rgba(55, 65, 81, 1)",
37
- "gray-800": "rgba(31, 41, 55, 1)",
38
- "gray-900": "rgba(17, 24, 39, 1)",
39
- "red-50": "rgba(254, 242, 242, 1)",
40
- "red-100": "rgba(254, 226, 226, 1)",
41
- "red-200": "rgba(254, 202, 202, 1)",
42
- "red-300": "rgba(252, 165, 165, 1)",
43
- "red-400": "rgba(248, 113, 113, 1)",
44
- "red-500": "rgba(239, 68, 68, 1)",
45
- "red-600": "rgba(220, 38, 38, 1)",
46
- "red-700": "rgba(185, 28, 28, 1)",
47
- "red-800": "rgba(153, 27, 27, 1)",
48
- "red-900": "rgba(127, 29, 29, 1)",
49
- "yellow-50": "rgba(255, 251, 235, 1)",
50
- "yellow-100": "rgba(254, 243, 199, 1)",
51
- "yellow-200": "rgba(253, 230, 138, 1)",
52
- "yellow-300": "rgba(252, 211, 77, 1)",
53
- "yellow-400": "rgba(251, 191, 36, 1)",
54
- "yellow-500": "rgba(245, 158, 11, 1)",
55
- "yellow-600": "rgba(217, 119, 6, 1)",
56
- "yellow-700": "rgba(180, 83, 9, 1)",
57
- "yellow-800": "rgba(146, 64, 14, 1)",
58
- "yellow-900": "rgba(120, 53, 15, 1)",
59
- "green-50": "rgba(236, 253, 245, 1)",
60
- "green-100": "rgba(209, 250, 229, 1)",
61
- "green-200": "rgba(167, 243, 208, 1)",
62
- "green-300": "rgba(110, 231, 183, 1)",
63
- "green-400": "rgba(52, 211, 153, 1)",
64
- "green-500": "rgba(16, 185, 129, 1)",
65
- "green-600": "rgba(5, 150, 105, 1)",
66
- "green-700": "rgba(4, 120, 87, 1)",
67
- "green-800": "rgba(6, 95, 70, 1)",
68
- "green-900": "rgba(6, 78, 59, 1)",
69
- "blue-50": "rgba(239, 246, 255, 1)",
70
- "blue-100": "rgba(219, 234, 254, 1)",
71
- "blue-200": "rgba(191, 219, 254, 1)",
72
- "blue-300": "rgba(147, 197, 253, 1)",
73
- "blue-400": "rgba(96, 165, 250, 1)",
74
- "blue-500": "rgba(59, 130, 246, 1)",
75
- "blue-600": "rgba(37, 99, 235, 1)",
76
- "blue-700": "rgba(29, 78, 216, 1)",
77
- "blue-800": "rgba(30, 64, 175, 1)",
78
- "blue-900": "rgba(30, 58, 138, 1)",
79
- "indigo-50": "rgba(238, 242, 255, 1)",
80
- "indigo-100": "rgba(224, 231, 255, 1)",
81
- "indigo-200": "rgba(199, 210, 254, 1)",
82
- "indigo-300": "rgba(165, 180, 252, 1)",
83
- "indigo-400": "rgba(129, 140, 248, 1)",
84
- "indigo-500": "rgba(99, 102, 241, 1)",
85
- "indigo-600": "rgba(79, 70, 229, 1)",
86
- "indigo-700": "rgba(67, 56, 202, 1)",
87
- "indigo-800": "rgba(55, 48, 163, 1)",
88
- "indigo-900": "rgba(49, 46, 129, 1)",
89
- "purple-50": "rgba(245, 243, 255, 1)",
90
- "purple-100": "rgba(237, 233, 254, 1)",
91
- "purple-200": "rgba(221, 214, 254, 1)",
92
- "purple-300": "rgba(196, 181, 253, 1)",
93
- "purple-400": "rgba(167, 139, 250, 1)",
94
- "purple-500": "rgba(139, 92, 246, 1)",
95
- "purple-600": "rgba(124, 58, 237, 1)",
96
- "purple-700": "rgba(109, 40, 217, 1)",
97
- "purple-800": "rgba(91, 33, 182, 1)",
98
- "purple-900": "rgba(76, 29, 149, 1)",
99
- "pink-50": "rgba(253, 242, 248, 1)",
100
- "pink-100": "rgba(252, 231, 243, 1)",
101
- "pink-200": "rgba(251, 207, 232, 1)",
102
- "pink-300": "rgba(249, 168, 212, 1)",
103
- "pink-400": "rgba(244, 114, 182, 1)",
104
- "pink-500": "rgba(236, 72, 153, 1)",
105
- "pink-600": "rgba(219, 39, 119, 1)",
106
- "pink-700": "rgba(190, 24, 93, 1)",
107
- "pink-800": "rgba(157, 23, 77, 1)",
108
- "pink-900": "rgba(131, 24, 67, 1)",
109
- },
110
- }
111
-
112
- var spacing = KeyValues{
113
- Keys: M{
114
- "mr": "margin-right",
115
- "ml": "margin-left",
116
- "mt": "margin-top",
117
- "mb": "margin-bottom",
118
- "mx": Arr{
119
- "margin-left",
120
- "margin-right",
121
- },
122
- "my": Arr{
123
- "margin-top",
124
- "margin-bottom",
125
- },
126
- "m": "margin",
127
- "pr": "padding-right",
128
- "pl": "padding-left",
129
- "pt": "padding-top",
130
- "pb": "padding-bottom",
131
- "px": Arr{
132
- "padding-left",
133
- "padding-right",
134
- },
135
- "py": Arr{
136
- "padding-top",
137
- "padding-bottom",
138
- },
139
- "p": "padding",
140
- },
141
- Values: MS{
142
- "0": "0px",
143
- "1": "0.25rem",
144
- "2": "0.5rem",
145
- "3": "0.75rem",
146
- "4": "1rem",
147
- "5": "1.25rem",
148
- "6": "1.5rem",
149
- "7": "1.75rem",
150
- "8": "2rem",
151
- "9": "2.25rem",
152
- "10": "2.5rem",
153
- "11": "2.75rem",
154
- "12": "3rem",
155
- "14": "3.5rem",
156
- "16": "4rem",
157
- "20": "5rem",
158
- "24": "6rem",
159
- "28": "7rem",
160
- "32": "8rem",
161
- "36": "9rem",
162
- "40": "10rem",
163
- "44": "11rem",
164
- "48": "12rem",
165
- "52": "13rem",
166
- "56": "14rem",
167
- "60": "15rem",
168
- "64": "16rem",
169
- "72": "18rem",
170
- "80": "20rem",
171
- "96": "24rem",
172
- "auto": "auto",
173
- "px": "1px",
174
- "0.5": "0.125rem",
175
- "1.5": "0.375rem",
176
- "2.5": "0.625rem",
177
- "3.5": "0.875rem",
178
- },
179
- }
180
-
181
- var radius = KeyValues{
182
- Keys: M{
183
- "rounded": "border-radius",
184
- "rounded-t": "border-top-radius",
185
- "rounded-r": "border-right-radius",
186
- "rounded-l": "border-left-radius",
187
- "rounded-b": "border-bottom-radius",
188
- "rounded-tl": Arr{
189
- "border-top-radius",
190
- "border-left-radius",
191
- },
192
- "rounded-tr": Arr{
193
- "border-top-radius",
194
- "border-right-radius",
195
- },
196
- "rounded-bl": Arr{
197
- "border-bottom-radius",
198
- "border-left-radius",
199
- },
200
- "rounded-br": Arr{
201
- "border-bottom-radius",
202
- "border-right-radius",
203
- },
204
- },
205
- Values: MS{
206
- "none": "0px",
207
- "sm": "0.125rem",
208
- "": "0.25rem",
209
- "md": "0.375rem",
210
- "lg": "0.5rem",
211
- "xl": "0.75rem",
212
- "2xl": "1rem",
213
- "3xl": "1.5rem",
214
- "full": "9999px",
215
- },
216
- }
217
-
218
- var borders = KeyValues{
219
- Keys: M{
220
- "border": "border-width",
221
- "border-l": "border-left-width",
222
- "border-r": "border-right-width",
223
- "border-t": "border-top-width",
224
- "border-b": "border-bottom-width",
225
- },
226
- Values: MS{
227
- "0": "0px",
228
- "2": "2px",
229
- "4": "4px",
230
- "8": "8px",
231
- "": "1px",
232
- },
233
- }
234
-
235
- var sizes = KeyValues{
236
- Keys: M{
237
- "h": "height",
238
- "w": "width",
239
- "top": "top",
240
- "left": "left",
241
- "bottom": "bottom",
242
- "right": "right",
243
- "minh": "min-height",
244
- "minw": "min-width",
245
- "maxh": "max-height",
246
- "maxw": "max-width",
247
- },
248
- Values: MS{
249
- "auto": "auto",
250
- "min": "min-content",
251
- "max": "max-content",
252
- "0": "0px",
253
- "1": "0.25rem",
254
- "2": "0.5rem",
255
- "3": "0.75rem",
256
- "4": "1rem",
257
- "5": "1.25rem",
258
- "6": "1.5rem",
259
- "7": "1.75rem",
260
- "8": "2rem",
261
- "9": "2.25rem",
262
- "10": "2.5rem",
263
- "11": "2.75rem",
264
- "12": "3rem",
265
- "14": "3.5rem",
266
- "16": "4rem",
267
- "20": "5rem",
268
- "24": "6rem",
269
- "28": "7rem",
270
- "32": "8rem",
271
- "36": "9rem",
272
- "40": "10rem",
273
- "44": "11rem",
274
- "48": "12rem",
275
- "52": "13rem",
276
- "56": "14rem",
277
- "60": "15rem",
278
- "64": "16rem",
279
- "72": "18rem",
280
- "80": "20rem",
281
- "96": "24rem",
282
- "px": "1px",
283
- "0.5": "0.125rem",
284
- "1.5": "0.375rem",
285
- "2.5": "0.625rem",
286
- "3.5": "0.875rem",
287
- "1/2": "50%",
288
- "1/3": "33.33%",
289
- "2/3": "66.66%",
290
- "1/4": "25%",
291
- "2/4": "50%",
292
- "3/4": "75%",
293
- "1/5": "20%",
294
- "2/5": "40%",
295
- "3/5": "60%",
296
- "4/5": "80%",
297
- "1/6": "16.66%",
298
- "2/6": "33.33%",
299
- "3/6": "50%",
300
- "4/6": "66.66%",
301
- "5/6": "83.33%",
302
- "1/12": "8.33%",
303
- "2/12": "16.66%",
304
- "3/12": "25%",
305
- "4/12": "33.33%",
306
- "5/12": "41.66%",
307
- "6/12": "50%",
308
- "7/12": "58.33%",
309
- "8/12": "66.66%",
310
- "9/12": "75%",
311
- "10/12": "83.33%",
312
- "11/12": "91.66%",
313
- "full": "100%",
314
- },
315
- }
316
-
317
- var twClassLookup = map[string]string{
318
- "flex": "display: flex;",
319
- "inline-flex": "display: inline-flex;",
320
- "block": "display: block;",
321
- "inline-block": "display: inline-block;",
322
- "inline": "display: inline;",
323
- "table": "display: table;",
324
- "inline-table": "display: inline-table;",
325
- "grid": "display: grid;",
326
- "inline-grid": "display: inline-grid;",
327
- "contents": "display: contents;",
328
- "list-item": "display: list-item;",
329
- "hidden": "display: none;",
330
- "flex-1": "flex: 1;",
331
- "flex-row": "flex-direction: row;",
332
- "flex-col": "flex-direction: column;",
333
- "flex-wrap": "flex-wrap: wrap;",
334
- "flex-nowrap": "flex-wrap: nowrap;",
335
- "flex-wrap-reverse": "flex-wrap: wrap-reverse;",
336
- "items-baseline": "align-items: baseline;",
337
- "items-start": "align-items: flex-start;",
338
- "items-center": "align-items: center;",
339
- "items-end": "align-items: flex-end;",
340
- "items-stretch": "align-items: stretch;",
341
- "justify-start": "justify-content: flex-start;",
342
- "justify-end": "justify-content: flex-end;",
343
- "justify-center": "justify-content: center;",
344
- "justify-between": "justify-content: space-between;",
345
- "justify-around": "justify-content: space-around;",
346
- "justify-evenly": "justify-content: space-evenly;",
347
- "uppercase": "text-transform: uppercase",
348
- "lowercase": "text-transform: lowercase",
349
- "capitalize": "text-transform: capitalize",
350
- "normal-case": "text-transform: normal-case",
351
- "text-left": "text-align: left;",
352
- "text-center": "text-align: center;",
353
- "text-right": "text-align: right;",
354
- "text-justify": "text-align: justify;",
355
- "underline": "text-decoration: underline;",
356
- "line-through": "text-decoration: line-through;",
357
- "no-underline": "text-decoration: none;",
358
- "whitespace-normal": "white-space: normal;",
359
- "whitespace-nowrap": "white-space: nowrap;",
360
- "whitespace-pre": "white-space: pre;",
361
- "whitespace-pre-line": "white-space: pre-line;",
362
- "whitespace-pre-wrap": "white-space: pre-wrap;",
363
- "break-normal": "word-break: normal; overflow-wrap: normal;",
364
- "break-words": "word-break: break-word;",
365
- "break-all": "word-break: break-all;",
366
- "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\";",
367
- "font-serif": "font-family: ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif;",
368
- "font-mono": "font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;",
369
- "font-thin": "font-weight: 100;",
370
- "font-extralight": "font-weight: 200;",
371
- "font-light": "font-weight: 300;",
372
- "font-normal": "font-weight: 400;",
373
- "font-medium": "font-weight: 500;",
374
- "font-semibold": "font-weight: 600;",
375
- "font-bold": "font-weight: 700;",
376
- "font-extrabold": "font-weight: 800;",
377
- "font-black": "font-weight: 900;",
378
- "text-xs": "font-size: 0.75rem; line-height: 1rem;",
379
- "text-sm": "font-size: 0.875rem; line-height: 1.25rem;",
380
- "text-base": "font-size: 1rem; line-height: 1.5rem;",
381
- "text-lg": "font-size: 1.125rem; line-height: 1.75rem;",
382
- "text-xl": "font-size: 1.25rem; line-height: 1.75rem;",
383
- "text-2xl": "font-size: 1.5rem; line-height: 2rem;",
384
- "text-3xl": "font-size: 1.875rem; line-height: 2.25rem;",
385
- "text-4xl": "font-size: 2.25rem; line-height: 2.5rem;",
386
- "text-5xl": "font-size: 3rem; line-height: 1;",
387
- "text-6xl": "font-size: 3.75rem;; line-height: 1;",
388
- "text-7xl": "font-size: 4.5rem; line-height: 1;",
389
- "text-8xl": "font-size: 6rem; line-height: 1;",
390
- "text-9xl": "font-size: 8rem; line-height: 1;",
391
- "cursor-auto": "cursor: auto;",
392
- "cursor-default": "cursor: default;",
393
- "cursor-pointer": "cursor: pointer;",
394
- "cursor-wait": "cursor: wait;",
395
- "cursor-text": "cursor: text;",
396
- "cursor-move": "cursor: move;",
397
- "cursor-help": "cursor: help;",
398
- "cursor-not-allowed": "cursor: not-allowed;",
399
- "pointer-events-none": "pointer-events: none;",
400
- "pointer-events-auto": "pointer-events: auto;",
401
- "select-none": "user-select: none;",
402
- "select-text": "user-select: text;",
403
- "select-all": "user-select: all;",
404
- "select-auto": "user-select: auto;",
405
- "w-screen": "100vw",
406
- "h-screen": "100vh",
407
- "static": "position: static;",
408
- "fixed": "position: fixed;",
409
- "absolute": "position: absolute;",
410
- "relative": "position: relative;",
411
- "sticky": "position: sticky;",
412
- "overflow-auto": "overflow: auto;",
413
- "overflow-hidden": "overflow: hidden;",
414
- "overflow-visible": "overflow: visible;",
415
- "overflow-scroll": "overflow: scroll;",
416
- "overflow-x-auto": "overflow-x: auto;",
417
- "overflow-y-auto": "overflow-y: auto;",
418
- "overflow-x-hidden": "overflow-x: hidden;",
419
- "overflow-y-hidden": "overflow-y: hidden;",
420
- "overflow-x-visible": "overflow-x: visible;",
421
- "overflow-y-visible": "overflow-y: visible;",
422
- "overflow-x-scroll": "overflow-x: scroll;",
423
- "overflow-y-scroll": "overflow-y: scroll;",
424
- "origin-center": "transform-origin: center;",
425
- "origin-top": "transform-origin: top;",
426
- "origin-top-right": "transform-origin: top right;",
427
- "origin-right": "transform-origin: right;",
428
- "origin-bottom-right": "transform-origin: bottom right;",
429
- "origin-bottom": "transform-origin: bottom;",
430
- "origin-bottom-left": "transform-origin: bottom left;",
431
- "origin-left": "transform-origin: left;",
432
- "origin-top-left": "transform-origin: top left;",
433
- "shadow-sm": "box-shadow: 0 0 #0000, 0 0 #0000, 0 1px 2px 0 rgba(0, 0, 0, 0.05);",
434
- "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);",
435
- "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);",
436
- "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);",
437
- "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);",
438
- "shadow-2xl": "box-shadow: 0 0 #0000, 0 0 #0000, 0 25px 50px -12px rgba(0, 0, 0, 0.25);",
439
- "shadow-inner": "box-shadow: 0 0 #0000, 0 0 #0000, inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);",
440
- "shadow-none": "box-shadow: 0 0 #0000, 0 0 #0000, 0 0 #0000;",
441
- "ring-inset": "--tw-ring-inset: insest;",
442
- "ring-0": "box-shadow: 0 0 0 calc(0px + 0px) rgba(59, 130, 246, 0.5);",
443
- "ring-1": "box-shadow: 0 0 0 calc(1px + 0px) rgba(59, 130, 246, 0.5);",
444
- "ring-2": "box-shadow: 0 0 0 calc(2px + 0px) rgba(59, 130, 246, 0.5);",
445
- "ring-4": "box-shadow: 0 0 0 calc(4px + 0px) rgba(59, 130, 246, 0.5);",
446
- "ring-8": "box-shadow: 0 0 0 calc(8px + 0px) rgba(59, 130, 246, 0.5);",
447
- "ring": "box-shadow: 0 0 0 calc(3px + 0px) rgba(59, 130, 246, 0.5);",
448
- }
449
-
450
- func init() {
451
- mapApply(sizes)
452
- mapApply(spacing)
453
- mapApply(colors)
454
- mapApply(borders)
455
- mapApply(radius)
456
- }
457
-
458
- func mapApply(obj KeyValues) {
459
- for key, v := range obj.Keys {
460
- for vkey, vv := range obj.Values {
461
- suffix := ""
462
- if vkey != "" {
463
- suffix = "-" + vkey
464
- }
465
- className := key + suffix
466
- if vstring, ok := v.(string); ok {
467
- twClassLookup[className] = vstring + ": " + vv + ";"
468
- }
469
- if varr, ok := v.(Arr); ok {
470
- twClassLookup[className] = ""
471
- for _, kk := range varr {
472
- twClassLookup[className] += kk.(string) + ": " + vv + ";"
473
- }
474
- }
475
- }
476
- }
477
- }
478
-
479
- func CreateTheme() {
480
- }
481
-
482
- func Styled(name string, classes string) {
483
- }
484
-
485
- // var Button = Styled("button", "px-20 bg-gray-500")
486
-
487
- var normalizeStyles = `
488
- *, ::before, ::after { box-sizing: border-box; }
489
- html { -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; line-height: 1.15; -webkit-text-size-adjust: 100%; }
490
- body { margin: 0; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; }
491
- hr { height: 0; color: inherit; }
492
- abbr[title] { -webkit-text-decoration: underline dotted; text-decoration: underline dotted; }
493
- b, strong { font-weight: bolder; }
494
- code, kbd, samp, pre { font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 1em; }
495
- small { font-size: 80%; }
496
- sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
497
- sub { bottom: -0.25em; }
498
- sup { top: -0.5em; }
499
- table { text-indent: 0; border-color: inherit; }
500
- button, input, optgroup, select, textarea { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; }
501
- button, select { text-transform: none; }
502
- button, [type='button'], [type='reset'], [type='submit'] { -webkit-appearance: button; }
503
- ::-moz-focus-inner { border-style: none; padding: 0; }
504
- :-moz-focusring { outline: 1px dotted ButtonText; outline: auto; }
505
- :-moz-ui-invalid { box-shadow: none; }
506
- legend { padding: 0; }
507
- progress { vertical-align: baseline; }
508
- ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; }
509
- [type='search'] { -webkit-appearance: textfield; outline-offset: -2px; }
510
- ::-webkit-search-decoration { -webkit-appearance: none; }
511
- ::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; }
512
- summary { display: list-item; }
513
- blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre { margin: 0; }
514
- button { background-color: transparent; background-image: none; }
515
- fieldset { margin: 0; padding: 0; }
516
- ol, ul { list-style: none; margin: 0; padding: 0; }
517
- 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; }
518
- body { font-family: inherit; line-height: inherit; }
519
- *, ::before, ::after { box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor; }
520
- hr { border-top-width: 1px; }
521
- img { border-style: solid; }
522
- textarea { resize: vertical; }
523
- input::-moz-placeholder, textarea::-moz-placeholder { opacity: 1; color: #9ca3af; }
524
- input:-ms-input-placeholder, textarea:-ms-input-placeholder { opacity: 1; color: #9ca3af; }
525
- input::placeholder, textarea::placeholder { opacity: 1; color: #9ca3af; }
526
- button, [role="button"] { cursor: pointer; }
527
- table { border-collapse: collapse; }
528
- h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; }
529
- a { color: inherit; text-decoration: inherit; }
530
- button, input, optgroup, select, textarea { padding: 0; line-height: inherit; color: inherit; }
531
- pre, code, kbd, samp { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
532
- img, svg, video, canvas, audio, iframe, embed, object { display: block; vertical-align: middle; }
533
- img, video { max-width: 100%; height: auto; }
534
- [hidden] { display: none; }
535
- *, ::before, ::after { --tw-border-opacity: 1; border-color: rgba(229, 231, 235, var(--tw-border-opacity)); }
536
- `
example/assets/assets.go ADDED
@@ -0,0 +1,8 @@
1
+ package assets
2
+
3
+ import (
4
+ "embed"
5
+ )
6
+
7
+ //go:embed *
8
+ var FS embed.FS
example/components/header.go CHANGED
@@ -1,7 +1,7 @@
1
1
  package components
2
2
 
3
3
  func Header() string {
4
- return `
4
+ return (`
5
5
  <div class="flex flex-row justify-center items-center w-full mb-20 font-bold text-xl text-gray-700 p-4">
6
6
  <div class="text-blue-700">
7
7
  <a href="https://pyros.sh"> pyros.sh </a>
@@ -26,5 +26,5 @@ func Header() string {
26
26
  </div>
27
27
  {{ children }}
28
28
  </div>
29
- `
29
+ `)
30
30
  }
example/components/page.go CHANGED
@@ -1,7 +1,7 @@
1
1
  package components
2
2
 
3
3
  func Page() string {
4
- return `
4
+ return (`
5
5
  <!DOCTYPE html>
6
6
  <html lang="en">
7
7
  <head>
@@ -22,5 +22,5 @@ func Page() string {
22
22
  {{ children }}
23
23
  </body>
24
24
  </html>
25
- `
25
+ `)
26
26
  }
example/context/context.go DELETED
@@ -1,13 +0,0 @@
1
- package context
2
-
3
- import (
4
- c "context"
5
- )
6
-
7
- func WithContext(ctx c.Context) (c.Context, error) {
8
- return c.WithValue(ctx, "userId", "123"), nil
9
- }
10
-
11
- func GetUserID(ctx c.Context) string {
12
- return ctx.Value("userId").(string)
13
- }
example/main.go CHANGED
@@ -2,9 +2,6 @@
2
2
  package main
3
3
 
4
4
  import (
5
- c "context"
6
- "embed"
7
- "net/http"
8
5
  "os"
9
6
 
10
7
  "github.com/gorilla/mux"
@@ -12,6 +9,7 @@ import (
12
9
  "github.com/rs/zerolog/log"
13
10
  "gocloud.dev/server"
14
11
 
12
+ "github.com/pyros2097/gromer/example/assets"
15
13
  "github.com/pyros2097/gromer/example/pages/api/todos"
16
14
  "github.com/pyros2097/gromer/example/pages"
17
15
  "github.com/pyros2097/gromer/example/pages/about"
@@ -20,25 +18,22 @@ import (
20
18
 
21
19
  )
22
20
 
23
- //go:embed assets/*
24
- var assetsFS embed.FS
25
-
26
21
  func main() {
27
22
  port := os.Getenv("PORT")
28
23
  r := mux.NewRouter()
29
24
  r.Use(gromer.CorsMiddleware)
30
25
  r.Use(gromer.LogMiddleware)
31
26
  r.NotFoundHandler = gromer.NotFoundHandler
32
- r.PathPrefix("/assets/").Handler(wrapCache(http.FileServer(http.FS(assetsFS))))
27
+ gromer.Static(r, "/assets/", assets.FS)
33
- handle(r, "GET", "/api", gromer.ApiExplorer(apiDefinitions()))
28
+ gromer.Handle(r, "GET", "/api", gromer.ApiExplorer, nil)
34
- handle(r, "GET", "/about", about.GET)
29
+ gromer.Handle(r, "GET", "/about", about.GET, about.GetParams{})
35
- handle(r, "GET", "/api/recover", recover.GET)
30
+ gromer.Handle(r, "GET", "/api/recover", recover.GET, recover.GetParams{})
36
- handle(r, "DELETE", "/api/todos/{todoId}", todos_todoId_.DELETE)
31
+ gromer.Handle(r, "DELETE", "/api/todos/{todoId}", todos_todoId_.DELETE, todos_todoId_.DeleteParams{})
37
- handle(r, "GET", "/api/todos/{todoId}", todos_todoId_.GET)
32
+ gromer.Handle(r, "GET", "/api/todos/{todoId}", todos_todoId_.GET, todos_todoId_.GetParams{})
38
- handle(r, "PUT", "/api/todos/{todoId}", todos_todoId_.PUT)
33
+ gromer.Handle(r, "PUT", "/api/todos/{todoId}", todos_todoId_.PUT, todos_todoId_.PutParams{})
39
- handle(r, "GET", "/api/todos", todos.GET)
34
+ gromer.Handle(r, "GET", "/api/todos", todos.GET, todos.GetParams{})
40
- handle(r, "POST", "/api/todos", todos.POST)
35
+ gromer.Handle(r, "POST", "/api/todos", todos.POST, todos.PostParams{})
41
- handle(r, "GET", "/", pages.GET)
36
+ gromer.Handle(r, "GET", "/", pages.GET, pages.GetParams{})
42
37
 
43
38
  println("http server listening on http://localhost:"+port)
44
39
  srv := server.New(r, nil)
@@ -46,75 +41,3 @@ func main() {
46
41
  log.Fatal().Stack().Err(err).Msg("failed to listen")
47
42
  }
48
43
  }
49
-
50
- func wrapCache(h http.Handler) http.Handler {
51
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
52
- w.Header().Set("Cache-Control", "public, max-age=2592000")
53
- h.ServeHTTP(w, r)
54
- })
55
- }
56
-
57
- func handle(router *mux.Router, method, route string, h interface{}) {
58
- router.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
59
- ctx := c.WithValue(
60
- c.WithValue(
61
- c.WithValue(r.Context(), "assetsFS", assetsFS),
62
- "url", r.URL),
63
- "header", r.Header)
64
- gromer.PerformRequest(route, h, ctx, w, r)
65
- }).Methods(method, "OPTIONS")
66
- }
67
-
68
- func apiDefinitions() []gromer.ApiDefinition {
69
- return []gromer.ApiDefinition{
70
-
71
- {
72
- Method: "GET",
73
- Path: "/api/recover",
74
- PathParams: []string{ },
75
- Params: map[string]interface{}{
76
-
77
- },
78
- },
79
- {
80
- Method: "DELETE",
81
- Path: "/api/todos/{todoId}",
82
- PathParams: []string{ "todoId", },
83
- Params: map[string]interface{}{
84
-
85
- },
86
- },
87
- {
88
- Method: "GET",
89
- Path: "/api/todos/{todoId}",
90
- PathParams: []string{ "todoId", },
91
- Params: map[string]interface{}{
92
-
93
- },
94
- },
95
- {
96
- Method: "PUT",
97
- Path: "/api/todos/{todoId}",
98
- PathParams: []string{ "todoId", },
99
- Params: map[string]interface{}{
100
-
101
- },
102
- },
103
- {
104
- Method: "GET",
105
- Path: "/api/todos",
106
- PathParams: []string{ },
107
- Params: map[string]interface{}{
108
-
109
- },
110
- },
111
- {
112
- Method: "POST",
113
- Path: "/api/todos",
114
- PathParams: []string{ },
115
- Params: map[string]interface{}{
116
-
117
- },
118
- },
119
- }
120
- }
example/pages/about/get.go CHANGED
@@ -6,9 +6,17 @@ import (
6
6
  . "github.com/pyros2097/gromer"
7
7
  )
8
8
 
9
+ type GetParams struct{}
10
+
9
11
  func GET(c context.Context) (HtmlContent, int, error) {
10
12
  return Html(`
13
+ {{#Page "gromer example"}}
14
+ <div class="flex flex-col justify-center items-center">
15
+ {{#Header "123"}}
16
+ A new link is here
17
+ {{/Header}}
18
+ <h1>About Me</h1>
11
- <!DOCTYPE html>
19
+ </div>
12
- <html lang="en">
13
- <head>`, nil)
20
+ {{/Page}}
21
+ `, M{})
14
22
  }
example/pages/api/recover/get.go CHANGED
@@ -5,11 +5,11 @@ import (
5
5
  "fmt"
6
6
  )
7
7
 
8
- type Params struct {
8
+ type GetParams struct {
9
9
  Limit int `json:"limit"`
10
10
  }
11
11
 
12
- func GET(ctx context.Context, params Params) (*Params, int, error) {
12
+ func GET(ctx context.Context, params GetParams) (*GetParams, int, error) {
13
13
  arr := []string{}
14
14
  v := arr[55]
15
15
  fmt.Printf("%s", v)
example/pages/api/todos/_todoId_/delete.go CHANGED
@@ -6,6 +6,8 @@ import (
6
6
  "github.com/pyros2097/gromer/example/db"
7
7
  )
8
8
 
9
+ type DeleteParams struct{}
10
+
9
11
  func DELETE(ctx context.Context, id string) (string, int, error) {
10
12
  _, status, err := GET(ctx, id, GetParams{})
11
13
  if err != nil {
example/pages/get.go CHANGED
@@ -29,6 +29,7 @@ func GET(ctx context.Context, params GetParams) (HtmlContent, int, error) {
29
29
  {{/Header}}
30
30
  <h1>Hello this is a h1</h1>
31
31
  <h2>Hello this is a h2</h2>
32
+ <img src="/assets/icon.png" width="48" height="48" />
32
33
  <h3 x-data="{ message: 'I ❤️ Alpine' }" x-text="message">I ❤️ Alpine</h3>
33
34
  <table class="table">
34
35
  <thead>
example/readme.md CHANGED
@@ -13,7 +13,6 @@ It uses postgres as the database and sqlc to generate queries and models. It use
13
13
 
14
14
  ```sh
15
15
  make setup
16
- make start-db
17
16
  make generate
18
17
  make run
19
18
  ```
hooks.go DELETED
@@ -1,44 +0,0 @@
1
- package gromer
2
-
3
- import (
4
- "context"
5
- )
6
-
7
- type StateContext struct {
8
- index int
9
- datas []interface{}
10
- }
11
-
12
- // Go 1.18 stuff
13
- // func UseState[T any](ctx *Context , s T) (func() T, func(v T)) {
14
- // if len(ctx.datas) <= ctx.index + 1 {
15
- // ctx .datas = append(ctx.datas, s)
16
- // }
17
- // return func() T {
18
- // return ctx.datas[ctx.index].(T)
19
- // }, func(v T) {
20
- // ctx.datas[ctx.index] = v
21
- // }
22
- // }
23
-
24
- func getState(ctx context.Context) *StateContext {
25
- return ctx.Value("state").(*StateContext)
26
- }
27
-
28
- func WithState(ctx context.Context) context.Context {
29
- return context.WithValue(ctx, "state", &StateContext{})
30
- }
31
-
32
- func UseState(ctx context.Context, s interface{}) (func() interface{}, func(v interface{})) {
33
- stateContext := getState(ctx)
34
- localIndex := stateContext.index
35
- if len(stateContext.datas) <= stateContext.index+1 {
36
- stateContext.datas = append(stateContext.datas, s)
37
- stateContext.index += 1
38
- }
39
- return func() interface{} {
40
- return stateContext.datas[localIndex].(interface{})
41
- }, func(v interface{}) {
42
- stateContext.datas[localIndex] = v
43
- }
44
- }
hooks_test.go DELETED
@@ -1,33 +0,0 @@
1
- package gromer
2
-
3
- import (
4
- "context"
5
- "testing"
6
-
7
- . "github.com/franela/goblin"
8
- )
9
-
10
- func TestHooks(t *testing.T) {
11
- g := Goblin(t)
12
- g.Describe("useState", func() {
13
- ctx := WithState(context.Background())
14
- stateCtx := getState(ctx)
15
- getValue, setValue := UseState(ctx, 12)
16
-
17
- g.It("should be initialized ", func() {
18
- g.Assert(1).Equal(stateCtx.index)
19
- g.Assert(1).Equal(len(stateCtx.datas))
20
- g.Assert(stateCtx.datas[0]).Equal(12)
21
- })
22
-
23
- g.It("should get value ", func() {
24
- g.Assert(getValue()).Equal(12)
25
- })
26
-
27
- g.It("should set value", func() {
28
- setValue(15)
29
- g.Assert(stateCtx.datas[0]).Equal(15)
30
- g.Assert(getValue()).Equal(15)
31
- })
32
- })
33
- }
http.go CHANGED
@@ -1,6 +1,8 @@
1
1
  package gromer
2
2
 
3
3
  import (
4
+ "context"
5
+ "embed"
4
6
  "encoding/json"
5
7
  "fmt"
6
8
  "html/template"
@@ -26,6 +28,15 @@ import (
26
28
  var info *debug.BuildInfo
27
29
  var IsCloundRun bool
28
30
 
31
+ var RouteDefs []RouteDefinition
32
+
33
+ type RouteDefinition struct {
34
+ Method string `json:"method"`
35
+ Pkg string `json:"pkg"`
36
+ Path string `json:"path"`
37
+ Params interface{} `json:"params"`
38
+ }
39
+
29
40
  type HtmlContent string
30
41
 
31
42
  func Html(tpl string, params map[string]interface{}) (HtmlContent, int, error) {
@@ -279,3 +290,22 @@ func CorsMiddleware(next http.Handler) http.Handler {
279
290
  var NotFoundHandler = LogMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
280
291
  RespondError(w, 404, fmt.Errorf("path '%s' not found", r.URL.String()))
281
292
  }))
293
+
294
+ func WrapCache(h http.Handler) http.Handler {
295
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
296
+ w.Header().Set("Cache-Control", "public, max-age=2592000")
297
+ h.ServeHTTP(w, r)
298
+ })
299
+ }
300
+
301
+ func Static(router *mux.Router, path string, fs embed.FS) {
302
+ router.PathPrefix(path).Handler(http.StripPrefix(path, WrapCache(http.FileServer(http.FS(fs)))))
303
+ }
304
+
305
+ func Handle(router *mux.Router, method, route string, h interface{}, params interface{}) {
306
+ RouteDefs = append(RouteDefs, RouteDefinition{method, route, "", params})
307
+ router.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
308
+ ctx := context.WithValue(context.WithValue(r.Context(), "url", r.URL), "header", r.Header)
309
+ PerformRequest(route, h, ctx, w, r)
310
+ }).Methods(method, "OPTIONS")
311
+ }
readme.md CHANGED
@@ -13,6 +13,8 @@ It also generates http handlers for your routes which follow a particular folder
13
13
  go get -u -v github.com/pyros2097/gromer/cmd/gromer
14
14
  ```
15
15
 
16
+ You can install this plugin https://marketplace.visualstudio.com/items?itemName=pyros2097.vscode-go-inline-html for syntax highlighting html templates in golang.
17
+
16
18
  # Using
17
19
 
18
20
  You need to follow this directory structure similar to nextjs for the api route handlers to be generated
utils.go CHANGED
@@ -11,6 +11,10 @@ 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
+
14
18
  var Validator = validator.New()
15
19
  var ValidatorErrorMap = map[string]string{
16
20
  "required": "is required",