547 lines
14 KiB
JavaScript
547 lines
14 KiB
JavaScript
|
|
(() => {
|
|||
|
|
"use strict";
|
|||
|
|
|
|||
|
|
// ----------- NESTED TABLES FROM OUTLINE ------------
|
|||
|
|
|
|||
|
|
// wikiTablesFromOutline :: [String] -> String -> String
|
|||
|
|
const wikiTablesFromOutline = colorSwatch =>
|
|||
|
|
outline => forestFromIndentedLines(
|
|||
|
|
indentLevelsFromLines(lines(outline))
|
|||
|
|
)
|
|||
|
|
.map(wikiTableFromTree(colorSwatch))
|
|||
|
|
.join("\n\n");
|
|||
|
|
|
|||
|
|
|
|||
|
|
// wikiTableFromTree :: [String] -> Tree String -> String
|
|||
|
|
const wikiTableFromTree = colorSwatch =>
|
|||
|
|
compose(
|
|||
|
|
wikiTableFromRows,
|
|||
|
|
levels,
|
|||
|
|
paintedTree(colorSwatch),
|
|||
|
|
widthLabelledTree,
|
|||
|
|
ap(paddedTree(""))(treeDepth)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// ---------------------- TEST -----------------------
|
|||
|
|
// main :: IO ()
|
|||
|
|
const main = () => {
|
|||
|
|
const outline = `Display an outline as a nested table.
|
|||
|
|
Parse the outline to a tree,
|
|||
|
|
measuring the indent of each line,
|
|||
|
|
translating the indentation to a nested structure,
|
|||
|
|
and padding the tree to even depth.
|
|||
|
|
count the leaves descending from each node,
|
|||
|
|
defining the width of a leaf as 1,
|
|||
|
|
and the width of a parent node as a sum.
|
|||
|
|
(The sum of the widths of its children)
|
|||
|
|
and write out a table with 'colspan' values
|
|||
|
|
either as a wiki table,
|
|||
|
|
or as HTML.`;
|
|||
|
|
|
|||
|
|
return wikiTablesFromOutline([
|
|||
|
|
"#ffffe6",
|
|||
|
|
"#ffebd2",
|
|||
|
|
"#f0fff0",
|
|||
|
|
"#e6ffff",
|
|||
|
|
"#ffeeff"
|
|||
|
|
])(outline);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// --------- TREE STRUCTURE FROM NESTED TEXT ---------
|
|||
|
|
|
|||
|
|
// forestFromIndentedLines :: [(Int, String)] ->
|
|||
|
|
// [Tree String]
|
|||
|
|
const forestFromIndentedLines = tuples => {
|
|||
|
|
const go = xs =>
|
|||
|
|
0 < xs.length ? (() => {
|
|||
|
|
// First line and its sub-tree,
|
|||
|
|
const [indented, body] = Array.from(
|
|||
|
|
xs[0]
|
|||
|
|
),
|
|||
|
|
[tree, rest] = Array.from(
|
|||
|
|
span(compose(lt(indented), fst))(
|
|||
|
|
tail(xs)
|
|||
|
|
)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// followed by the rest.
|
|||
|
|
return [
|
|||
|
|
Node(body)(go(tree))
|
|||
|
|
].concat(go(rest));
|
|||
|
|
})() : [];
|
|||
|
|
|
|||
|
|
return go(tuples);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
// indentLevelsFromLines :: [String] -> [(Int, String)]
|
|||
|
|
const indentLevelsFromLines = xs => {
|
|||
|
|
const
|
|||
|
|
pairs = xs.map(
|
|||
|
|
x => bimap(length)(cs => cs.join(""))(
|
|||
|
|
span(isSpace)(list(x))
|
|||
|
|
)
|
|||
|
|
),
|
|||
|
|
indentUnit = pairs.reduce(
|
|||
|
|
(a, tpl) => {
|
|||
|
|
const i = tpl[0];
|
|||
|
|
|
|||
|
|
return 0 < i ? (
|
|||
|
|
i < a ? i : a
|
|||
|
|
) : a;
|
|||
|
|
},
|
|||
|
|
Infinity
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return [Infinity, 0].includes(indentUnit) ? (
|
|||
|
|
pairs
|
|||
|
|
) : pairs.map(first(n => n / indentUnit));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ------------ TREE PADDED TO EVEN DEPTH ------------
|
|||
|
|
|
|||
|
|
// paddedTree :: a -> Tree a -> Int -> Tree a
|
|||
|
|
const paddedTree = padValue =>
|
|||
|
|
// All descendants expanded to same depth
|
|||
|
|
// with empty nodes where needed.
|
|||
|
|
node => depth => {
|
|||
|
|
const go = n => tree =>
|
|||
|
|
1 < n ? (() => {
|
|||
|
|
const children = nest(tree);
|
|||
|
|
|
|||
|
|
return Node(root(tree))(
|
|||
|
|
(
|
|||
|
|
0 < children.length ? (
|
|||
|
|
children
|
|||
|
|
) : [Node(padValue)([])]
|
|||
|
|
).map(go(n - 1))
|
|||
|
|
);
|
|||
|
|
})() : tree;
|
|||
|
|
|
|||
|
|
return go(depth)(node);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// treeDepth :: Tree a -> Int
|
|||
|
|
const treeDepth = tree =>
|
|||
|
|
foldTree(
|
|||
|
|
() => xs => 0 < xs.length ? (
|
|||
|
|
1 + maximum(xs)
|
|||
|
|
) : 1
|
|||
|
|
)(tree);
|
|||
|
|
|
|||
|
|
// ------------- SUBTREE WIDTHS MEASURED -------------
|
|||
|
|
|
|||
|
|
// widthLabelledTree :: Tree a -> Tree (a, Int)
|
|||
|
|
const widthLabelledTree = tree =>
|
|||
|
|
// A tree in which each node is labelled with
|
|||
|
|
// the width of its own subtree.
|
|||
|
|
foldTree(x => xs =>
|
|||
|
|
0 < xs.length ? (
|
|||
|
|
Node(Tuple(x)(
|
|||
|
|
xs.reduce(
|
|||
|
|
(a, node) => a + snd(root(node)),
|
|||
|
|
0
|
|||
|
|
)
|
|||
|
|
))(xs)
|
|||
|
|
) : Node(Tuple(x)(1))([])
|
|||
|
|
)(tree);
|
|||
|
|
|
|||
|
|
// -------------- COLOR SWATCH APPLIED ---------------
|
|||
|
|
|
|||
|
|
// paintedTree :: [String] -> Tree a -> Tree (String, a)
|
|||
|
|
const paintedTree = colorSwatch =>
|
|||
|
|
tree => 0 < colorSwatch.length ? (
|
|||
|
|
Node(
|
|||
|
|
Tuple(colorSwatch[0])(root(tree))
|
|||
|
|
)(
|
|||
|
|
zipWith(compose(fmapTree, Tuple))(
|
|||
|
|
cycle(colorSwatch.slice(1))
|
|||
|
|
)(
|
|||
|
|
nest(tree)
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
) : fmapTree(Tuple(""))(tree);
|
|||
|
|
|
|||
|
|
// --------------- WIKITABLE RENDERED ----------------
|
|||
|
|
|
|||
|
|
// wikiTableFromRows ::
|
|||
|
|
// [[(String, (String, Int))]] -> String
|
|||
|
|
const wikiTableFromRows = rows => {
|
|||
|
|
const
|
|||
|
|
cw = color => width => {
|
|||
|
|
const go = w =>
|
|||
|
|
1 < w ? (
|
|||
|
|
`colspan=${w} `
|
|||
|
|
) : "";
|
|||
|
|
|
|||
|
|
return `style="background:${color}; "` + (
|
|||
|
|
` ${go(width)}`
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
cellText = ctw => {
|
|||
|
|
const [color, tw] = Array.from(ctw);
|
|||
|
|
const [txt, width] = Array.from(tw);
|
|||
|
|
|
|||
|
|
return 0 < txt.length ? (
|
|||
|
|
`| ${cw(color)(width)}| ${txt}`
|
|||
|
|
) : "| |";
|
|||
|
|
},
|
|||
|
|
classText = "class=\"wikitable\"",
|
|||
|
|
styleText = "style=\"text-align:center;\"",
|
|||
|
|
header = `{| ${classText} ${styleText}\n|-`,
|
|||
|
|
tableBody = rows.map(
|
|||
|
|
cells => cells.map(cellText).join("\n")
|
|||
|
|
).join("\n|-\n");
|
|||
|
|
|
|||
|
|
return `${header}\n${tableBody}\n|}`;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ------------------ GENERIC TREES ------------------
|
|||
|
|
|
|||
|
|
// Node :: a -> [Tree a] -> Tree a
|
|||
|
|
const Node = v =>
|
|||
|
|
// Constructor for a Tree node which connects a
|
|||
|
|
// value of some kind to a list of zero or
|
|||
|
|
// more child trees.
|
|||
|
|
xs => ({
|
|||
|
|
type: "Node",
|
|||
|
|
root: v,
|
|||
|
|
nest: xs || []
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
|
|||
|
|
// fmapTree :: (a -> b) -> Tree a -> Tree b
|
|||
|
|
const fmapTree = f => {
|
|||
|
|
// A new tree. The result of a
|
|||
|
|
// structure-preserving application of f
|
|||
|
|
// to each root in the existing tree.
|
|||
|
|
const go = t => Node(
|
|||
|
|
f(t.root)
|
|||
|
|
)(
|
|||
|
|
t.nest.map(go)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return go;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
// foldTree :: (a -> [b] -> b) -> Tree a -> b
|
|||
|
|
const foldTree = f => {
|
|||
|
|
// The catamorphism on trees. A summary
|
|||
|
|
// value obtained by a depth-first fold.
|
|||
|
|
const go = tree => f(
|
|||
|
|
root(tree)
|
|||
|
|
)(
|
|||
|
|
nest(tree).map(go)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return go;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
// levels :: Tree a -> [[a]]
|
|||
|
|
const levels = tree => {
|
|||
|
|
// A list of lists, grouping the root
|
|||
|
|
// values of each level of the tree.
|
|||
|
|
const go = (a, node) => {
|
|||
|
|
const [h, ...t] = 0 < a.length ? (
|
|||
|
|
a
|
|||
|
|
) : [
|
|||
|
|
[],
|
|||
|
|
[]
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return [
|
|||
|
|
[node.root, ...h],
|
|||
|
|
...node.nest.slice(0)
|
|||
|
|
.reverse()
|
|||
|
|
.reduce(go, t)
|
|||
|
|
];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return go([], tree);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
// nest :: Tree a -> [a]
|
|||
|
|
const nest = tree => {
|
|||
|
|
// Allowing for lazy (on-demand) evaluation.
|
|||
|
|
// If the nest turns out to be a function –
|
|||
|
|
// rather than a list – that function is applied
|
|||
|
|
// here to the root, and returns a list.
|
|||
|
|
const xs = tree.nest;
|
|||
|
|
|
|||
|
|
return "function" !== typeof xs ? (
|
|||
|
|
xs
|
|||
|
|
) : xs(root(tree));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
// root :: Tree a -> a
|
|||
|
|
const root = tree =>
|
|||
|
|
// The value attached to a tree node.
|
|||
|
|
tree.root;
|
|||
|
|
|
|||
|
|
// --------------------- GENERIC ---------------------
|
|||
|
|
|
|||
|
|
// Just :: a -> Maybe a
|
|||
|
|
const Just = x => ({
|
|||
|
|
type: "Maybe",
|
|||
|
|
Nothing: false,
|
|||
|
|
Just: x
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
|
|||
|
|
// Nothing :: Maybe a
|
|||
|
|
const Nothing = () => ({
|
|||
|
|
type: "Maybe",
|
|||
|
|
Nothing: true
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
|
|||
|
|
// Tuple (,) :: a -> b -> (a, b)
|
|||
|
|
const Tuple = a =>
|
|||
|
|
b => ({
|
|||
|
|
type: "Tuple",
|
|||
|
|
"0": a,
|
|||
|
|
"1": b,
|
|||
|
|
length: 2
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
|
|||
|
|
// apFn :: (a -> b -> c) -> (a -> b) -> (a -> c)
|
|||
|
|
const ap = f =>
|
|||
|
|
// Applicative instance for functions.
|
|||
|
|
// f(x) applied to g(x).
|
|||
|
|
g => x => f(x)(
|
|||
|
|
g(x)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
|
|||
|
|
// bimap :: (a -> b) -> (c -> d) -> (a, c) -> (b, d)
|
|||
|
|
const bimap = f =>
|
|||
|
|
// Tuple instance of bimap.
|
|||
|
|
// A tuple of the application of f and g to the
|
|||
|
|
// first and second values respectively.
|
|||
|
|
g => tpl => Tuple(f(tpl[0]))(
|
|||
|
|
g(tpl[1])
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
|
|||
|
|
// compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
|
|||
|
|
const compose = (...fs) =>
|
|||
|
|
// A function defined by the right-to-left
|
|||
|
|
// composition of all the functions in fs.
|
|||
|
|
fs.reduce(
|
|||
|
|
(f, g) => x => f(g(x)),
|
|||
|
|
x => x
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
|
|||
|
|
// cycle :: [a] -> Generator [a]
|
|||
|
|
const cycle = function* (xs) {
|
|||
|
|
// An infinite repetition of xs,
|
|||
|
|
// from which an arbitrary prefix
|
|||
|
|
// may be taken.
|
|||
|
|
const lng = xs.length;
|
|||
|
|
let i = 0;
|
|||
|
|
|
|||
|
|
while (true) {
|
|||
|
|
yield xs[i];
|
|||
|
|
i = (1 + i) % lng;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
// first :: (a -> b) -> ((a, c) -> (b, c))
|
|||
|
|
const first = f =>
|
|||
|
|
// A simple function lifted to one which applies
|
|||
|
|
// to a tuple, transforming only its first item.
|
|||
|
|
xy => {
|
|||
|
|
const tpl = Tuple(f(xy[0]))(xy[1]);
|
|||
|
|
|
|||
|
|
return Array.isArray(xy) ? (
|
|||
|
|
Array.from(tpl)
|
|||
|
|
) : tpl;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
// fst :: (a, b) -> a
|
|||
|
|
const fst = tpl =>
|
|||
|
|
// First member of a pair.
|
|||
|
|
tpl[0];
|
|||
|
|
|
|||
|
|
|
|||
|
|
// isSpace :: Char -> Bool
|
|||
|
|
const isSpace = c =>
|
|||
|
|
// True if c is a white space character.
|
|||
|
|
(/\s/u).test(c);
|
|||
|
|
|
|||
|
|
|
|||
|
|
// length :: [a] -> Int
|
|||
|
|
const length = 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
|
|||
|
|
"GeneratorFunction" !== xs.constructor
|
|||
|
|
.constructor.name ? (
|
|||
|
|
xs.length
|
|||
|
|
) : Infinity;
|
|||
|
|
|
|||
|
|
|
|||
|
|
// lines :: String -> [String]
|
|||
|
|
const lines = s =>
|
|||
|
|
// A list of strings derived from a single
|
|||
|
|
// string delimited by newline and or CR.
|
|||
|
|
0 < s.length ? (
|
|||
|
|
s.split(/[\r\n]+/u)
|
|||
|
|
) : [];
|
|||
|
|
|
|||
|
|
|
|||
|
|
// list :: StringOrArrayLike b => b -> [a]
|
|||
|
|
const list = xs =>
|
|||
|
|
// xs itself, if it is an Array,
|
|||
|
|
// or an Array derived from xs.
|
|||
|
|
Array.isArray(xs) ? (
|
|||
|
|
xs
|
|||
|
|
) : Array.from(xs || []);
|
|||
|
|
|
|||
|
|
|
|||
|
|
// lt (<) :: Ord a => a -> a -> Bool
|
|||
|
|
const lt = a =>
|
|||
|
|
b => a < b;
|
|||
|
|
|
|||
|
|
|
|||
|
|
// maximum :: Ord a => [a] -> a
|
|||
|
|
const maximum = xs => (
|
|||
|
|
// The largest value in a non-empty list.
|
|||
|
|
ys => 0 < ys.length ? (
|
|||
|
|
ys.slice(1).reduce(
|
|||
|
|
(a, y) => y > a ? (
|
|||
|
|
y
|
|||
|
|
) : a, ys[0]
|
|||
|
|
)
|
|||
|
|
) : undefined
|
|||
|
|
)(list(xs));
|
|||
|
|
|
|||
|
|
|
|||
|
|
// snd :: (a, b) -> b
|
|||
|
|
const snd = tpl =>
|
|||
|
|
// Second member of a pair.
|
|||
|
|
tpl[1];
|
|||
|
|
|
|||
|
|
|
|||
|
|
// span :: (a -> Bool) -> [a] -> ([a], [a])
|
|||
|
|
const span = p =>
|
|||
|
|
// Longest prefix of xs consisting of elements which
|
|||
|
|
// all satisfy p, tupled with the remainder of xs.
|
|||
|
|
xs => {
|
|||
|
|
const i = xs.findIndex(x => !p(x));
|
|||
|
|
|
|||
|
|
return -1 !== i ? (
|
|||
|
|
Tuple(xs.slice(0, i))(
|
|||
|
|
xs.slice(i)
|
|||
|
|
)
|
|||
|
|
) : Tuple(xs)([]);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
// tail :: [a] -> [a]
|
|||
|
|
const tail = xs =>
|
|||
|
|
// A new list consisting of all
|
|||
|
|
// items of xs except the first.
|
|||
|
|
"GeneratorFunction" !== xs.constructor
|
|||
|
|
.constructor.name ? (
|
|||
|
|
(ys => 0 < ys.length ? ys.slice(1) : [])(
|
|||
|
|
xs
|
|||
|
|
)
|
|||
|
|
) : (take(1)(xs), xs);
|
|||
|
|
|
|||
|
|
|
|||
|
|
// take :: Int -> [a] -> [a]
|
|||
|
|
// take :: Int -> String -> String
|
|||
|
|
const take = n =>
|
|||
|
|
// The first n elements of a list,
|
|||
|
|
// string of characters, or stream.
|
|||
|
|
xs => "GeneratorFunction" !== xs
|
|||
|
|
.constructor.constructor.name ? (
|
|||
|
|
xs.slice(0, n)
|
|||
|
|
) : [].concat(...Array.from({
|
|||
|
|
length: n
|
|||
|
|
}, () => {
|
|||
|
|
const x = xs.next();
|
|||
|
|
|
|||
|
|
return x.done ? [] : [x.value];
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
|
|||
|
|
// uncons :: [a] -> Maybe (a, [a])
|
|||
|
|
const uncons = xs => {
|
|||
|
|
// Just a tuple of the head of xs and its tail,
|
|||
|
|
// Or Nothing if xs is an empty list.
|
|||
|
|
const lng = length(xs);
|
|||
|
|
|
|||
|
|
return (0 < lng) ? (
|
|||
|
|
Infinity > lng ? (
|
|||
|
|
// Finite list
|
|||
|
|
Just(Tuple(xs[0])(xs.slice(1)))
|
|||
|
|
) : (() => {
|
|||
|
|
// Lazy generator
|
|||
|
|
const nxt = take(1)(xs);
|
|||
|
|
|
|||
|
|
return 0 < nxt.length ? (
|
|||
|
|
Just(Tuple(nxt[0])(xs))
|
|||
|
|
) : Nothing();
|
|||
|
|
})()
|
|||
|
|
) : Nothing();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
// zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
|
|||
|
|
const zipWith = f =>
|
|||
|
|
// A list with the length of the shorter of
|
|||
|
|
// xs and ys, defined by zipping with a
|
|||
|
|
// custom function, rather than with the
|
|||
|
|
// default tuple constructor.
|
|||
|
|
xs => ys => {
|
|||
|
|
const n = Math.min(length(xs), length(ys));
|
|||
|
|
|
|||
|
|
return Infinity > n ? (
|
|||
|
|
(([as, bs]) => Array.from({
|
|||
|
|
length: n
|
|||
|
|
}, (_, i) => f(as[i])(
|
|||
|
|
bs[i]
|
|||
|
|
)))([xs, ys].map(
|
|||
|
|
take(n)
|
|||
|
|
))
|
|||
|
|
) : zipWithGen(f)(xs)(ys);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
// zipWithGen :: (a -> b -> c) ->
|
|||
|
|
// Gen [a] -> Gen [b] -> Gen [c]
|
|||
|
|
const zipWithGen = f => ga => gb => {
|
|||
|
|
const go = function* (ma, mb) {
|
|||
|
|
let
|
|||
|
|
a = ma,
|
|||
|
|
b = mb;
|
|||
|
|
|
|||
|
|
while (!a.Nothing && !b.Nothing) {
|
|||
|
|
const
|
|||
|
|
ta = a.Just,
|
|||
|
|
tb = b.Just;
|
|||
|
|
|
|||
|
|
yield f(fst(ta))(fst(tb));
|
|||
|
|
a = uncons(snd(ta));
|
|||
|
|
b = uncons(snd(tb));
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return go(uncons(ga), uncons(gb));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// MAIN ---
|
|||
|
|
return main();
|
|||
|
|
})();
|