2 Module : Text.PrettyPrint.Boxes
3 Copyright : (c) Brent Yorgey 2009
4 License : BSD-style (see LICENSE)
5 Maintainer : David.Feuer@gmail.com
6 Stability : experimental
9 A pretty-printing library for laying out text in two dimensions,
10 using a simple box model.
12 module Literate.Box where
14 import Control.Arrow (first, (***))
15 import Data.Foldable (Foldable (foldr), toList)
16 import Data.List (foldl', intersperse)
17 import Data.String (IsString (..), unwords, words)
18 import Prelude hiding (Word, (<>))
20 -- Use the build from GHC.Exts because GHC has some rules that make it faster.
21 import GHC.Exts (build)
23 {- | @'chunksOf' n@ splits a list into length-n pieces. The last
24 piece will be shorter if @n@ does not evenly divide the length of
25 the list. If @n <= 0@, @'chunksOf' n l@ returns an infinite list
26 of empty lists. For example:
28 Note that @'chunksOf' n []@ is @[]@, not @[[]]@. This is
29 intentional, and is consistent with a recursive definition of
30 'chunksOf'; it satisfies the property that
32 @chunksOf n xs ++ chunksOf n ys == chunksOf n (xs ++ ys)@
34 whenever @n@ evenly divides the length of @xs@.
36 chunksOf :: Int -> [e] -> [[e]]
37 chunksOf i ls = map (take i) (build (splitter ls))
39 splitter :: [e] -> ([e] -> a -> a) -> a -> a
41 splitter l c n = l `c` splitter (drop i l) c n
43 {- | The basic data type. A box has a specified size and some sort of
53 -- | Contents of a box.
59 | -- | A row of sub-boxes.
61 | -- | A column of sub-boxes.
63 | -- | A sub-box with a specified alignment.
64 SubBox Alignment Alignment Box
67 -- | Convenient ability to use bare string literals as boxes.
68 instance IsString Box where
71 -- | Data type for specifying the alignment of boxes.
73 = -- | Align at the top/left.
75 | -- | Centered, biased to the top/left.
77 | -- | Centered, biased to the bottom/right.
79 | -- | Align at the bottom/right.
81 deriving (Eq, Read, Show)
83 {- | The null box, which has no content and no size. It is quite
87 nullBox = emptyBox 0 0
89 {- | @emptyBox r c@ is an empty box with @r@ rows and @c@ columns.
90 Useful for effecting more fine-grained positioning of other
91 boxes, by inserting empty boxes of the desired size in between
94 emptyBox :: Int -> Int -> Box
95 emptyBox r c = Box r c Blank
97 -- | A @1x1@ box containing a single character.
99 char c = Box 1 1 (Text [c])
101 -- | A (@1 x len@) box containing a string of length @len@.
102 text :: String -> Box
103 text t = Box 1 (length t) (Text t)
105 {- | Paste two boxes together horizontally, using a default (top)
108 (<>) :: Box -> Box -> Box
109 l <> r = hcat AlignTopLeft [l, r]
111 {- | Paste two boxes together horizontally with a single intervening
112 column of space, using a default (top) alignment.
114 (<+>) :: Box -> Box -> Box
115 l <+> r = hcat AlignTopLeft [l, emptyBox 0 1, r]
117 {- | Paste two boxes together vertically, using a default (left)
120 (//) :: Box -> Box -> Box
121 t // b = vcat AlignTopLeft [t, b]
123 {- | Paste two boxes together vertically with a single intervening row
124 of space, using a default (left) alignment.
126 (/+/) :: Box -> Box -> Box
127 t /+/ b = vcat AlignTopLeft [t, emptyBox 1 0, b]
129 -- | Glue a list of boxes together horizontally, with the given alignment.
130 hcat :: Foldable f => Alignment -> f Box -> Box
131 hcat a bs = Box h w (Row $ map (alignVert a h) bsl)
133 (w, h) = sumMax cols 0 rows bsl
136 {- | @hsep sep a bs@ lays out @bs@ horizontally with alignment @a@,
137 with @sep@ amount of space in between each.
139 hsep :: Foldable f => Int -> Alignment -> f Box -> Box
140 hsep sep a bs = punctuateH a (emptyBox 0 sep) bs
142 -- | Glue a list of boxes together vertically, with the given alignment.
143 vcat :: Foldable f => Alignment -> f Box -> Box
144 vcat a bs = Box h w (Col $ map (alignHoriz a w) bsl)
146 (h, w) = sumMax rows 0 cols bsl
149 -- Calculate a sum and a maximum over a list in one pass. If the list is
150 -- empty, the maximum is reported as the given default. This would
151 -- normally be done using the foldl library, but we don't want that
153 sumMax :: (Num n, Ord b, Foldable f) => (a -> n) -> b -> (a -> b) -> f a -> (n, b)
154 sumMax f defaultMax g as = foldr go (,) as 0 defaultMax
156 go a r n b = (r $! f a + n) $! g a `max` b
158 {- | @vsep sep a bs@ lays out @bs@ vertically with alignment @a@,
159 with @sep@ amount of space in between each.
161 vsep :: Foldable f => Int -> Alignment -> f Box -> Box
162 vsep sep a bs = punctuateV a (emptyBox sep 0) (toList bs)
164 {- | @punctuateH a p bs@ horizontally lays out the boxes @bs@ with a
165 copy of @p@ interspersed between each.
167 punctuateH :: Foldable f => Alignment -> Box -> f Box -> Box
168 punctuateH a p bs = hcat a (intersperse p (toList bs))
170 -- | A vertical version of 'punctuateH'.
171 punctuateV :: Foldable f => Alignment -> Box -> f Box -> Box
172 punctuateV a p bs = vcat a (intersperse p (toList bs))
174 --------------------------------------------------------------------------------
175 -- Paragraph flowing ---------------------------------------------------------
176 --------------------------------------------------------------------------------
178 {- | @para algn w t@ is a box of width @w@, containing text @t@,
179 aligned according to @algn@, flowed to fit within the given
182 para :: Alignment -> Int -> String -> Box
183 para a n t = (\ss -> mkParaBox a (length ss) ss) $ flow n t
185 {- | @columns w h t@ is a list of boxes, each of width @w@ and height
186 at most @h@, containing text @t@ flowed into as many columns as
189 columns :: Alignment -> Int -> Int -> String -> [Box]
190 columns a w h t = map (mkParaBox a h) . chunksOf h $ flow w t
192 {- | @mkParaBox a n s@ makes a box of height @n@ with the text @s@
193 aligned according to @a@.
195 mkParaBox :: Alignment -> Int -> [String] -> Box
196 mkParaBox a n = alignVert AlignTopLeft n . vcat a . map text
198 -- | Flow the given text into the given width.
199 flow :: Int -> String -> [String]
203 $ foldl' addWordP (emptyPara n) (map mkWord . words $ t)
207 , paraContent :: ParaContent
209 data ParaContent = Block
210 { fullLines :: [Line]
214 emptyPara :: Int -> Para
215 emptyPara pw = Para pw (Block [] (Line 0 []))
217 getLines :: Para -> [String]
218 getLines (Para _ (Block ls l))
219 | lLen l == 0 = process ls
220 | otherwise = process (l : ls)
222 process = map (unwords . reverse . map getWord . getWords) . reverse
224 data Line = Line {lLen :: Int, getWords :: [Word]}
226 mkLine :: [Word] -> Line
227 mkLine ws = Line (sum (map ((+ 1) . wLen) ws) - 1) ws
229 startLine :: Word -> Line
230 startLine = mkLine . (: [])
232 data Word = Word {wLen :: Int, getWord :: String}
234 mkWord :: String -> Word
235 mkWord w = Word (length w) w
237 addWordP :: Para -> Word -> Para
238 addWordP (Para pw (Block fl l)) w
239 | wordFits pw w l = Para pw (Block fl (addWordL w l))
240 | otherwise = Para pw (Block (l : fl) (startLine w))
242 addWordL :: Word -> Line -> Line
243 addWordL w (Line len ws) = Line (len + wLen w + 1) (w : ws)
245 wordFits :: Int -> Word -> Line -> Bool
246 wordFits pw w l = lLen l == 0 || lLen l + wLen w + 1 <= pw
248 --------------------------------------------------------------------------------
249 -- Alignment -----------------------------------------------------------------
250 --------------------------------------------------------------------------------
252 {- | @alignHoriz algn n bx@ creates a box of width @n@, with the
253 contents and height of @bx@, horizontally aligned according to
256 alignHoriz :: Alignment -> Int -> Box -> Box
257 alignHoriz a c b = align a AlignTopLeft (rows b) c b
259 {- | @alignVert algn n bx@ creates a box of height @n@, with the
260 contents and width of @bx@, vertically aligned according to
263 alignVert :: Alignment -> Int -> Box -> Box
264 alignVert a r b = align AlignTopLeft a r (cols b) b
266 {- | @align ah av r c bx@ creates an @r@ x @c@ box with the contents
267 of @bx@, aligned horizontally according to @ah@ and vertically
270 align :: Alignment -> Alignment -> Int -> Int -> Box -> Box
271 align ah av r c = Box r c . SubBox ah av
273 {- | Move a box \"up\" by putting it in a larger box with extra rows,
274 aligned to the top. See the disclaimer for 'moveLeft'.
276 moveUp :: Int -> Box -> Box
277 moveUp n b = alignVert AlignTopLeft (rows b + n) b
279 {- | Move a box down by putting it in a larger box with extra rows,
280 aligned to the bottom. See the disclaimer for 'moveLeft'.
282 moveDown :: Int -> Box -> Box
283 moveDown n b = alignVert AlignLast (rows b + n) b
285 {- | Move a box left by putting it in a larger box with extra columns,
286 aligned left. Note that the name of this function is
287 something of a white lie, as this will only result in the box
288 being moved left by the specified amount if it is already in a
289 larger right-aligned context.
291 moveLeft :: Int -> Box -> Box
292 moveLeft n b = alignHoriz AlignTopLeft (cols b + n) b
294 {- | Move a box right by putting it in a larger box with extra
295 columns, aligned right. See the disclaimer for 'moveLeft'.
297 moveRight :: Int -> Box -> Box
298 moveRight n b = alignHoriz AlignLast (cols b + n) b
300 --------------------------------------------------------------------------------
301 -- Implementation ------------------------------------------------------------
302 --------------------------------------------------------------------------------
304 {- | Render a 'Box' as a String, suitable for writing to the screen or
307 render :: Box -> String
308 render = unlines . renderBox
310 -- XXX make QC properties for takeP
312 {- | \"Padded take\": @takeP a n xs@ is the same as @take n xs@, if @n
313 <= length xs@; otherwise it is @xs@ followed by enough copies of
314 @a@ to make the length equal to @n@.
316 takeP :: a -> Int -> [a] -> [a]
317 takeP _ n _ | n <= 0 = []
318 takeP b n [] = replicate n b
319 takeP b n (x : xs) = x : takeP b (n -1) xs
321 {- | @takePA @ is like 'takeP', but with alignment. That is, we
322 imagine a copy of @xs@ extended infinitely on both sides with
323 copies of @a@, and a window of size @n@ placed so that @xs@ has
324 the specified alignment within the window; @takePA algn a n xs@
325 returns the contents of this window.
327 takePA :: Alignment -> a -> Int -> [a] -> [a]
328 takePA c b n = glue . (takeP b (numRev c n) *** takeP b (numFwd c n)) . split
330 split t = first reverse . splitAt (numRev c (length t)) $ t
331 glue = uncurry (++) . first reverse
332 numFwd AlignTopLeft n = n
333 numFwd AlignLast _ = 0
334 numFwd AlignCenter1 n = n `div` 2
335 numFwd AlignCenter2 n = (n + 1) `div` 2
336 numRev AlignTopLeft _ = 0
337 numRev AlignLast n = n
338 numRev AlignCenter1 n = (n + 1) `div` 2
339 numRev AlignCenter2 n = n `div` 2
341 -- | Generate a string of spaces.
342 blanks :: Int -> String
343 blanks = flip replicate ' '
345 -- | Render a box as a list of lines.
346 renderBox :: Box -> [String]
347 renderBox (Box r c Blank) = resizeBox r c [""]
348 renderBox (Box r c (Text t)) = resizeBox r c [t]
349 renderBox (Box r c (Row bs)) =
352 . map (renderBoxWithRows r)
355 merge = foldr (zipWith (++)) (repeat [])
356 renderBox (Box r c (Col bs)) =
358 . concatMap (renderBoxWithCols c)
360 renderBox (Box r c (SubBox ha va b)) =
361 resizeBoxAligned r c ha va
365 -- | Render a box as a list of lines, using a given number of rows.
366 renderBoxWithRows :: Int -> Box -> [String]
367 renderBoxWithRows r b = renderBox (b{rows = r})
369 -- | Render a box as a list of lines, using a given number of columns.
370 renderBoxWithCols :: Int -> Box -> [String]
371 renderBoxWithCols c b = renderBox (b{cols = c})
373 -- | Resize a rendered list of lines.
374 resizeBox :: Int -> Int -> [String] -> [String]
375 resizeBox r c = takeP (blanks c) r . map (takeP ' ' c)
377 -- | Resize a rendered list of lines, using given alignments.
378 resizeBoxAligned :: Int -> Int -> Alignment -> Alignment -> [String] -> [String]
379 resizeBoxAligned r c ha va = takePA va (blanks c) r . map (takePA ha ' ' c)
381 -- | A convenience function for rendering a box to stdout.
382 printBox :: Box -> IO ()
383 printBox = putStr . render
386 -- | Align boxes along their tops.
390 -- | Align boxes along their bottoms.
394 -- | Align boxes to the left.
398 -- | Align boxes to the right.
402 {- | Align boxes centered, but biased to the left/top in case of
406 center1 = AlignCenter1
408 {- | Align boxes centered, but biased to the right/bottom in case of
412 center2 = AlignCenter2