Copy OmniGraffle 7 diagram outline as TaskPaper 3 text outline

The links between OmniGraffle shapes are represented in a left hand panel as an outline structure.

Here is a macro which copies:

  • All shape text in the open OG7 canvas,
  • in TaskPaper 3 outline format.

The mapping from OG to TP3 is:

  1. Shape texts -> TP3 items
  2. Shape notes -> TP3 notes
  3. Shape userData key value pairs -> TP3 key@(value) tags
  4. Shape name -> TP3 @name tag

Copy OmniGraffle 7 diagram outline as Taskpaper 3 text outline.kmmacros (25.6 KB)

JS Source text (omniJS + JXA)

(() => {
    'use strict';

    // Rob Trew (c) 2018

    // Ver 0.2
    //      - Shape name now added as TaskPaper tag
    //      - Spaces in tag names replaced with underscores

    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY
    // OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
    // LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
    // BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
    // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
    // ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    const jxaMain = () => {

        // main :: IO ()
        const main = () => {
            const
                og = Application('OmniGraffle'),
                lrTP3 = bindLR(
                    og.documents.length > 0 ? Right(
                        taskPaperFromTrees(
                            JSON.parse(
                                og.evaluateJavascript(
                                    '(' + ogJSContext + ')()'
                                )
                            ).nest
                        )
                    ) : Left('No documents open in OmniGraffle'),
                    strTP3 => (
                        standardAdditions()
                        .setTheClipboardTo(strTP3),
                        Right(strTP3)
                    )
                );

            // Either a message or the copied TaskPaper string.
            return lrTP3.Left || lrTP3.Right;
        };

        // GENERICS ---------------------------------------

        // Left :: a -> Either a b
        const Left = x => ({
            type: 'Either',
            Left: x
        });

        // Right :: b -> Either a b
        const Right = x => ({
            type: 'Either',
            Right: x
        });

        // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
        const bindLR = (m, mf) =>
            m.Right !== undefined ? (
                mf(m.Right)
            ) : m;

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

        // foldlTree :: (b -> a -> b) -> b -> Tree a -> b
        const foldlTree = (f, acc, node) => {
            const go = (a, x) => x.nest.reduce(go, f(a, x));
            return go(acc, node);
        };

        // lines :: String -> [String]
        const lines = s => s.split(/[\r\n]/);

        // map :: (a -> b) -> [a] -> [b]
        const map = (f, xs) => xs.map(f);

        // showJSON :: a -> String
        const showJSON = x => JSON.stringify(x, null, 2);

        // showLog :: a -> IO ()
        const showLog = (...args) =>
            console.log(
                args
                .map(JSON.stringify)
                .join(' -> ')
            );

        // unlines :: [String] -> String
        const unlines = xs => xs.join('\n');

        // TASKPAPER FROM TREES ---------------------------

        // taskPaperFromTrees :: [Tree] -> String
        const taskPaperFromTrees = xs => {
            const go = (strIndent, ts) =>
                foldl((a, x) => {
                    const
                        rgxSpace = /\s+/g,
                        nest = x.nest,
                        root = x.root,
                        txt = root.text,
                        name = root.name,
                        tags = root.userData,
                        ks = Boolean(tags) ? (
                            Object.keys(tags)
                        ) : [],
                        notes = root.notes,
                        blnName = Boolean(name),
                        blnNotes = Boolean(notes),
                        blnTags = ks.length > 0,
                        blnNest = nest.length > 0,
                        blnHasData = Boolean(txt) ||
                        (ks.length > 0) || blnNotes ||
                        blnNest,
                        strNext = '\t' + strIndent;

                    return blnHasData ? (
                        a + strIndent + '- ' + (root.text || '') +
                        (blnName ? (
                            ' @' + name.replace(rgxSpace, '_')
                        ) : '') +
                        (blnTags ? (
                            foldl(
                                (t, k) => {
                                    const v = tags[k];
                                    return t +
                                        ' @' + k.replace(rgxSpace, '_') +
                                        (Boolean(v) ? (
                                            '(' + v + ')'
                                        ) : '');
                                },
                                '',
                                ks
                            )
                        ) : '') + '\n' +
                        (blnNotes ? (
                            unlines(
                                map(s => strNext + s, lines(notes))
                            ) + '\n'
                        ) : '') + (blnNest ? (
                            go(strNext, x.nest)
                        ) : '')
                    ) : a;
                }, '', ts);
            return go('', xs);
        };

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

    const ogJSContext = () => {

        // ogMain :: OG () -> JSON String
        const ogMain = () =>
            showJSON(
                pureTreeOG(canvases[0].outlineRoot)
            );

        // Node :: a -> [Tree a] -> Tree a
        const Node = (v, xs) => ({
            type: 'Node',
            root: v,
            nest: xs || []
        });

        // pureTreeOG :: OGNode  -> Tree OGNode
        const pureTreeOG = node => {
            const go = x => {
                const
                    g = x.graphic || {},
                    xs = x.children;
                return Node(
                    ['text', 'name', 'notes', 'userData']
                    .reduce((a, k) => {
                        const v = g[k];
                        return Boolean(v) ? (
                            Object.assign(a, {
                                [k]: v
                            })
                        ) : a;
                    }, {}),
                    xs.length > 0 ? xs.map(go) : []
                );
            };
            return go(node);
        };

        // showJSON :: a -> String
        const showJSON = x =>
            JSON.stringify(x, null, 2);

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

    // JXA ------------------------------------------------

    // standardAdditions :: () -> Application
    const standardAdditions = () =>
        Object.assign(Application.currentApplication(), {
            includeStandardAdditions: true
        });

    return jxaMain();
})();

Updated to Ver 0.2 above:

  • Any shape name now added as a further TaskPaper tag,
  • any spaces in tag names now replaced with underscores, and
  • a message returned if no document is open in OmniGraffle.