~jaro/balkon

5543d47ef77d7240841a0d14ef7df1dfba00e75c — Jaro 1 year, 8 months ago 9cd91fb
Implement run splitting.
M balkon.cabal => balkon.cabal +2 -0
@@ 105,6 105,7 @@ library
        Data.Text.ParagraphLayout.ResolvedSpan,
        Data.Text.ParagraphLayout.Run,
        Data.Text.ParagraphLayout.Span,
        Data.Text.ParagraphLayout.TextContainer,
        Data.Text.Zipper

    -- Modules included in this library but not exported.


@@ 139,6 140,7 @@ test-suite balkon-test
        Data.Text.ParagraphLayout.RectSpec,
        Data.Text.ParagraphLayout.RunSpec,
        Data.Text.ParagraphLayout.SpanData,
        Data.Text.ParagraphLayout.TextContainerSpec,
        Data.Text.ZipperSpec

    -- Test dependencies.

M src/Data/Text/ParagraphLayout/Plain.hs => src/Data/Text/ParagraphLayout/Plain.hs +5 -0
@@ 45,6 45,7 @@ import Data.Text.ParagraphLayout.Rect
import qualified Data.Text.ParagraphLayout.ResolvedSpan as RS
import Data.Text.ParagraphLayout.Run
import Data.Text.ParagraphLayout.Span
import Data.Text.ParagraphLayout.TextContainer

-- | Text to be laid out as a paragraph.
--


@@ 94,6 95,10 @@ data WithSpan a = WithSpan RS.ResolvedSpan a
instance Functor WithSpan where
    fmap f (WithSpan s a) = WithSpan s (f a)

instance TextContainer a => TextContainer (WithSpan a) where
    getText (WithSpan _ c) = getText c
    setText t (WithSpan rs c) = WithSpan rs (setText t c)

splitBySpanIndex :: [WithSpan a] -> [[a]]
splitBySpanIndex xs = [getBySpanIndex i xs | i <- [0..]]


M src/Data/Text/ParagraphLayout/ResolvedSpan.hs => src/Data/Text/ParagraphLayout/ResolvedSpan.hs +5 -0
@@ 5,6 5,7 @@ import Data.Text (Text)
import Data.Text.Glyphize (Font)

import Data.Text.ParagraphLayout.LineHeight
import Data.Text.ParagraphLayout.TextContainer

-- | Internal structure containing resolved values that may be shared with
-- other spans across the paragraph.


@@ 19,3 20,7 @@ data ResolvedSpan = ResolvedSpan

instance Eq ResolvedSpan where
    a == b = spanIndex a == spanIndex b

instance TextContainer ResolvedSpan where
    getText = spanText
    setText t s = s { spanText = t }

M src/Data/Text/ParagraphLayout/Run.hs => src/Data/Text/ParagraphLayout/Run.hs +6 -0
@@ 9,9 9,11 @@ import Data.Text.Script (charScript)
import Data.Text.Zipper

import Data.Text.ParagraphLayout.ResolvedSpan
import Data.Text.ParagraphLayout.TextContainer

type ScriptCode = String


-- Each span can be broken into one or more runs by Balkón.
--
-- Each run could have a different script, language, or direction.


@@ 23,6 25,10 @@ data Run = Run
    }
    deriving (Eq, Show)

instance TextContainer Run where
    getText = runText
    setText t r = r { runText = t }

type ProtoRun = (Zipper, Maybe Direction, ScriptCode)

-- Represents a zipper that can advance by at least one character.

A src/Data/Text/ParagraphLayout/TextContainer.hs => src/Data/Text/ParagraphLayout/TextContainer.hs +44 -0
@@ 0,0 1,44 @@
module Data.Text.ParagraphLayout.TextContainer
    (TextContainer
    ,getText
    ,setText
    ,splitTextAt8
    ,splitTextsAt8
    )
where

import Data.Text (Text)
import Data.Text.Foreign (I8, dropWord8, lengthWord8, takeWord8)

class TextContainer a where
    getText :: a -> Text
    setText :: Text -> a -> a

-- | Split a text container at the given number of `Word8` units
-- from its beginning.
splitTextAt8 :: TextContainer a => I8 -> a -> (a, a)
splitTextAt8 n r = (setText text1 r, setText text2 r)
    where
        text1 = takeWord8 n $ getText r
        text2 = dropWord8 n $ getText r

-- | Treat a list of text containers as a contiguous sequence,
-- and make a split at the given number of `Word8` from the beginning
-- of this sequence.
--
-- If @n@ falls on a container boundary, the total number of output containers
-- will equal the number of input containers; otherwise, it will be one larger.
splitTextsAt8 :: TextContainer a => I8 -> [a] -> ([a], [a])
splitTextsAt8 n rs = (pre, post)
    where
        pre = reverse rpre
        (rpre, post) = splitTextsAt8' n [] rs

splitTextsAt8' :: TextContainer a => I8 -> [a] -> [a] -> ([a], [a])
splitTextsAt8' _ rpre [] = (rpre, [])
splitTextsAt8' n rpre (r:rs)
    | n <= 0 = (rpre, r:rs)
    | n >= l = splitTextsAt8' (n - l) (r:rpre) (rs)
    | otherwise = let (r1, r2) = splitTextAt8 n r in (r1:rpre, r2:rs)
    where
        l = fromIntegral $ lengthWord8 $ getText r

A test/Data/Text/ParagraphLayout/TextContainerSpec.hs => test/Data/Text/ParagraphLayout/TextContainerSpec.hs +82 -0
@@ 0,0 1,82 @@
module Data.Text.ParagraphLayout.TextContainerSpec (spec) where

import Data.Text (pack)
import Data.Text.Glyphize (Direction(..))

import Test.Hspec
import Data.Text.ParagraphLayout.Run
import Data.Text.ParagraphLayout.TextContainer

inputRuns :: [Run]
inputRuns =
    [ Run
        -- TODO: We might want both parentheses in the same run.
        { runText = pack "Vikipedija ("
        , runDirection = Just DirLTR
        , runScript = Just "Latn"
        }
    , Run
        { runText = pack "Википедија)"
        , runDirection = Just DirLTR
        , runScript = Just "Cyrl"
        }
    ]

spec :: Spec
spec = do
    describe "splitTextsAt8" $ do
        it "negative value splits at beginning of first run" $ do
            splitTextsAt8 (-1) inputRuns `shouldBe` ([], inputRuns)
        it "zero splits at beginning of first run" $ do
            splitTextsAt8 0 inputRuns `shouldBe` ([], inputRuns)
        it "splits in first run" $ do
            splitTextsAt8 11 inputRuns `shouldBe`
                (
                    [ Run
                        { runText = pack "Vikipedija "
                        , runDirection = Just DirLTR
                        , runScript = Just "Latn"
                        }
                    ]
                ,
                    [ Run
                        { runText = pack "("
                        , runDirection = Just DirLTR
                        , runScript = Just "Latn"
                        }
                    , Run
                        { runText = pack "Википедија)"
                        , runDirection = Just DirLTR
                        , runScript = Just "Cyrl"
                        }
                    ]
                )
        it "split at run edges does not generate extra run" $ do
            splitTextsAt8 12 inputRuns `shouldBe`
                (take 1 inputRuns, drop 1 inputRuns)
        it "splits in second run" $ do
            splitTextsAt8 20 inputRuns `shouldBe`
                (
                    [ Run
                        { runText = pack "Vikipedija ("
                        , runDirection = Just DirLTR
                        , runScript = Just "Latn"
                        }
                    , Run
                        { runText = pack "Вики"
                        , runDirection = Just DirLTR
                        , runScript = Just "Cyrl"
                        }
                    ]
                ,
                    [ Run
                        { runText = pack "педија)"
                        , runDirection = Just DirLTR
                        , runScript = Just "Cyrl"
                        }
                    ]
                )
        it "split at end does not generate extra run" $ do
            splitTextsAt8 33 inputRuns `shouldBe` (inputRuns, [])
        it "large value splits at end of last run" $ do
            splitTextsAt8 999 inputRuns `shouldBe` (inputRuns, [])