241 lines
4.7 KiB
Go
241 lines
4.7 KiB
Go
package beryl
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
type Task struct {
|
|
Title string
|
|
Tags map[string]string
|
|
Completed bool
|
|
Comment string
|
|
SubTasks []*Task
|
|
}
|
|
|
|
func ParseTask(input string) (*Task, error) {
|
|
var err error
|
|
task := Task{}
|
|
text := strings.Trim(input, "\t")
|
|
|
|
if text[0:6] == "- [ ] " {
|
|
task.Completed = false
|
|
}
|
|
if text[0:6] == "- [x] " {
|
|
task.Completed = true
|
|
}
|
|
|
|
task.Title = text[6:]
|
|
|
|
r := regexp.MustCompile(`((\w+):(\w+))`)
|
|
rSpace := regexp.MustCompile(` ((\w+):(\w+))`)
|
|
|
|
tags := r.FindAllString(task.Title, -1)
|
|
|
|
task.Title = rSpace.ReplaceAllString(task.Title, "")
|
|
|
|
task.Tags = make(map[string]string)
|
|
for _, element := range tags {
|
|
splits := strings.Split(element, ":")
|
|
task.Tags[splits[0]] = splits[1]
|
|
}
|
|
|
|
return &task, err
|
|
}
|
|
|
|
func Tabs(num int) string {
|
|
return strings.Repeat("\t", num)
|
|
}
|
|
|
|
func CountTabs(input string) int {
|
|
// this will also count any tabs that are in the task body itself
|
|
// ideally this would only count consecutave tabs before the first non-tab charicter
|
|
// for char, idx := range input {
|
|
//
|
|
// }
|
|
return strings.Count(input, "\t")
|
|
}
|
|
|
|
func IsTask(input string, numIndent int) bool {
|
|
noCheck := Tabs(numIndent) + "- [ ] "
|
|
check := Tabs(numIndent) + "- [x] "
|
|
|
|
hasUncheck := input[0:len(noCheck)] == noCheck
|
|
hasCheck := input[0:len(check)] == check
|
|
|
|
return hasUncheck || hasCheck
|
|
}
|
|
|
|
// -1 error, 0 task, 1 comment, 2 subtask
|
|
func Identify(input string, numIndent int) int {
|
|
actualIndent := CountTabs(input)
|
|
|
|
if IsTask(input, actualIndent) {
|
|
return 0
|
|
}
|
|
// if IsTask(input, numIndent+1) {
|
|
// return 0
|
|
// }
|
|
//
|
|
// // cant check negatively tabbed tasks
|
|
// if numIndent > 0 {
|
|
// if IsTask(input, numIndent-1) {
|
|
// return 0
|
|
// }
|
|
// }
|
|
|
|
tabs := Tabs(numIndent + 1)
|
|
|
|
if input[0:len(tabs)] == tabs {
|
|
return 1
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
func Parse(file *os.File) ([]*Task, error) {
|
|
var rootTask = Task{}
|
|
lastTasks := stack[*Task]{}
|
|
lastTasks.Push(&rootTask)
|
|
|
|
currentIndent := 0
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
// we want whitespace to get indent level
|
|
// text := strings.Trim(scanner.Text(), "\t\n\r ") // Read line
|
|
text := scanner.Text()
|
|
|
|
// Ignore blank
|
|
if text == "" {
|
|
continue
|
|
}
|
|
|
|
//determine current index
|
|
//update index as needed
|
|
|
|
diff := CountTabs(text) - currentIndent
|
|
ident := Identify(text, currentIndent)
|
|
|
|
switch ident {
|
|
case 0:
|
|
task, err := ParseTask(text)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if diff < 0 {
|
|
absDiff := 0 - diff
|
|
for i := 0; i < absDiff+1; i++ {
|
|
lastTasks.Pop()
|
|
}
|
|
|
|
// if there is one or more in stack
|
|
lastTasks.Push(task)
|
|
// if len(lastTasks) >= 1 {
|
|
// add current as subtask of it
|
|
lastTasks.SecondToLast().SubTasks = append(lastTasks.SecondToLast().SubTasks, task)
|
|
|
|
currentIndent = currentIndent - absDiff
|
|
} else if diff == 0 {
|
|
// if there is one or more in stack
|
|
// if len(lastTasks) >= 1 {
|
|
// add current as subtask of it
|
|
|
|
if len(lastTasks) != 1 {
|
|
_ = lastTasks.Pop()
|
|
}
|
|
lastTasks.Push(task)
|
|
lastTasks.SecondToLast().SubTasks = append(lastTasks.SecondToLast().SubTasks, task)
|
|
} else if diff > 0 {
|
|
lastTasks.Push(task)
|
|
lastTasks.SecondToLast().SubTasks = append(lastTasks.SecondToLast().SubTasks, task)
|
|
currentIndent += 1
|
|
}
|
|
|
|
case 1:
|
|
comment := strings.Trim(text, "\t\n ")
|
|
targetTask := lastTasks.Last()
|
|
|
|
if targetTask.Comment != "" {
|
|
targetTask.Comment += "\n"
|
|
}
|
|
|
|
targetTask.Comment += comment
|
|
default:
|
|
}
|
|
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return lastTasks[0].SubTasks, nil
|
|
}
|
|
|
|
func LoadFromFilename(filename string) ([]*Task, error) {
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
return Parse(file)
|
|
}
|
|
|
|
func String(tasklist []*Task) (text string) {
|
|
for _, task := range tasklist {
|
|
text += fmt.Sprintf("%s\n", task.String())
|
|
}
|
|
return text
|
|
}
|
|
|
|
func (task Task) String() (text string) {
|
|
// checkbox
|
|
text += `- [`
|
|
|
|
if task.Completed {
|
|
text += `x`
|
|
} else {
|
|
text += ` `
|
|
}
|
|
|
|
text += `] `
|
|
|
|
// title
|
|
|
|
text += task.Title + ` `
|
|
|
|
// tags
|
|
// it seems like keys can get output in a different order
|
|
// thats not ideal, but is good enough for now
|
|
|
|
for key, value := range task.Tags {
|
|
text += fmt.Sprintf("%s:%s ", key, value)
|
|
}
|
|
|
|
text = strings.TrimSuffix(text, ` `)
|
|
|
|
if task.Comment != `` {
|
|
text += "\n\t"
|
|
text += strings.Replace(task.Comment, "\n", "\n\t", -1)
|
|
}
|
|
|
|
subtasktxt := ""
|
|
for _, subtask := range task.SubTasks {
|
|
subtasktxt += "\n"
|
|
subtasktxt += subtask.String()
|
|
}
|
|
|
|
text += strings.Replace(subtasktxt, "\n", "\n\t", -1)
|
|
|
|
return
|
|
}
|
|
|
|
func WriteToFilename(tasklist []*Task, filename string) error {
|
|
return ioutil.WriteFile(filename, []byte(String(tasklist)), 0640)
|
|
}
|