131 lines
3.7 KiB
JavaScript
131 lines
3.7 KiB
JavaScript
(() => {
|
|
'use strict';
|
|
|
|
// drawTree :: Bool -> Tree String -> String
|
|
const drawTree = blnCompact => tree => {
|
|
// Simple decorated-outline style of ascii tree drawing,
|
|
// with nodeless lines pruned out if blnCompact is True.
|
|
const xs = draw(tree);
|
|
return unlines(
|
|
blnCompact ? (
|
|
xs.filter(
|
|
s => s.split('')
|
|
.some(c => !' │'.includes(c))
|
|
)
|
|
) : xs
|
|
);
|
|
};
|
|
|
|
// draw :: Tree String -> [String]
|
|
const draw = node => {
|
|
// shift :: String -> String -> [String] -> [String]
|
|
const shift = (first, other, xs) =>
|
|
zipWith(
|
|
append,
|
|
cons(first, replicate(xs.length - 1, other)),
|
|
xs
|
|
);
|
|
// drawSubTrees :: [Tree String] -> [String]
|
|
const drawSubTrees = xs => {
|
|
const lng = xs.length;
|
|
return 0 < lng ? (
|
|
1 < lng ? append(
|
|
cons(
|
|
'│',
|
|
shift('├─ ', '│ ', draw(xs[0]))
|
|
),
|
|
drawSubTrees(xs.slice(1))
|
|
) : cons('│', shift('└─ ', ' ', draw(xs[0])))
|
|
) : [];
|
|
};
|
|
return append(
|
|
lines(node.root.toString()),
|
|
drawSubTrees(node.nest)
|
|
);
|
|
};
|
|
|
|
// TEST -----------------------------------------------
|
|
const main = () => {
|
|
const tree = Node(
|
|
'Alpha', [
|
|
Node('Beta', [
|
|
Node('Epsilon', []),
|
|
Node('Zeta', []),
|
|
Node('Eta', [])
|
|
]),
|
|
Node('Gamma', [Node('Theta', [])]),
|
|
Node('Delta', [
|
|
Node('Iota', []),
|
|
Node('Kappa', []),
|
|
Node('Lambda', [])
|
|
])
|
|
]);
|
|
|
|
return [true, false]
|
|
.map(blnCompact => drawTree(blnCompact)(tree))
|
|
.join('\n\n');
|
|
};
|
|
|
|
// GENERIC FUNCTIONS ----------------------------------
|
|
|
|
// Node :: a -> [Tree a] -> Tree a
|
|
const Node = (v, xs) => ({
|
|
type: 'Node',
|
|
root: v, // any type of value (consistent across tree)
|
|
nest: xs || []
|
|
});
|
|
|
|
// append (++) :: [a] -> [a] -> [a]
|
|
// append (++) :: String -> String -> String
|
|
const append = (xs, ys) => xs.concat(ys);
|
|
|
|
// chars :: String -> [Char]
|
|
const chars = s => s.split('');
|
|
|
|
// cons :: a -> [a] -> [a]
|
|
const cons = (x, xs) => [x].concat(xs);
|
|
|
|
// Returns Infinity over objects without finite length.
|
|
// This enables zip and zipWith to choose the shorter
|
|
// argument when one is non-finite, like cycle, repeat etc
|
|
|
|
// length :: [a] -> Int
|
|
const length = xs =>
|
|
(Array.isArray(xs) || 'string' === typeof xs) ? (
|
|
xs.length
|
|
) : Infinity;
|
|
|
|
// lines :: String -> [String]
|
|
const lines = s => s.split(/[\r\n]/);
|
|
|
|
// replicate :: Int -> a -> [a]
|
|
const replicate = (n, x) =>
|
|
Array.from({
|
|
length: n
|
|
}, () => x);
|
|
|
|
// take :: Int -> [a] -> [a]
|
|
const take = (n, xs) =>
|
|
xs.slice(0, n);
|
|
|
|
// unlines :: [String] -> String
|
|
const unlines = xs => xs.join('\n');
|
|
|
|
// Use of `take` and `length` here allows zipping with non-finite lists
|
|
// i.e. generators like cycle, repeat, iterate.
|
|
|
|
// zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
|
|
const zipWith = (f, xs, ys) => {
|
|
const
|
|
lng = Math.min(length(xs), length(ys)),
|
|
as = take(lng, xs),
|
|
bs = take(lng, ys);
|
|
return Array.from({
|
|
length: lng
|
|
}, (_, i) => f(as[i], bs[i], i));
|
|
};
|
|
|
|
// MAIN ---
|
|
return main();
|
|
})();
|