{-# LANGUAGE OverloadedStrings #-} -- | Header formatting and pretty-printing. module Network.Email.Header.Doc ( -- * Rendering options RenderOptions(..) , Encoding(..) , defaultRenderOptions -- * Rendering , Doc , render -- * Construction , prim , group , builder , string , byteString , text -- * Spacing , space , newline , line , linebreak , softline , softbreak , () -- * Combinators , sep , punctuate ) where import qualified Data.ByteString as B import Data.ByteString.Lazy.Builder (Builder) import qualified Data.ByteString.Lazy.Builder as B import qualified Data.ByteString.Lazy as LB import Data.List (intersperse) import Data.String import qualified Data.Text.Lazy as L import qualified Data.Text.Lazy.Encoding as L import Network.Email.Charset import Network.Email.Header.Layout (Layout) import qualified Network.Email.Header.Layout as F infixr 6 -- | Rendering options. data RenderOptions = RenderOptions { -- | The maximum line width. lineWidth :: Int -- | The indent of each line, in spaces. , indent :: Int -- | The charset used to encode text outside US-ASCII range. , charset :: Charset -- | The header encoding used for encoded words. , encoding :: Encoding } deriving (Eq, Show) -- | The encoding used for binary characters in an encoded word. data Encoding -- | Quoted-printable encoding. Spaces are represented with underscores, -- and undisplayable characters are represented as hex pairs. = QP -- | Base 64 encoding of all characters. | Base64 deriving (Eq, Ord, Read, Show, Enum, Bounded) -- | Default rendering options, which uses a line width of 80, and indent of 2, -- and utf-8 quated-printable encoding. defaultRenderOptions :: RenderOptions defaultRenderOptions = RenderOptions { lineWidth = 80 , indent = 2 , charset = defaultCharset , encoding = QP } -- | A formatted email header. data Doc = Empty | Prim Bool (RenderOptions -> Bool -> Layout Builder) | Cat Doc Doc | Union Doc Doc instance Semigroup Doc where (<>) = Cat instance Monoid Doc where mempty = Empty mappend = (<>) instance IsString Doc where fromString = string -- | Render a document with the given options and initial position. render :: RenderOptions -> Int -> Doc -> Builder render r i doc = F.layout i (go [doc]) where w = lineWidth r go [] = mempty go (d:ds) = case d of Empty -> go ds Prim h f -> f r h <> go ds Cat x y -> go (x:y:ds) Union x y -> F.nicest w (go (x:ds)) (go (y:ds)) -- | Construct a primitive document from a layout function. The function takes -- two parameters: the rendering options, and a 'Bool' which indicates -- whether the containing group is laid out horizontally instead of vertically. prim :: (RenderOptions -> Bool -> Layout Builder) -> Doc prim = Prim False -- | Flatten a layout, removing all line breaks. flatten :: Doc -> Doc flatten Empty = Empty flatten (Prim _ f) = Prim True f flatten (Cat x y) = Cat (flatten x) (flatten y) flatten (Union x _) = x -- | Specify an alternative layout with all line breaks flattened. group :: Doc -> Doc group x = Union (flatten x) x -- | Construct a 'Doc' from a 'B.Builder' and a length. builder :: Int -> Builder -> Doc builder k s = prim $ \_ _ -> F.span k s -- | Construct a 'Doc' from a 'String'. string :: String -> Doc string s = builder (length s) (B.string8 s) -- | Construct a 'Doc' from a 'B.ByteString'. byteString :: B.ByteString -> Doc byteString s = builder (B.length s) (B.byteString s) -- | Construct a 'Builder' from a 'L.Text'. text :: L.Text -> Doc text = byteString . LB.toStrict . L.encodeUtf8 -- | A space layout. space :: Layout Builder space = F.span 1 (B.char8 ' ') -- | A newline layout. This will emit a @CRLF@ pair, break to a new line, -- and indent. newline :: RenderOptions -> Layout Builder newline r = F.span 2 (B.byteString "\r\n") <> F.break 0 <> mconcat (replicate1 (indent r) space) where replicate1 n a = a : replicate (n - 1) a -- | A line break. If undone, behaves like a space. line :: Doc line = prim $ \r h -> if h then space else newline r -- | A line break. If undone, behaves like `mempty`. linebreak :: Doc linebreak = prim $ \r h -> if h then mempty else newline r -- | A space if the remaining layout fits, and a line break otherwise. softline :: Doc softline = group line -- | `mempty` if the remaining layout fits, and a line break otherwise. softbreak :: Doc softbreak = group linebreak -- | Concatenate with a 'softline' in between. () :: Doc -> Doc -> Doc a b = a <> softline <> b -- | Separate a list with spaces if it fits. Otherwise, separate with lines. sep :: [Doc] -> Doc sep = group . mconcat . intersperse line -- | @punctuate p xs@ appends @p@ to every element of @xs@ but the last. punctuate :: Monoid a => a -> [a] -> [a] punctuate p = go where go [] = [] go [x] = [x] go (x:xs) = x <> p : go xs