Action

List to Things

Posted by Bert, Last update almost 6 years ago

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.