Draft script: Copy As > JavaScript for Automation

In the OmniGraffle 6 menu, Edit > Copy As offers AppleScript but not JavaScript for Automation.

Below is a first rough draft (please test and report where it fails) of a Copy As > JavaScript for Automation script.

Copy selected OmniGraffle 6 shapes as JavaScript for Automation

OVERVIEW

When OG6 shapes are selected, and the script is run, it places in the clipboard:

  1. A JavaScript Object representation of the shapes and their properties
  2. A JS function which takes the JSO as an argument, and creates the
    shapes in the front canvas of the front OG6 document

USE

  1. Select graphic(s) in OmniGraffle 6
  2. Run this script (‘Copy As JSA’)
  3. Paste the updated clipboard into an empty Script Editor document,
  4. Choose JavaScript at top left of Script Editor, and run the generated code to regenerate the shapes

// Copy selected OmniGraffle 6 shapes as JavaScript for Automation
//
// ver 0.19
//
// **OVERVIEW**
//
// When OG6 shapes are selected, and the script is run, it places in the clipboard:
//
//    1. A JavaScript Object representation of the shapes and their properties
//    2. A JS function which takes the JSO as an argument, and creates the
//     shapes in the front canvas of the front OG6 document
//
// **USE**
//
//    1. Select graphic(s) in OmniGraffle 6
//    2. Run this script  ('Copy As JSA')
//    3. Paste the updated clipboard into Script Editor,
//    4. Choose `JavaScript` at top left of Script Editor, and run the generated code to regenerate the shapes

var strClip = (function () {
    'use strict';

    var propSet = function (g, ps) {
            var ps = ps || g.properties(),

                // meta-properties - not directly used for creation of graphic
                mps = [
                    'canvas', 'class', 'destination', 'id', 'layer', 'source'
                ].reduce(function (a, k) {
                    var v = ps[k];

                    if (v) a[k] = v;
                    delete ps[k];

                    return a;
                }, {}),
                strClass = mps.class,

                // class, id, idFrom, idTo, and other properties directly used in creating graphic
                dctG = {
                    class: capitalised(strClass),
                    id: g.id()
                        .toString(),
                    props: Object.keys(ps)
                        .concat(['size', 'origin'])
                        .reduce(function (a, k) {
                            var v = ps[k],
                                kv = (typeof v === 'undefined') ? g[k]() :
                                (
                                    k === 'text' ? textRuns(g[k]) : v
                                );

                            if (kv) a[k] = kv;

                            return a;
                        }, {})
                },
                fnFrom = mps.source,
                fnTo = mps.destination;

            // Source and destination ids for connecting lines
            if (fnFrom) dctG.from = fnFrom()
                .id()
                .toString();
            if (fnTo) dctG.to = fnTo()
                .id()
                .toString();

            return dctG;
        },

        // Attribute runs for formatted text
        textRuns = function (v) {
            return (typeof v === 'string') ? [v] : (function () {
                var rs = v.attributeRuns,
                    lng = rs.length,
                    i = rs.length,
                    lst = [],
                    tps; // text properties

                for (i = 0; i < lng; i++) {
                    tps = rs.at(i)
                        .properties();
                    delete tps.class;

                    lst.push( // skipping 'falsy' values
                        Object.keys(tps)
                        .reduce(function (a, k) {
                            var v = tps[k];
                            if (v) a[k] = v;
                            return a;
                        }, {})
                    );
                };

                return lst;
            })();
        },

        capitalised = function (s) {
            return s.charAt(0)
                .toUpperCase() + s.slice(1);
        },

        og = Application("OmniGraffle"),
        ws = og.windows,
        w = ws.length ? ws[0] : undefined,
        selns = w ? w.selection() : [];

    var dctLayers = selns.reduce(function (a, g) {
        var k = g.layer.name(),
            v = a[k],
            dctProps = propSet(g);

        // array of graphics for each layer
        a[k] = v ? v.concat(dctProps) : [dctProps];
        return a;
    }, {});

    return '(' + (function (dctLayers) {
            'use strict';

            // Creates layers and shapes defined at end,
            // as arguments to this function

            var og = Application('OmniGraffle'),
                ds = og.documents,
                d = ds.length ? ds[0] : (ds.push(og.Document()), ds[0]),
                ws = og.windows,
                w = ws.length ? ws[0] : undefined,
                cnv = (w && w.id() !== -1) ? w.canvas() : undefined,
                layerNames = Object.keys(dctLayers),
                blnLayers = layerNames.length > 1,
                lstLinked = [];

            // CREATION of lines and shapes
            layerNames.forEach(function (strName) {
                if (blnLayers) {
                    cnv.layers.push(
                        og.Layer({
                            name: strName,
                            visible: true,
                            prints: true
                        })
                    )
                }

                var oLayer = cnv.layers[0],
                    lgs = oLayer.graphics,
                    dctNodes = {};

                dctLayers[strName].forEach(function (g) {

                    var dctProps = g.props,
                        oText = dctProps.text,
                        lstRuns = oText ? (
                            oText instanceof Array ?
                            oText : [oText]
                        ) : [],
                        oGraphic = og[g.class](
                            (
                                dctProps.text =
                                undefined,
                                dctProps
                            )
                        ),
                        textRuns = (
                            lgs.push(oGraphic),
                            oGraphic.text.attributeRuns
                        ),

                        strID = g.id.toString(),
                        strFrom = g.from,
                        strTo = g.to;

                    lstRuns.forEach(function (dctRun) {
                        textRuns.push(
                            og.Text(dctRun)
                        );
                    });

                    if (strFrom || strTo) lstLinked.push(
                        [strID, strFrom, strTo]
                    );

                    dctNodes[strID] = oGraphic;
                });

                // CONNECTION of lines and shapes
                lstLinked.forEach(function (tuple) {
                    var g = dctNodes[tuple[0]],
                        strFrom = tuple[1],
                        strTo = tuple[2];

                    if (strFrom) g.source = dctNodes[
                        strFrom];
                    if (strTo) g.destination = dctNodes[
                        strTo];
                });
            });

        })
        .toString() + ')(' + JSON.stringify(dctLayers, null, 2) + ');';

})();

var a = Application.currentApplication(),
    sa = (a.includeStandardAdditions = true, a);

sa.setTheClipboardTo(strClip);

strClip;

I like the idea as I’ve been playing with this as well. I really want to get it where I can give it information in json and have it start a network diagram with information I gather through SNMP or another method.

A couple of things I’ve noticed from using the script so far. These may be problems, limits, features, or bugs with OG and not the script :)

This does not work for grouped objects. After running a grouped object through the script and then running the output as a script you do not get a new object and the document gives an error when attempting to save:

See my next reply for another error.

Also if you attempt to copy an object that contains an image it gives an error when running the output as a new script:

Thanks – those reports are really helpful.

Because this is written on the scripting side, rather than ‘under the hood’ – within the application itself – there are some bottlenecks; the key one, of course, being the set of information exposed by .properties()

I’ll take a look at groups and images over the weekend - not impossible that either or both can be fixed by creating special objects, in the same way as Text objects need to be handled.

Omni’s request intray is probably a zone of unclearable mountain-formation, but it might be worth both of us, and any others who are interested, submitting a request for a built-in Copy As JS

1 Like

On Grouping through JavaScript, here, I now recall, is one constraint:

1 Like

Further gaps in the scripting interface for groups, as far as I can see:

  1. .properties() doesn’t reveal group components
  2. There is an assemble method, but no unassemble or ungroup method in the scripting interface, so we can’t temporarily open up the group to inspect its contents.
1 Like

A group graphic does, however, expose a graphics collection, so over the weekend I’ll aim to refactor for a recursive reading of subgraphics (and links to ⇄ from them), and an inclusion of the reference hack to rebuild them.

Haven’t looked at Images yet, but its possible that it could be done with a separate handler.

1 Like

I’m reading the dictionary too trying to figure something out. It does not take the image in as either a byte string or as a url type string.

I’ve got my email typed for a feature request or three too. :)

1 Like

I guess the way that Copy As AppleScript encodes it is the main clue there. Base64 or something ?

Given that the AS seems to work, if the worst came to the worst it should still be possible to embed an AS line in the JS code with something like:

function evalOSA(strLang, strCode) {

    var strIdiom = strLang.toLowerCase()
        .indexOf('j') !== -1 ? (
            'JavaScript'
        ) : 'AppleScript',
        error = $(),
        oScript = (
            $.OSALanguage.setDefaultLanguage(
                $.OSALanguage.languageForName(strIdiom)
            ),
            $.OSAScript.alloc.initWithSource(strCode)
        ),
        blnCompiled = oScript.compileAndReturnError(error),

        oDesc = blnCompiled ? (
            oScript.executeAndReturnError(error)
        ) : undefined;

    return oDesc ? (

        oDesc.stringValue.js

    ) : error.js.NSLocalizedDescription.js;

}