all repos — nergen.net-guestbook.git @ 2bde104308e1e08ef4dbe18d462a959e2b39e41f

Unnamed repository; edit this file 'description' to name the repository.

initial commit
nergen pusher@nergen.net
Thu, 11 Apr 2024 15:56:38 +0200
commit

2bde104308e1e08ef4dbe18d462a959e2b39e41f

A .air.toml

@@ -0,0 +1,46 @@

+root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go", "static.html"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html", "gohtml"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true
A .gitignore

@@ -0,0 +1,7 @@

+tmp/ +sqlc_gb/*.go +go.sum +gb.db +notes.md +nergen.net-guestbook +push.bash
A go.mod

@@ -0,0 +1,15 @@

+module git.nergen.net/nergen.net-guestbook + +go 1.22.2 + +require ( + github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 + github.com/mattn/go-sqlite3 v1.14.22 + github.com/microcosm-cc/bluemonday v1.0.26 +) + +require ( + github.com/aymerick/douceur v0.2.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + golang.org/x/net v0.24.0 // indirect +)
A main.go

@@ -0,0 +1,132 @@

+package main + +import ( + "context" + "crypto" + "database/sql" + "fmt" + "html/template" + "log" + "net/http" + "reflect" + "strings" + "time" + + "git.nergen.net/nergen.net-guestbook/sqlc_gb" + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" + _ "github.com/mattn/go-sqlite3" + "github.com/microcosm-cc/bluemonday" +) + +var templates map[string]*template.Template +var ctx context.Context +var queries *sqlc_gb.Queries +var p *bluemonday.Policy + +func init() { + + if templates == nil { + templates = make(map[string]*template.Template) + } + + // Do this once for each unique policy, and use the policy for the life of the program + // Policy creation/editing is not safe to use in multiple goroutines + // p = bluemonday.UGCPolicy() + // The policy can then be used to sanitize lots of input and it is safe to use the policy + // in multiple goroutines + + p = bluemonday.NewPolicy() + p.AllowElements("b", "code", "strong", "p", "blockquote", "pre", "br", "dt", "dl", "dd", "h1", "h2", "h3", "h4", "em", "sup", "div", "ol", "ul", "li") + + templates["index.html"] = template.Must(template.New("boilerplate.gohtml").Funcs(template.FuncMap{ + "safeHTML": func(b string) template.HTML { + return template.HTML(b) + }, + }).ParseFiles("templates/guestbook-body.gohtml", "templates/boilerplate.gohtml")) +} + +func main() { + + h1 := func(w http.ResponseWriter, r *http.Request) { + posts, err := queries.GetPosts(ctx) + + if err != nil { + log.Fatal(err) + } + + templates["index.html"].Execute(w, posts) + } + + h2 := func(w http.ResponseWriter, r *http.Request) { + + msg_markdown := r.PostFormValue("msg") + msg_html := string(mdToHTML([]byte(msg_markdown))) + msg_sanitized_html := p.Sanitize(msg_html) + + if msg_html != msg_sanitized_html { + fmt.Println("message was sanitized") + } + + if strings.TrimSpace(msg_sanitized_html) == "<p></p>" { + fmt.Println("message turned out empty") + return + } + + post, err := queries.AddPost(ctx, sqlc_gb.AddPostParams{ + Msg: msg_sanitized_html, + // Msg: msg_html, + Stamp: time.Now(), + }) + + if err != nil { + log.Fatal(err) + } + + templates["index.html"].ExecuteTemplate(w, "rendered-post", post) + } + + db, err := sql.Open("sqlite3", "gb.db") + + if err != nil { + log.Fatal(err) + } + + defer db.Close() + + ctx = context.Background() + queries = sqlc_gb.New(db) + + if err = queries.TableCreate(ctx); err != nil { + log.Fatal(err) + } + + http.HandleFunc("/", h1) + http.HandleFunc("/add-post/", h2) + + log.Fatal(http.ListenAndServe(":8000", nil)) +} + +func mdToHTML(md []byte) []byte { + // create markdown parser with extensions + extensions := parser.CommonExtensions | parser.NoEmptyLineBeforeBlock | parser.HardLineBreak | parser.Footnotes + p := parser.NewWithExtensions(extensions) + doc := p.Parse(md) + + // create HTML renderer with extensions + htmlFlags := html.CommonFlags | html.HrefTargetBlank + opts := html.RendererOptions{Flags: htmlFlags} + renderer := html.NewRenderer(opts) + + return markdown.Render(doc, renderer) +} + +func Hash(objs ...interface{}) string { + digester := crypto.MD5.New() + for _, ob := range objs { + fmt.Fprint(digester, reflect.TypeOf(ob)) + fmt.Fprint(digester, ob) + } + return fmt.Sprintf("%x", digester.Sum(nil)) +}
A readme.md

@@ -0,0 +1,1 @@

+execute `sqlc generate` to generate SQL files
A sqlc.json

@@ -0,0 +1,14 @@

+{ + "version": "2", + "sql": [{ + "engine": "sqlite", + "queries": "sqlc_gb/query.sql", + "schema": "sqlc_gb/schema.sql", + "gen": { + "go": { + "package": "sqlc_gb", + "out": "sqlc_gb" + } + } + }] +}
A sqlc_gb/query.sql

@@ -0,0 +1,37 @@

+-- name: TableCreate :exec +CREATE TABLE IF NOT EXISTS gb ( + id INTEGER NOT NULL PRIMARY KEY, + msg TEXT NOT NULL, + stamp DATETIME NOT NULL, + flags INTEGER NOT NULL DEFAULT 0 +); + +-- name: GetPost :one +SELECT * FROM gb +WHERE id = ? LIMIT 1; + +-- name: GetPosts :many +SELECT * FROM gb +ORDER BY stamp DESC; + +-- name: AddPost :one +INSERT INTO gb ( + msg, stamp, flags +) VALUES ( + ?, ?, ? +) +RETURNING *; + +-- name: UpdateMsg :exec +UPDATE gb +set msg = ? +WHERE id = ?; + +-- name: UpdateFlags :exec +UPDATE gb +set flags = ? +WHERE id = ?; + +-- name: DeleteFilm :exec +DELETE FROM gb +WHERE id = ?;
A sqlc_gb/schema.sql

@@ -0,0 +1,6 @@

+CREATE TABLE IF NOT EXISTS gb ( + id INTEGER NOT NULL PRIMARY KEY, + msg TEXT NOT NULL, + stamp DATETIME NOT NULL, + flags INTEGER NOT NULL DEFAULT 0 +);
A templates/boilerplate.gohtml

@@ -0,0 +1,25 @@

+<!DOCTYPE html> +<head> + <meta charset="utf-8"> + <title>Guestbook</title> + <script src="https://unpkg.com/htmx.org@1.9.11" integrity="sha384-0gxUXCCR8yv9FM2b+U3FDbsKthCI66oH5IA9fHppQq9DDMHuMauqq1ZHBpJxQ0J0" crossorigin="anonymous"></script> +</head> +<body> + <form + hx-post="add-post/" + hx-target="#rendered-posts" + hx-swap="afterbegin" + hx-indicator="#spinner" + hx-on::after-request="if(event.detail.successful) this.reset()" + > + <div> + <textarea rows="4" cols="50" name="msg" id="post-msg" required ></textarea> + </div> + + <button type="submit"> + <span id="spinner" class="htmx-indicator">br..</span>post + </button> + </form> + + {{ template "guestbookbody" . }} +</body>
A templates/guestbook-body.gohtml

@@ -0,0 +1,15 @@

+{{ define "guestbookbody" }} + <section id="rendered-posts"> + {{ range . }} + {{ block "rendered-post" .}} + <div class="rendered-post"> + <div class="rendered-post__date">{{ .Stamp.Format "Jan 02, 2006" }}</div> + <div class="rendered-post__id"><a href="#">#{{ .ID }}</a></div> + + <div class=rendered-post__content>{{ .Msg | safeHTML }}</div> + </div> + {{ end }} + {{ end }} + </section> +{{ end }} +