Different output when running "Copy As > Javascript" multiple times


#1

Hi,

I’m having trouble finding out why this happens:

  1. Select items in canvas
  2. Select “Copy As > Javascript”: This will yield a certain output
    (Selection stays the same)
  3. Select “Copy As > Javascript” again, it will yield a different output
  4. Select “Copy As > Javascript” again, it will yield a different output

Why is this happening?


#2

Show always works better than tell – what is the difference ?

If you show us by pasting two samples here between ‘code fence’ triple backticks, they should be legible:

```
paste differing outputs here ...
```

#3

PS impossible to know without seeing the output you referring to, but I wonder whether the proximity of AppleScript and JavaScript in the Copy As menu just led to inadvertent clicks of two different languages ?


#4

Thanks for the replies! Basically I have a rectangle with content “1” connected, using an arrow, to a rectangle with content “2”. I selected all items (or did cmd-A) then hit “Copy As > Javascript”…

What I’m seeing is the ordering of the output is different each time… In my case, order is important since I am parsing this JavaScript output.

1st output:

// Floating point values in this script may be rounded, resulting in minor visual differences from the original
var canvas = document.windows[0].selection.canvas;
var g1 = canvas.newShape();
var g2 = canvas.newShape();
var g3 = canvas.newLine();
g1.automationAction = [];
g1.textVerticalPlacement = VerticalTextPlacement.Middle;
g1.strokePattern = StrokeDash.Solid;
g1.plasticCurve = null;
g1.strokeJoin = LineJoin.Round;
g1.notes = "";
g1.blendFraction = 0;
g1.plasticHighlightAngle = null;
g1.textRotationIsRelative = true;
g1.gradientColor = Color.RGB(0.20000000298023224, 0.20000000298023224, 0.20000000298023224);
g1.shape = "Rectangle";
g1.blendColor = null;
g1.shadowVector = new Point(0.00, 2.00);
g1.gradientAngle = 90;
g1.strokeCap = LineCap.Round;
g1.geometry = new Rect(83.50, 236.00, 193.50, 58.50);
g1.fontName = "HelveticaNeue";
g1.image = null;
g1.textRotation = 0;
g1.name = null;
g1.textHorizontalPadding = 5;
g1.autosizing = TextAutosizing.Overflow;
g1.imageSizing = ImageSizing.Manual;
g1.locked = false;
g1.flippedHorizontally = false;
g1.flippedVertically = false;
g1.actionURL = null;
g1.imagePage = 0;
g1.textUnitRect = new Rect(0.00, 0.00, 1.00, 1.00);
g1.shadowColor = null;
g1.rotation = 0;
g1.userData = {};
g1.gradientCenter = new Point(0.00, 0.00);
g1.imageOpacity = 0;
g1.textColor = Color.black;
g1.shadowFuzziness = 3;
g1.allowsConnections = true;
g1.text = "1";
g1.fillType = FillType.Solid;
g1.tripleBlend = false;
g1.imageOffset = new Point(0.00, 0.00);
g1.textAlongPathGlyphAnchor = 0;
g1.fillColor = Color.RGB(0.0, 0.5, 1.0, 0.90221);
g1.cornerRadius = 2;
g1.imageScale = new Size(0.00, 0.00);
g1.textFlow = TextFlow.Overflow;
g1.textHorizontalAlignment = HorizontalTextAlignment.Center;
g1.strokeThickness = 2;
g1.textWraps = true;
g1.textSize = 16;
g1.strokeColor = Color.RGB(0.0, 0.0, 0.0);
g1.textVerticalPadding = 0;
g1.strokeType = StrokeType.Single;
...

2nd output:

// Floating point values in this script may be rounded, resulting in minor visual differences from the original
var canvas = document.windows[0].selection.canvas;
var g1 = canvas.newShape();
var g2 = canvas.newShape();
var g3 = canvas.newLine();
g1.strokePattern = StrokeDash.Solid;
g1.magnets = [];
g1.strokeThickness = 2;
g1.textHorizontalAlignment = HorizontalTextAlignment.Center;
g1.textVerticalPadding = 0;
g1.imagePage = 0;
g1.name = null;
g1.flippedVertically = false;
g1.image = null;
g1.imageOffset = new Point(0.00, 0.00);
g1.textUnitRect = new Rect(0.00, 0.00, 1.00, 1.00);
g1.textAlongPathGlyphAnchor = 0;
g1.shadowVector = new Point(0.00, 2.00);
g1.plasticHighlightAngle = null;
g1.fontName = "HelveticaNeue";
g1.flippedHorizontally = false;
g1.actionURL = null;
g1.notes = "";
g1.imageSizing = ImageSizing.Manual;
g1.textRotationIsRelative = true;
g1.textVerticalPlacement = VerticalTextPlacement.Middle;
g1.text = "1";
g1.textHorizontalPadding = 5;
g1.locked = false;
g1.strokeColor = Color.RGB(0.0, 0.0, 0.0);
g1.autosizing = TextAutosizing.Overflow;
g1.alignsEdgesToGrid = true;
g1.geometry = new Rect(83.50, 236.00, 193.50, 58.50);
g1.tripleBlend = false;
g1.fillColor = Color.RGB(0.0, 0.5, 1.0, 0.90221);
g1.userData = {};
g1.textRotation = 0;
g1.gradientColor = Color.RGB(0.20000000298023224, 0.20000000298023224, 0.20000000298023224);
g1.shadowFuzziness = 3;
g1.blendFraction = 0;
g1.automationAction = [];
g1.rotation = 0;
g1.plasticCurve = null;
g1.strokeCap = LineCap.Round;
g1.allowsConnections = true;
g1.textFlow = TextFlow.Overflow;
g1.gradientCenter = new Point(0.00, 0.00);
g1.shadowColor = null;
g1.textWraps = true;
g1.strokeType = StrokeType.Single;
g1.imageOpacity = 0;
g1.gradientAngle = 90;
g1.imageScale = new Size(0.00, 0.00);
g1.fillType = FillType.Solid;
g1.strokeJoin = LineJoin.Round;
g1.textSize = 16;
g1.shape = "Rectangle";
g1.cornerRadius = 2;
g1.textColor = Color.black;
g1.blendColor = null;
...

#5

A question for @support I think.

Not sure what the context of the parsing is, but you will get a better-structured model directly from the com.omnigroup.OmniGraffle.GraphicType content of NSPasteboard.generalPasteboard.

A Keyboard Maestro macro and its JS source:
Copy from OmniGraffle 7 as GraphicType JSON.kmmacros.zip (10.5 KB)

JS Source
(() => {
    'use strict';

    ObjC.import('AppKit');

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

    // elem :: Eq a => a -> [a] -> Bool
    const elem = (x, xs) => xs.includes(x);

    // isLeft :: Either a b -> Bool
    const isLeft = lr =>
        lr.type === 'Either' && lr.Left !== undefined;

    // String copied to general pasteboard
    // copyText :: String -> IO Bool
    const copyText = s => {
        const pb = $.NSPasteboard.generalPasteboard;
        return (
            pb.clearContents,
            pb.setStringForType(
                $(s),
                $.NSPasteboardTypeString
            )
        );
    };

    // MAIN ------------------------------------------------------------------
    const
        ogType = 'com.omnigroup.OmniGraffle.GraphicType',
        pBoard = $.NSPasteboard.generalPasteboard;

    const lrResult = bindLR(
        elem(ogType,
            ObjC.deepUnwrap(pBoard.pasteboardItems.js[0].types)
        ) ? Right(
            JSON.stringify(
                ObjC.deepUnwrap(
                    pBoard.propertyListForType(ogType)
                ),
                null, 2
            )
        ) : Left('OG7 graphics not found on clipboard'),
        strJSON => (
            copyText(strJSON),
            Right('Clipboard now contains JSON-encoded OG7 graphics')
        )
    );

    return lrResult.Right || lrResult.Left
})();


#6

Or as something:

  • to run stand-alone (without Keyboard Maestro),
  • after copying some OG graphics with plain ⌘C

Converts the OG7 graphic clipboard contents to a JSON format which you can paste into a text editor.

(() => {
    'use strict';

    ObjC.import('AppKit');

    // MAIN ---------------------------------------------------
    const main = () => {
        const
            ogType = 'com.omnigroup.OmniGraffle.GraphicType',
            pBoard = $.NSPasteboard.generalPasteboard;

        return either(alert('Empty clipboard'))(
            alert('OG7 clipboard converted to JSON')
        )(
            bindLR(
                elem(ogType)(
                    ObjC.deepUnwrap(pBoard.pasteboardItems.js[0].types)
                ) ? Right(
                    JSON.stringify(
                        ObjC.deepUnwrap(
                            pBoard.propertyListForType(ogType)
                        ),
                        null, 2
                    )
                ) : Left('OG7 graphics not found on clipboard'))(
                strJSON => (
                    copyText(strJSON),
                    Right('Clipboard now contains JSON-encoded OG7 graphics')
                )
            ));
    };

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

    // alert :: String -> String -> IO String
    const alert = title => s => {
        const
            sa = Object.assign(Application('System Events'), {
                includeStandardAdditions: true
            });
        return (
            sa.activate(),
            sa.displayDialog(s, {
                withTitle: title,
                buttons: ['OK'],
                defaultButton: 'OK'
            }),
            s
        );
    };

    // String copied to general pasteboard
    // copyText :: String -> IO Bool
    const copyText = s => {
        const pb = $.NSPasteboard.generalPasteboard;
        return (
            pb.clearContents,
            pb.setStringForType(
                $(s),
                $.NSPasteboardTypeString
            )
        );
    };

    // GENERIC FUNCTIONS ----------------------------------
    // https://github.com/RobTrew/prelude-jxa

    // 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 =>
        undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl => fr => e =>
        'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

    // elem :: Eq a => a -> [a] -> Bool
    // elem :: Char -> String -> Bool
    const elem = x =>
        xs => Array.isArray(xs) ? (
            xs.some(eq(x))
        ) : xs.includes(x);

    // eq (==) :: Eq a => a -> a -> Bool
    const eq = a => b => {
        const t = typeof a;
        return t !== typeof b ? (
            false
        ) : 'object' !== t ? (
            'function' !== t ? (
                a === b
            ) : a.toString() === b.toString()
        ) : (() => {
            const kvs = Object.entries(a);
            return kvs.length !== Object.keys(b).length ? (
                false
            ) : kvs.every(([k, v]) => eq(v)(b[k]));
        })();
    };

    return main();
})();