API cleanup
This commit is contained in:
parent
d81cfa5e67
commit
ab74339821
|
|
@ -1,8 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"denote/pkg/metadata"
|
||||
p9client "denote/internal/p9/client"
|
||||
"denote/pkg/encoding/frontmatter"
|
||||
"denote/pkg/util"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
|
@ -191,7 +192,7 @@ func windowMode(args []string) error {
|
|||
// Update frontmatter if applicable
|
||||
if supportsFrontmatter && len(body) > 0 {
|
||||
// Parse existing frontmatter to preserve Identifier and FileType
|
||||
existing, err := metadata.ParseFrontMatter(string(body), ext)
|
||||
existing, fileType, err := frontmatter.Unmarshal(string(body), ext)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse existing frontmatter: %w", err)
|
||||
}
|
||||
|
|
@ -202,7 +203,7 @@ func windowMode(args []string) error {
|
|||
existing.Signature = signature
|
||||
|
||||
// Apply updated frontmatter to content
|
||||
newContent, err := metadata.Apply(string(body), existing)
|
||||
newContent, err := util.Apply(string(body), existing, fileType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update frontmatter: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
package disk
|
||||
|
||||
import (
|
||||
"denote/pkg/metadata"
|
||||
p9client "denote/internal/p9/client"
|
||||
"denote/pkg/encoding/frontmatter"
|
||||
"denote/pkg/metadata"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
|
@ -33,50 +34,39 @@ func HandleUpdateEvent(f *client.Fsys, identifier, denoteDir string) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Only update frontmatter if file exists (skip if not yet created via Put)
|
||||
if _, err := os.Stat(path); err == nil && SupportsFrontMatter(path) {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
var fileType metadata.FileType
|
||||
switch ext {
|
||||
case ".org":
|
||||
fileType = metadata.FileTypeOrg
|
||||
case ".md":
|
||||
fileType = metadata.FileTypeMdYaml
|
||||
case ".txt":
|
||||
fileType = metadata.FileTypeTxt
|
||||
}
|
||||
|
||||
fm := &metadata.FrontMatter{
|
||||
Title: title,
|
||||
Tags: tags,
|
||||
Identifier: identifier,
|
||||
Signature: signature,
|
||||
FileType: fileType,
|
||||
}
|
||||
|
||||
if err := UpdateFrontMatter(path, fm); err != nil {
|
||||
log.Printf("failed to update front matter for %s: %v", identifier, err)
|
||||
}
|
||||
// Only update if file exists (skip if not yet created via Put)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// After updating content, check if a rename is needed
|
||||
// Only process files with frontmatter
|
||||
if !SupportsFrontMatter(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse existing frontmatter to get FileType and current metadata
|
||||
existing, fileType, err := ExtractFrontMatter(path)
|
||||
if err != nil {
|
||||
log.Printf("failed to extract frontmatter for %s: %v", identifier, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update fields from 9P metadata
|
||||
existing.Title = title
|
||||
existing.Tags = tags
|
||||
existing.Signature = signature
|
||||
|
||||
// Build new filename using updated metadata
|
||||
dir := filepath.Dir(path)
|
||||
if dir == "." {
|
||||
dir = denoteDir
|
||||
}
|
||||
|
||||
// Get original filename and extract extension by removing the denote components
|
||||
originalFilename := filepath.Base(path)
|
||||
// Find where the extension starts (after tags or after title)
|
||||
extStart := strings.Index(originalFilename, ".")
|
||||
if extStart == -1 {
|
||||
extStart = len(originalFilename)
|
||||
}
|
||||
fullExt := originalFilename[extStart:]
|
||||
|
||||
filename := metadata.BuildFilename(identifier, signature, title, tags, fullExt)
|
||||
ext := frontmatter.GetExtension(fileType)
|
||||
filename := metadata.BuildFilename(existing, ext)
|
||||
newPath := filepath.Join(dir, filename)
|
||||
|
||||
// If filename changed, update path in 9P (which might trigger rename)
|
||||
if newPath != path {
|
||||
newPath = filepath.Clean(newPath)
|
||||
absNew, err := filepath.Abs(newPath)
|
||||
|
|
@ -96,6 +86,11 @@ func HandleUpdateEvent(f *client.Fsys, identifier, denoteDir string) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Apply updated frontmatter to file content
|
||||
if err := UpdateFrontMatter(path, existing, fileType); err != nil {
|
||||
log.Printf("failed to update front matter for %s: %v", identifier, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package disk
|
||||
|
||||
import (
|
||||
"denote/pkg/encoding/frontmatter"
|
||||
"denote/pkg/metadata"
|
||||
"denote/pkg/util"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -14,6 +17,39 @@ func SupportsFrontMatter(path string) bool {
|
|||
return ext == ".org" || ext == ".md" || ext == ".txt"
|
||||
}
|
||||
|
||||
// extractTitleFromContent extracts the title from file content.
|
||||
// ext should be the file extension (e.g., ".md", ".org", ".txt").
|
||||
// Returns empty string if no title found or unsupported extension.
|
||||
func extractTitleFromContent(content string, ext string) string {
|
||||
ext = strings.ToLower(ext)
|
||||
if ext != ".org" && ext != ".md" && ext != ".txt" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Try org-mode #+title: first, then fall back to first heading
|
||||
if ext == ".org" {
|
||||
if m := regexp.MustCompile(`(?m)^#\+title:\s*(.+)$`).FindStringSubmatch(content); m != nil {
|
||||
return strings.TrimSpace(m[1])
|
||||
}
|
||||
// Fallback to first heading (lines starting with *)
|
||||
if m := regexp.MustCompile(`(?m)^\*+\s+(.+)$`).FindStringSubmatch(content); m != nil {
|
||||
return strings.TrimSpace(m[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Try markdown YAML front matter title: first, then fall back to # header
|
||||
if ext == ".md" {
|
||||
if m := regexp.MustCompile(`(?ms)^---\n.*?^title:\s*(.+?)$.*?^---`).FindStringSubmatch(content); m != nil {
|
||||
return strings.TrimSpace(strings.Trim(m[1], `"`))
|
||||
}
|
||||
if m := regexp.MustCompile(`(?m)^#\s+(.+)$`).FindStringSubmatch(content); m != nil {
|
||||
return strings.TrimSpace(m[1])
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// ExtractMetadata extracts metadata from a file (combines filename and content parsing).
|
||||
// This is the I/O wrapper around metadata's pure functions.
|
||||
func ExtractMetadata(path string) (*metadata.Metadata, error) {
|
||||
|
|
@ -35,7 +71,7 @@ func ExtractMetadata(path string) (*metadata.Metadata, error) {
|
|||
}
|
||||
|
||||
// Try to extract title from content
|
||||
if title := metadata.ExtractTitleFromContent(string(content), ext); title != "" {
|
||||
if title := extractTitleFromContent(string(content), ext); title != "" {
|
||||
note.Title = title
|
||||
}
|
||||
|
||||
|
|
@ -43,19 +79,20 @@ func ExtractMetadata(path string) (*metadata.Metadata, error) {
|
|||
}
|
||||
|
||||
// ExtractFrontMatter reads a file and parses its front matter.
|
||||
func ExtractFrontMatter(path string) (*metadata.FrontMatter, error) {
|
||||
// Returns the parsed FrontMatter and the detected FileType.
|
||||
func ExtractFrontMatter(path string) (*frontmatter.FrontMatter, frontmatter.FileType, error) {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
return nil, "", fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
return metadata.ParseFrontMatter(string(content), ext)
|
||||
return frontmatter.Unmarshal(string(content), ext)
|
||||
}
|
||||
|
||||
// UpdateFrontMatter updates the front matter in a file.
|
||||
func UpdateFrontMatter(path string, fm *metadata.FrontMatter) error {
|
||||
func UpdateFrontMatter(path string, fm *frontmatter.FrontMatter, fileType frontmatter.FileType) error {
|
||||
// Only update frontmatter for supported file types
|
||||
if !SupportsFrontMatter(path) {
|
||||
return nil
|
||||
|
|
@ -68,7 +105,7 @@ func UpdateFrontMatter(path string, fm *metadata.FrontMatter) error {
|
|||
}
|
||||
|
||||
// Apply front matter to content
|
||||
newContent, err := metadata.Apply(string(content), fm)
|
||||
newContent, err := util.Apply(string(content), fm, fileType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply front matter: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ func registerNewNote(path string) error {
|
|||
// syncFrontMatter reads the file's front matter and writes it to 9P metadata
|
||||
func syncFrontMatter(path, identifier string) error {
|
||||
// Extract front matter from file
|
||||
fm, err := ExtractFrontMatter(path)
|
||||
fm, _, err := ExtractFrontMatter(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -266,7 +266,7 @@ func GetAll() error {
|
|||
// getDenoteFile reloads a text file with front matter from disk to 9P
|
||||
func getDenoteFile(f *client.Fsys, path, identifier string) error {
|
||||
// Extract front matter from file
|
||||
fm, err := ExtractFrontMatter(path)
|
||||
fm, _, err := ExtractFrontMatter(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
11
main.go
11
main.go
|
|
@ -4,6 +4,7 @@ import (
|
|||
"denote/internal/disk"
|
||||
p9client "denote/internal/p9/client"
|
||||
p9server "denote/internal/p9/server"
|
||||
"denote/pkg/encoding/frontmatter"
|
||||
"denote/pkg/metadata"
|
||||
"fmt"
|
||||
"log"
|
||||
|
|
@ -21,7 +22,7 @@ import (
|
|||
|
||||
const (
|
||||
wname = "/Denote/"
|
||||
ftype = metadata.FileTypeMdYaml
|
||||
ftype = frontmatter.FileTypeMdYaml
|
||||
)
|
||||
|
||||
var denoteDir = os.Getenv("HOME") + "/doc"
|
||||
|
|
@ -113,11 +114,11 @@ func handleNewEvent(f *client.Fsys, identifier, denoteDir string) error {
|
|||
}
|
||||
|
||||
newIdentifier := metadata.GenerateIdentifier()
|
||||
ext := metadata.GetExtension(ftype)
|
||||
filename := metadata.BuildFilename(newIdentifier, signature, title, tags, ext)
|
||||
fm := frontmatter.New(title, signature, tags, newIdentifier)
|
||||
ext := frontmatter.GetExtension(ftype)
|
||||
filename := metadata.BuildFilename(fm, ext)
|
||||
path := filepath.Join(targetDir, filename)
|
||||
fm := metadata.NewFrontMatter(title, signature, tags, ftype, newIdentifier)
|
||||
content := string(fm.Bytes())
|
||||
content := string(frontmatter.Marshal(fm, ftype))
|
||||
|
||||
if err := p9client.WriteFile(f, "n/"+identifier+"/path", path); err != nil {
|
||||
return fmt.Errorf("failed to update path in metadata: %w", err)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package metadata
|
||||
package frontmatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -89,35 +89,36 @@ type FrontMatter struct {
|
|||
Tags []string
|
||||
Identifier string
|
||||
Signature string
|
||||
FileType FileType
|
||||
}
|
||||
|
||||
// Bytes returns the formatted frontmatter content as bytes
|
||||
func (fm *FrontMatter) Bytes() []byte {
|
||||
template := templates[fm.FileType]
|
||||
// Marshal returns the formatted frontmatter content as bytes
|
||||
func Marshal(fm *FrontMatter, fileType FileType) []byte {
|
||||
template := templates[fileType]
|
||||
dateStr := time.Now().Format("2006-01-02 Mon 15:04")
|
||||
|
||||
// For org-mode, wrap date in brackets for timestamp
|
||||
if fm.FileType == FileTypeOrg {
|
||||
if fileType == FileTypeOrg {
|
||||
dateStr = "[" + dateStr + "]"
|
||||
}
|
||||
|
||||
keywordsStr := FormatTags(fm.Tags, fm.FileType)
|
||||
keywordsStr := FormatTags(fm.Tags, fileType)
|
||||
content := fmt.Sprintf(template, fm.Title, dateStr, keywordsStr, fm.Identifier, fm.Signature)
|
||||
return []byte(content)
|
||||
}
|
||||
|
||||
// ParseFrontMatter extracts front matter from file content.
|
||||
// Unmarshal extracts front matter from file content.
|
||||
// ext should be the file extension (e.g., ".md", ".org", ".txt").
|
||||
func ParseFrontMatter(content string, ext string) (*FrontMatter, error) {
|
||||
// Returns the parsed frontmatter and the detected FileType.
|
||||
func Unmarshal(content string, ext string) (*FrontMatter, FileType, error) {
|
||||
ext = strings.ToLower(ext)
|
||||
text := content
|
||||
|
||||
fm := &FrontMatter{}
|
||||
var fileType FileType
|
||||
|
||||
switch ext {
|
||||
case ".org":
|
||||
fm.FileType = FileTypeOrg
|
||||
fileType = FileTypeOrg
|
||||
if m := regexp.MustCompile(`(?m)^#\+title:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil {
|
||||
fm.Title = strings.TrimSpace(m[1])
|
||||
}
|
||||
|
|
@ -135,7 +136,7 @@ func ParseFrontMatter(content string, ext string) (*FrontMatter, error) {
|
|||
// Try YAML first
|
||||
yamlRe := regexp.MustCompile(`(?ms)^---\n(.*?)\n---`)
|
||||
if m := yamlRe.FindStringSubmatch(text); m != nil {
|
||||
fm.FileType = FileTypeMdYaml
|
||||
fileType = FileTypeMdYaml
|
||||
yamlContent := m[1]
|
||||
if m := regexp.MustCompile(`(?m)^title:[ \t]*["']?(.+?)["']?$`).FindStringSubmatch(yamlContent); m != nil {
|
||||
fm.Title = strings.TrimSpace(m[1])
|
||||
|
|
@ -157,7 +158,7 @@ func ParseFrontMatter(content string, ext string) (*FrontMatter, error) {
|
|||
// Try TOML
|
||||
tomlRe := regexp.MustCompile(`(?ms)^\+\+\+\n(.*?)\n\+\+\+`)
|
||||
if m := tomlRe.FindStringSubmatch(text); m != nil {
|
||||
fm.FileType = FileTypeMdToml
|
||||
fileType = FileTypeMdToml
|
||||
tomlContent := m[1]
|
||||
if m := regexp.MustCompile(`(?m)^title[ \t]*=[ \t]*["']?(.+?)["']?$`).FindStringSubmatch(tomlContent); m != nil {
|
||||
fm.Title = strings.TrimSpace(m[1])
|
||||
|
|
@ -179,7 +180,7 @@ func ParseFrontMatter(content string, ext string) (*FrontMatter, error) {
|
|||
}
|
||||
|
||||
case ".txt":
|
||||
fm.FileType = FileTypeTxt
|
||||
fileType = FileTypeTxt
|
||||
if m := regexp.MustCompile(`(?m)^title:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil {
|
||||
fm.Title = strings.TrimSpace(m[1])
|
||||
}
|
||||
|
|
@ -194,77 +195,15 @@ func ParseFrontMatter(content string, ext string) (*FrontMatter, error) {
|
|||
}
|
||||
}
|
||||
|
||||
return fm, nil
|
||||
return fm, fileType, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func Apply(originalContent string, fm *FrontMatter) (string, error) {
|
||||
text := originalContent
|
||||
newFrontMatter := string(fm.Bytes())
|
||||
|
||||
var newText string
|
||||
switch fm.FileType {
|
||||
case FileTypeOrg:
|
||||
// Find end of front matter (first blank line or non-#+ line)
|
||||
lines := strings.Split(text, "\n")
|
||||
endIdx := 0
|
||||
for i, line := range lines {
|
||||
if i > 0 && (line == "" || !strings.HasPrefix(line, "#+")) {
|
||||
endIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
// Skip any blank lines after frontmatter since template includes one
|
||||
for endIdx < len(lines) && lines[endIdx] == "" {
|
||||
endIdx++
|
||||
}
|
||||
if endIdx > 0 {
|
||||
newText = newFrontMatter + strings.Join(lines[endIdx:], "\n")
|
||||
} else {
|
||||
newText = newFrontMatter + text
|
||||
}
|
||||
|
||||
case FileTypeMdYaml:
|
||||
// Replace YAML front matter (match trailing blank lines to avoid duplication)
|
||||
re := regexp.MustCompile(`(?s)^---\n.*?\n---\n\n*`)
|
||||
if re.MatchString(text) {
|
||||
newText = re.ReplaceAllString(text, newFrontMatter)
|
||||
} else {
|
||||
newText = newFrontMatter + text
|
||||
}
|
||||
|
||||
case FileTypeMdToml:
|
||||
// Replace TOML front matter (match trailing blank lines to avoid duplication)
|
||||
re := regexp.MustCompile(`(?s)^\+\+\+\n.*?\n\+\+\+\n\n*`)
|
||||
if re.MatchString(text) {
|
||||
newText = re.ReplaceAllString(text, newFrontMatter)
|
||||
} else {
|
||||
newText = newFrontMatter + text
|
||||
}
|
||||
|
||||
case FileTypeTxt:
|
||||
// Replace text front matter (match trailing blank lines to avoid duplication)
|
||||
re := regexp.MustCompile(`(?s)^title:.*?\n-+\n\n*`)
|
||||
if re.MatchString(text) {
|
||||
newText = re.ReplaceAllString(text, newFrontMatter)
|
||||
} else {
|
||||
newText = newFrontMatter + text
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported file type: %s", fm.FileType)
|
||||
}
|
||||
|
||||
return newText, nil
|
||||
}
|
||||
|
||||
// NewFrontMatter creates a new FrontMatter struct from given parameters
|
||||
func NewFrontMatter(title, signature string, tags []string, fileType FileType, identifier string) *FrontMatter {
|
||||
// New creates a new FrontMatter struct from given parameters
|
||||
func New(title, signature string, tags []string, identifier string) *FrontMatter {
|
||||
return &FrontMatter{
|
||||
Title: title,
|
||||
Tags: tags,
|
||||
Identifier: identifier,
|
||||
Signature: signature,
|
||||
FileType: fileType,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,387 @@
|
|||
package frontmatter
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestFormatTags validates tag formatting for different file types
|
||||
// Maps to dt-denote--format-front-matter from original tests
|
||||
func TestFormatTags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tags []string
|
||||
fileType FileType
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "org with multiple tags",
|
||||
tags: []string{"tag1", "tag2"},
|
||||
fileType: FileTypeOrg,
|
||||
want: ":tag1:tag2:",
|
||||
},
|
||||
{
|
||||
name: "org with single tag",
|
||||
tags: []string{"single"},
|
||||
fileType: FileTypeOrg,
|
||||
want: ":single:",
|
||||
},
|
||||
{
|
||||
name: "org with empty tags",
|
||||
tags: []string{},
|
||||
fileType: FileTypeOrg,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "md-yaml with multiple tags",
|
||||
tags: []string{"tag1", "tag2"},
|
||||
fileType: FileTypeMdYaml,
|
||||
want: "[tag1, tag2]",
|
||||
},
|
||||
{
|
||||
name: "md-yaml with single tag",
|
||||
tags: []string{"single"},
|
||||
fileType: FileTypeMdYaml,
|
||||
want: "[single]",
|
||||
},
|
||||
{
|
||||
name: "md-yaml with empty tags",
|
||||
tags: []string{},
|
||||
fileType: FileTypeMdYaml,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "md-toml with multiple tags",
|
||||
tags: []string{"tag1", "tag2"},
|
||||
fileType: FileTypeMdToml,
|
||||
want: "[tag1, tag2]",
|
||||
},
|
||||
{
|
||||
name: "md-toml with empty tags",
|
||||
tags: []string{},
|
||||
fileType: FileTypeMdToml,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "txt with multiple tags",
|
||||
tags: []string{"tag1", "tag2"},
|
||||
fileType: FileTypeTxt,
|
||||
want: "tag1 tag2",
|
||||
},
|
||||
{
|
||||
name: "txt with single tag",
|
||||
tags: []string{"single"},
|
||||
fileType: FileTypeTxt,
|
||||
want: "single",
|
||||
},
|
||||
{
|
||||
name: "txt with empty tags",
|
||||
tags: []string{},
|
||||
fileType: FileTypeTxt,
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := FormatTags(tt.tags, tt.fileType)
|
||||
if got != tt.want {
|
||||
t.Errorf("FormatTags(%v, %q) = %q, want %q", tt.tags, tt.fileType, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFrontMatterBytes validates front matter formatting for all file types
|
||||
// Maps to dt-denote--format-front-matter from original tests
|
||||
func TestFrontMatterBytes(t *testing.T) {
|
||||
identifier := "20240101T120000"
|
||||
title := "Test Note"
|
||||
tags := []string{"tag1", "tag2"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fileType FileType
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
name: "org format",
|
||||
fileType: FileTypeOrg,
|
||||
wantContains: []string{
|
||||
"#+title: Test Note",
|
||||
"#+filetags: :tag1:tag2:",
|
||||
"#+identifier: 20240101T120000",
|
||||
"#+date:",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "md-yaml format",
|
||||
fileType: FileTypeMdYaml,
|
||||
wantContains: []string{
|
||||
"---",
|
||||
"title: Test Note",
|
||||
"tags: [tag1, tag2]",
|
||||
"identifier: 20240101T120000",
|
||||
"date:",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "md-toml format",
|
||||
fileType: FileTypeMdToml,
|
||||
wantContains: []string{
|
||||
"+++",
|
||||
"title = Test Note",
|
||||
"tags = [tag1, tag2]",
|
||||
"identifier = 20240101T120000",
|
||||
"date =",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "txt format",
|
||||
fileType: FileTypeTxt,
|
||||
wantContains: []string{
|
||||
"title: Test Note",
|
||||
"tags: tag1 tag2",
|
||||
"identifier: 20240101T120000",
|
||||
"date:",
|
||||
"---------------------------",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fm := New(title, "", tags, identifier)
|
||||
got := string(Marshal(fm, tt.fileType))
|
||||
|
||||
for _, want := range tt.wantContains {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("FrontMatter.Bytes() missing %q\nGot:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
for _, notWant := range tt.wantNotContains {
|
||||
if strings.Contains(got, notWant) {
|
||||
t.Errorf("FrontMatter.Bytes() should not contain %q\nGot:\n%s", notWant, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFrontMatterBytesEmptyTags validates front matter formatting with no tags
|
||||
func TestFrontMatterBytesEmptyTags(t *testing.T) {
|
||||
identifier := "20240101T120000"
|
||||
title := "Test Note"
|
||||
tags := []string{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fileType FileType
|
||||
wantTags string
|
||||
}{
|
||||
{
|
||||
name: "org with empty tags",
|
||||
fileType: FileTypeOrg,
|
||||
wantTags: "#+filetags:",
|
||||
},
|
||||
{
|
||||
name: "md-yaml with empty tags",
|
||||
fileType: FileTypeMdYaml,
|
||||
wantTags: "tags:",
|
||||
},
|
||||
{
|
||||
name: "md-toml with empty tags",
|
||||
fileType: FileTypeMdToml,
|
||||
wantTags: "tags =",
|
||||
},
|
||||
{
|
||||
name: "txt with empty tags",
|
||||
fileType: FileTypeTxt,
|
||||
wantTags: "tags:",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fm := New(title, "", tags, identifier)
|
||||
got := string(Marshal(fm, tt.fileType))
|
||||
|
||||
if !strings.Contains(got, tt.wantTags) {
|
||||
t.Errorf("FrontMatter.Bytes() with empty tags should contain %q\nGot:\n%s",
|
||||
tt.wantTags, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnmarshal validates front matter parsing from content
|
||||
func TestUnmarshal(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
ext string
|
||||
wantTitle string
|
||||
wantTags []string
|
||||
wantIdentifier string
|
||||
wantFileType FileType
|
||||
}{
|
||||
{
|
||||
name: "org format",
|
||||
content: `#+title: Org Note
|
||||
#+date: [2024-01-01 Mon 12:00]
|
||||
#+filetags: :work:emacs:
|
||||
#+identifier: 20240101T120000
|
||||
|
||||
* First Heading`,
|
||||
ext: ".org",
|
||||
wantTitle: "Org Note",
|
||||
wantTags: []string{"work", "emacs"},
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeOrg,
|
||||
},
|
||||
{
|
||||
name: "org with single tag",
|
||||
content: `#+title: Single Tag
|
||||
#+filetags: :single:
|
||||
#+identifier: 20240101T120000`,
|
||||
ext: ".org",
|
||||
wantTitle: "Single Tag",
|
||||
wantTags: []string{"single"},
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeOrg,
|
||||
},
|
||||
{
|
||||
name: "org without tags",
|
||||
content: `#+title: No Tags
|
||||
#+identifier: 20240101T120000`,
|
||||
ext: ".org",
|
||||
wantTitle: "No Tags",
|
||||
wantTags: nil,
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeOrg,
|
||||
},
|
||||
{
|
||||
name: "markdown yaml",
|
||||
content: `---
|
||||
title: Markdown Note
|
||||
date: 2024-01-01
|
||||
tags: [work, personal]
|
||||
identifier: 20240101T120000
|
||||
---
|
||||
|
||||
# Content`,
|
||||
ext: ".md",
|
||||
wantTitle: "Markdown Note",
|
||||
wantTags: []string{"work", "personal"},
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeMdYaml,
|
||||
},
|
||||
{
|
||||
name: "markdown yaml with quoted title",
|
||||
content: `---
|
||||
title: "Quoted Title"
|
||||
tags: [test]
|
||||
identifier: 20240101T120000
|
||||
---`,
|
||||
ext: ".md",
|
||||
wantTitle: "Quoted Title",
|
||||
wantTags: []string{"test"},
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeMdYaml,
|
||||
},
|
||||
{
|
||||
name: "markdown toml",
|
||||
content: `+++
|
||||
title = TOML Note
|
||||
date = 2024-01-01
|
||||
tags = [rust, go]
|
||||
identifier = 20240101T120000
|
||||
+++
|
||||
|
||||
Content`,
|
||||
ext: ".md",
|
||||
wantTitle: "TOML Note",
|
||||
wantTags: []string{"rust", "go"},
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeMdToml,
|
||||
},
|
||||
{
|
||||
name: "txt format",
|
||||
content: `title: Plain Text
|
||||
date: 2024-01-01
|
||||
tags: simple plain
|
||||
identifier: 20240101T120000
|
||||
---------------------------
|
||||
|
||||
Content here`,
|
||||
ext: ".txt",
|
||||
wantTitle: "Plain Text",
|
||||
wantTags: []string{"simple", "plain"},
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeTxt,
|
||||
},
|
||||
{
|
||||
name: "txt without tags",
|
||||
content: `title: No Tags Text
|
||||
identifier: 20240101T120000
|
||||
---------------------------`,
|
||||
ext: ".txt",
|
||||
wantTitle: "No Tags Text",
|
||||
wantTags: nil,
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeTxt,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, fileType, err := Unmarshal(tt.content, tt.ext)
|
||||
if err != nil {
|
||||
t.Fatalf("Unmarshal() error = %v", err)
|
||||
}
|
||||
|
||||
if got.Title != tt.wantTitle {
|
||||
t.Errorf("Unmarshal().Title = %q, want %q", got.Title, tt.wantTitle)
|
||||
}
|
||||
|
||||
if !slices.Equal(got.Tags, tt.wantTags) {
|
||||
t.Errorf("Unmarshal().Tags = %v, want %v", got.Tags, tt.wantTags)
|
||||
}
|
||||
|
||||
if got.Identifier != tt.wantIdentifier {
|
||||
t.Errorf("Unmarshal().Identifier = %q, want %q", got.Identifier, tt.wantIdentifier)
|
||||
}
|
||||
|
||||
if fileType != tt.wantFileType {
|
||||
t.Errorf("Unmarshal() fileType = %q, want %q", fileType, tt.wantFileType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnmarshalMissingFields validates handling of missing fields
|
||||
func TestUnmarshalMissingFields(t *testing.T) {
|
||||
content := `---
|
||||
date: 2024-01-01
|
||||
---`
|
||||
|
||||
got, _, err := Unmarshal(content, ".md")
|
||||
if err != nil {
|
||||
t.Fatalf("Unmarshal() error = %v", err)
|
||||
}
|
||||
|
||||
// Should have empty values, not error
|
||||
if got.Title != "" {
|
||||
t.Errorf("Unmarshal().Title = %q, want empty", got.Title)
|
||||
}
|
||||
if got.Identifier != "" {
|
||||
t.Errorf("Unmarshal().Identifier = %q, want empty", got.Identifier)
|
||||
}
|
||||
if got.Tags != nil {
|
||||
t.Errorf("Unmarshal().Tags = %v, want nil", got.Tags)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,679 +0,0 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestFormatTags validates tag formatting for different file types
|
||||
// Maps to dt-denote--format-front-matter from original tests
|
||||
func TestFormatTags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tags []string
|
||||
fileType FileType
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "org with multiple tags",
|
||||
tags: []string{"tag1", "tag2"},
|
||||
fileType: FileTypeOrg,
|
||||
want: ":tag1:tag2:",
|
||||
},
|
||||
{
|
||||
name: "org with single tag",
|
||||
tags: []string{"single"},
|
||||
fileType: FileTypeOrg,
|
||||
want: ":single:",
|
||||
},
|
||||
{
|
||||
name: "org with empty tags",
|
||||
tags: []string{},
|
||||
fileType: FileTypeOrg,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "md-yaml with multiple tags",
|
||||
tags: []string{"tag1", "tag2"},
|
||||
fileType: FileTypeMdYaml,
|
||||
want: "[tag1, tag2]",
|
||||
},
|
||||
{
|
||||
name: "md-yaml with single tag",
|
||||
tags: []string{"single"},
|
||||
fileType: FileTypeMdYaml,
|
||||
want: "[single]",
|
||||
},
|
||||
{
|
||||
name: "md-yaml with empty tags",
|
||||
tags: []string{},
|
||||
fileType: FileTypeMdYaml,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "md-toml with multiple tags",
|
||||
tags: []string{"tag1", "tag2"},
|
||||
fileType: FileTypeMdToml,
|
||||
want: "[tag1, tag2]",
|
||||
},
|
||||
{
|
||||
name: "md-toml with empty tags",
|
||||
tags: []string{},
|
||||
fileType: FileTypeMdToml,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "txt with multiple tags",
|
||||
tags: []string{"tag1", "tag2"},
|
||||
fileType: FileTypeTxt,
|
||||
want: "tag1 tag2",
|
||||
},
|
||||
{
|
||||
name: "txt with single tag",
|
||||
tags: []string{"single"},
|
||||
fileType: FileTypeTxt,
|
||||
want: "single",
|
||||
},
|
||||
{
|
||||
name: "txt with empty tags",
|
||||
tags: []string{},
|
||||
fileType: FileTypeTxt,
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := FormatTags(tt.tags, tt.fileType)
|
||||
if got != tt.want {
|
||||
t.Errorf("FormatTags(%v, %q) = %q, want %q", tt.tags, tt.fileType, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFrontMatterBytes validates front matter formatting for all file types
|
||||
// Maps to dt-denote--format-front-matter from original tests
|
||||
func TestFrontMatterBytes(t *testing.T) {
|
||||
identifier := "20240101T120000"
|
||||
title := "Test Note"
|
||||
tags := []string{"tag1", "tag2"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fileType FileType
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
name: "org format",
|
||||
fileType: FileTypeOrg,
|
||||
wantContains: []string{
|
||||
"#+title: Test Note",
|
||||
"#+filetags: :tag1:tag2:",
|
||||
"#+identifier: 20240101T120000",
|
||||
"#+date:",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "md-yaml format",
|
||||
fileType: FileTypeMdYaml,
|
||||
wantContains: []string{
|
||||
"---",
|
||||
"title: Test Note",
|
||||
"tags: [tag1, tag2]",
|
||||
"identifier: 20240101T120000",
|
||||
"date:",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "md-toml format",
|
||||
fileType: FileTypeMdToml,
|
||||
wantContains: []string{
|
||||
"+++",
|
||||
"title = Test Note",
|
||||
"tags = [tag1, tag2]",
|
||||
"identifier = 20240101T120000",
|
||||
"date =",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "txt format",
|
||||
fileType: FileTypeTxt,
|
||||
wantContains: []string{
|
||||
"title: Test Note",
|
||||
"tags: tag1 tag2",
|
||||
"identifier: 20240101T120000",
|
||||
"date:",
|
||||
"---------------------------",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fm := NewFrontMatter(title, "", tags, tt.fileType, identifier)
|
||||
got := string(fm.Bytes())
|
||||
|
||||
for _, want := range tt.wantContains {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("FrontMatter.Bytes() missing %q\nGot:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
for _, notWant := range tt.wantNotContains {
|
||||
if strings.Contains(got, notWant) {
|
||||
t.Errorf("FrontMatter.Bytes() should not contain %q\nGot:\n%s", notWant, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFrontMatterBytesEmptyTags validates front matter formatting with no tags
|
||||
func TestFrontMatterBytesEmptyTags(t *testing.T) {
|
||||
identifier := "20240101T120000"
|
||||
title := "Test Note"
|
||||
tags := []string{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fileType FileType
|
||||
wantTags string
|
||||
}{
|
||||
{
|
||||
name: "org with empty tags",
|
||||
fileType: FileTypeOrg,
|
||||
wantTags: "#+filetags:",
|
||||
},
|
||||
{
|
||||
name: "md-yaml with empty tags",
|
||||
fileType: FileTypeMdYaml,
|
||||
wantTags: "tags:",
|
||||
},
|
||||
{
|
||||
name: "md-toml with empty tags",
|
||||
fileType: FileTypeMdToml,
|
||||
wantTags: "tags =",
|
||||
},
|
||||
{
|
||||
name: "txt with empty tags",
|
||||
fileType: FileTypeTxt,
|
||||
wantTags: "tags:",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fm := NewFrontMatter(title, "", tags, tt.fileType, identifier)
|
||||
got := string(fm.Bytes())
|
||||
|
||||
if !strings.Contains(got, tt.wantTags) {
|
||||
t.Errorf("FrontMatter.Bytes() with empty tags should contain %q\nGot:\n%s",
|
||||
tt.wantTags, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseFrontMatter validates front matter parsing from content
|
||||
func TestParseFrontMatter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
ext string
|
||||
wantTitle string
|
||||
wantTags []string
|
||||
wantIdentifier string
|
||||
wantFileType FileType
|
||||
}{
|
||||
{
|
||||
name: "org format",
|
||||
content: `#+title: Org Note
|
||||
#+date: [2024-01-01 Mon 12:00]
|
||||
#+filetags: :work:emacs:
|
||||
#+identifier: 20240101T120000
|
||||
|
||||
* First Heading`,
|
||||
ext: ".org",
|
||||
wantTitle: "Org Note",
|
||||
wantTags: []string{"work", "emacs"},
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeOrg,
|
||||
},
|
||||
{
|
||||
name: "org with single tag",
|
||||
content: `#+title: Single Tag
|
||||
#+filetags: :single:
|
||||
#+identifier: 20240101T120000`,
|
||||
ext: ".org",
|
||||
wantTitle: "Single Tag",
|
||||
wantTags: []string{"single"},
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeOrg,
|
||||
},
|
||||
{
|
||||
name: "org without tags",
|
||||
content: `#+title: No Tags
|
||||
#+identifier: 20240101T120000`,
|
||||
ext: ".org",
|
||||
wantTitle: "No Tags",
|
||||
wantTags: nil,
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeOrg,
|
||||
},
|
||||
{
|
||||
name: "markdown yaml",
|
||||
content: `---
|
||||
title: Markdown Note
|
||||
date: 2024-01-01
|
||||
tags: [work, personal]
|
||||
identifier: 20240101T120000
|
||||
---
|
||||
|
||||
# Content`,
|
||||
ext: ".md",
|
||||
wantTitle: "Markdown Note",
|
||||
wantTags: []string{"work", "personal"},
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeMdYaml,
|
||||
},
|
||||
{
|
||||
name: "markdown yaml with quoted title",
|
||||
content: `---
|
||||
title: "Quoted Title"
|
||||
tags: [test]
|
||||
identifier: 20240101T120000
|
||||
---`,
|
||||
ext: ".md",
|
||||
wantTitle: "Quoted Title",
|
||||
wantTags: []string{"test"},
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeMdYaml,
|
||||
},
|
||||
{
|
||||
name: "markdown toml",
|
||||
content: `+++
|
||||
title = TOML Note
|
||||
date = 2024-01-01
|
||||
tags = [rust, go]
|
||||
identifier = 20240101T120000
|
||||
+++
|
||||
|
||||
Content`,
|
||||
ext: ".md",
|
||||
wantTitle: "TOML Note",
|
||||
wantTags: []string{"rust", "go"},
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeMdToml,
|
||||
},
|
||||
{
|
||||
name: "txt format",
|
||||
content: `title: Plain Text
|
||||
date: 2024-01-01
|
||||
tags: simple plain
|
||||
identifier: 20240101T120000
|
||||
---------------------------
|
||||
|
||||
Content here`,
|
||||
ext: ".txt",
|
||||
wantTitle: "Plain Text",
|
||||
wantTags: []string{"simple", "plain"},
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeTxt,
|
||||
},
|
||||
{
|
||||
name: "txt without tags",
|
||||
content: `title: No Tags Text
|
||||
identifier: 20240101T120000
|
||||
---------------------------`,
|
||||
ext: ".txt",
|
||||
wantTitle: "No Tags Text",
|
||||
wantTags: nil,
|
||||
wantIdentifier: "20240101T120000",
|
||||
wantFileType: FileTypeTxt,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseFrontMatter(tt.content, tt.ext)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseFrontMatter() error = %v", err)
|
||||
}
|
||||
|
||||
if got.Title != tt.wantTitle {
|
||||
t.Errorf("ParseFrontMatter().Title = %q, want %q", got.Title, tt.wantTitle)
|
||||
}
|
||||
|
||||
if !slices.Equal(got.Tags, tt.wantTags) {
|
||||
t.Errorf("ParseFrontMatter().Tags = %v, want %v", got.Tags, tt.wantTags)
|
||||
}
|
||||
|
||||
if got.Identifier != tt.wantIdentifier {
|
||||
t.Errorf("ParseFrontMatter().Identifier = %q, want %q", got.Identifier, tt.wantIdentifier)
|
||||
}
|
||||
|
||||
if got.FileType != tt.wantFileType {
|
||||
t.Errorf("ParseFrontMatter().FileType = %q, want %q", got.FileType, tt.wantFileType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseFrontMatterMissingFields validates handling of missing fields
|
||||
func TestParseFrontMatterMissingFields(t *testing.T) {
|
||||
content := `---
|
||||
date: 2024-01-01
|
||||
---`
|
||||
|
||||
got, err := ParseFrontMatter(content, ".md")
|
||||
if err != nil {
|
||||
t.Fatalf("ParseFrontMatter() error = %v", err)
|
||||
}
|
||||
|
||||
// Should have empty values, not error
|
||||
if got.Title != "" {
|
||||
t.Errorf("ParseFrontMatter().Title = %q, want empty", got.Title)
|
||||
}
|
||||
if got.Identifier != "" {
|
||||
t.Errorf("ParseFrontMatter().Identifier = %q, want empty", got.Identifier)
|
||||
}
|
||||
if got.Tags != nil {
|
||||
t.Errorf("ParseFrontMatter().Tags = %v, want nil", got.Tags)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApply validates front matter updates
|
||||
func TestApply(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
original string
|
||||
fm *FrontMatter
|
||||
wantContains []string
|
||||
wantPreserve string
|
||||
}{
|
||||
{
|
||||
name: "update org front matter",
|
||||
original: `#+title: Old Title
|
||||
#+date: [2024-01-01 Mon 12:00]
|
||||
#+filetags: :old:
|
||||
#+identifier: 20240101T120000
|
||||
|
||||
* Original Content
|
||||
This should be preserved`,
|
||||
fm: &FrontMatter{
|
||||
Title: "New Title",
|
||||
Tags: []string{"new", "updated"},
|
||||
Identifier: "20240101T120000",
|
||||
FileType: FileTypeOrg,
|
||||
},
|
||||
wantContains: []string{
|
||||
"#+title: New Title",
|
||||
"#+filetags: :new:updated:",
|
||||
"* Original Content",
|
||||
"This should be preserved",
|
||||
},
|
||||
wantPreserve: "* Original Content",
|
||||
},
|
||||
{
|
||||
name: "update markdown yaml front matter",
|
||||
original: `---
|
||||
title: Old Title
|
||||
tags: [old]
|
||||
identifier: 20240101T120000
|
||||
---
|
||||
|
||||
# Original Heading
|
||||
Content preserved`,
|
||||
fm: &FrontMatter{
|
||||
Title: "New Title",
|
||||
Tags: []string{"new"},
|
||||
Identifier: "20240101T120000",
|
||||
FileType: FileTypeMdYaml,
|
||||
},
|
||||
wantContains: []string{
|
||||
"---",
|
||||
"title: New Title",
|
||||
"tags: [new]",
|
||||
"# Original Heading",
|
||||
"Content preserved",
|
||||
},
|
||||
wantPreserve: "# Original Heading",
|
||||
},
|
||||
{
|
||||
name: "update markdown toml front matter",
|
||||
original: `+++
|
||||
title = Old Title
|
||||
tags = [old]
|
||||
identifier = 20240101T120000
|
||||
+++
|
||||
|
||||
Content here`,
|
||||
fm: &FrontMatter{
|
||||
Title: "New Title",
|
||||
Tags: []string{"updated"},
|
||||
Identifier: "20240101T120000",
|
||||
FileType: FileTypeMdToml,
|
||||
},
|
||||
wantContains: []string{
|
||||
"+++",
|
||||
"title = New Title",
|
||||
"tags = [updated]",
|
||||
"Content here",
|
||||
},
|
||||
wantPreserve: "Content here",
|
||||
},
|
||||
{
|
||||
name: "update txt front matter",
|
||||
original: `title: Old Title
|
||||
tags: old
|
||||
identifier: 20240101T120000
|
||||
---------------------------
|
||||
|
||||
Text content`,
|
||||
fm: &FrontMatter{
|
||||
Title: "New Title",
|
||||
Tags: []string{"new"},
|
||||
Identifier: "20240101T120000",
|
||||
FileType: FileTypeTxt,
|
||||
},
|
||||
wantContains: []string{
|
||||
"title: New Title",
|
||||
"tags: new",
|
||||
"---------------------------",
|
||||
"Text content",
|
||||
},
|
||||
wantPreserve: "Text content",
|
||||
},
|
||||
{
|
||||
name: "add front matter when missing (org)",
|
||||
original: `* Original Heading`,
|
||||
fm: &FrontMatter{
|
||||
Title: "Added Title",
|
||||
Tags: []string{"added"},
|
||||
Identifier: "20240101T120000",
|
||||
FileType: FileTypeOrg,
|
||||
},
|
||||
wantContains: []string{
|
||||
"#+title: Added Title",
|
||||
"#+filetags: :added:",
|
||||
"* Original Heading",
|
||||
},
|
||||
wantPreserve: "* Original Heading",
|
||||
},
|
||||
{
|
||||
name: "add front matter when missing (md-yaml)",
|
||||
original: `# Original Heading`,
|
||||
fm: &FrontMatter{
|
||||
Title: "Added Title",
|
||||
Tags: []string{"added"},
|
||||
Identifier: "20240101T120000",
|
||||
FileType: FileTypeMdYaml,
|
||||
},
|
||||
wantContains: []string{
|
||||
"---",
|
||||
"title: Added Title",
|
||||
"tags: [added]",
|
||||
"# Original Heading",
|
||||
},
|
||||
wantPreserve: "# Original Heading",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Apply(tt.original, tt.fm)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply() error = %v", err)
|
||||
}
|
||||
|
||||
for _, want := range tt.wantContains {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("Apply() missing %q\nGot:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify original content is preserved
|
||||
if !strings.Contains(got, tt.wantPreserve) {
|
||||
t.Errorf("Apply() didn't preserve %q\nGot:\n%s", tt.wantPreserve, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyEmptyTags validates updating with empty tags
|
||||
func TestApplyEmptyTags(t *testing.T) {
|
||||
original := `---
|
||||
title: Test
|
||||
tags: [old, tags]
|
||||
identifier: 20240101T120000
|
||||
---
|
||||
|
||||
Content`
|
||||
|
||||
fm := &FrontMatter{
|
||||
Title: "Test",
|
||||
Tags: []string{},
|
||||
Identifier: "20240101T120000",
|
||||
FileType: FileTypeMdYaml,
|
||||
}
|
||||
|
||||
got, err := Apply(original, fm)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply() error = %v", err)
|
||||
}
|
||||
|
||||
// Should have empty tags field, not omit it
|
||||
if !strings.Contains(got, "tags:") {
|
||||
t.Errorf("Apply() should include tags field even when empty")
|
||||
}
|
||||
|
||||
// Content should be preserved
|
||||
if !strings.Contains(got, "Content") {
|
||||
t.Errorf("Apply() should preserve content")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyUnsupportedType validates error handling
|
||||
func TestApplyUnsupportedType(t *testing.T) {
|
||||
fm := &FrontMatter{
|
||||
Title: "Test",
|
||||
Tags: []string{"test"},
|
||||
Identifier: "20240101T120000",
|
||||
FileType: "unsupported",
|
||||
}
|
||||
|
||||
_, err := Apply("content", fm)
|
||||
if err == nil {
|
||||
t.Error("Apply() should error on unsupported file type")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "unsupported file type") {
|
||||
t.Errorf("Apply() error = %v, want 'unsupported file type'", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetExtension validates file extension mapping
|
||||
func TestGetExtension(t *testing.T) {
|
||||
tests := []struct {
|
||||
fileType FileType
|
||||
want string
|
||||
}{
|
||||
{FileTypeOrg, ".org"},
|
||||
{FileTypeMdYaml, ".md"},
|
||||
{FileTypeMdToml, ".md"},
|
||||
{FileTypeTxt, ".txt"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.fileType), func(t *testing.T) {
|
||||
got := GetExtension(tt.fileType)
|
||||
if got != tt.want {
|
||||
t.Errorf("GetExtension(%q) = %q, want %q", tt.fileType, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFrontMatterRoundTrip validates generate -> parse -> update cycle
|
||||
func TestFrontMatterRoundTrip(t *testing.T) {
|
||||
fileTypes := []FileType{FileTypeOrg, FileTypeMdYaml, FileTypeMdToml, FileTypeTxt}
|
||||
title := "Test Note"
|
||||
tags := []string{"tag1", "tag2"}
|
||||
identifier := "20240101T120000"
|
||||
|
||||
for _, fileType := range fileTypes {
|
||||
t.Run(string(fileType), func(t *testing.T) {
|
||||
// Generate front matter
|
||||
fm := NewFrontMatter(title, "", tags, fileType, identifier)
|
||||
content := string(fm.Bytes())
|
||||
|
||||
// Parse it back
|
||||
ext := GetExtension(fileType)
|
||||
fm, err := ParseFrontMatter(content, ext)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseFrontMatter() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify parsed values
|
||||
if fm.Title != title {
|
||||
t.Errorf("Parsed title = %q, want %q", fm.Title, title)
|
||||
}
|
||||
if !slices.Equal(fm.Tags, tags) {
|
||||
t.Errorf("Parsed tags = %v, want %v", fm.Tags, tags)
|
||||
}
|
||||
if fm.Identifier != identifier {
|
||||
t.Errorf("Parsed identifier = %q, want %q", fm.Identifier, identifier)
|
||||
}
|
||||
if fm.FileType != fileType {
|
||||
t.Errorf("Parsed fileType = %q, want %q", fm.FileType, fileType)
|
||||
}
|
||||
|
||||
// Update with new values
|
||||
newTitle := "Updated Title"
|
||||
newTags := []string{"new"}
|
||||
fm.Title = newTitle
|
||||
fm.Tags = newTags
|
||||
|
||||
updated, err := Apply(content, fm)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply() error = %v", err)
|
||||
}
|
||||
|
||||
// Parse again and verify
|
||||
fm2, err := ParseFrontMatter(updated, ext)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseFrontMatter() after update error = %v", err)
|
||||
}
|
||||
|
||||
if fm2.Title != newTitle {
|
||||
t.Errorf("After update title = %q, want %q", fm2.Title, newTitle)
|
||||
}
|
||||
if !slices.Equal(fm2.Tags, newTags) {
|
||||
t.Errorf("After update tags = %v, want %v", fm2.Tags, newTags)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"denote/pkg/encoding/frontmatter"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
|
@ -123,7 +124,6 @@ func Sort(md Results, sortType SortBy, order SortOrder) {
|
|||
|
||||
// ParseFilename extracts Denote metadata from a filename only (no file I/O).
|
||||
// Returns metadata with Path, Identifier, Signature, Title (from filename), and Tags.
|
||||
// Caller should use ExtractTitleFromContent to get title from file content.
|
||||
func ParseFilename(path string) *Metadata {
|
||||
fname := filepath.Base(path)
|
||||
note := &Metadata{Path: path}
|
||||
|
|
@ -149,39 +149,6 @@ func ParseFilename(path string) *Metadata {
|
|||
return note
|
||||
}
|
||||
|
||||
// ExtractTitleFromContent extracts the title from file content.
|
||||
// ext should be the file extension (e.g., ".md", ".org", ".txt").
|
||||
// Returns empty string if no title found or unsupported extension.
|
||||
func ExtractTitleFromContent(content string, ext string) string {
|
||||
ext = strings.ToLower(ext)
|
||||
if ext != ".org" && ext != ".md" && ext != ".txt" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Try org-mode #+title: first, then fall back to first heading
|
||||
if ext == ".org" {
|
||||
if m := regexp.MustCompile(`(?m)^#\+title:\s*(.+)$`).FindStringSubmatch(content); m != nil {
|
||||
return strings.TrimSpace(m[1])
|
||||
}
|
||||
// Fallback to first heading (lines starting with *)
|
||||
if m := regexp.MustCompile(`(?m)^\*+\s+(.+)$`).FindStringSubmatch(content); m != nil {
|
||||
return strings.TrimSpace(m[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Try markdown YAML front matter title: first, then fall back to # header
|
||||
if ext == ".md" {
|
||||
if m := regexp.MustCompile(`(?ms)^---\n.*?^title:\s*(.+?)$.*?^---`).FindStringSubmatch(content); m != nil {
|
||||
return strings.TrimSpace(strings.Trim(m[1], `"`))
|
||||
}
|
||||
if m := regexp.MustCompile(`(?m)^#\s+(.+)$`).FindStringSubmatch(content); m != nil {
|
||||
return strings.TrimSpace(m[1])
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GenerateIdentifier creates a new identifier timestamp.
|
||||
func GenerateIdentifier() string {
|
||||
return time.Now().Format("20060102T150405")
|
||||
|
|
@ -229,10 +196,9 @@ func formatSignature(sig string) string {
|
|||
}
|
||||
|
||||
// BuildFilename constructs a denote filename from metadata components.
|
||||
func BuildFilename(identifier, signature, title string, keywords []string, ext string) string {
|
||||
titleSlug := slugifyTitle(title)
|
||||
signaturePart := formatSignature(signature)
|
||||
keywordsPart := formatKeywords(keywords)
|
||||
return fmt.Sprintf("%s%s--%s%s%s", identifier, signaturePart, titleSlug, keywordsPart, ext)
|
||||
func BuildFilename(fm *frontmatter.FrontMatter, ext string) string {
|
||||
titleSlug := slugifyTitle(fm.Title)
|
||||
signaturePart := formatSignature(fm.Signature)
|
||||
keywordsPart := formatKeywords(fm.Tags)
|
||||
return fmt.Sprintf("%s%s--%s%s%s", fm.Identifier, signaturePart, titleSlug, keywordsPart, ext)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"denote/pkg/encoding/frontmatter"
|
||||
"regexp"
|
||||
"slices"
|
||||
"testing"
|
||||
|
|
@ -215,6 +216,7 @@ func TestBuildFilename(t *testing.T) {
|
|||
keywords []string
|
||||
ext string
|
||||
want string
|
||||
ftype frontmatter.FileType
|
||||
}{
|
||||
{
|
||||
name: "complete filename with keywords",
|
||||
|
|
@ -224,6 +226,7 @@ func TestBuildFilename(t *testing.T) {
|
|||
keywords: []string{"tag1", "tag2"},
|
||||
ext: ".md",
|
||||
want: "20231225T120000--my-title__tag1_tag2.md",
|
||||
ftype: frontmatter.FileTypeMdYaml,
|
||||
},
|
||||
{
|
||||
name: "filename without keywords",
|
||||
|
|
@ -233,6 +236,7 @@ func TestBuildFilename(t *testing.T) {
|
|||
keywords: []string{},
|
||||
ext: ".md",
|
||||
want: "20231225T120000--my-title.md",
|
||||
ftype: frontmatter.FileTypeMdYaml,
|
||||
},
|
||||
{
|
||||
name: "filename with signature",
|
||||
|
|
@ -242,6 +246,7 @@ func TestBuildFilename(t *testing.T) {
|
|||
keywords: []string{"tag1"},
|
||||
ext: ".md",
|
||||
want: "20231225T120000==hello--my-title__tag1.md",
|
||||
ftype: frontmatter.FileTypeMdYaml,
|
||||
},
|
||||
{
|
||||
name: "filename with signature and no keywords",
|
||||
|
|
@ -251,6 +256,7 @@ func TestBuildFilename(t *testing.T) {
|
|||
keywords: []string{},
|
||||
ext: ".md",
|
||||
want: "20231225T120000==test--my-title.md",
|
||||
ftype: frontmatter.FileTypeMdYaml,
|
||||
},
|
||||
{
|
||||
name: "filename with multi-part signature",
|
||||
|
|
@ -260,6 +266,7 @@ func TestBuildFilename(t *testing.T) {
|
|||
keywords: []string{"work"},
|
||||
ext: ".md",
|
||||
want: "20231225T120000==a==b--my-title__work.md",
|
||||
ftype: frontmatter.FileTypeMdYaml,
|
||||
},
|
||||
{
|
||||
name: "filename with special chars in title",
|
||||
|
|
@ -269,6 +276,7 @@ func TestBuildFilename(t *testing.T) {
|
|||
keywords: []string{"work"},
|
||||
ext: ".org",
|
||||
want: "20231225T120000--special-title__work.org",
|
||||
ftype: frontmatter.FileTypeOrg,
|
||||
},
|
||||
{
|
||||
name: "org format",
|
||||
|
|
@ -278,6 +286,7 @@ func TestBuildFilename(t *testing.T) {
|
|||
keywords: []string{"emacs"},
|
||||
ext: ".org",
|
||||
want: "20240101T000000--org-note__emacs.org",
|
||||
ftype: frontmatter.FileTypeOrg,
|
||||
},
|
||||
{
|
||||
name: "txt format",
|
||||
|
|
@ -287,12 +296,14 @@ func TestBuildFilename(t *testing.T) {
|
|||
keywords: []string{},
|
||||
ext: ".txt",
|
||||
want: "20240101T000000--plain-text.txt",
|
||||
ftype: frontmatter.FileTypeTxt,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := BuildFilename(tt.identifier, tt.signature, tt.title, tt.keywords, tt.ext)
|
||||
fm := frontmatter.New(tt.title, tt.signature, tt.keywords, tt.identifier)
|
||||
got := BuildFilename(fm, tt.ext)
|
||||
if got != tt.want {
|
||||
t.Errorf("BuildFilename() = %q, want %q", got, tt.want)
|
||||
}
|
||||
|
|
@ -409,103 +420,6 @@ func TestParseFilename(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestExtractTitleFromContent validates title extraction from file content
|
||||
func TestExtractTitleFromContent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
ext string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "org mode with title directive",
|
||||
content: `#+title: My Org Note
|
||||
#+date: [2024-01-01]
|
||||
|
||||
* First Heading`,
|
||||
ext: ".org",
|
||||
want: "My Org Note",
|
||||
},
|
||||
{
|
||||
name: "org mode fallback to heading",
|
||||
content: `#+date: [2024-01-01]
|
||||
|
||||
* Heading Title
|
||||
|
||||
Some content`,
|
||||
ext: ".org",
|
||||
want: "Heading Title",
|
||||
},
|
||||
{
|
||||
name: "markdown yaml front matter",
|
||||
content: `---
|
||||
title: Markdown Note
|
||||
date: 2024-01-01
|
||||
---
|
||||
|
||||
# First Heading`,
|
||||
ext: ".md",
|
||||
want: "Markdown Note",
|
||||
},
|
||||
{
|
||||
name: "markdown yaml with quotes",
|
||||
content: `---
|
||||
title: "Quoted Title"
|
||||
date: 2024-01-01
|
||||
---`,
|
||||
ext: ".md",
|
||||
want: "Quoted Title",
|
||||
},
|
||||
{
|
||||
name: "markdown fallback to heading",
|
||||
content: `---
|
||||
date: 2024-01-01
|
||||
---
|
||||
|
||||
# Heading Title
|
||||
|
||||
Content here`,
|
||||
ext: ".md",
|
||||
want: "Heading Title",
|
||||
},
|
||||
{
|
||||
name: "markdown heading without front matter",
|
||||
content: `# Simple Title
|
||||
|
||||
Content`,
|
||||
ext: ".md",
|
||||
want: "Simple Title",
|
||||
},
|
||||
{
|
||||
name: "unsupported extension",
|
||||
content: "Some content",
|
||||
ext: ".pdf",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "empty content",
|
||||
content: "",
|
||||
ext: ".md",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "no title found",
|
||||
content: "Just some text without structure",
|
||||
ext: ".md",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ExtractTitleFromContent(tt.content, tt.ext)
|
||||
if got != tt.want {
|
||||
t.Errorf("ExtractTitleFromContent() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSort validates sorting functionality
|
||||
func TestSort(t *testing.T) {
|
||||
notes := Results{
|
||||
|
|
@ -595,9 +509,9 @@ func TestSort(t *testing.T) {
|
|||
// TestResultsBytes validates serialization
|
||||
func TestResultsBytes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input Results
|
||||
want string
|
||||
name string
|
||||
input Results
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "single note with tags",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"denote/pkg/encoding/frontmatter"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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.
|
||||
func Apply(originalContent string, fm *frontmatter.FrontMatter, fileType frontmatter.FileType) (string, error) {
|
||||
text := originalContent
|
||||
newFrontMatter := string(frontmatter.Marshal(fm, fileType))
|
||||
|
||||
var newText string
|
||||
switch fileType {
|
||||
case frontmatter.FileTypeOrg:
|
||||
// Find end of front matter (first blank line or non-#+ line)
|
||||
lines := strings.Split(text, "\n")
|
||||
endIdx := 0
|
||||
for i, line := range lines {
|
||||
if i > 0 && (line == "" || !strings.HasPrefix(line, "#+")) {
|
||||
endIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
// Skip any blank lines after frontmatter since template includes one
|
||||
for endIdx < len(lines) && lines[endIdx] == "" {
|
||||
endIdx++
|
||||
}
|
||||
if endIdx > 0 {
|
||||
newText = newFrontMatter + strings.Join(lines[endIdx:], "\n")
|
||||
} else {
|
||||
newText = newFrontMatter + text
|
||||
}
|
||||
|
||||
case frontmatter.FileTypeMdYaml:
|
||||
// Replace YAML front matter (match trailing blank lines to avoid duplication)
|
||||
re := regexp.MustCompile(`(?s)^---\n.*?\n---\n\n*`)
|
||||
if re.MatchString(text) {
|
||||
newText = re.ReplaceAllString(text, newFrontMatter)
|
||||
} else {
|
||||
newText = newFrontMatter + text
|
||||
}
|
||||
|
||||
case frontmatter.FileTypeMdToml:
|
||||
// Replace TOML front matter (match trailing blank lines to avoid duplication)
|
||||
re := regexp.MustCompile(`(?s)^\+\+\+\n.*?\n\+\+\+\n\n*`)
|
||||
if re.MatchString(text) {
|
||||
newText = re.ReplaceAllString(text, newFrontMatter)
|
||||
} else {
|
||||
newText = newFrontMatter + text
|
||||
}
|
||||
|
||||
case frontmatter.FileTypeTxt:
|
||||
// Replace text front matter (match trailing blank lines to avoid duplication)
|
||||
re := regexp.MustCompile(`(?s)^title:.*?\n-+\n\n*`)
|
||||
if re.MatchString(text) {
|
||||
newText = re.ReplaceAllString(text, newFrontMatter)
|
||||
} else {
|
||||
newText = newFrontMatter + text
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported file type: %s", fileType)
|
||||
}
|
||||
|
||||
return newText, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"denote/pkg/encoding/frontmatter"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestApply(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
original string
|
||||
fm *frontmatter.FrontMatter
|
||||
fileType frontmatter.FileType
|
||||
wantContains []string
|
||||
wantPreserve string
|
||||
}{
|
||||
{
|
||||
name: "update org front matter",
|
||||
original: `#+title: Old Title
|
||||
#+date: [2024-01-01 Mon 12:00]
|
||||
#+filetags: :old:
|
||||
#+identifier: 20240101T120000
|
||||
|
||||
* Original Content
|
||||
This should be preserved`,
|
||||
fm: &frontmatter.FrontMatter{
|
||||
Title: "New Title",
|
||||
Tags: []string{"new", "updated"},
|
||||
Identifier: "20240101T120000",
|
||||
},
|
||||
fileType: frontmatter.FileTypeOrg,
|
||||
wantContains: []string{
|
||||
"#+title: New Title",
|
||||
"#+filetags: :new:updated:",
|
||||
"* Original Content",
|
||||
"This should be preserved",
|
||||
},
|
||||
wantPreserve: "* Original Content",
|
||||
},
|
||||
{
|
||||
name: "update markdown yaml front matter",
|
||||
original: `---
|
||||
title: Old Title
|
||||
tags: [old]
|
||||
identifier: 20240101T120000
|
||||
---
|
||||
|
||||
# Original Heading
|
||||
Content preserved`,
|
||||
fm: &frontmatter.FrontMatter{
|
||||
Title: "New Title",
|
||||
Tags: []string{"new"},
|
||||
Identifier: "20240101T120000",
|
||||
},
|
||||
fileType: frontmatter.FileTypeMdYaml,
|
||||
wantContains: []string{
|
||||
"---",
|
||||
"title: New Title",
|
||||
"tags: [new]",
|
||||
"# Original Heading",
|
||||
"Content preserved",
|
||||
},
|
||||
wantPreserve: "# Original Heading",
|
||||
},
|
||||
{
|
||||
name: "update markdown toml front matter",
|
||||
original: `+++
|
||||
title = Old Title
|
||||
tags = [old]
|
||||
identifier = 20240101T120000
|
||||
+++
|
||||
|
||||
Content here`,
|
||||
fm: &frontmatter.FrontMatter{
|
||||
Title: "New Title",
|
||||
Tags: []string{"updated"},
|
||||
Identifier: "20240101T120000",
|
||||
},
|
||||
fileType: frontmatter.FileTypeMdToml,
|
||||
wantContains: []string{
|
||||
"+++",
|
||||
"title = New Title",
|
||||
"tags = [updated]",
|
||||
"Content here",
|
||||
},
|
||||
wantPreserve: "Content here",
|
||||
},
|
||||
{
|
||||
name: "update txt front matter",
|
||||
original: `title: Old Title
|
||||
tags: old
|
||||
identifier: 20240101T120000
|
||||
---------------------------
|
||||
|
||||
Text content`,
|
||||
fm: &frontmatter.FrontMatter{
|
||||
Title: "New Title",
|
||||
Tags: []string{"new"},
|
||||
Identifier: "20240101T120000",
|
||||
},
|
||||
fileType: frontmatter.FileTypeTxt,
|
||||
wantContains: []string{
|
||||
"title: New Title",
|
||||
"tags: new",
|
||||
"---------------------------",
|
||||
"Text content",
|
||||
},
|
||||
wantPreserve: "Text content",
|
||||
},
|
||||
{
|
||||
name: "add front matter when missing (org)",
|
||||
original: `* Original Heading`,
|
||||
fm: &frontmatter.FrontMatter{
|
||||
Title: "Added Title",
|
||||
Tags: []string{"added"},
|
||||
Identifier: "20240101T120000",
|
||||
},
|
||||
fileType: frontmatter.FileTypeOrg,
|
||||
wantContains: []string{
|
||||
"#+title: Added Title",
|
||||
"#+filetags: :added:",
|
||||
"* Original Heading",
|
||||
},
|
||||
wantPreserve: "* Original Heading",
|
||||
},
|
||||
{
|
||||
name: "add front matter when missing (md-yaml)",
|
||||
original: `# Original Heading`,
|
||||
fm: &frontmatter.FrontMatter{
|
||||
Title: "Added Title",
|
||||
Tags: []string{"added"},
|
||||
Identifier: "20240101T120000",
|
||||
},
|
||||
fileType: frontmatter.FileTypeMdYaml,
|
||||
wantContains: []string{
|
||||
"---",
|
||||
"title: Added Title",
|
||||
"tags: [added]",
|
||||
"# Original Heading",
|
||||
},
|
||||
wantPreserve: "# Original Heading",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Apply(tt.original, tt.fm, tt.fileType)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply() error = %v", err)
|
||||
}
|
||||
|
||||
for _, want := range tt.wantContains {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("Apply() missing %q\nGot:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify original content is preserved
|
||||
if !strings.Contains(got, tt.wantPreserve) {
|
||||
t.Errorf("Apply() didn't preserve %q\nGot:\n%s", tt.wantPreserve, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyEmptyTags validates updating with empty tags
|
||||
func TestApplyEmptyTags(t *testing.T) {
|
||||
original := `---
|
||||
title: Test
|
||||
tags: [old, tags]
|
||||
identifier: 20240101T120000
|
||||
---
|
||||
|
||||
Content`
|
||||
|
||||
fm := &frontmatter.FrontMatter{
|
||||
Title: "Test",
|
||||
Tags: []string{},
|
||||
Identifier: "20240101T120000",
|
||||
}
|
||||
|
||||
got, err := Apply(original, fm, frontmatter.FileTypeMdYaml)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply() error = %v", err)
|
||||
}
|
||||
|
||||
// Should have empty tags field, not omit it
|
||||
if !strings.Contains(got, "tags:") {
|
||||
t.Errorf("Apply() should include tags field even when empty")
|
||||
}
|
||||
|
||||
// Content should be preserved
|
||||
if !strings.Contains(got, "Content") {
|
||||
t.Errorf("Apply() should preserve content")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyUnsupportedType validates error handling
|
||||
func TestApplyUnsupportedType(t *testing.T) {
|
||||
fm := &frontmatter.FrontMatter{
|
||||
Title: "Test",
|
||||
Tags: []string{"test"},
|
||||
Identifier: "20240101T120000",
|
||||
}
|
||||
|
||||
_, err := Apply("content", fm, "unsupported")
|
||||
if err == nil {
|
||||
t.Error("Apply() should error on unsupported file type")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "unsupported file type") {
|
||||
t.Errorf("Apply() error = %v, want 'unsupported file type'", err)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue