Action
Things Parser
Things Parser 3 turns TaskPaper documents into Things tasks and projects. See below for examples of the syntax, or read my blog post for more details.
In addition, to this, it also allows the use of variables using the Mustache Prompt system, which you can read about in this blog post.
Update 2020-12-22: Now supports multi-line notes for tasks and projects.
For Things Parser 2 (legacy version) see here.
Tasks
- Tasks outside projects
- with date @when(2020-12-13)
- alternatively @defer(2020-12-13)
- with natural language date @when(tomorrow)
- with deadline @deadline(2020-12-25)
- alternatively @due(2020-12-25)
- Canceled task @canceled
- British cancelled task @cancelled
- Tasks can be @done
- Or @completed
- Tasks can be assigned to a @list(name)
- alternatively @project(name)
- or using list ID @listID(id)
- and within a project to a @heading(name)
- Tasks can have a @tag(name)
- Or more than one @tags(tag1, tag2, tag3)
- Tasks can have notes
which are indented text
- And subtasks
- which are indented tasks
- like this
Projects
Projects can have dates too: @when(today)
- also supports @defer(today)
They can have deadlines: @deadline(2020-12-25)
- also supports @due(2020-12-25)
They can be: @canceled
- or if they are British @cancelled
They can be: @completed
- or @done
They can be assigned to an: @area(name)
Or using an id: @areaID(id)
They can have a: @tag(name)
- or multiple @tags(tag1, tag2, tag3)
They can have notes:
which are indented text
They can obviously have tasks:
- like this one
- and this one
But they can also have headings:
which are written like sub-projects:
- and tasks within the headings will be added to those headings
Steps
-
script
function libraryIsInstalled(name) { let fm = FileManager.createCloud() if (fm.readString("/Library/Scripts/" + name)) { return true } else { return false } } HTTP.prototype.getContent = function(url) { let r = this.request({"url": url, "method": "GET"}) if (r.success) { return r.responseText } else { alert("Error " + r.statusCode + ": " + r.error) } } HTTP.prototype.downloadLibrary = function(url, filename) { let fm = FileManager.createCloud() let path = "/Library/Scripts/" + filename let content = this.getContent(url) if (content) { fm.writeString(path, content) return true } else { return false } } const url = "https://raw.githubusercontent.com/pdavisonreiber/Public-Drafts-Scripts/master/Things%20Parser%203/birchoutline.js" if (libraryIsInstalled("birchoutline.js")) { require("birchoutline.js") } else { let p1 = Prompt.create() p1.title = "Confirm Download" p1.message = "Things Parser 3 requires the installation of the Birch Outline library. Press OK to download." p1.addButton("OK") if (p1.show()) { let http = HTTP.create() let success = http.downloadLibrary(url, "birchoutline.js") if (success) { let p2 = Prompt.create() p2.isCancellable = false p2.title = "Success" p2.message = "Birch Outline successfully installed to iCloud/Drafts/Library/Scripts/birchoutline.js" p2.addButton("OK") p2.show() require("birchoutline.js") } } }
-
script
function mustachePrompt(text, dateFormat) { dateFormat = (dateFormat === undefined) ? "%Y-%m-%d" : dateFormat const variableRegex = /{{(?:(date|bool):)?(#|^)?(\w+)\??([+|-]\d+[d|w|m])?}}/g const variableMatches = text.matchAll(variableRegex) variables = {} for (match of variableMatches) { let instance = new Object() instance.string = match[0] instance.type = match[1] instance.modifier = match[2] instance.name = match[3] instance.offset = match[4] if (!variables.hasOwnProperty(instance.name)) { let variable = new Object() variable.type = instance.type variable.modifier = instance.modifier variable.instances = [instance] variables[instance.name] = variable } else { variables[instance.name].instances.push(instance) if (!variables[instance.name].type) { variables[instance.name].type = instance.type } if (!variables[instance.name].modifier) { variables[instance.name].modifier = instance.modifier } } } //alert(JSON.stringify(variables)) let p = Prompt.create() for (name in variables) { let variable = variables[name] if (!variable.type) { p.addTextField(name, name, "") } else if (variable.type == "date") { p.addDatePicker(name, name, new Date(), {mode: "date"}) } else if (variable.type == "bool") { p.addSwitch(name, name, false) } } p.addButton("OK") data = {} let cancel = true if (Object.keys(variables).length !== 0) { cancel = !p.show() } if (!cancel) { for (key in p.fieldValues) { let fieldValue = p.fieldValues[key] if (fieldValue instanceof Date) { data[key] = strftime(fieldValue, dateFormat) } else if (typeof fieldValue == "string") { if (fieldValue.includes(",") && variables[key].modifier == "#") { data[key] = fieldValue.split(",").map(s => s.trim()) } else { data[key] = fieldValue } } else { data[key] = fieldValue } for (instance of variables[key].instances) { if (!instance.type && !instance.offset) { continue } if (instance.offset) { text = text.replace(instance.string, instance.string.replace(instance.type + ":", "").replace(instance.offset, snakify(instance.offset))) data[key + snakify(instance.offset)] = offsetDate(fieldValue, instance.offset, dateFormat) } else { text = text.replace(instance.string, instance.string.replace(instance.type + ":", "")) } } } //alert(JSON.stringify(data, 2)) let template = MustacheTemplate.createWithTemplate(text) let result = template.render(data) return result } else { context.cancel() } } function offsetDate(date, string, dateFormat) { let d = new Date(date) let offsetRegex = /(\+|-)(\d+)(d|w|m)/ let match = string.match(offsetRegex) let multiplier = (match[1] == "+" ? 1 : -1) if (match[3] == "d") { d.setDate(d.getDate() + multiplier * parseInt(match[2])) } else if (match[3] == "w") { d.setDate(d.getDate() + multiplier * 7 * parseInt(match[2])) } else if (match[3] == "m") { d = addMonths(d, multiplier * parseInt(match[2])) } return strftime(d, dateFormat) } function addMonths(date, months) { var d = date.getDate(); date.setMonth(date.getMonth() + +months); if (date.getDate() != d) { date.setDate(0); } return date; } function snakify(offset) { return offset.replace("+", "_offset_forward_").replace("-", "_offset_backwards_") }
-
script
let text = draft.content if (text.includes("{{")) { let mustache = mustachePrompt(text) if (mustache) { text = mustache } } var taskPaperOutline = new birchoutline.Outline.createTaskPaperOutline(draft.processTemplate(text)) var rootChildren = taskPaperOutline.root.children var tjsArray = [] for (child of rootChildren) { if (child.getAttribute("data-type") == "project") { tjsArray.push(processProject(child)) } else if (child.getAttribute("data-type") == "task") { tjsArray.push(processTask(child)) } } var container = TJSContainer.create(tjsArray) var cb = CallbackURL.create() cb.baseURL = container.url cb.addParameter("reveal", true) if (text && tjsArray.length != 0) { app.openURL(cb.url) } else { context.cancel() } function processSubTask(item) { var subtask = TJSChecklistItem.create() subtask.title = item.bodyContentString.trim() subtask.canceled = item.hasAttribute("data-canceled") || item.hasAttribute("data-cancelled") subtask.completed = item.hasAttribute("data-completed") || item.hasAttribute("data-done") return subtask } function processTask(item) { var task = TJSTodo.create() task.title = item.bodyContentString.trim() if (item.hasAttribute("data-when")) { task.when = item.getAttribute("data-when") } else if (item.hasAttribute("data-defer")) { task.when = item.getAttribute("data-defer") } if (item.hasAttribute("data-due")) { task.deadline = item.getAttribute("data-due") } else if (item.hasAttribute("data-deadline")) { task.deadline = item.getAttribute("data-deadline") } task.canceled = item.hasAttribute("data-canceled") || item.hasAttribute("data-cancelled") task.completed = item.hasAttribute("data-completed") || item.hasAttribute("data-done") if (item.hasAttribute("data-heading")) { task.heading = item.getAttribute("data-heading") } if (item.hasAttribute("data-list")) { task.list = item.getAttribute("data-list") } else if (item.hasAttribute("data-project")) { task.list = item.getAttribute("data-project") } if (item.hasAttribute("data-listID")) { task.listID = item.getAttribute("data-listID") } else if (item.hasAttribute("data-listid")) { task.listID = item.getAttribute("data-listid") } if (item.hasAttribute("data-tag")) { task.tags = [item.getAttribute("data-tag")] } else if (item.hasAttribute("data-tags")) { task.tags = item.getAttribute("data-tags").split(",").map(t => t.trim()) } if (item.hasChildren) { for (child of item.children) { if (child.getAttribute("data-type") == "task") { task.addChecklistItem(processSubTask(child)) } else if (child.getAttribute("data-type") == "note") { if (task.notes) { task.notes += "\n" + child.bodyContentString } else { task.notes = child.bodyContentString } } } } return task } function processHeading(item) { var heading = TJSHeading.create() heading.title = item.bodyContentString.trim() heading.archived = item.hasAttribute("data-archived") || item.hasAttribute("data-done") return heading } function processProject(item) { var project = TJSProject.create() project.title = item.bodyContentString.trim() if (item.hasAttribute("data-when")) { project.when = item.getAttribute("data-when") } else if (item.hasAttribute("data-defer")) { project.when = item.getAttribute("data-defer") } if (item.hasAttribute("data-due")) { project.deadline = item.getAttribute("data-due") } else if (item.hasAttribute("data-deadline")) { project.deadline = item.getAttribute("data-deadline") } project.canceled = item.hasAttribute("data-canceled") || item.hasAttribute("data-cancelled") project.completed = item.hasAttribute("data-completed") || item.hasAttribute("data-done") if (item.hasAttribute("data-area")) { project.area = item.getAttribute("data-area") } if (item.hasAttribute("data-areaID")) { project.areaID = item.getAttribute("data-areaID") } else if (item.hasAttribute("data-areaid")) { project.areaID = item.getAttribute("data-areaid") } if (item.hasAttribute("data-tag")) { project.tags = [item.getAttribute("data-tag")] } else if (item.hasAttribute("data-tags")) { project.tags = item.getAttribute("data-tags").split(",").map(t => t.trim()) } if (item.hasChildren) { for (child of item.children) { if (child.getAttribute("data-type") == "task") { project.addTodo(processTask(child)) } else if (child.getAttribute("data-type") == "note") { if (project.notes) { project.notes += "\n" + child.bodyContentString } else { project.notes = child.bodyContentString } } else if (child.getAttribute("data-type") == "project") { let heading = processHeading(child) project.addHeading(heading) if (child.hasChildren) { for (grandchild of child.children) { if (grandchild.getAttribute("data-type") == "task") { let task = processTask(grandchild) task.heading = heading.title project.addTodo(task) } } } } } } return project }
Options
-
After Success Archive Notification Info Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.