364 lines
9.3 KiB
Python
364 lines
9.3 KiB
Python
'''Display an outline as a nested table'''
|
|
|
|
from itertools import chain, cycle, takewhile
|
|
from functools import reduce
|
|
from operator import add
|
|
|
|
|
|
# wikiTablesFromOutline :: [String] -> String -> String
|
|
def wikiTablesFromOutline(colorSwatch):
|
|
'''Wikitable markup for (colspan) tables representing
|
|
the indentation of a given outline.
|
|
Each key-line point (child of a tree root) has a
|
|
distinct color, inherited by all its descendants.
|
|
The first color in the swatch is for the root node.
|
|
A sequence of tables is generated where the outline
|
|
represents a forest rather than a singly-rooted tree.
|
|
'''
|
|
def go(outline):
|
|
return '\n\n'.join([
|
|
wikiTableFromTree(colorSwatch)(tree) for tree in
|
|
forestFromLevels(
|
|
indentLevelsFromLines(
|
|
outline.splitlines()
|
|
)
|
|
)
|
|
])
|
|
return go
|
|
|
|
|
|
# wikiTableFromTree :: [String] -> Tree String -> String
|
|
def wikiTableFromTree(colorSwatch):
|
|
'''A wikitable rendered from a single tree.
|
|
'''
|
|
return compose(
|
|
wikiTableFromRows,
|
|
levels,
|
|
paintedTree(colorSwatch),
|
|
widthMeasuredTree,
|
|
ap(paddedTree(""))(treeDepth)
|
|
)
|
|
|
|
|
|
# ------------------------- TEST -------------------------
|
|
# main :: IO ()
|
|
def main():
|
|
'''A colored wikitable rendering of a given outline'''
|
|
|
|
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.'''
|
|
|
|
print(
|
|
wikiTablesFromOutline([
|
|
"#ffffe6",
|
|
"#ffebd2",
|
|
"#f0fff0",
|
|
"#e6ffff",
|
|
"#ffeeff"
|
|
])(outline)
|
|
)
|
|
|
|
|
|
# ------------------ TREE FROM OUTLINE -------------------
|
|
|
|
# indentLevelsFromLines :: [String] -> [(Int, String)]
|
|
def indentLevelsFromLines(xs):
|
|
'''Each input line stripped of leading
|
|
white space, and tupled with a preceding integer
|
|
giving its level of indentation from 0 upwards.
|
|
'''
|
|
indentTextPairs = [
|
|
(n, s[n:]) for (n, s)
|
|
in (
|
|
(len(list(takewhile(isSpace, x))), x)
|
|
for x in xs
|
|
)
|
|
]
|
|
indentUnit = len(next(
|
|
x for x in indentTextPairs if x[0]
|
|
)) or 1
|
|
return [
|
|
(x[0] // indentUnit, x[1])
|
|
for x in indentTextPairs
|
|
]
|
|
|
|
|
|
# forestFromLevels :: [(Int, String)] -> [Tree a]
|
|
def forestFromLevels(levelValuePairs):
|
|
'''A list of trees derived from a list of values paired
|
|
with integers giving their levels of indentation.
|
|
'''
|
|
def go(xs):
|
|
if xs:
|
|
level, v = xs[0]
|
|
children, rest = span(
|
|
lambda x: level < x[0]
|
|
)(xs[1:])
|
|
return [Node(v)(go(children))] + go(rest)
|
|
else:
|
|
return []
|
|
return go(levelValuePairs)
|
|
|
|
|
|
# -------------- TREE PADDED TO EVEN DEPTH ---------------
|
|
|
|
# paddedTree :: a -> (Int, Node a) -> Node a
|
|
def paddedTree(padValue):
|
|
'''A tree vertically padded to a given depth,
|
|
with additional nodes, containing padValue,
|
|
where needed.
|
|
'''
|
|
def go(tree):
|
|
def pad(n):
|
|
prev = n - 1
|
|
return Node(tree.get('root'))([
|
|
go(x)(prev) for x in (
|
|
tree.get('nest') or [Node(padValue)([])]
|
|
)
|
|
]) if prev else tree
|
|
return pad
|
|
return go
|
|
|
|
|
|
# treeDepth :: Tree a -> Int
|
|
def treeDepth(tree):
|
|
'''Maximum number of distinct levels in the tree.
|
|
'''
|
|
def go(_, xs):
|
|
return 1 + max(xs) if xs else 1
|
|
return foldTree(go)(tree)
|
|
|
|
|
|
# ------------ SPANNING WIDTH OF EACH SUBTREE ------------
|
|
|
|
# widthMeasuredTree :: Tree a -> Tree (a, Int)
|
|
def widthMeasuredTree(tree):
|
|
'''A tree in which each node value is tupled
|
|
with the width of the subtree.
|
|
'''
|
|
def go(x, xs):
|
|
return Node((x, 1))([]) if not xs else (
|
|
Node((x, reduce(
|
|
lambda a, child: a + (
|
|
child.get('root')[1]
|
|
),
|
|
xs,
|
|
0
|
|
)))(xs)
|
|
)
|
|
return foldTree(go)(tree)
|
|
|
|
|
|
# ----------------- COLOR SWATCH APPLIED -----------------
|
|
|
|
# paintedTree :: [String] -> Tree a -> Tree (String, a)
|
|
def paintedTree(swatch):
|
|
'''A tree in which every node value is tupled with
|
|
a hexadecimal color string taken from a swatch list.
|
|
The first colour is used for the root node.
|
|
The next n colours paint the root's n children.
|
|
All descendants of those children are painted with
|
|
the same color as their non-root ancestor.
|
|
'''
|
|
colors = cycle(swatch)
|
|
|
|
def go(tree):
|
|
return fmapTree(
|
|
lambda x: ("", x)
|
|
)(tree) if not swatch else (
|
|
Node(
|
|
(next(colors), tree.get('root'))
|
|
)(
|
|
list(map(
|
|
lambda k, child: fmapTree(
|
|
lambda v: (k, v)
|
|
)(child),
|
|
colors,
|
|
tree.get('nest')
|
|
))
|
|
)
|
|
)
|
|
return go
|
|
|
|
|
|
# ---------------- GENERIC TREE FUNCTIONS ----------------
|
|
|
|
# Node :: a -> [Tree a] -> Tree a
|
|
def Node(v):
|
|
'''Constructor for a Tree node which connects a
|
|
value of some kind to a list of zero or
|
|
more child trees.
|
|
'''
|
|
return lambda xs: {'root': v, 'nest': xs}
|
|
|
|
|
|
# fmapTree :: (a -> b) -> Tree a -> Tree b
|
|
def fmapTree(f):
|
|
'''A new tree holding the results of
|
|
an application of f to each root in
|
|
the existing tree.
|
|
'''
|
|
def go(x):
|
|
return Node(
|
|
f(x.get('root'))
|
|
)([go(v) for v in x.get('nest')])
|
|
return go
|
|
|
|
|
|
# foldTree :: (a -> [b] -> b) -> Tree a -> b
|
|
def foldTree(f):
|
|
'''The catamorphism on trees. A summary
|
|
value defined by a depth-first fold.
|
|
'''
|
|
def go(node):
|
|
return f(
|
|
node.get('root'),
|
|
[go(x) for x in node.get('nest')]
|
|
)
|
|
return go
|
|
|
|
|
|
# levels :: Tree a -> [[a]]
|
|
def levels(tree):
|
|
'''A list of lists, grouping the root
|
|
values of each level of the tree.
|
|
'''
|
|
return [[tree.get('root')]] + list(
|
|
reduce(
|
|
zipWithLong(add),
|
|
map(levels, tree.get('nest')),
|
|
[]
|
|
)
|
|
)
|
|
|
|
|
|
# ----------------- WIKITABLE RENDERING ------------------
|
|
|
|
# wikiTableFromRows :: [[(String, (String, Int))]] -> String
|
|
def wikiTableFromRows(rows):
|
|
'''A wiki table rendering of rows in which each cell
|
|
has the form (hexColorString, (text, colspan))
|
|
'''
|
|
def cw(color, width):
|
|
def go(w):
|
|
return f' colspan={w}' if 1 < w else ''
|
|
return f'style="background: {color}; "{go(width)}'
|
|
|
|
def cellText(cell):
|
|
color, (txt, width) = cell
|
|
return f'| {cw(color,width) if txt else ""} | {txt}'
|
|
|
|
def go(row):
|
|
return '\n'.join([cellText(cell) for cell in row])
|
|
|
|
return '{| class="wikitable" ' + (
|
|
'style="text-align: center;"\n|-\n'
|
|
) + '\n|-\n'.join([go(row) for row in rows]) + '\n|}'
|
|
|
|
|
|
# ----------------------- GENERIC ------------------------
|
|
|
|
# ap :: (a -> b -> c) -> (a -> b) -> a -> c
|
|
def ap(f):
|
|
'''Applicative instance for functions.
|
|
'''
|
|
def go(g):
|
|
return lambda x: f(x)(g(x))
|
|
return go
|
|
|
|
# compose :: ((a -> a), ...) -> (a -> a)
|
|
|
|
|
|
def compose(*fs):
|
|
'''Composition, from right to left,
|
|
of a series of functions.
|
|
'''
|
|
def go(f, g):
|
|
def fg(x):
|
|
return f(g(x))
|
|
return fg
|
|
return reduce(go, fs, lambda x: x)
|
|
|
|
|
|
# head :: [a] -> a
|
|
def head(xs):
|
|
'''The first element of a non-empty list.
|
|
'''
|
|
return xs[0] if isinstance(xs, list) else next(xs)
|
|
|
|
|
|
# isSpace :: Char -> Bool
|
|
# isSpace :: String -> Bool
|
|
def isSpace(s):
|
|
'''True if s is not empty, and
|
|
contains only white space.
|
|
'''
|
|
return s.isspace()
|
|
|
|
|
|
# span :: (a -> Bool) -> [a] -> ([a], [a])
|
|
def span(p):
|
|
'''The longest (possibly empty) prefix of xs that
|
|
contains only elements satisfying p, tupled with the
|
|
remainder of xs. span p xs is equivalent to
|
|
(takeWhile p xs, dropWhile p xs).
|
|
'''
|
|
def match(ab):
|
|
b = ab[1]
|
|
return not b or not p(b[0])
|
|
|
|
def f(ab):
|
|
a, b = ab
|
|
return a + [b[0]], b[1:]
|
|
|
|
def go(xs):
|
|
return until(match)(f)(([], xs))
|
|
return go
|
|
|
|
|
|
# until :: (a -> Bool) -> (a -> a) -> a -> a
|
|
def until(p):
|
|
'''The result of repeatedly applying f until p holds.
|
|
The initial seed value is x.
|
|
'''
|
|
def go(f):
|
|
def g(x):
|
|
v = x
|
|
while not p(v):
|
|
v = f(v)
|
|
return v
|
|
return g
|
|
return go
|
|
|
|
|
|
# zipWithLong :: ((a, a) -> a) -> ([a], [a]) -> [a]
|
|
def zipWithLong(f):
|
|
'''Analogous to map(f, xs, ys)
|
|
but returns a list with the length of the *longer*
|
|
of xs and ys, taking any surplus values unmodified.
|
|
'''
|
|
def go(xs, ys):
|
|
lxs = list(xs)
|
|
lys = list(ys)
|
|
i = min(len(lxs), len(lys))
|
|
return chain.from_iterable([
|
|
map(f, lxs, lys),
|
|
lxs[i:],
|
|
lys[i:]
|
|
])
|
|
return go
|
|
|
|
|
|
# MAIN ---
|
|
if __name__ == '__main__':
|
|
main()
|