Skip to content

Commit cf99aeb

Browse files
committed
Implement DropAction
This separates the measurement phase from the building phase.
1 parent fe0a1bc commit cf99aeb

4 files changed

Lines changed: 190 additions & 131 deletions

File tree

src/Text/Layout/Table/Cell.hs

Lines changed: 93 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
{-# LANGUAGE DeriveFunctor #-}
2+
{-# LANGUAGE FlexibleContexts #-}
23
{-# LANGUAGE FlexibleInstances #-}
34
{-# LANGUAGE RecordWildCards #-}
5+
{-# LANGUAGE TupleSections #-}
6+
{-# LANGUAGE TypeFamilies #-}
7+
{-# LANGUAGE ViewPatterns #-}
48

59
module Text.Layout.Table.Cell where
610

7-
import Control.Monad (join)
11+
import Data.Bifunctor (Bifunctor(..))
12+
import Data.Kind (Type)
813
import qualified Data.Text as T
914

1015
import Text.Layout.Table.Primitives.AlignInfo
@@ -48,10 +53,20 @@ dropBoth l r = adjustCell (negate $ truncateNegative l) (negate $ truncateNegati
4853

4954
instance Applicative CellView where
5055
pure x = CellView x 0 0
51-
(CellView f l r) <*> (CellView x l' r') = CellView (f x) (l + l') (r + r')
56+
CellView f l r <*> CellView x l' r' = CellView (f x) (l + l') (r + r')
5257

5358
instance Monad CellView where
54-
(CellView x l r) >>= f = let CellView y l' r' = f x in CellView y (l + l') (r + r')
59+
CellView x l r >>= f = let CellView y l' r' = f x in CellView y (l + l') (r + r')
60+
61+
instance Semigroup a => Semigroup (CellView a) where
62+
CellView a l r <> CellView b l' r' = CellView (a <> b) (l + l') (r + r')
63+
64+
instance Monoid a => Monoid (CellView a) where
65+
mempty = pure mempty
66+
67+
-- | Build the contents of a 'CellView' and add padding.
68+
buildCellView :: StringBuilder b => (a -> b) -> CellView a -> b
69+
buildCellView build (CellView a l r) = spacesB l <> build a <> spacesB r
5570

5671
-- | The total amount of adjustment in 'CellView'.
5772
totalAdjustment :: CellView a -> Int
@@ -64,33 +79,40 @@ redistributeAdjustment l r a = CellView (baseCell a) lAdjustment rAdjustment
6479
lAdjustment = (totalAdjustment a * l) `div` (l + r)
6580
rAdjustment = totalAdjustment a - lAdjustment
6681

82+
6783
-- | Types that can be measured for visible characters, define a sub-string
6884
-- operation and turned into a 'StringBuilder'.
69-
class Cell a where
85+
class Monoid (DropAction a) => Cell a where
86+
-- | Describes the action necessary to drop width elements.
87+
type DropAction a :: Type
88+
7089
-- | Returns the length of the visible characters as displayed on the
7190
-- output medium.
7291
visibleLength :: a -> Int
7392

93+
-- | Determine the final visible length after requesting to drop width
94+
-- units, along with the action needed to accomplish that.
95+
dropLengthUnits :: Int -> Int -> a -> (Int, DropAction a)
96+
7497
-- | Measure the preceding and following characters for a position where
7598
-- the predicate matches.
7699
measureAlignment :: (Char -> Bool) -> a -> AlignInfo
77100

101+
-- | Evaluate a 'DropAction' and build the result.
102+
dropAction :: StringBuilder b => DropAction a -> a -> b
103+
78104
-- | Insert the contents into a 'StringBuilder'.
79105
buildCell :: StringBuilder b => a -> b
80-
buildCell = buildCellView . pure
81106

82-
-- | Insert the contents into a 'StringBuilder', padding or trimming as
83-
-- necessary.
84-
--
85-
-- The 'Cell' instance of 'CellView a' means that this can usually be
86-
-- substituted with 'buildCell', and is only needed for defining the
87-
-- instance.
88-
buildCellView :: StringBuilder b => CellView a -> b
89-
90-
{-# MINIMAL visibleLength, measureAlignment, buildCellView #-}
107+
{-# MINIMAL visibleLength, dropLengthUnits, measureAlignment, dropAction, buildCell #-}
91108

92109
instance Cell a => Cell (CellView a) where
110+
type DropAction (CellView a) = CellView (DropAction a)
93111
visibleLength (CellView a l r) = visibleLength a + l + r
112+
dropLengthUnits l r (CellView a l' r') =
113+
-- Asking to drop more than the padding which exists: adjust the amount dropped
114+
-- Asking to drop less than the padding: just reduce the padding
115+
second (adjustCell (max 0 $ l' - l) (max 0 $ r' - r)) $ dropLengthUnits (max 0 $ l - l') (max 0 $ r - r') a
94116
measureAlignment f (CellView a l r) = case mMatchRemaining of
95117
-- No match
96118
Nothing -> AlignInfo (truncateNegative $ matchAt + l + r) Nothing
@@ -100,91 +122,89 @@ instance Cell a => Cell (CellView a) where
100122
Just matchRemaining -> AlignInfo (matchAt + l) (Just $ matchRemaining + r)
101123
where
102124
AlignInfo matchAt mMatchRemaining = measureAlignment f a
103-
buildCell = buildCellView
104-
buildCellView = buildCellView . join
125+
126+
dropAction action a = buildCellView id $ dropAction <$> action <*> a
127+
buildCell (CellView a l r) =
128+
case (compare l 0, compare r 0) of
129+
(GT, GT) -> spacesB l <> buildCell a <> spacesB r
130+
(GT, LT) -> spacesB l <> dropAction (snd (dropLengthUnits 0 (negate r) a)) a
131+
(GT, EQ) -> spacesB l <> buildCell a
132+
(LT, GT) -> dropAction (snd (dropLengthUnits (negate l) 0 a)) a <> spacesB r
133+
(LT, LT) -> dropAction (snd (dropLengthUnits (negate l) (negate r) a)) a
134+
(LT, EQ) -> dropAction (snd (dropLengthUnits (negate l) 0 a)) a
135+
(EQ, GT) -> buildCell a <> spacesB r
136+
(EQ, LT) -> dropAction (snd (dropLengthUnits 0 (negate r) a)) a
137+
(EQ, EQ) -> buildCell a
105138

106139
instance Cell a => Cell (Maybe a) where
140+
type DropAction (Maybe a) = DropAction a
107141
visibleLength = maybe 0 visibleLength
142+
dropLengthUnits l r (Just a) = dropLengthUnits l r a
143+
dropLengthUnits _ _ Nothing = (0, mempty)
108144
measureAlignment p = maybe mempty (measureAlignment p)
145+
146+
dropAction action = maybe mempty (dropAction action)
109147
buildCell = maybe mempty buildCell
110-
buildCellView (CellView a l r) = maybe (spacesB $ l + r) (buildCellView . adjustCell l r) a
111148

112149
instance (Cell a, Cell b) => Cell (Either a b) where
150+
type DropAction (Either a b) = (DropAction a, DropAction b)
113151
visibleLength = either visibleLength visibleLength
152+
dropLengthUnits l r (Left a) = second (,mempty) $ dropLengthUnits l r a
153+
dropLengthUnits l r (Right a) = second (mempty,) $ dropLengthUnits l r a
114154
measureAlignment p = either (measureAlignment p) (measureAlignment p)
155+
156+
dropAction = uncurry either . bimap dropAction dropAction
115157
buildCell = either buildCell buildCell
116-
buildCellView (CellView a l r) = either go go a
117-
where
118-
go x = buildCellView $ CellView x l r
158+
159+
-- | How to drop width units from many common types.
160+
data DefaultDropAction
161+
= DropAll
162+
| Drop Int Int
163+
deriving (Show)
164+
165+
instance Semigroup DefaultDropAction where
166+
DropAll <> _ = DropAll
167+
_ <> DropAll = DropAll
168+
Drop l r <> Drop l' r' = Drop (l + l') (r + r')
169+
170+
instance Monoid DefaultDropAction where
171+
mempty = Drop 0 0
172+
173+
-- | Construct a drop specification when every unit has width exactly one.
174+
--
175+
-- This can be used for 'dropLengthUnits' in most cases.
176+
defaultDropLengthUnits :: Cell a => Int -> Int -> a -> (Int, DefaultDropAction)
177+
defaultDropLengthUnits (truncateNegative -> l) (truncateNegative -> r) a
178+
| l + r >= n = (0, DropAll)
179+
| otherwise = (n - l - r, Drop l r)
180+
where
181+
n = visibleLength a
119182

120183
instance Cell String where
184+
type DropAction String = DefaultDropAction
121185
visibleLength = length
186+
dropLengthUnits = defaultDropLengthUnits
122187
measureAlignment p xs = case break p xs of
123188
(ls, rs) -> AlignInfo (length ls) $ case rs of
124189
[] -> Nothing
125190
_ : rs' -> Just $ length rs'
126191

192+
dropAction DropAll _ = mempty
193+
dropAction (Drop l r) a = stringB . drop l $ zipWith const a (drop r a)
127194
buildCell = stringB
128-
buildCellView = buildCellViewLRHelper stringB drop (\n s -> zipWith const s $ drop n s)
129195

130196
instance Cell T.Text where
197+
type DropAction T.Text = DefaultDropAction
131198
visibleLength = T.length
199+
dropLengthUnits = defaultDropLengthUnits
132200
measureAlignment p xs = case T.break p xs of
133201
(ls, rs) -> AlignInfo (T.length ls) $ if T.null rs
134202
then Nothing
135203
else Just $ T.length rs - 1
136204

205+
dropAction DropAll = const mempty
206+
dropAction (Drop l r) = textB . T.drop l . T.dropEnd r
137207
buildCell = textB
138-
buildCellView = buildCellViewLRHelper textB T.drop T.dropEnd
139-
140-
-- | Construct 'buildCellView' from a builder function, a function for
141-
-- trimming from the left, and a function for trimming from the right.
142-
--
143-
-- Used to define instances of 'Cell'.
144-
buildCellViewLRHelper :: StringBuilder b
145-
=> (a -> b) -- ^ Builder function for 'a'.
146-
-> (Int -> a -> a) -- ^ Function for trimming on the left.
147-
-> (Int -> a -> a) -- ^ Function for trimming on the right.
148-
-> CellView a
149-
-> b
150-
buildCellViewLRHelper build trimL trimR =
151-
buildCellViewHelper build (\i -> build . trimL i) (\i -> build . trimR i) (\l r -> build . trimL l . trimR r)
152-
153-
-- | Construct 'buildCellView' from a builder function, and a function for
154-
-- trimming from the left and right simultaneously.
155-
--
156-
-- Used to define instanced of 'Cell'.
157-
buildCellViewBothHelper
158-
:: StringBuilder b
159-
=> (a -> b) -- ^ Builder function for 'a'.
160-
-> (Int -> Int -> a -> a) -- ^ Function for trimming on the left and right simultaneously.
161-
-> CellView a
162-
-> b
163-
buildCellViewBothHelper build trimBoth =
164-
buildCellViewHelper build (\i -> build . trimBoth i 0) (\i -> build . trimBoth 0 i) (\l r -> build . trimBoth l r)
165-
166-
-- | Construct 'buildCellView' from builder functions and trimming functions.
167-
--
168-
-- Used to define instances of 'Cell'.
169-
buildCellViewHelper
170-
:: StringBuilder b
171-
=> (a -> b) -- ^ Builder function for 'a'.
172-
-> (Int -> a -> b) -- ^ Function for trimming on the left.
173-
-> (Int -> a -> b) -- ^ Function for trimming on the right.
174-
-> (Int -> Int -> a -> b) -- ^ Function for trimming on the left and right simultaneously.
175-
-> CellView a
176-
-> b
177-
buildCellViewHelper build trimL trimR trimBoth (CellView a l r) =
178-
case (compare l 0, compare r 0) of
179-
(GT, GT) -> spacesB l <> build a <> spacesB r
180-
(GT, LT) -> spacesB l <> trimR (negate r) a
181-
(GT, EQ) -> spacesB l <> build a
182-
(LT, GT) -> trimL (negate l) a <> spacesB r
183-
(LT, LT) -> trimBoth (negate l) (negate r) a
184-
(LT, EQ) -> trimL (negate l) a
185-
(EQ, GT) -> build a <> spacesB r
186-
(EQ, LT) -> trimR (negate r) a
187-
(EQ, EQ) -> build a
188208

189209
-- | Creates a 'StringBuilder' with the amount of missing spaces.
190210
remSpacesB
@@ -477,9 +497,9 @@ buildCellMod
477497
-> CellMod c
478498
-> s
479499
buildCellMod cutMark CellMod {..} =
480-
-- 'buildCellView' takes care of padding and trimming.
500+
-- 'buildCell takes care of padding and trimming.
481501
applyMarkOrEmpty applyLeftMark leftCutMarkLenCM
482-
<> buildCellView (CellView baseCellCM leftAdjustmentCM rightAdjustmentCM)
502+
<> buildCell (CellView baseCellCM leftAdjustmentCM rightAdjustmentCM)
483503
<> applyMarkOrEmpty applyRightMark rightCutMarkLenCM
484504
where
485505
applyMarkOrEmpty applyMark k = if k > 0 then applyMark k else mempty

src/Text/Layout/Table/Cell/Formatted.hs

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
{-# LANGUAGE DeriveTraversable #-}
2-
{-# LANGUAGE LambdaCase #-}
1+
{-# LANGUAGE DeriveTraversable #-}
2+
{-# LANGUAGE LambdaCase #-}
3+
{-# LANGUAGE ScopedTypeVariables #-}
4+
{-# LANGUAGE TupleSections #-}
5+
{-# LANGUAGE TypeFamilies #-}
36

47
-- | Provides formatting to an instance of 'Cell'. For example, in a unix
58
-- terminal one could use the following:
@@ -19,7 +22,11 @@ module Text.Layout.Table.Cell.Formatted
1922
, cataFormatted
2023
) where
2124

25+
import Control.Applicative (ZipList(..))
26+
import Data.Bifunctor (Bifunctor(..))
27+
import Data.Foldable (toList)
2228
import Data.List (foldl', mapAccumL, mapAccumR)
29+
import Data.Monoid (Ap(..))
2330
import Data.String
2431

2532
import Text.Layout.Table.Primitives.AlignInfo
@@ -84,28 +91,37 @@ instance Monoid (Formatted a) where
8491
mempty = Empty
8592

8693
instance Cell a => Cell (Formatted a) where
94+
-- Use Ap ZipList so it has the correct Monoid instance
95+
type DropAction (Formatted a) = Ap ZipList (DropAction a)
8796
visibleLength = sum . fmap visibleLength
97+
dropLengthUnits = dropFormattedLengthUnits
8898
measureAlignment p = foldl' (mergeAlign p) mempty
89-
buildCell = buildFormatted buildCell
90-
buildCellView = buildCellViewHelper
91-
(buildFormatted buildCell)
92-
(\i -> buildFormatted buildCell . trimLeft i)
93-
(\i -> buildFormatted buildCell . trimRight i)
94-
(\l r -> buildFormatted buildCell . trimLeft l . trimRight r)
99+
100+
dropAction (Ap (ZipList actions)) = buildFormatted id . snd . mapAccumL tagDropAction actions
95101
where
96-
trimLeft i = snd . mapAccumL (dropTrackRemaining dropLeft) i
97-
trimRight i = snd . mapAccumR (dropTrackRemaining dropRight) i
102+
tagDropAction (d:ds) x = (ds, dropAction d x)
103+
tagDropAction [] x = ([], buildCell x)
104+
buildCell = buildFormatted buildCell
98105

99106
-- | Build 'Formatted' using a given constructor.
100107
buildFormatted :: StringBuilder b => (a -> b) -> Formatted a -> b
101108
buildFormatted build = cataFormatted mempty mconcat build (\p a s -> stringB p <> a <> stringB s)
102109

103-
-- | Drop characters either from the right or left, while also tracking the
104-
-- remaining number of characters to drop.
105-
dropTrackRemaining :: Cell a => (Int -> a -> CellView a) -> Int -> a -> (Int, CellView a)
106-
dropTrackRemaining dropF i a
107-
| i <= 0 = (0, pure a)
108-
| otherwise = let l = visibleLength a in (max 0 $ i - l, dropF i a)
110+
-- | Drop width units from 'Formatted'.
111+
dropFormattedLengthUnits :: forall a. Cell a => Int -> Int -> Formatted a -> (Int, DropAction (Formatted a))
112+
dropFormattedLengthUnits l r = extract . dropFromLeft . dropFromRight . fmap tagLength
113+
where
114+
extract = bimap sum (Ap . ZipList . toList) . unzip . map fst . toList
115+
dropFromLeft = snd . mapAccumL (\n -> dropTrackActions n 0) l
116+
dropFromRight = snd . mapAccumR (dropTrackActions 0) r
117+
tagLength x = let v = visibleLength x in ((v, mempty) , (v, x))
118+
119+
dropTrackActions :: Int -> Int -> ((Int, DropAction a), (Int, a)) -> (Int, ((Int, DropAction a), (Int, a)))
120+
dropTrackActions l' r' ((oldDroppedLength, oldAction), (fullLength, x)) =
121+
(remainingToDrop, ((truncateNegative $ oldDroppedLength + newDroppedLength - fullLength, oldAction <> newAction), (fullLength, x)))
122+
where
123+
remainingToDrop = l' + r' - (fullLength - newDroppedLength)
124+
(newDroppedLength, newAction) = dropLengthUnits l' r' x
109125

110126
-- | Run 'measureAlignment' with an initial state, as though we were measuring the alignment in chunks.
111127
mergeAlign :: Cell a => (Char -> Bool) -> AlignInfo -> a -> AlignInfo

0 commit comments

Comments
 (0)