Pasting JSON as a tree diagram

A first draft of an omniJS plugin for pasting arbitrary JSON from from clipboard as a tree diagram in OmniGraffle.

pasteJSONTree.omnijs.zip (2.5 KB)

JS Source
/* eslint-disable no-console */
/* eslint-disable no-undef */
/* eslint-disable no-return-assign */
/* eslint-disable spaced-comment */
/*{
    "author": "Rob Trew",
    "targets": ["omnigraffle"],
    "type": "action",
    "identifier": "com.robtrew.pastejsontree",
    "version": "0.1",
    "description": "JSON pasted as OmniGraffle tree",
    "label": "Paste JSON as tree diagram",
    "mediumLabel": "JSON Tree",
    "paletteLabel": "JSON Tree",
}*/
(() => {
    "use strict";

    // main :: () -> Plugin
    const main = () => Object.assign(
        new PlugIn.Action(
            selection => either(
                msg => (
                    new Alert(
                        "Paste JSON as tree",
                        `No JSON in clipboard ?\n\n${msg}`
                    )
                    .show()
                )
            )(
                x => console.log(JSON.stringify(x))
            )(
                bindLR(
                    jsonParseLR(
                        Pasteboard.general.string || ""
                    )
                )(
                    jso => {
                        const
                            tree = jsoKeyTree(jso),
                            canvas = newNamedTopCanvas(
                                "JSON diagram"
                            );

                        return (
                            // In OmniFocus,
                            selection.view.canvas = canvas,
                            drawOGTree(canvas)(tree),
                            canvas.layout(),

                            // and in JavaScript.
                            Right(tree)
                        );
                    }
                )
            )
        ), {
            validate: () => true
        });

    // ---------- LINKED TEXT TO LINKED SHAPES -----------

    // newNamedTopCanvas :: String -> IO Canvas
    const newNamedTopCanvas = name => {
        // A new front canvas, with the given name.
        const canvas = globalThis.addCanvas();

        return (
            canvas.name = name,
            canvas.orderBefore(globalThis.canvases[0]),
            canvas
        );
    };

    // drawOGTree :: OG Canvas -> Tree a -> OG Shape
    const drawOGTree = canvas =>
        foldTree(x => xs =>
            xs.reduce(
                (a, shape) => (canvas.connect(a, shape), a),
                (
                    newShape => (
                        newShape.text = [
                            "[ ]", "{ }"
                        ].includes(x) ? (
                            x
                        ) : JSON.stringify(x),
                        newShape
                    )
                )(
                    canvas.addShape(
                        0 < xs.length ? (
                            "Circle"
                        ) : "Rectangle",
                        new Rect(30, 30, 100, 100)
                    )
                )
            )
        );


    // ----------------- JS OBJECT TREE ------------------

    // jsoKeyTree :: a -> Tree b
    const jsoKeyTree = jsValue => {
        const go = v =>
            isAtom(v) ? (
                Node(v)([])
            ) : (
                Node(
                    Array.isArray(v) ? "[ ]" : "{ }"
                )(
                    Object.keys(v).map(
                        k => Node(k)(
                            (() => {
                                const subValue = v[k];

                                return isAtom(subValue) ? (
                                    [Node(subValue)([])]
                                ) : [go(subValue)];
                            })()
                        )
                    )
                )
            );

        return go(jsValue);
    };


    // isAtom :: a -> Bool
    const isAtom = x =>
        // True if x is an atomic value:
        // Boolean, Number, String, or null.
        ("object" !== typeof x) || (null === x);


    // --------------------- GENERIC ---------------------

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


    // Node :: a -> [Tree a] -> Tree a
    const Node = v =>
        // Constructor for a Tree node which connects a
        // value of some kind to a list of zero or
        // more child trees.
        xs => ({
            type: "Node",
            root: v,
            nest: xs || []
        });


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


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => e.Left ? (
            fl(e.Left)
        ) : fr(e.Right);


    // foldTree :: (a -> [b] -> b) -> Tree a -> b
    const foldTree = f => {
        // The catamorphism on trees. A summary
        // value obtained by a depth-first fold.
        const go = tree => f(
            root(tree)
        )(
            nest(tree).map(go)
        );

        return go;
    };


    // jsonParseLR :: String -> Either String a
    const jsonParseLR = s => {
        // Either a message, or a JS value obtained
        // from a successful parse of s.
        try {
            return Right(JSON.parse(s));
        } catch (e) {
            return Left(
                `${e.message} (line:${e.line} col:${e.column})`
            );
        }
    };


    // nest :: Tree a -> [a]
    const nest = tree => {
        // Allowing for lazy (on-demand) evaluation.
        // If the nest turns out to be a function –
        // rather than a list – that function is applied
        // here to the root, and returns a list.
        const xs = tree.nest;

        return "function" !== typeof xs ? (
            xs
        ) : xs(root(x));
    };

    // root :: Tree a -> a
    const root = tree =>
        // The value attached to a tree node.
        tree.root;

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

If the clipboard contains an arbitrarily nested JSON string like:

{
    "various": [
        1,
        2,
        {
            "alpha": "Aberdeen",
            "beta": "Glasgow",
            "gamma": "Edinburgh"
        }
    ],
    "other": null
}

then this plugin will create a new tree diagram, on a fresh canvas in the active document.

3 Likes

Did not work right out the box; got “TypeError: null is not an object (evaluating ‘selection.view.canvas = canvas’)”. on line 47

Delete that line and all is well.

Yes, that line assumes that there is a current selection in OmniGraffle.

I can see that that might not always be the case. Thanks for the pointer.

1 Like

A very educational and stimulating plugin. Thank you very much!

1 Like