Add interactive Markdown checklist toggles in the ticket preview.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
package ticket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// taskCheckboxRe matches GFM/YouTrack-style task list items:
|
||||
//
|
||||
// - [ ] unchecked
|
||||
// - [x] checked
|
||||
// * [X] also accepted
|
||||
var taskCheckboxRe = regexp.MustCompile(`^(\s*)([-*+])\s+\[([ xX])\](\s*)(.*)$`)
|
||||
|
||||
// Checkbox is one task-list item inside a Markdown description.
|
||||
type Checkbox struct {
|
||||
Line int // 0-based line index in the source
|
||||
Checked bool
|
||||
Text string // label after the checkbox marker
|
||||
}
|
||||
|
||||
// ListCheckboxes returns all task-list items in markdown source order.
|
||||
func ListCheckboxes(markdown string) []Checkbox {
|
||||
if markdown == "" {
|
||||
return nil
|
||||
}
|
||||
lines := strings.Split(markdown, "\n")
|
||||
var out []Checkbox
|
||||
for i, line := range lines {
|
||||
m := taskCheckboxRe.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, Checkbox{
|
||||
Line: i,
|
||||
Checked: m[3] == "x" || m[3] == "X",
|
||||
Text: m[5],
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ToggleCheckbox flips the checked state of the checkbox at index
|
||||
// (0-based among ListCheckboxes results) and returns the updated markdown.
|
||||
func ToggleCheckbox(markdown string, index int) (string, error) {
|
||||
boxes := ListCheckboxes(markdown)
|
||||
if index < 0 || index >= len(boxes) {
|
||||
return markdown, fmt.Errorf("checkbox %d: not found", index)
|
||||
}
|
||||
lines := strings.Split(markdown, "\n")
|
||||
lineIdx := boxes[index].Line
|
||||
m := taskCheckboxRe.FindStringSubmatch(lines[lineIdx])
|
||||
if m == nil {
|
||||
return markdown, fmt.Errorf("checkbox %d: line changed", index)
|
||||
}
|
||||
mark := " "
|
||||
if !(m[3] == "x" || m[3] == "X") {
|
||||
mark = "x"
|
||||
}
|
||||
lines[lineIdx] = m[1] + m[2] + " [" + mark + "]" + m[4] + m[5]
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package ticket
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListCheckboxes(t *testing.T) {
|
||||
src := `# Steps
|
||||
|
||||
- [ ] one
|
||||
- [x] two
|
||||
* [X] three
|
||||
- [ ] nested
|
||||
|
||||
Not a box: [ ] plain
|
||||
`
|
||||
got := ListCheckboxes(src)
|
||||
if len(got) != 4 {
|
||||
t.Fatalf("len=%d want 4: %+v", len(got), got)
|
||||
}
|
||||
if got[0].Text != "one" || got[0].Checked || got[0].Line != 2 {
|
||||
t.Fatalf("first: %+v", got[0])
|
||||
}
|
||||
if !got[1].Checked || got[1].Text != "two" {
|
||||
t.Fatalf("second: %+v", got[1])
|
||||
}
|
||||
if !got[2].Checked || got[2].Text != "three" {
|
||||
t.Fatalf("third: %+v", got[2])
|
||||
}
|
||||
if got[3].Text != "nested" || got[3].Checked || got[3].Line != 5 {
|
||||
t.Fatalf("nested: %+v", got[3])
|
||||
}
|
||||
}
|
||||
|
||||
func TestToggleCheckbox(t *testing.T) {
|
||||
src := "- [ ] a\n- [x] b\n"
|
||||
out, err := ToggleCheckbox(src, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out != "- [x] a\n- [x] b\n" {
|
||||
t.Fatalf("toggle 0: %q", out)
|
||||
}
|
||||
out, err = ToggleCheckbox(out, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out != "- [x] a\n- [ ] b\n" {
|
||||
t.Fatalf("toggle 1: %q", out)
|
||||
}
|
||||
if _, err := ToggleCheckbox(src, 9); err == nil {
|
||||
t.Fatal("expected error for bad index")
|
||||
}
|
||||
}
|
||||
|
||||
func TestToggleCheckboxPreservesIndentAndBullet(t *testing.T) {
|
||||
src := " * [X] deep"
|
||||
out, err := ToggleCheckbox(src, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out != " * [ ] deep" {
|
||||
t.Fatalf("got %q", out)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user