module Boxes where -- Adapted from the Haskell boxes library by Brent Yorgey import Data.List (intersperse, transpose) --- A box has a defined size (rows x cols) and some content. data Box = Box { rows :: Int , cols :: Int , content :: Content } --- Box alignment. --- --- @cons AlignFirst - align at top/left --- @cons AlignCenter1 - centered, but biased to top/left --- @cons AlignCenter2 - centered, but biased to bottom/right --- @cons AlignLast - align at bottom/right data Alignment = AlignFirst | AlignCenter1 | AlignCenter2 | AlignLast --- Top alignment. top :: Alignment top = AlignFirst --- Botton alignment. bottom :: Alignment bottom = AlignLast --- Left alignment. left :: Alignment left = AlignFirst --- Right alignment. right :: Alignment right = AlignLast --- Center-top/left alignment. center1 :: Alignment center1 = AlignCenter1 --- Center-bottom/right alignment. center2 :: Alignment center2 = AlignCenter2 --- Content of a box. --- --- @cons Blank - no content --- @cons Text - a string --- @cons Row - a row of boxes --- @cons Col - a column of boxes --- @cons SubBox - an aligned subbox data Content = Blank | Text String | Row [Box] | Col [Box] | SubBox Alignment Alignment Box --- Creates an empty 0x0 box. nullBox :: Box nullBox = emptyBox 0 0 --- Creates an empty box with the given size. --- --- @param r number of rows --- @param c number of columns emptyBox :: Int -> Int -> Box emptyBox r c = Box r c Blank --- Creates a 1x1 box from a character. char :: Char -> Box char c = Box 1 1 (Text [c]) --- Creates a Nx1 box from a string of length N. text :: String -> Box text t = Box 1 (length t) (Text t) --- Combine two boxes horizontally with top alignment. (<>) :: Box -> Box -> Box l <> r = hcat top [l, r] --- Combine two boxes horizontally with top alignment and leave one column --- between the boxes. (<+>) :: Box -> Box -> Box l <+> r = hcat top [l, emptyBox 0 1, r] --- Combine two boxes vertically with left alignment. (//) :: Box -> Box -> Box t // b = vcat left [t, b] --- Combine two boxes vertically with left alignment and leave one row between --- the boxes. (/+/) :: Box -> Box -> Box t /+/ b = vcat left [t, emptyBox 1 0, b] --- Combines a list of boxes horizontally with the given alignment. hcat :: Alignment -> [Box] -> Box hcat a bs = Box h w (Row $ map (alignVert a h) bs) where (w, h) = sumMax cols 0 rows bs --- Combines a list of boxes horizontally with the given alignment and space --- between all boxes. hsep :: Int -> Alignment -> [Box] -> Box hsep sep a bs = punctuateH a (emptyBox 0 sep) bs --- Combines a list of boxes vertically with the given alignment. vcat :: Alignment -> [Box] -> Box vcat a bs = Box h w (Col $ map (alignHoriz a w) bs) where (h, w) = sumMax rows 0 cols bs --- Calculate sum and maximum of a list. sumMax :: Ord b => (a -> Int) -> b -> (a -> b) -> [a] -> (Int, b) sumMax f defaultMax g as = foldr go (,) as 0 defaultMax where go a r n b = (r $! f a + n) $! g a `max` b --- Combines a list of boxes vertically with the given alignment and space --- between all boxes. vsep :: Int -> Alignment -> [Box] -> Box vsep sep a bs = punctuateV a (emptyBox sep 0) bs --- Combine a list of boxes horizontally with the given alignment and a copy of --- the given box between each two boxes. punctuateH :: Alignment -> Box -> [Box] -> Box punctuateH a p bs = hcat a (intersperse p bs) --- Combine a list of boxes vertically with the given alignment and a copy of --- the given box between each two boxes. punctuateV :: Alignment -> Box -> [Box] -> Box punctuateV a p bs = vcat a (intersperse p bs) paraFill :: Alignment -> Int -> String -> Box paraFill a n t = (\ss -> mkParaBoxFill a (length ss) n ss) $ flow n t mkParaBoxFill :: Alignment -> Int -> Int -> [String] -> Box mkParaBoxFill a h w = align AlignFirst a h w . vcat a . map text --- Create a box of the given width, containing a specific text. The text is --- flowed to fit the width according to the alignment. --- --- @param a the alignment of the text --- @param w the box's width --- @param c the box's contents para :: Alignment -> Int -> String -> Box para a n t = (\ss -> mkParaBox a (length ss) ss) $ flow n t --- Creates a list of boxes, each of a specific width and height. The given --- text is flowed into as many columns as necessary according to the given --- alignment. columns :: Alignment -> Int -> Int -> String -> [Box] columns a w h t = map (mkParaBox a h) . chunksOf h $ flow w t --- Creates a box of a specific height that contains a list of texts. mkParaBox :: Alignment -> Int -> [String] -> Box mkParaBox a n = alignVert top n . vcat a . map text --- Flows a given text into a given width, creating many different strings. flow :: Int -> String -> [String] flow n t = map (take n) . getLines $ foldl addWordP (emptyPara n) (map mkWord . words $ t) --- A paragraph has a width and some content. data Para = Para { paraWidth :: Int , paraContent :: ParaContent } --- A paragraph's content is a block consisting of many full lines and a single --- last line. data ParaContent = Block { fullLines :: [Line] , lastLine :: Line } --- Creates an empty paragraph of the given width. emptyPara :: Int -> Para emptyPara pw = Para pw (Block [] (Line 0 [])) --- Returns all lines of a paragraph. 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 --- A line has a length and a list of words. data Line = Line { lLen :: Int , getWords :: [Word] } --- Creates a line from a list of words. mkLine :: [Word] -> Line mkLine ws = Line (sum (map ((+1) . wLen) ws) - 1) ws --- Creates a line from a single word. startLine :: Word -> Line startLine = mkLine . (:[]) --- A word has a length and its contents. data Word = Word { wLen :: Int , getWord :: String } --- Creates a word from a string. mkWord :: String -> Word mkWord w = Word (length w) w --- Adds a word to a paragraph. 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)) --- Adds a word to a line. addWordL :: Word -> Line -> Line addWordL w (Line len ws) = Line (len + wLen w + 1) (w:ws) --- Checks whether a word fits into a line. wordFits :: Int -> Word -> Line -> Bool wordFits pw w l = lLen l == 0 || lLen l + wLen w + 1 <= pw --- Creates a box of a specific width containing another box's content aligned --- according to the given alignment. alignHoriz :: Alignment -> Int -> Box -> Box alignHoriz a c b = align a AlignFirst (rows b) c b --- Creates a box of a specific height containing another box's content aligned --- according to the given alignment. alignVert :: Alignment -> Int -> Box -> Box alignVert a r b = align AlignFirst a r (cols b) b --- Creates a box of a specific width and height containing another box's --- content aligned according to the given alignment. 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 into a larger box with extra rows, aligned to --- the top. See remarks for moveLeft. moveUp :: Int -> Box -> Box moveUp n b = alignVert top (rows b + n) b --- Move a box down by putting it into a larger box with extra rows, aligned to --- the bottom. See remarks for moveLeft. moveDown :: Int -> Box -> Box moveDown n b = alignVert bottom (rows b + n) b --- Move a box left by putting it into a larger box with extra columns, aligned --- to the left. Note that this will only move the box by the specified amount --- if it is already in a larger right-aligned box. moveLeft :: Int -> Box -> Box moveLeft n b = alignHoriz left (cols b + n) b --- Move a box right by putting it into a larger box with extra columns, aligned --- to the right. See remarks for moveRight. moveRight :: Int -> Box -> Box moveRight n b = alignHoriz right (cols b + n) b --- Create a table from a list of rows. A fixed width for each column must be --- specified. table :: [[String]] -> [Int] -> Box table rows widths = vcat left $ map (hcat left . map (uncurry $ paraFill left)) withLengths where withLengths = map (zip widths) rows --- Render a box to a string. render :: Box -> String render = unlines . renderBox --- Takes a number of elements from a list. If the list is shorter than that --- number, fill the rest with a filler. takeP :: a -> Int -> [a] -> [a] takeP b n xs | n <= 0 = [] | otherwise = case xs of [] -> replicate n b (y:ys) -> y : takeP b (n - 1) ys fReverse :: ([a], b) -> ([a], b) fReverse (xs, y) = (reverse xs, y) (***) :: (a -> b) -> (c -> d) -> ((a, c) -> (b, d)) f1 *** f2 = \(x, y) -> (f1 x, f2 y) takePA :: Alignment -> a -> Int -> [a] -> [a] takePA c b x = glue . (takeP b (numRev c x) *** takeP b (numFwd c x)) . split where split t = fReverse . splitAt (numRev c (length t)) $ t glue = uncurry (++) . fReverse 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 --- Generates 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 with a given number of rows. renderBoxWithRows :: Int -> Box -> [String] renderBoxWithRows r b = renderBox (b { rows = r }) --- Render a box as a list of lines with 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 the given alignments. resizeBoxAligned :: Int -> Int -> Alignment -> Alignment -> [String] -> [String] resizeBoxAligned r c ha va = takePA va (blanks c) r . map (takePA ha ' ' c) --- Print a box to stdout. printBox :: Box -> IO () printBox = putStr . render -- From Haskell's Data.List.Split chunksOf :: Int -> [a] -> [[a]] chunksOf n xs = map (take n) (xs:(partials xs)) where partials [] = [] partials ys@(_:_) = let ys' = drop n ys in case ys' of [] -> [] (_:_) -> (ys':(partials ys')) sum :: [Int] -> Int sum = foldl (+) 0