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