omniJS – COPY selected OG 7.5 table as MultiMarkdown table

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

1 Like