Paste Markdown table (MMD) as editable OG 7.5 Test Table

Keyboard Maestro macro at: https://forum.keyboardmaestro.com/t/paste-markdown-table-mmd-as-omnigraffle-7-5-table-object/7829

and omniJS-executing Keyboard Maestro plugin at:

(Also pastes tab-delimited tables, using a default alignment)

Source code

Table-pasting in omniJS code

// Rob Trew (c) 2017

const mbTable = JSON.parse(kmvar.jsonMaybeTable);

// curry :: Function -> Function
const curry = (f, ...args) => {
    const go = xs => xs.length >= f.length ? (f.apply(null, xs)) :
        function () {
            return go(xs.concat(Array.from(arguments)));
        };
    return go([].slice.call(args, 1));
};

// quot :: Int -> Int -> Int
const quot = (n, m) => Math.floor(n / m);

// rem :: Int -> Int -> Int
const rem = (n, m) => m % n;

return mbTable.nothing ? (
    "No table found in clipboard"
) : (() => {
    const
        canvas = document.windows[0].selection.canvas,
        table = mbTable.just,
        intCols = table.columns,
        blnRuler = table.titleRow,
        lstAlign = table.alignments,
        isInRow = curry((iRow, i) => quot(i, intCols) === iRow),
        enumJust = [
            HorizontalTextAlignment.Left,
            HorizontalTextAlignment.Center,
            HorizontalTextAlignment.Right
        ],
        colAlign = i => enumJust[lstAlign[rem(intCols, i)] + 1],
        ogTable = Table.withRowsColumns(
            table.rows,
            table.columns,
            table.cells.map(
                (x, i) => {
                    return Object.assign(canvas.newShape(), {
                        autosizing: TextAutosizing.Full,
                        fontName: 'HelveticaNeue' + (
                            blnRuler && isInRow(0, i) ? '-Bold' : ''
                        ),
                        strokeColor: Color.RGB(0.5, 0.6, 0.75),
                        strokeThickness: 0.5,
                        text: x,
                        textHorizontalAlignment: colAlign(i),
                        textSize: 12
                    })
                })
        );
    return (
        table.cells.length + " cells in " +
        table.rows + " rows of " + table.columns + " columns."
    );
})();

Clipboard parsing in JavaScript for Automation - JXA:

((options) => {
    'use strict';

    // Rob Trew (c) 2017

        // Ver 0.02
        // - sheds leading or trailling blank lines in clipboard

        // Check for default table value justification ---------------------------
        // (See options record at foot of script)

        const eDefaultAlign = [-1, -0, 1].indexOf(options.defaultAlign) !== -1 ? (
            options.defaultAlign
        ) : 0; // Center [ or one of (-1|0|1) for (Left|Center|Right) ]

        // APPKIT IMPORTED FOR NSPasteboard --------------------------------------

        ObjC.import('AppKit');

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

        // curry :: Function -> Function
        const curry = (f, ...args) => {
            const go = xs => xs.length >= f.length ? (f.apply(null, xs)) :
                function () {
                    return go(xs.concat(Array.from(arguments)));
                };
            return go([].slice.call(args, 1));
        };

        // all :: (a -> Bool) -> [a] -> Bool
        const all = curry((f, xs) => xs.every(f));

        // (++) :: [a] -> [a] -> [a]
        const append = (xs, ys) => xs.concat(ys);

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

        // concat :: [[a]] -> [a] | [String] -> String
        const concat = xs =>
            xs.length > 0 ? (() => {
                const unit = typeof xs[0] === 'string' ? '' : [];
                return unit.concat.apply(unit, xs);
            })() : [];

        // cons :: a -> [a] -> [a]
        const cons = (x, xs) => [x].concat(xs);

        // dropWhile :: (a -> Bool) -> [a] -> [a]
        const dropWhile = curry((p, xs) => {
            let i = 0;
            for (let lng = xs.length;
                (i < lng) && p(xs[i]); i++) {}
            return xs.slice(i);
        });

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

        // filter :: (a -> Bool) -> [a] -> [a]
        const filter = (f, xs) => xs.filter(f);

        // head :: [a] -> a
        const head = xs => xs.length ? xs[0] : undefined;

        // isNull :: [a] | String -> Bool
        const isNull = xs =>
            Array.isArray(xs) || typeof xs === 'string' ? (
                xs.length < 1
            ) : undefined;

        // length :: [a] -> Int
        const length = xs => xs.length;

        // last :: [a] -> a
        const last = xs => xs.length ? xs.slice(-1)[0] : undefined;

        // lines :: String -> [String]
        const lines = s => s.split(/[\r\n]/);

        // log :: a -> IO ()
        const log = (...args) =>
            console.log(
                args
                .map(JSON.stringify)
                .join(' -> ')
            );

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

        // maximum :: [a] -> a
        const maximum = xs =>
            xs.reduce((a, x) => (x > a || a === undefined ? x : a), undefined);

        // min :: Ord a => a -> a -> a
        const min = (a, b) => b < a ? b : a;

        // not :: Bool -> Bool
        const not = b => !b;

        // replicate :: Int -> a -> [a]
        const replicate = (n, x) =>
            Array.from({
                length: n
            }, () => x);

        // reverse :: [a] -> [a]
        const reverse = xs =>
            typeof xs === 'string' ? (
                xs.split('')
                .reverse()
                .join('')
            ) : xs.slice(0)
            .reverse();

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

        // splitOn :: a -> [a] -> [[a]]
        // splitOn :: String -> String -> [String]
        const splitOn = curry((needle, haystack) =>
            typeof haystack === 'string' ? (
                haystack.split(needle)
            ) : (function sp_(ndl, hay) {
                const mbi = findIndex(x => ndl === x, hay);
                return mbi.nothing ? (
                    [hay]
                ) : append(
                    [take(mbi.just, hay)],
                    sp_(ndl, drop(mbi.just + 1, hay))
                );
            })(needle, haystack));

        // strip :: Text -> Text
        const strip = s => s.trim();

        // tail :: [a] -> [a]
        const tail = xs => xs.length ? xs.slice(1) : [];

        // take :: Int -> [a] -> [a]
        const take = (n, xs) => xs.slice(0, n);

        // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
        const zipWith = (f, xs, ys) =>
            Array.from({
                length: Math.min(xs.length, ys.length)
            }, (_, i) => f(xs[i], ys[i]));

        // UTF8, IF ANY, IN CLIPBOARD --------------------------------------------

        // textClipMay :: Maybe String
        const textClipMay = () => {
            const
                utf8 = "public.utf8-plain-text",
                pb = $.NSPasteboard.generalPasteboard;
            return ObjC.deepUnwrap(pb.pasteboardItems.js[0].types)
                .indexOf(utf8) !== -1 ? {
                    nothing: false,
                    just: ObjC.unwrap(
                        pb.stringForType(utf8)
                    )
                } : {
                    nothing: true
                };
        };

        // DIMENSIONS, CELL-ARRAY, ALIGNMENTS OF MMD OR TABBED TABLE  ------------

        // tableParse :: String -> {
        //      rows :: Int,  columns :: Int,
        //      alignments :: [(-1|0|1)],  cells :: [String] }
        const tableParse = s =>
            (isMMDTable(s) ? mmdTableParse : tabbedTableParse)(s);

        // Every non-empty line contains a pipe ?
        // isMMDTable :: String -> Bool
        const isMMDTable = s =>
            all(x => elem('|', x) || length(strip(x)) === 0, lines(s));

        // mmdTableParse :: String -> {
        //      rows :: Int,  columns :: Int,
        //      alignments :: [(-1|0|1)],  cells :: [String] }
        const mmdTableParse = s => {
            const
                rows = filter(x => !isNull(x), map(strip, lines(s))),
                rePrune = compose(reverse,
                    dropWhile(c => elem(c, [' ', '|', '\t']))
                ),
                cellRows = map(s => map(
                    strip,
                    splitOn('|', rePrune(rePrune(s)))
                ), rows),
                intRows = length(rows),
                intCols = maximum(map(length, cellRows)),
                isRuler = all(s => head(s) === ':' || last(s) === ':'),
                blnRuler = intRows > 1 && isRuler(cellRows[1]),
                lcr = s => head(s) === ':' ? (last(s) === ':' ? 0 : -1) : 1,
                defaultRuler = replicate(intCols, eDefaultAlign),
                xs = concat(blnRuler ? (
                    cons(head(cellRows), tail(tail(cellRows)))
                ) : cellRows),
                intCells = length(xs);
            return intCells > 0 && intCells <= (intCols * intRows) ? {
                just: {
                    rows: blnRuler ? intRows - 1 : intRows,
                    columns: intCols,
                    alignments: blnRuler ? (
                        take(intCols, append(map(lcr, cellRows[1]), defaultRuler))
                    ) : defaultRuler,
                    cells: xs,
                    titleRow: blnRuler
                }
            } : {
                nothing: true,
                msg: 'MMD table parse -> ' +
                    concat(zipWith(
                        (a, b) => a + ': ' + b.toString(), //
                        ['Columns', 'Rows', 'Cells'], //
                        [intCols, intRows, intCells]
                    ))
            };
        };

        // ROW-COL DIMENSIONS AND CELL-ARRAY OF *TABBED* TABLE  ------------------
        // tabbedTableParse :: String -> {
        //      rows :: Int,  columns :: Int,
        //      alignments :: [(-1|0|1)],  cells :: [String] }
        const tabbedTableParse = s => {
            const
                rows = filter(x => !isNull(x), map(strip, lines(s))),
                tableRows = map(splitOn('\t'), rows),
                intCols = maximum(map(length, tableRows)),
                intRows = length(tableRows),
                xs = map(strip, concat(tableRows)),
                intCells = length(xs);
            return intCells > 0 && intCells <= (intCols * intRows) ? {
                just: {
                    columns: intCols,
                    rows: intRows,
                    alignments: replicate(intCols, eDefaultAlign), // (-1|0|1)
                    cells: xs,
                    titleRow: false,
                }
            } : {
                nothing: true,
                msg: 'Tabbed table parse -> ' +
                    concat(zipWith(
                        (a, b) => a + ': ' + b.toString(), //
                        ['Columns', 'Rows', 'Cells'], //
                        [intCols, intRows, intCells]
                    ))
            };
        };

        // MAYBE PARSE OF ANY TABLE IN CLIPBOARD ---------------------------------
        const
            mbText = textClipMay(),
            mbTable = mbText.nothing ? mbText : (
                tableParse(mbText.just)
            );
        return JSON.stringify(mbTable);

    })({ // (Left | Center | Right): use (-1 | 0| 1)
        defaultAlign: 0
    });

1 Like