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


f3a4c91b pyros2097

5 years ago
make routing work
Files changed (8) hide show
  1. app_nowasm.go +42 -26
  2. app_wasm.go +22 -2
  3. attributes_test.go +2 -2
  4. example/main.go +7 -9
  5. go.mod +1 -0
  6. go.sum +2 -0
  7. tree.go +1281 -0
  8. utils.go +5 -29
app_nowasm.go CHANGED
@@ -8,46 +8,57 @@ import (
8
8
  "net/http"
9
9
  "os"
10
10
  "path/filepath"
11
- "strconv"
11
+ "strings"
12
12
 
13
13
  "github.com/akrylysov/algnhsa"
14
14
  "github.com/markbates/pkger"
15
15
  )
16
16
 
17
+ // ServeFiles serves files from the given file system root.
18
+ // The path must end with "/*filepath", files are then served from the local
19
+ // path /defined/root/dir/*filepath.
20
+ // For example if root is "/etc" and *filepath is "passwd", the local file
21
+ // "/etc/passwd" would be served.
22
+ // Internally a http.FileServer is used, therefore http.NotFound is used instead
23
+ // of the Router's NotFound handler.
24
+ // To use the operating system's file system implementation,
17
- type AppInfo struct {
25
+ // use http.Dir:
26
+ // router.ServeFiles("/src/*filepath", http.Dir("/var/www"))
27
+ // func (r *Router) ServeFiles(path string, root http.FileSystem) {
28
+ // if len(path) < 10 || path[len(path)-10:] != "/*filepath" {
29
+ // panic("path must end with /*filepath in path '" + path + "'")
18
- Title string
30
+ // }
31
+
19
- Description string
32
+ // fileServer := http.FileServer(root)
20
- Author string
21
- Keywords string
22
- }
23
33
 
34
+ // r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) {
24
- func Run(info AppInfo, routes map[string]RenderFunc) {
35
+ // req.URL.Path = ps.ByName("filepath")
36
+ // fileServer.ServeHTTP(w, req)
37
+ // })
38
+ // }
39
+
40
+ func Run() {
25
41
  isLambda := os.Getenv("_LAMBDA_SERVER_PORT") != ""
26
- wd, err := os.Getwd()
27
- if err != nil {
28
- fmt.Printf("could not get wd")
29
- return
30
- }
31
42
  if !isLambda {
43
+ wd, err := os.Getwd()
44
+ if err != nil {
45
+ fmt.Printf("could not get wd")
46
+ return
47
+ }
32
- http.Handle("/assets/", http.StripPrefix("/assets", http.FileServer(pkger.Dir(filepath.Join(wd, "assets")))))
48
+ assetsFS := http.FileServer(pkger.Dir(filepath.Join(wd, "assets")))
49
+ router.GET("/assets/*filepath", func(w http.ResponseWriter, r *http.Request) {
50
+ r.URL.Path = strings.Replace(r.URL.Path, "/assets", "", 1)
51
+ assetsFS.ServeHTTP(w, r)
52
+ })
33
53
  }
34
- http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
35
- println("route: " + r.URL.Path)
36
- renderFunc := MatchRoute(routes, r.URL.Path)
37
- page := createPage(info, renderFunc(NewRenderContext()))
38
- w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
39
- w.Header().Set("Content-Type", "text/html")
40
- w.WriteHeader(http.StatusOK)
41
- w.Write(page.Bytes())
42
- })
43
54
  if isLambda {
44
55
  println("running in lambda mode")
45
- algnhsa.ListenAndServe(http.DefaultServeMux, &algnhsa.Options{
56
+ algnhsa.ListenAndServe(router, &algnhsa.Options{
46
57
  BinaryContentTypes: []string{"application/wasm", "image/png"},
47
58
  })
48
59
  } else {
49
60
  println("Serving on HTTP port: 1234")
50
- http.ListenAndServe(":1234", nil)
61
+ http.ListenAndServe(":1234", router)
51
62
  }
52
63
  }
53
64
 
@@ -55,7 +66,12 @@ func Reload() {
55
66
  panic("wasm required")
56
67
  }
57
68
 
69
+ func Route(path string, render RenderFunc, info RouteInfo) {
70
+ println("registering route: " + path)
71
+ router.GET(path, render)
72
+ }
73
+
58
- func createPage(info AppInfo, ui UI) *bytes.Buffer {
74
+ func createPage(info RouteInfo, ui UI) *bytes.Buffer {
59
75
  isLambda := os.Getenv("_LAMBDA_SERVER_PORT") != ""
60
76
  page := bytes.NewBuffer(nil)
61
77
  page.WriteString("<!DOCTYPE html>\n")
app_wasm.go CHANGED
@@ -2,6 +2,8 @@
2
2
  package app
3
3
 
4
4
  import (
5
+ "bytes"
6
+
5
7
  "github.com/pyros2097/wapp/js"
6
8
  )
7
9
 
@@ -9,10 +11,20 @@ var (
9
11
  body *elem
10
12
  content UI
11
13
  rootPrefix string
14
+ renderFunc RenderFunc
12
15
  )
13
16
 
14
- func Run(routes map[string]RenderFunc) {
17
+ func Run() {
15
- renderFunc := MatchRoute(routes, js.Window.URL().Path)
18
+ handle, _, _ := router.Lookup("GET", js.Window.URL().Path)
19
+ if handle == nil {
20
+ if router.NotFound != nil {
21
+ renderFunc = router.NotFound
22
+ } else {
23
+ renderFunc = DefaultNotFound
24
+ }
25
+ } else {
26
+ renderFunc, _ = handle.(RenderFunc)
27
+ }
16
28
  defer func() {
17
29
  err := recover()
18
30
  // show alert
@@ -40,6 +52,10 @@ func Reload() {
40
52
  })
41
53
  }
42
54
 
55
+ func Route(path string, render RenderFunc, info RouteInfo) {
56
+ router.GET(path, render)
57
+ }
58
+
43
59
  func initBody() {
44
60
  body = &elem{
45
61
  jsvalue: js.Window.Get("document").Get("body"),
@@ -71,3 +87,7 @@ func initContent() {
71
87
  // func isFragmentNavigation(u *url.URL) bool {
72
88
  // return u.Fragment != ""
73
89
  // }
90
+
91
+ func createPage(info RouteInfo, ui UI) *bytes.Buffer {
92
+ return &bytes.Buffer{}
93
+ }
attributes_test.go CHANGED
@@ -28,7 +28,7 @@ func Counter(c *RenderContext) UI {
28
28
  )
29
29
  }
30
30
 
31
- func Route(c *RenderContext) UI {
31
+ func TestRoute(c *RenderContext) UI {
32
32
  return Div(
33
33
  Div(),
34
34
  Counter(c),
@@ -42,7 +42,7 @@ func TestCreatePage(t *testing.T) {
42
42
  Head(
43
43
  Title("Title"),
44
44
  ),
45
- Body(Route(NewRenderContext())),
45
+ Body(TestRoute(NewRenderContext())),
46
46
  ).Html(page)
47
47
  assert.Equal(t, "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\">\n <meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\">\n <meta http-equiv=\"encoding\" content=\"utf-8\">\n <title>\n Title\n </title>\n </head>\n <body>\n <div>\n <div></div>\n <div class=\"flex flex-col justify-center align-items-center\">\n <div class=\"flex flex-row justify-center align-items-center yellow\">\n Counter\n </div>\n <div class=\"flex flex-row justify-center align-items-center\">\n <div>\n -\n </div>\n <div>\n 0\n </div>\n <div>\n +\n </div>\n </div>\n </div>\n </div>\n </body>\n</html>", page.String())
48
48
  }
example/main.go CHANGED
@@ -4,18 +4,16 @@ import (
4
4
  . "github.com/pyros2097/wapp"
5
5
  )
6
6
 
7
- var routes = map[string]RenderFunc{
8
- "/about": About,
9
- "/clock": Clock,
10
- "/container": Container,
11
- "/": Index,
12
- }
13
-
14
7
  func main() {
15
- Run(AppInfo{
8
+ info := RouteInfo{
16
9
  Title: "wapp-example",
17
10
  Description: "wapp is a framework",
18
11
  Author: "pyros2097",
19
12
  Keywords: "wapp,wapp-example,golang,framework,frontend,ui,wasm,isomorphic",
13
+ }
14
+ Route("/about", About, info)
15
+ Route("/clock", Clock, info)
16
+ Route("/container", Container, info)
17
+ Route("/", Index, info)
20
- }, routes)
18
+ Run()
21
19
  }
go.mod CHANGED
@@ -6,6 +6,7 @@ require (
6
6
  github.com/akrylysov/algnhsa v0.12.1
7
7
  github.com/aws/aws-lambda-go v1.20.0
8
8
  github.com/awslabs/goformation/v4 v4.15.6
9
+ github.com/julienschmidt/httprouter v1.3.0
9
10
  github.com/kevinpollet/nego v0.0.0-20200702060216-3ff8e9f14a70
10
11
  github.com/markbates/pkger v0.17.1
11
12
  github.com/shurcooL/httpgzip v0.0.0-20190720172056-320755c1c1b0
go.sum CHANGED
@@ -30,6 +30,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
30
30
  github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
31
31
  github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
32
32
  github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
33
+ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
34
+ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
33
35
  github.com/kevinpollet/nego v0.0.0-20200702060216-3ff8e9f14a70 h1:vl63cy3DUIN3iZI5oU/2yfVXEFVPc5ahbwjWk0aF3nA=
34
36
  github.com/kevinpollet/nego v0.0.0-20200702060216-3ff8e9f14a70/go.mod h1:bST7PtmFt4otZfrYPAUmYA1v3hZBhX4ttQzBSxeRE4E=
35
37
  github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
tree.go ADDED
@@ -0,0 +1,1281 @@
1
+ package app
2
+
3
+ import (
4
+ "context"
5
+ "net/http"
6
+ "strconv"
7
+ "strings"
8
+ "sync"
9
+ "unicode"
10
+ "unicode/utf8"
11
+ )
12
+
13
+ // CleanPath is the URL version of path.Clean, it returns a canonical URL path
14
+ // for p, eliminating . and .. elements.
15
+ //
16
+ // The following rules are applied iteratively until no further processing can
17
+ // be done:
18
+ // 1. Replace multiple slashes with a single slash.
19
+ // 2. Eliminate each . path name element (the current directory).
20
+ // 3. Eliminate each inner .. path name element (the parent directory)
21
+ // along with the non-.. element that precedes it.
22
+ // 4. Eliminate .. elements that begin a rooted path:
23
+ // that is, replace "/.." by "/" at the beginning of a path.
24
+ //
25
+ // If the result of this process is an empty string, "/" is returned
26
+ func CleanPath(p string) string {
27
+ const stackBufSize = 128
28
+
29
+ // Turn empty string into "/"
30
+ if p == "" {
31
+ return "/"
32
+ }
33
+
34
+ // Reasonably sized buffer on stack to avoid allocations in the common case.
35
+ // If a larger buffer is required, it gets allocated dynamically.
36
+ buf := make([]byte, 0, stackBufSize)
37
+
38
+ n := len(p)
39
+
40
+ // Invariants:
41
+ // reading from path; r is index of next byte to process.
42
+ // writing to buf; w is index of next byte to write.
43
+
44
+ // path must start with '/'
45
+ r := 1
46
+ w := 1
47
+
48
+ if p[0] != '/' {
49
+ r = 0
50
+
51
+ if n+1 > stackBufSize {
52
+ buf = make([]byte, n+1)
53
+ } else {
54
+ buf = buf[:n+1]
55
+ }
56
+ buf[0] = '/'
57
+ }
58
+
59
+ trailing := n > 1 && p[n-1] == '/'
60
+
61
+ // A bit more clunky without a 'lazybuf' like the path package, but the loop
62
+ // gets completely inlined (bufApp calls).
63
+ // So in contrast to the path package this loop has no expensive function
64
+ // calls (except make, if needed).
65
+
66
+ for r < n {
67
+ switch {
68
+ case p[r] == '/':
69
+ // empty path element, trailing slash is added after the end
70
+ r++
71
+
72
+ case p[r] == '.' && r+1 == n:
73
+ trailing = true
74
+ r++
75
+
76
+ case p[r] == '.' && p[r+1] == '/':
77
+ // . element
78
+ r += 2
79
+
80
+ case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
81
+ // .. element: remove to last /
82
+ r += 3
83
+
84
+ if w > 1 {
85
+ // can backtrack
86
+ w--
87
+
88
+ if len(buf) == 0 {
89
+ for w > 1 && p[w] != '/' {
90
+ w--
91
+ }
92
+ } else {
93
+ for w > 1 && buf[w] != '/' {
94
+ w--
95
+ }
96
+ }
97
+ }
98
+
99
+ default:
100
+ // Real path element.
101
+ // Add slash if needed
102
+ if w > 1 {
103
+ bufApp(&buf, p, w, '/')
104
+ w++
105
+ }
106
+
107
+ // Copy element
108
+ for r < n && p[r] != '/' {
109
+ bufApp(&buf, p, w, p[r])
110
+ w++
111
+ r++
112
+ }
113
+ }
114
+ }
115
+
116
+ // Re-append trailing slash
117
+ if trailing && w > 1 {
118
+ bufApp(&buf, p, w, '/')
119
+ w++
120
+ }
121
+
122
+ // If the original string was not modified (or only shortened at the end),
123
+ // return the respective substring of the original string.
124
+ // Otherwise return a new string from the buffer.
125
+ if len(buf) == 0 {
126
+ return p[:w]
127
+ }
128
+ return string(buf[:w])
129
+ }
130
+
131
+ // Internal helper to lazily create a buffer if necessary.
132
+ // Calls to this function get inlined.
133
+ func bufApp(buf *[]byte, s string, w int, c byte) {
134
+ b := *buf
135
+ if len(b) == 0 {
136
+ // No modification of the original string so far.
137
+ // If the next character is the same as in the original string, we do
138
+ // not yet have to allocate a buffer.
139
+ if s[w] == c {
140
+ return
141
+ }
142
+
143
+ // Otherwise use either the stack buffer, if it is large enough, or
144
+ // allocate a new buffer on the heap, and copy all previous characters.
145
+ if l := len(s); l > cap(b) {
146
+ *buf = make([]byte, len(s))
147
+ } else {
148
+ *buf = (*buf)[:l]
149
+ }
150
+ b = *buf
151
+
152
+ copy(b, s[:w])
153
+ }
154
+ b[w] = c
155
+ }
156
+
157
+ func min(a, b int) int {
158
+ if a <= b {
159
+ return a
160
+ }
161
+ return b
162
+ }
163
+
164
+ func longestCommonPrefix(a, b string) int {
165
+ i := 0
166
+ max := min(len(a), len(b))
167
+ for i < max && a[i] == b[i] {
168
+ i++
169
+ }
170
+ return i
171
+ }
172
+
173
+ // Search for a wildcard segment and check the name for invalid characters.
174
+ // Returns -1 as index, if no wildcard was found.
175
+ func findWildcard(path string) (wilcard string, i int, valid bool) {
176
+ // Find start
177
+ for start, c := range []byte(path) {
178
+ // A wildcard starts with ':' (param) or '*' (catch-all)
179
+ if c != ':' && c != '*' {
180
+ continue
181
+ }
182
+
183
+ // Find end and check for invalid characters
184
+ valid = true
185
+ for end, c := range []byte(path[start+1:]) {
186
+ switch c {
187
+ case '/':
188
+ return path[start : start+1+end], start, valid
189
+ case ':', '*':
190
+ valid = false
191
+ }
192
+ }
193
+ return path[start:], start, valid
194
+ }
195
+ return "", -1, false
196
+ }
197
+
198
+ func countParams(path string) uint16 {
199
+ var n uint
200
+ for i := range []byte(path) {
201
+ switch path[i] {
202
+ case ':', '*':
203
+ n++
204
+ }
205
+ }
206
+ return uint16(n)
207
+ }
208
+
209
+ type nodeType uint8
210
+
211
+ const (
212
+ static nodeType = iota // default
213
+ root
214
+ param
215
+ catchAll
216
+ )
217
+
218
+ type node struct {
219
+ path string
220
+ indices string
221
+ wildChild bool
222
+ nType nodeType
223
+ priority uint32
224
+ children []*node
225
+ handle interface{}
226
+ }
227
+
228
+ // Increments priority of the given child and reorders if necessary
229
+ func (n *node) incrementChildPrio(pos int) int {
230
+ cs := n.children
231
+ cs[pos].priority++
232
+ prio := cs[pos].priority
233
+
234
+ // Adjust position (move to front)
235
+ newPos := pos
236
+ for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
237
+ // Swap node positions
238
+ cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
239
+ }
240
+
241
+ // Build new index char string
242
+ if newPos != pos {
243
+ n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
244
+ n.indices[pos:pos+1] + // The index char we move
245
+ n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
246
+ }
247
+
248
+ return newPos
249
+ }
250
+
251
+ // addRoute adds a node with the given handle to the path.
252
+ // Not concurrency-safe!
253
+ func (n *node) addRoute(path string, handle interface{}) {
254
+ fullPath := path
255
+ n.priority++
256
+
257
+ // Empty tree
258
+ if n.path == "" && n.indices == "" {
259
+ n.insertChild(path, fullPath, handle)
260
+ n.nType = root
261
+ return
262
+ }
263
+
264
+ walk:
265
+ for {
266
+ // Find the longest common prefix.
267
+ // This also implies that the common prefix contains no ':' or '*'
268
+ // since the existing key can't contain those chars.
269
+ i := longestCommonPrefix(path, n.path)
270
+
271
+ // Split edge
272
+ if i < len(n.path) {
273
+ child := node{
274
+ path: n.path[i:],
275
+ wildChild: n.wildChild,
276
+ nType: static,
277
+ indices: n.indices,
278
+ children: n.children,
279
+ handle: n.handle,
280
+ priority: n.priority - 1,
281
+ }
282
+
283
+ n.children = []*node{&child}
284
+ // []byte for proper unicode char conversion, see #65
285
+ n.indices = string([]byte{n.path[i]})
286
+ n.path = path[:i]
287
+ n.handle = nil
288
+ n.wildChild = false
289
+ }
290
+
291
+ // Make new node a child of this node
292
+ if i < len(path) {
293
+ path = path[i:]
294
+
295
+ if n.wildChild {
296
+ n = n.children[0]
297
+ n.priority++
298
+
299
+ // Check if the wildcard matches
300
+ if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
301
+ // Adding a child to a catchAll is not possible
302
+ n.nType != catchAll &&
303
+ // Check for longer wildcard, e.g. :name and :names
304
+ (len(n.path) >= len(path) || path[len(n.path)] == '/') {
305
+ continue walk
306
+ } else {
307
+ // Wildcard conflict
308
+ pathSeg := path
309
+ if n.nType != catchAll {
310
+ pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
311
+ }
312
+ prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
313
+ panic("'" + pathSeg +
314
+ "' in new path '" + fullPath +
315
+ "' conflicts with existing wildcard '" + n.path +
316
+ "' in existing prefix '" + prefix +
317
+ "'")
318
+ }
319
+ }
320
+
321
+ idxc := path[0]
322
+
323
+ // '/' after param
324
+ if n.nType == param && idxc == '/' && len(n.children) == 1 {
325
+ n = n.children[0]
326
+ n.priority++
327
+ continue walk
328
+ }
329
+
330
+ // Check if a child with the next path byte exists
331
+ for i, c := range []byte(n.indices) {
332
+ if c == idxc {
333
+ i = n.incrementChildPrio(i)
334
+ n = n.children[i]
335
+ continue walk
336
+ }
337
+ }
338
+
339
+ // Otherwise insert it
340
+ if idxc != ':' && idxc != '*' {
341
+ // []byte for proper unicode char conversion, see #65
342
+ n.indices += string([]byte{idxc})
343
+ child := &node{}
344
+ n.children = append(n.children, child)
345
+ n.incrementChildPrio(len(n.indices) - 1)
346
+ n = child
347
+ }
348
+ n.insertChild(path, fullPath, handle)
349
+ return
350
+ }
351
+
352
+ // Otherwise add handle to current node
353
+ if n.handle != nil {
354
+ panic("a handle is already registered for path '" + fullPath + "'")
355
+ }
356
+ n.handle = handle
357
+ return
358
+ }
359
+ }
360
+
361
+ func (n *node) insertChild(path, fullPath string, handle interface{}) {
362
+ for {
363
+ // Find prefix until first wildcard
364
+ wildcard, i, valid := findWildcard(path)
365
+ if i < 0 { // No wilcard found
366
+ break
367
+ }
368
+
369
+ // The wildcard name must not contain ':' and '*'
370
+ if !valid {
371
+ panic("only one wildcard per path segment is allowed, has: '" +
372
+ wildcard + "' in path '" + fullPath + "'")
373
+ }
374
+
375
+ // Check if the wildcard has a name
376
+ if len(wildcard) < 2 {
377
+ panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
378
+ }
379
+
380
+ // Check if this node has existing children which would be
381
+ // unreachable if we insert the wildcard here
382
+ if len(n.children) > 0 {
383
+ panic("wildcard segment '" + wildcard +
384
+ "' conflicts with existing children in path '" + fullPath + "'")
385
+ }
386
+
387
+ // param
388
+ if wildcard[0] == ':' {
389
+ if i > 0 {
390
+ // Insert prefix before the current wildcard
391
+ n.path = path[:i]
392
+ path = path[i:]
393
+ }
394
+
395
+ n.wildChild = true
396
+ child := &node{
397
+ nType: param,
398
+ path: wildcard,
399
+ }
400
+ n.children = []*node{child}
401
+ n = child
402
+ n.priority++
403
+
404
+ // If the path doesn't end with the wildcard, then there
405
+ // will be another non-wildcard subpath starting with '/'
406
+ if len(wildcard) < len(path) {
407
+ path = path[len(wildcard):]
408
+ child := &node{
409
+ priority: 1,
410
+ }
411
+ n.children = []*node{child}
412
+ n = child
413
+ continue
414
+ }
415
+
416
+ // Otherwise we're done. Insert the handle in the new leaf
417
+ n.handle = handle
418
+ return
419
+ }
420
+
421
+ // catchAll
422
+ if i+len(wildcard) != len(path) {
423
+ panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
424
+ }
425
+
426
+ if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
427
+ panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
428
+ }
429
+
430
+ // Currently fixed width 1 for '/'
431
+ i--
432
+ if path[i] != '/' {
433
+ panic("no / before catch-all in path '" + fullPath + "'")
434
+ }
435
+
436
+ n.path = path[:i]
437
+
438
+ // First node: catchAll node with empty path
439
+ child := &node{
440
+ wildChild: true,
441
+ nType: catchAll,
442
+ }
443
+ n.children = []*node{child}
444
+ n.indices = string('/')
445
+ n = child
446
+ n.priority++
447
+
448
+ // Second node: node holding the variable
449
+ child = &node{
450
+ path: path[i:],
451
+ nType: catchAll,
452
+ handle: handle,
453
+ priority: 1,
454
+ }
455
+ n.children = []*node{child}
456
+
457
+ return
458
+ }
459
+
460
+ // If no wildcard was found, simply insert the path and handle
461
+ n.path = path
462
+ n.handle = handle
463
+ }
464
+
465
+ // Returns the handle registered with the given path (key). The values of
466
+ // wildcards are saved to a map.
467
+ // If no handle can be found, a TSR (trailing slash redirect) recommendation is
468
+ // made if a handle exists with an extra (without the) trailing slash for the
469
+ // given path.
470
+ func (n *node) getValue(path string, params func() *Params) (handle interface{}, ps *Params, tsr bool) {
471
+ walk: // Outer loop for walking the tree
472
+ for {
473
+ prefix := n.path
474
+ if len(path) > len(prefix) {
475
+ if path[:len(prefix)] == prefix {
476
+ path = path[len(prefix):]
477
+
478
+ // If this node does not have a wildcard (param or catchAll)
479
+ // child, we can just look up the next child node and continue
480
+ // to walk down the tree
481
+ if !n.wildChild {
482
+ idxc := path[0]
483
+ for i, c := range []byte(n.indices) {
484
+ if c == idxc {
485
+ n = n.children[i]
486
+ continue walk
487
+ }
488
+ }
489
+
490
+ // Nothing found.
491
+ // We can recommend to redirect to the same URL without a
492
+ // trailing slash if a leaf exists for that path.
493
+ tsr = (path == "/" && n.handle != nil)
494
+ return
495
+ }
496
+
497
+ // Handle wildcard child
498
+ n = n.children[0]
499
+ switch n.nType {
500
+ case param:
501
+ // Find param end (either '/' or path end)
502
+ end := 0
503
+ for end < len(path) && path[end] != '/' {
504
+ end++
505
+ }
506
+
507
+ // Save param value
508
+ if params != nil {
509
+ if ps == nil {
510
+ ps = params()
511
+ }
512
+ // Expand slice within preallocated capacity
513
+ i := len(*ps)
514
+ *ps = (*ps)[:i+1]
515
+ (*ps)[i] = Param{
516
+ Key: n.path[1:],
517
+ Value: path[:end],
518
+ }
519
+ }
520
+
521
+ // We need to go deeper!
522
+ if end < len(path) {
523
+ if len(n.children) > 0 {
524
+ path = path[end:]
525
+ n = n.children[0]
526
+ continue walk
527
+ }
528
+
529
+ // ... but we can't
530
+ tsr = (len(path) == end+1)
531
+ return
532
+ }
533
+
534
+ if handle = n.handle; handle != nil {
535
+ return
536
+ } else if len(n.children) == 1 {
537
+ // No handle found. Check if a handle for this path + a
538
+ // trailing slash exists for TSR recommendation
539
+ n = n.children[0]
540
+ tsr = (n.path == "/" && n.handle != nil) || (n.path == "" && n.indices == "/")
541
+ }
542
+
543
+ return
544
+
545
+ case catchAll:
546
+ // Save param value
547
+ if params != nil {
548
+ if ps == nil {
549
+ ps = params()
550
+ }
551
+ // Expand slice within preallocated capacity
552
+ i := len(*ps)
553
+ *ps = (*ps)[:i+1]
554
+ (*ps)[i] = Param{
555
+ Key: n.path[2:],
556
+ Value: path,
557
+ }
558
+ }
559
+
560
+ handle = n.handle
561
+ return
562
+
563
+ default:
564
+ panic("invalid node type")
565
+ }
566
+ }
567
+ } else if path == prefix {
568
+ // We should have reached the node containing the handle.
569
+ // Check if this node has a handle registered.
570
+ if handle = n.handle; handle != nil {
571
+ return
572
+ }
573
+
574
+ // If there is no handle for this route, but this route has a
575
+ // wildcard child, there must be a handle for this path with an
576
+ // additional trailing slash
577
+ if path == "/" && n.wildChild && n.nType != root {
578
+ tsr = true
579
+ return
580
+ }
581
+
582
+ // No handle found. Check if a handle for this path + a
583
+ // trailing slash exists for trailing slash recommendation
584
+ for i, c := range []byte(n.indices) {
585
+ if c == '/' {
586
+ n = n.children[i]
587
+ tsr = (len(n.path) == 1 && n.handle != nil) ||
588
+ (n.nType == catchAll && n.children[0].handle != nil)
589
+ return
590
+ }
591
+ }
592
+ return
593
+ }
594
+
595
+ // Nothing found. We can recommend to redirect to the same URL with an
596
+ // extra trailing slash if a leaf exists for that path
597
+ tsr = (path == "/") ||
598
+ (len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
599
+ path == prefix[:len(prefix)-1] && n.handle != nil)
600
+ return
601
+ }
602
+ }
603
+
604
+ // Makes a case-insensitive lookup of the given path and tries to find a handler.
605
+ // It can optionally also fix trailing slashes.
606
+ // It returns the case-corrected path and a bool indicating whether the lookup
607
+ // was successful.
608
+ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (fixedPath string, found bool) {
609
+ const stackBufSize = 128
610
+
611
+ // Use a static sized buffer on the stack in the common case.
612
+ // If the path is too long, allocate a buffer on the heap instead.
613
+ buf := make([]byte, 0, stackBufSize)
614
+ if l := len(path) + 1; l > stackBufSize {
615
+ buf = make([]byte, 0, l)
616
+ }
617
+
618
+ ciPath := n.findCaseInsensitivePathRec(
619
+ path,
620
+ buf, // Preallocate enough memory for new path
621
+ [4]byte{}, // Empty rune buffer
622
+ fixTrailingSlash,
623
+ )
624
+
625
+ return string(ciPath), ciPath != nil
626
+ }
627
+
628
+ // Shift bytes in array by n bytes left
629
+ func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
630
+ switch n {
631
+ case 0:
632
+ return rb
633
+ case 1:
634
+ return [4]byte{rb[1], rb[2], rb[3], 0}
635
+ case 2:
636
+ return [4]byte{rb[2], rb[3]}
637
+ case 3:
638
+ return [4]byte{rb[3]}
639
+ default:
640
+ return [4]byte{}
641
+ }
642
+ }
643
+
644
+ // Recursive case-insensitive lookup function used by n.findCaseInsensitivePath
645
+ func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte {
646
+ npLen := len(n.path)
647
+
648
+ walk: // Outer loop for walking the tree
649
+ for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) {
650
+ // Add common prefix to result
651
+ oldPath := path
652
+ path = path[npLen:]
653
+ ciPath = append(ciPath, n.path...)
654
+
655
+ if len(path) > 0 {
656
+ // If this node does not have a wildcard (param or catchAll) child,
657
+ // we can just look up the next child node and continue to walk down
658
+ // the tree
659
+ if !n.wildChild {
660
+ // Skip rune bytes already processed
661
+ rb = shiftNRuneBytes(rb, npLen)
662
+
663
+ if rb[0] != 0 {
664
+ // Old rune not finished
665
+ idxc := rb[0]
666
+ for i, c := range []byte(n.indices) {
667
+ if c == idxc {
668
+ // continue with child node
669
+ n = n.children[i]
670
+ npLen = len(n.path)
671
+ continue walk
672
+ }
673
+ }
674
+ } else {
675
+ // Process a new rune
676
+ var rv rune
677
+
678
+ // Find rune start.
679
+ // Runes are up to 4 byte long,
680
+ // -4 would definitely be another rune.
681
+ var off int
682
+ for max := min(npLen, 3); off < max; off++ {
683
+ if i := npLen - off; utf8.RuneStart(oldPath[i]) {
684
+ // read rune from cached path
685
+ rv, _ = utf8.DecodeRuneInString(oldPath[i:])
686
+ break
687
+ }
688
+ }
689
+
690
+ // Calculate lowercase bytes of current rune
691
+ lo := unicode.ToLower(rv)
692
+ utf8.EncodeRune(rb[:], lo)
693
+
694
+ // Skip already processed bytes
695
+ rb = shiftNRuneBytes(rb, off)
696
+
697
+ idxc := rb[0]
698
+ for i, c := range []byte(n.indices) {
699
+ // Lowercase matches
700
+ if c == idxc {
701
+ // must use a recursive approach since both the
702
+ // uppercase byte and the lowercase byte might exist
703
+ // as an index
704
+ if out := n.children[i].findCaseInsensitivePathRec(
705
+ path, ciPath, rb, fixTrailingSlash,
706
+ ); out != nil {
707
+ return out
708
+ }
709
+ break
710
+ }
711
+ }
712
+
713
+ // If we found no match, the same for the uppercase rune,
714
+ // if it differs
715
+ if up := unicode.ToUpper(rv); up != lo {
716
+ utf8.EncodeRune(rb[:], up)
717
+ rb = shiftNRuneBytes(rb, off)
718
+
719
+ idxc := rb[0]
720
+ for i, c := range []byte(n.indices) {
721
+ // Uppercase matches
722
+ if c == idxc {
723
+ // Continue with child node
724
+ n = n.children[i]
725
+ npLen = len(n.path)
726
+ continue walk
727
+ }
728
+ }
729
+ }
730
+ }
731
+
732
+ // Nothing found. We can recommend to redirect to the same URL
733
+ // without a trailing slash if a leaf exists for that path
734
+ if fixTrailingSlash && path == "/" && n.handle != nil {
735
+ return ciPath
736
+ }
737
+ return nil
738
+ }
739
+
740
+ n = n.children[0]
741
+ switch n.nType {
742
+ case param:
743
+ // Find param end (either '/' or path end)
744
+ end := 0
745
+ for end < len(path) && path[end] != '/' {
746
+ end++
747
+ }
748
+
749
+ // Add param value to case insensitive path
750
+ ciPath = append(ciPath, path[:end]...)
751
+
752
+ // We need to go deeper!
753
+ if end < len(path) {
754
+ if len(n.children) > 0 {
755
+ // Continue with child node
756
+ n = n.children[0]
757
+ npLen = len(n.path)
758
+ path = path[end:]
759
+ continue
760
+ }
761
+
762
+ // ... but we can't
763
+ if fixTrailingSlash && len(path) == end+1 {
764
+ return ciPath
765
+ }
766
+ return nil
767
+ }
768
+
769
+ if n.handle != nil {
770
+ return ciPath
771
+ } else if fixTrailingSlash && len(n.children) == 1 {
772
+ // No handle found. Check if a handle for this path + a
773
+ // trailing slash exists
774
+ n = n.children[0]
775
+ if n.path == "/" && n.handle != nil {
776
+ return append(ciPath, '/')
777
+ }
778
+ }
779
+ return nil
780
+
781
+ case catchAll:
782
+ return append(ciPath, path...)
783
+
784
+ default:
785
+ panic("invalid node type")
786
+ }
787
+ } else {
788
+ // We should have reached the node containing the handle.
789
+ // Check if this node has a handle registered.
790
+ if n.handle != nil {
791
+ return ciPath
792
+ }
793
+
794
+ // No handle found.
795
+ // Try to fix the path by adding a trailing slash
796
+ if fixTrailingSlash {
797
+ for i, c := range []byte(n.indices) {
798
+ if c == '/' {
799
+ n = n.children[i]
800
+ if (len(n.path) == 1 && n.handle != nil) ||
801
+ (n.nType == catchAll && n.children[0].handle != nil) {
802
+ return append(ciPath, '/')
803
+ }
804
+ return nil
805
+ }
806
+ }
807
+ }
808
+ return nil
809
+ }
810
+ }
811
+
812
+ // Nothing found.
813
+ // Try to fix the path by adding / removing a trailing slash
814
+ if fixTrailingSlash {
815
+ if path == "/" {
816
+ return ciPath
817
+ }
818
+ if len(path)+1 == npLen && n.path[len(path)] == '/' &&
819
+ strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handle != nil {
820
+ return append(ciPath, n.path...)
821
+ }
822
+ }
823
+ return nil
824
+ }
825
+
826
+ // Param is a single URL parameter, consisting of a key and a value.
827
+ type Param struct {
828
+ Key string
829
+ Value string
830
+ }
831
+
832
+ // Params is a Param-slice, as returned by the router.
833
+ // The slice is ordered, the first URL parameter is also the first slice value.
834
+ // It is therefore safe to read values by the index.
835
+ type Params []Param
836
+
837
+ // ByName returns the value of the first Param which key matches the given name.
838
+ // If no matching Param is found, an empty string is returned.
839
+ func (ps Params) ByName(name string) string {
840
+ for _, p := range ps {
841
+ if p.Key == name {
842
+ return p.Value
843
+ }
844
+ }
845
+ return ""
846
+ }
847
+
848
+ type paramsKey struct{}
849
+
850
+ // ParamsKey is the request context key under which URL params are stored.
851
+ var ParamsKey = paramsKey{}
852
+
853
+ // ParamsFromContext pulls the URL parameters from a request context,
854
+ // or returns nil if none are present.
855
+ func ParamsFromContext(ctx context.Context) Params {
856
+ p, _ := ctx.Value(ParamsKey).(Params)
857
+ return p
858
+ }
859
+
860
+ // MatchedRoutePathParam is the Param name under which the path of the matched
861
+ // route is stored, if Router.SaveMatchedRoutePath is set.
862
+ var MatchedRoutePathParam = "$matchedRoutePath"
863
+
864
+ // MatchedRoutePath retrieves the path of the matched route.
865
+ // Router.SaveMatchedRoutePath must have been enabled when the respective
866
+ // handler was added, otherwise this function always returns an empty string.
867
+ func (ps Params) MatchedRoutePath() string {
868
+ return ps.ByName(MatchedRoutePathParam)
869
+ }
870
+
871
+ // Router is a http.Handler which can be used to dispatch requests to different
872
+ // handler functions via configurable routes
873
+ type Router struct {
874
+ trees map[string]*node
875
+
876
+ paramsPool sync.Pool
877
+ maxParams uint16
878
+
879
+ // If enabled, adds the matched route path onto the http.Request context
880
+ // before invoking the handler.
881
+ // The matched route path is only added to handlers of routes that were
882
+ // registered when this option was enabled.
883
+ SaveMatchedRoutePath bool
884
+
885
+ // Enables automatic redirection if the current route can't be matched but a
886
+ // handler for the path with (without) the trailing slash exists.
887
+ // For example if /foo/ is requested but a route only exists for /foo, the
888
+ // client is redirected to /foo with http status code 301 for GET requests
889
+ // and 308 for all other request methods.
890
+ RedirectTrailingSlash bool
891
+
892
+ // If enabled, the router tries to fix the current request path, if no
893
+ // handle is registered for it.
894
+ // First superfluous path elements like ../ or // are removed.
895
+ // Afterwards the router does a case-insensitive lookup of the cleaned path.
896
+ // If a handle can be found for this route, the router makes a redirection
897
+ // to the corrected path with status code 301 for GET requests and 308 for
898
+ // all other request methods.
899
+ // For example /FOO and /..//Foo could be redirected to /foo.
900
+ // RedirectTrailingSlash is independent of this option.
901
+ RedirectFixedPath bool
902
+
903
+ // If enabled, the router checks if another method is allowed for the
904
+ // current route, if the current request can not be routed.
905
+ // If this is the case, the request is answered with 'Method Not Allowed'
906
+ // and HTTP status code 405.
907
+ // If no other Method is allowed, the request is delegated to the NotFound
908
+ // handler.
909
+ HandleMethodNotAllowed bool
910
+
911
+ // If enabled, the router automatically replies to OPTIONS requests.
912
+ // Custom OPTIONS handlers take priority over automatic replies.
913
+ HandleOPTIONS bool
914
+
915
+ // An optional http.Handler that is called on automatic OPTIONS requests.
916
+ // The handler is only called if HandleOPTIONS is true and no OPTIONS
917
+ // handler for the specific path was set.
918
+ // The "Allowed" header is set before calling the handler.
919
+ GlobalOPTIONS http.Handler
920
+
921
+ // Cached value of global (*) allowed methods
922
+ globalAllowed string
923
+
924
+ // Configurable http.Handler which is called when no matching route is
925
+ // found. If it is not set, http.NotFound is used.
926
+ NotFound RenderFunc
927
+
928
+ // Configurable http.Handler which is called when a request
929
+ // cannot be routed and HandleMethodNotAllowed is true.
930
+ // If it is not set, http.Error with http.StatusMethodNotAllowed is used.
931
+ // The "Allow" header with allowed request methods is set before the handler
932
+ // is called.
933
+ MethodNotAllowed http.Handler
934
+
935
+ // Function to handle panics recovered from http handlers.
936
+ // It should be used to generate a error page and return the http error code
937
+ // 500 (Internal Server Error).
938
+ // The handler can be used to keep your server from crashing because of
939
+ // unrecovered panics.
940
+ PanicHandler func(http.ResponseWriter, *http.Request, interface{})
941
+ }
942
+
943
+ // func(http.ResponseWriter, *http.Request, Params)
944
+ // Make sure the Router conforms with the http.Handler interface
945
+ // var _ http.Handler = New()
946
+
947
+ var router = &Router{
948
+ RedirectTrailingSlash: true,
949
+ RedirectFixedPath: true,
950
+ HandleMethodNotAllowed: true,
951
+ HandleOPTIONS: true,
952
+ }
953
+
954
+ func (r *Router) getParams() *Params {
955
+ ps, _ := r.paramsPool.Get().(*Params)
956
+ *ps = (*ps)[0:0] // reset slice
957
+ return ps
958
+ }
959
+
960
+ func (r *Router) putParams(ps *Params) {
961
+ if ps != nil {
962
+ r.paramsPool.Put(ps)
963
+ }
964
+ }
965
+
966
+ // func (r *Router) saveMatchedRoutePath(path string, handle interface{}) interface{} {
967
+ // return func(w http.ResponseWriter, req *http.Request, ps Params) {
968
+ // if ps == nil {
969
+ // psp := r.getParams()
970
+ // ps = (*psp)[0:1]
971
+ // ps[0] = Param{Key: MatchedRoutePathParam, Value: path}
972
+ // println("route: " + r.URL.Path)
973
+ // page := createPage(info, render(NewRenderContext()))
974
+ // w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
975
+ // w.Header().Set("Content-Type", "text/html")
976
+ // w.WriteHeader(http.StatusOK)
977
+ // w.Write(page.Bytes())
978
+ // handle(w, req, ps)
979
+ // r.putParams(psp)
980
+ // } else {
981
+ // ps = append(ps, Param{Key: MatchedRoutePathParam, Value: path})
982
+ // handle(w, req, ps)
983
+ // }
984
+ // }
985
+ // }
986
+
987
+ // GET is a shortcut for router.Handle(http.MethodGet, path, handle)
988
+ func (r *Router) GET(path string, handle interface{}) {
989
+ r.Handle(http.MethodGet, path, handle)
990
+ }
991
+
992
+ // HEAD is a shortcut for router.Handle(http.MethodHead, path, handle)
993
+ func (r *Router) HEAD(path string, handle interface{}) {
994
+ r.Handle(http.MethodHead, path, handle)
995
+ }
996
+
997
+ // OPTIONS is a shortcut for router.Handle(http.MethodOptions, path, handle)
998
+ func (r *Router) OPTIONS(path string, handle interface{}) {
999
+ r.Handle(http.MethodOptions, path, handle)
1000
+ }
1001
+
1002
+ // POST is a shortcut for router.Handle(http.MethodPost, path, handle)
1003
+ func (r *Router) POST(path string, handle interface{}) {
1004
+ r.Handle(http.MethodPost, path, handle)
1005
+ }
1006
+
1007
+ // PUT is a shortcut for router.Handle(http.MethodPut, path, handle)
1008
+ func (r *Router) PUT(path string, handle interface{}) {
1009
+ r.Handle(http.MethodPut, path, handle)
1010
+ }
1011
+
1012
+ // PATCH is a shortcut for router.Handle(http.MethodPatch, path, handle)
1013
+ func (r *Router) PATCH(path string, handle interface{}) {
1014
+ r.Handle(http.MethodPatch, path, handle)
1015
+ }
1016
+
1017
+ // DELETE is a shortcut for router.Handle(http.MethodDelete, path, handle)
1018
+ func (r *Router) DELETE(path string, handle interface{}) {
1019
+ r.Handle(http.MethodDelete, path, handle)
1020
+ }
1021
+
1022
+ // Handle registers a new request handle with the given path and method.
1023
+ //
1024
+ // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
1025
+ // functions can be used.
1026
+ //
1027
+ // This function is intended for bulk loading and to allow the usage of less
1028
+ // frequently used, non-standardized or custom methods (e.g. for internal
1029
+ // communication with a proxy).
1030
+ func (r *Router) Handle(method, path string, handle interface{}) {
1031
+ varsCount := uint16(0)
1032
+
1033
+ if method == "" {
1034
+ panic("method must not be empty")
1035
+ }
1036
+ if len(path) < 1 || path[0] != '/' {
1037
+ panic("path must begin with '/' in path '" + path + "'")
1038
+ }
1039
+ if handle == nil {
1040
+ panic("handle must not be nil")
1041
+ }
1042
+
1043
+ // if r.SaveMatchedRoutePath {
1044
+ // varsCount++
1045
+ // handle = r.saveMatchedRoutePath(path, handle)
1046
+ // }
1047
+
1048
+ if r.trees == nil {
1049
+ r.trees = make(map[string]*node)
1050
+ }
1051
+
1052
+ root := r.trees[method]
1053
+ if root == nil {
1054
+ root = new(node)
1055
+ r.trees[method] = root
1056
+
1057
+ r.globalAllowed = r.allowed("*", "")
1058
+ }
1059
+
1060
+ root.addRoute(path, handle)
1061
+
1062
+ // Update maxParams
1063
+ if paramsCount := countParams(path); paramsCount+varsCount > r.maxParams {
1064
+ r.maxParams = paramsCount + varsCount
1065
+ }
1066
+
1067
+ // Lazy-init paramsPool alloc func
1068
+ if r.paramsPool.New == nil && r.maxParams > 0 {
1069
+ r.paramsPool.New = func() interface{} {
1070
+ ps := make(Params, 0, r.maxParams)
1071
+ return &ps
1072
+ }
1073
+ }
1074
+ }
1075
+
1076
+ // Handler is an adapter which allows the usage of an http.Handler as a
1077
+ // request handle.
1078
+ // The Params are available in the request context under ParamsKey.
1079
+ // func (r *Router) Handler(method, path string, handler http.Handler) {
1080
+ // r.Handle(method, path,
1081
+ // func(w http.ResponseWriter, req *http.Request, p Params) {
1082
+ // if len(p) > 0 {
1083
+ // ctx := req.Context()
1084
+ // ctx = context.WithValue(ctx, ParamsKey, p)
1085
+ // req = req.WithContext(ctx)
1086
+ // }
1087
+ // handler.ServeHTTP(w, req)
1088
+ // },
1089
+ // )
1090
+ // }
1091
+
1092
+ // HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a
1093
+ // request handle.
1094
+ // func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) {
1095
+ // r.Handler(method, path, handler)
1096
+ // }
1097
+
1098
+ func (r *Router) recv(w http.ResponseWriter, req *http.Request) {
1099
+ if rcv := recover(); rcv != nil {
1100
+ r.PanicHandler(w, req, rcv)
1101
+ }
1102
+ }
1103
+
1104
+ // Lookup allows the manual lookup of a method + path combo.
1105
+ // This is e.g. useful to build a framework around this router.
1106
+ // If the path was found, it returns the handle function and the path parameter
1107
+ // values. Otherwise the third return value indicates whether a redirection to
1108
+ // the same path with an extra / without the trailing slash should be performed.
1109
+ func (r *Router) Lookup(method, path string) (interface{}, Params, bool) {
1110
+ if root := r.trees[method]; root != nil {
1111
+ handle, ps, tsr := root.getValue(path, r.getParams)
1112
+ if handle == nil {
1113
+ r.putParams(ps)
1114
+ return nil, nil, tsr
1115
+ }
1116
+ if ps == nil {
1117
+ return handle, nil, tsr
1118
+ }
1119
+ return handle, *ps, tsr
1120
+ }
1121
+ return nil, nil, false
1122
+ }
1123
+
1124
+ func (r *Router) allowed(path, reqMethod string) (allow string) {
1125
+ allowed := make([]string, 0, 9)
1126
+
1127
+ if path == "*" { // server-wide
1128
+ // empty method is used for internal calls to refresh the cache
1129
+ if reqMethod == "" {
1130
+ for method := range r.trees {
1131
+ if method == http.MethodOptions {
1132
+ continue
1133
+ }
1134
+ // Add request method to list of allowed methods
1135
+ allowed = append(allowed, method)
1136
+ }
1137
+ } else {
1138
+ return r.globalAllowed
1139
+ }
1140
+ } else { // specific path
1141
+ for method := range r.trees {
1142
+ // Skip the requested method - we already tried this one
1143
+ if method == reqMethod || method == http.MethodOptions {
1144
+ continue
1145
+ }
1146
+
1147
+ handle, _, _ := r.trees[method].getValue(path, nil)
1148
+ if handle != nil {
1149
+ // Add request method to list of allowed methods
1150
+ allowed = append(allowed, method)
1151
+ }
1152
+ }
1153
+ }
1154
+
1155
+ if len(allowed) > 0 {
1156
+ // Add request method to list of allowed methods
1157
+ allowed = append(allowed, http.MethodOptions)
1158
+
1159
+ // Sort allowed methods.
1160
+ // sort.Strings(allowed) unfortunately causes unnecessary allocations
1161
+ // due to allowed being moved to the heap and interface conversion
1162
+ for i, l := 1, len(allowed); i < l; i++ {
1163
+ for j := i; j > 0 && allowed[j] < allowed[j-1]; j-- {
1164
+ allowed[j], allowed[j-1] = allowed[j-1], allowed[j]
1165
+ }
1166
+ }
1167
+
1168
+ // return as comma separated list
1169
+ return strings.Join(allowed, ", ")
1170
+ }
1171
+
1172
+ return allow
1173
+ }
1174
+
1175
+ // ServeHTTP makes the router implement the http.Handler interface.
1176
+ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
1177
+ if r.PanicHandler != nil {
1178
+ defer r.recv(w, req)
1179
+ }
1180
+
1181
+ path := req.URL.Path
1182
+
1183
+ if root := r.trees[req.Method]; root != nil {
1184
+ // TODO: use _ ps save it to context for useParam()
1185
+ if handle, _, tsr := root.getValue(path, r.getParams); handle != nil {
1186
+ println("route: " + req.URL.Path)
1187
+ if render, ok := handle.(RenderFunc); ok {
1188
+ page := createPage(RouteInfo{}, render(NewRenderContext()))
1189
+ w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
1190
+ w.Header().Set("Content-Type", "text/html")
1191
+ w.WriteHeader(http.StatusOK)
1192
+ w.Write(page.Bytes())
1193
+ return
1194
+ } else {
1195
+ handle.(func(w http.ResponseWriter, r *http.Request))(w, req)
1196
+ return
1197
+ }
1198
+
1199
+ } else if req.Method != http.MethodConnect && path != "/" {
1200
+ // Moved Permanently, request with GET method
1201
+ code := http.StatusMovedPermanently
1202
+ if req.Method != http.MethodGet {
1203
+ // Permanent Redirect, request with same method
1204
+ code = http.StatusPermanentRedirect
1205
+ }
1206
+
1207
+ if tsr && r.RedirectTrailingSlash {
1208
+ if len(path) > 1 && path[len(path)-1] == '/' {
1209
+ req.URL.Path = path[:len(path)-1]
1210
+ } else {
1211
+ req.URL.Path = path + "/"
1212
+ }
1213
+ http.Redirect(w, req, req.URL.String(), code)
1214
+ return
1215
+ }
1216
+
1217
+ // Try to fix the request path
1218
+ if r.RedirectFixedPath {
1219
+ fixedPath, found := root.findCaseInsensitivePath(
1220
+ CleanPath(path),
1221
+ r.RedirectTrailingSlash,
1222
+ )
1223
+ if found {
1224
+ req.URL.Path = fixedPath
1225
+ http.Redirect(w, req, req.URL.String(), code)
1226
+ return
1227
+ }
1228
+ }
1229
+ }
1230
+ }
1231
+
1232
+ if req.Method == http.MethodOptions && r.HandleOPTIONS {
1233
+ // Handle OPTIONS requests
1234
+ if allow := r.allowed(path, http.MethodOptions); allow != "" {
1235
+ w.Header().Set("Allow", allow)
1236
+ if r.GlobalOPTIONS != nil {
1237
+ r.GlobalOPTIONS.ServeHTTP(w, req)
1238
+ }
1239
+ return
1240
+ }
1241
+ } else if r.HandleMethodNotAllowed { // Handle 405
1242
+ if allow := r.allowed(path, req.Method); allow != "" {
1243
+ w.Header().Set("Allow", allow)
1244
+ if r.MethodNotAllowed != nil {
1245
+ r.MethodNotAllowed.ServeHTTP(w, req)
1246
+ } else {
1247
+ http.Error(w,
1248
+ http.StatusText(http.StatusMethodNotAllowed),
1249
+ http.StatusMethodNotAllowed,
1250
+ )
1251
+ }
1252
+ return
1253
+ }
1254
+ }
1255
+
1256
+ // Handle 404
1257
+ if r.NotFound != nil {
1258
+ page := createPage(RouteInfo{}, r.NotFound(NewRenderContext()))
1259
+ w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
1260
+ w.Header().Set("Content-Type", "text/html")
1261
+ w.WriteHeader(http.StatusOK)
1262
+ w.Write(page.Bytes())
1263
+ } else {
1264
+ page := createPage(RouteInfo{}, DefaultNotFound(NewRenderContext()))
1265
+ w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
1266
+ w.Header().Set("Content-Type", "text/html")
1267
+ w.WriteHeader(http.StatusOK)
1268
+ w.Write(page.Bytes())
1269
+ }
1270
+ }
1271
+
1272
+ func DefaultNotFound(c *RenderContext) UI {
1273
+ return Col(
1274
+ Row(
1275
+ "This is the default 404 - Not Found Route handler",
1276
+ ),
1277
+ Row(
1278
+ "Create a notfound.go file and add a func NotFound(c *RenderContext) UI {} to override it",
1279
+ ),
1280
+ )
1281
+ }
utils.go CHANGED
@@ -2,7 +2,6 @@ package app
2
2
 
3
3
  import (
4
4
  "io"
5
- "regexp"
6
5
  "unsafe"
7
6
 
8
7
  "github.com/pyros2097/wapp/js"
@@ -50,32 +49,9 @@ func stob(s string) []byte {
50
49
  return *(*[]byte)(unsafe.Pointer(&s))
51
50
  }
52
51
 
53
- func matchPath(k, p string) bool {
54
- validRoute := regexp.MustCompile(k)
55
- if validRoute.MatchString(p) {
52
+ type RouteInfo struct {
56
- return true
53
+ Title string
57
- }
58
- return false
59
- }
60
-
61
- func MatchRoute(routes map[string]RenderFunc, path string) RenderFunc {
62
- for key, renderFn := range routes {
63
- if matchPath(key, path) {
64
- return renderFn
54
+ Description string
65
- }
66
- }
67
- notFound, ok := routes["/notfound"]
68
- if ok {
69
- return notFound
55
+ Author string
70
- }
71
- return func(c *RenderContext) UI {
72
- return Col(
56
+ Keywords string
73
- Row(
74
- "This is the default 404 - Not Found Route handler",
75
- ),
76
- Row(
77
- "Create a notfound.go file and add a func NotFound(c *RenderContext) UI {} to override it",
78
- ),
79
- )
80
- }
81
57
  }