168 lines
3.4 KiB
Go
168 lines
3.4 KiB
Go
|
package todotxt
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
// Item represents todotxt task
|
||
|
type Item struct {
|
||
|
Complete bool
|
||
|
Priority *Priority
|
||
|
CompletionDate *time.Time
|
||
|
CreationDate *time.Time
|
||
|
Description string
|
||
|
Tags []Tag
|
||
|
}
|
||
|
|
||
|
// String returns text representation of Item
|
||
|
func (i *Item) String() string {
|
||
|
result := ""
|
||
|
if i.Complete {
|
||
|
result = "x "
|
||
|
}
|
||
|
if i.Priority != nil {
|
||
|
result += "(" + i.Priority.String() + ") "
|
||
|
}
|
||
|
|
||
|
if i.CompletionDate != nil {
|
||
|
result += i.CompletionDate.Format("2006-01-02") + " "
|
||
|
if i.CreationDate != nil {
|
||
|
result += i.CreationDate.Format("2006-01-02") + " "
|
||
|
} else {
|
||
|
result += time.Now().Format("2006-01-02") + " "
|
||
|
}
|
||
|
} else if i.CreationDate != nil {
|
||
|
result += i.CreationDate.Format("2006-01-02") + " "
|
||
|
}
|
||
|
result += i.Description + " "
|
||
|
for _, t := range i.Tags {
|
||
|
switch t.Key {
|
||
|
case TagContext:
|
||
|
result += "@" + t.Value + " "
|
||
|
case TagProject:
|
||
|
result += "+" + t.Value + " "
|
||
|
default:
|
||
|
result += t.Key + ":" + t.Value + " "
|
||
|
}
|
||
|
}
|
||
|
return strings.Trim(result, " \n")
|
||
|
}
|
||
|
|
||
|
// Parse multiline todotxt string
|
||
|
func Parse(todo string) ([]Item, error) {
|
||
|
lines := strings.Split(todo, "\n")
|
||
|
items := make([]Item, 0, len(lines))
|
||
|
for ln, line := range lines {
|
||
|
i, err := ParseLine(line)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("error at line %d: %v", ln, err)
|
||
|
}
|
||
|
items = append(items, i)
|
||
|
}
|
||
|
return items, nil
|
||
|
}
|
||
|
|
||
|
// ParseLine parses single todotxt line
|
||
|
func ParseLine(line string) (Item, error) {
|
||
|
i := Item{}
|
||
|
tokens := strings.Split(line, " ")
|
||
|
state := 0
|
||
|
for _, t := range tokens {
|
||
|
if state == 0 && t == "x" {
|
||
|
state = 1
|
||
|
i.Complete = true
|
||
|
continue
|
||
|
}
|
||
|
if state <= 1 && len(t) == 3 && t[0] == '(' && t[2] == ')' {
|
||
|
p, err := PriorityFromLetter(string(t[1]))
|
||
|
if err != nil {
|
||
|
return i, err
|
||
|
}
|
||
|
i.Priority = &p
|
||
|
state = 2
|
||
|
continue
|
||
|
}
|
||
|
if state <= 2 {
|
||
|
ti, err := time.Parse("2006-01-02", t)
|
||
|
if err == nil {
|
||
|
i.CreationDate = &ti
|
||
|
state = 3
|
||
|
continue
|
||
|
}
|
||
|
state = 4
|
||
|
}
|
||
|
if state <= 3 {
|
||
|
ti, err := time.Parse("2006-01-02", t)
|
||
|
if err == nil {
|
||
|
i.CompletionDate = i.CreationDate
|
||
|
i.CreationDate = &ti
|
||
|
state = 4
|
||
|
continue
|
||
|
}
|
||
|
state = 4
|
||
|
}
|
||
|
if t[0] == '+' {
|
||
|
i.Tags = append(i.Tags, Tag{
|
||
|
Key: TagProject,
|
||
|
Value: string(t[1:]),
|
||
|
})
|
||
|
continue
|
||
|
}
|
||
|
if t[0] == '@' {
|
||
|
i.Tags = append(i.Tags, Tag{
|
||
|
Key: TagContext,
|
||
|
Value: string(t[1:]),
|
||
|
})
|
||
|
continue
|
||
|
}
|
||
|
kv := strings.Split(t, ":")
|
||
|
if len(kv) == 2 {
|
||
|
i.Tags = append(i.Tags, Tag{
|
||
|
Key: kv[0],
|
||
|
Value: kv[1],
|
||
|
})
|
||
|
continue
|
||
|
}
|
||
|
if i.Description == "" {
|
||
|
i.Description = t
|
||
|
} else {
|
||
|
i.Description += " " + t
|
||
|
}
|
||
|
}
|
||
|
return i, nil
|
||
|
}
|
||
|
|
||
|
type Priority int
|
||
|
|
||
|
// String returns letter by priority (0=A, 1=B, 2=C ...)
|
||
|
func (p Priority) String() string {
|
||
|
return string([]byte{byte(p + 65)})
|
||
|
}
|
||
|
|
||
|
// PriorityFromLetter returns numeric priority from letter priority (A=0, B=1, C=2 ...)
|
||
|
func PriorityFromLetter(letter string) (Priority, error) {
|
||
|
if len(letter) != 1 {
|
||
|
return 0, errors.New("incorrect priority length")
|
||
|
}
|
||
|
code := []byte(letter)[0]
|
||
|
if code < 65 || code > 90 {
|
||
|
return 0, errors.New("priority must be between A and Z")
|
||
|
}
|
||
|
return Priority(code - 65), nil
|
||
|
}
|
||
|
|
||
|
// TagContext constant is key for context tag
|
||
|
const TagContext = "@context"
|
||
|
|
||
|
// TagProject constant is key for project tag
|
||
|
const TagProject = "+project"
|
||
|
|
||
|
// Tag represents builtin and custom tags
|
||
|
type Tag struct {
|
||
|
Key string
|
||
|
Value string
|
||
|
}
|