@@ -138,6 +138,8 @@ func commandPhrase(parts ...string) string {
138138var markdownAddressFlags = []string {"--body" , "--section" , "--item" , "--item-type" , "--task" , "--after" , "--before" , "--head" , "--tail" }
139139var markdownSetFlags = append (append ([]string {"--json" }, markdownAddressFlags ... ), "--hidden" )
140140var markdownDeleteFlags = markdownAddressFlags
141+ var markdownTaskListFlags = []string {"--section" , "--before" , "--after" }
142+ var markdownListAddFlags = []string {"--section" , "--before" , "--after" , "--task" }
141143
142144var allCommandSpecs = buildCommandSpecs ()
143145
@@ -155,6 +157,10 @@ func buildCommandSpecs() []commandSpec {
155157 command ("section replace" , "section replace <path> <heading> <content>" , "Replace the body under one Markdown heading." , ClassIdempotent , true , parseSection ("replace" )),
156158 command ("section append" , "section append <path> <heading> <content>" , "Append a block fragment under one Markdown heading." , ClassNonIdempotent , true , parseSection ("append" )),
157159 command ("section prepend" , "section prepend <path> <heading> <content>" , "Prepend a block fragment under one Markdown heading." , ClassNonIdempotent , true , parseSection ("prepend" )),
160+ command ("task close" , "task close <path> <text> [--section <heading>] [--before <item>] [--after <item>]" , "Ensure one Markdown task is closed." , ClassIdempotent , true , parseMarkdownTaskList ("task close" ), markdownTaskListFlags ... ),
161+ command ("task open" , "task open <path> <text> [--section <heading>] [--before <item>] [--after <item>]" , "Ensure one Markdown task is open, creating it when a destination is addressed." , ClassIdempotent , true , parseMarkdownTaskList ("task open" ), markdownTaskListFlags ... ),
162+ command ("list add" , "list add <path> <text> [--section <heading>] [--task] [--before <item>] [--after <item>]" , "Add one Markdown list item." , ClassNonIdempotent , true , parseMarkdownTaskList ("list add" ), markdownListAddFlags ... ),
163+ command ("task add" , "task add <path> <text> [--section <heading>] [--before <item>] [--after <item>]" , "Add one open Markdown task." , ClassNonIdempotent , true , parseMarkdownTaskList ("task add" ), markdownTaskListFlags ... ),
158164 command ("create" , "create <path> [<content>]" , "Create a new file; omitted content uses an extension-aware default." , ClassIdempotent , true , parseCreate ),
159165 command ("move" , "move <src> <dst>" , "Move a file path." , ClassIdempotent , true , parseFileVerb ("move" )),
160166 command ("copy" , "copy <src> <dst>" , "Copy a file path." , ClassIdempotent , true , parseFileVerb ("copy" )),
@@ -445,6 +451,57 @@ func parseSection(action string) commandParser {
445451 }
446452}
447453
454+ func parseMarkdownTaskList (verb string ) commandParser {
455+ return func (inv commandInvocation ) ([]Operation , error ) {
456+ spec , op := inv .Spec , inv .Op
457+ path , text , address , asTask , err := parseMarkdownTaskListArgs (inv .Args , verb == "list add" )
458+ if err != nil {
459+ return nil , err
460+ }
461+ actualVerb := verb
462+ if asTask {
463+ actualVerb = "task add"
464+ }
465+ op .Verb , op .Kind , op .Class , op .Path , op .Value , op .ValueMode = actualVerb , "md-task-list" , spec .Class , path , text , ValueModeString
466+ op .Target = PlanTarget {Path : path , Part : "body" , Section : address .Section }
467+ op .Markdown = address
468+ return parsedOperation (op )
469+ }
470+ }
471+
472+ func parseMarkdownTaskListArgs (args []string , allowTaskFlag bool ) (path , text string , address markdownAddress , asTask bool , err error ) {
473+ var positional []string
474+ for i := 0 ; i < len (args ); i ++ {
475+ arg := args [i ]
476+ if arg == "--task" {
477+ if ! allowTaskFlag {
478+ return "" , "" , markdownAddress {}, false , usagef ("--task is only accepted by list add" )
479+ }
480+ asTask = true
481+ continue
482+ }
483+ parsed , next , err := parseMarkdownAddressFlag (args , i , & address , markdownTaskListAddressFlagOptions )
484+ if err != nil {
485+ return "" , "" , markdownAddress {}, false , err
486+ }
487+ if parsed {
488+ i = next
489+ continue
490+ }
491+ if strings .HasPrefix (arg , "--" ) {
492+ return "" , "" , markdownAddress {}, false , usagef ("unknown Markdown task/list flag %s" , arg )
493+ }
494+ positional = append (positional , arg )
495+ }
496+ if len (positional ) != 2 {
497+ return "" , "" , markdownAddress {}, false , usagef ("usage: etch <task/list command> <path> <text> [flags]" )
498+ }
499+ if _ , err := markdownPlacementFromFlags (false , false , address .Before , address .After ); err != nil {
500+ return "" , "" , markdownAddress {}, false , err
501+ }
502+ return positional [0 ], positional [1 ], address , asTask , nil
503+ }
504+
448505func parseTable (format string , tablePath ... string ) commandParser {
449506 return func (inv commandInvocation ) ([]Operation , error ) {
450507 op , args := inv .Op , inv .Args
@@ -830,10 +887,10 @@ func fillDescriptor(op *Operation) {
830887 } else if op .Target .Selector != "" {
831888 parts = append (parts , op .Target .Selector )
832889 }
833- if op .Target .Section != "" && op .Kind != "md-field" {
890+ if op .Target .Section != "" && op .Kind != "md-field" && op . Kind != "md-task-list" {
834891 parts = append (parts , shellQuote (op .Target .Section ))
835892 }
836- if op .Kind == "md-field" {
893+ if op .Kind == "md-field" || op . Kind == "md-task-list" {
837894 parts = appendMarkdownAddressDescriptor (parts , op .Markdown )
838895 }
839896 if op .Target .Range != "" {
@@ -971,9 +1028,9 @@ func printHelp(w io.Writer, topic string, all bool) error {
9711028 }
9721029 fmt .Fprintln (w )
9731030 if all {
974- fmt .Fprint (w , "Topics: model, scripts, selectors, values, fields, plans, security, conflicts, addressing, section, table, csv\n " )
1031+ fmt .Fprint (w , "Topics: model, scripts, selectors, values, fields, plans, security, conflicts, addressing, section, tasks, table, csv\n " )
9751032 } else {
976- fmt .Fprint (w , "Topics: model, scripts, selectors, values, fields, plans, security, conflicts, addressing, section, table, csv. Use --all for plumbing commands.\n " )
1033+ fmt .Fprint (w , "Topics: model, scripts, selectors, values, fields, plans, security, conflicts, addressing, section, tasks, table, csv. Use --all for plumbing commands.\n " )
9771034 }
9781035 case "scripts" :
9791036 fmt .Fprint (w , scriptsHelp )
@@ -995,6 +1052,8 @@ func printHelp(w io.Writer, topic string, all bool) error {
9951052 fmt .Fprint (w , modelHelp )
9961053 case "section" :
9971054 fmt .Fprint (w , sectionHelp )
1055+ case "tasks" :
1056+ fmt .Fprint (w , tasksHelp )
9981057 case "table" , "csv" :
9991058 fmt .Fprint (w , tableHelp )
10001059 default :
@@ -1096,6 +1155,22 @@ Repeated matching headings are ambiguous. Append/prepend trim payload boundary b
10961155and use one blank line between non-empty block fragments.
10971156`
10981157
1158+ const tasksHelp = `Markdown task/list commands operate on exact source-normalized item text.
1159+
1160+ Commands:
1161+ etch task close note.md "Send follow-up" --section Actions
1162+ etch task open note.md "Send follow-up" --section Actions
1163+ etch list add note.md "Launch notes" --section Actions
1164+ etch task add note.md "Send follow-up" --section Actions
1165+
1166+ task close changes [ ] to [x] and never creates missing tasks.
1167+ task open ensures an open task: it reopens [x]/[X], no-ops on [ ], and creates
1168+ missing tasks only when a destination address such as --section, --before, or
1169+ --after is supplied. Custom checkbox statuses fail.
1170+ --before and --after match list items, not arbitrary prose.
1171+ list add and task add create source from plain item text; do not include "- " or "- [ ]".
1172+ `
1173+
10991174const tableHelp = `Tables are ordered rows and named columns of string cells.
11001175
11011176CSV:
0 commit comments