-- | The builder monoid from BlazeHtml.
--
-- Usage is fairly straightforward. Builders can be constructed from many
-- values, including 'String' and 'Text' values.
--
-- > strings :: [String]
-- > strings = replicate 10000 "Hello there!"
--
-- Concatenation should happen through the 'Monoid' interface.
--
-- > concatenation :: Builder
-- > concatenation = mconcat $ map fromString strings
--
-- There is only one way to efficiently obtain the result: to convert the
-- 'Builder' to a lazy 'L.ByteString' using 'toLazyByteString'.
--
-- > result :: L.ByteString
-- > result = toLazyByteString concatenation
--
{-# LANGUAGE BangPatterns, OverloadedStrings #-}
module Text.Blaze.Builder.Core
(
-- * Main builder type
Builder
-- * Custom writes to the builder
, Write (..)
, writeByte
, writeByteString
, writeSingleton
, writeList
-- * Creating builders
, singleton
, fromByteString
-- * Extracting the result from a builder
, toLazyByteString
) where
import Foreign
import Data.Monoid (Monoid, mempty, mappend, mconcat)
import qualified Data.ByteString.Char8 ()
import qualified Data.ByteString as S
import qualified Data.ByteString.Internal as S
import qualified Data.ByteString.Lazy as L
-- | Main builder type. It simply contains a function to extract the actual
-- data.
--
newtype Builder = Builder (BuildStep -> BuildStep)
-- | A buildsignal is a signal returned from a write to the builder, it tells us
-- what should happen next.
--
data BuildSignal
-- | Signal the completion of the write process.
= Done {-# UNPACK #-} !(Ptr Word8) -- ^ Pointer to the next free byte
-- | Signal that the buffer is full and a new one needs to be allocated.
-- It contains the minimal size required for the next buffer, a pointer to the
-- next free byte, and a continuation.
| BufferFull
{-# UNPACK #-} !Int
{-# UNPACK #-} !(Ptr Word8)
{-# UNPACK #-} !BuildStep
-- | Type for a single build step. Every build step checks that
--
-- > free + bytes-written <= last
--
type BuildStep = Ptr Word8 -- ^ Ptr to the next free byte in the buffer
-> Ptr Word8 -- ^ Ptr to the first byte AFTER the buffer
-> IO BuildSignal -- ^ Signal the next step to be taken
instance Monoid Builder where
mempty = Builder id
{-# INLINE mempty #-}
mappend (Builder f) (Builder g) = Builder $ f . g
{-# INLINE mappend #-}
mconcat = foldr mappend mempty
{-# INLINE mconcat #-}
-- | Write abstraction so we can avoid some gory and bloody details. A write
-- abstration holds the exact size of the write in bytes, and a function to
-- carry out the write operation.
--
data Write = Write
{-# UNPACK #-} !Int
(Ptr Word8 -> IO ())
-- A monoid interface for the write actions.
instance Monoid Write where
mempty = Write 0 (const $ return ())
{-# INLINE mempty #-}
mappend (Write l1 f1) (Write l2 f2) = Write (l1 + l2) $ \ptr -> do
f1 ptr
f2 (ptr `plusPtr` l1)
{-# INLINE mappend #-}
-- | Write a single byte.
--
writeByte :: Word8 -- ^ Byte to write
-> Write -- ^ Resulting write
writeByte x = Write 1 (\pf -> poke pf x)
{-# INLINE writeByte #-}
-- | Write a strict 'S.ByteString'.
--
writeByteString :: S.ByteString -- ^ 'S.ByteString' to write
-> Write -- ^ Resulting write
writeByteString bs = Write l io
where
(fptr, o, l) = S.toForeignPtr bs
io pf = withForeignPtr fptr $ \p -> copyBytes pf (p `plusPtr` o) l
{-# INLINE writeByteString #-}
-- | Construct a 'Builder' from a single 'Write' abstraction.
--
writeSingleton :: (a -> Write) -- ^ 'Write' abstraction
-> a -- ^ Actual value to write
-> Builder -- ^ Resulting 'Builder'
writeSingleton write = makeBuilder
where
makeBuilder x = Builder step
where
step k pf pe
| pf `plusPtr` size <= pe = do
io pf
let pf' = pf `plusPtr` size
pf' `seq` k pf' pe
| otherwise = return $ BufferFull size pf (step k)
where
Write size io = write x
{-# INLINE writeSingleton #-}
-- | Construct a builder writing a list of data from a write abstraction.
--
writeList :: (a -> Write) -- ^ 'Write' abstraction
-> [a] -- ^ List of values to write
-> Builder -- ^ Resulting 'Builder'
writeList write = makeBuilder
where
makeBuilder [] = mempty
makeBuilder xs0 = Builder $ step xs0
where
step xs1 k pf0 pe0 = go xs1 pf0
where
go [] !pf = k pf pe0
go xs@(x':xs') !pf
| pf `plusPtr` size <= pe0 = do
io pf
go xs' (pf `plusPtr` size)
| otherwise = do return $ BufferFull size pf (step xs k)
where
Write size io = write x'
{-# INLINE writeList #-}
-- | Construct a 'Builder' from a single byte.
--
singleton :: Word8 -- ^ Byte to create a 'Builder' from
-> Builder -- ^ Resulting 'Builder'
singleton = writeSingleton writeByte
-- | /O(n)./ A Builder taking a 'S.ByteString`, copying it.
--
fromByteString :: S.ByteString -- ^ Strict 'S.ByteString' to copy
-> Builder -- ^ Resulting 'Builder'
fromByteString = writeSingleton writeByteString
{-# INLINE fromByteString #-}
-- | Copied from Data.ByteString.Lazy.
--
defaultSize :: Int
defaultSize = 32 * k - overhead
where k = 1024
overhead = 2 * sizeOf (undefined :: Int)
-- | Run the builder with the default buffer size.
--
runBuilder :: Builder -> [S.ByteString] -> [S.ByteString]
runBuilder = runBuilderWith defaultSize
{-# INLINE runBuilder #-}
-- | Run the builder with buffers of at least the given size.
--
-- Note that the builders should guarantee that on average the desired buffer
-- size is attained almost perfectly. "Almost" because builders may decide to
-- start a new buffer and not completely fill the existing buffer, if this is
-- faster. However, they should not spill too much of the buffer, if they
-- cannot compensate for it.
--
runBuilderWith :: Int -> Builder -> [S.ByteString] -> [S.ByteString]
runBuilderWith bufSize (Builder b) k =
S.inlinePerformIO $ go bufSize (b finalStep)
where
finalStep pf _ = return $ Done pf
go !size !step = do
buf <- S.mallocByteString size
withForeignPtr buf $ \pf -> do
next <- step pf (pf `plusPtr` size)
case next of
Done pf'
| pf == pf' -> return k
| otherwise -> return $ S.PS buf 0 (pf' `minusPtr` pf) : k
BufferFull newSize pf' nextStep
| pf == pf' -> bufferFullError
| otherwise -> return $ S.PS buf 0 (pf' `minusPtr` pf) :
S.inlinePerformIO (go (max newSize bufSize) nextStep)
bufferFullError =
error "runBuilder: buffer cannot be full; no data was written."
-- | /O(n)./ Extract the lazy 'L.ByteString' from the builder.
--
toLazyByteString :: Builder -- ^ 'Builder' to evaluate
-> L.ByteString -- ^ Resulting UTF-8 encoded 'L.ByteString'
toLazyByteString = L.fromChunks . flip runBuilder []
{-# INLINE toLazyByteString #-}