ansiform-0.7.8.5: src/Ansiform/Internal/Content.hs
module Ansiform.Internal.Content where
--------------------------------------------------------------------------------
import Data.Char
import Data.List
import Data.List.Split
import Data.Maybe
import Ansiform.Internal.Number
import Ansiform.Internal.Sample
import Ansiform.Internal.Settings
import Ansiform.Internal.Style
import Ansiform.Internal.Utils
import Ansiform.Internal.Width
-- Content Types
--------------------------------------------------------------------------------
-- | A piece of text with a single style. Non-printable characters are removed.
newtype Item = Item (String,Style)
deriving (Eq, Show)
-- | A list of items printed without breaks. It contains no empty items.
newtype Block = Block [Item]
deriving (Eq, Show)
-- | A header block followed by body blocks.
newtype Group = Group [Block]
deriving (Eq, Show)
-- | A list of groups with the same number of blocks.
newtype Table = Table [Group]
deriving (Eq, Show)
-- | A list of blocks wrapped as one paragraph.
type Paragraph = [Block]
-- | A list of paragraphs.
type Article = [Paragraph]
-- Sample Instances
--------------------------------------------------------------------------------
instance Sample Item where
sampleShow _ (Item (s,v)) = "Item " ++ show s ++ " " ++ sampleShow 0 v
instance Sample Block where
sampleShow n (Block is) = sampleFormat "Block" n is
instance Sample Group where
sampleShow n (Group bs) = sampleFormat "Group" n bs
instance Sample Table where
sampleShow n (Table gs) = sampleFormat "Table" n gs
-- Wrappers
--------------------------------------------------------------------------------
formItem :: String -> Style -> Item
formItem s v = Item (itemTextClean s, v)
formItemList :: [(String, Style)] -> [Item]
formItemList = fmap (uncurry formItem)
-- Block builder, removes empty items beforehand.
formBlock :: [Item] -> Block
formBlock = Block . itemsEmptyFilter
formBlockList :: [[Item]] -> [Block]
formBlockList = fmap formBlock
-- Group builder, taking group header and contents separately.
formGroup :: Block -> [Block] -> Group
formGroup h b = Group (h:b)
-- Allows to build a Group without a separate header.
formGroup' :: [Block] -> Group
formGroup' = Group
-- Allows to build any Table, with length checking previously done.
formTable :: [Group] -> Bool -> Maybe Table
formTable gs b | b = Just (Table gs) | otherwise = Nothing
-- Unrwappers
--------------------------------------------------------------------------------
unItem :: Item -> (String, Style)
unItem (Item (s,v)) = (s,v)
unItemText :: Item -> String
unItemText (Item (t,_)) = t
unItemStyle :: Item -> Style
unItemStyle (Item (_,v)) = v
unBlock :: Block -> [Item]
unBlock (Block xs) = xs
unItemList :: [Item] -> [(String, Style)]
unItemList = fmap unItem
unBlockList :: [Block] -> [[Item]]
unBlockList = fmap unBlock
unGroup :: Group -> [Block]
unGroup (Group bs) = bs
unTable :: Table -> [Group]
unTable (Table gs) = gs
unMaybeTable :: Maybe Table -> [Group]
unMaybeTable mt = maybe[groupEmpty] unTable mt
-- Basic Builders
--------------------------------------------------------------------------------
-- Trivial Item, not allowed in Blocks.
itemEmpty :: Item
itemEmpty = formItem "" defaultStyle
-- Trivial Block.
-- | An empty block.
blockEmpty :: Block
blockEmpty = formBlock []
-- | A block containing a single text and style.
blockSimple :: String -> Style -> Block
blockSimple s v = formBlock [formItem s v]
-- Trivial Group.
groupEmpty :: Group
groupEmpty = formGroup' [blockEmpty]
-- Utilities
--------------------------------------------------------------------------------
-- Removes non-printable characters from the text of a formItem.
itemTextClean :: String -> String
itemTextClean = filter isPrint
-- Removes empty Items from a list of Items.
-- Enforces the Block invariant, when used to build them.
itemsEmptyFilter :: [Item] -> [Item]
itemsEmptyFilter = filter (not . isItemNull)
-- Checks if a Block is empty.
isBlockNull :: Block -> Bool
isBlockNull vb
| null items = True
| otherwise = and $ fmap isItemNull items
where
items = unBlock vb
-- Checks if a Item is empty.
isItemNull :: Item -> Bool
isItemNull = null . unItemText
-- Calculates the maximum length of the text in a list of Blocks.
blockTextMax :: [Block] -> Int
blockTextMax bs = maximum' 0
$ (return . blockTextWidth . formBlock) =<< unBlockList bs
-- Calculates the length of a Block text, summing each Item.
blockTextWidth :: Block -> Int
blockTextWidth vb = sum $ blockTextWidthList vb
-- Calculates the length of a Block text,
blockTextWidthList :: Block -> [Int]
blockTextWidthList vb = fmap itemTextWidth (unBlock vb)
-- Calculates the length of the text in a Item.
itemTextWidth :: Item -> Int
itemTextWidth vi = textWidth $ unItemText vi
-- Changes the background of each item in a Block.
blockSetBG :: Color -> Block -> Block
blockSetBG c b = formBlock $ fmap (itemSetBG c) (unBlock b)
-- Swaps the background of each item in a Block.
blockSwapBG :: Block -> Block
blockSwapBG b = formBlock $ fmap itemSwapBG (unBlock b)
-- Changes the background color of a Item.
itemSetBG :: Color -> Item -> Item
itemSetBG c i = itemChangeStyle i $ setBackground c $ unItemStyle i
-- Swaps the background of a Item.
itemSwapBG :: Item -> Item
itemSwapBG i = itemChangeStyle i $ swapBackground $ unItemStyle i
-- Changes the visual style of a Item.
itemChangeStyle :: Item -> Style -> Item
itemChangeStyle i s = formItem (unItemText i) s
-- Applies the Item prettifier to a whole block.
blockPrettify :: Block -> Block
blockPrettify vb = formBlock $ concatMap itemPrettify $ unBlock vb
-- Improves the styling of a Item so as to not style punctuation or spaces.
itemPrettify :: Item -> [Item]
itemPrettify vi
| isSpecial vs = [ formItem (fst splitHead) defaultStyle
, formItem (reverse (snd splitMiddle)) vs
, formItem (reverse (fst splitMiddle)) defaultStyle
]
| otherwise = [vi]
where
vs = unItemStyle vi
vt = unItemText vi
isBlank c = isPunctuation c || isSpace c
splitHead = span isBlank vt
splitMiddle = span isBlank $ reverse $ snd splitHead
-- Text Formatting
--------------------------------------------------------------------------------
-- Reformats a list of Blocks so they respect the same maximum width.
-- The first line can be indented using the padding given.
-- The function always makes progress, even when a single word is wider
-- than the available space, so narrow terminals cannot cause a loop.
blockPublish :: Int -> Int -> [Block] -> [Block]
blockPublish w p vbs
| w <= 0 = vbs
| otherwise = fmap (formBlock . reverse) $ publish firstLine p items
where
firstLine = if p > 0 then [formItem (replicate p ' ') defaultStyle] else []
items = concatMap unBlock vbs
publish :: [Item] -> Int -> [Item] -> [[Item]]
publish line _ [] = [line]
publish line cw (item:rest)
| itemWidth <= remaining = publish (item:line) (cw + itemWidth) rest
| otherwise = case itemSplit remaining item of
(fit, remainder)
| not (isItemNull fit) ->
publish (fit:line) (cw + itemTextWidth fit) next
| null line ->
publish (item:line) (cw + itemWidth) rest
| cw == p && p > 0 ->
publish (item:line) (cw + itemWidth) rest
| otherwise ->
line : publish [] 0 (item:rest)
where
next = if isItemNull remainder then rest else remainder:rest
where
remaining = w - cw
itemWidth = itemTextWidth item
-- Splits a Block into two, splitting on a desired text length.
blockSplit :: Int -> Block -> (Block, Block)
blockSplit w vb = ( formBlock (fst processItems)
, formBlock (snd processItems) )
where
items = unBlock vb
processItems = go [] 0 items
where
go cvis cw rvis
| curWidth <= w && not (null rvis) = go (vi:cvis) curWidth (drop 1 rvis)
| otherwise = (reverse (curItem:cvis), remItems:rvis)
where
vi = fromMaybe itemEmpty $ headMaybe rvis
curWidth = itemWidth + cw
itemWidth = itemTextWidth vi
curItem = fst splitItem
remItems = snd splitItem
splitItem = itemSplit (w - cw) vi
-- Splits a Item into two, splitting on a desired text length, but keeping words atomic.
itemSplit :: Int -> Item -> (Item, Item)
itemSplit w vi = ( formItem (fst processText) vs
, formItem (snd processText) vs )
where
vs = unItemStyle vi
splitWords = split (keepDelimsR (whenElt isSpace)) $ unItemText vi
processText = breakLength [] 0 splitWords
where
breakLength cwds cw wds
| totalLength <= w && not (null wds) =
breakLength (wd:cwds) totalLength (drop 1 wds)
| otherwise = (concatRev cwds, concat wds)
where
wd = fromMaybe "" $ headMaybe wds
wdLength = textWidth wd
totalLength = wdLength + cw
-- Group and Table Formatters
--------------------------------------------------------------------------------
-- A table is simply a list of Groups of the same length.
-- Reduces the length of groups in a list of Groups so that they don't exceed
-- the average length too much, based on the excess configuration constant.
groupAverage :: Config -> [Group] -> [Group]
groupAverage s gs = foldr groupProcess [] gs
where
gof = unGroupOverflow s
groupProcess g a
| grpLength > tgtLength = fmap formGroup' headerBlocks ++ a
| otherwise = g : a
where grpLength = length blocks
headerBlocks = take 1 splitBlocks ++ fmap (blockEmpty :) (drop 1 splitBlocks)
splitBlocks = splitPlaces (tgtLength + 1 : repeat tgtLength) blocks
blocks = unGroup g
tgtLength
| overflowRatio == 0 = avgLength
| otherwise = avgLength + div avgLength overflowRatio
overflowRatio = groupOverflowRatio gof
avgLength = average $ fmap (length . unGroup) gs
-- Builds a table where each element has its own style and composition.
-- Each element is a Block so it can use composite styles.
-- The header and body must follow the table invariant or the build will fail.
formTableBlocks :: [Block] -> [[Block]] -> Maybe Table
formTableBlocks h rs = formTable fullTable (isTableBalanced h rs)
where
fullTable = zip h (transpose rs) >>= \(h', r) -> return $ formGroup h' r
-- Builds a table where every column has a single Style, given in the header.
-- An additional Style is used to style the header itself.
-- Each row is given simply as a list of strings.
-- The header and body must follow the table invariant or the build will fail.
formTableSimple :: [Block] -> Style -> [[String]] -> Maybe Table
formTableSimple h v sss = formTable fullTable (isTableBalanced h sss)
where
headerList = unBlockList h
header = headerList >>= \b -> return $ blockSimple (concatMap (fst . unItem) b) v
bodyVis = fmap (maybe defaultStyle (snd . unItem) . headMaybe) headerList
body = transpose $
fmap (\ss -> fmap (uncurry blockSimple)(zip ss bodyVis)) sss
fullTable = fmap (uncurry formGroup) (zip header body)
-- Builds a table that is already properly built, with a Group for each column,
-- just lacking the invariant check.
formTableGroups :: [Group] -> Maybe Table
formTableGroups vgs = formTable vgs vgsLengthMatch
where
headLength = maybe 0 (length . unGroup) $ headMaybe vgs
vgsLengthMatch = and $ fmap ((==) headLength . length . unGroup) (drop 1 vgs)
-- Enforces the invariant that the header and each row
-- must have the same length.
isTableBalanced :: [Block] -> [[a]] -> Bool
isTableBalanced h rs = and $ fmap (\r -> headLength == length r) rs
where headLength = length (unBlockList h)
-- Rules && Solid
--------------------------------------------------------------------------------
-- | A character and a style used to draw a horizontal rule.
newtype Rule = Rule (Char, Style)
deriving (Eq, Show)
instance Sample Rule where
sampleShow _ (Rule (c,v)) = "Rule " ++ show c ++ " " ++ sampleShow 0 v
unRuleChar :: Rule -> Char
unRuleChar (Rule (c,_)) = c
unRuleStyle :: Rule -> Style
unRuleStyle (Rule (_,s)) = s
formRule :: Char -> Style -> Rule
formRule c v
| isPrint c = Rule (c,v)
| otherwise = Rule ('.',v)
-- Creates a Item for rules and solid spaces from a Rule.
-- Must be called direct from printing, as it calculates the width on the fly.
formRuleItem :: Rule -> Number -> Item
formRuleItem r n = formItem (replicate (unn n) (unRuleChar r)) (unRuleStyle r)