mirror of
https://github.com/acaloiaro/garbagespeak.com
synced 2026-07-21 18:29:08 +00:00
Commit grave sins for partial/full page retrieval
This commit is contained in:
parent
1fd47cd8a7
commit
863e850766
5 changed files with 154 additions and 14 deletions
1
go.mod
1
go.mod
|
|
@ -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
1
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.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
124
html_parser/parser.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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 }}
|
||||||
|
|
|
||||||
41
server.go
41
server.go
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue