Action
List to Things
Shares a markdown style list to Things. Lines beginning with “- [ ] “ are incomplete tasks, “- [x] “ are complete tasks, and “- [-] “ are canceled tasks (or projects or checklists depending on nesting). All other lines are notes in the item they’re nested under.
Whether a project or task is created is determined from the maximum nesting level. A task is created if it’s less than two, otherwise a project is created. The item created is placed in the Today view.
Steps
-
script
"use strict"; const MAX_DEPTH_POSSIBLE = 2; function getIndentationLevel(line) { const leadingWhitespace = /^(\s*)/.exec(line)[1]; return leadingWhitespace.length; } // Assumes depth >= 0 // tree looks likes {children: [trees]} function descend(tree, depth, mark_on_descent) { const origTree = tree; for (let level = 0; level < depth; level++) { const children = tree.children; if (children.length === 0) { return null; } tree = children[children.length - 1]; if (mark_on_descent) { tree.in_use = true; } } return tree; } function makeEmptyTask(in_use, title, completed, canceled) { return { title: title || "", children: [], noteLines: [], completed: completed, canceled: canceled, in_use: in_use, }; } function makeEmptyTaskTree(depth, title, completed, canceled) { const root = makeEmptyTask(true, title, completed, canceled); let currentNode = root; for (let i = 0; i < depth; i++) { const child = makeEmptyTask(false); currentNode.children.push(child); currentNode = child; } return root; } function toTasks(text, title) { text = text.trim(); title = title.trim(); if (!text) { return null; } const NOTE = 0; const COMPLETED_TASK = 1; const INCOMPLETE_TASK = 2; const CANCELED_TASK = 3; const newTaskStatuses = [INCOMPLETE_TASK, COMPLETED_TASK, CANCELED_TASK]; const incompleteTask = [/^\s*\* ?/, /^\s*- ?\[[^x-]?\] /, /^\s*\* ?\[[^x-]?\] /]; const completedTask = [/^\s*- ?\[x\] /, /^\s*\* ?\[x\] /]; const canceledTask = [/^\s*- ?\[-\] /, /^\s*\* ?\[-\] /]; const lineContentsRegex = /^\s*(?:(?:(?:-|\*) \[.?\])|(?:-|\*))?\s*(.*)$/; const lines = text.split("\n"); const lineData = []; for (var i = 0; i < lines.length; i++) { const line = lines[i]; const lineContent = lineContentsRegex.exec(line)[1].trim(); if (!lineContent) { continue; } const lineIsCompletedTask = completedTask.some(checkbox => checkbox.test(line)); const lineIsIncompleteTask = incompleteTask.some(checkbox => checkbox.test(line)); const lineIsCanceledTask = canceledTask.some(checkbox => checkbox.test(line)); const indentationLevel = getIndentationLevel(line); lineData.push({ lineContent: lineContent, indentationLevel: indentationLevel, status: lineIsIncompleteTask ? INCOMPLETE_TASK : lineIsCompletedTask ? COMPLETED_TASK : lineIsCanceledTask ? CANCELED_TASK : NOTE, }); } const indentationLevels = [...new Set(lineData.map(datum => datum.indentationLevel))]; if (indentationLevels.length === 0) { return null; } indentationLevels.sort(); const indentationMap = []; for (let i = 0; i < indentationLevels.length; i++) { indentationMap[indentationLevels[i]] = Math.min(i, MAX_DEPTH_POSSIBLE); } const topLevelTask = makeEmptyTaskTree(MAX_DEPTH_POSSIBLE, title); const depths = []; for (const datum of lineData) { const indentationLevel = indentationMap[datum.indentationLevel]; const isTask = newTaskStatuses.includes(datum.status); const depth = Math.min(indentationLevel, MAX_DEPTH_POSSIBLE - (isTask ? 0 : 1)); // (indentationLevel === MAX_DEPTH_POSSIBLE // ? MAX_DEPTH_POSSIBLE - 1 // : indentationLevel) - 1; depths.push(depth); const parentTask = descend(topLevelTask, depth, true); if (isTask) { const depthToBottom = MAX_DEPTH_POSSIBLE - depth; const newTask = makeEmptyTaskTree( depthToBottom, datum.lineContent, datum.status === COMPLETED_TASK, datum.status === CANCELED_TASK, ); if (parentTask.children.length === 1 && !parentTask.children[0].in_use) { parentTask.children[0] = newTask; } else { parentTask.children.push(newTask); } } else { parentTask.noteLines.push(datum.lineContent); } } topLevelTask.maxDepth = Math.max(...depths) + 1; return topLevelTask; } function asChecklistItem(item) { return { type: "checklist-item", attributes: { title: item.title, completed: item.completed, }, }; } function asTask(item, when) { // console.log(JSON.stringify(item.children)); const task = { type: "to-do", attributes: { title: item.title, "checklist-items": item.children .filter(child => child.in_use) .map(asChecklistItem), notes: item.noteLines.join("\n"), completed: item.completed, canceled: item.canceled, }, }; if (when) { task.attributes.when = when; } return task; } function asProject(item, when) { return { type: "project", attributes: { title: item.title, items: item.children .filter(child => child.in_use) .map(child => asTask(child, null)), notes: item.noteLines.join("\n"), when: when, completed: item.completed, canceled: item.canceled, }, }; } function taskToThingsJSON(topLevelTask, when) { const depth = topLevelTask.maxDepth; if (depth >= MAX_DEPTH_POSSIBLE) { return asProject(topLevelTask, when); } else { return asTask(topLevelTask, when); } } function textToThingsJSON(text, title, when) { return JSON.stringify([taskToThingsJSON(toTasks(text, title), when)]); } let text = editor.getSelectedText(); if (!text) { text = draft.content; } draft.setTemplateTag("things_data", textToThingsJSON(text, draft.title, "today"));
-
url
template things:///json?reveal=true&data=[[things_data]]
useSafari false
encodeTags true
-
callbackUrl
template things:///json?reveal=true&data=[[things_data]]
waitForResponse false
encodeTags true
Options
-
After Success Archive Notification Error Log Level Error
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.