packages feed

setdown-0.2.1.0: src/TableRender.hs

module TableRender
   ( Align(..)
   , renderTable
   ) where

import Data.List (intercalate, transpose)

-- | How a column's cells are padded to the column width.
data Align = AlignLeft | AlignRight

-- | Render a header row and body rows as a Unicode box-drawing table: a bold
-- (heavy-lined) top border and header separator, single-lined body borders,
-- one alignment per column, and column widths sized to the widest cell
-- (including the header). This reproduces the exact visual style setdown has
-- always used for its results tables.
--
-- Column widths and the header row always account for the header text, even
-- when there are zero body rows, so headers never disappear on empty input.
renderTable :: [Align] -> [String] -> [[String]] -> [String]
renderTable aligns headers rows =
   [topBorder, headerRow, headerSeparator] ++ map bodyRow rows ++ [bottomBorder]
   where
      widths = zipWith columnWidth headers (columnsOf headers rows)
      columnWidth header column = maximum (length header : map length column)

      columnsOf hs [] = replicate (length hs) []
      columnsOf _  rs = transpose rs

      topBorder       = border '┏' '┳' '┓' '━'
      headerSeparator = border '┡' '╇' '┩' '━'
      bottomBorder    = border '└' '┴' '┘' '─'

      border left mid right line =
         [left] ++ intercalate [mid] (map (\w -> replicate (w + 2) line) widths) ++ [right]

      headerRow          = rowOf '┃' (zipWith padCenter widths headers)
      bodyRow cells       = rowOf '│' (zipWith3 padAlign widths aligns cells)

      rowOf sep cells =
         [sep] ++ intercalate [sep] (map (\c -> ' ' : c ++ " ") cells) ++ [sep]

      padCenter width s =
         let extra = width - length s
             left  = extra `div` 2
             right = extra - left
         in replicate left ' ' ++ s ++ replicate right ' '

      padAlign width AlignLeft  s = s ++ replicate (width - length s) ' '
      padAlign width AlignRight s = replicate (width - length s) ' ' ++ s