From 1e1174f9ca4b0c4d2cda3060d40812b4c78f5bf7 Mon Sep 17 00:00:00 2001 From: Maximilian Friedersdorff Date: Sun, 26 Jan 2025 22:23:42 +0000 Subject: [PATCH] Initial commit with cru (no delete) of notes --- .gitignore | 1 + cmd/server/main.go | 19 +++++++++ conf.toml | 3 ++ go.mod | 7 ++++ go.sum | 4 ++ internal/conf/conf.go | 30 +++++++++++++++ internal/notes/notes.go | 52 +++++++++++++++++++++++++ internal/notes/views/views.go | 72 +++++++++++++++++++++++++++++++++++ internal/urls/urls.go | 47 +++++++++++++++++++++++ templates/edit.html | 6 +++ templates/view.html | 3 ++ 11 files changed, 244 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/server/main.go create mode 100644 conf.toml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/conf/conf.go create mode 100644 internal/notes/notes.go create mode 100644 internal/notes/views/views.go create mode 100644 internal/urls/urls.go create mode 100644 templates/edit.html create mode 100644 templates/view.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba2906d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +main diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..b07945d --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "log" + "net/http" + + "gitea.gwairfelin.com/max/gonotes/internal/conf" + "gitea.gwairfelin.com/max/gonotes/internal/notes/views" +) + +func main() { + conf.LoadConfig("./conf.toml") + + router := http.NewServeMux() + notesRouter := views.GetRoutes("/notes") + + router.Handle("/notes/", http.StripPrefix("/notes", notesRouter)) + log.Fatal(http.ListenAndServe(":8080", router)) +} diff --git a/conf.toml b/conf.toml new file mode 100644 index 0000000..488492c --- /dev/null +++ b/conf.toml @@ -0,0 +1,3 @@ +extension = "md" +notesdir = "saved_notes" +templatesdir = "templates" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d1334e1 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module gitea.gwairfelin.com/max/gonotes + +go 1.23.5 + +require github.com/yuin/goldmark v1.7.8 + +require github.com/pelletier/go-toml v1.9.5 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1449848 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= diff --git a/internal/conf/conf.go b/internal/conf/conf.go new file mode 100644 index 0000000..dce5fcf --- /dev/null +++ b/internal/conf/conf.go @@ -0,0 +1,30 @@ +package conf + +import ( + "log" + "os" + + "github.com/pelletier/go-toml" +) + +type Config struct { + Extension string + NotesDir string + TemplatesDir string +} + +var Conf Config + +func LoadConfig(path string) { + var err error + + b, err := os.ReadFile(path) + if err != nil { + log.Fatal(err) + } + + err = toml.Unmarshal([]byte(b), &Conf) + if err != nil { + log.Fatal(err) + } +} diff --git a/internal/notes/notes.go b/internal/notes/notes.go new file mode 100644 index 0000000..060d9f1 --- /dev/null +++ b/internal/notes/notes.go @@ -0,0 +1,52 @@ +// Notes implements a data structure for reasoning about and rendering +// markdown notes +package notes + +import ( + "bytes" + "fmt" + "log" + "os" + "path/filepath" + + "gitea.gwairfelin.com/max/gonotes/internal/conf" + "github.com/yuin/goldmark" +) + +// Note is the central data structure. It can be Saved, Rendered and Loaded +// using the Save, Render and LoadNote functions. +type Note struct { + Title string + BodyRendered string + Body []byte +} + +func fmtPath(path string) string { + return fmt.Sprintf("%s.%s", path, conf.Conf.Extension) +} + +// Save a note to a path derived from the title +func (n *Note) Save() error { + filename := filepath.Join(conf.Conf.NotesDir, fmtPath(n.Title)) + return os.WriteFile(filename, n.Body, 0600) +} + +// Render the markdown content of the note to HTML +func (n *Note) Render() { + var buf bytes.Buffer + err := goldmark.Convert(n.Body, &buf) + if err != nil { + log.Fatal(err) + } + n.BodyRendered = buf.String() +} + +// Load a note from the disk. The path is derived from the title +func LoadNote(title string) (*Note, error) { + filename := filepath.Join(conf.Conf.NotesDir, fmtPath(title)) + body, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + return &Note{Title: title, Body: body}, nil +} diff --git a/internal/notes/views/views.go b/internal/notes/views/views.go new file mode 100644 index 0000000..b52b7f5 --- /dev/null +++ b/internal/notes/views/views.go @@ -0,0 +1,72 @@ +package views + +import ( + "html/template" + "log" + "net/http" + "path/filepath" + + "gitea.gwairfelin.com/max/gonotes/internal/conf" + "gitea.gwairfelin.com/max/gonotes/internal/notes" + "gitea.gwairfelin.com/max/gonotes/internal/urls" +) + +var myurls urls.URLs + +func GetRoutes(prefix string) *http.ServeMux { + myurls = urls.URLs{ + Prefix: prefix, + URLs: map[string]urls.URL{ + "view": {Path: "/{note}/", Protocol: "GET", Handler: viewHandler}, + "edit": {Path: "/{note}/edit/", Protocol: "GET", Handler: editHandler}, + "save": {Path: "/{note}/edit/save/", Protocol: "POST", Handler: saveHandler}, + }, + } + return myurls.GetRouter() +} + +func viewHandler(w http.ResponseWriter, r *http.Request) { + title := r.PathValue("note") + note, err := notes.LoadNote(title) + if err != nil { + http.Redirect(w, r, myurls.Reverse("edit", map[string]string{"note": title}), http.StatusFound) + return + } + + note.Render() + err = renderTemplate(w, "view.html", note) + if err != nil { + log.Println(err) + http.Error(w, "Couldn't load template", http.StatusInternalServerError) + return + } +} + +func editHandler(w http.ResponseWriter, r *http.Request) { + title := r.PathValue("note") + note, err := notes.LoadNote(title) + if err != nil { + note = ¬es.Note{Title: title} + } + + err = renderTemplate(w, "edit.html", note) + if err != nil { + log.Println(err) + http.Error(w, "Couldn't load template", http.StatusInternalServerError) + return + } +} + +func saveHandler(w http.ResponseWriter, r *http.Request) { + title := r.PathValue("note") + body := r.FormValue("body") + note := ¬es.Note{Title: title, Body: []byte(body)} + note.Save() + http.Redirect(w, r, myurls.Reverse("view", map[string]string{"note": title}), http.StatusFound) +} + +func renderTemplate(w http.ResponseWriter, tmpl string, note *notes.Note) error { + t, err := template.ParseFiles(filepath.Join(conf.Conf.TemplatesDir, tmpl)) + t.Execute(w, note) + return err +} diff --git a/internal/urls/urls.go b/internal/urls/urls.go new file mode 100644 index 0000000..2e3f08a --- /dev/null +++ b/internal/urls/urls.go @@ -0,0 +1,47 @@ +// Package to manage routing of urls in net/http +// URLs type is the main thing of interest +package urls + +import ( + "fmt" + "net/http" + "regexp" +) + +// Represent a single URL with a name, a pattern and supported protocol +type URL struct { + Handler func(http.ResponseWriter, *http.Request) + Path string + Protocol string +} + +// Return the corresponding net/http pattern for a URL +func (url *URL) GetPattern() string { + return fmt.Sprintf("%s %s", url.Protocol, url.Path) +} + +// Represent a set of URLs at a prefix +type URLs struct { + URLs map[string]URL + Prefix string +} + +// Given a name and replacements, return a rendered path component of a URL +func (urls URLs) Reverse(name string, replacements map[string]string) string { + pattern := urls.URLs[name].Path + + for key, val := range replacements { + re := regexp.MustCompile("{" + key + "}") + pattern = re.ReplaceAllString(pattern, val) + } + return urls.Prefix + pattern +} + +// Return router for the urls +func (urls URLs) GetRouter() *http.ServeMux { + router := http.NewServeMux() + for _, url := range urls.URLs { + router.HandleFunc(url.GetPattern(), url.Handler) + } + return router +} diff --git a/templates/edit.html b/templates/edit.html new file mode 100644 index 0000000..80a1b49 --- /dev/null +++ b/templates/edit.html @@ -0,0 +1,6 @@ +

Editing {{.Title}}

+ +
+ + +
diff --git a/templates/view.html b/templates/view.html new file mode 100644 index 0000000..59e2cce --- /dev/null +++ b/templates/view.html @@ -0,0 +1,3 @@ +

{{.Title}}

+ +
{{printf "%s" .BodyRendered}}