From 74a0d9603c34ecb9e2f42637552e5630eedba162 Mon Sep 17 00:00:00 2001 From: Kristian Borgwarth <10348902@pm.me> Date: Wed, 25 Mar 2026 19:09:51 +0100 Subject: [PATCH 1/3] test(frontmatter): test simple frontmatter parser --- core/frontmatter/doc.go | 2 ++ core/frontmatter/parser.go | 33 +++++++++++++++++++++++++++++++++ test/doc.go | 2 ++ test/frontmatter_test.go | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 core/frontmatter/doc.go create mode 100644 core/frontmatter/parser.go create mode 100644 test/doc.go create mode 100644 test/frontmatter_test.go diff --git a/core/frontmatter/doc.go b/core/frontmatter/doc.go new file mode 100644 index 0000000..b723de9 --- /dev/null +++ b/core/frontmatter/doc.go @@ -0,0 +1,2 @@ +// Package frontmatter contains utilities for parsing front matter from documents. +package frontmatter diff --git a/core/frontmatter/parser.go b/core/frontmatter/parser.go new file mode 100644 index 0000000..2b82de1 --- /dev/null +++ b/core/frontmatter/parser.go @@ -0,0 +1,33 @@ +package frontmatter + +import ( + "bufio" + "bytes" + "errors" + "io" +) + +func ParseFrontMatter(r io.Reader) (map[string]string, error) { + scanner := bufio.NewScanner(r) + frontMatter := make(map[string]string) + + for scanner.Scan() { + line := scanner.Text() + if line == "---" { + break + } + parts := bytes.SplitN([]byte(line), []byte(":"), 2) + if len(parts) != 2 { + return nil, errors.New("invalid front matter format") + } + key := string(bytes.TrimSpace(parts[0])) + value := string(bytes.TrimSpace(parts[1])) + frontMatter[key] = value + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return frontMatter, nil +} diff --git a/test/doc.go b/test/doc.go new file mode 100644 index 0000000..361c240 --- /dev/null +++ b/test/doc.go @@ -0,0 +1,2 @@ +// Package test contains all test files for the project. +package test diff --git a/test/frontmatter_test.go b/test/frontmatter_test.go new file mode 100644 index 0000000..3f84a90 --- /dev/null +++ b/test/frontmatter_test.go @@ -0,0 +1,33 @@ +package test + +import ( + "strings" + "testing" + + "github.com/KristianJBorgwarth/dendrite.daemon/core/frontmatter" +) + +func TestParseFrontMatter(t *testing.T) { + input := `title: My Note +template: default +tags: ["test", "note"] +--- +This is the content of the note.` + + expected := map[string]string{ + "title": "My Note", + "template": "default", + "tags": `["test", "note"]`, + } + + result, err := frontmatter.ParseFrontMatter(strings.NewReader(input)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + for key, expectedValue := range expected { + if value, ok := result[key]; !ok || value != expectedValue { + t.Errorf("expected %s to be %s, got %s", key, expectedValue, value) + } + } +} From 5e0dbe5873d98cebee509b14ea462e563307f315 Mon Sep 17 00:00:00 2001 From: Kristian Borgwarth <10348902@pm.me> Date: Wed, 25 Mar 2026 20:34:53 +0100 Subject: [PATCH 2/3] feat(frontmatter): parsing and tag extract --- core/frontmatter/parser.go | 23 +++++++++++++++++++++++ test/doc.go | 2 -- test/frontmatter_test.go | 25 ++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 3 deletions(-) delete mode 100644 test/doc.go diff --git a/core/frontmatter/parser.go b/core/frontmatter/parser.go index 2b82de1..ce382e8 100644 --- a/core/frontmatter/parser.go +++ b/core/frontmatter/parser.go @@ -3,10 +3,20 @@ package frontmatter import ( "bufio" "bytes" + "encoding/json" "errors" "io" ) +type FrontMatter struct { + Title string + Tags []string + Created string + Updated string + Author string + +} + func ParseFrontMatter(r io.Reader) (map[string]string, error) { scanner := bufio.NewScanner(r) frontMatter := make(map[string]string) @@ -31,3 +41,16 @@ func ParseFrontMatter(r io.Reader) (map[string]string, error) { return frontMatter, nil } + +func ExtractTags(frontMatter map[string]string) ([]string, error) { + tagsStr, ok := frontMatter["tags"] + if !ok { + return nil, errors.New("tags not found in front matter") + } + var tags []string + err := json.Unmarshal([]byte(tagsStr), &tags) + if err != nil { + return nil, err + } + return tags, nil +} diff --git a/test/doc.go b/test/doc.go deleted file mode 100644 index 361c240..0000000 --- a/test/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package test contains all test files for the project. -package test diff --git a/test/frontmatter_test.go b/test/frontmatter_test.go index 3f84a90..858eb5f 100644 --- a/test/frontmatter_test.go +++ b/test/frontmatter_test.go @@ -1,4 +1,4 @@ -package test +package frontmatter_test import ( "strings" @@ -31,3 +31,26 @@ This is the content of the note.` } } } + +func TestExtractTags(t *testing.T) { + frontMatter := map[string]string{ + "tags": `["test", "note"]`, + } + + expected := []string{"test", "note"} + + result, err := frontmatter.ExtractTags(frontMatter) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(result) != len(expected) { + t.Fatalf("expected %d tags, got %d", len(expected), len(result)) + } + + for i, expectedTag := range expected { + if result[i] != expectedTag { + t.Errorf("expected tag %d to be %s, got %s", i, expectedTag, result[i]) + } + } +} From 2080821382ac470d165006fbc76565d807b9ff3c Mon Sep 17 00:00:00 2001 From: Kristian Borgwarth <10348902@pm.me> Date: Wed, 25 Mar 2026 22:46:52 +0100 Subject: [PATCH 3/3] feat(note_create): tag repo and note repo --- core/frontmatter/parser.go | 1 - core/frontmatter/slug.go | 19 +++++++++ core/handlers/create_note.go | 46 +++++++++++++++++---- core/rpc/models.go | 10 ++--- persistence/repositories/tag_repository.go | 48 ++++++++++++++++------ 5 files changed, 96 insertions(+), 28 deletions(-) create mode 100644 core/frontmatter/slug.go diff --git a/core/frontmatter/parser.go b/core/frontmatter/parser.go index ce382e8..073d394 100644 --- a/core/frontmatter/parser.go +++ b/core/frontmatter/parser.go @@ -14,7 +14,6 @@ type FrontMatter struct { Created string Updated string Author string - } func ParseFrontMatter(r io.Reader) (map[string]string, error) { diff --git a/core/frontmatter/slug.go b/core/frontmatter/slug.go new file mode 100644 index 0000000..894b782 --- /dev/null +++ b/core/frontmatter/slug.go @@ -0,0 +1,19 @@ +package frontmatter + +import ( + "bytes" +) + +func Slugify(s string) string { + var slug bytes.Buffer + for _, r := range s { + if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') { + slug.WriteRune(r) + } else if r >= 'A' && r <= 'Z' { + slug.WriteRune(r + 32) // Convert to lowercase + } else { + slug.WriteRune('-') + } + } + return slug.String() +} diff --git a/core/handlers/create_note.go b/core/handlers/create_note.go index df71e3e..d1b462a 100644 --- a/core/handlers/create_note.go +++ b/core/handlers/create_note.go @@ -1,8 +1,11 @@ package handlers import ( + "bytes" "encoding/json" + "os" + "github.com/KristianJBorgwarth/dendrite.daemon/core/frontmatter" "github.com/KristianJBorgwarth/dendrite.daemon/core/rpc" "github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories" ) @@ -14,21 +17,46 @@ type createNoteCommand struct { Vars map[string]string `json:"vars"` } -type CreateNoteHandler struct{ - repository repositories.NoteRepository +type CreateNoteHandler struct { + noteRepo repositories.NoteRepository + tagRepo repositories.TagRepository } -func (h CreateNoteHandler) Handle(params []byte) (any, *rpc.Error) { +func (h CreateNoteHandler) Handle(params []byte) (*rpc.Response, *rpc.Error) { var cmd createNoteCommand + if err := json.Unmarshal(params, &cmd); err != nil { - return nil, &rpc.Error{Code: -32602, Message: "invalid params"} + return nil, h.ReturnError(err) } - err := h.repository.Upsert(cmd.Title, cmd.Path, cmd.Path) - + data, err := os.ReadFile(cmd.TemplatePath) if err != nil { - return nil, &rpc.Error{Code: -1, Message: err.Error()} + return nil, h.ReturnError(err) } - - return map[string]any{"status": "ok"}, nil + + feMatter, err := frontmatter.ParseFrontMatter(bytes.NewReader(data)) + if err != nil { + return nil, h.ReturnError(err) + } + + tags, err := frontmatter.ExtractTags(feMatter) + if err != nil { + return nil, h.ReturnError(err) + } + + err = h.tagRepo.Upsert(tags) + if err != nil { + return nil, h.ReturnError(err) + } + + err = h.noteRepo.Upsert(cmd.Title, cmd.Path, frontmatter.Slugify(cmd.Title)) + if err != nil { + return nil, h.ReturnError(err) + } + + return &rpc.Response{Jsonrpc: "2.0", Result: cmd.Path}, nil +} + +func (h *CreateNoteHandler) ReturnError(err error) *rpc.Error { + return &rpc.Error{Code: -1, Message: err.Error()} } diff --git a/core/rpc/models.go b/core/rpc/models.go index a2d2245..3dcb7f1 100644 --- a/core/rpc/models.go +++ b/core/rpc/models.go @@ -7,16 +7,16 @@ import ( type Request struct { Jsonrpc string `json:"jsonrpc"` - ID *int `json:"id"` + ID *int `json:"id"` Method string `json:"method"` Params json.RawMessage `json:"params"` } type Response struct { - Jsonrpc string `json:"jsonrpc"` - ID *int `json:"id"` - Result any `json:"result,omitempty"` - Error *Error `json:"error,omitempty"` + Jsonrpc string `json:"jsonrpc"` + ID *int `json:"id"` + Result any `json:"result,omitempty"` + Error *Error `json:"error,omitempty"` } type Notification struct { diff --git a/persistence/repositories/tag_repository.go b/persistence/repositories/tag_repository.go index c198560..844ccc1 100644 --- a/persistence/repositories/tag_repository.go +++ b/persistence/repositories/tag_repository.go @@ -1,10 +1,12 @@ - package repositories -import "database/sql" +import ( + "database/sql" + "strings" +) type TagRepository interface { - Upsert(title string, path string) error + Upsert(names []string) error } type tagRepository struct { @@ -15,14 +17,34 @@ func NewTagRepository(db *sql.DB) TagRepository { return &tagRepository{db: db} } -func (r *tagRepository) Upsert(title string, path string) error { - query := ` - INSERT INTO tags (title, path) - VALUES ($1, $2) - ON CONFLICT (slug) DO UPDATE - SET title = EXCLUDED.title, - path = EXCLUDED.path; - ` - _, err := r.db.Exec(query, title, path) - return err +func (r *tagRepository) Upsert(names []string) error { + if len(names) == 0 { + return nil + } + + tx, err := r.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + placeholders := make([]string, 0, len(names)) + args := make([]any, 0, len(names)) + + for _, name := range names { + placeholders = append(placeholders, "(?)") + args = append(args, name) + } + + query := "WITH input(name) AS (VALUES " + + strings.Join(placeholders, ",") + + ") INSERT INTO tags(name) " + + "SELECT name FROM input " + + "ON CONFLICT(name) DO NOTHING;" + + if _, err := tx.Exec(query, args...); err != nil { + return err + } + + return tx.Commit() }