Repository: labstack/echo (go)

Issue:
Allow escaped `:` char in route path
### Discussed in https://github.com/labstack/echo/discussions/1986

<div type='discussions-op-text'>

<sup>Originally posted by **morilog** September 16, 2021</sup>
Can I able to escape `:` char in route path to achieve compatiblity with [google API design](https://cloud.google.com/apis/design/custom_methods) custom methods?  something like [fiber framework](https://docs.gofiber.io/guide/routing#parameters)

For example in  `https://service.name/v1/some/resource/name:customVerb`, the `:customVerb` is not route parameter and only is a simple string.</div>


```go
// curl -v -X POST "http://localhost:8088/v1/some/resource/name:undelete"
func main() {
	e := echo.New()

	// route path as Cloud API "custom method"
	// https://cloud.google.com/apis/design/custom_methods
	e.POST("/v1/some/resource/name\\:undelete", func(c echo.Context) error {
		resp := fmt.Sprintf("mehtod: %v\n", c.Request().Method)
		return c.String(200, resp)
	})
	log.Fatal(e.Start(":8088"))
}
```

Pull request description:
allow escaping of colon in route path 
allow escaping of colon in route path so Google Cloud API "custom methods" https://cloud.google.com/apis/design/custom_methods could be implemented (resolves #1987)

Patch:
diff --git a/router.go b/router.go
index 5b2474b..a8277c8 100644
--- a/router.go
+++ b/router.go
@@ -98,6 +98,9 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
 
 	for i, lcpIndex := 0, len(path); i < lcpIndex; i++ {
 		if path[i] == ':' {
+			if i > 0 && path[i-1] == '\\' {
+				continue
+			}
 			j := i + 1
 
 			r.insert(method, path[:i], nil, staticKind, "", nil)


Test patch:
diff --git a/router_test.go b/router_test.go
index 71cedf8..1cb36b4 100644
--- a/router_test.go
+++ b/router_test.go
@@ -1118,6 +1118,58 @@ func TestRouterParamStaticConflict(t *testing.T) {
 	}
 }
 
+func TestRouterParam_escapeColon(t *testing.T) {
+	// to allow Google cloud API like route paths with colon in them
+	// i.e. https://service.name/v1/some/resource/name:customVerb <- that `:customVerb` is not path param. It is just a string
+	e := New()
+
+	e.POST("/files/a/long/file\\:undelete", handlerFunc)
+	e.POST("/v1/some/resource/name:customVerb", handlerFunc)
+
+	var testCases = []struct {
+		whenURL     string
+		expectRoute interface{}
+		expectParam map[string]string
+		expectError string
+	}{
+		{
+			whenURL:     "/files/a/long/file\\:undelete",
+			expectRoute: "/files/a/long/file\\:undelete",
+			expectParam: map[string]string{},
+		},
+		{
+			whenURL:     "/files/a/long/file\\:notMatching",
+			expectRoute: nil,
+			expectError: "code=404, message=Not Found",
+			expectParam: nil,
+		},
+		{
+			whenURL:     "/v1/some/resource/name:PATCH",
+			expectRoute: "/v1/some/resource/name:customVerb",
+			expectParam: map[string]string{"customVerb": ":PATCH"},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.whenURL, func(t *testing.T) {
+			c := e.NewContext(nil, nil).(*context)
+
+			e.router.Find(http.MethodPost, tc.whenURL, c)
+			err := c.handler(c)
+
+			assert.Equal(t, tc.expectRoute, c.Get("path"))
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+			for param, expectedValue := range tc.expectParam {
+				assert.Equal(t, expectedValue, c.Param(param))
+			}
+			checkUnusedParamValues(t, c, tc.expectParam)
+		})
+	}
+}
+
 func TestRouterMatchAny(t *testing.T) {
 	e := New()
 	r := e.router