Add todo mode and replace the plan with TODO.md

This commit is contained in:
Ian Gulliver
2026-10-15 07:23:17 -07:00
parent 60a1d221f4
commit 1f44bc6d91
25 changed files with 1259 additions and 637 deletions
+142
View File
@@ -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")
}
+58
View File
@@ -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)
}
}