365 lines
9.5 KiB
Go
365 lines
9.5 KiB
Go
package ersteller_lib
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
|
|
"maragu.dev/gomponents"
|
|
htmx "maragu.dev/gomponents-htmx"
|
|
)
|
|
|
|
type HtmxContext interface {
|
|
Render(node gomponents.Node)
|
|
SetError(err error)
|
|
GetLanguage() Language
|
|
GetActivePath() *ActivePath
|
|
HasActivePath() bool
|
|
SetActivePath(activePath *ActivePath)
|
|
GetPath() string
|
|
GetPathTemplate() string
|
|
SetPathTemplate(pathTemplate string)
|
|
GetPathParam(key string, defaultValue ...string) string
|
|
GetFormValue(key string) string
|
|
SetHeader(key string, value string)
|
|
SetAllRoutes(routes []HtmxRoute)
|
|
GetAllRoutes() []HtmxRoute
|
|
GetQueryParams() []HtmxPathParam
|
|
}
|
|
|
|
type HtmxContextImpl struct {
|
|
req *http.Request
|
|
res http.ResponseWriter
|
|
language Language
|
|
activePath *ActivePath
|
|
pathTemplate string
|
|
routes []HtmxRoute
|
|
}
|
|
|
|
func (c *HtmxContextImpl) GetQueryParams() []HtmxPathParam {
|
|
// Extract query parameters from the current request URL and return them as
|
|
// a deterministic list of HtmxPathParam. If a key has multiple values,
|
|
// each value is included as a separate entry with the same key. Keys are
|
|
// returned in sorted order to keep behavior stable for tests and callers.
|
|
if c == nil || c.req == nil || c.req.URL == nil {
|
|
return []HtmxPathParam{}
|
|
}
|
|
|
|
values := c.req.URL.Query() // url.Values already decoded
|
|
if len(values) == 0 {
|
|
return []HtmxPathParam{}
|
|
}
|
|
|
|
// Collect and sort keys for deterministic output
|
|
keys := make([]string, 0, len(values))
|
|
for k := range values {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
params := make([]HtmxPathParam, 0)
|
|
for _, k := range keys {
|
|
vs := values[k]
|
|
if len(vs) == 0 {
|
|
params = append(params, HtmxPathParam{Key: k, Value: ""})
|
|
continue
|
|
}
|
|
for _, v := range vs {
|
|
params = append(params, HtmxPathParam{Key: k, Value: v})
|
|
}
|
|
}
|
|
return params
|
|
}
|
|
|
|
func (c *HtmxContextImpl) GetAllRoutes() []HtmxRoute {
|
|
return c.routes
|
|
}
|
|
|
|
func (c *HtmxContextImpl) SetAllRoutes(routes []HtmxRoute) {
|
|
c.routes = routes
|
|
}
|
|
|
|
func (c *HtmxContextImpl) SetPathTemplate(pathTemplate string) {
|
|
c.pathTemplate = pathTemplate
|
|
}
|
|
|
|
func (c *HtmxContextImpl) GetPathTemplate() string {
|
|
return c.pathTemplate
|
|
}
|
|
|
|
func (c *HtmxContextImpl) SetHeader(key string, value string) {
|
|
c.res.Header().Set(key, value)
|
|
}
|
|
|
|
func (c *HtmxContextImpl) GetFormValue(key string) string {
|
|
err := c.req.ParseForm()
|
|
if err != nil {
|
|
Error("failed to parse form", err)
|
|
c.SetError(err)
|
|
return ""
|
|
}
|
|
return c.req.FormValue(key)
|
|
}
|
|
|
|
func (c *HtmxContextImpl) SetError(err error) {
|
|
_, writeErr := c.res.Write([]byte(err.Error()))
|
|
if writeErr != nil {
|
|
Error("failed to write error", writeErr)
|
|
}
|
|
c.res.WriteHeader(500)
|
|
}
|
|
|
|
func (c *HtmxContextImpl) GetPathParam(key string, defaultValue ...string) string {
|
|
pathParam := c.req.PathValue(key)
|
|
if pathParam != "" {
|
|
return pathParam
|
|
}
|
|
if len(defaultValue) > 0 {
|
|
return defaultValue[0]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *HtmxContextImpl) GetPath() string {
|
|
return c.req.URL.Path
|
|
}
|
|
|
|
func (c *HtmxContextImpl) SetActivePath(activePath *ActivePath) {
|
|
c.activePath = activePath
|
|
}
|
|
|
|
func (c *HtmxContextImpl) GetActivePath() *ActivePath {
|
|
return c.activePath
|
|
}
|
|
|
|
func (c *HtmxContextImpl) HasActivePath() bool {
|
|
return c.activePath != nil
|
|
}
|
|
|
|
func (c *HtmxContextImpl) GetLanguage() Language {
|
|
return c.language
|
|
}
|
|
|
|
func NewHtmxContext(req *http.Request, res http.ResponseWriter, language Language) HtmxContext {
|
|
return &HtmxContextImpl{req: req, res: res, language: language}
|
|
}
|
|
|
|
func (c *HtmxContextImpl) Render(node gomponents.Node) {
|
|
err := node.Render(c.res)
|
|
if err != nil {
|
|
Error("failed to render", err)
|
|
c.SetError(err)
|
|
}
|
|
}
|
|
|
|
type HtmxRouteFunc func(ctx HtmxContext)
|
|
|
|
type HtmxPathParam struct {
|
|
Key string
|
|
Value string
|
|
}
|
|
|
|
type LocationRedirectParams struct {
|
|
Path string `json:"path"`
|
|
Target string `json:"target,omitempty"`
|
|
Swap string `json:"swap,omitempty"`
|
|
}
|
|
|
|
type HtmxRoute interface {
|
|
WithPathParams(params ...HtmxPathParam) HtmxRoute
|
|
ToUrl(language Language, queryParams ...HtmxPathParam) string
|
|
ToUrlFromContext(c HtmxContext, language Language) string
|
|
GetHtmx(language Language, queryParams ...HtmxPathParam) gomponents.Node
|
|
SetActivePath(activePath *ActivePath) HtmxRoute
|
|
Add(server HtmxServer)
|
|
Execute(c HtmxContext)
|
|
RedirectToThisRoute(c HtmxContext, params LocationRedirectParams)
|
|
IsCurrentRoute(c HtmxContext) bool
|
|
}
|
|
|
|
func addLanguageToPath(path string, language Language) string {
|
|
return "/" + string(language) + path
|
|
}
|
|
|
|
type CommonHtmxRoute struct {
|
|
Paths LanguagePaths
|
|
RouteFunc HtmxRouteFunc
|
|
PathParams []HtmxPathParam
|
|
ActivePath *ActivePath
|
|
Method string
|
|
HtmxMethod HtmxHttpMethodFunction
|
|
}
|
|
|
|
func (h CommonHtmxRoute) ToUrlFromContext(c HtmxContext, language Language) string {
|
|
routeWithParams := h.WithPathParams(h.extractPathParams(c)...)
|
|
queryParams := c.GetQueryParams()
|
|
return routeWithParams.ToUrl(language, queryParams...)
|
|
}
|
|
|
|
func (h CommonHtmxRoute) IsCurrentRoute(c HtmxContext) bool {
|
|
templatePath := c.GetPathTemplate()
|
|
urlOfThisRoute := h.ToUrl(c.GetLanguage())
|
|
return templatePath == urlOfThisRoute
|
|
}
|
|
|
|
func (h CommonHtmxRoute) RedirectToThisRoute(c HtmxContext, params LocationRedirectParams) {
|
|
params.Path = h.ToUrl(c.GetLanguage())
|
|
jsonData, err := json.Marshal(params)
|
|
if err != nil {
|
|
marshalError := fmt.Errorf("failed to marshal LocationRedirectParams: %w, %w", err, params)
|
|
Error(marshalError)
|
|
c.SetError(marshalError)
|
|
return
|
|
}
|
|
c.SetHeader("HX-Location", string(jsonData))
|
|
}
|
|
|
|
func NewCommonHtmxRoute(routeFunc HtmxRouteFunc, paths LanguagePaths, method string,
|
|
htmxMethod HtmxHttpMethodFunction) *CommonHtmxRoute {
|
|
return &CommonHtmxRoute{RouteFunc: routeFunc, Paths: paths, Method: method, HtmxMethod: htmxMethod}
|
|
}
|
|
|
|
func (h CommonHtmxRoute) Execute(c HtmxContext) {
|
|
h.RouteFunc(c)
|
|
}
|
|
|
|
func (h CommonHtmxRoute) SetActivePath(activePath *ActivePath) HtmxRoute {
|
|
h.ActivePath = activePath
|
|
return h
|
|
}
|
|
|
|
func (h CommonHtmxRoute) AddWithMethod(method string, server HtmxServer) {
|
|
for language, path := range h.Paths {
|
|
server.GetHttpServer().HandleFunc(method+" "+addLanguageToPath(path, language), func(res http.ResponseWriter, req *http.Request) {
|
|
context := NewHtmxContext(req, res, language)
|
|
context.SetPathTemplate(addLanguageToPath(path, language))
|
|
context.SetAllRoutes(server.GetAllRoutes())
|
|
if h.ActivePath != nil {
|
|
context.SetActivePath(h.ActivePath)
|
|
}
|
|
h.RouteFunc(context)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (h CommonHtmxRoute) Add(server HtmxServer) {
|
|
server.Add(h)
|
|
h.AddWithMethod(h.Method, server)
|
|
}
|
|
|
|
func (h CommonHtmxRoute) WithPathParams(params ...HtmxPathParam) HtmxRoute {
|
|
h.PathParams = params
|
|
return h
|
|
}
|
|
|
|
func (h CommonHtmxRoute) ToUrl(language Language, queryParams ...HtmxPathParam) string {
|
|
path := addLanguageToPath(h.Paths[language], language)
|
|
if len(h.PathParams) == 0 {
|
|
return path
|
|
}
|
|
for _, param := range h.PathParams {
|
|
path = strings.ReplaceAll(path, "{"+param.Key+"}", param.Value)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func (h CommonHtmxRoute) extractPathParams(c HtmxContext) []HtmxPathParam {
|
|
langPath := addLanguageToPath(h.Paths[c.GetLanguage()], c.GetLanguage())
|
|
|
|
// Extract all placeholders of the form {variable} from the language-specific path template
|
|
params := make([]HtmxPathParam, 0)
|
|
seen := make(map[string]bool)
|
|
|
|
for {
|
|
start := strings.Index(langPath, "{")
|
|
if start == -1 {
|
|
break
|
|
}
|
|
end := strings.Index(langPath[start+1:], "}")
|
|
if end == -1 {
|
|
break
|
|
}
|
|
end = start + 1 + end
|
|
|
|
key := strings.TrimSpace(langPath[start+1 : end])
|
|
if key != "" && !seen[key] {
|
|
seen[key] = true
|
|
value := c.GetPathParam(key)
|
|
params = append(params, HtmxPathParam{Key: key, Value: value})
|
|
}
|
|
|
|
// Continue searching after current closing brace
|
|
if end+1 >= len(langPath) {
|
|
break
|
|
}
|
|
langPath = langPath[end+1:]
|
|
}
|
|
|
|
return params
|
|
}
|
|
|
|
type HtmxHttpMethodFunction func(url string) gomponents.Node
|
|
|
|
func (h CommonHtmxRoute) GetHtmxFromMethod(method HtmxHttpMethodFunction, language Language, queryParams ...HtmxPathParam) gomponents.Node {
|
|
return method(h.ToUrl(language, queryParams...))
|
|
}
|
|
|
|
func (h CommonHtmxRoute) GetHtmx(language Language, queryParams ...HtmxPathParam) gomponents.Node {
|
|
return h.GetHtmxFromMethod(h.HtmxMethod, language, queryParams...)
|
|
}
|
|
|
|
func NewHtmxGetRoute(routeFunc HtmxRouteFunc, paths LanguagePaths) HtmxRoute {
|
|
return NewCommonHtmxRoute(routeFunc, paths, "GET", htmx.Get)
|
|
}
|
|
|
|
func NewHtmxPostRoute(routeFunc HtmxRouteFunc, paths LanguagePaths) HtmxRoute {
|
|
return NewCommonHtmxRoute(routeFunc, paths, "POST", htmx.Post)
|
|
}
|
|
|
|
type HtmxServer interface {
|
|
Add(route HtmxRoute)
|
|
GetAllRoutes() []HtmxRoute
|
|
GetHttpServer() *http.ServeMux
|
|
HandleStaticAndDefaultPath(defaultRoute HtmxRoute, defaultLanguage Language)
|
|
}
|
|
|
|
type HtmxServerImpl struct {
|
|
routes []HtmxRoute
|
|
server *http.ServeMux
|
|
}
|
|
|
|
func (h *HtmxServerImpl) GetAllRoutes() []HtmxRoute {
|
|
return h.routes
|
|
}
|
|
|
|
func NewHtmxServer() *HtmxServerImpl {
|
|
return &HtmxServerImpl{server: http.NewServeMux()}
|
|
}
|
|
|
|
func (h *HtmxServerImpl) Add(route HtmxRoute) {
|
|
h.routes = append(h.routes, route)
|
|
}
|
|
|
|
func (h *HtmxServerImpl) GetHttpServer() *http.ServeMux {
|
|
return h.server
|
|
}
|
|
|
|
func (h *HtmxServerImpl) HandleStaticAndDefaultPath(defaultRoute HtmxRoute, defaultLanguage Language) {
|
|
staticHandler := http.StripPrefix("/static", http.FileServer(http.Dir("./static")))
|
|
h.server.Handle("GET /static", staticHandler)
|
|
h.server.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
|
|
path := r.URL.Path
|
|
if strings.HasPrefix(path, "/static") {
|
|
staticHandler.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if path == "/" {
|
|
defaultRoute.Execute(NewHtmxContext(r, w, defaultLanguage))
|
|
return
|
|
}
|
|
http.Error(w, "Not found", http.StatusNotFound)
|
|
})
|
|
}
|