packages feed

doclayout 0.4.0.1 → 0.5

raw patch · 7 files changed

+638/−141 lines, 7 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Text.DocLayout: instance Text.DocLayout.HasChars Data.Text.Internal.Lazy.Text
- Text.DocLayout: instance Text.DocLayout.HasChars Data.Text.Internal.Text
- Text.DocLayout: instance Text.DocLayout.HasChars GHC.Base.String
- Text.DocLayout: instance Text.DocLayout.HasChars a => Data.String.IsString (Text.DocLayout.Doc a)
+ Text.DocLayout: CookedText :: Int -> Attributed a -> Doc a
+ Text.DocLayout: Linked :: Text -> Doc a -> Doc a
+ Text.DocLayout: Styled :: StyleReq -> Doc a -> Doc a
+ Text.DocLayout: bg :: HasChars a => Color -> Doc a -> Doc a
+ Text.DocLayout: black :: Color
+ Text.DocLayout: blue :: Color
+ Text.DocLayout: bold :: HasChars a => Doc a -> Doc a
+ Text.DocLayout: build :: HasChars a => a -> Builder
+ Text.DocLayout: cyan :: Color
+ Text.DocLayout: data Attributed a
+ Text.DocLayout: fg :: HasChars a => Color -> Doc a -> Doc a
+ Text.DocLayout: green :: Color
+ Text.DocLayout: instance Data.Data.Data a => Data.Data.Data (Text.DocLayout.FlatDoc a)
+ Text.DocLayout: instance Data.Foldable.Foldable Text.DocLayout.FlatDoc
+ Text.DocLayout: instance Data.Traversable.Traversable Text.DocLayout.FlatDoc
+ Text.DocLayout: instance GHC.Base.Functor Text.DocLayout.FlatDoc
+ Text.DocLayout: instance GHC.Classes.Eq a => GHC.Classes.Eq (Text.DocLayout.FlatDoc a)
+ Text.DocLayout: instance GHC.Classes.Ord a => GHC.Classes.Ord (Text.DocLayout.FlatDoc a)
+ Text.DocLayout: instance GHC.Generics.Generic (Text.DocLayout.FlatDoc a)
+ Text.DocLayout: instance GHC.Read.Read a => GHC.Read.Read (Text.DocLayout.FlatDoc a)
+ Text.DocLayout: instance GHC.Show.Show a => GHC.Show.Show (Text.DocLayout.FlatDoc a)
+ Text.DocLayout: instance Text.DocLayout.HasChars.HasChars a => Data.String.IsString (Text.DocLayout.Doc a)
+ Text.DocLayout: italic :: HasChars a => Doc a -> Doc a
+ Text.DocLayout: link :: HasChars a => Text -> Doc a -> Doc a
+ Text.DocLayout: magenta :: Color
+ Text.DocLayout: red :: Color
+ Text.DocLayout: renderANSI :: HasChars a => Maybe Int -> Doc a -> Text
+ Text.DocLayout: renderPlain :: HasChars a => Maybe Int -> Doc a -> a
+ Text.DocLayout: strikeout :: HasChars a => Doc a -> Doc a
+ Text.DocLayout: type Color = Color8
+ Text.DocLayout: underlined :: HasChars a => Doc a -> Doc a
+ Text.DocLayout: white :: Color
+ Text.DocLayout: yellow :: Color
- Text.DocLayout: Block :: Int -> [a] -> Doc a
+ Text.DocLayout: Block :: Int -> [Attributed a] -> Doc a

Files

changelog.md view
@@ -1,5 +1,36 @@ # doclayout +## 0.5++  * Extract existing HasChars definition into a module+    Text.DocLayout.HasChars (Evan Silberman).++  * Add a `build` method to the signature of HasChars [API change]+    (Evan Silberman). This has a default definition, and is only needed with+    the ANSI renderer, so existing users should not need to add anything to+    their HasChars instances.++  * Introduce support for ANSI-styled output (Evan Silberman) [API change].+    `renderPlain` and `renderANSI` are now exported; the old `render`+    is a synonym of `renderPlain`.  In addition, various functions are+    exported to add ANSI formatting (including bold, italics,+    underline, strikeout, links, and colors) to a Doc. The Attributed+    type is also now exported.++  * Change type of Block constructor, replacing `[a]` with `[Attributed a]`,+    which carries a Font along with an inner string type [API change]+    (Evan Silberman).++  * Introduce FlatDocs and use them for rendering (Evan Silberman).+    This is an internal concept, not part of the public API.+    FlatDoc is an "intermediate representation" for the Doc "interpreter".+    The general design is that any Doc can be turned into a list of FlatDocs+    that carry equivalent information. The main point of doing this+    is to replace the nested Styled and Linked Docs, which form a more+    complicated tree structure than previously existed in DocLayout.++  * Deprecate `unfoldD`.+ ## 0.4.0.1    * Add clause for Empty to renderList (#22).
doclayout.cabal view
@@ -1,5 +1,5 @@ name:                doclayout-version:             0.4.0.1+version:             0.5 synopsis:            A prettyprinting library for laying out text documents. description:         doclayout is a prettyprinting library for laying out                      text documents, with several features not present@@ -10,7 +10,7 @@ license-file:        LICENSE author:              John MacFarlane maintainer:          jgm@berkeley.edu-copyright:           2016-19 John MacFarlane+copyright:           2016-24 John MacFarlane category:            Text build-type:          Simple extra-source-files:  udhr/txt/*.txt,@@ -18,12 +18,15 @@                      udhr/languages.txt,                      src/Text/unicodeWidth.inc data-files:          README.md-                     changelog.md+extra-source-files:  changelog.md cabal-version:       >=1.10  library   hs-source-dirs:      src   exposed-modules:     Text.DocLayout+  other-modules:       Text.DocLayout.HasChars,+                       Text.DocLayout.ANSIFont,+                       Text.DocLayout.Attributed   build-depends:       base >= 4.12 && < 5,                        text,                        containers,
src/Text/DocLayout.hs view
@@ -27,6 +27,8 @@ module Text.DocLayout (      -- * Rendering        render+     , renderPlain+     , renderANSI      -- * Doc constructors      , cr      , blankline@@ -54,6 +56,22 @@      , parens      , quotes      , doubleQuotes+     , bold+     , italic+     , underlined+     , strikeout+     , fg+     , bg+     , Color+     , black+     , red+     , green+     , yellow+     , blue+     , magenta+     , cyan+     , white+     , link      , empty      -- * Functions for concatenating documents      , (<+>)@@ -84,6 +102,7 @@      -- * Types      , Doc(..)      , HasChars(..)+     , Attributed      )  where@@ -96,14 +115,21 @@ import Data.Bifunctor (second) import Data.Char (isSpace, ord) import Data.List (foldl', intersperse)+import Data.List.NonEmpty (NonEmpty((:|)))+import qualified Data.List.NonEmpty as N import qualified Data.IntMap.Strict as IM import qualified Data.Map.Strict as M import qualified Data.Map.Internal as MInt import Data.Data (Data, Typeable)+import Data.Foldable (toList) import Data.String import qualified Data.Text as T-import qualified Data.Text.Lazy as TL import Data.Text (Text)+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as B+import Text.DocLayout.HasChars+import Text.DocLayout.ANSIFont+import Text.DocLayout.Attributed #if MIN_VERSION_base(4,11,0) #else import Data.Semigroup@@ -111,51 +137,13 @@ import Text.Emoji (baseEmojis)  --- | Class abstracting over various string types that--- can fold over characters.  Minimal definition is 'foldrChar'--- and 'foldlChar', but defining the other methods can give better--- performance.-class (IsString a, Semigroup a, Monoid a, Show a) => HasChars a where-  foldrChar     :: (Char -> b -> b) -> b -> a -> b-  foldlChar     :: (b -> Char -> b) -> b -> a -> b-  replicateChar :: Int -> Char -> a-  replicateChar n c = fromString (replicate n c)-  isNull        :: a -> Bool-  isNull = foldrChar (\_ _ -> False) True-  splitLines    :: a -> [a]-  splitLines s = (fromString firstline : otherlines)-   where-    (firstline, otherlines) = foldrChar go ([],[]) s-    go '\n' (cur,lns) = ([], fromString cur : lns)-    go c    (cur,lns) = (c:cur, lns)--instance HasChars Text where-  foldrChar         = T.foldr-  foldlChar         = T.foldl'-  splitLines        = T.splitOn "\n"-  replicateChar n c = T.replicate n (T.singleton c)-  isNull            = T.null--instance HasChars String where-  foldrChar     = foldr-  foldlChar     = foldl'-  splitLines    = lines . (++"\n")-  replicateChar = replicate-  isNull        = null--instance HasChars TL.Text where-  foldrChar         = TL.foldr-  foldlChar         = TL.foldl'-  splitLines        = TL.splitOn "\n"-  replicateChar n c = TL.replicate (fromIntegral n) (TL.singleton c)-  isNull            = TL.null- -- | Document, including structure relevant for layout. data Doc a = Text Int a            -- ^ Text with specified width.-         | Block Int [a]           -- ^ A block with a width and lines.+         | Block Int [Attributed a]           -- ^ A block with a width and lines.          | VFill Int a             -- ^ A vertically expandable block;                  -- when concatenated with a block, expands to height                  -- of block, with each line containing the specified text.+         | CookedText Int (Attributed a) -- ^ Text which doesn't need further cooking          | Prefixed Text (Doc a)   -- ^ Doc with each line prefixed with text.                  -- Note that trailing blanks are omitted from the prefix                  -- when the line after it is empty.@@ -167,10 +155,13 @@          | NewLine                 -- ^ newline.          | BlankLines Int          -- ^ Ensure a number of blank lines.          | Concat (Doc a) (Doc a)  -- ^ Two documents concatenated.+         | Styled StyleReq (Doc a)+         | Linked Text (Doc a)       -- ^ A hyperlink          | Empty          deriving (Show, Read, Eq, Ord, Functor, Foldable, Traversable,                   Data, Typeable, Generic) + instance Semigroup (Doc a) where   x <> Empty = x   Empty <> x = x@@ -183,6 +174,7 @@ instance HasChars a => IsString (Doc a) where   fromString = text +{-# DEPRECATED unfoldD "unfoldD will be removed from the API." #-} -- | Unfold a 'Doc' into a flat list. unfoldD :: Doc a -> [Doc a] unfoldD Empty = []@@ -268,26 +260,89 @@           z     -> x <> z     _                         -> d +-- Elements of a document with Styled and Linked subtrees flattened out into+-- a linear structure with open and close tags. An implementation detail of+-- the rendering process.+data FlatDoc a = FText Int a+               | FBlock Int [Attributed a]+               | FVFill Int a+               | FCookedText Int (Attributed a)+               | FPrefixed Text (NonEmpty (FlatDoc a))+               | FBeforeNonBlank (NonEmpty (FlatDoc a))+               | FFlush (NonEmpty (FlatDoc a))+               | FBreakingSpace+               | FAfterBreak (NonEmpty (FlatDoc a))+               | FCarriageReturn+               | FNewLine+               | FBlankLines Int+               | FStyleOpen StyleReq+               | FStyleClose+               | FLinkOpen Text+               | FLinkClose+         deriving (Show, Read, Eq, Ord, Functor, Foldable, Traversable,+                  Data, Typeable, Generic)++-- Given a Doc, return an equivalent list of FlatDocs, to be processed by+-- renderList. Worth noting:+--   * Treelike docs (Styled, and Linked) are turned into lists beginning+--     with an "open" tag and ending with a "close" tag, with the flattened+--     inner content in between.+--   * Other Docs with inner content are eliminated if the inner content is+--     empty, otherwise the inner content is itself flattened and made into+--     a NonEmpty.+flatten :: HasChars a => Doc a -> [FlatDoc a]+flatten (Text n a) = [FText n a]+flatten (Block n a) = [FBlock n a]+flatten (VFill n a) = [FVFill n a]+flatten (CookedText n a) = [FCookedText n a]+flatten (Prefixed p d) | null f = []+                       | otherwise = [FPrefixed p (N.fromList f)]+                       where f = (normalize . flatten) d+flatten (BeforeNonBlank d) | null f = []+                           | otherwise = [FBeforeNonBlank (N.fromList f)]+                           where f = flatten d+flatten (Flush d) | null f = []+                  | otherwise = [FFlush (N.fromList f)]+                  where f = flatten d+flatten BreakingSpace = [FBreakingSpace]+flatten CarriageReturn = [FCarriageReturn]+flatten (AfterBreak t) | null f = []+                       | otherwise = [FAfterBreak (N.fromList f)]+                       where f = flatten $ fromString $ T.unpack t+flatten NewLine = [FNewLine]+flatten (BlankLines n) = [FBlankLines n]+flatten Empty = []+flatten (Concat x y) = flatten x <> flatten y+flatten (Linked l x) = FLinkOpen l : flatten x <> [FLinkClose]+flatten (Styled f x) = FStyleOpen f : flatten x <> [FStyleClose]+ type DocState a = State (RenderState a) ()  data RenderState a = RenderState{-         output     :: [a]        -- ^ In reverse order+         output     :: [Attr a]        -- ^ In reverse order        , prefix     :: Text        , usePrefix  :: Bool        , lineLength :: Maybe Int  -- ^ 'Nothing' means no wrapping        , column     :: Int        , newlines   :: Int        -- ^ Number of preceding newlines+       , fontStack  :: [Font]+       , linkTarget :: Maybe Text -- ^ Current link target        } +peekFont :: RenderState a -> Font+peekFont st = case fontStack st of+                [] -> baseFont+                x:_ -> x+ newline :: HasChars a => DocState a newline = do   st' <- get   let rawpref = prefix st'   when (column st' == 0 && usePrefix st' && not (T.null rawpref)) $ do      let pref = fromString $ T.unpack $ T.dropWhileEnd isSpace rawpref-     modify $ \st -> st{ output = pref : output st+     modify $ \st -> st{ output = Attr Nothing baseFont pref : output st                        , column = column st + realLength pref }-  modify $ \st -> st { output = "\n" : output st+  modify $ \st -> st { output = Attr Nothing baseFont "\n" : output st                      , column = 0                      , newlines = newlines st + 1                      }@@ -295,19 +350,48 @@ outp :: HasChars a => Int -> a -> DocState a outp off s = do           -- offset >= 0 (0 might be combining char)   st' <- get-  let pref = fromString $ T.unpack $ prefix st'-  when (column st' == 0 && usePrefix st' && not (isNull pref)) $-    modify $ \st -> st{ output = pref : output st+  let pref = if usePrefix st' then fromString $ T.unpack $ prefix st' else mempty+  let font = peekFont st'+  when (column st' == 0 && not (isNull pref && font == baseFont)) $+    modify $ \st -> st{ output = Attr Nothing baseFont pref : output st                     , column = column st + realLength pref }-  modify $ \st -> st{ output = s : output st+  modify $ \st -> st{ output = Attr (linkTarget st) font s : output st                     , column = column st + off                     , newlines = 0 } --- | Render a 'Doc'.  @render (Just n)@ will use--- a line length of @n@ to reflow text on breakable spaces.--- @render Nothing@ will not reflow text.+-- | Synonym for 'renderPlain'. render :: HasChars a => Maybe Int -> Doc a -> a-render linelen doc = mconcat . reverse . output $+render = renderPlain++-- | Render a 'Doc' with ANSI escapes.  @renderANSI (Just n)@ will use+-- a line length of @n@ to reflow text on breakable spaces.+-- @renderANSI Nothing@ will not reflow text.+renderANSI :: HasChars a => Maybe Int -> Doc a -> TL.Text+renderANSI n d = B.toLazyText $ go $ prerender n d where+  go s = (\(_,_,o) -> o) (go' s) <> B.fromText (renderFont baseFont) <> B.fromText (renderOSC8 Nothing)+  go' (Attributed s) = foldl attrRender (Nothing, baseFont, B.fromText "") s++-- | Render a 'Doc' without using ANSI escapes.  @renderPlain (Just n)@ will use+-- a line length of @n@ to reflow text on breakable spaces.+-- @renderPlain Nothing@ will not reflow text.+renderPlain :: HasChars a => Maybe Int -> Doc a -> a+renderPlain n d = go $ prerender n d where+  go (Attributed s) = foldMap attrStrip s++attrStrip :: HasChars a => Attr a -> a+attrStrip (Attr _ _ y) | isNull y = ""+                       | otherwise = y++attrRender :: HasChars a => (Link, Font, B.Builder) -> Attr a -> (Link, Font, B.Builder)+attrRender (l, f, acc) (Attr m g y)+    | isNull y = (l, f, acc)+    | otherwise = (m, g, acc <> B.fromText newFont <> B.fromText newLink <> build y)+  where+    newLink = if l == m then mempty else renderOSC8 m+    newFont = if f == g then mempty else renderFont g++prerender :: HasChars a => Maybe Int -> Doc a -> Attributed a+prerender linelen doc = fromList . reverse . output $   execState (renderDoc doc) startingState    where startingState = RenderState{                             output = mempty@@ -315,38 +399,38 @@                           , usePrefix = True                           , lineLength = linelen                           , column = 0-                          , newlines = 2 }+                          , newlines = 2+                          , fontStack = []+                          , linkTarget = Nothing }  renderDoc :: HasChars a => Doc a -> DocState a-renderDoc = renderList . normalize . unfoldD+renderDoc = renderList . normalize . flatten  -normalize :: HasChars a => [Doc a] -> [Doc a]+normalize :: HasChars a => [FlatDoc a] -> [FlatDoc a] normalize [] = []-normalize (Concat{} : xs) = normalize xs -- should not happen after unfoldD-normalize (Empty : xs) = normalize xs -- should not happen after unfoldD-normalize [NewLine] = normalize [CarriageReturn]-normalize [BlankLines _] = normalize [CarriageReturn]-normalize [BreakingSpace] = []-normalize (BlankLines m : BlankLines n : xs) =-  normalize (BlankLines (max m n) : xs)-normalize (BlankLines num : BreakingSpace : xs) =-  normalize (BlankLines num : xs)-normalize (BlankLines m : CarriageReturn : xs) = normalize (BlankLines m : xs)-normalize (BlankLines m : NewLine : xs) = normalize (BlankLines m : xs)-normalize (NewLine : BlankLines m : xs) = normalize (BlankLines m : xs)-normalize (NewLine : BreakingSpace : xs) = normalize (NewLine : xs)-normalize (NewLine : CarriageReturn : xs) = normalize (NewLine : xs)-normalize (CarriageReturn : CarriageReturn : xs) =-  normalize (CarriageReturn : xs)-normalize (CarriageReturn : BlankLines m : xs) = normalize (BlankLines m : xs)-normalize (CarriageReturn : BreakingSpace : xs) =-  normalize (CarriageReturn : xs)-normalize (BreakingSpace : CarriageReturn : xs) =-  normalize (CarriageReturn:xs)-normalize (BreakingSpace : NewLine : xs) = normalize (NewLine:xs)-normalize (BreakingSpace : BlankLines n : xs) = normalize (BlankLines n:xs)-normalize (BreakingSpace : BreakingSpace : xs) = normalize (BreakingSpace:xs)+normalize [FNewLine] = normalize [FCarriageReturn]+normalize [FBlankLines _] = normalize [FCarriageReturn]+normalize [FBreakingSpace] = []+normalize (FBlankLines m : FBlankLines n : xs) =+  normalize (FBlankLines (max m n) : xs)+normalize (FBlankLines num : FBreakingSpace : xs) =+  normalize (FBlankLines num : xs)+normalize (FBlankLines m : FCarriageReturn : xs) = normalize (FBlankLines m : xs)+normalize (FBlankLines m : FNewLine : xs) = normalize (FBlankLines m : xs)+normalize (FNewLine : FBlankLines m : xs) = normalize (FBlankLines m : xs)+normalize (FNewLine : FBreakingSpace : xs) = normalize (FNewLine : xs)+normalize (FNewLine : FCarriageReturn : xs) = normalize (FNewLine : xs)+normalize (FCarriageReturn : FCarriageReturn : xs) =+  normalize (FCarriageReturn : xs)+normalize (FCarriageReturn : FBlankLines m : xs) = normalize (FBlankLines m : xs)+normalize (FCarriageReturn : FBreakingSpace : xs) =+  normalize (FCarriageReturn : xs)+normalize (FBreakingSpace : FCarriageReturn : xs) =+  normalize (FCarriageReturn:xs)+normalize (FBreakingSpace : FNewLine : xs) = normalize (FNewLine:xs)+normalize (FBreakingSpace : FBlankLines n : xs) = normalize (FBlankLines n:xs)+normalize (FBreakingSpace : FBreakingSpace : xs) = normalize (FBreakingSpace:xs) normalize (x:xs) = x : normalize xs  mergeBlocks :: HasChars a => Int -> (Int, [a]) -> (Int, [a]) -> (Int, [a])@@ -364,45 +448,93 @@              else take h lns2   pad n s = s <> replicateChar (n - realLength s) ' ' -renderList :: HasChars a => [Doc a] -> DocState a+renderList :: HasChars a => [FlatDoc a] -> DocState a renderList [] = return () -renderList (Empty : xs) = renderList xs--renderList (Text off s : xs) = do+renderList (FText off s : xs) = do   outp off s   renderList xs -renderList (Prefixed pref d : xs) = do+renderList (FCookedText off s : xs) = do+  st' <- get+  let pref = if usePrefix st' then fromString $ T.unpack $ prefix st' else mempty+  let elems (Attributed x) = reverse $ toList x+  when (column st' == 0 && not (isNull pref))  $+    modify $ \st -> st{ output = Attr Nothing baseFont pref : output st+                      , column = column st + realLength pref }+  modify $ \st -> st{ output = elems s ++ output st+                    , column = column st + off+                    , newlines = 0 }+  renderList xs++-- FStyleOpen and FStyleClose are balanced by construction when we create+-- them in `flatten`, so we can just pop the stack when we encounter+-- FStyleClose+renderList (FStyleOpen style : xs) = do   st <- get+  let prevFont = peekFont st+  let nextFont = prevFont ~> style+  modify $ \s -> s{fontStack = nextFont : fontStack s}+  renderList xs++renderList (FStyleClose : xs) = do+  modify $ \s -> s{fontStack = drop 1 $ fontStack s}+  renderList xs++-- Nested links are nonsensical, we only handle the outermost and+-- silently ignore any attempts to have a link inside a link++-- Nested links are nonsensical, we only handle the outermost and+-- silently ignore any attempts to have a link inside a link+renderList (FLinkOpen target : xs) = do+  st <- get+  case linkTarget st of+    Nothing -> do+      modify $ \s -> s{linkTarget = Just target}+      renderList xs+    _ -> do+      let (next, rest) = break isLinkClose xs+      renderList (next <> drop 1 rest)+  where+    isLinkClose FLinkClose = True+    isLinkClose _ = False++renderList (FLinkClose : xs) = do+  modify $ \s -> s{linkTarget = Nothing}+  renderList xs++renderList (FPrefixed pref d : xs) = do+  st <- get   let oldPref = prefix st   put st{ prefix = prefix st <> pref }-  renderDoc d+  renderList $ normalize $ N.toList d   modify $ \s -> s{ prefix = oldPref }   -- renderDoc CarriageReturn   renderList xs -renderList (Flush d : xs) = do+renderList (FFlush d : xs) = do   st <- get   let oldUsePrefix = usePrefix st   put st{ usePrefix = False }-  renderDoc d+  renderList $ normalize $ N.toList d   modify $ \s -> s{ usePrefix = oldUsePrefix }   renderList xs -renderList (BeforeNonBlank d : xs) =-  case xs of+renderList (FBeforeNonBlank d : xs) = do+  let next = dropWhile (not . isPrintable) xs+  case next of     (x:_) | startsBlank x -> renderList xs-          | otherwise     -> renderDoc d >> renderList xs+          | otherwise     -> renderList (normalize $ N.toList d) >> renderList xs     []                    -> renderList xs-renderList (BlankLines num : xs) = do++renderList (FBlankLines num : xs) = do   st <- get   case output st of      _ | newlines st > num -> return ()        | otherwise -> replicateM_ (1 + num - newlines st) newline   renderList xs -renderList (CarriageReturn : xs) = do+renderList (FCarriageReturn : xs) = do   st <- get   if newlines st > 0      then renderList xs@@ -410,12 +542,12 @@        newline        renderList xs -renderList (NewLine : xs) = do+renderList (FNewLine : xs) = do   newline   renderList xs -renderList (BreakingSpace : xs) = do-  let isBreakingSpace BreakingSpace = True+renderList (FBreakingSpace : xs) = do+  let isBreakingSpace FBreakingSpace = True       isBreakingSpace _ = False   let xs' = dropWhile isBreakingSpace xs   let next = takeWhile (not . isBreakable) xs'@@ -426,41 +558,39 @@         _  -> when (column st > 0) $ outp 1 " "   renderList xs' -renderList (AfterBreak t : xs) = do+renderList (FAfterBreak t : xs) = do   st <- get   if newlines st > 0-     then renderList (fromString (T.unpack t) : xs)+     then renderList (toList t <> xs)      else renderList xs -renderList (b : xs) | isBlock b = do+-- FBlock and FVFill are all that's left+renderList (b : xs) = do+  st <- get+  let font = peekFont st   let (bs, rest) = span isBlock xs   -- ensure we have right padding unless end of line-  let heightOf (Block _ ls) = length ls+  let heightOf (FBlock _ ls) = length ls       heightOf _            = 1   let maxheight = maximum $ map heightOf (b:bs)-  let toBlockSpec (Block w ls) = (w, ls)-      toBlockSpec (VFill w t)  = (w, take maxheight $ repeat t)+  let toBlockSpec (FBlock w ls) = (w, ls)+      toBlockSpec (FVFill w t)  = (w, map (singleton . (Attr (linkTarget st) font)) (take maxheight $ repeat t))       toBlockSpec _            = (0, [])   let (_, lns') = foldl (mergeBlocks maxheight) (toBlockSpec b)                              (map toBlockSpec bs)-  st <- get   let oldPref = prefix st   case column st - realLength oldPref of         n | n > 0 -> modify $ \s -> s{ prefix = oldPref <> T.replicate n " " }         _ -> return ()-  renderList $ intersperse CarriageReturn (map literal lns')+  renderList $ intersperse FCarriageReturn (mapMaybe cook lns')   modify $ \s -> s{ prefix = oldPref }   renderList rest -renderList (x:_) = error $ "renderList encountered " ++ show x--isBreakable :: HasChars a => Doc a -> Bool-isBreakable BreakingSpace      = True-isBreakable CarriageReturn     = True-isBreakable NewLine            = True-isBreakable (BlankLines _)     = True-isBreakable (Concat Empty y)   = isBreakable y-isBreakable (Concat x _)       = isBreakable x+isBreakable :: HasChars a => FlatDoc a -> Bool+isBreakable FBreakingSpace      = True+isBreakable FCarriageReturn     = True+isBreakable FNewLine            = True+isBreakable (FBlankLines _)     = True isBreakable _                  = False  startsBlank' :: HasChars a => a -> Bool@@ -469,33 +599,43 @@    go Nothing  c = Just (isSpace c)    go (Just b) _ = Just b -startsBlank :: HasChars a => Doc a -> Bool-startsBlank (Text _ t)         = startsBlank' t-startsBlank (Block n ls)       = n > 0 && all startsBlank' ls-startsBlank (VFill n t)        = n > 0 && startsBlank' t-startsBlank (BeforeNonBlank x) = startsBlank x-startsBlank (Prefixed _ x)     = startsBlank x-startsBlank (Flush x)          = startsBlank x-startsBlank BreakingSpace      = True-startsBlank (AfterBreak t)     = startsBlank (Text 0 t)-startsBlank CarriageReturn     = True-startsBlank NewLine            = True-startsBlank (BlankLines _)     = True-startsBlank (Concat Empty y)   = startsBlank y-startsBlank (Concat x _)       = startsBlank x-startsBlank Empty              = True+startsBlank :: HasChars a => FlatDoc a -> Bool+startsBlank (FText _ t)                = startsBlank' t+startsBlank (FCookedText _ t)          = startsBlank' t+startsBlank (FBlock n ls)              = n > 0 && all startsBlank' ls+startsBlank (FVFill n t)               = n > 0 && startsBlank' t+startsBlank (FBeforeNonBlank (x :| _)) = startsBlank x+startsBlank (FPrefixed _ (x :| _))     = startsBlank x+startsBlank (FFlush (x :| _))          = startsBlank x+startsBlank FBreakingSpace             = True+startsBlank (FAfterBreak (t :| _))     = startsBlank t+startsBlank FCarriageReturn            = True+startsBlank FNewLine                   = True+startsBlank (FBlankLines _)            = True+startsBlank (FStyleOpen _)             = True+startsBlank (FLinkOpen _)              = True+startsBlank FStyleClose                = True+startsBlank FLinkClose                 = True -isBlock :: Doc a -> Bool-isBlock Block{} = True-isBlock VFill{} = True+isPrintable :: FlatDoc a -> Bool+isPrintable FLinkOpen{} = False+isPrintable FLinkClose{} = False+isPrintable FStyleOpen{} = False+isPrintable FStyleClose{} = False+isPrintable _ = True++isBlock :: FlatDoc a -> Bool+isBlock FBlock{} = True+isBlock FVFill{} = True isBlock _       = False -offsetOf :: Doc a -> Int-offsetOf (Text o _)      = o-offsetOf (Block w _)     = w-offsetOf (VFill w _)     = w-offsetOf BreakingSpace   = 1-offsetOf _               = 0+offsetOf :: FlatDoc a -> Int+offsetOf (FText o _)       = o+offsetOf (FBlock w _)      = w+offsetOf (FVFill w _)      = w+offsetOf (FCookedText w _) = w+offsetOf FBreakingSpace    = 1+offsetOf _                = 0  -- | Create a 'Doc' from a stringlike value. literal :: HasChars a => a -> Doc a@@ -509,6 +649,10 @@         splitLines x {-# NOINLINE literal #-} +cook :: HasChars a => Attributed a -> Maybe (FlatDoc a)+cook x | isNull x = Nothing+       | otherwise = let !len = realLength x in Just (FCookedText len x)+ -- | A literal string.  (Like 'literal', but restricted to String.) text :: HasChars a => String -> Doc a text = literal . fromString@@ -592,7 +736,10 @@     Text n _ -> (l, c + n)     Block n _ -> (l, c + n)     VFill n _ -> (l, c + n)+    CookedText n _ -> (l, c + n)     Empty -> (l, c)+    Styled _ d -> getOffset breakWhen (l, c) d+    Linked _ d -> getOffset breakWhen (l, c) d     CarriageReturn -> (max l c, 0)     NewLine -> (max l c, 0)     BlankLines _ -> (max l c, 0)@@ -646,12 +793,14 @@ height :: HasChars a => Doc a -> Int height = length . splitLines . render Nothing -block :: HasChars a => (a -> a) -> Int -> Doc a -> Doc a+block :: HasChars a => (Attributed a -> Attributed a) -> Int -> Doc a -> Doc a block filler width d   | width < 1 && not (isEmpty d) = block filler 1 d   | otherwise                    = Block width ls      where-       ls = map filler $ chop width $ render (Just width) d+       preimage = prerender (Just width) d+       reboxed = chop width preimage+       ls = map filler reboxed  -- | An expandable border that, when placed next to a box, -- expands to the height of the box.  Strings cycle through the@@ -706,6 +855,69 @@ -- | Wraps a 'Doc' in double quotes. doubleQuotes :: HasChars a => Doc a -> Doc a doubleQuotes = inside (char '"') (char '"')++styled :: HasChars a => StyleReq -> Doc a -> Doc a+styled _ Empty = Empty+styled s x = Styled s x++-- | Puts a 'Doc' in boldface.+bold :: HasChars a => Doc a -> Doc a+bold = styled (RWeight Bold)++-- | Puts a 'Doc' in italics.+italic :: HasChars a => Doc a -> Doc a+italic = styled (RShape Italic)++-- | Underlines a 'Doc'.+underlined :: HasChars a => Doc a -> Doc a+underlined = styled (RUnderline ULSingle)++-- | Puts a line through a 'Doc'.+strikeout :: HasChars a => Doc a -> Doc a+strikeout = styled (RStrikeout Struck)++-- The Color type is here as an opaque alias to Color8 for the public interface+-- and there's trivial smart constructors for the individual colors to+-- hopefully allow for easier extension to supporting indexed and rgb colors in+-- the future, without dramatically changing the public API.++type Color = Color8++-- | Set foreground color.+fg :: HasChars a => Color -> Doc a -> Doc a+fg = styled . RForeground . FG++-- | Set background color.+bg :: HasChars a => Color -> Doc a -> Doc a+bg = styled . RBackground . BG++blue :: Color+blue = Blue++black :: Color+black = Black++red :: Color+red = Red++green :: Color+green = Green++yellow :: Color+yellow = Yellow++magenta :: Color+magenta = Magenta++cyan :: Color+cyan = Cyan++white :: Color+white = White++-- | Make Doc a hyperlink.+link :: HasChars a => Text -> Doc a -> Doc a+link = Linked  -- | Returns width of a character in a monospace font:  0 for a combining -- character, 1 for a regular character, 2 for an East Asian wide character.
+ src/Text/DocLayout/ANSIFont.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE OverloadedStrings  #-}+{-# LANGUAGE DeriveDataTypeable #-}+module Text.DocLayout.ANSIFont+  ( Font(..)+  , baseFont+  , StyleReq(..)+  , Weight(..)+  , Shape(..)+  , Color8(..)+  , Underline(..)+  , Strikeout(..)+  , Foreground(..)+  , Background(..)+  , (~>)+  , renderFont+  , renderOSC8+  ) where++import Data.Data (Data)+import Data.String+import Data.Text (Text)++data Font = Font+  { ftWeight :: Weight,+    ftShape :: Shape,+    ftUnderline :: Underline,+    ftStrikeout :: Strikeout,+    ftForeground :: Foreground,+    ftBackground :: Background,+    ftLink :: Maybe Text+  }+  deriving (Show, Eq, Read, Data, Ord)++baseFont :: Font+baseFont = Font Normal Roman ULNone Unstruck FGDefault BGDefault Nothing++data Weight = Normal | Bold deriving (Show, Eq, Read, Data, Ord)+data Shape = Roman | Italic deriving (Show, Eq, Read, Data, Ord)+data Color8 = Black | Red | Green | Yellow | Blue | Magenta | Cyan | White deriving (Show, Eq, Enum, Read, Data, Ord)+data Underline = ULNone | ULSingle | ULDouble | ULCurly deriving (Show, Eq, Read, Data, Ord)+data Strikeout = Unstruck | Struck deriving (Show, Eq, Read, Data, Ord)+data Foreground = FGDefault | FG Color8 deriving (Show, Eq, Read, Data, Ord)+data Background = BGDefault | BG Color8 deriving (Show, Eq, Read, Data, Ord)++data StyleReq+  = RWeight Weight+  | RShape Shape+  | RForeground Foreground+  | RBackground Background+  | RUnderline Underline+  | RStrikeout Strikeout+  deriving (Show, Eq, Read, Data, Ord)++(~>) :: Font -> StyleReq -> Font+(~>) f (RWeight w) = f{ftWeight = w}+(~>) f (RShape s) = f{ftShape = s}+(~>) f (RForeground c) = f{ftForeground = c}+(~>) f (RBackground c) = f{ftBackground = c}+(~>) f (RUnderline u) = f{ftUnderline = u}+(~>) f (RStrikeout u) = f{ftStrikeout = u}++rawSGR :: (Semigroup a, IsString a) => a -> a+rawSGR n = "\ESC[" <> n <> "m"++class SGR b where+  renderSGR :: (Semigroup a, IsString a) => b -> a++instance SGR Weight where+  renderSGR Normal = rawSGR "22"+  renderSGR Bold = rawSGR "1"++instance SGR Shape where+  renderSGR Roman = rawSGR "23"+  renderSGR Italic = rawSGR "3"++instance SGR Foreground where+  renderSGR FGDefault = rawSGR "39"+  renderSGR (FG a) = (rawSGR . fromString . show . (+) 30 . fromEnum) a++instance SGR Background where+  renderSGR BGDefault = rawSGR "49"+  renderSGR (BG a) = (rawSGR . fromString . show . (+) 40 . fromEnum) a++instance SGR Underline where+  renderSGR ULNone = rawSGR "24"+  renderSGR ULSingle = rawSGR "4"+  renderSGR ULDouble = rawSGR "21"+  renderSGR ULCurly = rawSGR "4:3"++instance SGR Strikeout where+  renderSGR Unstruck = rawSGR "29"+  renderSGR Struck = rawSGR "9"++renderFont :: (Semigroup a, IsString a) => Font -> a+renderFont f+  | f == baseFont = rawSGR "0"+  | otherwise =+      renderSGR (ftWeight f)+        <> renderSGR (ftShape f)+        <> renderSGR (ftForeground f)+        <> renderSGR (ftBackground f)+        <> renderSGR (ftUnderline f)+        <> renderSGR (ftStrikeout f)++renderOSC8 :: (Semigroup a, IsString a) => Maybe a -> a+renderOSC8 Nothing = "\ESC]8;;\ESC\\"+renderOSC8 (Just t) = "\ESC]8;;" <> t <> "\ESC\\"
+ src/Text/DocLayout/Attributed.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE DeriveGeneric      #-}+{-# LANGUAGE DeriveTraversable  #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE FlexibleInstances #-}+module Text.DocLayout.Attributed (Attributed(..), Attr(..), Link, fromList, singleton)+  where++import Data.String+import Text.DocLayout.ANSIFont (Font, baseFont)+import Data.Data (Data, Typeable)+import GHC.Generics+import Data.Sequence ((><))+import qualified Data.Sequence as S+import Data.Text (Text)++type Link = Maybe Text++-- | Font attributes.+data Attr a = Attr Link Font a+  deriving (Show, Read, Eq, Ord, Functor, Foldable, Traversable,+    Data, Typeable, Generic)++instance Semigroup a => Semigroup (Attr a) where+  (<>) (Attr l f x) (Attr _ _ y) = Attr l f $ x <> y  -- This is arbitrary++instance (IsString a, Monoid a) => Monoid (Attr a) where+  mempty = Attr Nothing baseFont (fromString "")++-- | A sequence of strings with font attributes.+newtype Attributed a = Attributed (S.Seq (Attr a))+  deriving (Show, Read, Eq, Ord, Functor, Foldable, Traversable,+         Data, Typeable, Generic)++fromList :: [Attr a] -> Attributed a+fromList = Attributed . S.fromList++singleton :: Attr a -> Attributed a+singleton = Attributed . S.singleton++instance IsString a => IsString (Attr a) where+  fromString x = Attr Nothing baseFont (fromString x)++instance IsString a => IsString (Attributed a) where+  fromString x = Attributed $ S.singleton $ Attr Nothing baseFont (fromString x)++instance Semigroup a => Semigroup (Attributed a) where+  (<>) (Attributed a) (Attributed b) = Attributed $ a >< b++instance Monoid a => Monoid (Attributed a) where+  mempty = Attributed S.empty
+ src/Text/DocLayout/HasChars.hs view
@@ -0,0 +1,89 @@+{-# LANGUAGE CPP                #-}+{-# LANGUAGE FlexibleInstances  #-}+{-# LANGUAGE NoImplicitPrelude  #-}+{-# LANGUAGE OverloadedStrings  #-}+module Text.DocLayout.HasChars (HasChars(..)) where+import Prelude+import Data.String+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import Data.Text (Text)+import qualified Data.Text.Lazy.Builder as B+import Data.List (foldl', uncons)+import Data.Maybe (fromMaybe)+import Text.DocLayout.Attributed+import Data.Sequence (Seq(..), (|>))+import qualified Data.Sequence as S++-- | Class abstracting over various string types that+-- can fold over characters.  Minimal definition is 'foldrChar'+-- and 'foldlChar', but defining the other methods can give better+-- performance.+class (IsString a, Semigroup a, Monoid a, Show a) => HasChars a where+  foldrChar     :: (Char -> b -> b) -> b -> a -> b+  foldlChar     :: (b -> Char -> b) -> b -> a -> b+  replicateChar :: Int -> Char -> a+  replicateChar n c = fromString (replicate n c)+  isNull        :: a -> Bool+  isNull = foldrChar (\_ _ -> False) True+  splitLines    :: a -> [a]+  splitLines s = (fromString firstline : otherlines)+   where+    (firstline, otherlines) = foldrChar go ([],[]) s+    go '\n' (cur,lns) = ([], fromString cur : lns)+    go c    (cur,lns) = (c:cur, lns)+  build         :: a -> B.Builder+  build = foldrChar (mappend . B.singleton) (B.fromString "")++instance HasChars Text where+  foldrChar         = T.foldr+  foldlChar         = T.foldl'+  splitLines        = T.splitOn "\n"+  replicateChar n c = T.replicate n (T.singleton c)+  isNull            = T.null+  build             = B.fromText++instance HasChars String where+  foldrChar     = foldr+  foldlChar     = foldl'+  splitLines    = lines . (++"\n")+  replicateChar = replicate+  isNull        = null+  build         = B.fromString++instance HasChars TL.Text where+  foldrChar         = TL.foldr+  foldlChar         = TL.foldl'+  splitLines        = TL.splitOn "\n"+  replicateChar n c = TL.replicate (fromIntegral n) (TL.singleton c)+  isNull            = TL.null+  build             = B.fromLazyText++instance HasChars a => HasChars (Attr a) where+  foldrChar f a (Attr _ _ x) = foldrChar f a x+  foldlChar f a (Attr _ _ x) = foldlChar f a x+  splitLines (Attr l f x) = Attr l f <$> splitLines x+  build (Attr _ _ x) = build x++instance (HasChars a) => HasChars (Attributed a) where+  foldrChar _ acc (Attributed S.Empty) = acc+  foldrChar f acc (Attributed (xs :|> (Attr _ _ x))) =+    let l = foldrChar f acc x+        innerFold e a = foldrChar f a e+     in foldr innerFold l xs+  foldlChar _ acc (Attributed S.Empty) = acc+  foldlChar f acc (Attributed ((Attr _ _ x) :<| xs)) =+    let l = foldlChar f acc x+        innerFold e a = foldlChar f a e+     in foldr innerFold l xs+  splitLines (Attributed s) = fmap Attributed $ reverse $ go ([], S.empty) s+    where+      go (lns, cur) S.Empty = cur : lns+      go (lns, cur) (x :<| xs) =+        case splitLines x of+          []      -> go (cur : lns, S.empty) xs+          [k1]    -> go (lns, cur |> k1) xs+          k1 : ks ->+            let (end, most) = fromMaybe (S.empty, []) $ uncons $ reverse $ S.singleton <$> ks+             in go (most ++ (cur |> k1) : lns, end) xs+  build = foldMap build
test/test.hs view
@@ -287,6 +287,11 @@       (literal "a" <> cr <> literal "\nb" <> cr <> literal "c")       "a\n\nb\nc" +  , renderTest "breaking within styled text"+      (Just 5)+      ("hi" <+> (fg blue ("mom" <+> "and" <+> "dad")))+      "hi\nmom\nand\ndad"+   , testCase "length of normal text" $       realLength ("This is going to be too long anyway" :: String) @?= 35