diff --git a/htmx_route.go b/htmx_route.go index 9db68e4..28dac13 100644 --- a/htmx_route.go +++ b/htmx_route.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "runtime/debug" "sort" "strings" @@ -173,12 +174,144 @@ type HtmxRoute interface { ToUrlFromContext(c HtmxContext, language Language) string GetHtmx(language Language, queryParams ...HtmxPathParam) gomponents.Node SetActivePath(activePath *ActivePath) HtmxRoute + GetPaths() LanguagePaths Add(server HtmxServer) Execute(c HtmxContext) RedirectToThisRoute(c HtmxContext, params LocationRedirectParams) IsCurrentRoute(c HtmxContext) bool } +type GlobalHtmxRoutes interface { + GetRoutes() []HtmxRoute + Add(route HtmxRoute) + // MatchByPath tries to find a route whose template matches the given path + // even if the path contains path parameters. Returns (route, true) if found. + MatchByPath(path string) (*HtmxRouteInfo, bool) +} + +type HtmxRouteInfo struct { + initTrace string + route HtmxRoute +} + +type GlobalHtmxRoutesImpl struct { + routes map[string]*HtmxRouteInfo +} + +var GlobalHtmxRoutesInstance GlobalHtmxRoutes = NewGlobalHtmxRoutesImpl() + +func (g *GlobalHtmxRoutesImpl) GetRoutes() []HtmxRoute { + routes := make([]HtmxRoute, len(g.routes)) + index := 0 + for _, route := range g.routes { + Debug("index: ", index, "route: ", route, "") + routes[index] = route.route + index++ + } + return routes +} + +func (g *GlobalHtmxRoutesImpl) Add(route HtmxRoute) { + paths := route.GetPaths() + uniqueKey := "" + for language, path := range paths { + uniqueKey += string(language) + path + } + g.routes[uniqueKey] = &HtmxRouteInfo{ + initTrace: string(debug.Stack()), + route: route, + } +} + +// MatchByPath tries to find a route whose language-specific template matches the given +// concrete URL path. It treats path parameters like {id} as wildcards. +func (g *GlobalHtmxRoutesImpl) MatchByPath(path string) (*HtmxRouteInfo, bool) { + p := normalizePath(path) + // Extract language as first segment (if present) + trim := strings.Trim(p, "/") + segs := []string{} + if trim != "" { + segs = strings.Split(trim, "/") + } + if len(segs) == 0 { + return nil, false + } + lang := Language(segs[0]) + + for _, info := range g.routes { + paths := info.route.GetPaths() + tpl, ok := paths[lang] + if !ok { + continue + } + candidate := addLanguageToPath(tpl, lang) + if matchTemplate(candidate, p) { + return info, true + } + } + return nil, false +} + +func normalizePath(path string) string { + if path == "" { + return "/" + } + // strip query string and fragment + if i := strings.Index(path, "?"); i >= 0 { + path = path[:i] + } + if i := strings.Index(path, "#"); i >= 0 { + path = path[:i] + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + // remove duplicate slashes (basic) + for strings.Contains(path, "//") { + path = strings.ReplaceAll(path, "//", "/") + } + // drop trailing slash except root + if len(path) > 1 && strings.HasSuffix(path, "/") { + path = strings.TrimSuffix(path, "/") + } + return path +} + +func matchTemplate(templatePath, concretePath string) bool { + // Normalize both + t := normalizePath(templatePath) + p := normalizePath(concretePath) + // Quick equality short-circuit + if t == p { + return true + } + // Compare segment-wise + tSegs := strings.Split(strings.Trim(t, "/"), "/") + pSegs := strings.Split(strings.Trim(p, "/"), "/") + if len(tSegs) != len(pSegs) { + return false + } + for i := range tSegs { + seg := tSegs[i] + if isParamSegment(seg) { + // wildcard for one segment + continue + } + if seg != pSegs[i] { + return false + } + } + return true +} + +func isParamSegment(seg string) bool { + return strings.HasPrefix(seg, "{") && strings.HasSuffix(seg, "}") && len(seg) > 2 +} + +func NewGlobalHtmxRoutesImpl() *GlobalHtmxRoutesImpl { + return &GlobalHtmxRoutesImpl{routes: make(map[string]*HtmxRouteInfo)} +} + func addLanguageToPath(path string, language Language) string { return "/" + string(language) + path } @@ -192,6 +325,10 @@ type CommonHtmxRoute struct { HtmxMethod HtmxHttpMethodFunction } +func (h CommonHtmxRoute) GetPaths() LanguagePaths { + return h.Paths +} + func (h CommonHtmxRoute) ToUrlFromContext(c HtmxContext, language Language) string { routeWithParams := h.WithPathParams(h.extractPathParams(c)...) queryParams := c.GetQueryParams() @@ -218,7 +355,9 @@ func (h CommonHtmxRoute) RedirectToThisRoute(c HtmxContext, params LocationRedir func NewCommonHtmxRoute(routeFunc HtmxRouteFunc, paths LanguagePaths, method string, htmxMethod HtmxHttpMethodFunction) *CommonHtmxRoute { - return &CommonHtmxRoute{RouteFunc: routeFunc, Paths: paths, Method: method, HtmxMethod: htmxMethod} + route := &CommonHtmxRoute{RouteFunc: routeFunc, Paths: paths, Method: method, HtmxMethod: htmxMethod} + GlobalHtmxRoutesInstance.Add(route) + return route } func (h CommonHtmxRoute) Execute(c HtmxContext) {