tracetree-0.1.0.1: ttrender/Debug/Trace/Tree/Render/Options.hs
-- | Render simple trees
{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
module Debug.Trace.Tree.Render.Options where -- (RenderOptions(..), applyOptions) where
import Data.Bifunctor
import Data.Colour (Colour)
import Data.Colour.Names (readColourName)
import Data.Maybe (isJust)
import Data.List (groupBy)
import Data.Function (on)
import Diagrams.Backend.CmdLine (Parseable(..))
import Options.Applicative
import Text.Regex.Posix ((=~))
import qualified Text.Parsec as Parsec
import qualified Text.Parsec.String as Parsec
import Debug.Trace.Tree.Simple (SimpleTree, simpleETree)
import Debug.Trace.Tree.Edged
import Debug.Trace.Tree.Assoc (Assoc(..))
import qualified Debug.Trace.Tree.Simple as Simple
{-------------------------------------------------------------------------------
Matching nodes
-------------------------------------------------------------------------------}
data RegExp = RegExp String
instance Show RegExp where
show (RegExp regExp) = regExp
instance MatchAgainst String RegExp where
s `matchAgainst` (RegExp regExp) = s =~ regExp
{-------------------------------------------------------------------------------
Command line args proper
-------------------------------------------------------------------------------}
data RenderOptions = RenderOptions {
renderHideNodes :: [Hide String]
, renderMerge :: [String]
, renderVertical :: [RegExp]
, renderColours :: [(String, Colour Double)]
, renderMaxNotShown :: Int
, renderDelChildren :: [String]
, renderShowCoords :: Bool
, renderInput :: FilePath
}
instance Parseable RenderOptions where
parser = RenderOptions
<$> ( many (option (readParsec parseHide) $ mconcat [
long "hide"
, metavar "HIDE"
, help $ unlines [
"Hide certain nodes in the tree; arrows to these nodes are shown as dangling (modulo the max-not-shown option)."
, "Can be used multiple times."
, ""
, "Valid syntax for the argument is: "
, " \"node(SPEC)\": Hide all nodes matching SPEC."
, " \"max(RANGE,SPEC)\": Only show the children in RANGE of nodes matching SPEC."
, ""
, "Where SPEC is: "
, " \"y,x\": Node at specific coordinates"
, " REGEXP: Any node matching REGEXP"
, ""
, "RANGE is n-m, n-, or -m"
]
]))
<*> ( many (strOption $ mconcat [
long "merge"
, metavar "C"
, help "Collapse any tree of shape (C' .. (C args) ..) to (C' .. args ..). Can be used multiple times."
]))
<*> ( many (option (readParsec parseRegExp) $ mconcat [
long "vertical"
, metavar "REGEXP"
, help "Show any node matching the specified regular expression vertically. Can be used multiple times."
]))
<*> ( many (option readColourAssignment $ mconcat [
long "colour"
, help "Set the colour for a node. For example --colour P=red sets the background of all P nodes to be red. Can be used multiple times."
]))
<*> ( option auto $ mconcat [
long "max-not-shown"
, metavar "Int"
, value 3
, showDefault
, help "Maximum number of edges to hidden nodes (use together with --max-breadths)"
])
<*> ( many (strOption $ mconcat [
long "delete-children"
, help "Delete the children of the specified node. This actually removes the children from the tree, no dangling arrows are shown. Can be used multiple times."
]))
<*> ( switch $ mconcat [
long "show-coords"
, help "Superimpose node coordinates when rendering the tree. Useful when figuring out which arguments to pass to --hide."
])
<*> ( argument str (metavar "JSON") )
readParsec :: Parsec.Parser a -> ReadM a
readParsec p = do
arg <- str
case Parsec.parse p "command line argument" arg of
Left err -> fail (show err)
Right a -> return a
readColourAssignment :: ReadM (String, Colour Double)
readColourAssignment = do
arg <- str
case break (== '=') arg of
(constr, '=':colour) -> (constr,) <$> readColourName colour
_ -> fail "Invalid colour assignment"
applyOptions :: RenderOptions -> SimpleTree -> ETree String (Maybe String, Metadata)
applyOptions RenderOptions{..} =
applyMaxNotShown renderMaxNotShown
. hideNodes renderHideNodes
. annotate
. simpleETree
. applyMerge renderMerge
. applyDelChildren renderDelChildren
applyMerge :: [String] -> SimpleTree -> SimpleTree
applyMerge toMerge (Simple.Node c' (Assoc ts)) =
Simple.Node c' . fmap (applyMerge toMerge) . Assoc $ concatMap aux ts
where
aux t@(_, Simple.Node c (Assoc ts'))
| c `elem` toMerge = ts'
| otherwise = [t]
aux _ = error "inaccessible"
applyMerge _ _ = error "inaccessible"
applyDelChildren :: [String] -> SimpleTree -> SimpleTree
applyDelChildren toHide = go
where
go (Simple.Node c (Assoc ts))
| c `elem` toHide = Simple.Node c (Assoc [])
| otherwise = Simple.Node c (Assoc (map (second go) ts))
go _ = error "inaccessible"
applyMaxNotShown :: Int -> ETree String (Maybe String, Metadata) -> ETree String (Maybe String, Metadata)
applyMaxNotShown n = \(Node c (Assoc ts)) ->
let culled = concatMap aux (groupBy ((==) `on` isShown) ts)
in Node c $ fmap (applyMaxNotShown n) (Assoc culled)
where
-- Replace each group of hidden nodes with an ellipsis (if larger than n)
-- We re-use the node metadata of the first node in the group
aux :: [(String, ETree String (Maybe String, Metadata))]
-> [(String, ETree String (Maybe String, Metadata))]
aux [] = error "impossible: groupBy does not create empty groups"
aux ts@(t:_)
| not (isShown t) && length ts > n = [ellipsis (rootMeta (snd t))]
| otherwise = ts
ellipsis :: Metadata -> (String, ETree String (Maybe String, Metadata))
ellipsis meta = ("...", Node (Nothing, meta) (Assoc []))
isShown :: (String, ETree String (Maybe String, meta)) -> Bool
isShown (_key, (Node (c', _coords) _subtree)) = isJust c'
rootMeta :: ETree k (v, Metadata) -> Metadata
rootMeta (Node (_, meta) _) = meta
{-------------------------------------------------------------------------------
Parser for Hide
-------------------------------------------------------------------------------}
parseHide :: Parsec.Parser (Hide String)
parseHide =
(do Parsec.try $ Parsec.string "node("
s <- parseNodeSpec
Parsec.string ")"
return $ HideNode s)
<|> (do Parsec.try $ Parsec.string "max("
lo <- parseInt
Parsec.string "-"
hi <- parseInt
Parsec.string ","
s <- parseNodeSpec
Parsec.string ")"
return $ HideMax (lo, hi) s)
parseNodeSpec :: Parsec.Parser (NodeSpec String)
parseNodeSpec =
Parsec.try (do y <- parseInt
Parsec.string ","
x <- parseInt
return $ NodeCoords $ Coords y x)
<|>
(NodeMatch <$> parseRegExp)
parseInt :: Parsec.Parser Int
parseInt = read <$> Parsec.many1 Parsec.digit
-- | Regular expression delimited by an unmatched closing parenthesis
parseRegExp :: Parsec.Parser RegExp
parseRegExp = RegExp <$> matchBrackets
{-------------------------------------------------------------------------------
Auxiliary parsec
-------------------------------------------------------------------------------}
-- | Match as much as possible, until we see an unexpected closing parenthesis
matchBrackets :: Parsec.Parser String
matchBrackets = go 0
where
go :: Int -> Parsec.Parser String
go nestingDepth = (do
x <- if nestingDepth > 0 then Parsec.anyChar else Parsec.noneOf ")"
let nestingDepth' | x == '(' = nestingDepth + 1
| x == ')' = nestingDepth - 1
| otherwise = nestingDepth
xs <- go nestingDepth'
return (x:xs)) <|> return ""