Paste a MultiMarkdown table in transposed and tidied form

Here is a plain text (MMD) analogue of Excel's Transpose table feature.

If you copy a plain text MultiMarkdown table, you can use this macro to paste a transposed and tidied copy of it. (See examples below).

Paste a MultiMarkdown table in transposed and tidied form.kmmacros (29.5 KB)

Copy an MMD table like this:

|  | Constants || Accumulations |||| Node properties ||||
|--|:--|--|:--|--|--|--|:--|:--:|--|--|
| Scope | k | k | a | a | a | a | x | x | x | x |
| JSType | Int | Int | String | Int | Int | Int | Int | String | String | String |
|  |  |  |  |  |  |  |  |  |  |  |
| Name | topHash | hashLevels | enclosingStyle | enclosingHIndent | enclosingHashN | prevLevel | absLevel | style | tp3Type | prefix |
| Code |  |  | TRUE |  |  | TRUE |  | TRUE |  | TRUE |
| Quote |  |  | TRUE |  |  |  |  | TRUE |  | TRUE |
| Hash | TRUE | TRUE | TRUE | TRUE | TRUE |  | TRUE |  | TRUE |  |
| Numbered |  |  | TRUE | TRUE |  |  |  |  |  | TRUE |
| Body |  |  | TRUE | TRUE |  |  | TRUE |  |  |  |
| Bullet |  |  | TRUE | TRUE |  |  | TRUE |  |  |  |

and use the macro to paste a (row ⇄ cols) transposed and tidied copy:

|                 | Scope | JSType | Name             | Code | Quote | Hash | Numbered | Body | Bullet |
| --------------- | :---- | ------ | :--------------- | ---- | ----- | ---- | :------- | :--: | ------ |
| Constants       | k     | Int    | topHash          |      |       | TRUE |          |      |        |
|                 | k     | Int    | hashLevels       |      |       | TRUE |          |      |        |
| Accumulations   | a     | String | enclosingStyle   | TRUE | TRUE  | TRUE | TRUE     | TRUE | TRUE   |
|                 | a     | Int    | enclosingHIndent |      |       | TRUE | TRUE     | TRUE | TRUE   |
|                 | a     | Int    | enclosingHashN   |      |       | TRUE |          |      |        |
|                 | a     | Int    | prevLevel        | TRUE |       |      |          |      |        |
| Node properties | x     | Int    | absLevel         |      |       | TRUE |          | TRUE | TRUE   |
|                 | x     | String | style            | TRUE | TRUE  |      |          |      |        |
|                 | x     | String | tp3Type          |      |       | TRUE |          |      |        |
|                 | x     | String | prefix           | TRUE | TRUE  |      | TRUE     |      |        |

or a second time for a second transposition and tidy:

|          | Constants |            | Accumulations  |                  |                |           | Node properties |        |         |        |
| -------- | :-------- | ---------- | :------------- | ---------------- | -------------- | --------- | :-------------- | :----: | ------- | ------ |
| Scope    | k         | k          | a              | a                | a              | a         | x               |   x    | x       | x      |
| JSType   | Int       | Int        | String         | Int              | Int            | Int       | Int             | String | String  | String |
| Name     | topHash   | hashLevels | enclosingStyle | enclosingHIndent | enclosingHashN | prevLevel | absLevel        | style  | tp3Type | prefix |
| Code     |           |            | TRUE           |                  |                | TRUE      |                 |  TRUE  |         | TRUE   |
| Quote    |           |            | TRUE           |                  |                |           |                 |  TRUE  |         | TRUE   |
| Hash     | TRUE      | TRUE       | TRUE           | TRUE             | TRUE           |           | TRUE            |        | TRUE    |        |
| Numbered |           |            | TRUE           | TRUE             |                |           |                 |        |         | TRUE   |
| Body     |           |            | TRUE           | TRUE             |                |           | TRUE            |        |         |        |
| Bullet   |           |            | TRUE           | TRUE             |                |           | TRUE            |        |         |        |

Github-rendered in Marked 2 :

JavaScript for Automation source code:

(() => {
    'use strict';

    // main :: () -> IO String
    function main() {
        const strClip = standardAdditions()
            .theClipboard();
        return (typeof strClip === 'string') && strClip.includes('|') ? (
            transposedMDTable(strClip)
        ) : '';
    }

    // MARKDOWN TABLE TRANSPOSED ---------------------------------------------

    // transposedMDTable :: String -> String
    const transposedMDTable = strTable => {

        // [0:Left, 1:Center, 2:Right]
        // mmdTableAlignment :: String -> Int
        const mmdTableAlignment = s => {
            const
                l = s[0] === ':',
                r = s[s.length - 1] === ':';
            return l && r ? (
                1
            ) : r ? (
                2
            ) : 0;
        };

        // alignFn :: String -> String
        const alignFn = s => [
            justifyLeft, center, justifyRight
        ][mmdTableAlignment(s)];

        const
            rows = concatMap(
                row => {
                    const cells = map(strip, splitOn('|', row));
                    return any(x => length(x) > 0, cells) ? (
                        [cells]
                    ) : [];
                },
                lines(strTable)
            ),
            colWidths = concatMap(
                (r, i) => i !== 1 ? ( // Except ruler line
                    [length(maximumBy(comparing(length), r))]
                ) : [],
                rows
            ),
            flippedRows = filter(
                xs => any(x => length(x) > 0, xs),
                transpose(cons(rows[0], rows.slice(2)))
            ),
            // Ruler length adapted to transposed column count,
            // and ruler cells expanded to fit column width.
            ruler = map((x, i) => x[0] +
                replicateString(colWidths[i] - 2, '-') +
                x[x.length - 1],
                takeCycle(
                    flippedRows[0].length,
                    filter(
                        (x, i) => length(x) > 0,
                        rows[1] // Ruler before transposition
                    )
                )
            ),
            format = map(alignFn, ruler);

        // titleLine :: [Int] -> [String] -> String
        const titleLine = (widths, titles) => {
            const tpl = mapAccumL(
                (a, x, i) => x.length > 0 ? (
                    Tuple(0, replicateString(a, '|') + '| ' +
                        format[i](widths[i], ' ', x) + ' ')
                ) : Tuple(a + 1, replicateString(widths[i] + 2, ' ')), -1,
                titles
            );
            return '|' + concat(tpl[1]) +
                replicateString(tpl[0] + 1, '|');
        };

        return unlines(
            [titleLine(colWidths, flippedRows[0]),
                ...map(row => '| ' + intercalate(
                        ' | ',
                        map((col, i) => format[i](colWidths[i], ' ', col), row)
                    ) + ' |',
                    // Reuse the existing ruler pattern, adapting its length.
                    append([ruler], flippedRows.slice(1))
                )
            ]
        );
    };

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

    // Just :: a -> Just a
    const Just = x => ({
        type: 'Maybe',
        Nothing: false,
        Just: x
    });

    // Nothing :: () -> Nothing
    const Nothing = () => ({
        type: 'Maybe',
        Nothing: true,
    });

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = (a, b) => ({
        type: 'Tuple',
        '0': a,
        '1': b
    });

    // | True if any contained element satisfies the predicate.
    // any :: (a -> Bool) -> [a] -> Bool
    const any = (p, xs) => xs.some(p);

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

    // Size of space -> filler Char -> String -> Centered String
    // center :: Int -> Char -> String -> String
    const center = (n, c, s) => {
        const
            qr = quotRem(n - s.length, 2),
            q = qr[0];
        return concat(concat([replicate(q, c), s, replicate(q + qr[1], 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]
    // concat :: [String] -> String
    const concat = xs =>
        xs.length > 0 ? (() => {
            const unit = typeof xs[0] === 'string' ? '' : [];
            return unit.concat.apply(unit, xs);
        })() : [];

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

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

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

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

    // findIndex :: (a -> Bool) -> [a] -> Maybe Int
    const findIndex = (p, xs) => {
        const i = xs.findIndex(p);
        return i !== -1 ? (
            Just(i)
        ) : Nothing();
    };

    // intercalate :: [a] -> [[a]] -> [a]
    // intercalate :: String -> [String] -> String
    const intercalate = (sep, xs) =>
        xs.length > 0 && typeof sep === 'string' &&
        typeof xs[0] === 'string' ? (
            xs.join(sep)
        ) : concat(intersperse(sep, xs));

    // justifyLeft :: Int -> Char -> String -> String
    const justifyLeft = (n, cFiller, strText) =>
        n > strText.length ? (
            (strText + cFiller.repeat(n))
            .substr(0, n)
        ) : strText;

    // justifyRight :: Int -> Char -> String -> String
    const justifyRight = (n, cFiller, strText) =>
        n > strText.length ? (
            (cFiller.repeat(n) + strText)
            .slice(-n)
        ) : strText;

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

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

    // 'The mapAccumL function behaves like a combination of map and foldl;
    // it applies a function to each element of a list, passing an accumulating
    // parameter from left to right, and returning a final value of this
    // accumulator together with the new list.' (See Hoogle)
    // mapAccumL :: (acc -> x -> (acc, y)) -> acc -> [x] -> (acc, [y])
    const mapAccumL = (f, acc, xs) =>
        xs.reduce((a, x, i) => {
            const pair = f(a[0], x, i);
            return Tuple(pair[0], a[1].concat(pair[1]));
        }, Tuple(acc, []));

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

    //  Ordering: (LT|EQ|GT):
    //  GT: 1 (or other positive n)
    //    EQ: 0
    //  LT: -1 (or other negative n)
    // maximumBy :: (a -> a -> Ordering) -> [a] -> a
    const maximumBy = (f, xs) =>
        xs.length > 0 ? (
            xs.slice(1)
            .reduce((a, x) => f(x, a) > 0 ? x : a, xs[0])
        ) : undefined;

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

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

    // replicateString :: Int -> String -> String
    const replicateString = (n, s) =>
        ''.concat.apply('', Array.from({
            length: n
        }, () => s));

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

    // splitOn :: String -> String -> [String]
    // splitOn :: a -> [a] -> [[a]]
    const splitOn = (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 :: String -> String
    const strip = s => s.trim();

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

    // First n members of an infinite cycle of xs
    // takeCycle :: Int -> [a] -> [a]
    const takeCycle = (n, xs) => {
        const lng = xs.length;
        return take(n,
            (lng >= n ? xs : concat(replicate(Math.ceil(n / lng), xs)))
        );
    };

    // If some of the rows are shorter than the following rows,
    // their elements are skipped:
    // > transpose [[10,11],[20],[],[30,31,32]] == [[10,20,30],[11,31],[32]]
    // transpose :: [[a]] -> [[a]]
    // transpose :: [[a]] -> [[a]]
    const transpose = tbl => {
        const
            gaps = replicate(
                length(maximumBy(comparing(length), tbl)), []
            ),
            rows = map(xs => xs.concat(gaps.slice(xs.length)), tbl);
        return rows[0].map((_, col) => concatMap(row => [row[col]], rows));
    };

    // unlines :: [String] -> String
    const unlines = xs => xs.join('\n');

    // JXA --------------------------------------------------------------------

    // standardAdditions :: () -> Application
    const standardAdditions = () =>
        Object.assign(Application.currentApplication(), {
            includeStandardAdditions: true
        });

    // MAIN ------------------------------------------------------------------
    return main();
})();
1 Like

Updated code and macro above to restore MMD spanning headers, when using the macro repeatedly to toggle the transposition of rows ⇄ columns back and forth, e.g. between:

|                 | Scope | JSType | Name             | Code | Quote | Hash | Numbered | Body | Bullet |
| --------------- | :---- | ------ | :--------------- | ---- | ----- | ---- | :------- | :--: | ------ |
| Constants       | k     | Int    | topHash          |      |       | TRUE |          |      |        |
|                 | k     | Int    | hashLevels       |      |       | TRUE |          |      |        |
| Accumulations   | a     | String | enclosingStyle   | TRUE | TRUE  | TRUE | TRUE     | TRUE | TRUE   |
|                 | a     | Int    | enclosingHIndent |      |       | TRUE | TRUE     | TRUE | TRUE   |
|                 | a     | Int    | enclosingHashN   |      |       | TRUE |          |      |        |
|                 | a     | Int    | prevLevel        | TRUE |       |      |          |      |        |
| Node properties | x     | Int    | absLevel         |      |       | TRUE |          | TRUE | TRUE   |
|                 | x     | String | style            | TRUE | TRUE  |      |          |      |        |
|                 | x     | String | tp3Type          |      |       | TRUE |          |      |        |
|                 | x     | String | prefix           | TRUE | TRUE  |      | TRUE     |      |        |

and


|          | Constants             || Accumulations                                               |||| Node properties                          ||||
| -------- | :-------- | ---------- | :------------- | ---------------- | -------------- | --------- | :-------------- | :----: | ------- | ------ |
| Scope    | k         | k          | a              | a                | a              | a         | x               |   x    | x       | x      |
| JSType   | Int       | Int        | String         | Int              | Int            | Int       | Int             | String | String  | String |
| Name     | topHash   | hashLevels | enclosingStyle | enclosingHIndent | enclosingHashN | prevLevel | absLevel        | style  | tp3Type | prefix |
| Code     |           |            | TRUE           |                  |                | TRUE      |                 |  TRUE  |         | TRUE   |
| Quote    |           |            | TRUE           |                  |                |           |                 |  TRUE  |         | TRUE   |
| Hash     | TRUE      | TRUE       | TRUE           | TRUE             | TRUE           |           | TRUE            |        | TRUE    |        |
| Numbered |           |            | TRUE           | TRUE             |                |           |                 |        |         | TRUE   |
| Body     |           |            | TRUE           | TRUE             |                |           | TRUE            |        |         |        |
| Bullet   |           |            | TRUE           | TRUE             |                |           | TRUE            |        |         |        |

Hi there - this macro looks great. I’m interested in using it for tidying MMD tables but not transposing. Before I dive into the logic might you be able to give me a pointer on which section to remove so that the table is not transposed please?

Thanks,
Gordon

On vacation with iOS only here, but from memory, the first thing I would try is just to set up a KM Macro which fires the JS action twice in succession.

(It toggles between transpositions, and is fairly fast, so you may find that a fast transpose then untranspose gives you something usable until I am back after the holiday weekend, and can remind myself of the code details.

Otherwise (and I can’t test this from here) it is possible that if you:

  1. and an additional id function (definition below) to the code,
  2. find the point where the transpose function is used, and keep its arguments, but use the function name id in lieu of transpose

… things might still work, tidying without transposition.

id function

const id = x => x;

Replacing transpose with id:

Edit the definition of flippedRows from:

flippedRows = filter(
     xs => any(x => length(x) > 0, xs),
     transpose(cons(rows[0], rows.slice(2)))
),

to

flippedRows = filter(
     xs => any(x => length(x) > 0, xs),
     id(cons(rows[0], rows.slice(2)))
),

or just (removing a pair of parentheses)

flippedRows = filter(
     xs => any(x => length(x) > 0, xs),
     cons(rows[0], rows.slice(2))
),

If this did work, then it might make sense to search and replace flippedRows -> unFlippedRows (or some other new name which is not used elsewhere in the code)

Thanks for this - this largely works but adds an extra empty column before and after the set of columns that were defined in the original text

Might be worth trying this 'tidy-only' macro for nested or simple MultiMarkdown tables:

Thank you - works a treat :slight_smile: