{-# LANGUAGE OverloadedStrings #-} module MarXup.PrettyPrint where import Control.Applicative import Data.Monoid -- import MarXup.Latex () import MarXup.Tex -- import MarXup.MultiRef (BoxSpec(..)) import MarXup.PrettyPrint.Core import MarXup.PrettyPrint.Core as MarXup.PrettyPrint type Docu = Tex Doc text :: TeX -> Tex Doc text body = do b <- justBox body return $ Text $ TeX body b infixr 5 ,,<$$$>,<$$> infixr 6 <+> -- -- | The document @(list xs)@ comma separates the documents @xs@ and -- -- encloses them in square brackets. The documents are rendered -- -- horizontally if that fits the page. Otherwise they are aligned -- -- vertically. All comma separators are put in front of the elements. list :: [Doc] -> Tex Doc list = enclosure "[" "]" "," -- -- | The document @(tupled xs)@ comma separates the documents @xs@ and -- -- encloses them in parenthesis. The documents are rendered -- -- horizontally if that fits the page. Otherwise they are aligned -- -- vertically. All comma separators are put in front of the elements. -- tupled :: [Doc] -> Doc -- tupled = enclosure lparen rparen comma -- -- | The document @(semiBraces xs)@ separates the documents @xs@ with -- -- semi colons and encloses them in braces. The documents are rendered -- -- horizontally if that fits the page. Otherwise they are aligned -- -- vertically. All semi colons are put in front of the elements. -- semiBraces :: [Doc] -> Doc -- semiBraces = enclosure lbrace rbrace semi -- | The document @(enclosure l r sep xs)@ concatenates the documents -- @xs@ separated by @sep@ and encloses the resulting document by @l@ -- and @r@. The documents are rendered horizontally if that fits the -- page. Otherwise they are aligned vertically. All separators are put -- in front of the elements. For example, the combinator 'list' can be -- defined with @enclosure@: -- -- > list xs = enclosure lbracket rbracket comma xs -- > test = text "list" <+> (list (map int [10,200,3000])) -- -- Which is layed out with a page width of 20 as: -- -- @ -- list [10,200,3000] -- @ -- -- But when the page width is 15, it is layed out as: -- -- @ -- list [10 -- ,200 -- ,3000] -- @ encloseSep :: Doc -> Doc -> Doc -> [Doc] -> Doc encloseSep left right sep ds = case ds of [] -> left <> right [d] -> left <> d <> right _ -> align (cat (zipWith (<>) (left : repeat sep) ds) <> right) enclosure :: TeX -> TeX -> TeX -> [Doc] -> Tex Doc enclosure left right separ ds = do l <- text left r <- text right s <- text separ return $ encloseSep l r s ds ----------------------------------------------------------- -- punctuate p [d1,d2,...,dn] => [d1 <> p,d2 <> p, ... ,dn] ----------------------------------------------------------- -- | @(punctuate p xs)@ concatenates all documents in @xs@ with -- document @p@ except for the last document. -- -- > someText = map text ["words","in","a","tuple"] -- > test = parens (align (cat (punctuate comma someText))) -- -- This is layed out on a page width of 20 as: -- -- @ -- (words,in,a,tuple) -- @ -- -- But when the page width is 15, it is layed out as: -- -- @ -- (words, -- in, -- a, -- tuple) -- @ -- -- (If you want put the commas in front of their elements instead of -- at the end, you should use 'tupled' or, in general, 'encloseSep'.) punctuate :: Doc -> [Doc] -> [Doc] punctuate p [] = [] punctuate p [d] = [d] punctuate p (d:ds) = (d <> p) : punctuate p ds ----------------------------------------------------------- -- high-level combinators ----------------------------------------------------------- -- | The document @(sep xs)@ concatenates all documents @xs@ either -- horizontally with @(\<+\>)@, if it fits the page, or vertically with -- @(\<$\>)@. -- -- > sep xs = group (vsep xs) sep :: [Doc] -> Doc sep = group . vsep -- | The document @(fillSep xs)@ concatenates documents @xs@ -- horizontally with @(\<+\>)@ as long as its fits the page, than -- inserts a @line@ and continues doing that for all documents in -- @xs@. -- -- > fillSep xs = foldr (\<\/\>) empty xs fillSep :: [Doc] -> Doc fillSep = foldDoc () -- | The document @(hsep xs)@ concatenates all documents @xs@ -- horizontally with @(\<+\>)@. hsep :: [Doc] -> Doc hsep = foldDoc (<+>) -- | The document @(vsep xs)@ concatenates all documents @xs@ -- vertically with @(\<$\>)@. If a 'group' undoes the line breaks -- inserted by @vsep@, all documents are separated with a space. -- -- > someText = map text (words ("text to lay out")) -- > -- > test = text "some" <+> vsep someText -- -- This is layed out as: -- -- @ -- some text -- to -- lay -- out -- @ -- -- The 'align' combinator can be used to align the documents under -- their first element -- -- > test = text "some" <+> align (vsep someText) -- -- Which is printed as: -- -- @ -- some text -- to -- lay -- out -- @ vsep :: [Doc] -> Doc vsep = foldDoc (<$$$>) -- | The document @(cat xs)@ concatenates all documents @xs@ either -- horizontally with @(\<\>)@, if it fits the page, or vertically with -- @(\<$$\>)@. -- -- > cat xs = group (vcat xs) cat :: [Doc] -> Doc cat = group . vcat -- | The document @(fillCat xs)@ concatenates documents @xs@ -- horizontally with @(\<\>)@ as long as its fits the page, than inserts -- a @linebreak@ and continues doing that for all documents in @xs@. -- -- > fillCat xs = foldr (\<\/\/\>) empty xs fillCat :: [Doc] -> Doc fillCat = foldDoc () -- | The document @(hcat xs)@ concatenates all documents @xs@ -- horizontally with @(\<\>)@. hcat :: [Doc] -> Doc hcat = foldDoc (<>) -- | The document @(vcat xs)@ concatenates all documents @xs@ -- vertically with @(\<$$\>)@. If a 'group' undoes the line breaks -- inserted by @vcat@, all documents are directly concatenated. vcat :: [Doc] -> Doc vcat = foldDoc (<$$>) foldDoc :: (Doc -> Doc -> Doc) -> [Doc] -> Doc foldDoc f [] = mempty foldDoc f ds = foldr1 f ds -- | The document @(x \<+\> y)@ concatenates document @x@ and @y@ with a -- @space@ in between. (infixr 6) (<+>) :: Doc -> Doc -> Doc x <+> y = x <> space <> y -- | The document @(x \<\/\> y)@ concatenates document @x@ and @y@ with a -- 'softline' in between. This effectively puts @x@ and @y@ either -- next to each other (with a @space@ in between) or underneath each -- other. (infixr 5) () :: Doc -> Doc -> Doc x y = x <> softline <> y -- | The document @(x \<\/\/\> y)@ concatenates document @x@ and @y@ with -- a 'softbreak' in between. This effectively puts @x@ and @y@ either -- right next to each other or underneath each other. (infixr 5) () :: Doc -> Doc -> Doc x y = x <> softbreak <> y -- | The document @(x \<$\> y)@ concatenates document @x@ and @y@ with a -- 'line' in between. (infixr 5) (<$$$>) :: Doc -> Doc -> Doc x <$$$> y = x <> line <> y -- | The document @(x \<$$\> y)@ concatenates document @x@ and @y@ with -- a @linebreak@ in between. (infixr 5) (<$$>) :: Doc -> Doc -> Doc x <$$> y = x <> linebreak <> y -- | The document @softline@ behaves like 'space' if the resulting -- output fits the page, otherwise it behaves like 'line'. -- -- > softline = group line softline :: Doc softline = group line -- | The document @softbreak@ behaves like 'empty' if the resulting -- output fits the page, otherwise it behaves like 'line'. -- -- > softbreak = group linebreak softbreak :: Doc softbreak = group linebreak -- -- | Document @(squotes x)@ encloses document @x@ with single quotes -- -- \"'\". -- squotes :: Doc -> Doc -- squotes = enclose squote squote -- -- | Document @(dquotes x)@ encloses document @x@ with double quotes -- -- '\"'. -- dquotes :: Doc -> Doc -- dquotes = enclose dquote dquote -- -- | Document @(braces x)@ encloses document @x@ in braces, \"{\" and -- -- \"}\". -- braces :: Doc -> Doc -- braces = enclose lbrace rbrace -- -- | Document @(parens x)@ encloses document @x@ in parenthesis, \"(\" -- -- and \")\". -- parens :: Doc -> Doc -- parens = enclose lparen rparen -- -- | Document @(angles x)@ encloses document @x@ in angles, \"\<\" and -- -- \"\>\". -- angles :: Doc -> Doc -- angles = enclose langle rangle -- -- | Document @(brackets x)@ encloses document @x@ in square brackets, -- -- \"[\" and \"]\". -- brackets :: Doc -> Doc -- brackets = enclose lbracket rbracket -- | The document @(enclose l r x)@ encloses document @x@ between -- documents @l@ and @r@ using @(\<\>)@. -- -- > enclose l r x = l <> x <> r enclose :: Doc -> Doc -> Doc -> Doc enclose l r x = l <> x <> r ----------------------------------------------------------- -- semi primitive: fill and fillBreak ----------------------------------------------------------- -- | The document @(fillBreak i x)@ first renders document @x@. It -- than appends @space@s until the width is equal to @i@. If the -- width of @x@ is already larger than @i@, the nesting level is -- increased by @i@ and a @line@ is appended. When we redefine @ptype@ -- in the previous example to use @fillBreak@, we get a useful -- variation of the previous output: -- -- > ptype (name,tp) -- > = fillBreak 6 (text name) <+> text "::" <+> text tp -- -- The output will now be: -- -- @ -- let empty :: Doc -- nest :: Double -> Doc -> Doc -- linebreak -- :: Doc -- @ fillBreak :: Double -> Doc -> Doc fillBreak f x = width x (\w -> if (w > f) then nest f linebreak else spacing (f - w)) -- | The document @(fill i x)@ renders document @x@. It than appends -- @space@s until the width is equal to @i@. If the width of @x@ is -- already larger, nothing is appended. This combinator is quite -- useful in practice to output a list of bindings. The following -- example demonstrates this. -- -- > types = [("empty","Doc") -- > ,("nest","Double -> Doc -> Doc") -- > ,("linebreak","Doc")] -- > -- > ptype (name,tp) -- > = fill 6 (text name) <+> text "::" <+> text tp -- > -- > test = text "let" <+> align (vcat (map ptype types)) -- -- Which is layed out as: -- -- @ -- let empty :: Doc -- nest :: Double -> Doc -> Doc -- linebreak :: Doc -- @ fill :: Double -> Doc -> Doc fill f d = width d (\w -> if (w >= f) then mempty else spacing (f - w)) width :: Doc -> (Double -> Doc) -> Doc width d f = column (\k1 -> d <> column (\k2 -> f (k2 - k1))) ----------------------------------------------------------- -- semi primitive: Alignment and indentation ----------------------------------------------------------- -- | The document @(indent i x)@ indents document @x@ with @i@ spaces. -- -- > test = indent 4 (fillSep (map text -- > (words "the indent combinator indents these words !"))) -- -- Which lays out with a page width of 20 as: -- -- @ -- the indent -- combinator -- indents these -- words ! -- @ indent :: Double -> Doc -> Doc indent i d = hang i (spacing i <> d) -- | The hang combinator implements hanging indentation. The document -- @(hang i x)@ renders document @x@ with a nesting level set to the -- current column plus @i@. The following example uses hanging -- indentation for some text: -- -- > test = hang 4 (fillSep (map text -- > (words "the hang combinator indents these words !"))) -- -- Which lays out on a page with a width of 20 characters as: -- -- @ -- the hang combinator -- indents these -- words ! -- @ -- -- The @hang@ combinator is implemented as: -- -- > hang i x = align (nest i x) hang :: Double -> Doc -> Doc hang i d = align (nest i d) -- | The document @(align x)@ renders document @x@ with the nesting -- level set to the current column. It is used for example to -- implement 'hang'. -- -- As an example, we will put a document right above another one, -- regardless of the current nesting level: -- -- > x $$ y = align (x <$$$> y) -- -- > test = text "hi" <+> (text "nice" $$ text "world") -- -- which will be layed out as: -- -- @ -- hi nice -- world -- @ align :: Doc -> Doc align d = column (\k -> nesting (\i -> nest (k - i) d)) --nesting might be negative :-) -- | The @line@ document advances to the next line and indents to the -- current nesting level. Document @line@ behaves like @(text \" \")@ -- if the line break is undone by 'group'. line :: Doc line = Line False -- | The @linebreak@ document advances to the next line and indents to -- the current nesting level. Document @linebreak@ behaves like -- 'empty' if the line break is undone by 'group'. linebreak :: Doc linebreak = Line True -- | The document @(nest i x)@ renders document @x@ with the current -- indentation level increased by i (See also 'hang', 'align' and -- 'indent'). -- -- > nest 2 (text "hello" <$$$> text "world") <$$$> text "!" -- -- outputs as: -- -- @ -- hello -- world -- ! -- @ nest :: Double -> Doc -> Doc nest i x = Nest i x column, nesting :: (Double -> Doc) -> Doc column f = Column f nesting f = Nesting f