1 {-# LANGUAGE OverloadedStrings #-}
4 Module : Text.PrettyPrint.Boxes
5 Copyright : (c) Brent Yorgey 2009
6 License : BSD-style (see LICENSE)
7 Maintainer : David.Feuer@gmail.com
8 Stability : experimental
11 A pretty-printing library for laying out text in two dimensions,
12 using a simple box model.
14 module Literate.Box where
16 import Control.Arrow (first, (***))
17 import Data.Foldable (toList)
18 import Data.List (foldl', intersperse)
19 import Data.String (IsString (..))
20 import Prelude hiding (Word, (<>))
22 -- Use the build from GHC.Exts because GHC has some rules that make it faster.
23 import GHC.Exts (build)
25 {- | The basic data type. A box has a specified size and some sort of
35 -- | Contents of a box.
41 | -- | A row of sub-boxes.
43 | -- | A column of sub-boxes.
45 | -- | A sub-box with a specified alignment.
46 SubBox Alignment Alignment Box
49 instance IsString Box where
50 fromString t = Box 1 (length t) (Text t)
52 -- | Data type for specifying the alignment of boxes.
54 = -- | Align at the top/left.
56 | -- | Centered, biased to the top/left.
58 | -- | Centered, biased to the bottom/right.
59 AlignBottomRightCenter
60 | -- | Align at the bottom/right.
62 deriving (Eq, Read, Show)
64 {- | The null box, which has no content and no size. It is quite
68 nullBox = emptyBox 0 0
70 {- | @emptyBox r c@ is an empty box with @r@ rows and @c@ columns.
71 Useful for effecting more fine-grained positioning of other
72 boxes, by inserting empty boxes of the desired size in between
75 emptyBox :: Int -> Int -> Box
76 emptyBox r c = Box r c Blank
78 -- | A @1x1@ box containing a single character.
80 char c = Box 1 1 (Text [c])
82 {- | Paste two boxes together horizontally, using a default (top)
85 (<>) :: Box -> Box -> Box
86 l <> r = hcat AlignTopLeft [l, r]
88 {- | Paste two boxes together horizontally with a single intervening
89 column of space, using a default (top) alignment.
91 (<+>) :: Box -> Box -> Box
92 l <+> r = hcat AlignTopLeft [l, emptyBox 0 1, r]
94 {- | Paste two boxes together vertically, using a default (left)
97 (//) :: Box -> Box -> Box
98 t // b = vcat AlignTopLeft [t, b]
100 {- | Paste two boxes together vertically with a single intervening row
101 of space, using a default (left) alignment.
103 (/+/) :: Box -> Box -> Box
104 t /+/ b = vcat AlignTopLeft [t, emptyBox 1 0, b]
106 -- | Glue a list of boxes together horizontally, with the given alignment.
107 hcat :: Foldable f => Alignment -> f Box -> Box
108 hcat a bs = Box h w (Row $ map (alignVert a h) bsl)
110 (w, h) = sumMax cols 0 rows bsl
113 {- | @hsep sep a bs@ lays out @bs@ horizontally with alignment @a@,
114 with @sep@ amount of space in between each.
116 hsep :: Foldable f => Int -> Alignment -> f Box -> Box
117 hsep sep a bs = punctuateH a (emptyBox 0 sep) bs
119 -- | Glue a list of boxes together vertically, with the given alignment.
120 vcat :: Foldable f => Alignment -> f Box -> Box
121 vcat a bs = Box h w (Col $ map (alignHoriz a w) bsl)
123 (h, w) = sumMax rows 0 cols bsl
126 -- Calculate a sum and a maximum over a list in one pass. If the list is
127 -- empty, the maximum is reported as the given default. This would
128 -- normally be done using the foldl library, but we don't want that
130 sumMax :: (Num n, Ord b, Foldable f) => (a -> n) -> b -> (a -> b) -> f a -> (n, b)
131 sumMax f defaultMax g as = foldr go (,) as 0 defaultMax
133 go a r n b = (r $! f a + n) $! g a `max` b
135 {- | @vsep sep a bs@ lays out @bs@ vertically with alignment @a@,
136 with @sep@ amount of space in between each.
138 vsep :: Foldable f => Int -> Alignment -> f Box -> Box
139 vsep sep a bs = punctuateV a (emptyBox sep 0) (toList bs)
141 {- | @punctuateH a p bs@ horizontally lays out the boxes @bs@ with a
142 copy of @p@ interspersed between each.
144 punctuateH :: Foldable f => Alignment -> Box -> f Box -> Box
145 punctuateH a p bs = hcat a (intersperse p (toList bs))
147 -- | A vertical version of 'punctuateH'.
148 punctuateV :: Foldable f => Alignment -> Box -> f Box -> Box
149 punctuateV a p bs = vcat a (intersperse p (toList bs))
151 --------------------------------------------------------------------------------
152 -- Paragraph flowing ---------------------------------------------------------
153 --------------------------------------------------------------------------------
155 {- | @para algn w t@ is a box of width @w@, containing text @t@,
156 aligned according to @algn@, flowed to fit within the given
158 >>> para AlignTopLeft 10 "12 34 56 78 9 10 11 12"
159 Box {rows = 3, cols = 10, content = SubBox AlignTopLeft AlignTopLeft (Box {rows = 3, cols = 10, content = Col
160 [Box {rows = 1, cols = 10, content = SubBox AlignTopLeft AlignTopLeft (Box {rows = 1, cols = 8, content = Text "12 34 56"})}
161 ,Box {rows = 1, cols = 10, content = SubBox AlignTopLeft AlignTopLeft (Box {rows = 1, cols = 10, content = Text "78 9 10 11"})}
162 ,Box {rows = 1, cols = 10, content = SubBox AlignTopLeft AlignTopLeft (Box {rows = 1, cols = 2, content = Text "12"})}]})}
164 para :: Alignment -> Int -> String -> Box
165 para a n t = (\ss -> mkParaBox a (length ss) ss) $ flow n t
167 {- | @columns w h t@ is a list of boxes, each of width @w@ and height
168 at most @h@, containing text @t@ flowed into as many columns as
170 >>> error $ render $ hcat AlignTopLeft $ columns AlignTopLeft 10 5 "1 2 3 4 5 6 7 8 9 10 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29"
177 columns :: Alignment -> Int -> Int -> String -> [Box]
178 columns a w h t = map (mkParaBox a h) . chunksOf h $ flow w t
180 {- | @'chunksOf' n@ splits a list into length-n pieces. The last
181 piece will be shorter if @n@ does not evenly divide the length of
182 the list. If @n <= 0@, @'chunksOf' n l@ returns an infinite list
183 of empty lists. For example:
185 Note that @'chunksOf' n []@ is @[]@, not @[[]]@. This is
186 intentional, and is consistent with a recursive definition of
187 'chunksOf'; it satisfies the property that
189 @chunksOf n xs ++ chunksOf n ys == chunksOf n (xs ++ ys)@
191 whenever @n@ evenly divides the length of @xs@.
193 chunksOf :: Int -> [e] -> [[e]]
194 chunksOf i ls = map (take i) (build (splitter ls))
196 splitter :: [e] -> ([e] -> a -> a) -> a -> a
198 splitter l c n = l `c` splitter (drop i l) c n
200 {- | @mkParaBox a n s@ makes a box of height @n@ with the text @s@
201 aligned according to @a@.
203 mkParaBox :: Alignment -> Int -> [String] -> Box
204 mkParaBox a n = alignVert AlignTopLeft n . vcat a . map fromString
206 {- | Flow the given text into the given width.
207 >>> flow 10 "1234567890abc abcdefghijkl"
208 ["1234567890","abcdefghij"]
210 >>> flow 10 "1234567890abcdefghij"
213 flow :: Int -> String -> [String]
215 (take n <$>) . getLines $
216 foldl' addWordP (emptyPara n) (fromString <$> words t)
221 , unPara :: ParaContent
223 data ParaContent = ParaContent
224 { paraLines :: [Line]
225 , paraLastLine :: Line
228 emptyPara :: Int -> Para
229 emptyPara pw = Para pw (ParaContent [] (Line 0 []))
231 getLines :: Para -> [String]
232 getLines (Para _ (ParaContent ls l))
233 | lineLen l == 0 = process ls
234 | otherwise = process (l : ls)
236 process = map (unwords . reverse . map unWord . unLine) . reverse
243 mkLine :: [Word] -> Line
244 mkLine ws = Line (sum (map ((+ 1) . wordLen) ws) - 1) ws
246 startLine :: Word -> Line
247 startLine = mkLine . (: [])
255 instance IsString Word where
256 fromString w = Word (length w) w
258 addWordP :: Para -> Word -> Para
259 addWordP (Para pw (ParaContent fl l)) w
260 | wordFits pw w l = Para pw (ParaContent fl (addWordL w l))
261 | otherwise = Para pw (ParaContent (l : fl) (startLine w))
263 addWordL :: Word -> Line -> Line
264 addWordL w (Line len ws) = Line (len + wordLen w + 1) (w : ws)
266 wordFits :: Int -> Word -> Line -> Bool
267 wordFits pw w l = lineLen l == 0 || lineLen l + wordLen w + 1 <= pw
269 --------------------------------------------------------------------------------
270 -- Alignment -----------------------------------------------------------------
271 --------------------------------------------------------------------------------
273 {- | @alignHoriz algn n bx@ creates a box of width @n@, with the
274 contents and height of @bx@, horizontally aligned according to
277 alignHoriz :: Alignment -> Int -> Box -> Box
278 alignHoriz a c b = align a AlignTopLeft (rows b) c b
280 {- | @alignVert algn n bx@ creates a box of height @n@, with the
281 contents and width of @bx@, vertically aligned according to
283 >>> error $ render $ alignVert AlignTopLeft 4 "123"
289 >>> error $ render $ alignVert AlignBottomRight 4 "123"
296 -- | >>> error $ render $ alignVert AlignBottomRightCenter 4 "123"
305 -- | >>> error $ render $ alignVert AlignTopLeftCenter 4 "123"
311 alignVert :: Alignment -> Int -> Box -> Box
312 alignVert a r b = align AlignTopLeft a r (cols b) b
314 {- | @align ah av r c bx@ creates an @r@ x @c@ box with the contents
315 of @bx@, aligned horizontally according to @ah@ and vertically
318 align :: Alignment -> Alignment -> Int -> Int -> Box -> Box
319 align ah av r c = Box r c . SubBox ah av
321 {- | Move a box \"up\" by putting it in a larger box with extra rows,
322 aligned to the top. See the disclaimer for 'moveLeft'.
324 moveUp :: Int -> Box -> Box
325 moveUp n b = alignVert AlignTopLeft (rows b + n) b
327 {- | Move a box down by putting it in a larger box with extra rows,
328 aligned to the bottom. See the disclaimer for 'moveLeft'.
330 moveDown :: Int -> Box -> Box
331 moveDown n b = alignVert AlignBottomRight (rows b + n) b
333 {- | Move a box left by putting it in a larger box with extra columns,
334 aligned left. Note that the name of this function is
335 something of a white lie, as this will only result in the box
336 being moved left by the specified amount if it is already in a
337 larger right-aligned context.
339 >>> error $ render $ moveLeft 4 "123"
342 >>> error $ render $ alignHoriz AlignBottomRight 20 $ moveLeft 15 "123"
345 >>> error $ render $ alignHoriz AlignTopLeft 10 $ moveLeft 15 "123"
349 moveLeft :: Int -> Box -> Box
350 moveLeft n b = alignHoriz AlignTopLeft (cols b + n) b
352 {- | Move a box right by putting it in a larger box with extra
353 columns, aligned right. See the disclaimer for 'moveLeft'.
355 >>> error $ render $ moveRight 4 "123"
358 moveRight :: Int -> Box -> Box
359 moveRight n b = alignHoriz AlignBottomRight (cols b + n) b
361 --------------------------------------------------------------------------------
362 -- Implementation ------------------------------------------------------------
363 --------------------------------------------------------------------------------
365 {- | Render a 'Box' as a String, suitable for writing to the screen or
368 render :: Box -> String
369 render = unlines . renderBox
371 -- XXX make QC properties for takeExactly
373 {- | \"Padded take\": @takeExactly a n xs@ is the same as @take n xs@, if @n
374 <= length xs@; otherwise it is @xs@ followed by enough copies of
375 @a@ to make the length equal to @n@.
377 takeExactly :: a -> Int -> [a] -> [a]
378 takeExactly _ n _ | n <= 0 = []
379 takeExactly pad n [] = replicate n pad
380 takeExactly pad n (x : xs) = x : takeExactly pad (n - 1) xs
382 {- | @takeExactlyAligned @ is like 'takeExactly', but with alignment. That is, we
383 imagine a copy of @xs@ extended infinitely on both sides with
384 copies of @a@, and a window of size @n@ placed so that @xs@ has
385 the specified alignment within the window; @takeExactlyAligned algn a n xs@
386 returns the contents of this window.
387 >>> takeExactlyAligned AlignTopLeft ' ' 10 "12345"
390 >>> takeExactlyAligned AlignBottomRight ' ' 10 "12345"
393 >>> takeExactlyAligned AlignTopLeftCenter ' ' 10 "12345"
396 >>> takeExactlyAligned AlignBottomRightCenter ' ' 10 "12345"
399 takeExactlyAligned :: Alignment -> a -> Int -> [a] -> [a]
400 takeExactlyAligned ali pad len l =
401 reverse (takeExactly pad (leftLen ali len) (reverse leftList))
402 ++ takeExactly pad (rightLen ali len) rightList
404 (leftList, rightList) = splitAt (leftLen ali (length l)) l
406 rightLen AlignTopLeft n = n
407 rightLen AlignTopLeftCenter n = n `div` 2
408 rightLen AlignBottomRight _ = 0
409 rightLen AlignBottomRightCenter n = (n + 1) `div` 2
411 leftLen a n = n - rightLen a n
413 -- | Generate a string of spaces.
414 blanks :: Int -> String
415 blanks = flip replicate ' '
417 -- | Render a box as a list of lines.
418 renderBox :: Box -> [String]
419 renderBox (Box r c Blank) = resizeBox r c [""]
420 renderBox (Box r c (Text t)) = resizeBox r c [t]
421 renderBox (Box r c (Row bs)) =
422 resizeBox r c $ merge $ (\b -> renderBox b{rows=r}) <$> bs
424 merge = foldr (zipWith (++)) (repeat [])
425 renderBox (Box r c (Col bs)) =
426 resizeBox r c $ concatMap (\b -> renderBox b{cols=c}) bs
427 renderBox (Box r c (SubBox ha va b)) =
428 takeExactlyAligned va (blanks c) r $
429 takeExactlyAligned ha ' ' c <$> renderBox b
431 {- | Resize a rendered list of lines.
432 >>> resizeBox 5 4 ["1 2 3 4 5 6 7 8 9", "10 11 12 13 14 15 16 17 18 19"]
433 ["1 2 ","10 1"," "," "," "]
435 resizeBox :: Int -> Int -> [String] -> [String]
436 resizeBox rowLen colLen ls =
437 takeExactly (blanks colLen) rowLen $
438 takeExactly ' ' colLen <$> ls
440 -- | A convenience function for rendering a box to stdout.
441 printBox :: Box -> IO ()
442 printBox = putStr . render
444 {- >>> error $ render $ ("123" // "4") <+> ("toto" <+> "foo" // "titi") <+> "bar"
451 -- | Align boxes along their tops.
455 -- | Align boxes along their bottoms.
457 bottom = AlignBottomRight
459 -- | Align boxes to the left.
463 -- | Align boxes to the right.
465 right = AlignBottomRight
467 {- | Align boxes centered, but biased to the left/top in case of
471 center1 = AlignTopLeftCenter
473 {- | Align boxes centered, but biased to the right/bottom in case of
477 center2 = AlignBottomRightCenter