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.
This commit is contained in:
@@ -14,16 +14,16 @@ import (
|
|||||||
|
|
||||||
// Task represents a parsed todo item.
|
// Task represents a parsed todo item.
|
||||||
type Task struct {
|
type Task struct {
|
||||||
Raw string `json:"raw"`
|
Raw string `json:"raw"`
|
||||||
Completed bool `json:"completed"`
|
Completed bool `json:"completed"`
|
||||||
CompletionDate *time.Time `json:"completion_date,omitempty"`
|
CompletionDate *time.Time `json:"completion_date,omitempty"`
|
||||||
Priority *rune `json:"priority,omitempty"` // 'A'..'Z'
|
Priority *rune `json:"priority,omitempty"` // 'A'..'Z'
|
||||||
CreationDate *time.Time `json:"creation_date,omitempty"`
|
CreationDate *time.Time `json:"creation_date,omitempty"`
|
||||||
DueDate *time.Time `json:"due_date,omitempty"` // parsed from due:YYYY-MM-DD
|
DueDate *time.Time `json:"due_date,omitempty"` // parsed from due:YYYY-MM-DD
|
||||||
Description string `json:"description"` // description with projects/contexts/metadata removed
|
Description string `json:"description"` // description with projects/contexts/metadata removed
|
||||||
Projects []string `json:"projects"`
|
Projects []string `json:"projects"`
|
||||||
Contexts []string `json:"contexts"`
|
Contexts []string `json:"contexts"`
|
||||||
Metadata map[string]string `json:"metadata"`
|
Metadata map[string][]string `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Task) ToRemind() string {
|
func (t Task) ToRemind() string {
|
||||||
@@ -37,13 +37,19 @@ func (t Task) ToRemind() string {
|
|||||||
sb.WriteString(" ++5 INFO \"Calendar: TODO.TX\" ")
|
sb.WriteString(" ++5 INFO \"Calendar: TODO.TX\" ")
|
||||||
|
|
||||||
if len(t.Metadata) > 0 {
|
if len(t.Metadata) > 0 {
|
||||||
for k, v := range t.Metadata {
|
for k, values := range t.Metadata {
|
||||||
if k == "due" {
|
if k == "due" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// k must have the first letter uppercase for Remind
|
// k must have the first letter uppercase for Remind
|
||||||
k = strings.ToUpper(k[:1]) + k[1:]
|
kUpper := strings.ToUpper(k[:1]) + k[1:]
|
||||||
sb.WriteString(fmt.Sprintf("INFO \"%s: %s\" ", k, v))
|
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 {
|
if len(t.Metadata) > 0 {
|
||||||
for k := range t.Metadata {
|
for k, values := range t.Metadata {
|
||||||
if k == "due" {
|
if k == "due" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// uppercase first letter for Remind
|
// uppercase first letter for Remind
|
||||||
k = strings.ToUpper(k[:1]) + k[1:]
|
kUpper := strings.ToUpper(k[:1]) + k[1:]
|
||||||
sb.WriteString(fmt.Sprintf("%%_%s: %%<%s>", k, k))
|
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) {
|
func ParseLine(line string) (Task, error) {
|
||||||
t := Task{
|
t := Task{
|
||||||
Raw: line,
|
Raw: line,
|
||||||
Metadata: make(map[string]string),
|
Metadata: make(map[string][]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
working := strings.TrimSpace(line)
|
working := strings.TrimSpace(line)
|
||||||
@@ -262,14 +275,14 @@ func ParseLine(line string) (Task, error) {
|
|||||||
}
|
}
|
||||||
val += " " + toks[i]
|
val += " " + toks[i]
|
||||||
}
|
}
|
||||||
t.Metadata["location"] = val
|
t.Metadata["location"] = append(t.Metadata["location"], val)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !strings.ContainsAny(tok, " \t") && strings.Contains(tok, ":") {
|
if !strings.ContainsAny(tok, " \t") && strings.Contains(tok, ":") {
|
||||||
parts := strings.SplitN(tok, ":", 2)
|
parts := strings.SplitN(tok, ":", 2)
|
||||||
k, v := parts[0], parts[1]
|
k, v := parts[0], parts[1]
|
||||||
if k != "" && v != "" && !isProtocolKey(k) {
|
if k != "" && v != "" && !isProtocolKey(k) {
|
||||||
t.Metadata[k] = v
|
t.Metadata[k] = append(t.Metadata[k], v)
|
||||||
if k == "due" && dateRe.MatchString(v) {
|
if k == "due" && dateRe.MatchString(v) {
|
||||||
if dt, err := time.Parse(dateLayout, v); err == nil {
|
if dt, err := time.Parse(dateLayout, v); err == nil {
|
||||||
t.DueDate = &dt
|
t.DueDate = &dt
|
||||||
@@ -277,6 +290,7 @@ func ParseLine(line string) (Task, error) {
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
descParts = append(descParts, tok)
|
descParts = append(descParts, tok)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user