diff --git a/internal/parser/parser.go b/internal/parser/parser.go index fa84922..136c7ad 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -64,11 +64,9 @@ func (t Task) MarshalJSON() ([]byte, error) { } var ( - dateRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) - priorityHead = regexp.MustCompile(`^\([A-Z]\)\s+`) - completedHead = regexp.MustCompile(`^x\s+`) - spaceRe = regexp.MustCompile(`\s+`) - dateLayout = "2006-01-02" + dateRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) + spaceRe = regexp.MustCompile(`\s+`) + dateLayout = "2006-01-02" // Protocols to exclude as metadata keys protocols = map[string]struct{}{ "http": {}, @@ -119,48 +117,56 @@ func ParseLine(line string) (Task, error) { } working := strings.TrimSpace(line) + toks := splitTokens(working) + i := 0 // 1) Completed? - if completedHead.MatchString(working) { + if i < len(toks) && toks[i] == "x" { t.Completed = true - working = completedHead.ReplaceAllString(working, "") - toks := splitTokens(working) - consumed := 0 - if len(toks) > 0 && dateRe.MatchString(toks[0]) { - if dt, err := time.Parse(dateLayout, toks[0]); err == nil { + i++ + // 2) Completion date + if i < len(toks) && dateRe.MatchString(toks[i]) { + if dt, err := time.Parse(dateLayout, toks[i]); err == nil { t.CompletionDate = &dt - consumed = 1 + i++ } } - if consumed < len(toks) && dateRe.MatchString(toks[consumed]) { - if dt, err := time.Parse(dateLayout, toks[consumed]); err == nil { + // 3) Creation date + if i < len(toks) && dateRe.MatchString(toks[i]) { + if dt, err := time.Parse(dateLayout, toks[i]); err == nil { t.CreationDate = &dt - consumed++ - } - } - if consumed > 0 { - working = strings.TrimSpace(strings.Join(toks[consumed:], " ")) - } - } else { - if priorityHead.MatchString(working) { - if len(working) >= 3 { - r := rune(working[1]) - t.Priority = &r - } - working = priorityHead.ReplaceAllString(working, "") - } - toks := splitTokens(working) - if len(toks) > 0 && dateRe.MatchString(toks[0]) { - if dt, err := time.Parse(dateLayout, toks[0]); err == nil { - t.CreationDate = &dt - working = strings.TrimSpace(strings.Join(toks[1:], " ")) + i++ } } } - tokens := splitTokens(working) - descParts := make([]string, 0, len(tokens)) - for _, tok := range tokens { + // 4) Priority (can appear after x, dates) + if i < len(toks) && len(toks[i]) == 3 && toks[i][0] == '(' && toks[i][2] == ')' && toks[i][1] >= 'A' && toks[i][1] <= 'Z' { + r := rune(toks[i][1]) + t.Priority = &r + i++ + } + + // 5) If not completed, check for priority at start + if !t.Completed { + if i < len(toks) && len(toks[i]) == 3 && toks[i][0] == '(' && toks[i][2] == ')' && toks[i][1] >= 'A' && toks[i][1] <= 'Z' { + r := rune(toks[i][1]) + t.Priority = &r + i++ + } + // Creation date for incomplete tasks + if i < len(toks) && dateRe.MatchString(toks[i]) { + if dt, err := time.Parse(dateLayout, toks[i]); err == nil { + t.CreationDate = &dt + i++ + } + } + } + + // 6) Parse remaining tokens + descParts := make([]string, 0) + for ; i < len(toks); i++ { + tok := toks[i] if strings.HasPrefix(tok, "+") && len(tok) > 1 { t.Projects = append(t.Projects, tok[1:]) continue @@ -174,7 +180,6 @@ func ParseLine(line string) (Task, error) { k, v := parts[0], parts[1] if k != "" && v != "" && !isProtocolKey(k) { t.Metadata[k] = v - // If the field is due:YYYY-MM-DD, also parse it as DueDate if k == "due" && dateRe.MatchString(v) { if dt, err := time.Parse(dateLayout, v); err == nil { t.DueDate = &dt @@ -183,7 +188,6 @@ func ParseLine(line string) (Task, error) { continue } } - // else part of description descParts = append(descParts, tok) } t.Description = strings.TrimSpace(strings.Join(descParts, " "))