Add todo mode and replace the plan with TODO.md
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package todo_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/todo"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
src := "# TODO\n\n" +
|
||||
"Notes before the list are not items.\n\n" +
|
||||
"- Public key pinning\n" +
|
||||
" When libcurl4-openssl-dev >= 7.39.0, add support to the C client via the\n" +
|
||||
" options struct for public key pinning.\n" +
|
||||
"\n" +
|
||||
" ```\n" +
|
||||
" - not an item\n" +
|
||||
" ```\n" +
|
||||
"- [ ] Client ID rotation\n" +
|
||||
" - JS client\n" +
|
||||
" - C client\n" +
|
||||
"- [x] Already done\n" +
|
||||
" with a body\n" +
|
||||
"\n" +
|
||||
"## Web\n\n" +
|
||||
"1. Double-click zoom in\n" +
|
||||
"2. Pinch to zoom\n" +
|
||||
"\n" +
|
||||
"A trailing paragraph.\n"
|
||||
|
||||
want := []api.TodoItem{
|
||||
{Title: "Public key pinning", Section: "TODO", Body: "When libcurl4-openssl-dev >= 7.39.0, add support to the C client via the\noptions struct for public key pinning.\n\n```\n- not an item\n```"},
|
||||
{Title: "Client ID rotation", Section: "TODO", Body: "- JS client\n- C client"},
|
||||
{Title: "Double-click zoom in", Section: "Web"},
|
||||
{Title: "Pinch to zoom", Section: "Web"},
|
||||
}
|
||||
|
||||
got := todo.Parse(src)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("parse:\n got %#v\nwant %#v", got, want)
|
||||
}
|
||||
|
||||
item, ok := todo.Find(got, "Client ID rotation")
|
||||
if !ok {
|
||||
t.Fatalf("find: no item")
|
||||
}
|
||||
if md := todo.Markdown(item); md != "- Client ID rotation\n - JS client\n - C client" {
|
||||
t.Fatalf("markdown: %q", md)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEmpty(t *testing.T) {
|
||||
if got := todo.Parse(""); len(got) != 0 {
|
||||
t.Fatalf("parse empty: %#v", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user