todotxt-parser
Version:
A parser for Gina Trapani's todo.txt format with optional extended features
159 lines (137 loc) • 4.93 kB
text/coffeescript
_ = require "underscore"
buildPattern = (opt) ->
# returns interpolation-friendly regex
interp = (expr) ->
regex = expr().toString()
regex[1..(regex.lastIndexOf("/") - 1)]
DATE = -> opt.dateRegex
START = -> if opt.relaxedWhitespace then /^\s*/ else /^/
SPACE = -> if opt.relaxedWhitespace then /\s+/ else /\s/
COMPLETE = -> ///
(x)
(?:#{interp SPACE}(#{interp DATE}))
#{if opt.requireCompletionDate then "" else "?"}
///
PRIORITY = -> if opt.ignorePriorityCase then /\(([A-Za-z])\)/ else /\(([A-Z])\)/
///
#{interp START}
(?:#{interp COMPLETE}#{interp SPACE})? # completion mark and date
(?:#{interp PRIORITY}#{interp SPACE})? # priority
(?:(#{interp DATE})#{interp SPACE})? # created date
(.*) # task text (may contain +projects, @contexts, meta:data)
$
///
module.exports =
options:
# Gina Trapani's todo.txt-cli format & implementation
CANONICAL:
dateParser: (s) -> new Date(s).toJSON()
dateRegex: /\d{4}-\d{2}-\d{2}/
relaxedWhitespace: false
requireCompletionDate: true
ignorePriorityCase: false
heirarchical: false
inherit: false
commentRegex: null
projectRegex: /(?:\s|^)\+(\S+)/g
contextRegex: /(?:\s|^)@(\S+)/g
extensions: []
RELAXED:
dateParser: (s) -> new Date(s).toJSON()
dateRegex: /\d{4}-\d{2}-\d{2}/
relaxedWhitespace: true
requireCompletionDate: false
ignorePriorityCase: true
heirarchical: false
inherit: false
commentRegex: /^\s*#.*$/
projectRegex: /(?:\s+|^)\+(\S+)/g
contextRegex: /(?:\s+|^)@(\S+)/g
extensions: [
(text) ->
metadata = {}
metadataRegex = /(?:\s+|^)(\S+):(\S+)/g
while match = metadataRegex.exec text
metadata[match[1].toLowerCase()] = match[2]
metadata
]
parse: (s, options = {}) ->
_.defaults options, module.exports.options.CANONICAL
if options.hierarchical then options.relaxedWhitespace = true
pattern = buildPattern options
root = { subtasks: [], indentLevel: -1, contexts: [], projects: [], metadata: {} }
stack = [root]
for line in s.split "\n"
taskMatch = line.match pattern
commentMatch = if options.commentRegex then line.match options.commentRegex
if !taskMatch or commentMatch then continue
text = taskMatch[5].trim()
indentLevel = if match = line.match /^(\s+).+/
# if line starts with a space, then count the number of leading whitespace characters
match[1].length
else if match = line.match /^x(\s+).+/
# if line starts with x, then count the whitespace after it + 1 (for the x)
match[1].length + 1
else 0
# figure out where we are in the hierarchy
prevSibling = _.last(_.last(stack).subtasks) || _.last(stack)
if indentLevel > prevSibling.indentLevel
stack.push prevSibling
while indentLevel <= _.last(stack).indentLevel
stack.pop()
parent = _.last(stack)
# projects
projectsSet = {}
if options.inherit
projectsSet[project] = true for project in parent.projects
while match = options.projectRegex.exec text
projectsSet[match[1]] = true
# contexts
contextsSet = {}
if options.inherit
contextsSet[context] = true for context in parent.contexts
while match = options.contextRegex.exec text
contextsSet[match[1]] = true
# metadata from extensions
metadata = {}
if options.inherit
metadata[key] = value for key, value of parent.metadata
for dataParser in options.extensions
data = dataParser text
for key, value of data
metadata[key] = value
complete = if taskMatch[1]
true
else if options.inherit
parent.complete
else false
dateCreated = if taskMatch[4]
options.dateParser taskMatch[4]
else if options.inherit
parent.dateCreated
else null
dateCompleted = if taskMatch[2]
options.dateParser taskMatch[2]
else if options.inherit
parent.dateCompleted
else null
priority = (taskMatch[3] || metadata.pri)?.toUpperCase() || if options.inherit
parent.priority
else null
task =
raw: taskMatch[0]
text: text
projects: key for key of projectsSet
contexts: key for key of contextsSet
complete: complete
dateCreated: dateCreated
dateCompleted: dateCompleted
priority: priority
metadata: metadata
subtasks: []
indentLevel: indentLevel
_.last(stack).subtasks.push task
root.subtasks
# parsing function with relaxed options
relaxed: (s, options = {}) ->
module.exports.parse s, _.defaults options, module.exports.options.RELAXED