~jaro/balkon

cf3ec3e35ebdbbe540c0a1490545622157271543 — Jaro 10 months ago 7e49e20
Allow trimmed whitespace to generate empty fragments.

BREAKING: Output may now contain more fragments with zero glyphs,
representing runs of whitespace that was trimmed away. These runs may
also start a box at the end of a line, instead of the beginning of the
next line.

BREAKING: Trimmed whitespace now affects line height.
M .golden/paragraphLayout/spannedLoremIpsum20em.golden => .golden/paragraphLayout/spannedLoremIpsum20em.golden +24 -2
@@ 71,9 71,20 @@ ParagraphLayout
        SpanLayout
        [ Fragment
            { fragmentUserData = ()
            , fragmentLine = 1
            , fragmentAncestorBoxes =
                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
                ]
            , fragmentRect = Rect {x_origin = 18310, y_origin = 0, x_size = 0, y_size = -1121}
            , fragmentPen = (0, -932)
            , fragmentGlyphs =
                []
            }
        , Fragment
            { fragmentUserData = ()
            , fragmentLine = 2
            , fragmentAncestorBoxes =
                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = SpacedEdge 0, boxStartEdge = SpacedEdge 0, boxEndEdge = SpacedEdge 0}
                [ AncestorBox {boxUserData = (), boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
                ]
            , fragmentRect = Rect {x_origin = 0, y_origin = -1121, x_size = 8553, y_size = -1121}
            , fragmentPen = (0, -932)


@@ 137,9 148,20 @@ ParagraphLayout
        SpanLayout
        [ Fragment
            { fragmentUserData = ()
            , fragmentLine = 2
            , fragmentAncestorBoxes =
                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
                ]
            , fragmentRect = Rect {x_origin = 17443, y_origin = -1121, x_size = 0, y_size = -1121}
            , fragmentPen = (0, -932)
            , fragmentGlyphs =
                []
            }
        , Fragment
            { fragmentUserData = ()
            , fragmentLine = 3
            , fragmentAncestorBoxes =
                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = SpacedEdge 0, boxStartEdge = SpacedEdge 0, boxEndEdge = SpacedEdge 0}
                [ AncestorBox {boxUserData = (), boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
                ]
            , fragmentRect = Rect {x_origin = 0, y_origin = -2242, x_size = 9114, y_size = -1121}
            , fragmentPen = (0, -932)

M .golden/richParagraphLayout/mixedLineHeight.golden => .golden/richParagraphLayout/mixedLineHeight.golden +52 -2
@@ 1,5 1,5 @@
ParagraphLayout
    { paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 16708, y_size = -8900}
    { paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 16708, y_size = -9400}
    , paragraphFragments =
        [ Fragment
            { fragmentUserData = "mediumText"


@@ 86,6 86,16 @@ ParagraphLayout
                ]
            }
        , Fragment
            { fragmentUserData = "lineBreak"
            , fragmentLine = 1
            , fragmentAncestorBoxes =
                []
            , fragmentRect = Rect {x_origin = 16708, y_origin = 0, x_size = 0, y_size = -1300}
            , fragmentPen = (0, -1022)
            , fragmentGlyphs =
                []
            }
        , Fragment
            { fragmentUserData = "smallText"
            , fragmentLine = 2
            , fragmentAncestorBoxes =


@@ 136,6 146,16 @@ ParagraphLayout
                ]
            }
        , Fragment
            { fragmentUserData = "lineBreak"
            , fragmentLine = 2
            , fragmentAncestorBoxes =
                []
            , fragmentRect = Rect {x_origin = 8852, y_origin = -1700, x_size = 0, y_size = -1300}
            , fragmentPen = (0, -1022)
            , fragmentGlyphs =
                []
            }
        , Fragment
            { fragmentUserData = "mediumText"
            , fragmentLine = 3
            , fragmentAncestorBoxes =


@@ 169,6 189,16 @@ ParagraphLayout
                ]
            }
        , Fragment
            { fragmentUserData = "lineBreak"
            , fragmentLine = 3
            , fragmentAncestorBoxes =
                []
            , fragmentRect = Rect {x_origin = 6303, y_origin = -3400, x_size = 0, y_size = -1300}
            , fragmentPen = (0, -1022)
            , fragmentGlyphs =
                []
            }
        , Fragment
            { fragmentUserData = "largeText"
            , fragmentLine = 4
            , fragmentAncestorBoxes =


@@ 202,6 232,16 @@ ParagraphLayout
                ]
            }
        , Fragment
            { fragmentUserData = "lineBreak"
            , fragmentLine = 4
            , fragmentAncestorBoxes =
                []
            , fragmentRect = Rect {x_origin = 6246, y_origin = -4700, x_size = 0, y_size = -1300}
            , fragmentPen = (0, -1022)
            , fragmentGlyphs =
                []
            }
        , Fragment
            { fragmentUserData = "smallText"
            , fragmentLine = 5
            , fragmentAncestorBoxes =


@@ 218,12 258,22 @@ ParagraphLayout
                ]
            }
        , Fragment
            { fragmentUserData = "lineBreak"
            , fragmentLine = 5
            , fragmentAncestorBoxes =
                []
            , fragmentRect = Rect {x_origin = 2375, y_origin = -6400, x_size = 0, y_size = -1300}
            , fragmentPen = (0, -1022)
            , fragmentGlyphs =
                []
            }
        , Fragment
            { fragmentUserData = "largeText"
            , fragmentLine = 6
            , fragmentAncestorBoxes =
                [ AncestorBox {boxUserData = "largeBox", boxLeftEdge = SpacedEdge 0, boxRightEdge = SpacedEdge 0, boxStartEdge = SpacedEdge 0, boxEndEdge = SpacedEdge 0}
                ]
            , fragmentRect = Rect {x_origin = 0, y_origin = -7200, x_size = 2318, y_size = -1700}
            , fragmentRect = Rect {x_origin = 0, y_origin = -7700, x_size = 2318, y_size = -1700}
            , fragmentPen = (0, -1222)
            , fragmentGlyphs =
                [ (GlyphInfo {codepoint = 79, cluster = 95, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 273, y_advance = 0, x_offset = 0, y_offset = 0})

M CHANGELOG.md => CHANGELOG.md +4 -0
@@ 14,6 14,10 @@
* Added function `paragraphSafeWidth` to help calculating max-content width
  for CSS.

* Trimmed white space at the beginning and end of lines may now generate
  fragments with zero glyphs, to more closely match CSS behaviour.
  These fragments may affect line height and/or the start edge of boxes.

* Text will now overflow the end edge of the paragraph according to the
  text direction of the root box, instead of always overflowing the right edge.


M src/Data/Text/ParagraphLayout/Internal/Layout.hs => src/Data/Text/ParagraphLayout/Internal/Layout.hs +26 -31
@@ 10,7 10,7 @@ import Data.Int (Int32)
import Data.List (mapAccumL)
import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty, (<|))
import qualified Data.List.NonEmpty as NonEmpty
import Data.Maybe (catMaybes, fromMaybe)
import Data.Maybe (fromMaybe)
import Data.Text.Foreign (lengthWord8)
import Data.Text.Glyphize
    ( Buffer (..)


@@ 64,30 64,26 @@ layoutAndAlignLines
    -> [FragmentWithSpan d]
layoutAndAlignLines dir align maxWidth runs = frags
    where
        frags = concatMap NonEmpty.toList fragsInLines
        frags = concatMap toList fragsInLines
        (_, fragsInLines) = mapAccumL positionLine originY numberedLines
        positionLine = positionLineH dir align maxWidth
        numberedLines = zip [1 ..] canonicalLines
        canonicalLines = fmap reorderProtoFragments visibleLines
        visibleLines = filter PL.visible logicalLines
        logicalLines = nonEmptyItems $ layoutLines maxWidth [] runs
        logicalLines = toList $ layoutLines maxWidth [] runs
        originY = paragraphOriginY

reorderProtoFragments :: PL.ProtoLine NonEmpty d -> PL.ProtoLine NonEmpty d
reorderProtoFragments pl@(PL.ProtoLine { PL.protoFragments = pfs }) =
    pl { PL.protoFragments = reorder pfs }

nonEmptyItems :: Foldable t =>
    t (PL.ProtoLine [] d) -> [PL.ProtoLine NonEmpty d]
nonEmptyItems = catMaybes . map PL.nonEmpty . toList

-- | Create a multi-line layout from the given runs, splitting them as
-- necessary to fit within the requested line width.
--
-- The output is a two-dimensional list of fragments positioned along the
-- horizontal axis.
layoutLines :: Int32 -> [RB.ResolvedBox d] -> NonEmpty (WithSpan d Run) ->
    NonEmpty (PL.ProtoLine [] d)
    NonEmpty (PL.ProtoLine NonEmpty d)
layoutLines maxWidth openBoxes runs = case nonEmpty rest of
        -- Everything fits. We are done.
        Nothing -> fitting :| []


@@ 95,10 91,9 @@ layoutLines maxWidth openBoxes runs = case nonEmpty rest of
        Just runs' -> fitting <| layoutLines maxWidth openBoxes' runs'
    where
        (fitting, rest) = layoutAndWrapRunsH maxWidth openBoxes runs
        -- Update the list of open boxes using the logically last run on this
        -- line, if there is one.
        openBoxes' =
            openBoxes `fromMaybe` lastSpanBoxes (PL.protoFragments fitting)
        -- Update the list of open boxes using the logically last run
        -- on this line.
        openBoxes' = lastSpanBoxes $ PL.protoFragments fitting

-- | Position all the given horizontal fragments on the same line,
-- using @originY@ as its top edge, and return the bottom edge for continuation.


@@ 231,7 226,7 @@ layoutAndWrapRunsH
    :: Int32
    -> [RB.ResolvedBox d]
    -> NonEmpty (WithSpan d Run)
    -> (PL.ProtoLine [] d, [WithSpan d Run])
    -> (PL.ProtoLine NonEmpty d, [WithSpan d Run])
layoutAndWrapRunsH maxWidth prevOpenBoxes runs = NonEmpty.head $ validProtoLines
    where
        validProtoLines = dropWhile1 tooLong layouts


@@ 250,9 245,9 @@ layoutAndWrapRunsH maxWidth prevOpenBoxes runs = NonEmpty.head $ validProtoLines
-- to determine `PL.nextOpenBoxes`.
protoLine
    :: [RB.ResolvedBox d]
    -> [ProtoFragmentWithSpan d]
    -> NonEmpty (ProtoFragmentWithSpan d)
    -> [WithSpan d Run]
    -> PL.ProtoLine [] d
    -> PL.ProtoLine NonEmpty d
protoLine prev pfs rest = PL.ProtoLine pfs prev next
    where
        next = [] `fromMaybe` firstSpanBoxes rest


@@ 262,10 257,9 @@ firstSpanBoxes xs = case xs of
    [] -> Nothing
    (WithSpan rs _) : _ -> Just $ RS.spanBoxes rs

lastSpanBoxes :: [WithSpan d a] -> Maybe [RB.ResolvedBox d]
lastSpanBoxes xs = case reverse xs of
    [] -> Nothing
    (WithSpan rs _) : _ -> Just $ RS.spanBoxes rs
lastSpanBoxes :: NonEmpty (WithSpan d a) -> [RB.ResolvedBox d]
lastSpanBoxes xs = case NonEmpty.last xs of
    WithSpan rs _ -> RS.spanBoxes rs

-- | Treat a list of runs as a contiguous sequence, and split them into two
-- lists so that the first list contains as many non-whitespace characters as


@@ 281,21 275,20 @@ lastSpanBoxes xs = case reverse xs of
--
-- If there is no hard line break in the input, the first output list will
-- contain the whole input, and the second output list will be empty.
hardSplit :: NonEmpty (WithSpan d Run) -> ([WithSpan d Run], [WithSpan d Run])
hardSplit :: NonEmpty (WithSpan d Run) ->
    (NonEmpty (WithSpan d Run), [WithSpan d Run])
hardSplit runs = case reverse hSplits of
    [] -> noSplit
    (splitRuns : _) -> forcedSplit splitRuns
    where
        noSplit =
            (NonEmpty.toList (trim runs), [])
        forcedSplit (runs1, runs2) =
            (NonEmpty.toList $ markHard $ trim runs1, runs2)
        noSplit = (trim runs, [])
        forcedSplit (runs1, runs2) = (markHard $ trim runs1, runs2)
        markHard = mapLast markHard'
        markHard' (WithSpan rs x) = WithSpan rs x { runHardBreak = True }
        trim
            = trimTextsStartPreserve isStartSpace
            . trimTextsEndPreserve isEndSpace
            . trimTextsEndPreserve isNewline
            = dropWhileStartCascade isStartSpace
            . dropWhileEndCascade isEndSpace
            . dropWhileEndCascade isNewline
        -- TODO: Consider optimising.
        --       We do not need to look for any line breaks further than the
        --       shortest hard break.


@@ 323,11 316,13 @@ mapLast f xs = case NonEmpty.uncons xs of
-- closer to the start of the list are preferred for line breaking, but without
-- considering overflows.
softSplits :: NonEmpty (WithSpan d Run) ->
    [([WithSpan d Run], [WithSpan d Run])]
    [(NonEmpty (WithSpan d Run), [WithSpan d Run])]
softSplits runs = map (allowSndEmpty . trimFst) splits
    where
        trimFst (runs1, runs2) = (trim runs1, runs2)
        trim = trimTextsStart isStartSpace . trimTextsEnd isEndSpace
        trim
            = dropWhileStartCascade isStartSpace
            . dropWhileEndCascade isEndSpace
        splits = lSplits ++ cSplits
        lSplits = nonEmptyPairs $
            splitTextsBy (map fst . runLineBreaks) runs


@@ 349,8 344,8 @@ dropWhile1 p list = case NonEmpty.uncons list of

-- | Calculate layout for multiple horizontal runs on the same line, without
-- any breaking.
layoutRunsH :: [WithSpan d Run] -> [ProtoFragmentWithSpan d]
layoutRunsH runs = map layoutRunH runs
layoutRunsH :: Functor f => f (WithSpan d Run) -> f (ProtoFragmentWithSpan d)
layoutRunsH runs = fmap layoutRunH runs

-- | Calculate layout for the given horizontal run and attach extra information.
layoutRunH :: WithSpan d Run -> ProtoFragmentWithSpan d

M src/Data/Text/ParagraphLayout/Internal/TextContainer.hs => src/Data/Text/ParagraphLayout/Internal/TextContainer.hs +38 -59
@@ 2,20 2,18 @@ module Data.Text.ParagraphLayout.Internal.TextContainer
    ( SeparableTextContainer
    , TextContainer
    , dropWhileEnd
    , dropWhileEndCascade
    , dropWhileStart
    , dropWhileStartCascade
    , getText
    , splitTextAt8
    , splitTextsBy
    , trimTextsEnd
    , trimTextsEndPreserve
    , trimTextsStart
    , trimTextsStartPreserve
    )
where

import Data.Foldable (toList)
import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import Data.List (mapAccumL, mapAccumR)
import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Text.Foreign (dropWord8, takeWord8)


@@ 88,61 86,42 @@ collapse (tc :| tcs)
-- | Treat a list of text containers as a contiguous sequence,
-- and remove a prefix of characters that match the given predicate.
--
-- Empty text containers are removed from the output, so the result may
-- potentially be an empty list.
trimTextsStart :: (SeparableTextContainer a, Foldable f) =>
    (Char -> Bool) -> f a -> [a]
trimTextsStart p tcs = trimTextsStart' p $ toList tcs

-- | Treat a list of text containers as a contiguous sequence,
-- and remove a prefix of characters that match the given predicate.
--
-- Empty text containers are removed from the output except the first one,
-- which is instead truncated to zero length.
trimTextsStartPreserve :: SeparableTextContainer a =>
    (Char -> Bool) -> NonEmpty a -> NonEmpty a
trimTextsStartPreserve p tcs =
    case nonEmpty $ trimTextsStart p $ NonEmpty.toList tcs of
        Nothing -> truncateText (NonEmpty.head tcs) :| []
        Just out -> out
-- All text containers are preserved but their contents may end up having
-- zero length.
dropWhileStartCascade :: (SeparableTextContainer a, Traversable t) =>
    (Char -> Bool) -> t a -> t a
dropWhileStartCascade p tcs = trimTextsStartCascade (dropWhileStart p) tcs

-- | Treat a list of text containers as a contiguous sequence,
-- and remove a suffix of characters that match the given predicate.
--
-- Empty text containers are removed from the output, so the result may
-- potentially be an empty list.
trimTextsEnd :: (SeparableTextContainer a, Foldable f) =>
    (Char -> Bool) -> f a -> [a]
trimTextsEnd p tcs = trimTextsEnd' p $ reverse $ toList tcs

-- | Treat a list of text containers as a contiguous sequence,
-- and remove a suffix of characters that match the given predicate.
--
-- Empty text containers are removed from the output except the first one,
-- which is instead truncated to zero length.
trimTextsEndPreserve :: SeparableTextContainer a =>
    (Char -> Bool) -> NonEmpty a -> NonEmpty a
trimTextsEndPreserve p tcs =
    case nonEmpty $ trimTextsEnd p $ NonEmpty.toList tcs of
        Nothing -> truncateText (NonEmpty.head tcs) :| []
        Just out -> out

trimTextsStart' :: SeparableTextContainer a => (Char -> Bool) -> [a] -> [a]
trimTextsStart' _ [] = []
trimTextsStart' p (tc : tcs)
    | Text.null (getText trimmed) = trimTextsStart' p tcs
    | otherwise = trimmed : tcs
-- All text containers are preserved but their contents may end up having
-- zero length.
dropWhileEndCascade :: (SeparableTextContainer a, Traversable t) =>
    (Char -> Bool) -> t a -> t a
dropWhileEndCascade p tcs = trimTextsEndCascade (dropWhileEnd p) tcs

-- | Traverse the given structure from start to end, applying the given
-- text trimming function to each text container until a non-empty container
-- is produced.
trimTextsStartCascade :: (SeparableTextContainer a, Traversable t) =>
    (a -> a) -> t a -> t a
trimTextsStartCascade trimFunc tcs =
    snd $ mapAccumL (cascadingTrim trimFunc) True tcs

-- | Traverse the given structure from end to start, applying the given
-- text trimming function to each text container until a non-empty container
-- is produced.
trimTextsEndCascade :: (SeparableTextContainer a, Traversable t) =>
    (a -> a) -> t a -> t a
trimTextsEndCascade trimFunc tcs =
    snd $ mapAccumR (cascadingTrim trimFunc) True tcs

-- | Wraps a text trimming function in a controlled cascade.
-- When the trim produces an empty text, the cascade continues.
cascadingTrim :: SeparableTextContainer a => (a -> a) -> Bool -> a -> (Bool, a)
cascadingTrim _ False tc = (False, tc)
cascadingTrim trimFunc True tc = (continue, trimmed)
    where
        trimmed = dropWhileStart p tc

trimTextsEnd' :: SeparableTextContainer a => (Char -> Bool) -> [a] -> [a]
trimTextsEnd' _ [] = []
trimTextsEnd' p (tc : tcs)
    | Text.null (getText trimmed) = trimTextsEnd' p tcs
    | otherwise = reverse $ trimmed : tcs
    where
        trimmed = dropWhileEnd p tc

-- | Discard all text from the container by creating a prefix of length 0.
truncateText :: SeparableTextContainer a => a -> a
truncateText tc = fst $ splitTextAt8 0 tc
        trimmed = trimFunc tc
        continue = Text.null $ getText trimmed

M test/Data/Text/ParagraphLayout/Internal/TextContainerSpec.hs => test/Data/Text/ParagraphLayout/Internal/TextContainerSpec.hs +71 -15
@@ 87,7 87,9 @@ isSpace = (== ' ')

spec :: Spec
spec = do

    describe "splitTextsBy" $ do

        it "splits example text containers (start bias)" $ do
            splitTextsBy startBiasedBreakPoints exampleContainers `shouldBe`
                [ ( [ contain "Vikipedija " 10, contain "(Википеди" 21 ]


@@ 121,6 123,7 @@ spec = do
                  , [ contain "Vikipedija " 10, contain "(Википедија)" 21 ]
                  )
                ]

        it "splits example text containers (end bias)" $ do
            splitTextsBy endBiasedBreakPoints exampleContainers `shouldBe`
                [ ( [ contain "Vikipedija " 10, contain "(Википедија)" 21 ]


@@ 154,30 157,83 @@ spec = do
                  , [ contain "kipedija " 12, contain "(Википедија)" 21 ]
                  )
                ]
    describe "trimTextsEnd" $ do

    describe "dropWhileStartCascade" $ do

        describe "isSpace" $ do

            it "does nothing on an empty list" $ do
                let inputTexts = [] :: [Text]
                trimTextsEnd isSpace inputTexts `shouldBe` inputTexts
            it "does nothing when last run does not end with space" $ do
                dropWhileStartCascade isSpace inputTexts `shouldBe` inputTexts

            it "does nothing on list of empty texts" $ do
                let inputTexts = [empty, empty, empty]
                dropWhileStartCascade isSpace inputTexts `shouldBe` inputTexts

            it "does nothing when first run does not start with space" $ do
                let inputTexts = [pack "some ", pack "text"]
                trimTextsEnd isSpace inputTexts `shouldBe` inputTexts
            it "trims empty texts down to an empty list" $ do
                dropWhileStartCascade isSpace inputTexts `shouldBe` inputTexts

            it "does nothing when first non-empty run does not start with space" $ do
                let inputTexts = [empty, empty, pack "some ", pack "text"]
                dropWhileStartCascade isSpace inputTexts `shouldBe` inputTexts

            it "trims spaces from first text" $ do
                let inputTexts = [pack "      some ", pack "text"]
                dropWhileStartCascade isSpace inputTexts `shouldBe`
                    [pack "some ", pack "text"]

            it "trims texts containing only spaces to empty" $ do
                let inputTexts = [pack "    ", pack "some ", pack "text"]
                dropWhileStartCascade isSpace inputTexts `shouldBe`
                    [empty, pack "some ", pack "text"]

            it "trims first text that contains non-spaces" $ do
                let inputTexts = [pack "    ", pack "    some ", pack "text  "]
                dropWhileStartCascade isSpace inputTexts `shouldBe`
                    [empty, pack "some ", pack "text  "]

            it "trims space-only input down to empty texts" $ do
                let inputTexts = [pack " ", pack "  ", pack "   "]
                dropWhileStartCascade isSpace inputTexts `shouldBe`
                    [empty, empty, empty]

    describe "dropWhileEndCascade" $ do

        describe "isSpace" $ do

            it "does nothing on an empty list" $ do
                let inputTexts = [] :: [Text]
                dropWhileEndCascade isSpace inputTexts `shouldBe` inputTexts

            it "does nothing on list of empty texts" $ do
                let inputTexts = [empty, empty, empty]
                trimTextsEnd isSpace inputTexts `shouldBe` []
            it "trims empty texts from a list" $ do
                dropWhileEndCascade isSpace inputTexts `shouldBe` inputTexts

            it "does nothing when last run does not end with space" $ do
                let inputTexts = [pack "some ", pack "text"]
                dropWhileEndCascade isSpace inputTexts `shouldBe` inputTexts

            it "does nothing when last non-empty run does not end with space" $ do
                let inputTexts = [pack "some ", pack "text", empty, empty]
                trimTextsEnd isSpace inputTexts `shouldBe`
                    [pack "some ", pack "text"]
                dropWhileEndCascade isSpace inputTexts `shouldBe` inputTexts

            it "trims spaces from last text" $ do
                let inputTexts = [pack "some ", pack "text      "]
                trimTextsEnd isSpace inputTexts `shouldBe`
                dropWhileEndCascade isSpace inputTexts `shouldBe`
                    [pack "some ", pack "text"]
            it "trims texts containing only spaces" $ do

            it "trims texts containing only spaces to empty" $ do
                let inputTexts = [pack "some ", pack "text", pack "    "]
                trimTextsEnd isSpace inputTexts `shouldBe`
                    [pack "some ", pack "text"]
                dropWhileEndCascade isSpace inputTexts `shouldBe`
                    [pack "some ", pack "text", empty]

            it "trims last text that contains non-spaces" $ do
                let inputTexts = [pack "some ", pack "text  ", pack "    "]
                trimTextsEnd isSpace inputTexts `shouldBe`
                    [pack "some ", pack "text"]
                dropWhileEndCascade isSpace inputTexts `shouldBe`
                    [pack "some ", pack "text", empty]

            it "trims space-only input down to empty texts" $ do
                let inputTexts = [pack " ", pack "  ", pack "   "]
                dropWhileEndCascade isSpace inputTexts `shouldBe`
                    [empty, empty, empty]