Illustrative omniJS macro for OmniGraffle 7.5 test builds

omniJS (scripting in a JS Context embedded inside an Omni app) is an interesting development because it allows cross-platform macros - omniJS code written for the macOS 7.4 (Test) OmniGraffle for example, can also be run in the current iOS OmniGraffle tests.

While very fast and exceptionally cross-platform, app-embedded JS Contexts are like web browsers in that they don't have direct access to the file system.

On the macOS side that can be bridged by getting JavaScript for Automation and omniJS to talk to each other. That process is going to be simplified (an evaluateOmniJS function is being written for AppleScript and JXA Omni App libraries), but it can already be done indirectly.

For anyone who is curious, here is an example - copying a hierarchical diagram from OmniGraffle 7.4 (Test) as a TaskPaper text outline:

Originally written in ES6 JavaScript, but I've back-converted it to ES5 through the Babel JS REPL:

Copy (OG 7.4 Test build) diagram outline as Taskaper text.kmmacros (25.3 KB)

ES5 version of JS Code, both OmniJS and JXA below. The ES6 source can be found at:

'use strict';

(function () {
    'use strict';

    // COPY AN OG7 TEST BUILD TREE DIAGRAM (OR ANY NESTED 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]
    var ap = function ap(fs, xs) {
        return ( //
            [].concat.apply([], fs.map(function (f) {
                return ( //
                    [].concat.apply([], xs.map(function (x) {
                        return [f(x)];
                    }))
                );
            }))
        );
    };

    // compose :: (b -> c) -> (a -> b) -> (a -> c)
    var compose = function compose(f, g) {
        return function (x) {
            return f(g(x));
        };
    };

    // foldl :: (b -> a -> b) -> b -> [a] -> b
    var foldl = function foldl(f, a, xs) {
        return xs.reduce(f, a);
    };

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

    // An integer prepended to the argument list yields an indentation level
    // show :: a -> String
    var show = function show() {
        for (var _len = arguments.length, x = Array(_len), _key =
                0; _key < _len; _key++) {
            x[_key] = arguments[_key];
        }

        return 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
    var evaluateOmniJS = function evaluateOmniJS(f, dctOptions) {
        var 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
        var og = Application('OmniGraffle'),
            ws = (og.activate(), og.windows),
            mw = ws.length > 0 ? {
                just: ws.at(0),
                nothing: false
            } : {
                nothing: true
            },
            mResult = mw.nothing ? mw : function () {
                var w = mw.just;
                return w.name()
                    .indexOf('Automation Console') === 0 ? {
                        nothing: true,
                        msg: 'Front window was Automation Console'
                    } : function () {
                        var 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
    var ogFrontDoc = function ogFrontDoc(dctOptions) {
        var 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) : function () {
                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
    var treeJSON = function treeJSON() {

        // show :: a -> String
        var show = function show() {
            for (var _len2 = arguments.length, x =
                    Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
                x[_key2] = arguments[_key2];
            }

            return JSON.stringify.apply(null, x.length > 1 ? [x[0], null, x[1]] : [x]);
        };

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

        var 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
    var jsoToTaskPaper = function jsoToTaskPaper(xs) {
        var indented = function indented(strIndent, v) {
            return Array.isArray(v) ? foldl(function (a, x) {
                return 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 --------------
    var 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;
})();