ansiform-0.7.8.5: 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 = if left > 0 then [formItem (replicate left ' ') defaultStyle] else []
rightItems = if right > 0 then [formItem (replicate right ' ') defaultStyle] else []
-- 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)