kvitable-1.2.0.0: src/Data/KVITable/Render/ASCII.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
-- | This module provides the 'KVITable' 'render' function for
-- rendering the table in a plain ASCII format.
module Data.KVITable.Render.ASCII
(
render
-- re-export Render definitions to save the caller an additional import
, RenderConfig(..)
, defaultRenderConfig
)
where
import qualified Data.List as L
import Data.Maybe ( fromMaybe, isNothing )
import Data.Name
import Data.String ( fromString )
import Data.Text ( Text )
import qualified Data.Text as T
import Lens.Micro ( (^.) )
import Numeric.Natural
import qualified Prettyprinter as PP
import Text.Sayable
import Data.KVITable ( KVITable, KeySpec, keyVals )
import qualified Data.KVITable as KVIT
import Data.KVITable.Internal.Helpers
import Data.KVITable.Render
import Data.KVITable.Render.Internal
import Prelude hiding ( lookup )
-- | Renders the specified table in ASCII format, using the specified
-- 'RenderConfig' controls.
render :: Sayable "normal" v
=> Maybe (PP.Doc SayableAnn -> Text)
-- ^ Custom renderer which can be used to reAnnotate and perform
-- special rendering if desired. The default is the plain text
-- rendering.
-> RenderConfig -> KVITable v
-> Text
render rndr cfg t =
let kmap = renderingKeyVals cfg $ t ^. keyVals
(fmt, hdr) = renderHdrs cfg t kmap
bdy = renderSeq rndr cfg fmt kmap t
in T.unlines $ hdr <> bdy
----------------------------------------------------------------------
data FmtLine = FmtLine [Natural] Sigils Sigils -- last is for sepline
data Sigils = Sigils { sep :: Text, pad :: Text, cap :: Text }
fmtLine :: [Natural] -> FmtLine
fmtLine cols = FmtLine cols
Sigils { sep = "|", pad = " ", cap = "_" }
Sigils { sep = "+", pad = "-", cap = "_" }
fmtColCnt :: FmtLine -> Natural
fmtColCnt (FmtLine cols _ _) = nLength cols
perColOvhd :: Natural
perColOvhd = 2 -- pad chars on either side of each column's entry
-- | Formatted width of output, including pad on either side of each
-- column's value (but not the outer set), and a separator between columns.
--
-- Note that a column size of 0 indicates that hideBlankCols is active
-- and the column was found to be empty of values, so it should not be
-- counted.
fmtWidth :: FmtLine -> Natural
fmtWidth (FmtLine cols _ _) =
let cols' = L.filter (/= 0) cols
in sum cols' + ((perColOvhd + 1) * (nLength cols' - 1))
fmtEmptyCols :: FmtLine -> Bool
fmtEmptyCols (FmtLine cols _ _) = sum cols == 0
fmtAddColLeft :: Natural -> FmtLine -> FmtLine
fmtAddColLeft leftCol (FmtLine cols s s') = FmtLine (leftCol : cols) s s'
data FmtVal = Separator | TxtVal Natural Text | CenterVal Natural Text
| Overage -- cells for the overflow row/column
fmtRender :: FmtLine -> [FmtVal] -> Text
fmtRender (FmtLine _cols _sigils _sepsigils) [] = ""
fmtRender (FmtLine cols sigils sepsigils) vals@(val:_) =
if length cols == length vals
then let sig f o = case o of
Separator -> f sepsigils
TxtVal {} -> f sigils
CenterVal {} -> f sigils
Overage -> f sigils
l = sig sep val
charRepeat n c = T.pack (replicate (fromEnum n) c)
rightAlign n w t = let rt = charRepeat (n - w) ' ' <> t
in if w >= n then t else rt
centerIn n w t = let (w',e) = (n - w - 2) `divMod` 2
m = cap sigils
ls = T.replicate (fromEnum $ w' + 0) m
rs = T.replicate (fromEnum $ w' + e) m
in if w + 2 >= n
then rightAlign n w t
else ls <> " " <> t <> " " <> rs
in l <>
T.concat
[ sig pad fld <>
(case fld of
Separator -> charRepeat sz '-'
TxtVal w v -> rightAlign sz w v
CenterVal w t -> centerIn sz w t
Overage -> centerIn sz 1 "+"
) <>
sig pad fld <>
sig sep fld -- KWQ or if next fld is Nothing
| (sz,fld) <- zip cols vals, sz /= 0
]
else error ("Insufficient arguments (" <>
show (length vals) <> ")" <>
" for FmtLine " <> show (length cols))
----------------------------------------------------------------------
data HeaderLine = HdrLine FmtLine HdrVals Trailer
type HdrVals = [FmtVal]
type Trailer = Name "column header"
hdrFmt :: HeaderLine -> FmtLine
hdrFmt (HdrLine fmt _ _) = fmt
renderHdrs :: Sayable "normal" v
=> RenderConfig -> KVITable v -> (TblHdrs, TblHdrs)
-> (FmtLine, [Text])
renderHdrs cfg t kmap =
( lastFmt
, [ fmtRender fmt hdrvals
<> (if nullName trailer then "" else (" <- " <> nameText trailer))
| (HdrLine fmt hdrvals trailer) <- hrows
] <>
(single $ let sz = fromEnum $ fmtColCnt lastFmt
in fmtRender lastFmt (replicate sz Separator)) )
where
hrows = hdrstep cfg t kmap
lastFmt = case reverse hrows of
[] -> fmtLine mempty
(hrow:_) -> hdrFmt hrow
hdrstep :: Sayable "normal" v
=> RenderConfig
-> KVITable v
-> (TblHdrs, TblHdrs)
-> [HeaderLine]
hdrstep _cfg t ([], []) =
-- colStackAt wasn't recognized, so devolve into a non-colstack table
let valcoltxt = t ^. KVIT.valueColName
valcoltsz = nameLength valcoltxt
valsizes = nLength . sez @"normal" . snd <$> KVIT.toList t
valwidth = maxOf 0 $ valcoltsz : valsizes
hdrVal = TxtVal (nameLength valcoltxt) (nameText valcoltxt)
in single $ HdrLine (fmtLine $ single valwidth) (single hdrVal) ""
hdrstep cfg t ([], colKeyMap) =
hdrvalstep cfg t colKeyMap mempty -- switch to column-stacking mode
hdrstep cfg t ((key,keyvals) : keys, colKeyMap) =
let keyw = max (nameLength key)
$ (maxOf 0 . fmap (nameLength . toHdrText)) keyvals
mkhdr (hs, v) (HdrLine fmt hdrvals trailer) =
( HdrLine (fmtAddColLeft keyw fmt)
(TxtVal (nameLength v) (nameText v) : hdrvals) trailer : hs , "")
in reverse $ fst $ foldl mkhdr (mempty, key) $ hdrstep cfg t (keys, colKeyMap)
-- first line shows hdrval for non-colstack'd columns, others are blank
hdrvalstep :: Sayable "normal" v
=> RenderConfig -> KVITable v -> TblHdrs -> KeySpec
-> [HeaderLine]
hdrvalstep cfg t ((key,titles) : []) steppath =
let cvalWidths = \case
V kv -> fmap (nLength . sez @"normal" . snd)
$ filter ((L.isSuffixOf (snoc steppath (key, kv))) . fst)
$ KVIT.toList t
AndMore _ -> [1] -- always show this column, although it has no contents
colWidth kv = let cvw = cvalWidths kv
in if hideBlankCols cfg && sum cvw == 0
then 0
else maxOf (nameLength $ toHdrText kv) cvw
cwidths = fmap colWidth titles
fmtcols = if equisizedCols cfg
then (replicate (length cwidths) (maxOf 0 cwidths))
else cwidths
tr = convertName key
-- hdrTxt = nameText . toHdrText
toTxtVal x = TxtVal (nameLength x) (nameText x)
in single $ HdrLine (fmtLine $ fmtcols) (toTxtVal . toHdrText <$> titles) tr
hdrvalstep cfg t ((key,vals) : keys) steppath =
let subhdrsV = \case
V v -> hdrvalstep cfg t keys (snoc steppath (key,v))
_ -> mempty
subTtlHdrs = let subAtVal v = (nameLength $ toHdrText v, subhdrsV v)
in fmap subAtVal vals
szexts = let subW (hl,sh) =
case sh of
[] -> (0, 0) -- should never be the case
(sh0:_) ->
let sv = fmtWidth $ hdrFmt sh0
in if hideBlankCols cfg && (fmtEmptyCols $ hdrFmt sh0)
then (0, 0)
else (hl, sv)
in fmap (uncurry max . subW) subTtlHdrs
rsz_extsubhdrs = fmap hdrJoin $
L.transpose $
fmap (uncurry rsz_hdrstack) $
zip szhdrs $ fmap snd subTtlHdrs
largest = maxOf 0 szexts
szhdrs = if equisizedCols cfg && not (hideBlankCols cfg)
then replicate (length vals) largest
else szexts
rsz_hdrstack s vhs = fmap (rsz_hdrs s) vhs
rsz_hdrs hw (HdrLine (FmtLine c s j) v r) =
let nzCols = L.filter (/= 0) c
numNZCols = nLength nzCols
pcw = sum nzCols + ((perColOvhd + 1) * (numNZCols - 1))
(ew,w0) = let l = length nzCols
in if l == 0 then (0,0)
else max 0 (hw - pcw) `divMod` numNZCols
c' = fst $ foldl (\(c'',n) w -> (snoc c'' $ n+w, ew)) (mempty,ew+w0) c
in HdrLine (FmtLine c' s j) v r
hdrJoin hl = foldl hlJoin (HdrLine (fmtLine mempty) mempty "") hl
hlJoin (HdrLine (FmtLine c s j) v _) (HdrLine (FmtLine c' _ _) v' r) =
HdrLine (FmtLine (c<>c') s j) (v<>v') r
tvals = let cVal v = let ht = toHdrText v
in CenterVal (nameLength ht) (nameText ht)
in cVal <$> vals
in HdrLine (fmtLine szhdrs) tvals (convertName key) : rsz_extsubhdrs
hdrvalstep _ _ [] _ = error "ASCII hdrvalstep with empty keys after matching colStackAt -- impossible"
toHdrText :: TblHdr -> Name "column header"
toHdrText = \case
V kv -> convertName kv
AndMore n -> fromString $ sez @"normal" $ t'"{+" &+ n &+ '}'
----------------------------------------------------------------------
renderSeq :: Sayable "normal" v
=> Maybe (PP.Doc SayableAnn -> Text)
-> RenderConfig
-> FmtLine
-> (TblHdrs, TblHdrs)
-> KVITable v
-> [Text]
renderSeq rndr cfg fmt kmap kvitbl =
fmtRender fmt . snd <$> asciiRows kmap mempty
where
filterBlank = if hideBlankRows cfg
then L.filter (not . all isNothing . snd)
else id
asciiRows :: (TblHdrs, TblHdrs)
-> KeySpec
-> [ (Bool, [FmtVal]) ]
asciiRows ([], []) path =
let v = KVIT.lookup' path kvitbl
skip = case v of
Nothing -> hideBlankRows cfg
Just _ -> False
in if skip then mempty
else let toTxtVal x =
let xl = toEnum $ length $ sez @"normal" x
xp = maybe (fromString . sez) id rndr
$ saying @"normal"
$ sayable x
in TxtVal xl xp
in single $ (False, single $ maybe (TxtVal 0 "") toTxtVal v)
asciiRows ([], colKeyMap) path =
let filterOrDefaultBlankRows = fmap (fmap defaultBlanks) . filterBlank
defaultBlanks = fmap (\case
Nothing -> TxtVal 0 ""
Just (vl, vp) -> TxtVal vl vp
)
in filterOrDefaultBlankRows $ single $ (False, multivalRows colKeyMap path)
asciiRows ((key, keyvals) : kseq, colKeyMap) path =
let subrows = \case
V keyval -> asciiRows (kseq, colKeyMap) $ snoc path (key, keyval)
_ ->
let ttlcols = product ( length . snd <$> colKeyMap)
in [ (False , replicate (ttlcols + length kseq) $ Overage) ]
grprow = \case
subs@(sub0:_) | key `elem` rowGroup cfg ->
let subl = single (True, replicate (length $ snd sub0) Separator)
in if fst (last subs) then init subs <> subl else subs <> subl
subs -> subs
genSubRow keyval = grprow $ fst
$ foldl leftAdd (mempty, toHdrText keyval) $ subrows keyval
leftAdd (acc,kv) (b,subrow) =
(snoc acc (b, TxtVal (nameLength kv) (nameText kv) : subrow)
, if rowRepeat cfg then kv else ""
)
in concat (genSubRow <$> keyvals)
multivalRows :: TblHdrs -> KeySpec -> [ Maybe (Natural, Text) ]
multivalRows ((key, keyvals) : []) path =
let showEnt x = ( toEnum $ length $ sez @"normal" x
, fromMaybe (fromString . sez) rndr
$ saying
$ sayable @"normal" x
)
in (\case
V v -> (showEnt <$> (KVIT.lookup' (snoc path (key,v)) kvitbl))
_ -> Nothing
) <$> keyvals
multivalRows ((key, keyvals) : kseq) path =
concatMap (\case
V v -> multivalRows kseq (snoc path (key,v))
_ -> mempty
) keyvals
multivalRows [] _ = error "multivalRows cannot be called with no keys!"