diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 6151333..ac43c12 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -44,10 +44,10 @@ func (t Task) ToRemind(indent int) string { // k must have the first letter uppercase for Remind kUpper := strings.ToUpper(k[:1]) + k[1:] if len(values) == 1 { - sb.WriteString(fmt.Sprintf("INFO \"%s: %s\" ", kUpper, values[0])) + fmt.Fprintf(&sb, "INFO \"%s: %s\" ", kUpper, values[0]) } else { for i, v := range values { - sb.WriteString(fmt.Sprintf("INFO \"%s%d: %s\" ", kUpper, i+1, v)) + fmt.Fprintf(&sb, "INFO \"%s%d: %s\" ", kUpper, i+1, v) } } } @@ -55,22 +55,22 @@ func (t Task) ToRemind(indent int) string { if len(t.Projects) > 0 { for _, p := range t.Projects { - sb.WriteString(fmt.Sprintf("INFO \"List: %s\" ", p)) + fmt.Fprintf(&sb, "INFO \"List: %s\" ", p) } } if len(t.Contexts) > 0 { for _, c := range t.Contexts { - sb.WriteString(fmt.Sprintf("INFO \"Tag: %s\" ", c)) + fmt.Fprintf(&sb, "INFO \"Tag: %s\" ", c) } } if t.Completed { - sb.WriteString(fmt.Sprintf("COMPLETE-THROUGH %s ", t.DueDate.Format("2006-01-02"))) + fmt.Fprintf(&sb, "COMPLETE-THROUGH %s ", t.DueDate.Format("2006-01-02")) } if t.Priority != nil { - sb.WriteString(fmt.Sprintf("PRIORITY %d ", t.PriorityAsRemind())) + fmt.Fprintf(&sb, "PRIORITY %d ", t.PriorityAsRemind()) } sb.WriteString("MSG") @@ -79,7 +79,7 @@ func (t Task) ToRemind(indent int) string { sb.WriteString(" %:") } - sb.WriteString(fmt.Sprintf(" %s", t.Description)) + fmt.Fprintf(&sb, " %s", t.Description) if t.Completed { sb.WriteString("%:") @@ -97,11 +97,11 @@ func (t Task) ToRemind(indent int) string { // uppercase first letter for Remind kUpper := strings.ToUpper(k[:1]) + k[1:] if len(values) == 1 { - sb.WriteString(fmt.Sprintf("%%_%s%s: %%<%s>", headingSpaces, kUpper, kUpper)) + fmt.Fprintf(&sb, "%%_%s%s: %%<%s>", headingSpaces, kUpper, kUpper) } else { for i := range values { kNumbered := fmt.Sprintf("%s%d", kUpper, i+1) - sb.WriteString(fmt.Sprintf("%%_%s%s: %%<%s>", headingSpaces, kNumbered, kNumbered)) + fmt.Fprintf(&sb, "%%_%s%s: %%<%s>", headingSpaces, kNumbered, kNumbered) } } } @@ -202,6 +202,7 @@ func ParseLine(line string) (Task, error) { working := strings.TrimSpace(line) toks := splitTokens(working) i := 0 + var invalidDue string // 1) Completed? if i < len(toks) && toks[i] == "x" { @@ -285,9 +286,12 @@ func ParseLine(line string) (Task, error) { k, v := parts[0], parts[1] if k != "" && v != "" && !isProtocolKey(k) { t.Metadata[k] = append(t.Metadata[k], v) - if k == "due" && dateRe.MatchString(v) { - if dt, err := time.Parse(dateLayout, v); err == nil { + if k == "due" { + dt, err := time.Parse(dateLayout, v) + if dateRe.MatchString(v) && err == nil { t.DueDate = &dt + } else { + invalidDue = v } } continue @@ -302,6 +306,10 @@ func ParseLine(line string) (Task, error) { return t, errors.New("completed task missing completion date (spec requires completion date directly after 'x')") } + if invalidDue != "" { + return t, fmt.Errorf("invalid due date %q: expected format YYYY-MM-DD (task will be dropped, no Remind entry generated)", invalidDue) + } + return t, nil } diff --git a/test.todo.txt b/test.todo.txt index 385f81d..1f33379 100644 --- a/test.todo.txt +++ b/test.todo.txt @@ -73,6 +73,9 @@ X 2012-01-01 Make resolutions (A) 2024-13-99 Call Mom x 2024-99-99 Call Mom +# Malformed: due date with invalid format (should error, task dropped from output) +(A) Call Mom due:2024-13-99 + # Malformed: key:value with whitespace in key or value foo bar:baz foo:bar baz