diff --git a/.idea/dataSources.local.xml b/.idea/dataSources.local.xml index 5ccd4f5..6f95933 100644 --- a/.idea/dataSources.local.xml +++ b/.idea/dataSources.local.xml @@ -1,6 +1,6 @@ - + " diff --git a/go.mod b/go.mod index 16d020d..b14e268 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,12 @@ module mhtml go 1.20 require ( + github.com/dave/jennifer v1.7.1 github.com/fsnotify/fsnotify v1.7.0 github.com/gofiber/fiber/v2 v2.52.4 github.com/google/uuid v1.6.0 github.com/redis/go-redis/v9 v9.0.5 + golang.org/x/net v0.21.0 golang.org/x/tools v0.4.0 ) diff --git a/go.sum b/go.sum index e7cff45..a51937e 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= +github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -31,6 +33,8 @@ github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7g github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= diff --git a/h/base.go b/h/base.go index e5f4db2..1d2ccaf 100644 --- a/h/base.go +++ b/h/base.go @@ -38,6 +38,10 @@ func NewPageWithHttpMethod(httpMethod string, root *Node) *Page { } } +func WrapPartial(ctx *fiber.Ctx, cb func(ctx *fiber.Ctx) *Partial) *Node { + return cb(ctx).Root +} + func NewPartialWithHeaders(headers *Headers, root *Node) *Partial { return &Partial{ Headers: headers, diff --git a/h/livereload.go b/h/livereload.go index c029ed1..7deca76 100644 --- a/h/livereload.go +++ b/h/livereload.go @@ -25,7 +25,7 @@ func LiveReloadHandler(c *fiber.Ctx) error { } func LiveReload() *Node { - return Div(Get("/livereload"), Trigger("every 2s")) + return Div(Get("/livereload"), Trigger("every 200ms")) } func AddLiveReloadHandler(path string, app *fiber.App) { diff --git a/main.go b/main.go index badb0c3..8ca7be5 100644 --- a/main.go +++ b/main.go @@ -4,11 +4,9 @@ import ( "github.com/gofiber/fiber/v2" "github.com/google/uuid" "log" - "mhtml/database" "mhtml/h" "mhtml/pages" "mhtml/partials" - "mhtml/partials/sheet" "time" ) @@ -42,7 +40,7 @@ func main() { return err }) - f.Get("/mhtml/partials*", func(ctx *fiber.Ctx) error { + f.All("/mhtml/partials*", func(ctx *fiber.Ctx) error { partial := partials.GetPartialFromContext(ctx) if partial == nil { return ctx.SendStatus(404) @@ -51,24 +49,6 @@ func main() { }) pages.RegisterPages(f) - - f.Post("/api/patients", func(ctx *fiber.Ctx) error { - name := ctx.FormValue("name") - reason := ctx.FormValue("reason-for-visit") - location := ctx.FormValue("location-name") - - database.HSet("patients", uuid.New().String(), partials.Patient{ - Name: name, - ReasonForVisit: reason, - AppointmentDate: time.Now(), - LocationName: location, - }) - - return h.PartialViewWithHeaders(ctx, &map[string]string{ - "HX-Trigger": "patient-added", - }, sheet.Close(ctx)) - }) - h.Start(f, h.App{ LiveReload: true, }) diff --git a/pages/patients.index.go b/pages/patients.index.go index 0b5fc65..7a48920 100644 --- a/pages/patients.index.go +++ b/pages/patients.index.go @@ -4,7 +4,7 @@ import ( "github.com/gofiber/fiber/v2" "mhtml/h" "mhtml/pages/base" - "mhtml/partials" + "mhtml/partials/patient" ) func PatientsIndex(ctx *fiber.Ctx) *h.Page { @@ -15,9 +15,9 @@ func PatientsIndex(ctx *fiber.Ctx) *h.Page { h.Div( h.Class("flex justify-between items-center"), h.P("Manage Patients", h.Class("text-lg font-bold")), - partials.AddPatientButton(), + patient.AddPatientButton(), ), - h.ViewWithTriggers(partials.PatientList, "load", "patient-added from:body"), + h.ViewWithTriggers(patient.List, "load", "patient-added from:body"), ), ), )) diff --git a/partials/generated.go b/partials/generated.go index da8c4ba..18772d8 100644 --- a/partials/generated.go +++ b/partials/generated.go @@ -3,6 +3,7 @@ package partials import "mhtml/h" import "github.com/gofiber/fiber/v2" +import "mhtml/partials/patient" import "mhtml/partials/sheet" func GetPartialFromContext(ctx *fiber.Ctx) *h.Partial { @@ -13,11 +14,14 @@ func GetPartialFromContext(ctx *fiber.Ctx) *h.Partial { if path == "NewsSheetOpenCount" || path == "/mhtml/partials.NewsSheetOpenCount" { return NewsSheetOpenCount(ctx) } - if path == "PatientList" || path == "/mhtml/partials.PatientList" { - return PatientList(ctx) + if path == "Create" || path == "/mhtml/partials/patient.Create" { + return patient.Create(ctx) } - if path == "AddPatientSheet" || path == "/mhtml/partials.AddPatientSheet" { - return AddPatientSheet(ctx) + if path == "List" || path == "/mhtml/partials/patient.List" { + return patient.List(ctx) + } + if path == "AddPatientSheet" || path == "/mhtml/partials/patient.AddPatientSheet" { + return patient.AddPatientSheet(ctx) } if path == "Close" || path == "/mhtml/partials/sheet.Close" { return sheet.Close(ctx) diff --git a/partials/nav.go b/partials/nav.go index c08dfd5..1c52e50 100644 --- a/partials/nav.go +++ b/partials/nav.go @@ -15,8 +15,10 @@ func NavBar() *h.Node { {"Patients", "/patients"}, } - return h.Nav(h.Class("flex gap-4 items-center p-4 text-slate-600"), h.Children(h.Map(links, func(link Link) *h.Node { - return h.A(link.Name, h.Href(link.Path), h.Class("cursor-pointer hover:text-blue-400")) - }), - )) + return h.Nav(h.Class("flex gap-4 items-center p-4 text-slate-600"), + h.Children( + h.Map(links, func(link Link) *h.Node { + return h.A(link.Name, h.Href(link.Path), h.Class("cursor-pointer hover:text-blue-400")) + }), + )) } diff --git a/partials/patient/create.go b/partials/patient/create.go new file mode 100644 index 0000000..b2f0884 --- /dev/null +++ b/partials/patient/create.go @@ -0,0 +1,29 @@ +package patient + +import ( + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" + "mhtml/database" + "mhtml/h" + "mhtml/partials/sheet" + "time" +) + +func Create(ctx *fiber.Ctx) *h.Partial { + name := ctx.FormValue("name") + reason := ctx.FormValue("reason-for-visit") + location := ctx.FormValue("location-name") + + database.HSet("patients", uuid.New().String(), Patient{ + Name: name, + ReasonForVisit: reason, + AppointmentDate: time.Now(), + LocationName: location, + }) + + headers := &map[string]string{ + "HX-Trigger": "patient-added", + } + + return h.NewPartialWithHeaders(headers, h.WrapPartial(ctx, sheet.Close)) +} diff --git a/partials/patient.go b/partials/patient/patient.go similarity index 91% rename from partials/patient.go rename to partials/patient/patient.go index 0edb503..8c97910 100644 --- a/partials/patient.go +++ b/partials/patient/patient.go @@ -1,4 +1,4 @@ -package partials +package patient import ( "github.com/gofiber/fiber/v2" @@ -16,7 +16,7 @@ type Patient struct { LocationName string } -func PatientList(ctx *fiber.Ctx) *h.Partial { +func List(ctx *fiber.Ctx) *h.Partial { patients, err := database.HList[Patient]("patients") if err != nil { @@ -36,7 +36,7 @@ func PatientList(ctx *fiber.Ctx) *h.Partial { return h.NewPartial(h.Div( h.Class("mt-8"), h.Id("patient-list"), - h.List(patients, PatientRow), + h.List(patients, Row), )) } @@ -54,7 +54,7 @@ func AddPatientSheet(ctx *fiber.Ctx) *h.Partial { func addPatientForm() *h.Node { return h.Form( - h.Post("/api/patients"), + h.Post(h.GetPartialPath(Create)), h.Class("flex flex-col gap-2"), ui.Input(ui.InputProps{ Type: "text", @@ -84,7 +84,7 @@ func addPatientForm() *h.Node { ) } -func PatientRow(patient *Patient, index int) *h.Node { +func Row(patient *Patient, index int) *h.Node { return h.Div( h.Class("flex flex-col gap-2 rounded p-4", h.Ternary(index%2 == 0, "bg-red-100", "")), h.Pf("Name: %s", patient.Name), diff --git a/sandbox.go b/sandbox.go new file mode 100644 index 0000000..2ebefa7 --- /dev/null +++ b/sandbox.go @@ -0,0 +1,55 @@ +package main + +import "mhtml/h" + +func init() { + h.Div( + h.Id("sandbox"), + h.Button( + h.Id("btn"), + h.Class("bg-blue-500 text-white p-2"), + ), + ) + + h.Html( + h.Head(), + h.Body( + h.Nav( + h.Class("flex gap-4 items-center p-4 text-slate-600 "), + h.A(h.Href("/"), h.Class("cursor-pointer hover:text-blue-400 ")), + h.A(h.Class("cursor-pointer hover:text-blue-400 "), + h.Href("/news")), h.A( + h.Href("/patients"), + h.Class("cursor-pointer hover:text-blue-400 "))), + h.Div( + h.Id("active-modal")), + h.Div(h.Class("flex flex-col gap-2 bg-white h-full "), + h.Div(h.Class("flex flex-col p-4 w-full "), + h.Div(h.Div(h.Class("flex justify-between items-center "), + h.P( + h.Class("text-lg font-bold ")), + h.Button(h.HxTarget("#active-modal"), + h.Type("button"), + h.Id("add-patient"), + h.Class("flex gap-1 items-center border p-4 rounded cursor-hover bg-blue-700 text-white rounded p-2 h-12 "), + h.HxGet("mhtml/partials/patient.AddPatientSheet"))), + h.Div(h.HxGet("mhtml/partials/patient.List"), + h.HxTrigger("load, patient-added from:body"), + h.Class(""), h.Div(h.Class("mt-8"), + h.Id("patient-list"), + h.Div(h.Class("flex flex-col gap-2 rounded p-4 bg-red-100 "), + h.P(), + h.P()), + ), + ), + ), + ), + ), + h.Div( + h.HxGet("/livereload"), + h.HxTrigger("every 200ms"), + h.Class(""), + ), + )) + +} diff --git a/tooling/htmltogo/entry.go b/tooling/htmltogo/entry.go new file mode 100644 index 0000000..8285055 --- /dev/null +++ b/tooling/htmltogo/entry.go @@ -0,0 +1,119 @@ +package main + +import ( + "bytes" + "fmt" + "log" + "strings" + + "github.com/dave/jennifer/jen" + "golang.org/x/net/html" +) + +func main() { + // Example HTML input + htmlData := ` +

Manage Patients

Name: Sydne

Reason for visit: arm hurts

+ ` + + // Parse the HTML + doc, err := html.Parse(bytes.NewReader([]byte(htmlData))) + if err != nil { + log.Fatal(err) + } + + // Create a new Jennifer file + f := jen.NewFile("main") + + // Generate Jennifer code for the parsed HTML tree + generatedCode := processNode(doc.FirstChild) + + // Add the generated code to the file + f.Func().Id("Render").Params().Block(generatedCode...) + + // Render the generated code + var buf bytes.Buffer + err = f.Render(&buf) + if err != nil { + log.Fatal(err) + } + + //// Format the generated code + //formattedCode, err := format.Source(buf.Bytes()) + //if err != nil { + // log.Fatal(err) + //} + + // Output the formatted code + fmt.Println(string(buf.Bytes())) +} + +// Recursively process the HTML nodes and generate Jennifer code +func processNode(n *html.Node) []jen.Code { + var code []jen.Code + + // Only process element nodes + if n.Type == html.ElementNode { + // Create a dynamic method call based on the tag name + tagMethod := strings.Title(n.Data) // Capitalize the first letter of the tag + + // Add dynamic method call for the tag (e.g., h.Div(), h.Button(), etc.) + code = append(code, jen.Id("h").Dot(tagMethod).Call(mergeArgs(n)...)) + } + + return code +} + +// Merge attributes and children into a single slice for Call() +func mergeArgs(n *html.Node) []jen.Code { + // Process attributes + attrs := processAttributes(n.Attr) + + // Process children + children := processChildren(n) + + // Combine attributes and children into one slice + return append(attrs, children...) +} + +// Process child nodes of a given HTML node +func processChildren(n *html.Node) []jen.Code { + var children []jen.Code + + for c := n.FirstChild; c != nil; c = c.NextSibling { + children = append(children, processNode(c)...) + } + + return children +} + +func FormatFieldName(name string) string { + split := strings.Split(name, "_") + if strings.Contains(name, "-") { + split = strings.Split(name, "-") + } + parts := make([]string, 0) + for _, s := range split { + parts = append(parts, PascalCase(s)) + } + return strings.Join(parts, "") +} + +func PascalCase(s string) string { + if s == "" { + return s + } + // Convert the first rune (character) to uppercase and concatenate with the rest of the string + return strings.ToUpper(string(s[0])) + s[1:] +} + +// Process the attributes of an HTML node and return Jennifer code +func processAttributes(attrs []html.Attribute) []jen.Code { + var args []jen.Code + for _, attr := range attrs { + // Dynamically handle all attributes + attrMethod := FormatFieldName(attr.Key) // E.g., convert "data-role" to "DataRole" + args = append(args, jen.Id("h").Dot(attrMethod).Call(jen.Lit(attr.Val))) + } + return args +}