api cleanup

This commit is contained in:
Levi Neely 2025-11-26 19:14:42 +01:00
parent 3e7e625051
commit d81cfa5e67
8 changed files with 172 additions and 216 deletions

View File

@ -190,15 +190,19 @@ func windowMode(args []string) error {
// Update frontmatter if applicable // Update frontmatter if applicable
if supportsFrontmatter && len(body) > 0 { if supportsFrontmatter && len(body) > 0 {
// Create new frontmatter // Parse existing frontmatter to preserve Identifier and FileType
fm := &metadata.FrontMatter{ existing, err := metadata.ParseFrontMatter(string(body), ext)
Title: title, if err != nil {
Tags: tags, return fmt.Errorf("failed to parse existing frontmatter: %w", err)
Signature: signature,
} }
// Update frontmatter in content // Update only the fields that changed from rename
newContent, err := metadata.UpdateFrontMatter(string(body), fm) existing.Title = title
existing.Tags = tags
existing.Signature = signature
// Apply updated frontmatter to content
newContent, err := metadata.Apply(string(body), existing)
if err != nil { if err != nil {
return fmt.Errorf("failed to update frontmatter: %w", err) return fmt.Errorf("failed to update frontmatter: %w", err)
} }

View File

@ -36,14 +36,14 @@ func HandleUpdateEvent(f *client.Fsys, identifier, denoteDir string) error {
// Only update frontmatter if file exists (skip if not yet created via Put) // Only update frontmatter if file exists (skip if not yet created via Put)
if _, err := os.Stat(path); err == nil && SupportsFrontMatter(path) { if _, err := os.Stat(path); err == nil && SupportsFrontMatter(path) {
ext := strings.ToLower(filepath.Ext(path)) ext := strings.ToLower(filepath.Ext(path))
var fileType string var fileType metadata.FileType
switch ext { switch ext {
case ".org": case ".org":
fileType = "org" fileType = metadata.FileTypeOrg
case ".md": case ".md":
fileType = "md-yaml" fileType = metadata.FileTypeMdYaml
case ".txt": case ".txt":
fileType = "txt" fileType = metadata.FileTypeTxt
} }
fm := &metadata.FrontMatter{ fm := &metadata.FrontMatter{

View File

@ -67,10 +67,10 @@ func UpdateFrontMatter(path string, fm *metadata.FrontMatter) error {
return fmt.Errorf("failed to read file: %w", err) return fmt.Errorf("failed to read file: %w", err)
} }
// Update front matter (pure function) // Apply front matter to content
newContent, err := metadata.UpdateFrontMatter(string(content), fm) newContent, err := metadata.Apply(string(content), fm)
if err != nil { if err != nil {
return fmt.Errorf("failed to update front matter: %w", err) return fmt.Errorf("failed to apply front matter: %w", err)
} }
// Write back to file // Write back to file

11
main.go
View File

@ -2,9 +2,9 @@ package main
import ( import (
"denote/internal/disk" "denote/internal/disk"
"denote/pkg/metadata"
p9client "denote/internal/p9/client" p9client "denote/internal/p9/client"
p9server "denote/internal/p9/server" p9server "denote/internal/p9/server"
"denote/pkg/metadata"
"fmt" "fmt"
"log" "log"
"os" "os"
@ -21,7 +21,7 @@ import (
const ( const (
wname = "/Denote/" wname = "/Denote/"
ftype = "md-yaml" ftype = metadata.FileTypeMdYaml
) )
var denoteDir = os.Getenv("HOME") + "/doc" var denoteDir = os.Getenv("HOME") + "/doc"
@ -112,7 +112,12 @@ func handleNewEvent(f *client.Fsys, identifier, denoteDir string) error {
} }
} }
path, content := metadata.GenerateNote(targetDir, title, signature, tags, ftype) newIdentifier := metadata.GenerateIdentifier()
ext := metadata.GetExtension(ftype)
filename := metadata.BuildFilename(newIdentifier, signature, title, tags, ext)
path := filepath.Join(targetDir, filename)
fm := metadata.NewFrontMatter(title, signature, tags, ftype, newIdentifier)
content := string(fm.Bytes())
if err := p9client.WriteFile(f, "n/"+identifier+"/path", path); err != nil { if err := p9client.WriteFile(f, "n/"+identifier+"/path", path); err != nil {
return fmt.Errorf("failed to update path in metadata: %w", err) return fmt.Errorf("failed to update path in metadata: %w", err)

View File

@ -8,15 +8,15 @@ import (
) )
// Templates for denote frontmatter (org, md, txt) // Templates for denote frontmatter (org, md, txt)
var templates = map[string]string{ var templates = map[FileType]string{
"org": `#+title: %s FileTypeOrg: `#+title: %s
#+date: %s #+date: %s
#+filetags: %s #+filetags: %s
#+identifier: %s #+identifier: %s
#+signature: %s #+signature: %s
`, `,
"md-yaml": `--- FileTypeMdYaml: `---
title: %s title: %s
date: %s date: %s
tags: %s tags: %s
@ -25,7 +25,7 @@ signature: %s
--- ---
`, `,
"md-toml": `+++ FileTypeMdToml: `+++
title = %s title = %s
date = %s date = %s
tags = %s tags = %s
@ -34,7 +34,7 @@ signature = %s
+++ +++
`, `,
"txt": `title: %s FileTypeTxt: `title: %s
date: %s date: %s
tags: %s tags: %s
identifier: %s identifier: %s
@ -44,24 +44,39 @@ signature: %s
`, `,
} }
// FileType represents supported file formats for denote notes
type FileType string
const (
FileTypeOrg FileType = "org"
FileTypeMdYaml FileType = "md-yaml"
FileTypeMdToml FileType = "md-toml"
FileTypeTxt FileType = "txt"
)
// fileExtensions contains the list of file extensions // fileExtensions contains the list of file extensions
// for which Denote should add front matter. // for which Denote should add front matter.
var fileExtensions = map[string]string{ var fileExtensions = map[FileType]string{
"org": ".org", FileTypeOrg: ".org",
"md-yaml": ".md", FileTypeMdYaml: ".md",
"md-toml": ".md", FileTypeMdToml: ".md",
"txt": ".txt", FileTypeTxt: ".txt",
}
// GetExtension returns the file extension for a given file type.
func GetExtension(fileType FileType) string {
return fileExtensions[fileType]
} }
// FormatTags formats tags according to file type // FormatTags formats tags according to file type
func FormatTags(tags []string, fileType string) string { func FormatTags(tags []string, fileType FileType) string {
if len(tags) == 0 { if len(tags) == 0 {
return "" return ""
} }
switch fileType { switch fileType {
case "org": case FileTypeOrg:
return ":" + strings.Join(tags, ":") + ":" return ":" + strings.Join(tags, ":") + ":"
case "md-yaml", "md-toml": case FileTypeMdYaml, FileTypeMdToml:
return "[" + strings.Join(tags, ", ") + "]" return "[" + strings.Join(tags, ", ") + "]"
default: default:
return strings.Join(tags, " ") return strings.Join(tags, " ")
@ -74,7 +89,22 @@ type FrontMatter struct {
Tags []string Tags []string
Identifier string Identifier string
Signature string Signature string
FileType string // org, md-yaml, md-toml, txt FileType FileType
}
// Bytes returns the formatted frontmatter content as bytes
func (fm *FrontMatter) Bytes() []byte {
template := templates[fm.FileType]
dateStr := time.Now().Format("2006-01-02 Mon 15:04")
// For org-mode, wrap date in brackets for timestamp
if fm.FileType == FileTypeOrg {
dateStr = "[" + dateStr + "]"
}
keywordsStr := FormatTags(fm.Tags, fm.FileType)
content := fmt.Sprintf(template, fm.Title, dateStr, keywordsStr, fm.Identifier, fm.Signature)
return []byte(content)
} }
// ParseFrontMatter extracts front matter from file content. // ParseFrontMatter extracts front matter from file content.
@ -87,7 +117,7 @@ func ParseFrontMatter(content string, ext string) (*FrontMatter, error) {
switch ext { switch ext {
case ".org": case ".org":
fm.FileType = "org" fm.FileType = FileTypeOrg
if m := regexp.MustCompile(`(?m)^#\+title:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil { if m := regexp.MustCompile(`(?m)^#\+title:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil {
fm.Title = strings.TrimSpace(m[1]) fm.Title = strings.TrimSpace(m[1])
} }
@ -105,7 +135,7 @@ func ParseFrontMatter(content string, ext string) (*FrontMatter, error) {
// Try YAML first // Try YAML first
yamlRe := regexp.MustCompile(`(?ms)^---\n(.*?)\n---`) yamlRe := regexp.MustCompile(`(?ms)^---\n(.*?)\n---`)
if m := yamlRe.FindStringSubmatch(text); m != nil { if m := yamlRe.FindStringSubmatch(text); m != nil {
fm.FileType = "md-yaml" fm.FileType = FileTypeMdYaml
yamlContent := m[1] yamlContent := m[1]
if m := regexp.MustCompile(`(?m)^title:[ \t]*["']?(.+?)["']?$`).FindStringSubmatch(yamlContent); m != nil { if m := regexp.MustCompile(`(?m)^title:[ \t]*["']?(.+?)["']?$`).FindStringSubmatch(yamlContent); m != nil {
fm.Title = strings.TrimSpace(m[1]) fm.Title = strings.TrimSpace(m[1])
@ -127,7 +157,7 @@ func ParseFrontMatter(content string, ext string) (*FrontMatter, error) {
// Try TOML // Try TOML
tomlRe := regexp.MustCompile(`(?ms)^\+\+\+\n(.*?)\n\+\+\+`) tomlRe := regexp.MustCompile(`(?ms)^\+\+\+\n(.*?)\n\+\+\+`)
if m := tomlRe.FindStringSubmatch(text); m != nil { if m := tomlRe.FindStringSubmatch(text); m != nil {
fm.FileType = "md-toml" fm.FileType = FileTypeMdToml
tomlContent := m[1] tomlContent := m[1]
if m := regexp.MustCompile(`(?m)^title[ \t]*=[ \t]*["']?(.+?)["']?$`).FindStringSubmatch(tomlContent); m != nil { if m := regexp.MustCompile(`(?m)^title[ \t]*=[ \t]*["']?(.+?)["']?$`).FindStringSubmatch(tomlContent); m != nil {
fm.Title = strings.TrimSpace(m[1]) fm.Title = strings.TrimSpace(m[1])
@ -149,7 +179,7 @@ func ParseFrontMatter(content string, ext string) (*FrontMatter, error) {
} }
case ".txt": case ".txt":
fm.FileType = "txt" fm.FileType = FileTypeTxt
if m := regexp.MustCompile(`(?m)^title:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil { if m := regexp.MustCompile(`(?m)^title:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil {
fm.Title = strings.TrimSpace(m[1]) fm.Title = strings.TrimSpace(m[1])
} }
@ -167,25 +197,15 @@ func ParseFrontMatter(content string, ext string) (*FrontMatter, error) {
return fm, nil return fm, nil
} }
// UpdateFrontMatter updates front matter in file content and returns the new content. // Apply applies front matter to file content, replacing existing front matter if present.
// originalContent is the current file content, fm is the new front matter to apply. // originalContent is the current file content, fm is the new front matter to apply.
func UpdateFrontMatter(originalContent string, fm *FrontMatter) (string, error) { func Apply(originalContent string, fm *FrontMatter) (string, error) {
text := originalContent text := originalContent
newFrontMatter := string(fm.Bytes())
dateStr := time.Now().Format("2006-01-02 Mon 15:04")
keywordsStr := FormatTags(fm.Tags, fm.FileType)
// For org-mode, wrap date in brackets for timestamp
if fm.FileType == "org" {
dateStr = "[" + dateStr + "]"
}
template := templates[fm.FileType]
newFrontMatter := fmt.Sprintf(template, fm.Title, dateStr, keywordsStr, fm.Identifier, fm.Signature)
var newText string var newText string
switch fm.FileType { switch fm.FileType {
case "org": case FileTypeOrg:
// Find end of front matter (first blank line or non-#+ line) // Find end of front matter (first blank line or non-#+ line)
lines := strings.Split(text, "\n") lines := strings.Split(text, "\n")
endIdx := 0 endIdx := 0
@ -205,7 +225,7 @@ func UpdateFrontMatter(originalContent string, fm *FrontMatter) (string, error)
newText = newFrontMatter + text newText = newFrontMatter + text
} }
case "md-yaml": case FileTypeMdYaml:
// Replace YAML front matter (match trailing blank lines to avoid duplication) // Replace YAML front matter (match trailing blank lines to avoid duplication)
re := regexp.MustCompile(`(?s)^---\n.*?\n---\n\n*`) re := regexp.MustCompile(`(?s)^---\n.*?\n---\n\n*`)
if re.MatchString(text) { if re.MatchString(text) {
@ -214,7 +234,7 @@ func UpdateFrontMatter(originalContent string, fm *FrontMatter) (string, error)
newText = newFrontMatter + text newText = newFrontMatter + text
} }
case "md-toml": case FileTypeMdToml:
// Replace TOML front matter (match trailing blank lines to avoid duplication) // Replace TOML front matter (match trailing blank lines to avoid duplication)
re := regexp.MustCompile(`(?s)^\+\+\+\n.*?\n\+\+\+\n\n*`) re := regexp.MustCompile(`(?s)^\+\+\+\n.*?\n\+\+\+\n\n*`)
if re.MatchString(text) { if re.MatchString(text) {
@ -223,7 +243,7 @@ func UpdateFrontMatter(originalContent string, fm *FrontMatter) (string, error)
newText = newFrontMatter + text newText = newFrontMatter + text
} }
case "txt": case FileTypeTxt:
// Replace text front matter (match trailing blank lines to avoid duplication) // Replace text front matter (match trailing blank lines to avoid duplication)
re := regexp.MustCompile(`(?s)^title:.*?\n-+\n\n*`) re := regexp.MustCompile(`(?s)^title:.*?\n-+\n\n*`)
if re.MatchString(text) { if re.MatchString(text) {
@ -238,16 +258,13 @@ func UpdateFrontMatter(originalContent string, fm *FrontMatter) (string, error)
return newText, nil return newText, nil
} }
// Generate generates front matter content for given parameters // NewFrontMatter creates a new FrontMatter struct from given parameters
func Generate(title, signature string, tags []string, fileType, identifier string) string { func NewFrontMatter(title, signature string, tags []string, fileType FileType, identifier string) *FrontMatter {
template := templates[fileType] return &FrontMatter{
dateStr := time.Now().Format("2006-01-02 Mon 15:04") Title: title,
Tags: tags,
// For org-mode, wrap date in brackets for timestamp Identifier: identifier,
if fileType == "org" { Signature: signature,
dateStr = "[" + dateStr + "]" FileType: fileType,
} }
keywordsStr := FormatTags(tags, fileType)
return fmt.Sprintf(template, title, dateStr, keywordsStr, identifier, signature)
} }

View File

@ -12,73 +12,73 @@ func TestFormatTags(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
tags []string tags []string
fileType string fileType FileType
want string want string
}{ }{
{ {
name: "org with multiple tags", name: "org with multiple tags",
tags: []string{"tag1", "tag2"}, tags: []string{"tag1", "tag2"},
fileType: "org", fileType: FileTypeOrg,
want: ":tag1:tag2:", want: ":tag1:tag2:",
}, },
{ {
name: "org with single tag", name: "org with single tag",
tags: []string{"single"}, tags: []string{"single"},
fileType: "org", fileType: FileTypeOrg,
want: ":single:", want: ":single:",
}, },
{ {
name: "org with empty tags", name: "org with empty tags",
tags: []string{}, tags: []string{},
fileType: "org", fileType: FileTypeOrg,
want: "", want: "",
}, },
{ {
name: "md-yaml with multiple tags", name: "md-yaml with multiple tags",
tags: []string{"tag1", "tag2"}, tags: []string{"tag1", "tag2"},
fileType: "md-yaml", fileType: FileTypeMdYaml,
want: "[tag1, tag2]", want: "[tag1, tag2]",
}, },
{ {
name: "md-yaml with single tag", name: "md-yaml with single tag",
tags: []string{"single"}, tags: []string{"single"},
fileType: "md-yaml", fileType: FileTypeMdYaml,
want: "[single]", want: "[single]",
}, },
{ {
name: "md-yaml with empty tags", name: "md-yaml with empty tags",
tags: []string{}, tags: []string{},
fileType: "md-yaml", fileType: FileTypeMdYaml,
want: "", want: "",
}, },
{ {
name: "md-toml with multiple tags", name: "md-toml with multiple tags",
tags: []string{"tag1", "tag2"}, tags: []string{"tag1", "tag2"},
fileType: "md-toml", fileType: FileTypeMdToml,
want: "[tag1, tag2]", want: "[tag1, tag2]",
}, },
{ {
name: "md-toml with empty tags", name: "md-toml with empty tags",
tags: []string{}, tags: []string{},
fileType: "md-toml", fileType: FileTypeMdToml,
want: "", want: "",
}, },
{ {
name: "txt with multiple tags", name: "txt with multiple tags",
tags: []string{"tag1", "tag2"}, tags: []string{"tag1", "tag2"},
fileType: "txt", fileType: FileTypeTxt,
want: "tag1 tag2", want: "tag1 tag2",
}, },
{ {
name: "txt with single tag", name: "txt with single tag",
tags: []string{"single"}, tags: []string{"single"},
fileType: "txt", fileType: FileTypeTxt,
want: "single", want: "single",
}, },
{ {
name: "txt with empty tags", name: "txt with empty tags",
tags: []string{}, tags: []string{},
fileType: "txt", fileType: FileTypeTxt,
want: "", want: "",
}, },
} }
@ -93,22 +93,22 @@ func TestFormatTags(t *testing.T) {
} }
} }
// TestGenerate validates front matter generation for all file types // TestFrontMatterBytes validates front matter formatting for all file types
// Maps to dt-denote--format-front-matter from original tests // Maps to dt-denote--format-front-matter from original tests
func TestGenerate(t *testing.T) { func TestFrontMatterBytes(t *testing.T) {
identifier := "20240101T120000" identifier := "20240101T120000"
title := "Test Note" title := "Test Note"
tags := []string{"tag1", "tag2"} tags := []string{"tag1", "tag2"}
tests := []struct { tests := []struct {
name string name string
fileType string fileType FileType
wantContains []string wantContains []string
wantNotContains []string wantNotContains []string
}{ }{
{ {
name: "org format", name: "org format",
fileType: "org", fileType: FileTypeOrg,
wantContains: []string{ wantContains: []string{
"#+title: Test Note", "#+title: Test Note",
"#+filetags: :tag1:tag2:", "#+filetags: :tag1:tag2:",
@ -118,7 +118,7 @@ func TestGenerate(t *testing.T) {
}, },
{ {
name: "md-yaml format", name: "md-yaml format",
fileType: "md-yaml", fileType: FileTypeMdYaml,
wantContains: []string{ wantContains: []string{
"---", "---",
"title: Test Note", "title: Test Note",
@ -129,7 +129,7 @@ func TestGenerate(t *testing.T) {
}, },
{ {
name: "md-toml format", name: "md-toml format",
fileType: "md-toml", fileType: FileTypeMdToml,
wantContains: []string{ wantContains: []string{
"+++", "+++",
"title = Test Note", "title = Test Note",
@ -140,7 +140,7 @@ func TestGenerate(t *testing.T) {
}, },
{ {
name: "txt format", name: "txt format",
fileType: "txt", fileType: FileTypeTxt,
wantContains: []string{ wantContains: []string{
"title: Test Note", "title: Test Note",
"tags: tag1 tag2", "tags: tag1 tag2",
@ -153,64 +153,64 @@ func TestGenerate(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := Generate(title, "", tags, tt.fileType, identifier) fm := NewFrontMatter(title, "", tags, tt.fileType, identifier)
got := string(fm.Bytes())
for _, want := range tt.wantContains { for _, want := range tt.wantContains {
if !strings.Contains(got, want) { if !strings.Contains(got, want) {
t.Errorf("Generate(%q, %v, %q, %q) missing %q\nGot:\n%s", t.Errorf("FrontMatter.Bytes() missing %q\nGot:\n%s", want, got)
title, tags, tt.fileType, identifier, want, got)
} }
} }
for _, notWant := range tt.wantNotContains { for _, notWant := range tt.wantNotContains {
if strings.Contains(got, notWant) { if strings.Contains(got, notWant) {
t.Errorf("Generate(%q, %v, %q, %q) should not contain %q\nGot:\n%s", t.Errorf("FrontMatter.Bytes() should not contain %q\nGot:\n%s", notWant, got)
title, tags, tt.fileType, identifier, notWant, got)
} }
} }
}) })
} }
} }
// TestGenerateEmptyTags validates front matter generation with no tags // TestFrontMatterBytesEmptyTags validates front matter formatting with no tags
func TestGenerateEmptyTags(t *testing.T) { func TestFrontMatterBytesEmptyTags(t *testing.T) {
identifier := "20240101T120000" identifier := "20240101T120000"
title := "Test Note" title := "Test Note"
tags := []string{} tags := []string{}
tests := []struct { tests := []struct {
name string name string
fileType string fileType FileType
wantTags string wantTags string
}{ }{
{ {
name: "org with empty tags", name: "org with empty tags",
fileType: "org", fileType: FileTypeOrg,
wantTags: "#+filetags:", wantTags: "#+filetags:",
}, },
{ {
name: "md-yaml with empty tags", name: "md-yaml with empty tags",
fileType: "md-yaml", fileType: FileTypeMdYaml,
wantTags: "tags:", wantTags: "tags:",
}, },
{ {
name: "md-toml with empty tags", name: "md-toml with empty tags",
fileType: "md-toml", fileType: FileTypeMdToml,
wantTags: "tags =", wantTags: "tags =",
}, },
{ {
name: "txt with empty tags", name: "txt with empty tags",
fileType: "txt", fileType: FileTypeTxt,
wantTags: "tags:", wantTags: "tags:",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := Generate(title, "", tags, tt.fileType, identifier) fm := NewFrontMatter(title, "", tags, tt.fileType, identifier)
got := string(fm.Bytes())
if !strings.Contains(got, tt.wantTags) { if !strings.Contains(got, tt.wantTags) {
t.Errorf("Generate() with empty tags should contain %q\nGot:\n%s", t.Errorf("FrontMatter.Bytes() with empty tags should contain %q\nGot:\n%s",
tt.wantTags, got) tt.wantTags, got)
} }
}) })
@ -226,7 +226,7 @@ func TestParseFrontMatter(t *testing.T) {
wantTitle string wantTitle string
wantTags []string wantTags []string
wantIdentifier string wantIdentifier string
wantFileType string wantFileType FileType
}{ }{
{ {
name: "org format", name: "org format",
@ -240,7 +240,7 @@ func TestParseFrontMatter(t *testing.T) {
wantTitle: "Org Note", wantTitle: "Org Note",
wantTags: []string{"work", "emacs"}, wantTags: []string{"work", "emacs"},
wantIdentifier: "20240101T120000", wantIdentifier: "20240101T120000",
wantFileType: "org", wantFileType: FileTypeOrg,
}, },
{ {
name: "org with single tag", name: "org with single tag",
@ -251,7 +251,7 @@ func TestParseFrontMatter(t *testing.T) {
wantTitle: "Single Tag", wantTitle: "Single Tag",
wantTags: []string{"single"}, wantTags: []string{"single"},
wantIdentifier: "20240101T120000", wantIdentifier: "20240101T120000",
wantFileType: "org", wantFileType: FileTypeOrg,
}, },
{ {
name: "org without tags", name: "org without tags",
@ -261,7 +261,7 @@ func TestParseFrontMatter(t *testing.T) {
wantTitle: "No Tags", wantTitle: "No Tags",
wantTags: nil, wantTags: nil,
wantIdentifier: "20240101T120000", wantIdentifier: "20240101T120000",
wantFileType: "org", wantFileType: FileTypeOrg,
}, },
{ {
name: "markdown yaml", name: "markdown yaml",
@ -277,7 +277,7 @@ identifier: 20240101T120000
wantTitle: "Markdown Note", wantTitle: "Markdown Note",
wantTags: []string{"work", "personal"}, wantTags: []string{"work", "personal"},
wantIdentifier: "20240101T120000", wantIdentifier: "20240101T120000",
wantFileType: "md-yaml", wantFileType: FileTypeMdYaml,
}, },
{ {
name: "markdown yaml with quoted title", name: "markdown yaml with quoted title",
@ -290,7 +290,7 @@ identifier: 20240101T120000
wantTitle: "Quoted Title", wantTitle: "Quoted Title",
wantTags: []string{"test"}, wantTags: []string{"test"},
wantIdentifier: "20240101T120000", wantIdentifier: "20240101T120000",
wantFileType: "md-yaml", wantFileType: FileTypeMdYaml,
}, },
{ {
name: "markdown toml", name: "markdown toml",
@ -306,7 +306,7 @@ Content`,
wantTitle: "TOML Note", wantTitle: "TOML Note",
wantTags: []string{"rust", "go"}, wantTags: []string{"rust", "go"},
wantIdentifier: "20240101T120000", wantIdentifier: "20240101T120000",
wantFileType: "md-toml", wantFileType: FileTypeMdToml,
}, },
{ {
name: "txt format", name: "txt format",
@ -321,7 +321,7 @@ Content here`,
wantTitle: "Plain Text", wantTitle: "Plain Text",
wantTags: []string{"simple", "plain"}, wantTags: []string{"simple", "plain"},
wantIdentifier: "20240101T120000", wantIdentifier: "20240101T120000",
wantFileType: "txt", wantFileType: FileTypeTxt,
}, },
{ {
name: "txt without tags", name: "txt without tags",
@ -332,7 +332,7 @@ identifier: 20240101T120000
wantTitle: "No Tags Text", wantTitle: "No Tags Text",
wantTags: nil, wantTags: nil,
wantIdentifier: "20240101T120000", wantIdentifier: "20240101T120000",
wantFileType: "txt", wantFileType: FileTypeTxt,
}, },
} }
@ -385,8 +385,8 @@ date: 2024-01-01
} }
} }
// TestUpdateFrontMatter validates front matter updates // TestApply validates front matter updates
func TestUpdateFrontMatter(t *testing.T) { func TestApply(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
original string original string
@ -407,7 +407,7 @@ This should be preserved`,
Title: "New Title", Title: "New Title",
Tags: []string{"new", "updated"}, Tags: []string{"new", "updated"},
Identifier: "20240101T120000", Identifier: "20240101T120000",
FileType: "org", FileType: FileTypeOrg,
}, },
wantContains: []string{ wantContains: []string{
"#+title: New Title", "#+title: New Title",
@ -431,7 +431,7 @@ Content preserved`,
Title: "New Title", Title: "New Title",
Tags: []string{"new"}, Tags: []string{"new"},
Identifier: "20240101T120000", Identifier: "20240101T120000",
FileType: "md-yaml", FileType: FileTypeMdYaml,
}, },
wantContains: []string{ wantContains: []string{
"---", "---",
@ -455,7 +455,7 @@ Content here`,
Title: "New Title", Title: "New Title",
Tags: []string{"updated"}, Tags: []string{"updated"},
Identifier: "20240101T120000", Identifier: "20240101T120000",
FileType: "md-toml", FileType: FileTypeMdToml,
}, },
wantContains: []string{ wantContains: []string{
"+++", "+++",
@ -477,7 +477,7 @@ Text content`,
Title: "New Title", Title: "New Title",
Tags: []string{"new"}, Tags: []string{"new"},
Identifier: "20240101T120000", Identifier: "20240101T120000",
FileType: "txt", FileType: FileTypeTxt,
}, },
wantContains: []string{ wantContains: []string{
"title: New Title", "title: New Title",
@ -494,7 +494,7 @@ Text content`,
Title: "Added Title", Title: "Added Title",
Tags: []string{"added"}, Tags: []string{"added"},
Identifier: "20240101T120000", Identifier: "20240101T120000",
FileType: "org", FileType: FileTypeOrg,
}, },
wantContains: []string{ wantContains: []string{
"#+title: Added Title", "#+title: Added Title",
@ -510,7 +510,7 @@ Text content`,
Title: "Added Title", Title: "Added Title",
Tags: []string{"added"}, Tags: []string{"added"},
Identifier: "20240101T120000", Identifier: "20240101T120000",
FileType: "md-yaml", FileType: FileTypeMdYaml,
}, },
wantContains: []string{ wantContains: []string{
"---", "---",
@ -524,27 +524,27 @@ Text content`,
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := UpdateFrontMatter(tt.original, tt.fm) got, err := Apply(tt.original, tt.fm)
if err != nil { if err != nil {
t.Fatalf("UpdateFrontMatter() error = %v", err) t.Fatalf("Apply() error = %v", err)
} }
for _, want := range tt.wantContains { for _, want := range tt.wantContains {
if !strings.Contains(got, want) { if !strings.Contains(got, want) {
t.Errorf("UpdateFrontMatter() missing %q\nGot:\n%s", want, got) t.Errorf("Apply() missing %q\nGot:\n%s", want, got)
} }
} }
// Verify original content is preserved // Verify original content is preserved
if !strings.Contains(got, tt.wantPreserve) { if !strings.Contains(got, tt.wantPreserve) {
t.Errorf("UpdateFrontMatter() didn't preserve %q\nGot:\n%s", tt.wantPreserve, got) t.Errorf("Apply() didn't preserve %q\nGot:\n%s", tt.wantPreserve, got)
} }
}) })
} }
} }
// TestUpdateFrontMatterEmptyTags validates updating with empty tags // TestApplyEmptyTags validates updating with empty tags
func TestUpdateFrontMatterEmptyTags(t *testing.T) { func TestApplyEmptyTags(t *testing.T) {
original := `--- original := `---
title: Test title: Test
tags: [old, tags] tags: [old, tags]
@ -557,27 +557,27 @@ Content`
Title: "Test", Title: "Test",
Tags: []string{}, Tags: []string{},
Identifier: "20240101T120000", Identifier: "20240101T120000",
FileType: "md-yaml", FileType: FileTypeMdYaml,
} }
got, err := UpdateFrontMatter(original, fm) got, err := Apply(original, fm)
if err != nil { if err != nil {
t.Fatalf("UpdateFrontMatter() error = %v", err) t.Fatalf("Apply() error = %v", err)
} }
// Should have empty tags field, not omit it // Should have empty tags field, not omit it
if !strings.Contains(got, "tags:") { if !strings.Contains(got, "tags:") {
t.Errorf("UpdateFrontMatter() should include tags field even when empty") t.Errorf("Apply() should include tags field even when empty")
} }
// Content should be preserved // Content should be preserved
if !strings.Contains(got, "Content") { if !strings.Contains(got, "Content") {
t.Errorf("UpdateFrontMatter() should preserve content") t.Errorf("Apply() should preserve content")
} }
} }
// TestUpdateFrontMatterUnsupportedType validates error handling // TestApplyUnsupportedType validates error handling
func TestUpdateFrontMatterUnsupportedType(t *testing.T) { func TestApplyUnsupportedType(t *testing.T) {
fm := &FrontMatter{ fm := &FrontMatter{
Title: "Test", Title: "Test",
Tags: []string{"test"}, Tags: []string{"test"},
@ -585,36 +585,33 @@ func TestUpdateFrontMatterUnsupportedType(t *testing.T) {
FileType: "unsupported", FileType: "unsupported",
} }
_, err := UpdateFrontMatter("content", fm) _, err := Apply("content", fm)
if err == nil { if err == nil {
t.Error("UpdateFrontMatter() should error on unsupported file type") t.Error("Apply() should error on unsupported file type")
} }
if !strings.Contains(err.Error(), "unsupported file type") { if !strings.Contains(err.Error(), "unsupported file type") {
t.Errorf("UpdateFrontMatter() error = %v, want 'unsupported file type'", err) t.Errorf("Apply() error = %v, want 'unsupported file type'", err)
} }
} }
// TestFileExtensions validates file extension mapping // TestGetExtension validates file extension mapping
func TestFileExtensions(t *testing.T) { func TestGetExtension(t *testing.T) {
tests := []struct { tests := []struct {
fileType string fileType FileType
want string want string
}{ }{
{"org", ".org"}, {FileTypeOrg, ".org"},
{"md-yaml", ".md"}, {FileTypeMdYaml, ".md"},
{"md-toml", ".md"}, {FileTypeMdToml, ".md"},
{"txt", ".txt"}, {FileTypeTxt, ".txt"},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.fileType, func(t *testing.T) { t.Run(string(tt.fileType), func(t *testing.T) {
got, ok := fileExtensions[tt.fileType] got := GetExtension(tt.fileType)
if !ok {
t.Errorf("fileExtensions[%q] not found", tt.fileType)
}
if got != tt.want { if got != tt.want {
t.Errorf("fileExtensions[%q] = %q, want %q", tt.fileType, got, tt.want) t.Errorf("GetExtension(%q) = %q, want %q", tt.fileType, got, tt.want)
} }
}) })
} }
@ -622,18 +619,19 @@ func TestFileExtensions(t *testing.T) {
// TestFrontMatterRoundTrip validates generate -> parse -> update cycle // TestFrontMatterRoundTrip validates generate -> parse -> update cycle
func TestFrontMatterRoundTrip(t *testing.T) { func TestFrontMatterRoundTrip(t *testing.T) {
fileTypes := []string{"org", "md-yaml", "md-toml", "txt"} fileTypes := []FileType{FileTypeOrg, FileTypeMdYaml, FileTypeMdToml, FileTypeTxt}
title := "Test Note" title := "Test Note"
tags := []string{"tag1", "tag2"} tags := []string{"tag1", "tag2"}
identifier := "20240101T120000" identifier := "20240101T120000"
for _, fileType := range fileTypes { for _, fileType := range fileTypes {
t.Run(fileType, func(t *testing.T) { t.Run(string(fileType), func(t *testing.T) {
// Generate front matter // Generate front matter
content := Generate(title, "", tags, fileType, identifier) fm := NewFrontMatter(title, "", tags, fileType, identifier)
content := string(fm.Bytes())
// Parse it back // Parse it back
ext := fileExtensions[fileType] ext := GetExtension(fileType)
fm, err := ParseFrontMatter(content, ext) fm, err := ParseFrontMatter(content, ext)
if err != nil { if err != nil {
t.Fatalf("ParseFrontMatter() error = %v", err) t.Fatalf("ParseFrontMatter() error = %v", err)
@ -659,9 +657,9 @@ func TestFrontMatterRoundTrip(t *testing.T) {
fm.Title = newTitle fm.Title = newTitle
fm.Tags = newTags fm.Tags = newTags
updated, err := UpdateFrontMatter(content, fm) updated, err := Apply(content, fm)
if err != nil { if err != nil {
t.Fatalf("UpdateFrontMatter() error = %v", err) t.Fatalf("Apply() error = %v", err)
} }
// Parse again and verify // Parse again and verify

View File

@ -236,13 +236,3 @@ func BuildFilename(identifier, signature, title string, keywords []string, ext s
return fmt.Sprintf("%s%s--%s%s%s", identifier, signaturePart, titleSlug, keywordsPart, ext) return fmt.Sprintf("%s%s--%s%s%s", identifier, signaturePart, titleSlug, keywordsPart, ext)
} }
// GenerateNote creates a new note with generated identifier and content.
// Returns (path, content).
func GenerateNote(dir, title, signature string, keywords []string, fileType string) (string, string) {
identifier := GenerateIdentifier()
ext := fileExtensions[fileType]
filename := BuildFilename(identifier, signature, title, keywords, ext)
path := filepath.Join(dir, filename)
content := Generate(title, signature, keywords, fileType, identifier)
return path, content
}

View File

@ -3,7 +3,6 @@ package metadata
import ( import (
"regexp" "regexp"
"slices" "slices"
"strings"
"testing" "testing"
"time" "time"
) )
@ -772,60 +771,3 @@ func TestResultsFromString(t *testing.T) {
}) })
} }
} }
// TestGenerateNote validates note generation (integration test)
func TestGenerateNote(t *testing.T) {
t.Run("without signature", func(t *testing.T) {
dir := "/tmp/notes"
title := "Test Note"
signature := ""
keywords := []string{"test", "example"}
fileType := "md-yaml"
path, content := GenerateNote(dir, title, signature, keywords, fileType)
// Validate path format
if !strings.HasPrefix(path, dir+"/") {
t.Errorf("GenerateNote() path = %q, should start with %q", path, dir+"/")
}
// Validate filename contains identifier and title
if !strings.Contains(path, "--test-note__test_example.md") {
t.Errorf("GenerateNote() path = %q, should contain proper filename", path)
}
// Validate content has front matter
if !strings.Contains(content, "---") {
t.Errorf("GenerateNote() content should contain YAML front matter")
}
if !strings.Contains(content, "title:") {
t.Errorf("GenerateNote() content should contain title field")
}
if !strings.Contains(content, "[test, example]") {
t.Errorf("GenerateNote() content should contain formatted tags")
}
})
t.Run("with signature", func(t *testing.T) {
dir := "/tmp/notes"
title := "Test Note"
signature := "hello"
keywords := []string{"test"}
fileType := "md-yaml"
path, content := GenerateNote(dir, title, signature, keywords, fileType)
// Validate filename contains signature
if !strings.Contains(path, "==hello--test-note__test.md") {
t.Errorf("GenerateNote() path = %q, should contain signature ==hello", path)
}
// Validate content has signature in front matter
if !strings.Contains(content, "signature:") {
t.Errorf("GenerateNote() content should contain signature field")
}
if !strings.Contains(content, "signature: hello") {
t.Errorf("GenerateNote() content should contain signature value")
}
})
}