||| Core definitions and primitives. module Text.PrettyPrint.WL.Core %default total %access export -- -------------------------------------------------------------------- [ Core ] ||| The abstract data type `Doc` represents pretty documents. ||| data Doc : Type where Empty : Doc Chara : (c : Char) -> Doc Text : (len : Int) -> (str : String) -> Doc Line : (undone : Bool) -> Doc Cat : (x : Doc) -> (y : Doc) -> Doc Nest : (lvl : Int) -> (x : Doc) -> Doc Union : (x : Doc) -> (y : Lazy Doc) -> Doc Column : (f : Int -> Doc) -> Doc Nesting : (f : Int -> Doc) -> Doc -- -------------------------------------------------------------- [ Primitives ] ||| The empty document is, indeed, empty. empty : Doc empty = Empty ||| 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 document `(char c)` contains the literal character `c`. If the ||| character is a newline (`'\n'`) the function returns a line ||| document. Ideally, the function 'line' should be used for line ||| breaks. char : (c : Char) -> Doc char '\n' = line char c = Chara c ||| The document `(text s)` contains the literal string `s`. The ||| string shouldn't contain any newline (`'\n'`) characters. If the ||| string contains newline characters, the function 'literal' should be ||| used. text : (str : String) -> Doc text "" = empty text str = Text (cast $ length str) str ||| The `breakLine` document advances to the next line and indents to ||| the current nesting level. Document `breakLine` behaves like ||| 'empty' if the line break is undone by 'group'. breakLine : Doc breakLine = Line True ||| Horizontal concatenation of documents beside : Doc -> Doc -> Doc beside = Cat ||| The document `(nest lvl x)` renders document `x` with the current ||| indentation level increased by lvl. nest : (lvl : Int) -> (x : Doc) -> Doc nest = Nest column : (f : Int -> Doc) -> Doc column = Column nesting : (f : Int -> Doc) -> Doc nesting = Nesting private %inline flatten : Doc -> Doc flatten (Line break) = if break then Empty else Text 1 " " flatten (Cat x y) = Cat (flatten x) (flatten y) flatten (Nest lvl x) = Nest lvl (flatten x) flatten (Union x _) = flatten x flatten (Column f) = Column (\i => flatten $ f i) flatten (Nesting f) = Nesting (\i => flatten $ f i) flatten other = other ||| The `group` combinator is used to specify alternative ||| layouts. The document `(group x)` undoes all line breaks in ||| document `x`. The resulting line is added to the current line if ||| that fits the page. Otherwise, the document `x` is rendered without ||| any changes. group : Doc -> Doc group x = Union (flatten x) x Semigroup Doc where (<+>) x y = beside x y Monoid Doc where neutral = empty fold : (f : Doc -> Doc -> Doc) -> (ds : List Doc) -> Doc fold _ Nil = empty fold f (x::xs) = foldl f x xs -- --------------------------------------------------------------- [ Renderers ] namespace PrettyDoc ||| The data type `PrettyDoc` represents rendered documents and is ||| used by the display functions. data PrettyDoc : Type where Empty : PrettyDoc Chara : (c : Char) -> (rest : PrettyDoc) -> PrettyDoc Text : (len : Int) -> (str : String) -> (rest : PrettyDoc) -> PrettyDoc Line : (lvl : Int) -> (rest : PrettyDoc) -> PrettyDoc ||| Helper function to construct spaces. spaces : (n : Int) -> String spaces n = if n <= 0 then "" else pack $ replicate (cast n) ' ' private indentation : Int -> String indentation n = spaces n ||| `(showPrettyDoc doc)` takes the output `PrettyDoc` from a ||| rendering function and transforms it to a String for printing. showPrettyDoc : (doc : PrettyDoc) -> String showPrettyDoc doc = showPrettyDocS doc "" where showPrettyDocS : (doc : PrettyDoc) -> (acc : String) -> String showPrettyDocS Empty acc = acc showPrettyDocS (Chara c rest) acc = strCons c (showPrettyDocS rest acc) showPrettyDocS (Text len str rest) acc = str ++ (showPrettyDocS rest acc) showPrettyDocS (Line lvl rest) acc = (strCons '\n' (indentation lvl)) ++ showPrettyDocS rest acc private fits : (w : Int) -> (x : PrettyDoc) -> Bool fits w Empty = if w < 0 then False else True fits w (Chara c rest) = if w < 0 then False else fits (w - 1) rest fits w (Text len str rest) = if w < 0 then False else fits (w - len) rest fits w (Line _ _) = if w < 0 then False else True ||| Use the Wadler-Leijen algorithm to pretty print the `doc`. ||| The algorithm uses a page width of `width` and a ribbon ||| width of `(rfrac * width)` characters. The ribbon width is ||| the maximal amount of non-indentation characters on a line. The ||| parameter `rfrac` should be between `0.0` and `1.0`. If it is ||| lower or higher, the ribbon width will be 0 or `width` ||| respectively. render : (rfrac : Double) -> (width : Int) -> (doc : Doc) -> PrettyDoc render rfrac w doc = best w 0 0 doc (const Empty) where rwidth : Int rwidth = max 0 $ min w (cast (cast w * rfrac)) nicest : (lvl : Int) -> (col : Int) -> (x : PrettyDoc) -> (y : Lazy PrettyDoc) -> PrettyDoc nicest n k x y = if fits (min (w - k) (rwidth - k + n)) x then x else y best : (lvl : Int) -> (col : Int) -> (curr : Int) -> (doc : Doc) -> (k : Int -> PrettyDoc) -> PrettyDoc best lvl col curr Empty k = k col best lvl col curr (Chara c) k = Chara c $ k (col + 1) best lvl col curr (Text len str) k = Text len str $ k (col + len) best lvl col curr (Line undone) k = Line curr $ k curr best lvl col curr (Cat x y) k = best lvl col curr x (\curr' => best lvl curr' curr y k) best lvl col curr (Nest x y) k = best lvl col (curr+x) y k best lvl col curr (Union x y) k = nicest lvl col (best lvl col curr x k) (best lvl col curr y k) best lvl col curr (Column f) k = best lvl col curr (f col) k best lvl col curr (Nesting f) k = best lvl col curr (f curr) k ||| Print the document to a string using the given ribbon fraction ||| and page width. toString : (rfrac : Double) -> (width : Int) -> (doc : Doc) -> String toString rfrac width doc = showPrettyDoc (render rfrac width doc) writeDoc : (fname : String) -> (rfrac : Double) -> (width : Int) -> (doc : Doc) -> IO (Either FileError ()) writeDoc fname rfrac width doc = writeFile fname $ toString rfrac width doc namespace Default ||| Render the document with a default ribbon fraction of 0.4 and page width of 120 characters. render : (doc : Doc) -> PrettyDoc render = render 0.4 120 ||| Print the document to a string using a default ribbon fraction ||| of 0.4 and page width of 120 characters. toString : (doc : Doc) -> String toString = toString 0.4 120 ||| Write the doc to `fname` using a default ribbon fraction of 0.4 ||| and page width of 120 characters. writeDoc : (fname : String) -> (doc : Doc) -> IO (Either FileError ()) writeDoc fname doc = writeFile fname $ Default.toString doc