feat: initial parser for todo.txt format with spec and test cases
- Add parser for todo.txt tasks supporting priority, dates, projects, contexts, and key:value metadata - Include full todo.txt format specification (SPECS.md) and visual description (description.svg) - Add sample test.todo.txt file with valid, borderline, and malformed cases - Initialize Go module and main entrypoint for parsing and JSON output - Add .gitignore for binary artifacts
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
todotxt2remind
|
||||||
198
SPECS.md
Normal file
198
SPECS.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
This spec file has been taken from the original todo.txt project at https://github.com/todotxt/todo.txt
|
||||||
|
|
||||||
|
# todo.txt format
|
||||||
|
[](https://gitter.im/todotxt/todotxt)
|
||||||
|
|
||||||
|
A complete primer on the whys and hows of todo.txt.
|
||||||
|
|
||||||
|
The first and most important rule of todo.txt:
|
||||||
|
|
||||||
|
> A single line in your todo.txt text file represents a single task.
|
||||||
|
|
||||||
|
|
||||||
|
## Why plain text?
|
||||||
|
|
||||||
|
Plain text is software and operating system agnostic. It's searchable, portable, lightweight, and easily manipulated. It's unstructured. It works when someone else's web server is down or your Outlook .PST file is corrupt. There's no exporting and importing, no databases or tags or flags or stars or prioritizing or _insert company name here_-induced rules on what you can and can't do with it.
|
||||||
|
|
||||||
|
|
||||||
|
## The 3 axes of an effective todo list
|
||||||
|
|
||||||
|
Using special notation in todo.txt, you can create a list that's sliceable by 3 key axes.
|
||||||
|
|
||||||
|
|
||||||
|
### Priority
|
||||||
|
Your todo list should be able to tell you what's the next most important thing for you to get done - either by project or by context or overall. You can optionally assign tasks a priority that'll bubble them up to the top of the list.
|
||||||
|
|
||||||
|
|
||||||
|
### Project
|
||||||
|
The only way to move a big project forward is to tackle a small subtask associated with it. Your `todo.txt` should be able to list out all the tasks specific to a project.
|
||||||
|
|
||||||
|
In order to move along a project like "Cleaning out the garage," my task list should give me the next logical action to take in order to move that project along. "Clean out the garage" isn't a good todo item; but "Call Goodwill to schedule pickup" in the "Clean out garage" project is.
|
||||||
|
|
||||||
|
|
||||||
|
### Context
|
||||||
|
[Getting Things Done](https://en.wikipedia.org/wiki/Getting_Things_Done) author David Allen suggests splitting up your task lists by context - ie, the place and situation where you'll work on the job. Messages that you need to send go in the `@email` context; calls to be made `@phone`, household projects `@home`.
|
||||||
|
|
||||||
|
That way, when you've got a few minutes in the car with your cell phone, you can easily check your `@phone` tasks and make a call or two while you have the opportunity.
|
||||||
|
|
||||||
|
This is all possible inside `todo.txt`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## `todo.txt` format rules
|
||||||
|
|
||||||
|
<img src="./description.svg" width="100%" height="500">
|
||||||
|
|
||||||
|
Your `todo.txt` is a plain text file. To take advantage of structured task metadata like priority, projects, context, creation, and completion date, there are a few simple but flexible file format rules.
|
||||||
|
|
||||||
|
Philosophically, the `todo.txt` file format has two goals:
|
||||||
|
|
||||||
|
- The file contents should be human-readable without requiring any tools other than a plain text viewer or editor.
|
||||||
|
- A user can manipulate the file contents in a plain text editor in sensible, expected ways. For example, a text editor that can sort lines alphabetically should be able to sort your task list in a meaningful way.
|
||||||
|
|
||||||
|
These two goals are why, for example, lines start with priority and/or dates, so that they are easily sorted by priority or time, and completed items are marked with an `x`, which both sorts at the bottom of an alphabetical list and looks like a filled-in checkbox.
|
||||||
|
|
||||||
|
Here are the rest.
|
||||||
|
|
||||||
|
|
||||||
|
## Incomplete Tasks: 3 Format Rules
|
||||||
|
|
||||||
|
The beauty of todo.txt is that it's completely unstructured; the fields you can attach to each task are only limited by your imagination. To get started, use special notation to indicate task context (e.g. `@phone` ), project (e.g. `+GarageSale` ) and priority (e.g. `(A)` ).
|
||||||
|
|
||||||
|
A todo.txt file might look like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
(A) Thank Mom for the meatballs @phone
|
||||||
|
(B) Schedule Goodwill pickup +GarageSale @phone
|
||||||
|
Post signs around the neighborhood +GarageSale
|
||||||
|
@GroceryStore pies
|
||||||
|
```
|
||||||
|
|
||||||
|
A search and filter for the `@phone` contextual items would output:
|
||||||
|
|
||||||
|
```
|
||||||
|
(A) Thank Mom for the meatballs @phone
|
||||||
|
(B) Schedule Goodwill pickup +GarageSale @phone
|
||||||
|
```
|
||||||
|
|
||||||
|
To just see the `+GarageSale` project items would output:
|
||||||
|
|
||||||
|
```
|
||||||
|
(B) Schedule Goodwill pickup +GarageSale @phone
|
||||||
|
Post signs around the neighborhood +GarageSale
|
||||||
|
```
|
||||||
|
|
||||||
|
There are three formatting rules for current todo's.
|
||||||
|
|
||||||
|
### Rule 1: If priority exists, it ALWAYS appears first.
|
||||||
|
|
||||||
|
The priority is an uppercase character from A-Z enclosed in parentheses and followed by a space.
|
||||||
|
|
||||||
|
This task has a priority:
|
||||||
|
|
||||||
|
```
|
||||||
|
(A) Call Mom
|
||||||
|
```
|
||||||
|
|
||||||
|
These tasks do not have any priorities:
|
||||||
|
|
||||||
|
```
|
||||||
|
Really gotta call Mom (A) @phone @someday
|
||||||
|
(b) Get back to the boss
|
||||||
|
(B)->Submit TPS report
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Rule 2: A task's creation date may optionally appear directly after priority and a space.
|
||||||
|
|
||||||
|
If there is no priority, the creation date appears first. If the creation date exists, it should be in the format `YYYY-MM-DD`.
|
||||||
|
|
||||||
|
These tasks have creation dates:
|
||||||
|
|
||||||
|
```
|
||||||
|
2011-03-02 Document +TodoTxt task format
|
||||||
|
(A) 2011-03-02 Call Mom
|
||||||
|
```
|
||||||
|
|
||||||
|
This task doesn't have a creation date:
|
||||||
|
|
||||||
|
```
|
||||||
|
(A) Call Mom 2011-03-02
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Rule 3: Contexts and Projects may appear anywhere in the line _after_ priority/prepended date.
|
||||||
|
|
||||||
|
- A *context* is preceded by a single space and an at-sign (`@`).
|
||||||
|
- A *project* is preceded by a single space and a plus-sign (`+`).
|
||||||
|
- A *project* or *context* contains any non-whitespace character.
|
||||||
|
- A *task* may have zero, one, or more than one *projects* and *contexts* included in it.
|
||||||
|
|
||||||
|
For example, this task is part of the `+Family` and `+PeaceLoveAndHappiness` projects as well as the `@iphone` and `@phone` contexts:
|
||||||
|
|
||||||
|
```
|
||||||
|
(A) Call Mom +Family +PeaceLoveAndHappiness @iphone @phone
|
||||||
|
```
|
||||||
|
|
||||||
|
This task has no contexts in it:
|
||||||
|
|
||||||
|
```
|
||||||
|
Email SoAndSo at soandso@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
This task has no projects in it:
|
||||||
|
|
||||||
|
```
|
||||||
|
Learn how to add 2+2
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Complete Tasks: 2 Format Rules
|
||||||
|
|
||||||
|
Two things indicate that a task has been completed.
|
||||||
|
|
||||||
|
|
||||||
|
### Rule 1: A completed task starts with a lowercase x character (`x`).
|
||||||
|
|
||||||
|
If a task starts with an `x` (case-sensitive and lowercase) followed directly by a space, it is marked as complete.
|
||||||
|
|
||||||
|
This is a complete task:
|
||||||
|
|
||||||
|
```
|
||||||
|
x 2011-03-03 Call Mom
|
||||||
|
```
|
||||||
|
|
||||||
|
These are not complete tasks.
|
||||||
|
|
||||||
|
```
|
||||||
|
xylophone lesson
|
||||||
|
X 2012-01-01 Make resolutions
|
||||||
|
(A) x Find ticket prices
|
||||||
|
```
|
||||||
|
|
||||||
|
We use a lowercase x so that completed tasks sort to the bottom of the task list using standard sort tools.
|
||||||
|
|
||||||
|
|
||||||
|
### Rule 2: The date of completion appears directly after the x, separated by a space.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
x 2011-03-02 2011-03-01 Review Tim's pull request +TodoTxtTouch @github
|
||||||
|
```
|
||||||
|
|
||||||
|
If you’ve prepended the creation date to your task, on completion it will appear directly after the completion date. This is so your completed tasks sort by date using standard sort tools. Many Todo.txt clients discard priority on task completion. To preserve it, use the `key:value` format described below (e.g. `pri:A`)
|
||||||
|
|
||||||
|
With the completed date (required), if you've used the prepended date (optional), you can calculate how many days it took to complete a task.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Additional File Format Definitions
|
||||||
|
|
||||||
|
Tool developers may define additional formatting rules for extra metadata.
|
||||||
|
|
||||||
|
Developers should use the format `key:value` to define additional metadata (e.g. `due:2010-01-02` as a due date).
|
||||||
|
|
||||||
|
Both `key` and `value` must consist of non-whitespace characters, which are not colons. Only one colon separates the `key` and `value`.
|
||||||
|
|
||||||
38
description.svg
Normal file
38
description.svg
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="svg2" viewBox="0 0 632.2 281">
|
||||||
|
<style>
|
||||||
|
.st0{font-family:Courier;font-size:12px;}
|
||||||
|
.st1{fill:none;stroke:#000000;stroke-miterlimit:10;}
|
||||||
|
.st2{font-family:Arial;font-size:10px;}
|
||||||
|
</style>
|
||||||
|
<rect width="100%" height="100%" fill="#FFF"/>
|
||||||
|
<text class="st0" transform="translate(4.024 172.96)">
|
||||||
|
x (A) 2016-05-20 2016-04-30 measure space for +chapelShelving @chapel due:2016-05-30
|
||||||
|
</text>
|
||||||
|
<path d="M15.2 159v-6H.5v6m36 0v-6H21.8v6m96.5 0v-6H47.5v6m149.3 0v-6H126v6m483.7.2V153H206v6.1m238.7 22.8v7.2H335V182m274.7-.1v7.2H509V182m-7.5-.1v7.2h-49.1V182" class="st1"/>
|
||||||
|
<text class="st2" transform="rotate(-45 161.175 55.627)">
|
||||||
|
Optional — Marks completion
|
||||||
|
</text>
|
||||||
|
<text class="st2" transform="rotate(-45 171.8 29.976)">
|
||||||
|
Optional -- Marks priority
|
||||||
|
</text>
|
||||||
|
<text class="st2" transform="rotate(-45 198.684 -34.92)">
|
||||||
|
Optional -- Completion Date
|
||||||
|
</text>
|
||||||
|
<text transform="rotate(-45 237.935 -129.68)">
|
||||||
|
<tspan x="0" y="0" class="st2">Optional -- Creation Date</tspan>
|
||||||
|
<tspan x="0" y="12" class="st2">(must be specified if completion date is)</tspan>
|
||||||
|
</text>
|
||||||
|
<text transform="rotate(-45 352.18 -405.478)">
|
||||||
|
<tspan x="0" y="0" class="st2">Description; tags (optional)</tspan>
|
||||||
|
<tspan x="0" y="12" class="st2">can be placed anywhere in here</tspan>
|
||||||
|
</text>
|
||||||
|
<text class="st2" transform="rotate(45 -60.77 576.533)">
|
||||||
|
Project Tag
|
||||||
|
</text>
|
||||||
|
<text class="st2" transform="rotate(45 -17.238 681.627)">
|
||||||
|
Context tag
|
||||||
|
</text>
|
||||||
|
<text class="st2" transform="rotate(45 23.978 781.127)">
|
||||||
|
Special key/value tag
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
3
go.mod
Normal file
3
go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module git.donadeo.net/pdonadeo/todotxt2remind
|
||||||
|
|
||||||
|
go 1.24.1
|
||||||
201
internal/parser/parser.go
Normal file
201
internal/parser/parser.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Task represents a parsed todo item.
|
||||||
|
type Task struct {
|
||||||
|
Raw string `json:"raw"`
|
||||||
|
Completed bool `json:"completed"`
|
||||||
|
CompletionDate *time.Time `json:"completion_date,omitempty"`
|
||||||
|
Priority *rune `json:"priority,omitempty"` // 'A'..'Z'
|
||||||
|
CreationDate *time.Time `json:"creation_date,omitempty"`
|
||||||
|
DueDate *time.Time `json:"due_date,omitempty"` // parsed from due:YYYY-MM-DD
|
||||||
|
Description string `json:"description"` // description with projects/contexts/metadata removed
|
||||||
|
Projects []string `json:"projects"`
|
||||||
|
Contexts []string `json:"contexts"`
|
||||||
|
Metadata map[string]string `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PriorityAsRemind returns an integer representing the priority for sorting.
|
||||||
|
// A = 9999 (highest), Z = 0 (lowest), absent = 5000.
|
||||||
|
func (t Task) PriorityAsRemind() int {
|
||||||
|
if t.Priority == nil {
|
||||||
|
return 5000
|
||||||
|
}
|
||||||
|
p := *t.Priority
|
||||||
|
if p < 'A' || p > 'Z' {
|
||||||
|
return 5000
|
||||||
|
}
|
||||||
|
// A=9999, Z=0, linear scale
|
||||||
|
return 9999 - int(p-'A')*400
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Task) MarshalJSON() ([]byte, error) {
|
||||||
|
type Alias Task
|
||||||
|
aux := struct {
|
||||||
|
Alias
|
||||||
|
Priority *string `json:"priority,omitempty"`
|
||||||
|
PriorityValue *int `json:"priority_value,omitempty"`
|
||||||
|
}{
|
||||||
|
Alias: (Alias)(t),
|
||||||
|
Priority: nil,
|
||||||
|
PriorityValue: nil,
|
||||||
|
}
|
||||||
|
if t.Priority != nil {
|
||||||
|
s := string(*t.Priority)
|
||||||
|
aux.Priority = &s
|
||||||
|
val := t.PriorityAsRemind()
|
||||||
|
aux.PriorityValue = &val
|
||||||
|
}
|
||||||
|
return json.Marshal(aux)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
dateRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
|
||||||
|
priorityHead = regexp.MustCompile(`^\([A-Z]\)\s+`)
|
||||||
|
completedHead = regexp.MustCompile(`^x\s+`)
|
||||||
|
spaceRe = regexp.MustCompile(`\s+`)
|
||||||
|
dateLayout = "2006-01-02"
|
||||||
|
// Protocols da escludere come chiave dei metadati
|
||||||
|
protocols = map[string]struct{}{
|
||||||
|
"http": {},
|
||||||
|
"https": {},
|
||||||
|
"ftp": {},
|
||||||
|
"mailto": {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// isProtocolKey controlla se la chiave è un protocollo standard.
|
||||||
|
func isProtocolKey(k string) bool {
|
||||||
|
_, found := protocols[strings.ToLower(k)]
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseReader reads all lines from r and returns parsed tasks.
|
||||||
|
// Blank lines are skipped.
|
||||||
|
func ParseReader(r io.Reader) ([]Task, []error) {
|
||||||
|
var tasks []Task
|
||||||
|
var errs []error
|
||||||
|
sc := bufio.NewScanner(r)
|
||||||
|
lineNo := 0
|
||||||
|
for sc.Scan() {
|
||||||
|
lineNo++
|
||||||
|
line := sc.Text()
|
||||||
|
trim := strings.TrimSpace(line)
|
||||||
|
if trim == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t, err := ParseLine(line)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("line %d: %w", lineNo, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tasks = append(tasks, t)
|
||||||
|
}
|
||||||
|
if err := sc.Err(); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
return tasks, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseLine parses one line in the todo.txt format and returns a Task.
|
||||||
|
func ParseLine(line string) (Task, error) {
|
||||||
|
t := Task{
|
||||||
|
Raw: line,
|
||||||
|
Metadata: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
working := strings.TrimSpace(line)
|
||||||
|
|
||||||
|
// 1) Completed?
|
||||||
|
if completedHead.MatchString(working) {
|
||||||
|
t.Completed = true
|
||||||
|
working = completedHead.ReplaceAllString(working, "")
|
||||||
|
toks := splitTokens(working)
|
||||||
|
consumed := 0
|
||||||
|
if len(toks) > 0 && dateRe.MatchString(toks[0]) {
|
||||||
|
if dt, err := time.Parse(dateLayout, toks[0]); err == nil {
|
||||||
|
t.CompletionDate = &dt
|
||||||
|
consumed = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if consumed < len(toks) && dateRe.MatchString(toks[consumed]) {
|
||||||
|
if dt, err := time.Parse(dateLayout, toks[consumed]); err == nil {
|
||||||
|
t.CreationDate = &dt
|
||||||
|
consumed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if consumed > 0 {
|
||||||
|
working = strings.TrimSpace(strings.Join(toks[consumed:], " "))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if priorityHead.MatchString(working) {
|
||||||
|
if len(working) >= 3 {
|
||||||
|
r := rune(working[1])
|
||||||
|
t.Priority = &r
|
||||||
|
}
|
||||||
|
working = priorityHead.ReplaceAllString(working, "")
|
||||||
|
}
|
||||||
|
toks := splitTokens(working)
|
||||||
|
if len(toks) > 0 && dateRe.MatchString(toks[0]) {
|
||||||
|
if dt, err := time.Parse(dateLayout, toks[0]); err == nil {
|
||||||
|
t.CreationDate = &dt
|
||||||
|
working = strings.TrimSpace(strings.Join(toks[1:], " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens := splitTokens(working)
|
||||||
|
descParts := make([]string, 0, len(tokens))
|
||||||
|
for _, tok := range tokens {
|
||||||
|
if strings.HasPrefix(tok, "+") && len(tok) > 1 {
|
||||||
|
t.Projects = append(t.Projects, tok[1:])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(tok, "@") && len(tok) > 1 {
|
||||||
|
t.Contexts = append(t.Contexts, tok[1:])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Nuova logica per metadati: nessuno spazio, almeno un ':', chiave non protocollo
|
||||||
|
if !strings.ContainsAny(tok, " \t") && strings.Contains(tok, ":") {
|
||||||
|
parts := strings.SplitN(tok, ":", 2)
|
||||||
|
k, v := parts[0], parts[1]
|
||||||
|
if k != "" && v != "" && !isProtocolKey(k) {
|
||||||
|
t.Metadata[k] = v
|
||||||
|
// Se il campo è due:YYYY-MM-DD, parsalo anche come DueDate
|
||||||
|
if k == "due" && dateRe.MatchString(v) {
|
||||||
|
if dt, err := time.Parse(dateLayout, v); err == nil {
|
||||||
|
t.DueDate = &dt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// else part of description
|
||||||
|
descParts = append(descParts, tok)
|
||||||
|
}
|
||||||
|
t.Description = strings.TrimSpace(strings.Join(descParts, " "))
|
||||||
|
|
||||||
|
if t.Completed && t.CompletionDate == nil {
|
||||||
|
return t, errors.New("completed task missing completion date (spec requires completion date directly after 'x')")
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitTokens splits a line on whitespace preserving token order and ignoring extra spaces.
|
||||||
|
func splitTokens(s string) []string {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return spaceRe.Split(strings.TrimSpace(s), -1)
|
||||||
|
}
|
||||||
31
main.go
Normal file
31
main.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.donadeo.net/pdonadeo/todotxt2remind/internal/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Fprintf(os.Stderr, "usage: example /path/to/todo.txt\n")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
f, err := os.Open(os.Args[1])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = f.Close() }()
|
||||||
|
|
||||||
|
tasks, errors := parser.ParseReader(f)
|
||||||
|
if len(errors) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "errors while parsing:\n")
|
||||||
|
for _, e := range errors {
|
||||||
|
fmt.Fprintf(os.Stderr, " - %v\n", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b, _ := json.MarshalIndent(tasks, "", " ")
|
||||||
|
fmt.Println(string(b))
|
||||||
|
}
|
||||||
90
test.todo.txt
Normal file
90
test.todo.txt
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Valid tasks
|
||||||
|
(A) Call Mom +Family @phone
|
||||||
|
(B) Schedule Goodwill pickup +GarageSale @phone
|
||||||
|
Post signs around the neighborhood +GarageSale
|
||||||
|
@GroceryStore pies
|
||||||
|
2011-03-02 Document +TodoTxt task format
|
||||||
|
(A) 2011-03-02 Call Mom
|
||||||
|
x 2011-03-03 Call Mom
|
||||||
|
x 2011-03-02 2011-03-01 Review Tim's pull request +TodoTxtTouch @github
|
||||||
|
(A) Call Mom due:2024-12-31
|
||||||
|
x 2024-11-24 2024-11-20 Finish project due:2024-12-01 +Work @office
|
||||||
|
|
||||||
|
# Valid metadata
|
||||||
|
Buy milk due:2024-12-01
|
||||||
|
Do taxes taxyear:2024
|
||||||
|
Plan trip location:Rome
|
||||||
|
|
||||||
|
# Valid multiple projects/contexts
|
||||||
|
(A) Call Mom +Family +PeaceLoveAndHappiness @iphone @phone
|
||||||
|
|
||||||
|
# Borderline: priority not at start
|
||||||
|
Really gotta call Mom (A) @phone @someday
|
||||||
|
(B)->Submit TPS report
|
||||||
|
(b) Get back to the boss
|
||||||
|
|
||||||
|
# Borderline: creation date not at start
|
||||||
|
(A) Call Mom 2011-03-02
|
||||||
|
|
||||||
|
# Borderline: completed but missing completion date (should error)
|
||||||
|
x Call Mom
|
||||||
|
|
||||||
|
# Borderline: project/context with only symbol
|
||||||
|
Do something +
|
||||||
|
Do something @
|
||||||
|
|
||||||
|
# Borderline: key:value with protocol as key (should NOT be metadata)
|
||||||
|
http://example.com
|
||||||
|
https://example.com
|
||||||
|
ftp://server.com
|
||||||
|
mailto:someone@example.com
|
||||||
|
|
||||||
|
# Borderline: key:value with protocol as key (should NOT be metadata)
|
||||||
|
http:example.com
|
||||||
|
https:example.com
|
||||||
|
ftp:server.com
|
||||||
|
mailto:someone@example.com
|
||||||
|
|
||||||
|
# Borderline: key:value with value as URL (should be metadata)
|
||||||
|
link:http://example.com
|
||||||
|
website:https://example.com
|
||||||
|
|
||||||
|
# Borderline: key:value with spaces (should NOT be metadata)
|
||||||
|
foo:bar baz
|
||||||
|
foo :bar
|
||||||
|
foo: bar
|
||||||
|
|
||||||
|
# Borderline: key:value with multiple colons (only first is split)
|
||||||
|
multi:colon:example
|
||||||
|
|
||||||
|
# Borderline: empty line
|
||||||
|
|
||||||
|
|
||||||
|
# Malformed: priority with lowercase letter
|
||||||
|
(a) Call Mom
|
||||||
|
|
||||||
|
# Malformed: completed with uppercase X
|
||||||
|
X 2012-01-01 Make resolutions
|
||||||
|
|
||||||
|
# Malformed: completed with x not at start
|
||||||
|
(A) x Find ticket prices
|
||||||
|
|
||||||
|
# Malformed: date with invalid format
|
||||||
|
(A) 2024-13-99 Call Mom
|
||||||
|
x 2024-99-99 Call Mom
|
||||||
|
|
||||||
|
# Malformed: key:value with whitespace in key or value
|
||||||
|
foo bar:baz
|
||||||
|
foo:bar baz
|
||||||
|
foo:bar:baz
|
||||||
|
|
||||||
|
# Malformed: only symbols
|
||||||
|
+
|
||||||
|
@
|
||||||
|
:
|
||||||
|
|
||||||
|
# Malformed: random text
|
||||||
|
Just a random line with no special tokens
|
||||||
|
|
||||||
|
# Malformed: only spaces
|
||||||
|
|
||||||
Reference in New Issue
Block a user