Pretty printing (was: Standard Library for text formatting?)

Zdenek Dvorak rakdver@hotmail.com
Mon, 26 Aug 2002 17:33:16 +0000


Hello,

> > After doing some searching, it seems that "pretty printing" is
> > a prominant "Haskell way" of doing text output.  I still am
> > interested in finding a library of standard text formatting
> > (String formatting) functions, but it seems like it might
> > be worth my while investigating pretty printing.
>
>Pretty printing is not very suited for printf like formatting,

I have looked for something simmilar some time ago, but unsuccesfully. 
Pretty printing libraries are not very useful for breaking paragraphs into 
lines, distributing spaces evenly, etc. (at least I was not able to persuade 
them to work like that). I have written following module that implements 
stupid first-fit algorithm for these purposes; the interface is simmilar to 
the pretty printing libraries.

Zdenek Dvorak

module ReportPrinter (
LowDoc,HighDoc,
(<>),(<+>),($$),(<$>),
int, lowText, text, empty,
colon, comma, space, semi, period,
brackets, parens,
vcat, sep,
punctuate,
high, nest, par,
render
) where

data LowDoc=Text String |
            Join LowDoc LowDoc |
            Append LowDoc LowDoc |
            Empty deriving (Show)

data HighDoc=HLowDoc LowDoc |
             Paragraph Int HighDoc |
             Nest Int HighDoc |
             Above HighDoc HighDoc |
             Beside HighDoc HighDoc deriving (Show)

(<>)::LowDoc->LowDoc->LowDoc
x <> Empty = x
Empty <> x = x
x <> y     = Join x y

(<+>)::LowDoc->LowDoc->LowDoc
x <+> Empty = x
Empty <+> x = x
x <+> y     = Append x y

(<$>)::HighDoc->HighDoc->HighDoc
x <$> HLowDoc Empty=x
HLowDoc Empty <$> x=x
x <$> y=Beside x y

($$)::HighDoc->HighDoc->HighDoc
x $$ HLowDoc Empty=x
HLowDoc Empty $$ x=x
x $$ y=Above x y

lowText::String->LowDoc
lowText=Text

text::String->LowDoc
text str=sep $ map lowText $ words str

int::Int->LowDoc
int=text . show

empty::LowDoc
empty=Empty

colon::LowDoc
colon=lowText ":"

comma::LowDoc
comma=lowText ","

space::LowDoc
space=lowText " "

semi::LowDoc
semi=lowText ";"

period::LowDoc
period=lowText "."

brackets::LowDoc->LowDoc
brackets doc=lowText "[" <> doc <> lowText "]"

parens::LowDoc->LowDoc
parens doc=lowText "(" <> doc <> lowText ")"

sep::[LowDoc]->LowDoc
sep=foldl (<+>) empty

high::LowDoc->HighDoc
high=HLowDoc

vcat::[HighDoc]->HighDoc
vcat=foldl ($$) (high empty)

punctuate::LowDoc->[LowDoc]->[LowDoc]
punctuate _ [] = []
punctuate _ [x] = [x]
punctuate pun (h:t) = (h <> pun) : punctuate pun t

nest::Int->HighDoc->HighDoc
nest=Nest

par::Int->HighDoc->HighDoc
par=Paragraph

render::Int->HighDoc->String
render width doc=highRender width 0 0 doc ""

highRender::Int->Int->Int->HighDoc->String->String
highRender width indFirst indRest doc rest=
case doc of
  HLowDoc lowDoc     -> lowRender width indFirst indRest lowDoc rest
  Paragraph ind pDoc -> highRender width indFirst (indRest+ind) pDoc rest
  Nest ind pDoc      -> highRender width (indFirst+ind) (indRest+ind) pDoc 
rest
  Above doc1 doc2    -> highRender width indFirst indRest doc1 $
                        highRender width indRest indRest doc2 rest
  Beside doc1 doc2   -> error "Rendering two HighDocs beside not supported."

lowRender::Int->Int->Int->LowDoc->String->String
lowRender width indFirst indRest doc rest=
  stringRender width indFirst indRest strs rest
  where
   (strs,_) = stringify False doc []
   stringify False (Text txt) rest =
               ((txt,length txt):rest,False)
   stringify True (Text txt) [] =
               ([(txt,length txt)],False)
   stringify True (Text txt) ((txt',len'):rest) =
               ((txt++txt',length txt+len'):rest,False)
   stringify join Empty rest = (rest,join)
   stringify join (Append doc1 doc2) rest =
     let (rest',join')=stringify join doc2 rest
      in stringify join' doc1 rest'
   stringify join (Join doc1 doc2) rest =
     let (rest',_)=stringify join doc2 rest
      in stringify True doc1 rest'

stringRender::Int->Int->Int->[(String,Int)]->String->String
stringRender width indFirst indRest [] rest=rest
stringRender width indFirst indRest strs rest=
let (lne,rst)=cutLine (width-indFirst) strs
     rest'=stringRender width indRest indRest rst rest
  in lineRender (rst==[]) width indFirst lne rest'


cutLine tot ((txt,len):t)=(txt:lne',rst)
where
  (lne',rst)=cutLine' (tot-len) t
  cutLine' ml []=([],[])
  cutLine' ml r@((tx,ln):t')
   | ml>ln     = let (lne'',rst')=cutLine' (ml-ln-1) t'
                  in (tx:lne'',rst')
   | otherwise = ([],r)

lineRender::Bool->Int->Int->[String]->String->String
lineRender last width ind lne rest=indent ind rslt
where
  totlen   = sum $ map length lne
  words    = length lne
  reqlen   = totlen + words - 1
  havelen  = width - ind
  distsp   = if havelen > reqlen && not last then havelen - reqlen else 0
  count    = words `div` 2
  rslt     = distribSpaces count distsp words lne rest
  indent n = (take n (cycle " ") ++)

distribSpaces::Int->Int->Int->[String]->String->String
distribSpaces _ _ _ [h] rest = h ++ '\n' : rest
distribSpaces count minus plus (h:t) rest = h ++ ' ' : spaced
where
  (spaced,count') = recount count
  rest' = distribSpaces (count'-minus) minus plus t rest
  recount cnt
   | cnt < 0   = let (spaced',cnt')=recount (cnt+plus)
                  in (' ':spaced',cnt')
   | otherwise = (rest',cnt)

_________________________________________________________________
Join the world’s largest e-mail service with MSN Hotmail. 
http://www.hotmail.com