mmsyn4-0.6.0.0: MMSyn4.hs
{-# OPtIONS_HADDOCK show-extensions #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE BangPatterns #-}
{-|
Module : MMSyn4
Description : The "glue" between electronic tables and GraphViz
Copyright : (c) OleksandrZhabenko, 2017-2020
License : MIT
Maintainer : olexandr543@yahoo.com
Stability : Experimental
A program @mmsyn4@ converts a specially formated @.csv@ file with a colon as a field separator obtained from the electronic table
into a visualized by GraphViz graph in the one of the supported by GraphViz graphics format. The proper GraphViz installation is required.
-}
module MMSyn4 (getFormat,process2) where
#ifdef __GLASGOW_HASKELL__
#if __GLASGOW_HASKELL__>=710
/* code that applies only to GHC 7.10.* and higher versions */
import GHC.Base (mconcat)
#endif
#endif
import Data.List (nub)
import System.Info (os)
import System.CPUTime (getCPUTime)
import System.Process (callCommand)
import CaseBi.Arr
import GHC.Arr
import EndOfExe (showE)
import Data.Maybe (isJust,fromJust)
#ifdef __GLASGOW_HASKELL__
#if __GLASGOW_HASKELL__==708
/* code that applies only to GHC 7.8.* */
mconcat = concat
#endif
#endif
isSep :: Char -> Bool
isSep = (== ':')
-- | Returns @True@ if OS is Windows.
isWindows :: Bool
isWindows = take 5 os == "mingw"
{-# INLINE isWindows #-}
divideString :: (Char -> Bool) -> String -> [String]
divideString p xs
| null xs = []
| otherwise = let (zs,ys) = break p xs in zs:(if null ys then [""] else divideString p (drop 1 ys))
isEscapeChar :: Char -> Bool
isEscapeChar x = x == '\n' || x == '\r'
dropEmptyLines :: [String] -> [String]
dropEmptyLines [] = []
dropEmptyLines (ys:yss)
| let ts = dropWhile isSep ys in all isEscapeChar ts || null ts = dropEmptyLines yss
| otherwise = ys:dropEmptyLines yss
cells :: String -> Array Int [String]
cells xs = amap (divideString isSep) . listArray (0,l) . dropEmptyLines . map (\rs -> if drop (length rs - 1) rs == "\r" then init rs else rs) $ yss
where (yss,l) = linesL1 xs
{-# INLINE cells #-}
-- | Inspired by: <https://hackage.haskell.org/package/base-4.14.0.0/docs/src/Data.OldList.html#lines>
linesL :: ([String],Int) -> String -> ([String],Int)
linesL (xs,y) "" = (xs,y)
linesL (xs,y) s = linesL (l:xs,y + 1) (case s' of { [] -> [] ; _:s'' -> s'' })
where (l, s') = break (== '\n') s
-- | Inspired by: <https://hackage.haskell.org/package/base-4.14.0.0/docs/src/Data.OldList.html#lines>
linesL1 :: String -> ([String],Int)
linesL1 = linesL ([],-1)
processCells :: String -> Array Int [String] -> String
processCells xs arr = makeRecordGv xs . convertElemsToStringGv . filterNeeded . changeNeededCells $ arr
{-# INLINE processCells #-}
processCellsG :: String -> String -> String
processCellsG xs = processCells xs . cells
{-# INLINE processCellsG #-}
-- | Do not change the lengths of element lists
changeNeededCells :: Array Int [String] -> Array Int [String]
changeNeededCells arr = listArray (bounds arr) . map (\(i, e) -> changeLine i e arr) . assocs $ arr
{-# INLINE changeNeededCells #-}
-- | Changes every line by changing (if needed) one empty String to the needed one non-empty. It is necessary for this to find the parent cell for the
-- line in the previous elements of the 'Array'. The contents of the cell (if exist) are substituted instead of the empty 'String' in the line being
-- processed. Afterwards, drops all the preceding empty strings in the line. The length of the line now is not constant.
changeLine :: Int -> [String] -> Array Int [String] -> [String]
changeLine i yss arr =
let !n = length . takeWhile null $ yss
!xs = parentCellContents n i arr in if null xs then drop n yss else xs:(drop n yss)
{-# NOINLINE changeLine #-}
parentCellContents :: Int -> Int -> Array Int [String] -> String
parentCellContents n i arr
| n == 0 || i == 0 = []
| otherwise = foldr1Elems (\x y -> if not . null $ y then y else x) . amap (!! (n - 1)) $ arr
-- | Change the lengths of element lists by dropping the last empty strings in every element.
filterNeeded :: Array Int [String] -> Array Int [String]
filterNeeded = amap (takeWhile (not . null))
{-# INLINE filterNeeded #-}
-- | Makes conversion for every line
convertElemsToStringGv :: Array Int [String] -> (Array Int String, String)
convertElemsToStringGv arr = (amap convertLineToStrGv arr, findAndMakeFilledWithClr arr)
convertLineToStrGv :: [String] -> String
convertLineToStrGv xss = "\"" ++ (let ys = concatMap (++"\"->\"") xss in take (length ys - 3) ys) ++ endOfLineGv
{-# INLINE convertLineToStrGv #-}
endOfLineGv :: String
endOfLineGv | isWindows = "\r\n"
| otherwise = "\n"
{-# INLINE endOfLineGv #-}
findAndMakeFilledWithClr :: Array Int [String] -> String
findAndMakeFilledWithClr = concatMap (('\"':) .
(++ "\" [style=filled, fillcolor=\"#ffffba\"];" ++ endOfLineGv)) . nub . mconcat . elems . amap lineWithAtSign
{-# INLINE findAndMakeFilledWithClr #-}
-- | In every list (representing a line) returns only those strings that begin with at-sign.
lineWithAtSign :: [String] -> [String]
lineWithAtSign = filter beginsWithAtSign
{-# INLINE lineWithAtSign #-}
beginsWithAtSign :: String -> Bool
beginsWithAtSign xs = if take 1 xs == "@" then True else take 2 xs == "\"@"
{-# INLINE beginsWithAtSign #-}
-- | Makes all needed additions and synthesizes into a single 'String' ready to be recorded to the .gv file.
makeRecordGv :: String -> (Array Int String, String) -> String
makeRecordGv xs (arr1,str2) = mconcat ["strict digraph 1 {", endOfLineGv, "overlap=false", endOfLineGv, "splines=",
case xs of { "0" -> "false" ; "1" -> "true" ; "2" -> "ortho" ; "3" -> "polyline" ; ~vvv -> "true" }, endOfLineGv,
mconcat (elems arr1 `mappend` [str2]), "}", endOfLineGv]
{-# INLINE makeRecordGv #-}
-- | Processes the given text (the first 'String' argument). The second one is used to get a name of the command to be
-- executed to obtain a visualization file. The third argument is used for the 'getFormat'. The fourth argument is the
-- basic name for the created files (without prefixes and extensions), the fifth one is an option for GraphVize splines
-- functionality. The sixth argument is used to specify whether to remove at-signs from the created files.
process2 :: String -> String -> String -> String -> String -> String -> IO ()
process2 text xxs yys bnames splines remAts
| length text > 0 = do
ts <- getCPUTime
[bnames1,splines1] <- proc2Params2 bnames splines
if remAts == "y"
then do
let ys = filter (/='@') . processCellsG splines1 $ text in writeFile (show ts ++ "." ++ bnames1 ++ ".gv") ys
putStrLn "The visualization will be created without the at-sign."
processFile 'n' ts bnames1 xxs yys
else do
let ys = processCellsG splines1 text in writeFile ("at." ++ show ts ++ "." ++ bnames1 ++ ".gv") ys
putStrLn "The visualization will be created with the at-sign preserved."
processFile 'a' ts bnames1 xxs yys
| otherwise = error "Empty text to be processed! "
procCtrl :: Int -> IO String
procCtrl 1 = putStrLn "Please, input the basic name of the visualization file!" >> getLine
procCtrl 2 = do
putStrLn "Please, specify the splines mode for GraphViz (see the documentation for GraphViz)"
putStrLn "0 -- for \"splines=false\""
putStrLn "1 -- for \"splines=true\""
putStrLn "2 -- for \"splines=ortho\""
putStrLn "3 -- for \"splines=polyline\""
putStrLn "The default one is \"splines=true\""
getLine
procCtrl _ = putStrLn "Would you like to remove all \'@\' signs from the visualization file?" >> getLine
processFile :: Char -> Integer -> String -> String -> String -> IO ()
processFile w t zs xxs yys = do
if all (isJust . showE) ["fdp","twopi","circo","neato","sfdp","dot","patchwork","osage"]
then processFile1 w t zs xxs yys
else error "MMSyn4.processFile: Please, install the GraphViz so that its executables are in the directories mentioned in the variable PATH!"
{-# INLINE processFile #-}
processFile1 :: Char -> Integer -> String -> String -> String -> IO ()
processFile1 w t zs xxs yys = do
[vs,spec] <- proc2Params xxs yys
let u = take 1 vs
if null u || u == "\n" || u == "\x0000"
then error "MMSyn4.processFile1: Please, specify the needed character."
else do
let temp = fromJust . showE . (\x -> case x of { "c" -> "circo" ; "d" -> "dot" ; "f" -> "fdp" ; "n" -> "neato" ;
"o" ->"osage" ; "p" -> "patchwork" ; "s" -> "sfdp" ; "t" -> "twopi" ; ~vv -> "sfdp" })
q = getFormat spec
callCommand $ temp u ++ (if w == 'n' then " -T" ++ q ++ " " else " -T" ++ q ++ " at.") ++ show t ++ "." ++ zs ++ ".gv -O "
proc2Params :: String -> String -> IO [String]
proc2Params xxs yys
| null xxs = if null yys then mapM getFormat1 [1,2] else do { vs <- getFormat1 1 ; return [vs,yys] }
| null yys = do { spec <- getFormat1 2 ; return [xxs,spec] }
| otherwise = return [xxs,yys]
{-# INLINE proc2Params #-}
specFormatFile :: IO String
specFormatFile = do
putStrLn "Please, specify the GraphViz output format for the file: "
mapM_ printFormF ["do", "xd", "ps", "pd", "sv", "sz", "fi", "pn", "gi", "jp", "je", "js", "im", "cm"]
putStrLn "otherwise there will be used the default -Tsvg"
getLine
{-# INLINE specFormatFile #-}
proc2Params2 :: String -> String -> IO [String]
proc2Params2 bnames splines
| null bnames = if null splines then mapM procCtrl [1,2] else do { bnames1 <- procCtrl 1 ; return [bnames1,splines] }
| null splines = do { splines1 <- procCtrl 2 ; return [bnames,splines1] }
| otherwise = return [bnames,splines]
{-# INLINE proc2Params2 #-}
getFormat1 :: Int -> IO String
getFormat1 1 = do
putStrLn "Please, specify the GraphViz command: "
mapM_ printGraphFilter ["d","f","t","c","n","s","p","o"]
putStrLn "otherwise there will be used the default sfdp"
getLine
getFormat1 _ = specFormatFile
{-# INLINE getFormat1 #-}
-- | For the given argument (usually of two characters) return the full form of the file format to be generated by GraphViz and @mmsyn4@. The default one
-- is \"svg\".
getFormat :: String -> String
getFormat xs = case xs of { "cm" -> "cmapx" ; "do" -> "dot" ; "fi" -> "fig" ; "gi" -> "gif" ; "im" -> "imap" ;
"je" -> "jpeg" ; "jp" -> "jpg" ; "js" -> "json" ; "pd" -> "pdf" ; "pn" -> "png" ; "ps" -> "ps" ; "sv" -> "svg" ; "sz" -> "svgz" ; "xd" -> "xdot" ; ~vvv -> "svg" }
{-# INLINE getFormat #-}
printFormF :: String -> IO ()
printFormF xs = putStrLn $ show xs ++ " -- for -T" ++ case xs of { "cm" -> "cmapx" ; "do" -> "dot" ; "fi" -> "fig" ;
"gi" -> "gif" ; "im" -> "imap" ; "je" -> "jpeg" ; "jp" -> "jpg" ; "js" -> "json" ; "pd" -> "pdf" ; "pn" -> "png" ;
"ps" -> "ps" ; "sv" -> "svg" ; "sz" -> "svgz" ; "xd" -> "xdot" ; ~vvv -> "svg" } ++ "\""
{-# INLINE printFormF #-}
printGraphFilter :: String -> IO ()
printGraphFilter xs = putStrLn $ show (take 1 xs) ++ " -- for " ++ case take 1 xs of { "c" -> "circo" ; "d" -> "dot" ;
"f" -> "fdp" ; "n" -> "neato" ; "o" -> "osage" ; "p" -> "patchwork" ; "s" -> "sfdp" ; "t" -> "twopi" ;
~vvv -> "sfdp" }
{-# INLINE printGraphFilter #-}