Action

Import Taskpaper-formatted Draft into OmniOutliner

Posted by unlocked2412, Last update almost 3 years ago

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

If you found this useful, you can:

ko-fi

Steps

  • script

    const options = {
    	content: draft.content
    }
  • script

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

Options

  • After Success Default
    Notification Info
    Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.