package parse import ( "fmt" "strconv" "strings" ) type Result struct { Body string Glyph string TimeAnchor *string Tags []string CardSuffix *string } var validCardTypes = map[string]string{ "card": "snippet", "c": "snippet", "snippet": "snippet", "template": "template", "checklist": "checklist", "decision": "decision", "link": "link", } func Parse(input string) (*Result, error) { input = strings.TrimSpace(input) if input == "" { return nil, fmt.Errorf("empty input") } r := &Result{ Glyph: "note", Tags: []string{}, } tokens := strings.Fields(input) if len(tokens) == 0 { return nil, fmt.Errorf("empty input") } first := tokens[0] switch first { case "-", "▸": r.Glyph = "todo" tokens = tokens[1:] case "*", "◇": r.Glyph = "event" tokens = tokens[1:] } var bodyParts []string seen := map[string]bool{} for _, tok := range tokens { switch { case strings.HasPrefix(tok, "@") && len(tok) > 1: timeStr := tok[1:] if err := validateTime(timeStr); err != nil { return nil, fmt.Errorf("invalid time %q: %w", timeStr, err) } if r.TimeAnchor != nil { return nil, fmt.Errorf("multiple time anchors") } r.TimeAnchor = &timeStr case strings.HasPrefix(tok, "#") && len(tok) > 1: tag := tok[1:] if !seen[tag] { r.Tags = append(r.Tags, tag) seen[tag] = true } case strings.HasPrefix(tok, "^") && len(tok) > 1: suffix := tok[1:] cardType, ok := validCardTypes[suffix] if !ok { return nil, fmt.Errorf("invalid card type %q", suffix) } if r.CardSuffix != nil { return nil, fmt.Errorf("multiple card suffixes") } r.CardSuffix = &cardType default: bodyParts = append(bodyParts, tok) } } r.Body = strings.Join(bodyParts, " ") if r.Body == "" { return nil, fmt.Errorf("empty body after extracting modifiers") } return r, nil } func validateTime(s string) error { parts := strings.SplitN(s, ":", 2) if len(parts) != 2 { return fmt.Errorf("expected HH:MM format") } h, err := strconv.Atoi(parts[0]) if err != nil || h < 0 || h > 23 { return fmt.Errorf("hours must be 0-23") } m, err := strconv.Atoi(parts[1]) if err != nil || m < 0 || m > 59 { return fmt.Errorf("minutes must be 0-59") } return nil }