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
});