1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
|
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
module Text.ProjectTemplate
( -- * Create a template
createTemplate
-- * Unpack a template
, unpackTemplate
-- ** Receivers
, FileReceiver
, receiveMem
, receiveFS
-- * Exceptions
, ProjectTemplateException (..)
) where
import Control.Exception (Exception, assert)
import Control.Monad (unless)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Resource (MonadResource, MonadThrow,
throwM)
import Control.Monad.Writer (MonadWriter, tell)
import Data.ByteString (ByteString)
import qualified Data.ByteString as S
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Lazy as L
import Data.Conduit (ConduitM, await,
awaitForever, leftover, yield,
runConduit, (.|))
import qualified Data.Conduit.Binary as CB
import Data.Conduit.List (consume, sinkNull)
import Conduit (concatMapC, chunksOfCE)
import qualified Data.Conduit.List as CL
import qualified Data.Conduit.Text as CT
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Data.Typeable (Typeable)
import Data.Void (Void)
import System.Directory (createDirectoryIfMissing)
import System.FilePath (takeDirectory, (</>))
-- | Create a template file from a stream of file/contents combinations.
--
-- Since 0.1.0
createTemplate
:: Monad m => ConduitM (FilePath, m ByteString) ByteString m ()
createTemplate = awaitForever $ \(fp, getBS) -> do
bs <- lift getBS
case runConduit $ yield bs .| CT.decode CT.utf8 .| sinkNull of
Nothing -> do
yield "{-# START_FILE BASE64 "
yield $ encodeUtf8 $ T.pack fp
yield " #-}\n"
yield (B64.encode bs) .| chunksOfCE 76 .| concatMapC (\x -> [x, "\n"])
yield "\n"
Just _ -> do
yield "{-# START_FILE "
yield $ encodeUtf8 $ T.pack fp
yield " #-}\n"
yield bs
yield "\n"
-- | Unpack a template to some destination. Destination is provided by the
-- first argument.
--
-- The second argument allows you to modify the incoming stream, usually to
-- replace variables. For example, to replace PROJECTNAME with myproject, you
-- could use:
--
-- > Data.Text.replace "PROJECTNAME" "myproject"
--
-- Note that this will affect both file contents and file names.
--
-- Since 0.1.0
unpackTemplate
:: MonadThrow m
=> (FilePath -> ConduitM ByteString o m ()) -- ^ receive individual files
-> (Text -> Text) -- ^ fix each input line, good for variables
-> ConduitM ByteString o m ()
unpackTemplate perFile fixLine =
CT.decode CT.utf8 .| CT.lines .| CL.map fixLine .| start
where
start =
await >>= maybe (return ()) go
where
go t =
case getFileName t of
Nothing -> lift $ throwM $ InvalidInput t
Just (fp', isBinary) -> do
let src
| isBinary = binaryLoop .| decode64
| otherwise = textLoop True
src .| perFile (T.unpack fp')
start
binaryLoop = do
await >>= maybe (return ()) go
where
go t =
case getFileName t of
Just{} -> leftover t
Nothing -> do
yield $ encodeUtf8 t
binaryLoop
textLoop isFirst =
await >>= maybe (return ()) go
where
go t =
case getFileName t of
Just{} -> leftover t
Nothing -> do
unless isFirst $ yield "\n"
yield $ encodeUtf8 t
textLoop False
getFileName t =
case T.words t of
["{-#", "START_FILE", fn, "#-}"] -> Just (fn, False)
["{-#", "START_FILE", "BASE64", fn, "#-}"] -> Just (fn, True)
_ -> Nothing
-- | The first argument to 'unpackTemplate', specifying how to receive a file.
--
-- Since 0.1.0
type FileReceiver m = FilePath -> ConduitM ByteString Void m ()
-- | Receive files to the given folder on the filesystem.
--
-- > unpackTemplate (receiveFS "some-destination") (T.replace "PROJECTNAME" "foo")
--
-- Since 0.1.0
receiveFS :: MonadResource m
=> FilePath -- ^ root
-> FileReceiver m
receiveFS root rel = do
liftIO $ createDirectoryIfMissing True $ takeDirectory fp
CB.sinkFile fp
where
fp = root </> rel
-- | Receive files to a @Writer@ monad in memory.
--
-- > execWriter $ runExceptionT_ $ src $$ unpackTemplate receiveMem id
--
-- Since 0.1.0
receiveMem :: MonadWriter (Map FilePath L.ByteString) m
=> FileReceiver m
receiveMem fp = do
bss <- consume
lift $ tell $ Map.singleton fp $ L.fromChunks bss
-- | Exceptions that can be thrown.
--
-- Since 0.1.0
data ProjectTemplateException = InvalidInput Text
| BinaryLoopNeedsOneLine
deriving (Show, Typeable)
instance Exception ProjectTemplateException
decode64 :: Monad m => ConduitM ByteString ByteString m ()
decode64 = codeWith 4 B64.decodeLenient
codeWith :: Monad m => Int -> (ByteString -> ByteString) -> ConduitM ByteString ByteString m ()
codeWith size f =
loop
where
loop = await >>= maybe (return ()) push
loopWith bs
| S.null bs = loop
| otherwise = await >>= maybe (yield (f bs)) (pushWith bs)
push bs = do
let (x, y) = S.splitAt (len - (len `mod` size)) bs
unless (S.null x) $ yield $ f x
loopWith y
where
len = S.length bs
pushWith bs1 bs2 | S.length bs1 + S.length bs2 < size = loopWith (S.append bs1 bs2)
pushWith bs1 bs2 = assertion1 $ assertion2 $ do
yield $ f bs1'
push y
where
m = S.length bs1 `mod` size
(x, y) = S.splitAt (size - m) bs2
bs1' = S.append bs1 x
assertion1 = assert $ S.length bs1 < size
assertion2 = assert $ S.length bs1' `mod` size == 0
|