tree-render-text-0.4.0.0: src/Data/Tree/Render/Text.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE Safe #-}
-- | Configurable text rendering of trees.
--
-- Example renderings for:
--
-- > import Data.Tree
-- > import Data.Tree.Render.Text
-- >
-- > tree :: Tree String
-- > tree
-- > = Node "Add"
-- > [ Node "Add"
-- > [ Node "0" []
-- > , Node "Mul"
-- > [ Node "1" []
-- > , Node "2" []
-- > ]
-- > ]
-- > , Node "Neg"
-- > [ Node "Max"
-- > [ Node "3" []
-- > , Node "4" []
-- > , Node "5" []
-- > , Node "Var"
-- > [ Node "x" []
-- > ]
-- > , Node "6" []
-- > ]
-- > ]
-- > ]
-- >
-- > renderTree (tracedRenderOptions id) tree
-- >
-- > ● Add
-- > ├─● Add
-- > │ ├─● 0
-- > │ ╰─● Mul
-- > │ ├─● 1
-- > │ ╰─● 2
-- > ╰─● Neg
-- > ╰─● Max
-- > ├─● 3
-- > ├─● 4
-- > ├─● 5
-- > ├─● Var
-- > │ ╰─● x
-- > ╰─● 6
-- >
-- > Other renderings by setting 'ParentLocation' and 'ChildOrder' in the options:
-- >
-- > ╭─● 0 ╭─● 0 ● Add ╭─● 6 ╭─● 6
-- > │ ╭─● 1 ╭─● Add ├─● Neg │ ╭─● x │ ╭─● x
-- > │ ├─● 2 │ │ ╭─● 1 │ ╰─● Max ├─● Var ├─● Var
-- > ├─● Mul │ ╰─● Mul │ ├─● 6 ├─● 5 ├─● 5
-- > ╭─● Add │ ╰─● 2 │ ├─● Var ├─● 4 ╭─● Max
-- > │ ╭─● 3 ● Add │ │ ╰─● x ├─● 3 │ ├─● 4
-- > │ ├─● 4 ╰─● Neg │ ├─● 5 ╭─● Max │ ╰─● 3
-- > │ ├─● 5 │ ╭─● 3 │ ├─● 4 ╭─● Neg ╭─● Neg
-- > │ │ ╭─● x │ ├─● 4 │ ╰─● 3 │ ╭─● 2 ● Add
-- > │ ├─● Var ╰─● Max ╰─● Add │ ├─● 1 │ ╭─● 2
-- > │ ├─● 6 ├─● 5 ├─● Mul │ ╭─● Mul │ ╭─● Mul
-- > │ ╭─● Max ├─● Var │ ├─● 2 │ ├─● 0 │ │ ╰─● 1
-- > ├─● Neg │ ╰─● x │ ╰─● 1 ├─● Add ╰─● Add
-- > ● Add ╰─● 6 ╰─● 0 ● Add ╰─● 0
--
module Data.Tree.Render.Text (
ParentLocation(..),
ChildOrder(..),
BranchPath(..),
LocalContext(..),
RenderOptionsM(..),
RenderOptions,
renderTree,
renderForest,
renderTreeM,
renderForestM,
tracedRenderOptions,
tracedRenderOptionsAscii,
middleCutRenderOptions,
zigZagRenderOptions,
tabbedRenderOptions,
tracedRenderOptionsM,
tracedRenderOptionsAsciiM,
middleCutRenderOptionsM,
zigZagRenderOptionsM,
tabbedRenderOptionsM,
) where
import qualified Control.Monad.State.Strict as M
import qualified Control.Monad.Writer as M
import qualified Data.List as List
import qualified Data.Maybe as Maybe
import Data.Monoid ( Endo(Endo, appEndo) )
import qualified Data.Tree as Tree
import Data.Tree ( Tree, Forest )
-- | A difference list on typ 'a'.
type DList a = Endo [a]
runDListWriter :: M.Writer (DList a) () -> [a]
runDListWriter = ($ []) . appEndo . M.execWriter
-- | Appends a list '[a]' to the output of a 'M.Writer (DList a)'.
tellDList :: [a] -> M.Writer (DList a) ()
tellDList s = M.tell $ Endo (s <>)
-- | Describes where a parent node is rendered, relative to its children.
data ParentLocation
= ParentBeforeChildren
-- ^ Renders the parent before any of its children.
| ParentAfterChildren
-- ^ Renders the parent after all of its children.
| ParentBetweenChildren
-- ^ Renders the parent in the middle of its children (if there are multiple children).
-- The index is rounded down when using 'FirstToLast' and rounded up when using 'LastToFirst'.
| ParentAtChildIndex Int
-- ^ This is a value from @[0, 1, ..., length children]@ inclusive.
-- (Values outside this range are clamped to the closest valid value.)
--
-- A value of @0@ makes the parent rendered before any of its children
-- A value of @length children@ makes the parent rendered after all of its children.
-- Other values place the parent in the corresponding spot between its children.
deriving (Show, Eq, Ord)
-- | Describes the render order of a node's children.
data ChildOrder
= FirstToLast
| LastToFirst
deriving (Show, Eq, Ord)
-- | A part of a path along a rendered tree.
data BranchPath
= BranchUp
-- ^ Describes a turn going up toward the left.
--
-- e.g. @"╭─"@
| BranchDown
-- ^ Describes a turn going down toward the left.
--
-- e.g. @"╰─"@
| BranchJoin
-- ^ Describes a T-join of a path going up and down toward the left.
--
-- e.g. @"├─"@
| BranchContinue
-- ^ Describes a path going up and down.
--
-- e.g. @"│ "@
| BranchEmpty
-- ^ Describes a part that does NOT contain a path piece.
--
-- e.g. @" "@
deriving (Show, Eq, Ord)
-- | Local context about a node.
data LocalContext label
= LocalContext
{ lcCurrentNode :: Tree label
-- ^ The node assiated with this context.
, lcCurrentDepth :: !Int
-- ^ The depth of the current node.
, lcLitterIndex :: !Int
-- ^ The index of the current node with respect to its parent's children.
, lcLitterSize :: !Int
-- ^ The number of children the current node's parent has.
}
-- | Options used for rendering a 'Tree'.
data RenderOptionsM m string label = RenderOptions
{ oParentLocation :: Maybe (LocalContext label) -> m ParentLocation
-- ^ Controls where parent nodes are rendered.
--
-- A value of 'Nothing' is passed when rending the artificial root of a 'Forest'.
--
-- Simple use cases would use a constant function ignoring the local context.
, oChildOrder :: Maybe (LocalContext label) -> m ChildOrder
-- ^ Controls the order a node's children are rendered.
--
-- A value of 'Nothing' is passed when rending the artificial root of a 'Forest'.
--
-- Simple use cases would use a constant function ignoring the local context.
, oVerticalPad :: Int
-- ^ The amount of vertical spacing between nodes.
, oPrependNewline :: Bool
-- ^ If 'True', a newline is prepended to the rendered output.
, oWriteNewline :: m ()
-- ^ Writes a newline.
, oWrite :: string -> m ()
-- ^ Writes a 'string'.
, oShowNodeLabel :: Maybe label -> m string
-- ^ Shows a 'Tree.rootLabel'.
-- The returned value should contain no newlines.
, oNodeMarker :: Maybe (LocalContext label) -> m string
-- ^ Get the marker for a node (without rendering its label).
-- The returned value should contain no newlines.
--
-- 'LocalContext' is passed as an argument to allow things such as:
--
-- - Rendering a node marker differently for labels that fail to pass a test.
-- - Highlighting a node currently being visited.
-- - Numbered bullets.
--
-- A value of 'Nothing' is passed when rending the artificial root of a 'Forest'.
--
-- Simple use cases would typically ignore the local context.
, oShowBranchPath :: BranchPath -> string
-- ^ Shows a 'BranchPath'. The returned value should contain no newlines and
-- should all be of the same printed width when rendered as text.
}
mkStringRenderOptionsM
:: Monad m
=> (Bool -> String)
-> (BranchPath -> String)
-> (String -> string)
-> (string -> m ())
-> (label -> m string)
-> RenderOptionsM m string label
mkStringRenderOptionsM showMarker showPath fromStr write showLabel
= RenderOptions
{ oParentLocation = const loc
, oChildOrder = const ord
, oVerticalPad = 0
, oPrependNewline = False
, oWriteNewline = write newline
, oWrite = write
, oShowNodeLabel = maybe nil showLabel
, oNodeMarker = \case
Just {} -> node
Nothing -> root
, oShowBranchPath = \case
BranchUp -> up
BranchDown -> down
BranchJoin -> join
BranchContinue -> continue
BranchEmpty -> empty
}
where
loc = pure ParentBeforeChildren
ord = pure FirstToLast
nil = pure $ fromStr ""
node = pure $ fromStr $ showMarker True
root = pure $ fromStr $ showMarker False
up = fromStr $ showPath BranchUp
down = fromStr $ showPath BranchDown
join = fromStr $ showPath BranchJoin
continue = fromStr $ showPath BranchContinue
empty = fromStr $ showPath BranchEmpty
newline = fromStr "\n"
unicodeMarker :: Bool -> String
unicodeMarker = \case
True -> "● "
False -> "●"
unicodePath :: BranchPath -> String
unicodePath = \case
BranchUp -> "╭─"
BranchDown -> "╰─"
BranchJoin -> "├─"
BranchContinue -> "│ "
BranchEmpty -> " "
-- | Options for rendering a line-traced tree using unicode drawing characters.
--
-- This uses:
--
-- > BranchUp -> "╭─"
-- > BranchDown -> "╰─"
-- > BranchJoin -> "├─"
-- > BranchContinue -> "│ "
-- > BranchEmpty -> " "
--
-- > oNodeMarker = \case
-- > Just {} -> "● "
-- > Nothing -> "●"
--
tracedRenderOptionsM
:: Monad m
=> (String -> string)
-- ^ Promotes a 'String' to a 'string'.
-> (string -> m ())
-- ^ Writes a 'string'.
-> (label -> m string)
-- ^ Shows a 'Tree.rootLabel'.
-> RenderOptionsM m string label
tracedRenderOptionsM = mkStringRenderOptionsM unicodeMarker unicodePath
-- | Options for rendering a line-traced tree using ASCII characters.
--
-- This uses:
--
-- > BranchUp -> ",-"
-- > BranchDown -> "`-"
-- > BranchJoin -> "|-"
-- > BranchContinue -> "| "
-- > BranchEmpty -> " "
--
-- > oNodeMarker = \case
-- > Just {} -> "o "
-- > Nothing -> "o"
--
tracedRenderOptionsAsciiM
:: Monad m
=> (String -> string)
-- ^ Promotes a 'String' to a 'string'.
-> (string -> m ())
-- ^ Writes a 'string'.
-> (label -> m string)
-- ^ Shows a 'Tree.rootLabel'.
-> RenderOptionsM m string label
tracedRenderOptionsAsciiM = mkStringRenderOptionsM marker path
where
marker = \case
True -> "o "
False -> "o"
path = \case
BranchUp -> ",-"
BranchDown -> "`-"
BranchJoin -> "|-"
BranchContinue -> "| "
BranchEmpty -> " "
-- | A variety on 'tracedRenderOptionsM' where the path tracing is
-- performed in a zig-zag-like fashion such that there is a cut down
-- the middle of a node's children.
middleCutRenderOptionsM
:: Monad m
=> (String -> string)
-- ^ Promotes a 'String' to a 'string'.
-> (string -> m ())
-- ^ Writes a 'string'.
-> (label -> m string)
-- ^ Shows a 'Tree.rootLabel'.
-> RenderOptionsM m string label
middleCutRenderOptionsM fromStr write showLabel = options
{ oParentLocation = pure . \case
Nothing -> ParentBeforeChildren
Just LocalContext
{ lcLitterIndex = index
, lcLitterSize = size
} -> case index < (size `div` 2) of
True -> ParentBeforeChildren
False -> ParentAfterChildren
}
where
options = tracedRenderOptionsM fromStr write showLabel
-- | A variety on 'tracedRenderOptionsM' where the path tracing is
-- performed in a zig-zag fashion.
zigZagRenderOptionsM
:: Monad m
=> (String -> string)
-- ^ Promotes a 'String' to a 'string'.
-> (string -> m ())
-- ^ Writes a 'string'.
-> (label -> m string)
-- ^ Shows a 'Tree.rootLabel'.
-> RenderOptionsM m string label
zigZagRenderOptionsM fromStr write showLabel = options
{ oParentLocation = pure . \case
Nothing -> ParentBeforeChildren
Just LocalContext
{ lcCurrentDepth = depth
} -> case depth `mod` 2 of
0 -> ParentBeforeChildren
_ -> ParentAfterChildren
}
where
options = tracedRenderOptionsM fromStr write showLabel
-- | Options for rendering a tree in rows indented only by tabs.
tabbedRenderOptionsM
:: Monad m
=> String
-- ^ The string used for a tab.
-> (String -> string)
-- ^ Promotes a 'String' to a 'string'.
-> (string -> m ())
-- ^ Writes a 'string'.
-> (label -> m string)
-- ^ Shows a 'Tree.rootLabel'.
-> RenderOptionsM m string label
tabbedRenderOptionsM tab = mkStringRenderOptionsM marker path
where
marker = const ""
path = const tab
-- | An alias of 'RenderOptionsM' for producing pure 'String' renders.
type RenderOptions = RenderOptionsM (M.Writer (DList Char))
-- | A simplified 'tracedRenderOptionsM' specialized to @RenderOptions@.
tracedRenderOptions
:: (label -> String)
-- ^ Shows a 'Tree.rootLabel'.
-> RenderOptions String label
tracedRenderOptions = tracedRenderOptionsM id tellDList . fmap pure
-- | A simplified 'tabbedRenderOptionsM' specialized to @RenderOptions@.
tabbedRenderOptions
:: String
-- ^ The string used for a tab.
-> (label -> String)
-- ^ Shows a 'Tree.rootLabel'.
-> RenderOptions String label
tabbedRenderOptions tab = tabbedRenderOptionsM tab id tellDList . fmap pure
-- | A simplified 'middleCutRenderOptionsM' specialized to @RenderOptions@.
middleCutRenderOptions
:: (label -> String)
-- ^ Shows a 'Tree.rootLabel'.
-> RenderOptions String label
middleCutRenderOptions = middleCutRenderOptionsM id tellDList . fmap pure
-- | A simplified 'zigZagRenderOptionsM' specialized to @RenderOptions@.
zigZagRenderOptions
:: (label -> String)
-- ^ Shows a 'Tree.rootLabel'.
-> RenderOptions String label
zigZagRenderOptions = zigZagRenderOptionsM id tellDList . fmap pure
-- | A simplified 'tracedRenderOptionsAsciiM' specialized to @RenderOptions@.
tracedRenderOptionsAscii
:: (label -> String)
-- ^ Shows a 'Tree.rootLabel'.
-> RenderOptions String label
tracedRenderOptionsAscii = tracedRenderOptionsAsciiM id tellDList . fmap pure
-- | Renders a 'Tree' to a 'String'.
renderTree :: RenderOptions String label -> Tree label -> String
renderTree options = runDListWriter . renderTreeM options
-- | Renders a 'Forest' to a 'String'.
renderForest :: RenderOptions String label -> Forest label -> String
renderForest options = runDListWriter . renderForestM options
-- | Renders a pretty printed 'Tree' within a monadic context.
renderTreeM :: Monad m => RenderOptionsM m string label -> Tree label -> m ()
renderTreeM options tree = M.evalStateT action options
where
action = render lc []
lc = LocalContext
{ lcCurrentNode = tree
, lcCurrentDepth = 0
, lcLitterIndex = 0
, lcLitterSize = 1
}
catMaybes :: Tree (Maybe label) -> Maybe (Tree label)
catMaybes = \case
Tree.Node
{ Tree.rootLabel = mLabel
, Tree.subForest = kids
} -> case mLabel of
Nothing -> Nothing
Just label -> Just Tree.Node
{ Tree.rootLabel = label
, Tree.subForest = Maybe.mapMaybe catMaybes kids
}
-- | Renders a pretty printed 'Forest' within a monadic context.
renderForestM :: Monad m => RenderOptionsM m string label -> Forest label -> m ()
renderForestM options trees = do
let forestTree = Tree.Node Nothing $ map (fmap Just) trees
let flattenLc = \case
Nothing -> Nothing
Just lc ->
let node = lcCurrentNode lc
in case catMaybes node of
Nothing -> Nothing
Just node' -> Just lc { lcCurrentNode = node' }
let options' = options
{ oShowNodeLabel = oShowNodeLabel options . maybe Nothing id
, oParentLocation = oParentLocation options . flattenLc
, oChildOrder = oChildOrder options . flattenLc
, oNodeMarker = oNodeMarker options . flattenLc
}
renderTreeM options' forestTree
type Render string label m = M.StateT (RenderOptionsM m string label) m
renderString :: Monad m => string -> Render string label m ()
renderString s = do
w <- M.gets oWrite
M.lift $ w s
writeNewline :: Monad m => Render string label m ()
writeNewline = M.gets oWriteNewline >>= M.lift
render :: Monad m => LocalContext label -> [BranchPath] -> Render string label m ()
render lc trail = case lcCurrentNode lc of
Tree.Node
{ Tree.rootLabel = label
, Tree.subForest = kids'
} -> do
parentLoc <- M.gets (flip oParentLocation $ Just lc) >>= M.lift
childOrder <- M.gets (flip oChildOrder $ Just lc) >>= M.lift
let renderCurr = do
M.gets oPrependNewline >>= \case
True -> writeNewline
False -> M.modify' $ \st -> st
{ oPrependNewline = True
}
renderTrail trail
marker <- M.gets (flip oNodeMarker $ Just lc) >>= M.lift
renderString marker
shownLabel <- M.gets (flip oShowNodeLabel $ Just label) >>= M.lift
renderString shownLabel
let kidCount = length kids'
let kids =
let f = case childOrder of
FirstToLast -> id
LastToFirst -> reverse
in flip map (f $ zip kids' [0..]) $ \(kid, idx) -> LocalContext
{ lcCurrentNode = kid
, lcCurrentDepth = lcCurrentDepth lc + 1
, lcLitterIndex = idx
, lcLitterSize = kidCount
}
let trailL = case trail of
BranchDown : rest -> BranchContinue : rest
_ -> trail
trailR = case trail of
BranchUp : rest -> BranchContinue : rest
_ -> trail
renderNextL path lc' = render lc' (path : trailL)
renderNextR path lc' = render lc' (path : trailR)
let index = case parentLoc of
ParentBeforeChildren -> 0
ParentAfterChildren -> kidCount
ParentBetweenChildren -> case childOrder of
FirstToLast -> kidCount `div` 2
LastToFirst -> case kidCount `divMod` 2 of
(d, 0) -> d
(d, _) -> d + 1
ParentAtChildIndex i -> max 0 $ min kidCount i
case (index == 0, index == kidCount) of
(True, _ ) -> do
case initLast kids of
Nothing -> do
renderCurr
Just (ks, k) -> do
renderCurr
M.forM_ ks $ \k' -> do
renderVerticalSpace trailR
renderNextR BranchJoin k'
renderVerticalSpace trailR
renderNextR BranchDown k
( _, True) -> do
case kids of
[] -> do
renderCurr
k : ks -> do
renderNextL BranchUp k
M.forM_ ks $ \k' -> do
renderVerticalSpace trailL
renderNextL BranchJoin k'
renderVerticalSpace trailL
renderCurr
( _ , _ ) -> do
case headMiddleLast kids of
Nothing -> undefined -- This can't happen.
Just (_, Nothing) -> undefined -- This can't happen.
Just (k0, Just (ks, kn)) -> do
let (ksL, ksR) = List.splitAt (index - 1) ks
renderNextL BranchUp k0
M.forM_ ksL $ \k -> do
renderVerticalSpace trailL
renderNextL BranchJoin k
renderVerticalSpace trailL
renderCurr
M.forM_ ksR $ \k -> do
renderVerticalSpace trailR
renderNextR BranchJoin k
renderVerticalSpace trailR
renderNextR BranchDown kn
renderVerticalSpace :: Monad m => [BranchPath] -> Render string label m ()
renderVerticalSpace trail = do
n <- M.gets oVerticalPad
M.replicateM_ n $ do
writeNewline
renderTrail $ BranchContinue : trail
renderTrail :: Monad m => [BranchPath] -> Render string label m ()
renderTrail trail = do
showPath <- M.gets oShowBranchPath
let renderPath = renderString . showPath
case trail of
[] -> pure ()
p : ps -> do
M.forM_ (reverse ps) $ renderPath . \case
BranchDown -> BranchEmpty
BranchUp -> BranchEmpty
BranchEmpty -> BranchEmpty
_ -> BranchContinue
renderString $ showPath p
initLast :: [a] -> Maybe ([a], a)
initLast = \case
[] -> Nothing
xs -> Just (init xs, last xs)
headMiddleLast :: [a] -> Maybe (a, Maybe ([a], a))
headMiddleLast = \case
[] -> Nothing
x : xs -> case xs of
[] -> Just (x, Nothing)
_ -> Just (x, Just (init xs, last xs))