mirror of
https://github.com/acaloiaro/garbagespeak.com
synced 2026-07-21 10:12:19 +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/v5 v5.4.2
|
||||
golang.org/x/crypto v0.9.0
|
||||
golang.org/x/net v0.10.0
|
||||
)
|
||||
|
||||
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.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=
|
||||
|
|
|
|||
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="#"
|
||||
hx-get="{{ $.ApiBaseUrl }}/garbage/{{ .ID }}"
|
||||
hx-push-url="{{ $.ApiBaseUrl }}/garbage/{{ .ID }}"
|
||||
hx-target="#content">Link</a>
|
||||
|
||||
{{ if eq .OwnerID.String $.UserID }}
|
||||
|
|
|
|||
41
server.go
41
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue