Markdown Todos

The Markdown Format

In addition to flat Todo.txt files, TodoFiles.jl can parse markdown files where #-headings define sections and - list items are todo entries:

# Work
- (A) Finish report @office +ClientProject
- Email client @office

## Personal
- Buy groceries @store +Errands
- (B) Call Mom @phone +Family due:2024-02-01

Each list item is parsed using the same Todo.txt rules as parse_todo.

Subtasks

Indented - items under a top-level todo are parsed as subtasks:

subtask_text = """
# Work
- (A) Finish report @office
    - Write introduction
    - Add charts
- Email client
"""

sections = parse_markdown_todos(subtask_text)
sections[1].todos[1]
Todo: (A) Finish report @office
sections[1].todos[1].subtasks
2-element Vector{Todo}:
 Todo: Write introduction
 Todo: Add charts

When iterating a MarkdownTodoFile, subtasks are flattened alongside their parents:

sub_path = tempname() * ".md"
write_markdown_todos(sub_path, sections)
sub_mf = MarkdownTodoFile(sub_path)
length(sub_mf)  # 4: 2 top-level + 2 subtasks
4
collect(sub_mf)
4-element Vector{Todo}:
 Todo: (A) Finish report @office
 Todo: Write introduction
 Todo: Add charts
 Todo: Email client

When writing back to markdown, subtasks are indented with 4 spaces:

print(write_markdown_todos(sections))
# Work
- (A) Finish report @office
    - Write introduction
    - Add charts
- Email client

Notes (Blockquotes)

Tasks can have associated notes, written as blockquote lines (> ...) beneath the task. Notes are stored in the notes field as a single string (lines joined by \n):

notes_text = """
# Work
- (A) Finish report @office +ClientProject due:2024-02-01
    > Need to include Q4 numbers from the finance team.
    > Ask Sarah for the updated spreadsheet.
    - Get Q4 data from finance
    - Write executive summary

- Email client @office +ClientProject
    > They prefer to be contacted after 2pm EST.
"""

sections = parse_markdown_todos(notes_text)
sections[1].todos[1].notes
"Need to include Q4 numbers from the finance team.\nAsk Sarah for the updated spreadsheet."
sections[1].todos[2].notes
"They prefer to be contacted after 2pm EST."

Subtasks can also have notes:

sub_notes_text = """
# Work
- Report
    - Get data
        > From the finance team.
"""

sub_sections = parse_markdown_todos(sub_notes_text)
sub_sections[1].todos[1].subtasks[1].notes
"From the finance team."

Notes roundtrip through write and parse:

print(write_markdown_todos(sections))
# Work
- (A) Finish report @office +ClientProject due:2024-02-01
    > Need to include Q4 numbers from the finance team.
    > Ask Sarah for the updated spreadsheet.
    - Get Q4 data from finance
    - Write executive summary
- Email client @office +ClientProject
    > They prefer to be contacted after 2pm EST.

Tasks without notes have an empty string and are written without any blockquote lines (fully backward-compatible).

Parsing

parse_markdown_todos returns a vector of TodoSections, each with a heading, level, and todos:

text = """
# Work
- (A) Finish report @office +ClientProject
- Email client @office

## Personal
- Buy groceries @store +Errands
- (B) Call Mom @phone +Family due:2024-02-01
"""

sections = parse_markdown_todos(text)
2-element Vector{TodoSection}:
 TodoSection("Work", 2 tasks)
 TodoSection("Personal", 2 tasks)
sections[1].heading, sections[1].level
("Work", 1)
sections[1].todos
2-element Vector{Todo}:
 Todo: (A) Finish report @office +ClientProject
 Todo: Email client @office
sections[2].heading, sections[2].level
("Personal", 2)

Todos appearing before any heading are placed in a section with heading="" and level=0:

sections = parse_markdown_todos("""
- Orphan task
# Section
- Task in section
""")

sections[1].heading, sections[1].level, sections[1].todos
("", 0, Todo[Todo: Orphan task])

Writing

write_markdown_todos serializes sections back to markdown with blank lines between sections:

sections = parse_markdown_todos(text)
print(write_markdown_todos(sections))
# Work
- (A) Finish report @office +ClientProject
- Email client @office

## Personal
- Buy groceries @store +Errands
- (B) Call Mom @phone +Family due:2024-02-01

MarkdownTodoFile

MarkdownTodoFile is the section-aware counterpart to TodoFile. It reads a markdown file, provides iteration over all todos (flattened across sections), and can write back to disk:

path = tempname() * ".md"
write_markdown_todos(path, parse_markdown_todos(text))

mf = MarkdownTodoFile(path)
Finish reportA@office+ClientProject
Email client@office
Buy groceries@store+Errands
Call MomB@phone+Familydue:2024-02-01
length(mf)
4
mf[1]
Todo: (A) Finish report @office +ClientProject
collect(mf)
4-element Vector{Todo}:
 Todo: (A) Finish report @office +ClientProject
 Todo: Email client @office
 Todo: Buy groceries @store +Errands
 Todo: (B) Call Mom @phone +Family due:2024-02-01

Write back to disk with write_todos:

write_todos(mf)  # writes to mf.filepath
163

HTML Views

All existing views work with MarkdownTodoFile – todos are flattened automatically:

ListView(mf)
Finish reportA@office+ClientProject
Email client@office
Buy groceries@store+Errands
Call MomB@phone+Familydue:2024-02-01
TableView(mf)
Done Priority Description Tags Created Completed
A Finish report @office+ClientProject
Email client @office
Buy groceries @store+Errands
B Call Mom @phone+Familydue:2024-02-01
KanbanView(mf)

A

Finish reportA@office+ClientProject

B

Call MomB@phone+Familydue:2024-02-01

(none)

Email client@office
Buy groceries@store+Errands

Group by section

KanbanView supports :section as a group_by option, which groups todos by their markdown heading:

KanbanView(mf, :section)

Personal

Buy groceries@store+Errands
Call MomB@phone+Familydue:2024-02-01

Work

Finish reportA@office+ClientProject
Email client@office

The html_view convenience function also accepts MarkdownTodoFile:

html_view(mf; view=:kanban, group_by=:section)

Personal

Buy groceries@store+Errands
Call MomB@phone+Familydue:2024-02-01

Work

Finish reportA@office+ClientProject
Email client@office