packages feed

balkon-0.3.0.0: test/Data/Text/ParagraphLayoutSpec.hs

module Data.Text.ParagraphLayoutSpec (spec) where

import Data.Text.Glyphize
    ( Font
    , FontOptions (optionPPEm, optionScale)
    , defaultFontOptions
    )

import Test.Hspec
import Test.Hspec.Golden
import System.FilePath ((</>))
import Data.Text.ParagraphLayout
import Data.Text.ParagraphLayout.FontLoader
import Data.Text.ParagraphLayout.Internal.Fragment
import Data.Text.ParagraphLayout.Internal.Plain.ParagraphLayout
import Data.Text.ParagraphLayout.ParagraphData
import Data.Text.ParagraphLayout.Rect

class PrettyShow a where
    prettyShow :: a -> String

newtype ShapedRuns = ShapedRuns { getShapedRuns :: [ShapedRun] }
    deriving (Eq)

instance PrettyShow ShapedRuns where
    prettyShow (ShapedRuns xs) = concat
        [ "["
        , concat $ indentedList indent0 $ map (prettyShow . ShapedRun') xs
        , "]"
        ]

newtype ShapedRun' = ShapedRun' ShapedRun

instance PrettyShow ShapedRun' where
    prettyShow (ShapedRun' (x, y, glyphs)) = concat
        [ "("
        , show x
        , ","
        , show y
        , ","
        , newline
        , indent1
        , "["
        , concat $ indentedList indent1 $ map show glyphs
        , "]"
        , newline
        , ")"
        ]

type Page = (PageContinuity, ParagraphLayout)

newtype Pages = Pages { getPages :: [Page] }
    deriving (Eq)

instance PrettyShow Pages where
    prettyShow (Pages ps) = concat
        [ "["
        , concat $ indentedList indent0 $ map (prettyShow . Page') ps
        , "]"
        ]

newtype Page' = Page' Page

instance PrettyShow Page' where
    prettyShow (Page' (c, pl)) = concat
        [ "("
        , show c
        , ", "
        , prettyShow pl
        , ")"
        ]

instance PrettyShow ParagraphLayout where
    prettyShow (ParagraphLayout pr sls) = concat
        [ "ParagraphLayout {paragraphRect = "
        , show pr
        , commaSpace
        , "spanLayouts = ["
        , newline
        , indent1
        , concat $ indentedList indent1 $ map prettyShow sls
        , newline
        , "]}"
        , newline
        ]

instance PrettyShow SpanLayout where
    prettyShow (SpanLayout frags) = concat
        [ "SpanLayout ["
        , concat $ inlineList $ map prettyShow frags
        , "]"
        ]

instance PrettyShow Fragment where
    prettyShow (Fragment r pen glyphs) = concat
        [ "Fragment {fragmentRect = "
        , show r
        , commaSpace
        , "fragmentPen = "
        , show pen
        , commaSpace
        , "fragmentGlyphs ="
        , newline
        , indent2
        , "["
        , concat $ indentedList indent2 $ map show glyphs
        , "]"
        , newline
        , indent1
        , "}"
        ]

inlineList :: [String] -> [String]
inlineList items = suffixInit commaSpace items

indentedList :: String -> [String] -> [String]
indentedList indent items = prefixTail indent $ suffixInit commaNewline items

suffixInit :: String -> [String] -> [String]
suffixInit suffix = mapInit (++ suffix)

mapInit :: (a -> a) -> [a] -> [a]
mapInit _ [] = []
mapInit _ [x] = [x]
mapInit f (x : y : ys) = f x : mapInit f (y : ys)

prefixTail :: String -> [String] -> [String]
prefixTail prefix = mapTail (prefix ++)

mapTail :: (a -> a) -> [a] -> [a]
mapTail _ [] = []
mapTail f (x : xs) = x : (map f xs)

indent0 :: String
indent0 = ""

indent1 :: String
indent1 = "    "

indent2 :: String
indent2 = indent1 ++ indent1

newline :: String
newline = "\n"

commaSpace :: String
commaSpace = ", "

commaNewline :: String
commaNewline = "," ++ newline

class ShouldBeGolden a where
    shouldBeGolden :: a -> FilePath -> Golden a

instance ShouldBeGolden ParagraphLayout where
    shouldBeGolden output_ name = Golden
        { output = output_
        , encodePretty = show
        , writeToFile = \ path -> writeFile path . prettyShow
        , readFromFile = \ path -> readFile path >>= return . read
        , goldenFile = ".golden" </> name </> "golden"
        , actualFile = Just (".golden" </> name </> "actual")
        , failFirstTime = True
        }

instance ShouldBeGolden Pages where
    shouldBeGolden output_ name = Golden
        { output = output_
        , encodePretty = show . getPages
        , writeToFile = \ path -> writeFile path . prettyShow
        , readFromFile = \ path -> readFile path >>= return . Pages . read
        , goldenFile = ".golden" </> name </> "golden"
        , actualFile = Just (".golden" </> name </> "actual")
        , failFirstTime = True
        }

instance ShouldBeGolden ShapedRuns where
    shouldBeGolden output_ name = Golden
        { output = output_
        , encodePretty = show . getShapedRuns
        , writeToFile = \ path -> writeFile path . prettyShow
        , readFromFile = \ path -> readFile path >>= return . ShapedRuns . read
        , goldenFile = ".golden" </> "shapedRuns" </> name </> "golden"
        , actualFile = Just (".golden" </> "shapedRuns" </> name </> "actual")
        , failFirstTime = True
        }

fontInfoPath :: String -> FilePath
fontInfoPath name = ".golden" </> "shapedRuns" </> name </> "fontInfo"

shapedRuns' :: ParagraphLayout -> ShapedRuns
shapedRuns' = ShapedRuns . shapedRuns

emptyLayout :: ParagraphLayout
emptyLayout = ParagraphLayout (Rect 0 0 0 0) []

emptySpanLayout :: ParagraphLayout
emptySpanLayout = ParagraphLayout (Rect 0 0 0 0) [SpanLayout []]

-- Test primarily with a high level of detail (1000 units per EM).
-- Hinting should behave as if the font size were 24px.
-- TODO: Test hinting.
testingOptions :: FontOptions
testingOptions = defaultFontOptions {
    optionPPEm = Just (24, 24),
    optionScale = Just (1000, 1000)
}

-- For the demo, use a 15px font size with exactly one scale unit per pixel.
demoOptions :: FontOptions
demoOptions = defaultFontOptions {
    optionPPEm = Just (15, 15),
    optionScale = Just (15, 15)
}

-- | Test shaped runs against an expected value, and write metadata about the
-- used font afterwards.
shapedRunsSpecWithFont ::
    FilePath -> Font -> String -> FilePath -> Paragraph -> SpecWith ()
shapedRunsSpecWithFont fontPath font subject name p = do
    let writeInfo = writeFontInfo (fontInfoPath name) fontPath font
    after_ writeInfo $ it subject $ do
        let result = layoutPlain p
        shapedRuns' result `shouldBeGolden` name

paginateAll :: PageOptions -> ParagraphLayout -> [Page]
paginateAll opts pl = case paginate opts pl of
    (c, pl1, next) -> (c, pl1) : case next of
        Just pl2 -> paginateAll opts' pl2
        Nothing -> []
    where
        opts' = opts { pageCurrentHeight = pageNextHeight opts }

spec :: Spec
spec = do

    describe "layoutPlain" $ do

        describe "with Arabic font" $ do
            font <- runIO $ loadFont arabicFont 0 testingOptions

            it "handles input with no spans" $ do
                let opts = ParagraphOptions font Normal 8000
                let result = layoutPlain $ emptyParagraph opts
                result `shouldBe` emptyLayout

            it "wraps filler text at 20em" $ do
                let opts = ParagraphOptions font Normal 20000
                let result = layoutPlain $ arabicFillerParagraph opts
                result `shouldBeGolden` "arabicFiller20em"

            it "wraps filler text with spans at 20em" $ do
                let opts = ParagraphOptions font Normal 20000
                let result = layoutPlain $ spannedArabicFillerParagraph opts
                result `shouldBeGolden` "spannedArabicFiller20em"

            it "spans do not reposition filler text at 20em" $ do
                let opts = ParagraphOptions font Normal 20000
                let withoutSpans = layoutPlain $ arabicFillerParagraph opts
                let withSpans = layoutPlain $ spannedArabicFillerParagraph opts
                paragraphRect withoutSpans `shouldBe` paragraphRect withSpans

            it "applies hard breaks correctly" $ do
                let opts = ParagraphOptions font Normal 6000
                let result = layoutPlain $ hardBreaksRTLParagraph opts
                result `shouldBeGolden` "hardBreaksRTLParagraph"

        describe "with Devanagari font" $ do
            font <- runIO $ loadFont devanagariFont 0 testingOptions

            describe "lone accent character" $ do
                let
                    opts = ParagraphOptions font Normal 8000
                    result = layoutPlain $
                        devanagariAccentParagraph opts

                it "inserts a dotted circle" $ do
                    x_size (paragraphRect result) `shouldBe` 645

                it "is golden" $ do
                    result `shouldBeGolden` "devanagariAccentParagraph"

            describe "lone accent character after prefix" $ do
                let
                    opts = ParagraphOptions font Normal 8000
                    result = layoutPlain $
                        devanagariPrefixedAccentParagraph opts

                it "does not insert a dotted circle" $ do
                    x_size (paragraphRect result) `shouldBe` 0

                it "is golden" $ do
                    result `shouldBeGolden` "devanagariPrefixedAccentParagraph"

            it "handles input without wrapping" $ do
                let opts = ParagraphOptions font Normal 9000
                let result = layoutPlain $ devanagariParagraph opts
                result `shouldBeGolden` "devanagariParagraph"

        describe "with Latin font" $ do
            -- Note: This font does not contain Japanese glyphs.
            font <- runIO $ loadFont latinFont 0 testingOptions

            it "handles input with no spans" $ do
                let opts = ParagraphOptions font Normal 8000
                let result = layoutPlain $ emptyParagraph opts
                result `shouldBe` emptyLayout

            it "handles one span with no text" $ do
                let opts = ParagraphOptions font Normal 8000
                let result = layoutPlain $ emptySpanParagraph opts
                result `shouldBe` emptySpanLayout

            it "handles Czech hello" $ do
                let opts = ParagraphOptions font Normal 8000
                let result = layoutPlain $ czechHelloParagraph opts
                result `shouldBeGolden` "czechHelloParagraph"

            it "renders an \"ffi\" ligature" $ do
                let opts = ParagraphOptions font Normal 8000
                let result = layoutPlain $ ligatureParagraph opts
                result `shouldBeGolden` "ligatureParagraph"

            it "breaks an \"ffi\" ligature into \"ff\" + \"i\"" $ do
                let opts = ParagraphOptions font Normal 2418
                let result = layoutPlain $ ligatureParagraph opts
                result `shouldBeGolden` "ligatureParagraphBreak1"

            it "breaks an \"ffi\" ligature into \"f\" + \"fi\"" $ do
                let opts = ParagraphOptions font Normal 1800
                let result = layoutPlain $ ligatureParagraph opts
                result `shouldBeGolden` "ligatureParagraphBreak2"

            it "handles mixed languages in LTR layout" $ do
                let opts = ParagraphOptions font Normal 8000
                let result = layoutPlain $ mixedLanguageLTRParagraph opts
                result `shouldBeGolden` "mixedLanguageLTRParagraph"

            it "handles normal line height" $ do
                let opts = ParagraphOptions font Normal 8000
                let result = layoutPlain $ trivialParagraph opts
                result `shouldBeGolden` "lineHeightNormal"

            it "handles larger line height" $ do
                let opts = ParagraphOptions font (Absolute 1600) 8000
                let result = layoutPlain $ trivialParagraph opts
                result `shouldBeGolden` "lineHeightLarger"

            it "handles smaller line height" $ do
                let opts = ParagraphOptions font (Absolute 599) 8000
                let result = layoutPlain $ trivialParagraph opts
                result `shouldBeGolden` "lineHeightSmaller"

            it "wraps mid-word when line is narrow" $ do
                let opts = ParagraphOptions font Normal 1300
                let result = layoutPlain $ czechHelloParagraph opts
                result `shouldBeGolden` "czechHelloParagraphNarrow"

            it "wraps by characters when line is ultra narrow" $ do
                let opts = ParagraphOptions font Normal 100
                let result = layoutPlain $ czechHelloParagraph opts
                result `shouldBeGolden` "czechHelloParagraphUltraNarrow"

            it "wraps lorem ipsum at 20em" $ do
                let opts = ParagraphOptions font Normal 20000
                let result = layoutPlain $ loremIpsumParagraph opts
                result `shouldBeGolden` "loremIpsum20em"

            it "wraps lorem ipsum at 100em" $ do
                let opts = ParagraphOptions font Normal 100000
                let result = layoutPlain $ loremIpsumParagraph opts
                result `shouldBeGolden` "loremIpsum100em"

            it "wraps lorem ipsum with spans at 20em" $ do
                let opts = ParagraphOptions font Normal 20000
                let result = layoutPlain $ spannedLoremIpsumParagraph opts
                result `shouldBeGolden` "spannedLoremIpsum20em"

            it "spans do not reposition lorem ipsum at 20em" $ do
                let opts = ParagraphOptions font Normal 20000
                let withoutSpans = layoutPlain $ loremIpsumParagraph opts
                let withSpans = layoutPlain $ spannedLoremIpsumParagraph opts
                paragraphRect withoutSpans `shouldBe` paragraphRect withSpans

            it "wraps mixed-script words correctly" $ do
                let opts = ParagraphOptions font Normal 6000
                let result = layoutPlain $ mixedScriptWordsParagraph opts
                result `shouldBeGolden` "mixedScriptWordsParagraph"

            it "trims spaces around lines" $ do
                let opts = ParagraphOptions font Normal 6000
                let result = layoutPlain $ manySpacesParagraph opts
                result `shouldBeGolden` "manySpacesParagraph"

            it "applies hard breaks correctly" $ do
                let opts = ParagraphOptions font Normal 5000
                let result = layoutPlain $ hardBreaksLTRParagraph opts
                result `shouldBeGolden` "hardBreaksLTRParagraph"

    describe "paginate" $ do

        describe "with Arabic font" $ do
            font <- runIO $ loadFont arabicFont 0 testingOptions

            it "wraps filler text with spans at 20em" $ do
                let
                    opts = ParagraphOptions font Normal 20000
                    pl = layoutPlain $ spannedArabicFillerParagraph opts
                    popts = PageOptions
                        { pageCurrentHeight = 1000
                        , pageNextHeight = 5000
                        , pageOrphans = 2
                        , pageWidows = 2
                        }
                    pages = paginateAll popts pl
                Pages pages `shouldBeGolden` "spannedArabicFiller20emPaginated"

        describe "with Latin font" $ do
            font <- runIO $ loadFont latinFont 0 testingOptions

            it "wraps lorem ipsum at 20em" $ do
                let
                    opts = ParagraphOptions font Normal 20000
                    pl = layoutPlain $ loremIpsumParagraph opts
                    popts = PageOptions
                        { pageCurrentHeight = 2500
                        , pageNextHeight = 8500
                        , pageOrphans = 2
                        , pageWidows = 3
                        }
                    pages = paginateAll popts pl
                Pages pages `shouldBeGolden` "loremIpsum20emPaginated"

    describe "shaped runs for demo" $ do

        describe "with Latin font" $ do
            let fontPath = latinFont
            font <- runIO $ loadFont fontPath 0 demoOptions
            let shapedRunsSpec = shapedRunsSpecWithFont fontPath font

            shapedRunsSpec "wraps lorem ipsum with spans at 20em"
                "spannedLoremIpsum20em" $
                spannedLoremIpsumParagraph $
                ParagraphOptions font Normal 300