Import Taskpaper Project into a specific folder using Applescript

I have an OmniFocus project in a text file in TaskPaper format that shows up in a Dropbox folder being watched by Hazel. I need to import it into a particular OF Project as a nested sub-project. Right now, I’m doing this by Keyboard Maestro, but because it fires off automatically when Hazel detects the new project, and because KM simulates keystrokes, if I’m in the middle of typing something else, my keystrokes end up messing everything up.

But if I could do the automation in the background via AppleScript, I don’t think that would be a problem.

Thus my question: Can I import a Taskpaper-format Project into an OF Project as a nested sub-project using AppleScript? And if so, can someone point me to some resources on scripting it? Thanks.

I’m interested in this, @dombett. I can help you doing an OmniJS script to solve your problem. Are you familiar with JavaScript For Automation? We could call the OmniJS script from JXA.

Thank you. I’m not yet familiar with JavaScript for Automation but I’m willing to give it a shot.

Good. I will look into it and report back.

@dombett, this is a first draft of a script that accomplishes what you want. Before stepping into the actual reading of the TP file. Could you test with the sample text that the script provides in order to see if it works for you ?

You can test it in Script Editor, for example. I do not have Hazel, but I think there is a hazelProcessFile handler.

Full code:

(() => {
    'use strict';

    // @unlocked2412 2020
    // CONTACT
    // Gabriel - Twitter: @unlocked2412
    // First draft of a TaskPaper import into OmniFocus.

    // Ver 0.01

    // This Mac version imports task titles and notes. Also,
    // the following tags:
    // - defer
    // - flagged

    // OmniJS code to be run from a JXA context.

    // USER OPTIONS --------------------------------------
        const options = {
            project: 'TaskPaper Import'
        };

    // OMNI JS CODE ---------------------------------------
    const omniJSContext = opts => {
        // main :: IO ()
        const main = () => {
            const
                s = `- An Idea
	- Sub-idea
Another idea
    - Sub-idea @Communications
        - Child item 1 @Errands
        - Child item 2
	- Sub-idea with note @Errands
		Sub-idea note
	Another sub-idea with note
		Another sub-idea note`

            return showJSON(
                compose(
                    ofObjectsFromForest(
                        projectFoundOrCreated(opts.project)
                    ),
                    map(
                        compose(
                            fmapTree(ofDictFromTpKV),
                            treeWithNotes
                        )
                    ),
                    forestFromTaskPaperString
                )(s)
            )
        };

        // GENERIC FUNCTIONS ----------------------------------
        // https://github.com/RobTrew/prelude-jxa
        // JS Prelude --------------------------------------------------
        // Just :: a -> Maybe a
        const Just = x => ({
            type: 'Maybe',
            Nothing: false,
            Just: x
        });

        // Node :: a -> [Tree a] -> Tree a
        const Node = v =>
            // Constructor for a Tree node which connects a
            // value of some kind to a list of zero or
            // more child trees.
            xs => ({
                type: 'Node',
                root: v,
                nest: xs || []
            });

        // Nothing :: Maybe a
        const Nothing = () => ({
            type: 'Maybe',
            Nothing: true,
        });

        // Tuple (,) :: a -> b -> (a, b)
        const Tuple = a =>
            b => ({
                type: 'Tuple',
                '0': a,
                '1': b,
                length: 2
            });

        // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
        const compose = (...fs) =>
            // A function defined by the right-to-left
            // composition of all the functions in fs.
            fs.reduce(
                (f, g) => x => f(g(x)),
                x => x
            );

        // constant :: a -> b -> a
        const constant = k =>
            _ => k;

        // div :: Int -> Int -> Int
        const div = x =>
            y => Math.floor(x / y);

        // eq (==) :: Eq a => a -> a -> Bool
        const eq = a =>
            // True when a and b are equivalent in the terms
            // defined below for their shared data type.
            b => {
                const t = typeof a;
                return t !== typeof b ? (
                    false
                ) : 'object' !== t ? (
                    'function' !== t ? (
                        a === b
                    ) : a.toString() === b.toString()
                ) : (() => {
                    const kvs = Object.entries(a);
                    return kvs.length !== Object.keys(b).length ? (
                        false
                    ) : kvs.every(([k, v]) => eq(v)(b[k]));
                })();
            };

        // filter :: (a -> Bool) -> [a] -> [a]
        const filter = p =>
            // The elements of xs which match
            // the predicate p.
            xs => [...xs].filter(p);

        // findIndices :: (a -> Bool) -> [a] -> [Int]
        // findIndices :: (String -> Bool) -> String -> [Int]
        const findIndices = p =>
            xs => (
                ys => ys.flatMap((y, i) => p(y, i, ys) ? (
                    [i]
                ) : [])
            )([...xs])

        // first :: (a -> b) -> ((a, c) -> (b, c))
        const first = f =>
            // A simple function lifted to one which applies
            // to a tuple, transforming only its first item.
            xy => Tuple(f(xy[0]))(
                xy[1]
            );

        // flip :: (a -> b -> c) -> b -> a -> c
        const flip = op =>
            // The binary function op with its arguments reversed.
            1 < op.length ? (
                (a, b) => op(b, a)
            ) : (x => y => op(y)(x));

        // fmapTree :: (a -> b) -> Tree a -> Tree b
        const fmapTree = f => {
            // A new tree. The result of a structure-preserving
            // application of f to each root in the existing tree.
            const go = tree => Node(f(tree.root))(
                tree.nest.map(go)
            );
            return go;
        };

        // foldTree :: (a -> [b] -> b) -> Tree a -> b
        const foldTree = f => {
            // The catamorphism on trees. A summary
            // value obtained by a depth-first fold.
            const go = tree => f(tree.root)(
                tree.nest.map(go)
            );
            return go;
        };

        // foldl :: (a -> b -> a) -> a -> [b] -> a
        const foldl = f =>
            a => xs => [...xs].reduce(
                (x, y) => f(x)(y),
                a
            );

        // fst :: (a, b) -> a
        const fst = tpl =>
            // First member of a pair.
            tpl[0];

        // identity :: a -> a
        const identity = x =>
            // The identity function. (`id`, in Haskell)
            x;

        // isSpace :: Char -> Bool
        const isSpace = c =>
            // True if c is a white space character.
            /\s/.test(c);

        // length :: [a] -> Int
        const length = xs =>
            // Returns Infinity over objects without finite
            // length. This enables zip and zipWith to choose
            // the shorter argument when one is non-finite,
            // like cycle, repeat etc
            'GeneratorFunction' !== xs.constructor.constructor.name ? (
                xs.length
            ) : Infinity;

        // lines :: String -> [String]
        const lines = s =>
            // A list of strings derived from a single
            // newline-delimited string.
            0 < s.length ? (
                s.split(/[\r\n]/)
            ) : [];

        // list :: StringOrArrayLike b => b -> [a]
        const list = xs =>
            // xs itself, if it is an Array,
            // or an Array derived from xs.
            Array.isArray(xs) ? (
                xs
            ) : Array.from(xs || []);

        // map :: (a -> b) -> [a] -> [b]
        const map = f =>
            // The list obtained by applying f
            // to each element of xs.
            // (The image of xs under f).
            xs => [...xs].map(f);

        // matching :: [a] -> (a -> Int -> [a] -> Bool)
        const matching = pat => {
            // A sequence-matching function for findIndices etc
            // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3])
            // -> [1, 4]
            const
                lng = pat.length,
                bln = 0 < lng,
                h = bln ? pat[0] : undefined;
            return x => i => src =>
                bln && h == x && eq(pat)(
                    src.slice(i, lng + i)
                );
        };

        // minimum :: Ord a => [a] -> a
        const minimum = xs => (
            // The least value of xs.
            ys => 0 < ys.length ? (
                ys.slice(1)
                .reduce((a, y) => y < a ? y : a, ys[0])
            ) : undefined
        )(list(xs));

        // nest :: Tree a -> [a]
        const nest = tree => {
            // Allowing for lazy (on-demand) evaluation.
            // If the nest turns out to be a function –
            // rather than a list – that function is applied
            // here to the root, and returns a list.
            const xs = tree.nest;
            return 'function' !== typeof xs ? (
                xs
            ) : xs(root(x));
        };

        // Derive a function from the name of a JS infix operator
        // op :: String -> (a -> a -> b)
        const op = strOp =>
            eval(`(a, b) => a ${strOp} b`);

        // partition :: (a -> Bool) -> [a] -> ([a], [a])
        const partition = p =>
            // A tuple of two lists - those elements in 
            // xs which match p, and those which don't.
            xs => list(xs).reduce(
                (a, x) => p(x) ? (
                    Tuple(a[0].concat(x))(a[1])
                ) : Tuple(a[0])(a[1].concat(x)),
                Tuple([])([])
            );

        // root :: Tree a -> a
        const root = tree => tree.root;

        // showJSON :: a -> String
        const showJSON = x =>
            // Indented JSON representation of the value x.
            JSON.stringify(x, null, 2);

        // snd :: (a, b) -> b
        const snd = tpl => tpl[1];

        // span, applied to a predicate p and a list xs, returns a tuple of xs of 
        // elements that satisfy p and second element is the remainder of the list:
        //
        // > span (< 3) [1,2,3,4,1,2,3,4] == ([1,2],[3,4,1,2,3,4])
        // > span (< 9) [1,2,3] == ([1,2,3],[])
        // > span (< 0) [1,2,3] == ([],[1,2,3])
        //
        // span p xs is equivalent to (takeWhile p xs, dropWhile p xs) 
        // span :: (a -> Bool) -> [a] -> ([a], [a])
        const span = p =>
            // Longest prefix of xs consisting of elements which
            // all satisfy p, tupled with the remainder of xs.
            xs => {
                const
                    ys = 'string' !== typeof xs ? (
                        list(xs)
                    ) : xs,
                    iLast = ys.length - 1;
                return splitAt(
                    until(
                        i => iLast < i || !p(ys[i])
                    )(i => 1 + i)(0)
                )(ys);
            };

        // splitArrow (***) :: (a -> b) -> (c -> d) -> ((a, c) -> (b, d))
        const splitArrow = f =>
            // The functions f and g combined in a single function
            // from a tuple (x, y) to a tuple of (f(x), g(y))
            // (see bimap)
            g => tpl => Tuple(f(tpl[0]))(
                g(tpl[1])
            );

        // splitAt :: Int -> [a] -> ([a], [a])
        const splitAt = n =>
            xs => Tuple(xs.slice(0, n))(
                xs.slice(n)
            );

        // splitOn :: [a] -> [a] -> [[a]]
        // splitOn :: String -> String -> [String]
        const splitOn = pat => src =>
            /* A list of the strings delimited by
               instances of a given pattern in s. */
            ('string' === typeof src) ? (
                src.split(pat)
            ) : (() => {
                const
                    lng = pat.length,
                    tpl = findIndices(matching(pat))(src).reduce(
                        (a, i) => Tuple(
                            fst(a).concat([src.slice(snd(a), i)])
                        )(lng + i),
                        Tuple([])(0),
                    );
                return fst(tpl).concat([src.slice(snd(tpl))]);
            })();

        // take :: Int -> [a] -> [a]
        // take :: Int -> String -> String
        const take = n =>
            // The first n elements of a list,
            // string of characters, or stream.
            xs => 'GeneratorFunction' !== xs
            .constructor.constructor.name ? (
                xs.slice(0, n)
            ) : [].concat.apply([], Array.from({
                length: n
            }, () => {
                const x = xs.next();
                return x.done ? [] : [x.value];
            }));

        // uncons :: [a] -> Maybe (a, [a])
        const uncons = xs => {
            // Just a tuple of the head of xs and its tail, 
            // Or Nothing if xs is an empty list.
            const lng = length(xs);
            return (0 < lng) ? (
                Infinity > lng ? (
                    Just(Tuple(xs[0])(xs.slice(1))) // Finite list
                ) : (() => {
                    const nxt = take(1)(xs);
                    return 0 < nxt.length ? (
                        Just(Tuple(nxt[0])(xs))
                    ) : Nothing();
                })() // Lazy generator
            ) : Nothing();
        };

        // unlines :: [String] -> String
        const unlines = xs =>
            // A single string formed by the intercalation
            // of a list of strings with the newline character.
            xs.join('\n');

        // until :: (a -> Bool) -> (a -> a) -> a -> a
        const until = p => f => x => {
            let v = x;
            while (!p(v)) v = f(v);
            return v;
        };

        // JS Trees ----------------------------------------------------
        // forestFromLineIndents :: [(Int, String)] -> [Tree String]
        const forestFromLineIndents = tuples => {
            const go = xs =>
                0 < xs.length ? (() => {
                    const [n, s] = Array.from(xs[0]);
                    // Lines indented under this line,
                    // tupled with all the rest.
                    const [firstTreeLines, rest] = Array.from(
                        span(x => n < x[0])(xs.slice(1))
                    );
                    // This first tree, and then the rest.
                    return [
                        Node({
                            body: s,
                            depth: n
                        })(go(firstTreeLines))
                    ].concat(go(rest));
                })() : [];
            return go(tuples);
        };

        // forestFromTaskPaperString :: String -> Tree Dict
        const forestFromTaskPaperString = s => {
            const
                tpItemType = x => x.startsWith('- ') ? ({
                    text: x.slice(2),
                    type: 'task'
                }) : x.endsWith(':') ? ({
                    text: x.slice(0, -1),
                    type: 'project'
                }) : {
                    text: x,
                    type: 'note'
                },
                tpTagDict = xs => xs.reduce((a, x) => {
                    const kv = x.split('(');
                    return Object.assign(a, {
                        [kv[0]]: 1 < kv.length ? (
                            kv[1].split(')')[0]
                        ) : ''
                    })
                }, {}),
                tpParse = dct => {
                    const
                        pair = splitArrow(tpItemType)(tpTagDict)(
                            uncons(
                                splitOn(' @')(dct.body)
                            ).Just
                        );
                    return Object.assign({}, dct, pair[0], {
                        tags: pair[1]
                    });
                };
            return compose(
                map(fmapTree(tpParse)),
                forestFromLineIndents,
                indentLevelsFromLines,
                filter(Boolean),
                lines
            )(s);
        };

        // indentLevelsFromLines :: [String] -> [(Int, String)]
        const indentLevelsFromLines = xs => {
            const
                indentTextPairs = xs.map(compose(
                    first(length),
                    span(isSpace)
                )),
                indentUnit = minimum(
                    indentTextPairs.flatMap(pair => {
                        const w = fst(pair);
                        return 0 < w ? [w] : [];
                    })
                );
            return indentTextPairs.map(
                first(flip(div)(indentUnit))
            );
        };

        // indentedLinesFromTrees :: String -> (a -> String) ->
        //      [Tree a] -> [String]
        const indentedLinesFromTrees = strTab => f => trees => {
            const go = indent => node => [indent + f(node)]
                .concat(node.nest.flatMap(go(strTab + indent)));
            return trees.flatMap(go(''));
        };

        // treeWithNotes :: Tree Dict -> Tree Dict
        const treeWithNotes = foldTree(item => subtrees => {
            const [withNotes, withoutNotes] = Array.from(
                partition(
                    child => child.root.type === 'note'
                )(subtrees)
            );
            return Node(
                Object.assign({},
                    item, {
                        note: compose(
                            unlines,
                            indentedLinesFromTrees('\t')(
                                compose(
                                    x => x.text,
                                    root
                                )
                            )
                        )(withNotes)
                    }
                )
            )(
                withoutNotes
            )
        })

        // OmniFocus OmniJS --------------------------------------------
        const flattenTpDict = dct => {
            const [kvNames, tagNames] = Array.from(
                partition(
                    k => Boolean(dct['tags'][k]) || k === 'flagged'
                )(Object.keys(dct.tags))
            );
            return Object.assign({}, {
                text: dct.text,
                note: dct.note
            }, kvNames.reduce((a, k) =>
                Object.assign({},
                    a, {
                        [k]: dct['tags'][k]
                    }
                ), {}
            ), {
                tags: tagNames.reduce((a, k) =>
                    Object.assign({},
                        a, {
                            [k]: dct['tags'][k]
                        }
                    ), {}
                )
            })
        }

        // ofDictFromTpKV :: JS Dict -> JS Dict
        const ofDictFromTpKV = tpDct => {
            const
                dct = flattenTpDict(tpDct),
                propTable = {
                    'text': 'name',
                    'note': 'note',
                    'flagged': 'flagged',
                    'defer': 'deferDate',
                    'tags': 'tags'
                },
                fnTable = {
                    'text': identity,
                    'note': identity,
                    'flagged': constant(true),
                    'defer': x => new Date(x),
                    'tags': dct => Object.keys(dct)
                };
            return foldl(
                a => x => {
                    const
                        k = propTable[x],
                        v = dct[x],
                        f = fnTable[x];
                    return Object.assign({},
                        a, {
                            [k]: f(v)
                        })
                }
            )({})(Object.keys(dct))
        };

        // ofObjectsFromForest :: OF Object -> [Tree String] -> [OF Object]
        const ofObjectsFromForest = parent =>
            // New Folders, Projects or Tasks appended
            // to the children of the given parent,
            // which can be a Document, Folder, Project, or Task.
            trees => {
                const go = parent => tree => {
                    const item = ofocInsertedChild(parent)(tree.root);
                    return (
                        tree.nest.map(go(item)),
                        item
                    );
                };
                return trees.map(go(parent));
            };

        // ofocInsertedChild :: OF Object -> Dict -> OF Object
        const ofocInsertedChild = parent =>
            // A new Folder, Project or Task appended
            // to the children of the given parent.
            dct => {
                const
                    strParentType = parent.constructor.name,
                    {
                        name,
                        tags,
                        rest
                    } = dct,
                    item = new this[{
                        'Database': 'Folder',
                        'Folder': 'Project',
                        'Project': 'Task'
                    } [strParentType] || 'Task'](
                        name,
                        'Database' !== strParentType ? (
                            parent
                        ) : null
                    ),
                    itemType = item.constructor.name;
                if (itemType !== 'Folder') {
                    item.addTags(
                        map(tagFoundOrCreated)(tags)
                    )
                }
                return Object.assign(
                    item,
                    rest
                )
            };

        // projectFoundOrCreated :: Project Name -> Project Object
        const projectFoundOrCreated = strProject =>
            projectNamed(strProject) || new Project(strProject)

        // tagFoundOrCreated :: Tag Name -> Tag Object
        const tagFoundOrCreated = strTag =>
            tagNamed(strTag) || new Tag(strTag)

        // MAIN -----------------------------------------
        return main()
    };


    // OmniJS Context Evaluation ------------------------------------------------
    return 0 < Application('OmniFocus').documents.length ? (
        Application('OmniFocus').evaluateJavascript(
            `(${omniJSContext})(${JSON.stringify(options)})`
        )
    ) : 'No documents open in OmniFocus.'
})();