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 $ ""
-- -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