143 lines
2.7 KiB
Go
143 lines
2.7 KiB
Go
package todo
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"g.fc.run/misc/byre/internal/api"
|
|
)
|
|
|
|
const File = "TODO.md"
|
|
|
|
var (
|
|
heading = regexp.MustCompile(`^#{1,6}\s+(.*?)\s*#*\s*$`)
|
|
listItem = regexp.MustCompile(`^(?:[-*+]|\d+[.)])\s+(.*)$`)
|
|
checkbox = regexp.MustCompile(`^\[([ xX])\]\s+`)
|
|
fence = regexp.MustCompile("^\\s*(```|~~~)")
|
|
)
|
|
|
|
func Parse(src string) []api.TodoItem {
|
|
items := []api.TodoItem{}
|
|
section := ""
|
|
var cur *api.TodoItem
|
|
body := []string{}
|
|
inFence := false
|
|
blank := false
|
|
|
|
finish := func() {
|
|
if cur == nil {
|
|
return
|
|
}
|
|
cur.Body = dedent(body)
|
|
items = append(items, *cur)
|
|
cur = nil
|
|
body = []string{}
|
|
}
|
|
|
|
for _, line := range strings.Split(strings.ReplaceAll(src, "\r\n", "\n"), "\n") {
|
|
if fence.MatchString(line) {
|
|
inFence = !inFence
|
|
if cur != nil {
|
|
body = append(body, line)
|
|
}
|
|
continue
|
|
}
|
|
if inFence {
|
|
if cur != nil {
|
|
body = append(body, line)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if strings.TrimSpace(line) == "" {
|
|
blank = true
|
|
if cur != nil {
|
|
body = append(body, "")
|
|
}
|
|
continue
|
|
}
|
|
indented := line[0] == ' ' || line[0] == '\t'
|
|
afterBlank := blank
|
|
blank = false
|
|
|
|
if m := heading.FindStringSubmatch(line); m != nil {
|
|
finish()
|
|
section = m[1]
|
|
continue
|
|
}
|
|
if m := listItem.FindStringSubmatch(line); m != nil {
|
|
finish()
|
|
title := m[1]
|
|
box := checkbox.FindStringSubmatch(title)
|
|
if box != nil && box[1] != " " {
|
|
continue
|
|
}
|
|
title = strings.TrimSpace(checkbox.ReplaceAllString(title, ""))
|
|
cur = &api.TodoItem{Title: title, Section: section}
|
|
continue
|
|
}
|
|
if cur == nil {
|
|
continue
|
|
}
|
|
if !indented && afterBlank {
|
|
finish()
|
|
continue
|
|
}
|
|
body = append(body, line)
|
|
}
|
|
finish()
|
|
|
|
return items
|
|
}
|
|
|
|
func Find(items []api.TodoItem, title string) (api.TodoItem, bool) {
|
|
for _, item := range items {
|
|
if item.Title == title {
|
|
return item, true
|
|
}
|
|
}
|
|
return api.TodoItem{}, false
|
|
}
|
|
|
|
func Markdown(item api.TodoItem) string {
|
|
if item.Body == "" {
|
|
return "- " + item.Title
|
|
}
|
|
lines := []string{"- " + item.Title}
|
|
for _, line := range strings.Split(item.Body, "\n") {
|
|
if line == "" {
|
|
lines = append(lines, "")
|
|
continue
|
|
}
|
|
lines = append(lines, " "+line)
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func dedent(lines []string) string {
|
|
for len(lines) > 0 && lines[len(lines)-1] == "" {
|
|
lines = lines[:len(lines)-1]
|
|
}
|
|
|
|
indent := -1
|
|
for _, line := range lines {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
n := len(line) - len(strings.TrimLeft(line, " \t"))
|
|
if indent < 0 || n < indent {
|
|
indent = n
|
|
}
|
|
}
|
|
|
|
out := []string{}
|
|
for _, line := range lines {
|
|
if line == "" {
|
|
out = append(out, "")
|
|
continue
|
|
}
|
|
out = append(out, line[indent:])
|
|
}
|
|
return strings.Join(out, "\n")
|
|
}
|