From cf3ec3e35ebdbbe540c0a1490545622157271543 Mon Sep 17 00:00:00 2001 From: Jaro Date: Fri, 23 Jun 2023 09:57:46 +0200 Subject: [PATCH] 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. --- .../spannedLoremIpsum20em.golden | 26 ++++- .../mixedLineHeight.golden | 54 ++++++++++- CHANGELOG.md | 4 + .../Text/ParagraphLayout/Internal/Layout.hs | 57 +++++------ .../ParagraphLayout/Internal/TextContainer.hs | 97 ++++++++----------- .../Internal/TextContainerSpec.hs | 86 +++++++++++++--- 6 files changed, 215 insertions(+), 109 deletions(-) diff --git a/.golden/paragraphLayout/spannedLoremIpsum20em.golden b/.golden/paragraphLayout/spannedLoremIpsum20em.golden index 6dce77d..37e7acc 100644 --- a/.golden/paragraphLayout/spannedLoremIpsum20em.golden +++ b/.golden/paragraphLayout/spannedLoremIpsum20em.golden @@ -70,10 +70,21 @@ 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) @@ -136,10 +147,21 @@ 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) diff --git a/.golden/richParagraphLayout/mixedLineHeight.golden b/.golden/richParagraphLayout/mixedLineHeight.golden index b4ae748..7a78ccc 100644 --- a/.golden/richParagraphLayout/mixedLineHeight.golden +++ b/.golden/richParagraphLayout/mixedLineHeight.golden @@ -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" @@ -85,6 +85,16 @@ ParagraphLayout , (GlyphInfo {codepoint = 80, cluster = 37, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 861, y_advance = 0, x_offset = 0, y_offset = 0}) ] } + , 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 @@ -135,6 +145,16 @@ ParagraphLayout , (GlyphInfo {codepoint = 72, cluster = 57, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 559, y_advance = 0, x_offset = 0, y_offset = 0}) ] } + , 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 @@ -168,6 +188,16 @@ ParagraphLayout , (GlyphInfo {codepoint = 79, cluster = 71, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 273, y_advance = 0, x_offset = 0, y_offset = 0}) ] } + , 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 @@ -201,6 +231,16 @@ ParagraphLayout , (GlyphInfo {codepoint = 80, cluster = 85, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 861, y_advance = 0, x_offset = 0, y_offset = 0}) ] } + , 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 @@ -217,13 +257,23 @@ ParagraphLayout , (GlyphInfo {codepoint = 79, cluster = 92, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 273, y_advance = 0, x_offset = 0, y_offset = 0}) ] } + , 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}) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8110c3..e63b51e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/Data/Text/ParagraphLayout/Internal/Layout.hs b/src/Data/Text/ParagraphLayout/Internal/Layout.hs index c6f974d..9421634 100644 --- a/src/Data/Text/ParagraphLayout/Internal/Layout.hs +++ b/src/Data/Text/ParagraphLayout/Internal/Layout.hs @@ -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 diff --git a/src/Data/Text/ParagraphLayout/Internal/TextContainer.hs b/src/Data/Text/ParagraphLayout/Internal/TextContainer.hs index f926c9e..a916f81 100644 --- a/src/Data/Text/ParagraphLayout/Internal/TextContainer.hs +++ b/src/Data/Text/ParagraphLayout/Internal/TextContainer.hs @@ -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 diff --git a/test/Data/Text/ParagraphLayout/Internal/TextContainerSpec.hs b/test/Data/Text/ParagraphLayout/Internal/TextContainerSpec.hs index 393c0ad..7b421df 100644 --- a/test/Data/Text/ParagraphLayout/Internal/TextContainerSpec.hs +++ b/test/Data/Text/ParagraphLayout/Internal/TextContainerSpec.hs @@ -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] -- 2.30.2