An early omniJS experiment - returning a JSON outline to JXA

For OmniGraffle Test version 7.4 test (v179.5 r290241)

OmniJS, already fast and cross-platform, is also becoming useful.

FWIW, as a kind of tester’s journal entry, here is an experiment in which an omniJS function is called by JavaScript for Automation, and uses the new OGOutlineNode interface to return to JXA a JSON text-nest version of the outline text of a nested diagram in the selected canvas.

(omniJS is still being finished and polished in the workshop - so this draft uses an interim hack for getting a value from omniJS back to JXA – Omni is already preparing something much better).

(() => {
    'use strict';

    // Rough draft .003 - purely illustrative no claims or guarantees
    // use with caution, and not on real data.

    // Rob Trew June 20 2017

    // GENERIC JS FUNCTIONS --------------------------------------------------

    // A list of functions applied to a list of arguments
    // <*> :: [(a -> b)] -> [a] -> [b]
    const ap = (fs, xs) => //
        [].concat.apply([], fs.map(f => //
            [].concat.apply([], xs.map(x => [f(x)]))));

    // An integer prepended to the argument list yields an indentation level
    // show :: a -> String
    const show = (...x) =>
        JSON.stringify.apply(
            null, x.length > 1 ? [x[0], null, x[1]] : x
        );

    // JAVASCRIPT FOR AUTOMATION FUNCTIONS  ---------------------------------

    // menuItemClick :: String -> [String] -> UI ()
    const menuItemClick = (strAppName, lstMenuPath) => {
        const intMenuPath = lstMenuPath.length;

        if (intMenuPath > 1) {
            const appProcs = Application('System Events')
                .processes.where({
                    name: strAppName
                }),
                procApp = appProcs.length ? appProcs[0] : undefined;

            if (procApp) {
                Application(strAppName)
                    .activate();

                lstMenuPath.slice(1, -1)
                    .reduce((a, x) => a.menuItems[x].menus[x],
                        procApp.menuBars[0].menus.byName(lstMenuPath[0]))
                    .menuItems[lstMenuPath[intMenuPath - 1]].click();
            }
        }
    };

    // OMNIGRAFFLE JXA FUNCTIONS ---------------------------------------------

    // evaluateOmniJS :: (Options -> OG Maybe JSON String) -> {KeyValues}
    //      -> Maybe JSON String
    const evaluateOmniJS = (f, dctOptions) => {
        const
            a = Application.currentApplication(),
            sa = (a.includeStandardAdditions = true, a);

        ap([sa.openLocation, sa.setTheClipboardTo], //
            ['omnigraffle:///omnijs-run?script=' +
                encodeURIComponent(
                    '(' + f.toString() + ')(' +
                    (dctOptions && Object.keys(dctOptions)
                        .length > 0 ? show(dctOptions) : '') + ')'
                )
            ]);
        // Possible harvest of return value from canvasbackground.userData
        const
            og = Application('OmniGraffle'),
            ws = og.windows,
            mw = ws.length > 0 ? {
                just: ws.at(0),
                nothing: false
            } : {
                nothing: true
            },
            mResult = mw.nothing ? mw : (() => {
                const w = mw.just;
                return w.name()
                    .indexOf('Automation Console') === 0 ? ({
                        nothing: true,
                        msg: 'Front window was Automation Console'
                    }) : (() => {
                        const v = w.canvas()
                            .canvasbackground.userDataItems.byName('omniJSON')
                            .value()
                        return (v === undefined || v === null) ? ({
                            nothing: true,
                            msg: "No JSON found in " +
                                ".canvasbackground.userDataItems.byName('omniJSON')"
                        }) : {
                            just: v,
                            nothing: false
                        }
                    })();
            })();
        return mResult.nothing ? mResult : (
            mw.just.canvas()
            .canvasbackground.userDataItems.byName('omniJSON')
            .value = null,
            mResult
        );
    };


    // ogFrontDoc :: {useExisting : Bool, templateName: String} -> OG.Document
    const ogFrontDoc = dctOptions => {
        const
            options = dctOptions || {},
            og = Application('OmniGraffle'),
            optTemplate = options.templateName,
            xs = og.availableTemplates(),
            strTemplate = (optTemplate && elem(optTemplate, xs)) ? (
                optTemplate
            ) : xs[0],
            ds = og.documents,
            d = options.useExisting && ds.length > 0 ? (
                ds.at(0)
            ) : (() => {
                return (
                    ds.push(og.Document({
                        template: strTemplate
                    })),
                    ds.at(0)
                );
            })();
        return (
            og.activate(),
            d
        );
    };


    // AN OMNIJS FUNCTION to be run with evaluateOmniJS()  (above) ----------

    // treeJSON :: OG () -> JSON String
    const treeJSON = () => {

        // show :: a -> String
        const show = (...x) =>
            JSON.stringify.apply(
                null, x.length > 1 ? [x[0], null, x[1]] : x
            );

        // textNest :: OGoutlineNodes -> Node {text: String, nest:[Node]}
        const textNest = xs =>
            xs.length ? xs.map(x => ({
                text: x.graphic.text,
                nest: x.children.length > 0 ? (
                    textNest(x.children)
                ) : []
            })) : [];

        const
            cnv = document.windows[0].selection.canvas,
            strJSON = show(textNest(
                cnv.outlineRoot.children
            ));
            
        // Save a return value for JXA to find in Canvas background user-data
        return (
            cnv.background.setUserData('omniJSON', strJSON),
            strJSON
        );
    };

    // TEST ------------------------------------------------------------------
    const
        d = ogFrontDoc({
            useExisting: true
        }),
        maybeReturnValue = evaluateOmniJS(treeJSON);

    // // Open the OG Script Console
    //menuItemClick('OmniGraffle', ['Automation', 'Show Console']);

    return maybeReturnValue.nothing ? (
        undefined
    ) : JSON.stringify(JSON.parse(maybeReturnValue.just), null, 2);
})();

And a simple application - copy tree diagram (any nested diagram) as a TaskPaper text outline.

Works with: OmniGraffle 7.4 test (v179.5 r290288)

(Why copy a tree diagram as a TaskPaper outline ? I regularly generate nested diagrams from text outlines (typically from the TaskPaper app, which I find rather fast and flexible as well as scriptable) and I often find myself making textual edits in the diagram nodes. A script like this just helps me capture the final version of the edited outline back from the diagram.)

Sierra (ES6) Javascript – for back-conversion to Yosemite+ ES5 JS, paste into the REPL at https://babeljs.io/repl/

This can be launched from Script Editor, from Atom with the Script plugin, or from utilities like Keyboard Maestro etc

    (() => {
        'use strict';

    // COPY AN OG7 TEST-BUILD TREE DIAGRAM (OR ANY NEST DIAGRAM)

        // TO THE CLIPBOARD AS A TASKPAPER TAB-INDENTED TEXT OUTLINE

        // Rough draft .001 - purely illustrative no claims or guarantees
        // use with caution, and not on real data.

        // Rob Trew June 20 2017

        // GENERIC JS FUNCTIONS --------------------------------------------------

        // A list of functions applied to a list of arguments
        // <*> :: [(a -> b)] -> [a] -> [b]
        const ap = (fs, xs) => //
            [].concat.apply([], fs.map(f => //
                [].concat.apply([], xs.map(x => [f(x)]))));

        // compose :: (b -> c) -> (a -> b) -> (a -> c)
        const compose = (f, g) => x => f(g(x));

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

        // Default value (n) if m.nothing, or f(m.just)
        // maybe :: b -> (a -> b) -> Maybe a -> b
        const maybe = (n, f, m) =>
            m.nothing ? n : f(m.just);

        // An integer prepended to the argument list yields an indentation level
        // show :: a -> String
        const show = (...x) =>
            JSON.stringify.apply(
                null, x.length > 1 ? [x[0], null, x[1]] : x
            );

        // OMNIGRAFFLE JXA FUNCTIONS ---------------------------------------------

        // evaluateOmniJS :: (Options -> OG Maybe JSON String) -> {KeyValues}
        //      -> Maybe JSON String
        const evaluateOmniJS = (f, dctOptions) => {
            const
                a = Application.currentApplication(),
                sa = (a.includeStandardAdditions = true, a);

            ap([sa.openLocation, sa.setTheClipboardTo], //
                ['omnigraffle:///omnijs-run?script=' +
                    encodeURIComponent(
                        '(' + f.toString() + ')(' +
                        (dctOptions && Object.keys(dctOptions)
                            .length > 0 ? show(dctOptions) : '') + ')'
                    )
                ]);
            // Possible harvest of return value from canvasbackground.userData
            const
                og = Application('OmniGraffle'),
                ws = (og.activate(), og.windows),
                mw = ws.length > 0 ? {
                    just: ws.at(0),
                    nothing: false
                } : {
                    nothing: true
                },
                mResult = mw.nothing ? mw : (() => {
                    const w = mw.just;
                    return w.name()
                        .indexOf('Automation Console') === 0 ? ({
                            nothing: true,
                            msg: 'Front window was Automation Console'
                        }) : (() => {
                            const v = w.canvas()
                                .canvasbackground.userDataItems.byName('omniJSON')
                                .value()
                            return (v === undefined || v === null) ? ({
                                nothing: true,
                                msg: "No JSON found in " +
                                    ".canvasbackground.userDataItems.byName('omniJSON')"
                            }) : {
                                just: v,
                                nothing: false
                            }
                        })();
                })();
            return mResult.nothing ? mResult : (
                mw.just.canvas()
                .canvasbackground.userDataItems.byName('omniJSON')
                .value = null,
                mResult
            );
        };

        // ogFrontDoc :: {useExisting : Bool, templateName: String} -> OG.Document
        const ogFrontDoc = dctOptions => {
            const
                options = dctOptions || {},
                og = Application('OmniGraffle'),
                optTemplate = options.templateName,
                xs = og.availableTemplates(),
                strTemplate = (optTemplate && elem(optTemplate, xs)) ? (
                    optTemplate
                ) : xs[0],
                ds = og.documents,
                d = options.useExisting && ds.length > 0 ? (
                    ds.at(0)
                ) : (() => {
                    return (
                        ds.push(og.Document({
                            template: strTemplate
                        })),
                        ds.at(0)
                    );
                })();
            return (
                og.activate(),
                d
            );
        };

        // AN OMNIJS FUNCTION to be run with evaluateOmniJS()  (above) ----------

        // treeJSON :: OG () -> JSON String
        const treeJSON = () => {

            // show :: a -> String
            const show = (...x) =>
                JSON.stringify.apply(
                    null, x.length > 1 ? [x[0], null, x[1]] : [x]
                );

            // textNest :: OGoutlineNodes -> Node {text: String, nest:[Node]}
            const textNest = xs =>
                xs.length ? xs.map(x => ({
                    text: x.graphic.text,
                    nest: x.children.length > 0 ? (
                        textNest(x.children)
                    ) : []
                })) : [];

            const
                cnv = document.windows[0].selection.canvas,
                strJSON = show(textNest(
                    cnv.outlineRoot.children
                ));

            // Save a return value for JXA to find in Canvas background user-data
            return (
                cnv.background.setUserData('omniJSON', strJSON),
                strJSON
            );
        };

        // JSO TEXT NEST FUNCTION ------------------------------------------------

        // jsoToTaskPaper :: [TextNest] -> String
        const jsoToTaskPaper = xs => {
            const indented = (strIndent, v) =>
                Array.isArray(v) ? (
                    foldl((a, x) => a + indented(strIndent, x), '', v)
                ) : (
                    (strIndent === '' ? (
                        v.text + ':'
                    ) : strIndent + '- ' + v.text) + '\n' +
                    (v.nest.length > 0 ? (
                        indented('\t' + strIndent, v.nest)
                    ) : '')
                );
            return indented('', xs);
        };

        // TEST: COPY ACTIVE OMNIGRAFFLE OUTLINE AS TASKAPER OUTLINE --------------
        const
            d = ogFrontDoc({
                useExisting: true
            }),
            a = Application.currentApplication(),
            sa = (a.includeStandardAdditions = true, a),
            strTaskPaper = maybe(
                '',
                compose(jsoToTaskPaper, JSON.parse),
                evaluateOmniJS(treeJSON)
            );

        return (
            sa.setTheClipboardTo(strTaskPaper),
            strTaskPaper
        );
    })();