An experiment in using omniJS, JXA and Keyboard Maestro to paste from a range of copied Excel cells (or any other tab-delimited utf8 lines in the clipboard) to an editable and formattable OmniGraffle 7.5 Table object.
From (Excel):
To (OG 7.5 test)
Keyboard Maestro (and a required plugin for using omniJS) at:
Code in omniJS plugin action (json -> Table)
const mbTable = JSON.parse(kmvar.jsonMaybeTable);
return mbTable.nothing ? (
"No table found in clipboard"
) : (() => {
const
canvas = document.windows[0].selection.canvas,
table = mbTable.just;
return (
Table.withRowsColumns(
table.rows,
table.columns,
table.values.map(
x => Object.assign(canvas.newShape(), {
textSize: 12,
text: x,
geometry: new Rect(0, 0, 50, 20),
strokeThickness: 0.5,
strokeColor : Color.RGB(0.5, 0.6, 0.75)
})
)
),
table.values.length + " cells in " +
table.rows + " rows of " + table.columns + " columns."
);
})();
Code in JXA action (clipboard -> maybe json)
(() => {
'use strict';
// APPKIT IMPORTED FOR NSPasteboard --------------------------------------
ObjC.import('AppKit');
// GENERIC FUNCTIONS -----------------------------------------------------
// comparing :: (a -> b) -> (a -> a -> Ordering)
const comparing = f =>
(x, y) => {
const
a = f(x),
b = f(y);
return a < b ? -1 : (a > b ? 1 : 0);
};
// concat :: [[a]] -> [a] | [String] -> String
const concat = xs =>
xs.length > 0 ? (() => {
const unit = typeof xs[0] === 'string' ? '' : [];
return unit.concat.apply(unit, xs);
})() : [];
// 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));
};
// length :: [a] -> Int
const length = xs => xs.length;
// 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);
// maximumBy :: (a -> a -> Ordering) -> [a] -> a
const maximumBy = (f, xs) =>
xs.reduce((a, x) => a === undefined ? x : (
f(x, a) > 0 ? x : a
), undefined);
// 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));
// 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
};
};
// ROW-COL DIMENSIONS AND CELL-ARRAY OF TABLE IN CLIPBOARD -----------
const
mbText = textClipMay(),
mbTable = mbText.nothing ? mbText : (() => {
const clipRows = map(splitOn('\t'), lines(mbText.just));
return {
just: {
columns: length(
maximumBy(comparing(length), clipRows)
),
rows: length(clipRows),
values: concat(clipRows)
},
nothing: false
};
})();
return JSON.stringify(mbTable);
})();