Importing to OmniOutliner in Notes

I like using the notes under an outliner line so that it can be easily hidden. But I am most often coming into Outliner from Markdown in Drafts or some other outline format. Is there a way to format incoming text so it will go into a note instead of the main outline?

1 Like

Interesting question. I think new lines are interpreted as distinct rows in OmniOutliner when text is pasted and probably there isn’t a way to automatically create new rows with notes using a specific formatting. Perhaps, you would need to use automation to achieve what you want.

I have tried to pursue this via automation also (using the javascript etc on omniautomation) but haven’t been able to figure it out.

Could you give an example of what you are trying to do ? How do you format your notes ?

Is there a specific database field identifier for notes that you can access with OmniJS? If so, perhaps, when you parse your file, you could use certain tags to identify lines you want the plugin to insert into the note of the row you most recently created.

Here is what I want to get (specifically the two grey lines

And the text I’m importing:

  • An Idea
    • Sub-idea
  • Another idea
    • Sub-idea
    • Sub-idea with note
      What a great note!!! //<–– This
    • Another sub-idea with note
      And then you did it again!! //<–– This

That’s a very nice example, @jarno527. I will try to use automation to solve this problem.

Finished the first draft. Could you tell me if it solves your problem, @jarno527 ?

Tested on OmniOutliner 5.7.1 Mac (do not think it works on previous versions). Would be possible to make a Drafts action out of it, I think.

Usage:

  • Copy taskpaper-formatted text.
  • Execute the plugin.

Full code:

/*{
	"type": "action"
}*/
// Twitter: @unlocked2412
(() => Object.assign(
    new PlugIn.Action(selection => {

        // omniJSContext :: IO ()
        const omniJSContext = () => {
            // main :: IO ()
            const main = () => {
                const forest = compose(
                    map(
                        compose(
                            fmapTree(
                                x => ({
                                    topic: x.text,
                                    note: x.note
                                })
                            ),
                            treeWithNotes
                        )
                    ),
                    forestFromTaskPaperString
                )(Pasteboard.general.string)

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

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

            // GENERIC FUNCTIONS --------------------------------------------
            // https://github.com/RobTrew/prelude-jxa
            // partition :: (a -> Bool) -> [a] -> ([a], [a])
            const partition = p => xs =>
                xs.reduce(
                    (a, x) =>
                    p(x) ? (
                        Tuple(a[0].concat(x))(a[1])
                    ) : Tuple(a[0])(a[1].concat(x)),
                    Tuple([])([])
                );
            // 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');

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


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

            // Just :: a -> Maybe a
            const Just = x => ({
                type: 'Maybe',
                Nothing: false,
                Just: x
            });

            // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
            const compose = (...fs) =>
                x => fs.reduceRight((a, f) => f(a), x);

            // filter :: (a -> Bool) -> [a] -> [a]
            const filter = f => xs => xs.filter(f);

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

            // isSpace :: Char -> Bool
            const isSpace = c => /\s/.test(c);

            // second :: (a -> b) -> ((c, a) -> (c, b))
            const second = f =>
                // A function over a simple value lifted
                // to a function over a tuple.
                // f (a, b) -> (a, f(b))
                xy => Tuple(xy[0])(
                    f(xy[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 => xs => {
                const iLast = xs.length - 1;
                return splitAt(
                    until(i => iLast < i || !p(xs[i]))(
                        succ
                    )(0)
                )(xs);
            };

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

            // stringFromList :: [Char] -> String
            const stringFromList = cs =>
                // A String derived from a list of characters.
                cs.join('');

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

            // flip :: (a -> b -> c) -> b -> a -> c
            const flip = f =>
                1 < f.length ? (
                    (a, b) => f(b, a)
                ) : (x => y => f(y)(x));

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

            // 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 || []
                });

            // 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))
                g => tpl => Tuple(f(tpl[0]))(
                    g(tpl[1])
                );

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

            // splitOn("\r\n", "a\r\nb\r\nd\r\ne") //--> ["a", "b", "d", "e"]
            // splitOn("aaa", "aaaXaaaXaaaXaaa") //--> ["", "X", "X", "X", ""]
            // splitOn("x", "x") //--> ["", ""]
            // splitOn([3, 1], [1,2,3,1,2,3,1,2,3]) //--> [[1,2],[2],[2,3]]
            // 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))]);
                })();

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

            // 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
                (Array.isArray(xs) || 'string' === typeof xs) ? (
                    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]/)
                ) : [];

            // 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 => (
                    Array.isArray(xs) ? (
                        xs
                    ) : xs.split('')
                ).map(f);

            // minimum :: Ord a => [a] -> a
            const minimum = xs =>
                0 < xs.length ? (
                    xs.slice(1)
                    .reduce((a, x) => x < a ? x : a, xs[0])
                ) : undefined;

            // succ :: Enum a => a -> a
            const succ = x => {
                const t = typeof x;
                return 'number' !== t ? (() => {
                    const [i, mx] = [x, maxBound(x)].map(fromEnum);
                    return i < mx ? (
                        toEnum(x)(1 + i)
                    ) : Error('succ :: enum out of range.')
                })() : x < Number.MAX_SAFE_INTEGER ? (
                    1 + x
                ) : Error('succ :: Num out of range.')
            };


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

            // 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(
                        //second(cs => cs.join('')),
                        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(''));
            };

            // taskPaperFromTree :: Tree -> String
            const taskPaperFromTree = x => {
                const
                    rgxSpace = /\s+/g,
                    go = strIndent => x => {
                        const
                            nest = x.nest,
                            root = x.root,
                            txt = root.text || '',
                            tags = root.tags,
                            ks = Boolean(tags) ? (
                                Object.keys(tags)
                            ) : [],
                            note = root.note,
                            blnNotes = Boolean(note),
                            blnTags = ks.length > 0,
                            blnNest = nest.length > 0,
                            strNext = '\t' + strIndent;

                        return or([Boolean(txt), blnTags, blnNotes, blnNest]) ? (
                            strIndent + (root.type !== 'project' ? (
                                '- ' + txt
                            ) : txt + ':') +
                            (blnTags ? foldl(
                                t => k => {
                                    const v = tags[k];
                                    return t + (Boolean(v) ? (
                                        ' @' + k.replace(rgxSpace, '_') +
                                        (Boolean(v) && v !== true ? (
                                            '(' + v + ')'
                                        ) : '')
                                    ) : '');
                                }
                            )('')(ks) : '') +
                            (blnNotes ? (
                                '\n' + unlines(map(
                                    s => strNext + s
                                )(lines(note)))
                            ) : '') + (blnNest ? (
                                '\n' + unlines(map(go(strNext))(nest))
                            ) : '')
                        ) : '';
                    };
                return go('')(x);
            };

            // 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, {
                            text: item.text,
                            note: compose(
                                unlines,
                                indentedLinesFromTrees('\t')(
                                    compose(
                                        x => x.text,
                                        root
                                    )
                                )
                            )(withNotes)
                        }
                    )
                )(
                    withoutNotes
                )
            })


            // main :: IO ()
            return main()
        };

        return omniJSContext()
        
        }), {
            validate: selection => true
        }
))();
1 Like

Oh this is phenomenal. Wow. Super impressed. Thank you for your work here.

Ok. I’m going to try to understand the code enough to turn it into a Drafts action. Or if you can shoot me a note on which variable is the text entering the function that would also be helpful.

I have wanted something like this for a super long time. Thank you!

1 Like

One other quick note. it runs like a charm on Mac OS but I’m not getting it to work on iPad. Which hey—this is already crazy further than I hoped to get. But just a thought.

You’re very welcome, @jarno527.

I will do the Drafts action as soon as I am in front of the computer coding.

Yes, I’m not getting clipboard contents through Pasteboard.general.string on OO iPad version. Perhaps, would be good to contact support.

On Slack, I was told that Pasteboard.general.string is working in current build.

@jarno527, just posted a Drafts 5 action, here:

Oh this is phenomenal. Super awesome. Thanks! Impressive skills to whip that out there. I’ll noise this abroad also in the Drafts community.

1 Like

@jarno527, I cross-posted this to Drafts community 10 hours ago. I’m glad it is useful.

1 Like