module Data.Text.ParagraphLayout.Internal.Paginable ( PageOptions (..) , Paginable , paginate , paginateAll ) where import Data.Int (Int32) import Data.Text.ParagraphLayout.Internal.LineNumbers import Data.Text.ParagraphLayout.Internal.LinePagination import Data.Text.ParagraphLayout.Internal.ParagraphLine import qualified Data.Text.ParagraphLayout.Internal.Plain.ParagraphLayout as Plain import qualified Data.Text.ParagraphLayout.Internal.Rich.ParagraphLayout as Rich -- | Defines options for breaking a layout into pages. data PageOptions = PageOptions { pageCurrentHeight :: Int32 -- ^ Amount of vertical space available for the paragraph -- on the current page. , pageNextHeight :: Int32 -- ^ Expected amount of vertical space available for the paragraph -- on the next page. -- -- If this is greater than `pageCurrentHeight`, the paragraph may be pushed -- onto the next page in order to better satisfy orphan/widow constraints. , pageOrphans :: Word -- ^ If a page break is required inside the paragraph, this will be the -- minimum number of lines to keep at the bottom of this page, if possible. , pageWidows :: Word -- ^ If a page break is required inside the paragraph, this will be the -- minimum number of lines to keep at the top of the next page, if possible. } -- | Typeclass for layouts that can be broken into pages. class Paginable pl where -- | Break a chunk of content from the given layout, to be placed together -- on a page. -- -- Explanation of return values: -- -- - @(`Continue`, p, `Nothing`)@ -- means that @p@ is the entire layout and fits best on the current page. -- -- - @(`Break`, p, `Nothing`)@ -- means that @p@ is the entire layout and fits best on a new page. -- In other words, @p@ should be preceded by a page break. -- -- - @(`Continue`, p, `Just` rest)@ -- means that @p@ is a part of the layout that fits best on the current -- page, and @rest@ should be passed to this function again. -- In other words, @p@ should be followed by a page break. -- -- - @(`Break`, p, `Just` rest)@ -- means that @p@ is a part of the layout that fits best on a new page, -- and @rest@ should be passed to this function again. -- In other words, @p@ should be surrounded by page breaks -- on both sides. paginate :: PageOptions -> pl -> (PageContinuity, pl, Maybe pl) -- | Internal implementation of paginating a simple list of generic lines. instance LineHeight a => Paginable [a] where paginate opts ls = case paginateLines o w h1 h2 ls of (c, p, []) -> (c, p, Nothing) (c, p, rest) -> (c, p, Just rest) where o = pageOrphans opts w = pageWidows opts h1 = pageCurrentHeight opts h2 = pageNextHeight opts -- | Implementation of paginating a plain text paragraph layout. -- Breaks the layout on page boundaries and automatically adjusts coordinates. instance Paginable (Plain.ParagraphLayout d) where paginate = paginateLayout -- | Implementation of paginating a rich text paragraph layout. -- Breaks the layout on page boundaries and automatically adjusts coordinates. instance Paginable (Rich.ParagraphLayout d) where paginate = paginateLayout paginateLayout :: (LineHeight a, LineNumbers a, GenericLayout a) => PageOptions -> a -> (PageContinuity, a, Maybe a) paginateLayout opts pl = case paginate opts (cutLines pl) of (c, p, Nothing) -> (c, mergeLines p, Nothing) (c, p, Just rest) -> (c, mergeLines p, Just (mergeLines rest)) -- | Perform page breaking on the entire input, returning a list of pages. paginateAll :: Paginable a => PageOptions -> a -> [(PageContinuity, a)] 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 }