From 863e850766cea234ed3317c167662a0d065ecf53 Mon Sep 17 00:00:00 2001 From: Adriano Caloiaro Date: Sun, 30 Jul 2023 12:03:39 -0600 Subject: [PATCH] Commit grave sins for partial/full page retrieval --- go.mod | 1 + go.sum | 1 + html_parser/parser.go | 124 +++++++++++++++++++++++++++++++++++++ partials/garbage/list.html | 1 + server.go | 41 +++++++----- 5 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 html_parser/parser.go diff --git a/go.mod b/go.mod index 6c76d3b..800c72c 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2 github.com/jackc/pgx/v5 v5.4.2 golang.org/x/crypto v0.9.0 + golang.org/x/net v0.10.0 ) require ( diff --git a/go.sum b/go.sum index 23db9f7..7bd8652 100644 --- a/go.sum +++ b/go.sum @@ -120,6 +120,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/html_parser/parser.go b/html_parser/parser.go new file mode 100644 index 0000000..86df19b --- /dev/null +++ b/html_parser/parser.go @@ -0,0 +1,124 @@ +package html_parser + +import ( + "bytes" + "io" + "log" + "strings" + + "golang.org/x/net/html" +) + +func htmlGetAttr(n *html.Node, key string) (string, bool) { + for _, attr := range n.Attr { + if attr.Key == key { + return attr.Val, true + } + } + return "", false +} + +// htmlRenderNode renders a node, excluding its children +func htmlRenderNode(n *html.Node) string { + var buf bytes.Buffer + w := io.Writer(&buf) + + // walk backwards through the document tree to find the root + for { + if n.Parent != nil { + n = n.Parent + continue + } + break + } + + err := html.Render(w, n) + if err != nil { + return "" + } + + return buf.String() +} + +func htmlHasID(n *html.Node, id string) bool { + if n.Type == html.ElementNode { + s, ok := htmlGetAttr(n, "id") + if ok && s == id { + return true + } + } + + return false +} + +func htmlWalkTree(n *html.Node, id string) *html.Node { + if htmlHasID(n, id) { + return n + } + + for c := n.FirstChild; c != nil; c = c.NextSibling { + res := htmlWalkTree(c, id) + if res != nil { + return res + } + } + + return nil +} + +func htmlGetByID(n *html.Node, id string) *html.Node { + return htmlWalkTree(n, id) +} + +// ParseAndSplice parses HTML within 'file' and splices `htmlContent` into its node tree at `id` +// +// Returns rendered HTML content as a string +func ParseAndSplice(file io.Reader, id, htmlContent string) (content string) { + doc, err := html.Parse(file) + if err != nil { + log.Fatal(err) + } + + log.Println("HERE", htmlContent) + doc = htmlGetByID(doc, id) + if doc == nil { + return + } + + // Get rid of the element's children, as we want to swap our own content in as this element's _only_ content + doc.FirstChild = nil + doc.LastChild = nil + + // Parse the content we're swapping in + // This creates a "well-formed" tree, meaning that it adds + // but we don't want a well-formed tree, we want our new content as a single Node. The tree parents will be + // removed + newHtml, err := html.Parse(strings.NewReader(htmlContent)) + if err != nil { + log.Fatal(err) + } + + // Remove the tree parents, as we do not want a "well-formed" tree + for { + if newHtml.LastChild == nil { + break + } + newHtml = newHtml.LastChild + } + + // We've walked all the way down the new tree to the lowermost node of our new HTML tree. + // Get its parent to get the outermost element of the new content (spliced in nodes should + // not consist of nested node) + newHtml = newHtml.Parent + + // "detach" the new node so it can be spliced into the tree + newHtml.Parent = nil + newHtml.PrevSibling = nil + newHtml.NextSibling = nil + + doc.AppendChild(newHtml) + + content = htmlRenderNode(doc) + + return +} diff --git a/partials/garbage/list.html b/partials/garbage/list.html index f045093..c2537ed 100644 --- a/partials/garbage/list.html +++ b/partials/garbage/list.html @@ -14,6 +14,7 @@ Link {{ if eq .OwnerID.String $.UserID }} diff --git a/server.go b/server.go index f5df000..8bfbe44 100644 --- a/server.go +++ b/server.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "crypto/tls" "embed" @@ -16,6 +17,7 @@ import ( "text/template" "time" + "github.com/acaloiaro/garbage_speak/html_parser" "github.com/acaloiaro/neoq" "github.com/acaloiaro/neoq/backends/postgres" "github.com/acaloiaro/neoq/handler" @@ -70,13 +72,13 @@ type UserEmailVerification struct { } //go:embed migrations/*.sql -var migrations embed.FS +var migrationsFS embed.FS //go:embed all:public -var content embed.FS +var publicFS embed.FS //go:embed all:partials -var partials embed.FS +var partialsFS embed.FS var db *pgxpool.Pool var sessions *scs.SessionManager @@ -85,7 +87,7 @@ var NQ types.Backend // run migrations, acquire a database connection pool, and create the session store func init() { - d, err := iofs.New(migrations, "migrations") + d, err := iofs.New(migrationsFS, "migrations") if err != nil { log.Fatal(err) } @@ -170,7 +172,7 @@ func main() { defer sessionStore.StopCleanup() defer NQ.Shutdown(context.Background()) - serverRoot, _ := fs.Sub(content, "public") + serverRoot, _ := fs.Sub(publicFS, "public") r := chi.NewRouter() r.Use(sessions.LoadAndSave) @@ -311,7 +313,7 @@ func editGarbageHandler(w http.ResponseWriter, r *http.Request) { "SelectedTags": selectedTags, "AvailableTags": availableTags, } - tmpl := template.Must(template.ParseFS(partials, "partials/garbage/edit.html")) + tmpl := template.Must(template.ParseFS(partialsFS, "partials/garbage/edit.html")) err = tmpl.ExecuteTemplate(w, "edit.html", tmplVars) if err != nil { ise(err, w) @@ -325,7 +327,7 @@ func editGarbageHandler(w http.ResponseWriter, r *http.Request) { // newUserValidationHandler checks whether a username is available during account creation func newUserValidationHandler(w http.ResponseWriter, r *http.Request) { tmplVars := map[string]any{"ApiBaseUrl": apiURL()} - tmpl := template.Must(template.ParseFS(partials, "partials/users/new_user_validation.html")) + tmpl := template.Must(template.ParseFS(partialsFS, "partials/users/new_user_validation.html")) errCnt := 0 @@ -410,7 +412,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { return } - tmpl := template.Must(template.ParseFS(partials, "partials/users/login.html")) + tmpl := template.Must(template.ParseFS(partialsFS, "partials/users/login.html")) err = tmpl.ExecuteTemplate(w, "login.html", map[string]any{ "ApiBaseURL": apiURL(), "LoginError": "Incorrect username or password", @@ -437,7 +439,7 @@ func navUserItems(w http.ResponseWriter, r *http.Request) { var tmpl *template.Template var err error - tmpl = template.Must(template.ParseFS(partials, "partials/nav/*.html")) + tmpl = template.Must(template.ParseFS(partialsFS, "partials/nav/*.html")) if isLoggedIn(r) { err = tmpl.ExecuteTemplate(w, "user_nav_items.html", map[string]any{"ApiURL": apiURL()}) } else { @@ -455,7 +457,7 @@ func navUserItems(w http.ResponseWriter, r *http.Request) { // creatAccountPageHandler serves the new account form func creatAccountPageHandler(w http.ResponseWriter, r *http.Request) { - tmpl := template.Must(template.ParseFS(partials, "partials/users/*")) + tmpl := template.Must(template.ParseFS(partialsFS, "partials/users/*")) err := tmpl.ExecuteTemplate(w, "create.html", map[string]any{}) if err != nil { ise(err, w) @@ -619,7 +621,7 @@ func createAccountHandler(w http.ResponseWriter, r *http.Request) { return } - tmpl := template.Must(template.ParseFS(partials, "partials/users/*")) + tmpl := template.Must(template.ParseFS(partialsFS, "partials/users/*")) err = tmpl.ExecuteTemplate(w, "created.html", map[string]any{"Email": email}) if err != nil { ise(err, w) @@ -647,7 +649,7 @@ func listGarbageHandler(w http.ResponseWriter, r *http.Request) { return } - tmpl := template.Must(template.ParseFS(partials, "partials/garbage/list.html")) + tmpl := template.Must(template.ParseFS(partialsFS, "partials/garbage/list.html")) err = tmpl.ExecuteTemplate(w, "list.html", map[string]any{ "Posts": garbage, "ApiBaseUrl": apiURL(), @@ -664,6 +666,7 @@ func listGarbageHandler(w http.ResponseWriter, r *http.Request) { // showGarbageHandler returns the latest garbage func showGarbageHandler(w http.ResponseWriter, r *http.Request) { + isPartialRequest := r.Header.Get("hx-request") != "" garbageID := chi.URLParam(r, "garbage_id") userID := sessions.GetString(r.Context(), "userID") ctx := context.Background() @@ -682,8 +685,9 @@ func showGarbageHandler(w http.ResponseWriter, r *http.Request) { return } - tmpl := template.Must(template.ParseFS(partials, "partials/garbage/show.html")) - err = tmpl.ExecuteTemplate(w, "show.html", map[string]any{ + var buff = bytes.NewBufferString("") + tmpl := template.Must(template.ParseFS(partialsFS, "partials/garbage/show.html")) + err = tmpl.ExecuteTemplate(buff, "show.html", map[string]any{ "Garbage": garbage, "ApiBaseUrl": apiURL(), "LoggedIn": isLoggedIn(r), @@ -694,6 +698,15 @@ func showGarbageHandler(w http.ResponseWriter, r *http.Request) { return } + if isPartialRequest { + w.Write(buff.Bytes()) + } else { + indexFile, _ := publicFS.Open("public/index.html") + content := html_parser.ParseAndSplice(indexFile, "content", buff.String()) + log.Println(content) + w.Write([]byte(content)) + } + w.WriteHeader(http.StatusOK) }