packages feed

transf-0.8: src/Text/Transf.hs

{-# LANGUAGE
    GeneralizedNewtypeDeriving #-}

module Text.Transf (
        Line,
        Lines,
        RelativePath,

        -- * Combinators
        Transf,
        newTransf,
        namedTransf,
        readFile,
        writeFile,
        eval,
        eval',
        inform,

        -- ** Runing transformations
        runTransf,
        runTransf',

        -- * TF monad
        TFT,
        TF,
        runTFT,
        runTF,

        -- * Predefined transformations
        printT,
        evalT,
        musicT,
) 
where

import Prelude hiding (mapM, readFile, writeFile)         

import Control.Exception
import Control.Monad.Error hiding (mapM)
import Control.Monad.Plus hiding (mapM)
import Data.Semigroup
import Data.Traversable (mapM)     
import Data.Typeable
import Data.Hashable
import Data.Maybe
import Language.Haskell.Interpreter hiding (eval)
import System.IO (hPutStr, stderr)
import System.Process
import Numeric
import Music.Prelude.Basic

import qualified Prelude
import qualified Data.List as List
import qualified Data.Char as Char


-- | A line of text.
type Line = String

-- | Multiple-line text.
type Lines = String

-- | A relative file path.
type RelativePath = FilePath

-- | Transformer version of 'TF'.
newtype TFT m a = TFT { runTFT :: ErrorT String m a }
    deriving (Monad, MonadPlus, MonadError String, MonadIO)

-- | The 'TF' monad defines the context of a transformation function.
--   Think of it as a restricted version of 'IO' or 'STM'.
type TF = TFT IO

runTF :: TF a -> IO (Either String a)
runTF = runErrorT . runTFT

-- | Read a file.
readFile :: RelativePath -> TF String
readFile path = do
    input <- liftIO $ try $ Prelude.readFile path
    case input of
        Left e  -> throwError $ "readFile: " ++ show (e::SomeException)
        Right a -> return a

-- appendFile   :: RelativePath -> String -> TF ()

-- | Write to a file.
writeFile :: RelativePath -> String -> TF ()
writeFile path str = liftIO $ Prelude.writeFile path str

-- | Evaluate a Haskell expression.
eval :: Typeable a => String -> TF a
eval = eval' ["Prelude", "Music.Prelude.Basic"]

-- | Evaluate a Haskell expression.
eval' :: Typeable a => [String] -> String -> TF a
eval' imps str = do                                         
    res <- liftIO $ runInterpreter $ do
        setImports imps
        interpret str infer
    case res of
        Left e -> throwError $ "eval: " ++ show e
        Right a -> return a

-- | Write to the standard error stream.
inform :: String -> TF ()
inform m = liftIO $ hPutStr stderr $ m ++ "\n"

-- | A transformation function, or transformation for short.
data Transf 
    = CompTrans {
        decomp    :: [Transf]
    }
    | SingTrans {
        guard     :: (Line -> Bool, Line -> Bool),
        function  :: Lines -> TF Lines
    }

instance Semigroup Transf where
    a <> b = CompTrans [a,b]

-- | Create a new transformation.
newTransf :: (Line -> Bool) -> (Line -> Bool) -> (Lines -> TF Lines) -> Transf
newTransf b e = SingTrans (b, e)

-- | Create a new transformation. 
--
--   This transformation processes everything in between lines containing
--
--   > ~~~name
--   > ~~~
--
--   or alternatively
--
--   > ```name
--   > ```
--
--   where @name@ is the name of the transformation.
--
namedTransf :: String -> (Lines -> TF Lines) -> Transf
namedTransf name f = newTransf (namedGuard name) (namedGuard "") f

namedGuard :: String -> String -> Bool
namedGuard name = namedGuardWithPrefix "```" name `or'` namedGuardWithPrefix "~~~" name

namedGuardWithPrefix :: String -> String -> String -> Bool
namedGuardWithPrefix prefix name = (== (prefix ++ name)) . trimEnd


printT :: Transf
printT = namedTransf "print" $ \input -> do
    return input

evalT :: Transf
evalT = namedTransf "eval" $ \input -> do
    eval input

musicT :: Transf
musicT = namedTransf "music" $ \input -> do
    let name = showHex (abs $ hash input) ""
    music <- eval input :: TF (Score Note)
    liftIO $ writeLy (name++".ly") music
    liftIO $ system $ "lilypond -f png -dresolution=300 "++name++".ly"
    liftIO $ system $ "convert -transparent white -resize 30% "++name++".png "++name++"x.png"
    return $ "![]("++name++"x.png)"
    --  -resize 30%


-- | Run a transformation with the given error handler and input.
runTransf :: Transf -> (String -> IO String) -> String -> IO String
runTransf t handler input = do
    res <- runTF $ runTransf' t input
    case res of
        Left e  -> handler e
        Right a -> return a


runTransf' :: Transf -> String -> TF String
runTransf' (CompTrans []) as = return as

runTransf' (CompTrans (t:ts)) as = do
    bs <- runTransf' t as
    runTransf' (CompTrans ts) bs
    
runTransf' (SingTrans (start,stop) f) as = do
    let bs = (sections start stop . lines) as                   :: [([Line], Maybe [Line])]
    let cs = fmap (first unlines . second (fmap unlines)) bs    :: [(String, Maybe String)]
    ds <- mapM (secondM (mapM f)) cs                            :: TF [(String, Maybe String)]    
    return $ concatMap (\(a, b) -> a ++ fromMaybe [] b ++ "\n") ds


-- | Separate the sections delimited by the separators from their context. Returns
--      [(outside1, inside1), (outside2, inside2)...] 
--
sections :: (a -> Bool) -> (a -> Bool) -> [a] -> [([a], Maybe [a])]
sections start stop as = case (bs,cs) of
    ([], [])  -> []    
    (bs, [])  -> [(bs, Nothing)]
    (bs, [c]) -> [(bs, Nothing)]
    (bs, cs)  -> (bs, Just $ tail cs) : sections start stop (drop skip as)
    where
        (bs,cs) = sections1 start stop as                       
        skip    = length bs + length cs + 1

sections1 :: (a -> Bool) -> (a -> Bool) -> [a] -> ([a],[a])
sections1 start stop as = 
    (takeWhile (not . start) as, takeWhile (not . stop) $ dropWhile (not . start) as)


first  f (a, b) = (f a, b)
second f (a, b) = (a, f b)

trimEnd :: String -> String
trimEnd = List.dropWhileEnd Char.isSpace

secondM :: Monad m => (a -> m b) -> (c, a) -> m (c, b)
secondM f (a, b) = do
    b' <-  f b
    return (a, b')
    
or' :: (a -> Bool) -> (a -> Bool) -> a -> Bool
or' p q x = p x || q x