Why might we want to copy an existing OmniGraffle table as MultiMarkdown ?
Apart from capturing data for use elsewhere, it can also be useful in combination with a companion (Paste from MMD to OG 7.5 Table) macro for deriving a consistently styled new table from an existing rough table.
Keyboard Maestro macro (omniJS and JXA source code below) at:
omniJS :: (OG 7.5 Table -> MultiMarkdown table text)
// GENERIC FUNCTIONS ----------------------------------------
// (++) :: [a] -> [a] -> [a]
const append = (xs, ys) => xs.concat(ys);
// Size of space -> filler Char -> Text -> Centered Text
// center :: Int -> Char -> Text -> Text
const center = (n, c, s) => {
const [q, r] = quotRem(n - s.length, 2);
return concat(concat([replicate(q, c), s, replicate(q + r, c)]));
};
// 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);
})() : [];
// enumFromTo :: Int -> Int -> [Int]
const enumFromTo = (m, n) =>
Array.from({
length: Math.floor(n - m) + 1
}, (_, i) => m + i);
// head :: [a] -> a
const head = xs => xs.length ? xs[0] : undefined;
// intercalate :: String -> [a] -> String
const intercalate = (s, xs) => xs.join(s);
// isNull :: [a] | String -> Bool
const isNull = xs =>
Array.isArray(xs) || typeof xs === 'string' ? (
xs.length < 1
) : undefined;
// justifyLeft :: Int -> Char -> Text -> Text
const justifyLeft = (n, cFiller, strText) =>
n > strText.length ? (
(strText + cFiller.repeat(n))
.substr(0, n)
) : strText;
// justifyRight :: Int -> Char -> Text -> Text
const justifyRight = (n, cFiller, strText) =>
n > strText.length ? (
(cFiller.repeat(n) + strText)
.slice(-n)
) : strText;
// length :: [a] -> Int
const length = xs => xs.length;
// 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);
// max :: Ord a => a -> a -> a
const max = (a, b) => b > a ? b : a;
// maximumByMay :: (a -> a -> Ordering) -> [a] -> Maybe a
const maximumByMay = (f, xs) =>
xs.length > 0 ? {
just: xs.slice(1)
.reduce((a, x) => f(x, a) > 0 ? x : a, xs[0])
} : {
nothing: true
};
// quotRem :: Integral a => a -> a -> (a, a)
const quotRem = (m, n) => [Math.floor(m / n), m % n];
// replicate :: Int -> a -> [a]
const replicate = (n, x) =>
Array.from({
length: n
}, () => x);
// tail :: [a] -> [a]
const tail = xs => xs.length ? xs.slice(1) : undefined;
// transpose :: [[a]] -> [[a]]
const transpose = xs =>
xs[0].map((_, col) => xs.map(row => row[col]));
// unlines :: [String] -> String
const unlines = xs => xs.join('\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]));
// MULTIMARKDOWN TABLES -----------------------------------
// alignFunction :: (-1|0|1) -> (Int -> Char -> Text -> Text)
const alignFunction = eAlign =>
eAlign === -1 ? justifyLeft : (eAlign === 1 ? justifyRight : center);
// mmdTableFromRulerAndRows ::
// [LeftCenterRight (-1|0|1)] -> [[String]] -> String
const mmdTableFromRulerAndRows = (alignments, rows) =>
isNull(rows) ? '' : (() => {
const // Width of each column in characters (minimum 3)
widths = map(
cells => {
const mbMax = maximumByMay(comparing(length), cells);
return mbMax.nothing ? 3 : max(3, length(mbMax.just));
},
transpose(rows) // i.e. columns
),
// Array of MMD ruler strings, one for each column
rulerCells = zipWith((lcr, w) =>
(lcr !== 1 ? ':' : '-') +
concat(replicate(max(1, w - 2), '-')) +
(lcr !== -1 ? ':' : '-'),
alignments, widths),
// Pretty-printed text cells (space-padded left/right/center)
dataCells = map(row => map((cell, iCol) =>
alignFunction(alignments[iCol])(
widths[iCol], ' ', cell
), row), rows);
// All piped together as MMD lines.
return unlines(
map(
xs => '|' + intercalate('|', xs) + '|',
append([head(dataCells), rulerCells], tail(dataCells))
)
);
})();
// OMNIGRAFFLE 7.5 TABLES -----------------------------------
// ogTableRow :: OG Table -> Int -> [Maybe Graphic]
const ogTableRow = (oTable, iRow) =>
map(iCol => {
const mb = oTable.graphicAt(iRow, iCol);
return mb !== null ? {
just: mb
} : {
nothing: true
};
}, enumFromTo(0, oTable.columns - 1));
// mmdFromTableMay :: OG Graphic -> Maybe String
const mmdFromTableMay = g =>
g.constructor.name !== 'Table' ? {
nothing: true
} : (() => {
const [l, r] = map(
k => HorizontalTextAlignment[k], //
['Left', 'Right'] // If neither: Center
);
return {
just: mmdTableFromRulerAndRows(
// Ruler enum values, from first row cell alignments,
map(
mbCell => mbCell.nothing ? 0 : (() => {
const ta = mbCell.just.textHorizontalAlignment;
return ta === l ? -1 : (ta === r ? 1 : 0);
})(),
ogTableRow(g, 0)
),
// and rows of cells as arrays of strings.
map(
iRow => map(
mbCell => mbCell.nothing ? '' : mbCell.just.text,
ogTableRow(g, iRow)
),
enumFromTo(0, g.rows - 1)
)
)
};
})();
// MAIN ----------------------------------------------------------
const
gs = document.windows[0].selection.graphics,
mbg = gs.length > 0 ? {
just: gs[0]
} : {
nothing: true
},
mbTable = mbg.nothing ? mbg : mmdFromTableMay(mbg.just);
return mbTable.nothing ? '' : mbTable.just;
JXA :: (JSON string -> MMD string -> Updated clipboard)
(() => {
'use strict';
// readMaybe :: String -> {Nothing:Boolean, Just:a, msg: String}
const readMaybe = s => {
try {
return {
just: JSON.parse(s),
nothing: false
}
} catch (e) {
return {
nothing: true,
msg: ['line', 'column', 'message']
.map(k => k + ':' + e[k])
.join(' ')
};
}
};
const mbTable = readMaybe(
Application('Keyboard Maestro Engine')
.getvariable('JSONClip')
);
return mbTable.nothing ? (
mbTable
) : (() => {
const
a = Application.currentApplication(),
sa = (a.includeStandardAdditions = true, a);
return (
sa.setTheClipboardTo(mbTable.just),
mbTable.just
);
})();
})();