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..073d394 --- /dev/null +++ b/core/frontmatter/parser.go @@ -0,0 +1,55 @@ +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) + + 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 +} + +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/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() } diff --git a/test/frontmatter_test.go b/test/frontmatter_test.go new file mode 100644 index 0000000..858eb5f --- /dev/null +++ b/test/frontmatter_test.go @@ -0,0 +1,56 @@ +package frontmatter_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) + } + } +} + +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]) + } + } +}