packages feed

ansiform-1.0.0.1: src/Ansiform/Internal/Table.hs

-- | Per-column formatting for tables and groups.
module Ansiform.Internal.Table where

--------------------------------------------------------------------------------
import Ansiform.Internal.Content
import Ansiform.Internal.Number
import Ansiform.Internal.Sample
import Ansiform.Internal.Style
import Ansiform.Internal.Width

-- Table Format Types
--------------------------------------------------------------------------------

-- | Horizontal alignment of a column.
data Align = AlignLeft | AlignCenter | AlignRight
    deriving (Eq, Show)

-- | Box-drawing style for tables and groups.
data Borders = BordersNone | BordersAscii | BordersLight | BordersHeavy | BordersDouble
    deriving (Eq, Show)

-- | Characters used to draw one border style.
data BorderChars = BorderChars
    { borderTopLeft :: Char
    , borderTopMid :: Char
    , borderTopRight :: Char
    , borderLeft :: Char
    , borderRight :: Char
    , borderMidLeft :: Char
    , borderMidMid :: Char
    , borderMidRight :: Char
    , borderBottomLeft :: Char
    , borderBottomMid :: Char
    , borderBottomRight :: Char
    , borderHorizontal :: Char
    , borderVertical :: Char
    }

-- | Marker appended when a cell is truncated.
data Ellipsis = EllipsisNone | EllipsisChar Char
    deriving (Eq, Show)

-- | Alignment, maximum width and truncation for one column.
data ColumnSpec = ColumnSpec
    { columnAlign :: Align
    , columnMaxWidth :: Maybe Width
    , columnEllipsis :: Ellipsis
    }
    deriving (Eq, Show)

-- | Formatting options for all columns of a table.
newtype TableFormat = TableFormat [ColumnSpec]
    deriving (Eq, Show)

instance Sample Align where
    sampleShow _ = show

instance Sample Borders where
    sampleShow _ = show

instance Sample Ellipsis where
    sampleShow _ = show

instance Sample ColumnSpec where
    sampleShow _ = show

instance Sample TableFormat where
    sampleShow _ = show

-- Wrappers and Defaults
--------------------------------------------------------------------------------

-- | Left-aligned, without a maximum width and without an ellipsis.
defaultColumnSpec :: ColumnSpec
defaultColumnSpec = ColumnSpec AlignLeft Nothing EllipsisNone

-- | Empty format; every column uses 'defaultColumnSpec'.
defaultTableFormat :: TableFormat
defaultTableFormat = TableFormat []

-- Returns the characters for a border style.
borderChars :: Borders -> Maybe BorderChars
borderChars b = case b of
    BordersNone -> Nothing
    BordersAscii ->
        Just
            BorderChars
                { borderTopLeft = '+'
                , borderTopMid = '+'
                , borderTopRight = '+'
                , borderLeft = '|'
                , borderRight = '|'
                , borderMidLeft = '+'
                , borderMidMid = '+'
                , borderMidRight = '+'
                , borderBottomLeft = '+'
                , borderBottomMid = '+'
                , borderBottomRight = '+'
                , borderHorizontal = '-'
                , borderVertical = '|'
                }
    BordersLight ->
        Just
            BorderChars
                { borderTopLeft = '┌'
                , borderTopMid = '┬'
                , borderTopRight = '┐'
                , borderLeft = '│'
                , borderRight = '│'
                , borderMidLeft = '├'
                , borderMidMid = '┼'
                , borderMidRight = '┤'
                , borderBottomLeft = '└'
                , borderBottomMid = '┴'
                , borderBottomRight = '┘'
                , borderHorizontal = '─'
                , borderVertical = '│'
                }
    BordersHeavy ->
        Just
            BorderChars
                { borderTopLeft = '┏'
                , borderTopMid = '┳'
                , borderTopRight = '┓'
                , borderLeft = '┃'
                , borderRight = '┃'
                , borderMidLeft = '┣'
                , borderMidMid = '╋'
                , borderMidRight = '┫'
                , borderBottomLeft = '┗'
                , borderBottomMid = '┻'
                , borderBottomRight = '┛'
                , borderHorizontal = '━'
                , borderVertical = '┃'
                }
    BordersDouble ->
        Just
            BorderChars
                { borderTopLeft = '╔'
                , borderTopMid = '╦'
                , borderTopRight = '╗'
                , borderLeft = '║'
                , borderRight = '║'
                , borderMidLeft = '╠'
                , borderMidMid = '╬'
                , borderMidRight = '╣'
                , borderBottomLeft = '╚'
                , borderBottomMid = '╩'
                , borderBottomRight = '╝'
                , borderHorizontal = '═'
                , borderVertical = '║'
                }

formTableFormat :: [ColumnSpec] -> TableFormat
formTableFormat = TableFormat

unTableFormat :: TableFormat -> [ColumnSpec]
unTableFormat (TableFormat cs) = cs

-- Returns the spec for a column, falling back to the default.
columnSpecAt :: TableFormat -> Int -> ColumnSpec
columnSpecAt fmt i = case drop i (unTableFormat fmt) of
    (c : _) -> c
    [] -> defaultColumnSpec

-- Formatting
--------------------------------------------------------------------------------

-- Natural width of a group, capped by its specification.
cappedWidth :: ColumnSpec -> Group -> Int
cappedWidth spec g = case columnMaxWidth spec of
    Nothing -> natural
    Just cap -> min natural (unn cap)
  where
    natural = blockTextMax (unGroup g)

-- Formats every block of a group to a target width.
formatGroup :: ColumnSpec -> Width -> Group -> Group
formatGroup spec w g = formGroup' $ fmap (formatBlock spec w) (unGroup g)

-- Truncates and pads a block to a target width.
formatBlock :: ColumnSpec -> Width -> Block -> Block
formatBlock spec w b =
    padBlock w (columnAlign spec) $
        truncateBlock w (columnEllipsis spec) b

-- Pads a block with spaces according to the alignment.
padBlock :: Width -> Align -> Block -> Block
padBlock w align b = formBlock (leftItems ++ unBlock b ++ rightItems)
  where
    missing = max 0 (unn w - blockTextWidth b)
    (left, right) = case align of
        AlignLeft -> (0, missing)
        AlignRight -> (missing, 0)
        AlignCenter -> (missing `div` 2, missing - missing `div` 2)
    leftItems = [formItem (replicate left ' ') defaultStyle | left > 0]
    rightItems = [formItem (replicate right ' ') defaultStyle | right > 0]

-- Truncates a block to a display width, optionally appending an ellipsis.
truncateBlock :: Width -> Ellipsis -> Block -> Block
truncateBlock w ell b
    | blockTextWidth b <= w' = b
    | otherwise = formBlock $ takeItems contentWidth (unBlock b) ++ ellipsisItems
  where
    w' = unn w
    (contentWidth, ellipsisItems) = case ell of
        EllipsisNone -> (w', [])
        EllipsisChar c ->
            let cw = charWidth c
             in if cw <= w'
                    then (w' - cw, [formItem [c] ellipsisStyle])
                    else (w', [])
    ellipsisStyle = case unBlock b of
        [] -> defaultStyle
        items -> unItemStyle (last items)

-- Keeps items while their display width fits.
takeItems :: Int -> [Item] -> [Item]
takeItems _ [] = []
takeItems n (i : is)
    | n <= 0 = []
    | itemWidth <= n = i : takeItems (n - itemWidth) is
    | otherwise = case splitTextAtWidth n (unItemText i) of
        ("", _) -> []
        (kept, _) -> [formItem kept (unItemStyle i)]
  where
    itemWidth = textWidth (unItemText i)

-- Splits a string at a display width, keeping combining marks with their base.
splitTextAtWidth :: Int -> String -> (String, String)
splitTextAtWidth n = go 0 []
  where
    go _ acc [] = (reverse acc, [])
    go cw acc (c : cs)
        | cw + charWidth c <= n = go (cw + charWidth c) (c : acc) cs
        | otherwise = (reverse acc, c : cs)