module Data.Text.ParagraphLayout (Span(..), layout) where import Data.Text.Glyphize (Buffer(..) ,ContentType(ContentTypeUnicode) ,Direction(..) ,Font ,GlyphInfo ,GlyphPos ,defaultBuffer ,shape ) import qualified Data.Text.ICU.Char as ICUChar import Data.Text.Lazy (Text) import qualified Data.Text.Lazy as Text import Data.Text.Script (charScript) type ScriptCode = String type Language = String -- Paragraph is broken into spans by the caller. -- -- Each span could have a different font family, size, style, text decoration, -- colour, language, etc. -- -- TODO: Add all relevant attributes. -- data Span = Span { spanText :: Text , spanFont :: Font , spanLanguage :: Maybe Language } deriving (Eq, Show) -- Each span can be broken into one or more runs by Balkón. -- -- Each run could have a different script, language, or direction. -- data Run = Run { runText :: Text , runDirection :: Maybe Direction , runScript :: Maybe ScriptCode , runOriginalSpan :: Span } deriving (Eq, Show) type ProtoRun = (String, Maybe Direction, ScriptCode) data Merged a = Incompatible | Merged a data Position = Beginning | Middle | End | Only deriving (Eq) -- TODO: Add maximum line length as input. -- TODO: Compute and return bounding box for each provided span. -- More if implementing the CSS Box Model. -- TODO: Also compute and return overall bounding box, in addition to individual -- ones. -- TODO: Allow a run across multiple spans (e.g. if they only differ by colour). layout :: [Span] -> [[(GlyphInfo, GlyphPos)]] layout = layoutRuns . concat . map spanToRuns -- TODO: Optimise and preserve the Data.Text.Lazy structure. spanToRuns :: Span -> [Run] spanToRuns s = map run $ protoRuns chars where chars = reverse $ Text.unpack $ spanText s run (t, d, sc) = Run { runText = Text.pack t , runDirection = d , runScript = Just sc , runOriginalSpan = s } -- TODO: Try to avoid reversing. protoRuns :: [Char] -> [ProtoRun] protoRuns = reverse . map (\(t, d, s) -> (reverse t, d, s)) . foldr foldRun [] foldRun :: Char -> [ProtoRun] -> [ProtoRun] foldRun c [] = -- If there are no runs, create a new run with a single character. [([c], charDirection c, charScript c)] foldRun c (r@(oldString, d1, s1):rs) = case (mergeDirections d1 d2, mergeScripts s1 s2) of -- If direction & script are compatible, add to existing run. (Merged d, Merged s) -> ((c:oldString, d, s):rs) -- Otherwise create a new run. _ -> (([c], d2, s2):r:rs) where d2 = charDirection c s2 = charScript c -- Simplified detection of text direction for unidirectional text. mergeDirections :: Maybe Direction -> Maybe Direction -> Merged (Maybe Direction) mergeDirections Nothing Nothing = Merged Nothing mergeDirections (Just d1) Nothing = Merged (Just d1) mergeDirections Nothing (Just d2) = Merged (Just d2) mergeDirections (Just d1) (Just d2) | d1 == d2 = Merged (Just d1) | otherwise = Incompatible -- TODO: Implement proper inheritance rules. mergeScripts :: ScriptCode -> ScriptCode -> Merged ScriptCode mergeScripts "Zyyy" s2 = Merged s2 mergeScripts s1 "Zyyy" = Merged s1 mergeScripts s1 "Zinh" = Merged s1 mergeScripts s1 s2 | s1 == s2 = Merged s1 | otherwise = Incompatible -- TODO: Use the BiDi algorithm to support bidirectional text. charDirection :: Char -> Maybe Direction charDirection c = case ICUChar.direction c of ICUChar.LeftToRight -> Just DirLTR ICUChar.RightToLeft -> Just DirRTL ICUChar.RightToLeftArabic -> Just DirRTL _ -> Nothing layoutRuns :: [Run] -> [[(GlyphInfo, GlyphPos)]] layoutRuns [] = [] layoutRuns [s] = [layoutOneRun Only s] -- TODO: What if there are no visible characters in the edge runs? layoutRuns (s1:s2:ss) = (layoutOneRun Beginning s1):(layoutRemainingRuns s2 ss) layoutRemainingRuns :: Run -> [Run] -> [[(GlyphInfo, GlyphPos)]] layoutRemainingRuns s [] = [layoutOneRun End s] layoutRemainingRuns s1 (s2:ss) = (layoutOneRun Middle s1):(layoutRemainingRuns s2 ss) layoutOneRun :: Position -> Run -> [(GlyphInfo, GlyphPos)] layoutOneRun pos run = shape font buffer features where originalSpan = runOriginalSpan run font = spanFont originalSpan lang = spanLanguage originalSpan buffer = defaultBuffer { text = runText run , contentType = Just ContentTypeUnicode , direction = runDirection run , script = runScript run , language = lang , beginsText = pos == Beginning || pos == Only , endsText = pos == End || pos == Only } features = []