A first draft of something I needed this week for working with (plain or) nested MMD tables, in which a parent cell may span several child cells.
Note that this differs from Fletcher's Format > Clean up > Selected Table(s) in MultiMarkdown composer.
Where Fletcher's Clean up normalises his deliberately extreme and eccentric sample table at http://fletcher.github.io/MultiMarkdown-5/tables.html
from:
| | Grouping ||
First Header | Second Header | Third Header |
------------ | :-----------: | -----------: |
Content | *Long Cell* ||
Content | **Cell** | Cell |
New section | More | Data |
And more | With an escaped '\|' ||
[Prototype table]
to a non-spanning table with elastic tabs:
| | Grouping | |
First Header | Second Header | Third Header |
------------ | :-----------: | -----------: |
Content | *Long Cell* | |
Content | **Cell** | Cell |
New section | More | Data |
And more | With an escaped 'MMD-ESCAPED-PIPE' | |
[Prototype table]
This tidying macro:
- Preserves any parent cell spanning of multiple child cells
- Ignores any claim of a child cell to have two mothers (e.g. the Long Cell in Fletcher's example)
- Assumes monospaced fonts, and uses space characters instead of tabs
- Leaves escaped pipe characters in place, rather than replacing them with 'MMD-ESCAPED-PIPE'
Returning:
| | Grouping |||
| First Header | Second Header || Third Header |
|:------------:|:--------------------:|--------------:|:------------:|
| Content | *Long Cell* ||
| Content | **Cell** | Cell|
| New section | More | Data|
| And more | With an escaped '\|' |
[Prototype table]
More generally, it aims to provide the (monospaced font) tidying that you would expect for any MMD table which is either:
- A simple grid, with no spanning cells,
- A nested structure in which parent cells may have several child cells, but no child cells have multiple or ambiguous parents.
(Test with dummy data to make sure that this is the form of tidying that you want).
Tidy MMD table (simple or nested).kmmacros (38.6 KB)
JavaScript source
(() => {
'use strict';
// Tidy a plain or nested (spanning) MultiMarkdown table
// MultiMarkdown table in the clipboard
// Rob Trew (c) 2018
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// Ver 0.4
// main :: () -> IO String
const main = () => {
const
sa = standardAdditions(),
rows = lines(sa.theClipboard()),
ixs = nonTableIndices(rows);
const lr = bindLR(
bindLR(
rulerAndTreeFromMMDTableLR(
foldl(
(a, x, i) => ixs.includes(i) ? (
a
) : a + x + '\n',
'',
rows
)
),
tplRulerTree => Right(
uncurry(mmdTableFromTree)(tplRulerTree)
)
),
strTidy => {
const
xs = lines(strTidy),
strClip = unlines(
mapAccumL((a, x, i) =>
ixs.includes(i) ? (
Tuple(a, x)
) : Tuple(a + 1, xs[a]),
0, rows
)[1]
);
return (
sa.setTheClipboardTo(strClip),
Right(strClip)
);
});
return isLeft(lr) ? (
JSON.stringify(lr)
) : lr.Right;
};
// GENERIC FUNCTIONS ------------------------------------
// Just :: a -> Just a
const Just = x => ({
type: 'Maybe',
Nothing: false,
Just: x
});
// Left :: a -> Either a b
const Left = x => ({
type: 'Either',
Left: x
});
// Node :: a -> [Tree a] -> Tree a
const Node = (v, xs) => ({
type: 'Node',
root: v, // any type of value (but must be consistent across tree)
nest: xs || []
});
// Nothing :: () -> Nothing
const Nothing = () => ({
type: 'Maybe',
Nothing: true,
});
// Right :: b -> Either a b
const Right = x => ({
type: 'Either',
Right: x
});
// Tuple (,) :: a -> b -> (a, b)
const Tuple = (a, b) => ({
type: 'Tuple',
'0': a,
'1': b,
length: 2
});
// Determines whether all elements of the structure satisfy the predicate.
// all :: (a -> Bool) -> [a] -> Bool
const all = (p, xs) => xs.every(p);
// bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
const bindLR = (m, mf) =>
m.Right !== undefined ? (
mf(m.Right)
) : m;
// bindMay (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
const bindMay = (mb, mf) =>
mb.Nothing ? mb : mf(mb.Just);
// 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 replicateString(q, c) +
s + replicateString(q + qr[1], c);
};
// 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));
// 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();
};
// flip :: (a -> b -> c) -> b -> a -> c
const flip = f => (a, b) => f.apply(null, [b, a]);
// foldl :: (a -> b -> a) -> a -> [b] -> a
const foldl = (f, a, xs) => xs.reduce(f, a);
// foldl1 :: (a -> a -> a) -> [a] -> a
const foldl1 = (f, xs) =>
xs.length > 1 ? xs.slice(1)
.reduce(f, xs[0]) : xs[0];
// Note that that the Haskell signature of foldr is different from that of
// foldl - the positions of accumulator and current value are reversed
// foldr :: (a -> b -> b) -> b -> [a] -> b
const foldr = (f, a, xs) => xs.reduceRight(flip(f), a);
// fst :: (a, b) -> a
const fst = tpl => tpl.type !== 'Tuple' ? undefined : tpl[0];
// init :: [a] -> [a]
const init = xs => xs.length > 0 ? xs.slice(0, -1) : undefined;
// isLeft :: Either a b -> Bool
const isLeft = lr =>
lr.type === 'Either' && lr.Left !== undefined;
// iterateUntil :: (a -> Bool) -> (a -> a) -> a -> [a]
const iterateUntil = (p, f, x) => {
const go = x => p(x) ? x : [x].concat(go(f(x)));
return go(x);
};
// 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;
// last :: [a] -> a
const last = xs => xs.length ? xs.slice(-1)[0] : undefined;
// levelNodes :: Tree a -> [[Tree a]]
const levelNodes = tree =>
iterateUntil(
xs => xs.length < 1,
xs => concatMap(x => x.nest, xs), [tree]
);
// lines :: String -> [String]
const lines = s => s.split(/[\r\n]/);
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) => xs.map(f);
// '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, []));
// max :: Ord a => a -> a -> a
const max = (a, b) => b > a ? b : a;
// maximum :: Ord a => [a] -> a
const maximum = xs =>
xs.length > 0 ? (
foldl1((a, x) => x > a ? x : a, xs)
) : undefined;
// not :: Bool -> Bool
const not = b => !b;
// quotRem :: Int -> Int -> (Int, Int)
const quotRem = (m, n) => Tuple(Math.floor(m / n), m % n);
// regexMatches :: String -> String -> [[String]]
const regexMatches = (strRgx, strHay) => {
const rgx = new RegExp(strRgx, 'g');
let m = rgx.exec(strHay),
xs = [];
while (m)(xs.push(m), m = rgx.exec(strHay));
return xs;
};
// replicate :: Int -> a -> [a]
const replicate = (n, x) =>
Array.from({
length: n
}, () => x);
// replicateString :: Int -> String -> String
const replicateString = (n, s) => s.repeat(n);
// showLog :: a -> IO ()
const showLog = (...args) =>
console.log(
args
.map(JSON.stringify)
.join(' -> ')
);
// snd :: (a, b) -> b
const snd = tpl => tpl.type !== 'Tuple' ? undefined : tpl[1];
// 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 (lng >= n ? (
xs
) : concat(replicate(Math.ceil(n / lng), xs)))
.slice(0, n)
};
// Converts a function of more than one argument
// to a function on Tuple type (Tuple ... TupleN)
// or array which contains those arguments.
// This implementation uses the fact that the Tuple
// constructors create an object with a private .length property
// uncurry :: (a -> b -> c) -> ((a, b) -> c)
const uncurry = f => args => f.apply(null, args);
// unlines :: [String] -> String
const unlines = xs => xs.join('\n');
// until :: (a -> Bool) -> (a -> a) -> a -> a
const until = (p, f, x) => {
let v = x;
while (!p(v)) v = f(v);
return v;
};
// JXA --------------------------------------------------
// standardAdditions :: () -> Application
const standardAdditions = () =>
Object.assign(Application.currentApplication(), {
includeStandardAdditions: true
});
// MMD --------------------------------------------------
// Write MMD --------------------------------------------
// alignFn :: Ordering -> (String -> String)
const alignFn = o => ([
justifyLeft, center, justifyRight
][1 + o]);
// Maximum unit text width in 'column' above each leaf
// (dimensions for a MultiMarkdown ruler)
// treeColWidths :: Tree -> [Int]
const treeColWidths = oNode => {
const go = intMax => node => {
const
root = node.root,
width = 2 + (Boolean(root) ? (
root.text.length
) : 0),
nest = node.nest,
intPeers = nest.length;
return intPeers > 0 ? (
concatMap(
go(max(intMax / intPeers, Math.ceil(width / intPeers))),
nest
)
) : [max(intMax, width)];
};
const root = oNode.root;
return go(2 + (Boolean(root) ? (
root.text.length / max(1, oNode.nest)
) : 0))(oNode);
};
// (Tree layers -> Decorated Tree layers )
// withColIndices :: [[Node]] -> [[Node]]
const withColIndices = treeLevels =>
map(
xs => {
const tpl = mapAccumL((iCol, dctNode) => {
const
root = dctNode.root,
blnRoot = Boolean(root);
return Tuple(
iCol + (blnRoot ? root.leafWidth : 1),
Object.assign(dctNode, {
root: Object.assign(blnRoot ? root : {}, {
col: iCol
})
})
);
}, 0, xs);
return tpl[1];
},
treeLevels
);
// ( Tree to decorated Tree )
// withLeafWidths :: Tree -> Tree
const withLeafWidths = node => {
const
nest = node.nest,
sub = Object.assign(
node, {
nest: nest.map(withLeafWidths)
}
),
root = node.root;
return Boolean(root) ? Object.assign(
sub, {
root: {
text: (typeof root === 'string' ? (
root
) : root.text).replace(/&%/g, '\\|'),
leafWidth: nest.length > 0 ? (
sub.nest.reduce((a, x) => a + x.root.leafWidth, 0)
) : 1
}
}
) : sub;
};
// MAIN TREE -> MMD WRITER
// Ruler Tuple: (
// Index of ruler row
// sequence of Orderings (-1, 0, 1) = (Left, Centre, Right)
// )
// RulerTuple -> Tree -> String
// mmdTableFromTree :: (Int, [Ordering]) -> Tree -> String
const mmdTableFromTree = (tplRuler, oTree) => {
const
tree = withLeafWidths(oTree),
colWidths = treeColWidths(tree),
leafWidth = colWidths.length;
const
iRuler = tplRuler[0],
alignments = tplRuler[1],
lng = alignments.length,
rules = lng >= leafWidth ? (
alignments.slice(0, leafWidth)
) : takeCycle(leafWidth, (
lng > 0 ? (
alignments
) : [0]));
const rows = map(
cells => '|' + map(
cell => {
const
root = cell.root,
iCol = root.col,
intLeaves = root.leafWidth;
return (
alignFn(rules[iCol])(
colWidths.slice(iCol, iCol + intLeaves)
.reduce((a, x) => a + x, 0),
' ',
(root.text || '')
)
) + replicateString(max(1, intLeaves), '|');
},
cells
).join(''),
withColIndices(levelNodes(tree))
);
// MMD Table string
const iTopRow = rows[0].includes(' ') ? 0 : 1;
// Initial rows ---------------------------------------
return rows.slice(iTopRow, iRuler + iTopRow).concat(
// Ruler row ----------------------------------------
'|' + map(
(x, i) => (x <= 0 ? ':' : '-') +
replicateString(colWidths[i] - 2, '-') +
(x >= 0 ? ':' : '-'),
rules
).join('|') + '|'
).concat(
// Remaining rows -----------------------------------
rows.slice(iTopRow + iRuler)
).join('\n');
};
// READ MMD ---------------------------------------------
// isTable :: [[String]] -> Bool
const isTable = xs =>
xs.length > 1 && all(x => x.length > 0, xs);
// nonTableIndices :: [String] -> [(Int, String)]
const nonTableIndices = rows =>
foldl(
(a, x, i) => x.includes('|') ? (
a
) : a.concat(i), [], rows
);
// isRuler :: String -> Bool
const isRuler = s => /^[\|\-\:\s]+$/.test(s);
// [-1:Left, 0:Center, 1:Right]
// mmdTableAlignment :: String -> Int
const mmdTableAlignment = s => {
const
l = s[0] === ':',
r = s[s.length - 1] === ':';
return l === r ? (
0
) : r ? (
1
) : -1;
};
// As many of the nodes (from the left) as are required
// to reach a target leaf sum.
// Node {leafWidth :: Int}
// takeLeaves :: Int -> [Node] -> [Node]
const takeLeaves = (n, xs) => {
const lng = xs.length;
return take(until(
x => x.total >= n || x.index >= lng,
x => ({
total: x.total + xs[x.index].root.leafWidth,
index: x.index + 1
}), {
total: 0,
index: 0
}
).index, xs);
};
// rulerAndTreeFromMMDTableLR :: String ->
// Either String ([Ordering], Tree)
const rulerAndTreeFromMMDTableLR = s => {
const
rgxEscPipe = new RegExp('\\\\\\|', 'g'),
rows = filter(
x => x.length > 0 && x.includes('|'),
map(x => x.trim(), lines(s.replace(rgxEscPipe, '&%')))
);
return isTable(rows) ? (() => {
const
mbiRuler = findIndex(isRuler, rows),
cellRows = map(
x => map(
ms => Node({
text: ms[1].trim(),
leafWidth: max(1, ms[2].length)
}),
regexMatches(/([^|]+)(\|*)/, x)
),
rows
);
const tplAlignmentsRows = mbiRuler.Nothing ? Tuple(
replicate(
maximum(map(
cells => foldl((a, x) => a + x[1], 0, cells),
cellRows
)),
0
),
cellRows
) : bindMay(
mbiRuler,
iRuler => Tuple(
map(
mmdTableAlignment,
filter(
x => x.length > 0,
rows[iRuler].split(/\s*\|\s*/g)
)
),
cellRows.slice(0, iRuler)
.concat(cellRows.slice(iRuler + 1)),
)
);
// forest :: [Node]
const forest = foldr(
(xCurrent, aBelow) => {
const intBelow = aBelow.length;
return intBelow > 0 ? (() => {
const tpl = mapAccumL(
(iFrom, nodeAbove, i) => iFrom >= intBelow ? Tuple(
iFrom, nodeAbove
) : (() => {
const
xs = takeLeaves(
nodeAbove.root.leafWidth,
aBelow.slice(iFrom)
);
return Tuple(
iFrom + xs.length,
Object.assign(nodeAbove, {
nest: xs
})
);
})(),
0,
init(xCurrent)
);
// All remaining children gathered by rightmost parent
return snd(tpl).concat(Object.assign(last(xCurrent), {
nest: aBelow.slice(fst(tpl))
}));
})() : xCurrent;
}, [],
tplAlignmentsRows[1]
),
intNodes = forest.length;
return intNodes > 0 ? Right(
Tuple(
Tuple(mbiRuler.Just, tplAlignmentsRows[0]),
intNodes > 1 ? (
Node(undefined, forest)
) : forest[0]
)
) : Left('Could not be parsed as an MMD table');
})() : Left('Too sparse to parse as an MMD table.')
};
// MAIN -------------------------------------------------
return main();
})();