diff --git a/README.md b/README.md index 1daaecd..676858e 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,28 @@ You may change this by changing `ftype` in main.go. This program supports encrypted notes by integrating with [acme-crypt](https://github.com/lneely/acme-crypt). Set up the [plumbing rules](./PLUMBING.md) to read encrypted notes, and use `CryptPut` to write them. +## Signature Support + +Denote supports an optional signature component in filenames: `ID==SIGNATURE--TITLE__TAGS.ext`. Signatures are useful for sequential numbering, context markers, or priorities. + +**Plumbing Configuration:** + +To plumb files with signatures, you must add `=` to the file pattern character classes in your plumbing rules. Update these three patterns: + +**Original patterns:** +``` +data matches '([.a-zA-Z�-\uffff0-9_/\-@]*[a-zA-Z�-\uffff0-9_/\-])':$twocolonaddr,$twocolonaddr +data matches '([.a-zA-Z�-\uffff0-9_/\-@]*[a-zA-Z�-\uffff0-9_/\-])':$twocolonaddr +data matches '([.a-zA-Z�-\uffff0-9_/\-@]*[a-zA-Z�-\uffff0-9_/\-])('$addr')?' +``` + +**Modified patterns (add `=` to character classes):** +``` +data matches '([.a-zA-Z�-\uffff0-9_/\-@=]*[a-zA-Z�-\uffff0-9_/\-=])':$twocolonaddr,$twocolonaddr +data matches '([.a-zA-Z�-\uffff0-9_/\-@=]*[a-zA-Z�-\uffff0-9_/\-=])':$twocolonaddr +data matches '([.a-zA-Z�-\uffff0-9_/\-@=]*[a-zA-Z�-\uffff0-9_/\-=])('$addr')?' +``` + ## Extensions Some extensions have also been ported. While these extensions, like the main program, try to stay as true as possible to the original program and be as feature-complete as possible, they are intentionally *not* exact replicas. diff --git a/internal/disk/events.go b/internal/disk/events.go index e023fae..ea35ecd 100644 --- a/internal/disk/events.go +++ b/internal/disk/events.go @@ -113,8 +113,8 @@ func HandleRenameEvent(f *client.Fsys, identifier, denoteDir string) error { return nil } - // Find old file on disk. - pattern := filepath.Join(denoteDir, identifier+"--*") + // Find old file on disk (matches with or without signature). + pattern := filepath.Join(denoteDir, identifier+"*") matches, err := filepath.Glob(pattern) if err != nil { return fmt.Errorf("failed to find file: %w", err) @@ -165,7 +165,7 @@ func HandleDeleteEvent(f *client.Fsys, identifier, denoteDir string) error { dir = denoteDir } - pattern := filepath.Join(dir, identifier+"--*") + pattern := filepath.Join(dir, identifier+"*") matches, err := filepath.Glob(pattern) if err != nil { return fmt.Errorf("failed to find file: %w", err) diff --git a/internal/disk/sync.go b/internal/disk/sync.go index 957d196..1133d85 100644 --- a/internal/disk/sync.go +++ b/internal/disk/sync.go @@ -222,7 +222,7 @@ func SyncAll() error { // Path in metadata doesn't exist - search for actual file on disk // Use the directory from the metadata path (handles subdirs like journal/) dir := filepath.Dir(path) - pattern := filepath.Join(dir, identifier+"--*") + pattern := filepath.Join(dir, identifier+"*") matches, err := filepath.Glob(pattern) if err != nil { log.Printf("sync: failed to glob for %s: %v", identifier, err) diff --git a/internal/metadata/frontmatter.go b/internal/metadata/frontmatter.go index c583e1d..aeb59f8 100644 --- a/internal/metadata/frontmatter.go +++ b/internal/metadata/frontmatter.go @@ -88,16 +88,16 @@ func ParseFrontMatter(content string, ext string) (*FrontMatter, error) { switch ext { case ".org": fm.FileType = "org" - if m := regexp.MustCompile(`(?m)^#\+title:\s*(.+)$`).FindStringSubmatch(text); m != nil { + if m := regexp.MustCompile(`(?m)^#\+title:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil { fm.Title = strings.TrimSpace(m[1]) } - if m := regexp.MustCompile(`(?m)^#\+filetags:\s*:(.+):$`).FindStringSubmatch(text); m != nil { + if m := regexp.MustCompile(`(?m)^#\+filetags:[ \t]*:(.+):$`).FindStringSubmatch(text); m != nil { fm.Tags = strings.Split(m[1], ":") } - if m := regexp.MustCompile(`(?m)^#\+identifier:\s*(.+)$`).FindStringSubmatch(text); m != nil { + if m := regexp.MustCompile(`(?m)^#\+identifier:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil { fm.Identifier = strings.TrimSpace(m[1]) } - if m := regexp.MustCompile(`(?m)^#\+signature:\s*(.+)$`).FindStringSubmatch(text); m != nil { + if m := regexp.MustCompile(`(?m)^#\+signature:[ \t]*(.*)$`).FindStringSubmatch(text); m != nil { fm.Signature = strings.TrimSpace(m[1]) } @@ -107,20 +107,20 @@ func ParseFrontMatter(content string, ext string) (*FrontMatter, error) { if m := yamlRe.FindStringSubmatch(text); m != nil { fm.FileType = "md-yaml" yamlContent := m[1] - if m := regexp.MustCompile(`(?m)^title:\s*["']?(.+?)["']?$`).FindStringSubmatch(yamlContent); m != nil { + if m := regexp.MustCompile(`(?m)^title:[ \t]*["']?(.+?)["']?$`).FindStringSubmatch(yamlContent); m != nil { fm.Title = strings.TrimSpace(m[1]) } - if m := regexp.MustCompile(`(?m)^tags:\s*\[(.+?)\]$`).FindStringSubmatch(yamlContent); m != nil { + if m := regexp.MustCompile(`(?m)^tags:[ \t]*\[(.+?)\]$`).FindStringSubmatch(yamlContent); m != nil { tags := strings.Split(m[1], ",") for i, t := range tags { tags[i] = strings.TrimSpace(t) } fm.Tags = tags } - if m := regexp.MustCompile(`(?m)^identifier:\s*["']?(.+?)["']?$`).FindStringSubmatch(yamlContent); m != nil { + if m := regexp.MustCompile(`(?m)^identifier:[ \t]*["']?(.+?)["']?$`).FindStringSubmatch(yamlContent); m != nil { fm.Identifier = strings.TrimSpace(m[1]) } - if m := regexp.MustCompile(`(?m)^signature:\s*["']?(.+?)["']?$`).FindStringSubmatch(yamlContent); m != nil { + if m := regexp.MustCompile(`(?m)^signature:[ \t]*["']?(.*)["']?$`).FindStringSubmatch(yamlContent); m != nil { fm.Signature = strings.TrimSpace(m[1]) } } else { @@ -129,20 +129,20 @@ func ParseFrontMatter(content string, ext string) (*FrontMatter, error) { if m := tomlRe.FindStringSubmatch(text); m != nil { fm.FileType = "md-toml" tomlContent := m[1] - if m := regexp.MustCompile(`(?m)^title\s*=\s*["']?(.+?)["']?$`).FindStringSubmatch(tomlContent); m != nil { + if m := regexp.MustCompile(`(?m)^title[ \t]*=[ \t]*["']?(.+?)["']?$`).FindStringSubmatch(tomlContent); m != nil { fm.Title = strings.TrimSpace(m[1]) } - if m := regexp.MustCompile(`(?m)^tags\s*=\s*\[(.+?)\]$`).FindStringSubmatch(tomlContent); m != nil { + if m := regexp.MustCompile(`(?m)^tags[ \t]*=[ \t]*\[(.+?)\]$`).FindStringSubmatch(tomlContent); m != nil { tags := strings.Split(m[1], ",") for i, t := range tags { tags[i] = strings.TrimSpace(t) } fm.Tags = tags } - if m := regexp.MustCompile(`(?m)^identifier\s*=\s*["']?(.+?)["']?$`).FindStringSubmatch(tomlContent); m != nil { + if m := regexp.MustCompile(`(?m)^identifier[ \t]*=[ \t]*["']?(.+?)["']?$`).FindStringSubmatch(tomlContent); m != nil { fm.Identifier = strings.TrimSpace(m[1]) } - if m := regexp.MustCompile(`(?m)^signature\s*=\s*["']?(.+?)["']?$`).FindStringSubmatch(tomlContent); m != nil { + if m := regexp.MustCompile(`(?m)^signature[ \t]*=[ \t]*["']?(.*)["']?$`).FindStringSubmatch(tomlContent); m != nil { fm.Signature = strings.TrimSpace(m[1]) } } @@ -150,16 +150,16 @@ func ParseFrontMatter(content string, ext string) (*FrontMatter, error) { case ".txt": fm.FileType = "txt" - if m := regexp.MustCompile(`(?m)^title:\s*(.+)$`).FindStringSubmatch(text); m != nil { + if m := regexp.MustCompile(`(?m)^title:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil { fm.Title = strings.TrimSpace(m[1]) } - if m := regexp.MustCompile(`(?m)^tags:\s*(.+)$`).FindStringSubmatch(text); m != nil { + if m := regexp.MustCompile(`(?m)^tags:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil { fm.Tags = strings.Fields(m[1]) } - if m := regexp.MustCompile(`(?m)^identifier:\s*(.+)$`).FindStringSubmatch(text); m != nil { + if m := regexp.MustCompile(`(?m)^identifier:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil { fm.Identifier = strings.TrimSpace(m[1]) } - if m := regexp.MustCompile(`(?m)^signature:\s*(.+)$`).FindStringSubmatch(text); m != nil { + if m := regexp.MustCompile(`(?m)^signature:[ \t]*(.*)$`).FindStringSubmatch(text); m != nil { fm.Signature = strings.TrimSpace(m[1]) } } @@ -206,8 +206,8 @@ func UpdateFrontMatter(originalContent string, fm *FrontMatter) (string, error) } case "md-yaml": - // Replace YAML front matter - re := regexp.MustCompile(`(?s)^---\n.*?\n---\n`) + // 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 { @@ -215,8 +215,8 @@ func UpdateFrontMatter(originalContent string, fm *FrontMatter) (string, error) } case "md-toml": - // Replace TOML front matter - re := regexp.MustCompile(`(?s)^\+\+\+\n.*?\n\+\+\+\n`) + // 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 { @@ -224,8 +224,8 @@ func UpdateFrontMatter(originalContent string, fm *FrontMatter) (string, error) } case "txt": - // Replace text front matter - re := regexp.MustCompile(`(?s)^title:.*?\n-+\n`) + // 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 {