Import Taskpaper-formatted Draft into OmniOutliner (Mac/iOS)

This Drafts 5 action imports a TaskPaper-formatted Draft into OmniOutliner using the new OmniJS API.

For example, starting with this text:

Gardening:
	- Choose location.
	- Buy basic garden tools.
	- Nurture the garden.
		A rule of thumb for watering the plants is one inch of water per week during the growing season.
Write a book:
	- Establish my writing space.
	- Assemble my writing tools.
		Pen, pencil and paper.
	- Write.

Produces this OmniOutliner outline:

Link:
https://actions.getdrafts.com/a/1ar

Code:

// Twitter: @unlocked2412
// DRAFTS JS CODE --------------------------------------------------------
const draftsJSContext = () => {
    // main :: IO ()
    const main = () => {
        return runOmniJSWithArgsFromDrafts(
            'omnioutliner://localhost/omnijs-run',
            omniJSContext,
            options
        )
    };

    // OMNI JS CODE ---------------------------------------
    const omniJSContext = opts => {
        // main :: IO ()
        const main = () => {
            const forest = compose(
                map(
                    compose(
                        fmapTree(
                            x => ({
                                topic: x.text,
                                note: x.note
                            })
                        ),
                        treeWithNotes
                    )
                ),
                forestFromTaskPaperString
            )(opts.content)

            return Document.makeNewAndShow(
                doc => ooRowsFromForest(doc.outline.rootItem)(
                    forest
                )
            )
        };

        // 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
            );

        // 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;
        };

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

        // 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;

        // 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
            )
        })

        // OmniOutliner OmniJS -----------------------------------------
        // ooRowsFromForest :: OO Item -> [Tree] -> [OO Item]
        const ooRowsFromForest = parent => trees => {
            const go = parent => tree => {
                const
                    item = parent.addChild(
                        null,
                        x => Object.assign(
                            x, tree.root
                        )
                    );
                return (
                    tree.nest.map(go(item)),
                    item
                );
            };
            return trees.map(go(parent));
        };

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

    // runOmniJSWithArgsFromDrafts :: URL String -> Function -> [...OptionalArgs] -> a
    function runOmniJSWithArgsFromDrafts(baseURL, f) {
        const
            strCode = encodeURIComponent(
                `(${f})(${Array.from(arguments)
                .slice(2).map(JSON.stringify)})`
            ),
            strURL = `${baseURL}?script=${strCode}`;
        return app.openURL(strURL)
}

    return main()
};
draftsJSContext()
1 Like