From daffdf44414f42226a93f970bc3420b4d475e5ad Mon Sep 17 00:00:00 2001 From: Paolo Donadeo Date: Wed, 17 Dec 2025 00:16:16 +0100 Subject: [PATCH] feat(parser): support multiple values per metadata key in Task BREAKING CHANGE: The Task struct's Metadata field now maps to slices of strings (map[string][]string) instead of single strings. This allows tasks to have multiple values for the same metadata key. All code interacting with Metadata must be updated to handle slices. Version bumped to v3.0.0. --- internal/parser/parser.go | 52 +++++++++++++++++++++++++-------------- main.go | 2 +- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/internal/parser/parser.go b/internal/parser/parser.go index cb0721c..8bcc5ed 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -14,16 +14,16 @@ import ( // Task represents a parsed todo item. type Task struct { - Raw string `json:"raw"` - Completed bool `json:"completed"` - CompletionDate *time.Time `json:"completion_date,omitempty"` - Priority *rune `json:"priority,omitempty"` // 'A'..'Z' - CreationDate *time.Time `json:"creation_date,omitempty"` - DueDate *time.Time `json:"due_date,omitempty"` // parsed from due:YYYY-MM-DD - Description string `json:"description"` // description with projects/contexts/metadata removed - Projects []string `json:"projects"` - Contexts []string `json:"contexts"` - Metadata map[string]string `json:"metadata"` + Raw string `json:"raw"` + Completed bool `json:"completed"` + CompletionDate *time.Time `json:"completion_date,omitempty"` + Priority *rune `json:"priority,omitempty"` // 'A'..'Z' + CreationDate *time.Time `json:"creation_date,omitempty"` + DueDate *time.Time `json:"due_date,omitempty"` // parsed from due:YYYY-MM-DD + Description string `json:"description"` // description with projects/contexts/metadata removed + Projects []string `json:"projects"` + Contexts []string `json:"contexts"` + Metadata map[string][]string `json:"metadata"` } func (t Task) ToRemind() string { @@ -37,13 +37,19 @@ func (t Task) ToRemind() string { sb.WriteString(" ++5 INFO \"Calendar: TODO.TX\" ") if len(t.Metadata) > 0 { - for k, v := range t.Metadata { + for k, values := range t.Metadata { if k == "due" { continue } // k must have the first letter uppercase for Remind - k = strings.ToUpper(k[:1]) + k[1:] - sb.WriteString(fmt.Sprintf("INFO \"%s: %s\" ", k, v)) + kUpper := strings.ToUpper(k[:1]) + k[1:] + if len(values) == 1 { + sb.WriteString(fmt.Sprintf("INFO \"%s: %s\" ", kUpper, values[0])) + } else { + for i, v := range values { + sb.WriteString(fmt.Sprintf("INFO \"%s%d: %s\" ", kUpper, i+1, v)) + } + } } } @@ -82,13 +88,20 @@ func (t Task) ToRemind() string { } if len(t.Metadata) > 0 { - for k := range t.Metadata { + for k, values := range t.Metadata { if k == "due" { continue } // uppercase first letter for Remind - k = strings.ToUpper(k[:1]) + k[1:] - sb.WriteString(fmt.Sprintf("%%_%s: %%<%s>", k, k)) + kUpper := strings.ToUpper(k[:1]) + k[1:] + if len(values) == 1 { + sb.WriteString(fmt.Sprintf("%%_%s: %%<%s>", kUpper, kUpper)) + } else { + for i := range values { + kNumbered := fmt.Sprintf("%s%d", kUpper, i+1) + sb.WriteString(fmt.Sprintf("%%_%s: %%<%s>", kNumbered, kNumbered)) + } + } } } @@ -181,7 +194,7 @@ func ParseReader(r io.Reader) ([]Task, []error) { func ParseLine(line string) (Task, error) { t := Task{ Raw: line, - Metadata: make(map[string]string), + Metadata: make(map[string][]string), } working := strings.TrimSpace(line) @@ -262,14 +275,14 @@ func ParseLine(line string) (Task, error) { } val += " " + toks[i] } - t.Metadata["location"] = val + t.Metadata["location"] = append(t.Metadata["location"], val) continue } if !strings.ContainsAny(tok, " \t") && strings.Contains(tok, ":") { parts := strings.SplitN(tok, ":", 2) k, v := parts[0], parts[1] if k != "" && v != "" && !isProtocolKey(k) { - t.Metadata[k] = v + t.Metadata[k] = append(t.Metadata[k], v) if k == "due" && dateRe.MatchString(v) { if dt, err := time.Parse(dateLayout, v); err == nil { t.DueDate = &dt @@ -277,6 +290,7 @@ func ParseLine(line string) (Task, error) { } continue } + } descParts = append(descParts, tok) } diff --git a/main.go b/main.go index 46114fa..459aae4 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,7 @@ var ( inputFile string outputFile string debug bool - version = "v2.0.0" + version = "v3.0.0" ) func main() {