boxes-0.1.1: Text/PrettyPrint/Boxes.hs
{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
-- |
-- Module : Text.PrettyPrint.Boxes
-- Copyright : (c) Brent Yorgey 2009
-- License : BSD-style (see LICENSE)
-- Maintainer : byorgey@cis.upenn.edu
-- Stability : experimental
-- Portability : portable
--
-- A pretty-printing library for laying out text in two dimensions,
-- using a simple box model.
--
-----------------------------------------------------------------------------
module Text.PrettyPrint.Boxes
( -- * Constructing boxes
Box
, nullBox
, emptyBox
, char
, text
, para
, columns
-- * Layout of boxes
, (<>)
, (<+>)
, hcat
, hsep
, (//)
, (/+/)
, vcat
, vsep
, punctuateH, punctuateV
-- * Alignment
, Alignment
, left, right
, top, bottom
, center1, center2
, moveLeft
, moveRight
, moveUp
, moveDown
, alignHoriz
, alignVert
, align
-- * Rendering boxes
, render
, printBox
) where
import Data.String
import Control.Arrow ((***), first)
import Data.List (foldl', intersperse)
import Data.List.Split
-- | The basic data type. A box has a specified size and some sort of
-- contents.
data Box = Box { rows :: Int
, cols :: Int
, content :: Content
}
deriving (Show)
-- | Convenient ability to use bare string literals as boxes.
instance IsString Box where
fromString = text
-- | Data type for specifying the alignment of boxes.
data Alignment = AlignFirst -- ^ Align at the top/left.
| AlignCenter1 -- ^ Centered, biased to the top/left.
| AlignCenter2 -- ^ Centered, biased to the bottom/right.
| AlignLast -- ^ Align at the bottom/right.
deriving (Eq, Read, Show)
-- | Align boxes along their tops.
top :: Alignment
top = AlignFirst
-- | Align boxes along their bottoms.
bottom :: Alignment
bottom = AlignLast
-- | Align boxes to the left.
left :: Alignment
left = AlignFirst
-- | Align boxes to the right.
right :: Alignment
right = AlignLast
-- | Align boxes centered, but biased to the left/top in case of
-- unequal parities.
center1 :: Alignment
center1 = AlignCenter1
-- | Align boxes centered, but biased to the right/bottom in case of
-- unequal parities.
center2 :: Alignment
center2 = AlignCenter2
-- | Contents of a box.
data Content = Blank -- ^ No content.
| Text String -- ^ A raw string.
| Row [Box] -- ^ A row of sub-boxes.
| Col [Box] -- ^ A column of sub-boxes.
| SubBox Alignment Alignment Box
-- ^ A sub-box with a specified alignment.
deriving (Show)
-- | The null box, which has no content and no size. It is quite
-- useless.
nullBox :: Box
nullBox = emptyBox 0 0
-- | @emptyBox r c@ is an empty box with @r@ rows and @c@ columns.
-- Useful for effecting more fine-grained positioning of other
-- boxes, by inserting empty boxes of the desired size in between
-- them.
emptyBox :: Int -> Int -> Box
emptyBox r c = Box r c Blank
-- | A @1x1@ box containing a single character.
char :: Char -> Box
char c = Box 1 1 (Text [c])
-- | A (@1 x len@) box containing a string of length @len@.
text :: String -> Box
text t = Box 1 (length t) (Text t)
-- | Paste two boxes together horizontally, using a default (top)
-- alignment.
(<>) :: Box -> Box -> Box
l <> r = hcat top [l,r]
-- | Paste two boxes together horizontally with a single intervening
-- column of space, using a default (top) alignment.
(<+>) :: Box -> Box -> Box
l <+> r = hcat top [l, emptyBox 0 1, r]
-- | Paste two boxes together vertically, using a default (left)
-- alignment.
(//) :: Box -> Box -> Box
t // b = vcat left [t,b]
-- | Paste two boxes together vertically with a single intervening row
-- of space, using a default (left) alignment.
(/+/) :: Box -> Box -> Box
t /+/ b = vcat left [t, emptyBox 1 0, b]
-- | Glue a list of boxes together horizontally, with the given alignment.
hcat :: Alignment -> [Box] -> Box
hcat a bs = Box h w (Row $ map (alignVert a h) bs)
where h = maximum . (0:) . map rows $ bs
w = sum . map cols $ bs
-- | @hsep sep a bs@ lays out @bs@ horizontally with alignment @a@,
-- with @sep@ amount of space in between each.
hsep :: Int -> Alignment -> [Box] -> Box
hsep sep a bs = punctuateH a (emptyBox 0 sep) bs
-- | Glue a list of boxes together vertically, with the given alignment.
vcat :: Alignment -> [Box] -> Box
vcat a bs = Box h w (Col $ map (alignHoriz a w) bs)
where h = sum . map rows $ bs
w = maximum . (0:) . map cols $ bs
-- | @vsep sep a bs@ lays out @bs@ vertically with alignment @a@,
-- with @sep@ amount of space in between each.
vsep :: Int -> Alignment -> [Box] -> Box
vsep sep a bs = punctuateV a (emptyBox sep 0) bs
-- | @punctuateH a p bs@ horizontally lays out the boxes @bs@ with a
-- copy of @p@ interspersed between each.
punctuateH :: Alignment -> Box -> [Box] -> Box
punctuateH a p bs = hcat a (intersperse p bs)
-- | A vertical version of 'punctuateH'.
punctuateV :: Alignment -> Box -> [Box] -> Box
punctuateV a p bs = vcat a (intersperse p bs)
--------------------------------------------------------------------------------
-- Paragraph flowing ---------------------------------------------------------
--------------------------------------------------------------------------------
-- | @para algn w t@ is a box of width @w@, containing text @t@,
-- aligned according to @algn@, flowed to fit within the given
-- width.
para :: Alignment -> Int -> String -> Box
para a n t = (\ss -> mkParaBox a (length ss) ss) $ flow n t
-- | @columns w h t@ is a list of boxes, each of width @w@ and height
-- at most @h@, containing text @t@ flowed into as many columns as
-- necessary.
columns :: Alignment -> Int -> Int -> String -> [Box]
columns a w h t = map (mkParaBox a h) . chunk h $ flow w t
-- | @mkParaBox a n s@ makes a box of height @n@ with the text @s@
-- aligned according to @a@.
mkParaBox :: Alignment -> Int -> [String] -> Box
mkParaBox a n = alignVert top n . vcat a . map text
-- | Flow the given text into the given width.
flow :: Int -> String -> [String]
flow n t = map (take n)
. getLines
$ foldl' addWordP (emptyPara n) (map mkWord . words $ t)
data Para = Para { paraWidth :: Int
, paraContent :: ParaContent
}
data ParaContent = Block { fullLines :: [Line]
, lastLine :: Line
}
emptyPara :: Int -> Para
emptyPara pw = Para pw (Block [] (Line 0 []))
getLines :: Para -> [String]
getLines (Para _ (Block ls l))
| lLen l == 0 = process ls
| otherwise = process (l:ls)
where process = map (unwords . reverse . map getWord . getWords) . reverse
data Line = Line { lLen :: Int, getWords :: [Word] }
mkLine :: [Word] -> Line
mkLine ws = Line (sum (map wLen ws) + length ws - 1) ws
startLine :: Word -> Line
startLine = mkLine . (:[])
data Word = Word { wLen :: Int, getWord :: String }
mkWord :: String -> Word
mkWord w = Word (length w) w
addWordP :: Para -> Word -> Para
addWordP (Para pw (Block fl l)) w
| wordFits pw w l = Para pw (Block fl (addWordL w l))
| otherwise = Para pw (Block (l:fl) (startLine w))
addWordL :: Word -> Line -> Line
addWordL w (Line len ws) = Line (len + wLen w + 1) (w:ws)
wordFits :: Int -> Word -> Line -> Bool
wordFits pw w l = lLen l == 0 || lLen l + wLen w + 1 <= pw
--------------------------------------------------------------------------------
-- Alignment -----------------------------------------------------------------
--------------------------------------------------------------------------------
-- | @alignHoriz algn n bx@ creates a box of width @n@, with the
-- contents and height of @bx@, horizontally aligned according to
-- @algn@.
alignHoriz :: Alignment -> Int -> Box -> Box
alignHoriz a c b = Box (rows b) c $ SubBox a AlignFirst b
-- | @alignVert algn n bx@ creates a box of height @n@, with the
-- contents and width of @bx@, vertically aligned according to
-- @algn@.
alignVert :: Alignment -> Int -> Box -> Box
alignVert a r b = Box r (cols b) $ SubBox AlignFirst a b
-- | @align ah av r c bx@ creates an @r@ x @c@ box with the contents
-- of @bx@, aligned horizontally according to @ah@ and vertically
-- according to @av@.
align :: Alignment -> Alignment -> Int -> Int -> Box -> Box
align ah av r c = Box r c . SubBox ah av
-- | Move a box \"up\" by putting it in a larger box with extra rows,
-- aligned to the top. See the disclaimer for 'moveLeft'.
moveUp :: Int -> Box -> Box
moveUp n b = alignVert top (rows b + n) b
-- | Move a box down by putting it in a larger box with extra rows,
-- aligned to the bottom. See the disclaimer for 'moveLeft'.
moveDown :: Int -> Box -> Box
moveDown n b = alignVert bottom (rows b + n) b
-- | Move a box left by putting it in a larger box with extra columns,
-- aligned left. Note that the name of this function is
-- something of a white lie, as this will only result in the box
-- being moved left by the specified amount if it is already in a
-- larger right-aligned context.
moveLeft :: Int -> Box -> Box
moveLeft n b = alignHoriz left (cols b + n) b
-- | Move a box right by putting it in a larger box with extra
-- columns, aligned right. See the disclaimer for 'moveLeft'.
moveRight :: Int -> Box -> Box
moveRight n b = alignHoriz right (cols b + n) b
--------------------------------------------------------------------------------
-- Implementation ------------------------------------------------------------
--------------------------------------------------------------------------------
-- | Render a 'Box' as a String, suitable for writing to the screen or
-- a file.
render :: Box -> String
render = unlines . renderBox
-- XXX make QC properties for takeP
-- | \"Padded take\": @takeP a n xs@ is the same as @take n xs@, if @n
-- <= length xs@; otherwise it is @xs@ followed by enough copies of
-- @a@ to make the length equal to @n@.
takeP :: a -> Int -> [a] -> [a]
takeP _ n _ | n <= 0 = []
takeP b n [] = replicate n b
takeP b n (x:xs) = x : takeP b (n-1) xs
-- | @takePA @ is like 'takeP', but with alignment. That is, we
-- imagine a copy of @xs@ extended infinitely on both sides with
-- copies of @a@, and a window of size @n@ placed so that @xs@ has
-- the specified alignment within the window; @takePA algn a n xs@
-- returns the contents of this window.
takePA :: Alignment -> a -> Int -> [a] -> [a]
takePA c b n = glue . (takeP b (numRev c n) *** takeP b (numFwd c n)) . split
where split t = first reverse . splitAt (numRev c (length t)) $ t
glue = uncurry (++) . first reverse
numFwd AlignFirst n = n
numFwd AlignLast _ = 0
numFwd AlignCenter1 n = n `div` 2
numFwd AlignCenter2 n = (n+1) `div` 2
numRev AlignFirst _ = 0
numRev AlignLast n = n
numRev AlignCenter1 n = (n+1) `div` 2
numRev AlignCenter2 n = n `div` 2
-- | Generate a string of spaces.
blanks :: Int -> String
blanks = flip replicate ' '
-- | Render a box as a list of lines.
renderBox :: Box -> [String]
renderBox (Box r c Blank) = resizeBox r c [""]
renderBox (Box r c (Text t)) = resizeBox r c [t]
renderBox (Box r c (Row bs)) = resizeBox r c
. merge
. map (renderBoxWithRows r)
$ bs
where merge = foldr (zipWith (++)) (repeat [])
renderBox (Box r c (Col bs)) = resizeBox r c
. concatMap (renderBoxWithCols c)
$ bs
renderBox (Box r c (SubBox ha va b)) = resizeBoxAligned r c ha va
. renderBox
$ b
-- | Render a box as a list of lines, using a given number of rows.
renderBoxWithRows :: Int -> Box -> [String]
renderBoxWithRows r b = renderBox (b{rows = r})
-- | Render a box as a list of lines, using a given number of columns.
renderBoxWithCols :: Int -> Box -> [String]
renderBoxWithCols c b = renderBox (b{cols = c})
-- | Resize a rendered list of lines.
resizeBox :: Int -> Int -> [String] -> [String]
resizeBox r c = takeP (blanks c) r . map (takeP ' ' c)
-- | Resize a rendered list of lines, using given alignments.
resizeBoxAligned :: Int -> Int -> Alignment -> Alignment -> [String] -> [String]
resizeBoxAligned r c ha va = takePA va (blanks c) r . map (takePA ha ' ' c)
-- | A convenience function for rendering a box to stdout.
printBox :: Box -> IO ()
printBox = putStr . render