Commit grave sins for partial/full page retrieval

This commit is contained in:
Adriano Caloiaro 2023-07-30 12:03:39 -06:00
parent 1fd47cd8a7
commit 863e850766
No known key found for this signature in database
GPG key ID: C2BC56DE73CE3F75
5 changed files with 154 additions and 14 deletions

1
go.mod
View file

@ -14,6 +14,7 @@ require (
github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2 github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2
github.com/jackc/pgx/v5 v5.4.2 github.com/jackc/pgx/v5 v5.4.2
golang.org/x/crypto v0.9.0 golang.org/x/crypto v0.9.0
golang.org/x/net v0.10.0
) )
require ( require (

1
go.sum
View file

@ -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.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.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 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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

124
html_parser/parser.go Normal file
View file

@ -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 <html><head></head><body><our node/></body></html>
// 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
}

View file

@ -14,6 +14,7 @@
<a href="#" <a href="#"
hx-get="{{ $.ApiBaseUrl }}/garbage/{{ .ID }}" hx-get="{{ $.ApiBaseUrl }}/garbage/{{ .ID }}"
hx-push-url="{{ $.ApiBaseUrl }}/garbage/{{ .ID }}"
hx-target="#content">Link</a> hx-target="#content">Link</a>
{{ if eq .OwnerID.String $.UserID }} {{ if eq .OwnerID.String $.UserID }}

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"context" "context"
"crypto/tls" "crypto/tls"
"embed" "embed"
@ -16,6 +17,7 @@ import (
"text/template" "text/template"
"time" "time"
"github.com/acaloiaro/garbage_speak/html_parser"
"github.com/acaloiaro/neoq" "github.com/acaloiaro/neoq"
"github.com/acaloiaro/neoq/backends/postgres" "github.com/acaloiaro/neoq/backends/postgres"
"github.com/acaloiaro/neoq/handler" "github.com/acaloiaro/neoq/handler"
@ -70,13 +72,13 @@ type UserEmailVerification struct {
} }
//go:embed migrations/*.sql //go:embed migrations/*.sql
var migrations embed.FS var migrationsFS embed.FS
//go:embed all:public //go:embed all:public
var content embed.FS var publicFS embed.FS
//go:embed all:partials //go:embed all:partials
var partials embed.FS var partialsFS embed.FS
var db *pgxpool.Pool var db *pgxpool.Pool
var sessions *scs.SessionManager var sessions *scs.SessionManager
@ -85,7 +87,7 @@ var NQ types.Backend
// run migrations, acquire a database connection pool, and create the session store // run migrations, acquire a database connection pool, and create the session store
func init() { func init() {
d, err := iofs.New(migrations, "migrations") d, err := iofs.New(migrationsFS, "migrations")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -170,7 +172,7 @@ func main() {
defer sessionStore.StopCleanup() defer sessionStore.StopCleanup()
defer NQ.Shutdown(context.Background()) defer NQ.Shutdown(context.Background())
serverRoot, _ := fs.Sub(content, "public") serverRoot, _ := fs.Sub(publicFS, "public")
r := chi.NewRouter() r := chi.NewRouter()
r.Use(sessions.LoadAndSave) r.Use(sessions.LoadAndSave)
@ -311,7 +313,7 @@ func editGarbageHandler(w http.ResponseWriter, r *http.Request) {
"SelectedTags": selectedTags, "SelectedTags": selectedTags,
"AvailableTags": availableTags, "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) err = tmpl.ExecuteTemplate(w, "edit.html", tmplVars)
if err != nil { if err != nil {
ise(err, w) 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 // newUserValidationHandler checks whether a username is available during account creation
func newUserValidationHandler(w http.ResponseWriter, r *http.Request) { func newUserValidationHandler(w http.ResponseWriter, r *http.Request) {
tmplVars := map[string]any{"ApiBaseUrl": apiURL()} 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 errCnt := 0
@ -410,7 +412,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
return 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{ err = tmpl.ExecuteTemplate(w, "login.html", map[string]any{
"ApiBaseURL": apiURL(), "ApiBaseURL": apiURL(),
"LoginError": "Incorrect username or password", "LoginError": "Incorrect username or password",
@ -437,7 +439,7 @@ func navUserItems(w http.ResponseWriter, r *http.Request) {
var tmpl *template.Template var tmpl *template.Template
var err error var err error
tmpl = template.Must(template.ParseFS(partials, "partials/nav/*.html")) tmpl = template.Must(template.ParseFS(partialsFS, "partials/nav/*.html"))
if isLoggedIn(r) { if isLoggedIn(r) {
err = tmpl.ExecuteTemplate(w, "user_nav_items.html", map[string]any{"ApiURL": apiURL()}) err = tmpl.ExecuteTemplate(w, "user_nav_items.html", map[string]any{"ApiURL": apiURL()})
} else { } else {
@ -455,7 +457,7 @@ func navUserItems(w http.ResponseWriter, r *http.Request) {
// creatAccountPageHandler serves the new account form // creatAccountPageHandler serves the new account form
func creatAccountPageHandler(w http.ResponseWriter, r *http.Request) { 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{}) err := tmpl.ExecuteTemplate(w, "create.html", map[string]any{})
if err != nil { if err != nil {
ise(err, w) ise(err, w)
@ -619,7 +621,7 @@ func createAccountHandler(w http.ResponseWriter, r *http.Request) {
return 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}) err = tmpl.ExecuteTemplate(w, "created.html", map[string]any{"Email": email})
if err != nil { if err != nil {
ise(err, w) ise(err, w)
@ -647,7 +649,7 @@ func listGarbageHandler(w http.ResponseWriter, r *http.Request) {
return 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{ err = tmpl.ExecuteTemplate(w, "list.html", map[string]any{
"Posts": garbage, "Posts": garbage,
"ApiBaseUrl": apiURL(), "ApiBaseUrl": apiURL(),
@ -664,6 +666,7 @@ func listGarbageHandler(w http.ResponseWriter, r *http.Request) {
// showGarbageHandler returns the latest garbage // showGarbageHandler returns the latest garbage
func showGarbageHandler(w http.ResponseWriter, r *http.Request) { func showGarbageHandler(w http.ResponseWriter, r *http.Request) {
isPartialRequest := r.Header.Get("hx-request") != ""
garbageID := chi.URLParam(r, "garbage_id") garbageID := chi.URLParam(r, "garbage_id")
userID := sessions.GetString(r.Context(), "userID") userID := sessions.GetString(r.Context(), "userID")
ctx := context.Background() ctx := context.Background()
@ -682,8 +685,9 @@ func showGarbageHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
tmpl := template.Must(template.ParseFS(partials, "partials/garbage/show.html")) var buff = bytes.NewBufferString("")
err = tmpl.ExecuteTemplate(w, "show.html", map[string]any{ tmpl := template.Must(template.ParseFS(partialsFS, "partials/garbage/show.html"))
err = tmpl.ExecuteTemplate(buff, "show.html", map[string]any{
"Garbage": garbage, "Garbage": garbage,
"ApiBaseUrl": apiURL(), "ApiBaseUrl": apiURL(),
"LoggedIn": isLoggedIn(r), "LoggedIn": isLoggedIn(r),
@ -694,6 +698,15 @@ func showGarbageHandler(w http.ResponseWriter, r *http.Request) {
return 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) w.WriteHeader(http.StatusOK)
} }