A first draft of a Copy As JSON action for OmniFocus:
Copy as JSON.omnijs.zip (2.0 KB)
JS Source
/* eslint-disable no-undef */
/* eslint-disable no-return-assign */
/* eslint-disable spaced-comment */
/*{
"author": "Rob Trew",
"targets": ["omnifocus"],
"type": "action",
"identifier": "com.robtrew.copyOFasJSON",
"version": "0.1",
"description": "Copies selected OF items as JSON",
"label": "Copy as JSON",
"mediumLabel": "Copy as JSON",
"paletteLabel": "Copy as JSON",
}*/
(() => {
"use strict";
// main :: () -> Plugin
const main = () => Object.assign(
new PlugIn.Action(selection => {
// nodePropertyDict :: OF DBObject -> Dict
const nodePropertyDict = x => {
const
nodeType = x.constructor.name,
isProjectTask = ["Project", "Task"]
.includes(nodeType);
return {
"type": nodeType,
"name": x.name,
"tags": "Task" === nodeType ? (
x.tags.map(tagPath)
) : undefined,
"due": isProjectTask ? (
null !== x.dueDate ? (
x.dueDate.toISOString()
) : undefined
) : undefined,
"note": isProjectTask ? (
x.note
) : undefined
};
};
// json :: JSON String
const json = JSON.stringify(
commonAncestors(
selection.databaseObjects
).map(
jsObjectFromOmniFocusObject(
nodePropertyDict
)
)
// null, 2
);
return (
Pasteboard.general.string = json,
new Alert("Copied as JSON", json).show()
);
}), {
validate: selection =>
0 < selection.databaseObjects.length
}
);
// -------------------- OMNIFOCUS --------------------
// commonAncestors :: [OFItem] -> [OFItem]
const commonAncestors = items => {
// Only items which do not descend from
// other items in the list.
const itemSet = new Set(items);
return items.filter(
x => !until(
parent => !parent || itemSet.has(parent)
)(
v => v.parent
)(
x.parent
)
);
};
// jsObjectFromOmniFocusObject :: (dbObject -> a) ->
// dbObject -> Tree [a, [Tree]]
const jsObjectFromOmniFocusObject = f =>
// A mapping from an OF database object and
// its descendants to a Tree of (Value, [Tree])
// pairs, in which the Value is some representation
// of one or more of the OF object's properties,
// (perhaps a name string, or a key:value dictionary)
// and the [Tree] is a list of similar
// representations of its descendants, or
// an empty list β [] β if it has no descendants.
//
// The f function (dbObject -> a) takes an
// individual OF object and returns a corresponding
// Value.
dbObject => {
const go = x => [
f(x),
x.children.map(go)
];
return go(dbObject);
};
// tagPath :: Tag -> String
const tagPath = tag => {
// A " : " delimited path for a tag or
// the tag's name if it has no parent tag.
const go = x =>
null !== x.parent ? (
go(x.parent).concat(x.name)
) : [x.name];
return go(tag).join(" : ");
};
// --------------------- GENERIC ---------------------
// until :: (a -> Bool) -> (a -> a) -> a -> a
const until = p =>
// The value resulting from repeated applications
// of f to the seed value x, terminating when
// that result returns true for the predicate p.
f => x => {
let v = x;
while (!p(v)) {
v = f(v);
}
return v;
};
// MAIN ---
return main();
})();
If you are writing a script which needs a nested tree of OmniFocus objects to be in place, for example:
- a particular tag with a given set of descendants,
- or a template project or folder with a particular set of descendants
then it may be useful for the script to contain a light, structured (and human-legible) representation of that nested hierarchy of objects, from which to:
- build,
- check,
- or repair
the nested structure (or tree, hierarchy if you prefer) which you need.
If you install the omniJS
action above (dragging it, for example, into the Automation > Configure
window), you should find that you can copy one or more:
- top level tags,
- projects,
- folders,
- or just tasks
and choose Automation > Copy As JSON
(or click a button dragged to the toolbar from a Customize Toolbar
dialog) to copy the OmniFocus object structure to a JSON representation.
For example, if we have a Kanban tag with four descendants, and you selected the Kanban parent tag, it might be copied as JSON, with its descendants, to:
The JSON structure is a list of pairs.
The left-hand item of each pair gives the details of an item (as a dictionary of property values).
The right hand item of each pair is a list of descendants.
Even βleafβ items (bottom-level tags or task with no descendants, for example, are represented as a [details, descendants]
pair.
In the case of leaves without descendants, the descendant list is empty.
The pairs themselves (languages like TypeScript, Python and Haskell would call them tuples), are, in JSON, just plain lists with two items:
[Properties dictionary, List]
e.g.
[{"type":"Tag","name":"To Do"},[]]
This draft just copies a basic set of properties for each selected OmniFocus database item.
To refine it, and add any additional properties that you might need, you can extend this function in the source code:
// nodePropertyDict :: OF DBObject -> Dict
const nodePropertyDict = x => {
const
nodeType = x.constructor.name,
isProjectTask = ["Project", "Task"]
.includes(nodeType);
return {
"type": nodeType,
"name": x.name,
"tags": "Task" === nodeType ? (
x.tags.map(tagPath)
) : undefined,
"due": isProjectTask ? (
null !== x.dueDate ? (
x.dueDate.toISOString()
) : undefined
) : undefined,
"note": isProjectTask ? (
x.note
) : undefined
};
};