uu-cco-0.1.0.0: src/CCO/Printing/Rendering.hs
-------------------------------------------------------------------------------
-- |
-- Module : CCO.Printing.Rendering
-- Copyright : (c) 2008 Utrecht University
-- License : All rights reserved
--
-- Maintainer : stefan@cs.uu.nl
-- Stability : provisional
-- Portability : portable
--
-- Facilities for rendering pretty-printable documents.
--
-------------------------------------------------------------------------------
module CCO.Printing.Rendering (
render -- :: Printer a => Int -> Doc -> Maybe a
) where
import CCO.Printing.Colour (Colour)
import CCO.Printing.Doc (Doc (..))
import CCO.Printing.Printer (Printer (..))
import Data.Maybe (listToMaybe)
import Data.Monoid (Monoid (..))
import Data.List (intersperse)
-- This module is at the heart of the pretty-printing library.
-- Rendering involves a hierarchy of data structures, that share a common
-- interface ('PP').
-- At the top of the hierarchy, we have 'Formatters' that, given a amount of
-- available horizontal space, spawn a list of all possible 'Format's, from
-- which concrete printers can be obtained.
-- Across the hierarchy, empty documents are expected to not appear as operands
-- to \"above\" or \"besides\" operations.
-- (The hence necessary normalisation is performed within the function
-- 'fromDoc'.)
-------------------------------------------------------------------------------
-- The PP class
-------------------------------------------------------------------------------
-- | The common interface of the data structures in the rendering hierarchy.
--
-- A minimal complete definition must supply all six methods, i.e., @ppEmpty@,
-- @ppText@, @ppIndent@, @ppAbove@, @ppBesides@, and @ppColour@.
class PP a where
-- | Involved in rendering the empty document.
ppEmpty :: a
-- | Involved in rendering text.
ppText :: String -> a
-- ^ Involved in rendering an indented document.
ppIndent :: Int -> a -> a
-- ^ Involved in rendering two stacked up documents.
ppAbove :: a -> a -> a
-- ^ Involved in rendering two \"dovetailed\" documents.
ppBesides :: a -> a -> a
-- ^ Involved in rendering coloured output.
ppColour :: Colour -> a -> a
-------------------------------------------------------------------------------
-- Concrete renderings
-------------------------------------------------------------------------------
-- | The most low-level data structure in the rendering hierarchy.
--
-- A @Rendering@ holds a list of printers: one for each printed line within the
-- rendered document.
newtype Rendering a = R [a]
instance Printer a => PP (Rendering a) where
ppEmpty = R []
ppText txt = R [ptext txt]
ppIndent n (R ps) = R (map (ws n `mappend`) ps)
ppAbove (R psl) (R psr) = R (psl ++ psr)
-- \"dovetailing\" involves concatenating
-- * all but the last of the lines in the left-hand side rendering;
-- * the line obtained by glueing together the last and the first line of
-- the left- and right-hand side renderings, respectively; and
-- * the lines obtained by prefixing all but the first of the lines in the
-- right-hand side rendering with an amount of whitespace that matches the
-- width of the last line in the left-hand side rendering
ppBesides (R []) rr = rr
ppBesides rl (R []) = rl
ppBesides (R psl) (R (pr : psr')) = let (psl', pl) = unSnoc psl
prefix = ws (width pl)
in R $ psl' ++ [pl `mappend` pr] ++
map (prefix `mappend`) psr'
ppColour _ r@(R []) = r
ppColour c (R [p]) = R [beginColour c `mappend` p `mappend` endColour]
ppColour c (R (p : ps)) = let (ps', p') = unSnoc ps
in R $ beginColour c `mappend` p :
ps' ++ [p' `mappend` endColour]
-------------------------------------------------------------------------------
-- Dimensions
-------------------------------------------------------------------------------
-- | Provides an abstraction of a 'Rendering': the total height, total width,
-- and the width of the last line of a rendering.
data Dim = D !Int !Int !Int
instance PP Dim where
ppEmpty = D 0 0 0
ppText txt = D 1 w w where w = length txt
ppIndent n (D h w l) = D h (w + n) (l + n)
ppAbove (D hl wl _) (D hr wr lr) = D (hl + hr) (wl `max` wr) lr
ppBesides (D hl _ ll) (D hr wr lr) = D (hl + hr - 1) (ll + wr) (ll + lr)
ppColour _ d = d
-------------------------------------------------------------------------------
-- Formats
-------------------------------------------------------------------------------
-- | A concrete 'Rendering' together with its 'Dim'-abstraction.
data Format a = Rendering a :^ Dim
instance Printer a => PP (Format a) where
ppEmpty = ppEmpty :^ ppEmpty
ppText txt = ppText txt :^ ppText txt
ppIndent n (r :^ d) = ppIndent n r :^ ppIndent n d
ppAbove (rl :^ dl) (rr :^ dr) = ppAbove rl rr :^ ppAbove dl dr
ppBesides (rl :^ dl) (rr :^ dr) = ppBesides rl rr :^ ppBesides dl dr
ppColour c (r :^ d) = ppColour c r :^ ppColour c d
-------------------------------------------------------------------------------
-- Formatters
-------------------------------------------------------------------------------
-- | Represents the minimal amount of horizontal space needed to render a
-- document, expressed in terms of the total width and the width of the last
-- line in the most efficient rendering.
type Frame = (Int, Int)
-- | The top-level data structure in the rendering hierarchy: it provides the
-- minimal amount of horizontal space needed to render the document (expressed
-- in terms of a 'Frame') together with a function that spawns all possible
-- formats for a document that fit within a specifed amount of horizontal
-- space (expressed in terms of the total width of a rendering).
--
-- @Formatter@s maintain the invariant that the lists of possible formats are
-- sorted on ascending heights and descending widths /simultaneously/.
data Formatter a = Frame :< (Int -> [Format a])
instance Printer a => PP (Formatter a) where
ppEmpty = (0, 0) :< \_ -> [ppEmpty]
ppText txt = let w = length txt
in (w, w) :< \wmax -> if w <= wmax then [ppText txt] else []
ppIndent n ((wmin, lmin) :< spwn)
= (wmin + n, lmin + n) :< \wmax ->
[ppIndent n fmt | fmt <- spwn (wmax - n)]
-- * if we stack to documents, we have to be careful not to generate too many
-- formats;
-- * more specifically, we do not have to take the cross product of the lists
-- of formats for the upper document and the lower document
-- * rather, if we move through the lists in order of descending widths, it
-- is sufficient to combine each format with at most one format of lesser
-- or equal width: combining it with other formats of lesser width cannot
-- yield narrower formats, for the width of the format itself will
-- determine the combined width with those other formats;
-- * therefore, we can process the two lists of formats in a left-to-right
-- fashion, as implemented by the auxiliary function 'stack'
ppAbove ((wminl, _) :< spwnl) ((wminr, lminr) :< spwnr)
= (wminl `max` wminr, lminr) :< \wmax ->
if wminl <= wmax && wminr <= wmax
then stack (spwnl wmax) (spwnr wmax)
else []
-- * a straightforward approach to \"dovetailing\" would be to first spawn
-- all formats that fit the available horizontal space for the left-hand
-- document and then, /for each of these formats/, determine the amount of
-- space left and spawn all fiting formats for the right-hand document;
-- * however, this can be rather costly if there are many fitting formats for
-- the left-hand document;
-- * therefore, we adopt an alternative technique: we spawn all left-hand
-- formats and use the lower bound on the widths of their last lines
-- (provided by the 'Frame' component of the left-hand formatter) as a
-- conservative estimate on the amount of horizontal space available for
-- the right-hand formats;
-- * this way, we may end up with combined formats that are too wide
-- * so, after combining the formats, the ones that occupy too much space are
-- cut off
ppBesides ((_, lminl) :< spwnl) ((wminr, lminr) :< spwnr)
= (lminl + wminr, lminl + lminr) :< \wmax ->
let fmtsl = spwnl wmax
fmtsr = spwnr (wmax - lminl)
in if lminl + wminr <= wmax
then (cut wmax . foldr merge [])
[[ppBesides fmtl fmtr | fmtr <- fmtsr] | fmtl <- fmtsl]
else []
ppColour c (frm :< spwn) = (frm :< \wmax -> map (ppColour c) (spwn wmax))
-- | Drops all 'Format's that do not fit within a specified amount of
-- horizontal space (expressed as the total width of a rendering).
cut :: Int -> [Format a] -> [Format a]
cut wmax = dropWhile tooWide -- we can use 'dropWhile', for we may assume
-- that the formats are sorted on descending
-- widths
where
tooWide (_ :^ D _ w _) = w > wmax
-- | A (proper) partial order on formats, according to which the lists
-- produced by 'Formatter's are sorted.
(<=.) :: Format a -> Format a -> Bool
(_ :^ D hl wl _) <=. (_ :^ D hr wr _) = hl <= hr && wl >= wr
-- | Merges two lists of 'Format's preserving the '(<=.)'-ordering of their
-- elements.
--
-- Because '(<=.)' is a proper partial order, elements from the right-hand
-- list may be dropped if they cannot be merged into the left-hand side list.
merge :: [Format a] -> [Format a] -> [Format a]
merge [] fmtsr = fmtsr
merge fmtsl [] = fmtsl
merge fmtsl@(fmtl : fmtsl') fmtsr@(fmtr : fmtsr')
| fmtl <=. fmtr = fmtl : merge fmtsl' fmtsr
| fmtr <=. fmtl = fmtr : merge fmtsl fmtsr'
| otherwise = fmtl : merge fmtsl' fmtsr'
-- | Takes two sorted lists of formats and combines them to form a list that
-- containts the most efficient formats obtained by stacking formats from the
-- first list on top of formats from the second list.
--
-- (Auxiliary to the implementation of 'ppAbove' for 'Formatter'.)
stack :: Printer a => [Format a] -> [Format a] -> [Format a]
stack [] fmtsr = []
stack fmtsl [] = []
stack fmtsl@(fmtl : fmtsl') fmtsr@(fmtr : fmtsr') = case (fmtl, fmtr) of
(_ :^ D _ wl _, _ :^ D _ wr _)
| wl <= wr -> ppAbove fmtl fmtr : stack fmtsl fmtsr'
| otherwise -> ppAbove fmtl fmtr : stack fmtsl' fmtsr
-- | Takes a single-line text and produces a @Formatter@ for
-- its \'paragraph fill'.
ppWrapped :: Printer a => String -> Formatter a
ppWrapped txt = frm :< \wmax -> if null wrds then [ppText ""] else
let ws = [wmax, wmax - minw .. minw]
in [ppFill w wrds | w <- ws]
where
wrds = [(txt', length txt') | txt' <- words txt]
lengths = map snd wrds
frm@(minw, _) | null wrds = (0, 0)
| otherwise = (maximum lengths, last lengths)
-- | Takes a list of words and their lengths and produces the 'Format' for its
-- \"paragraph\" fill with respect to a given paragraph width.
-- Words on the same line are separated by a single whitespace character.
ppFill :: Printer a => Int -> [(String, Int)] -> Format a
ppFill _ [] = ppText ""
ppFill wmax wrds = foldr1 ppAbove . map (ppText . unwords) . fill (-1) id $
wrds
where
fill _ acc [] = [acc []]
fill n acc wrds@((txt, len) : wrds')
| n + len <= wmax - 1 = fill (n + len + 1) (acc . (txt :)) wrds'
| otherwise = acc [] : fill (- 1) id wrds
-- | Parallelisation: combines two alternative formatters into a single
-- formatter.
ppSplit :: Formatter a -> Formatter a -> Formatter a
ppSplit ((wminl, lminl) :< spwnl) ((wminr, lminr) :< spwnr)
= (wminl `min` wminr, lminl `min` lminr) :< \wmax -> case () of
_ | wminl <= wmax && wminr < wmax -> merge (spwnl wmax) (spwnr wmax)
| wminl <= wmax -> spwnl wmax
| wminr <= wmax -> spwnr wmax
| otherwise -> []
-- | Joins all parallel formats into a single format.
ppJoin :: Formatter a -> Formatter a
ppJoin (frm :< spwn) = frm :< \wmax -> case spwn wmax of
[] -> []
fmt : _ -> [fmt]
-------------------------------------------------------------------------------
-- Interface
-------------------------------------------------------------------------------
-- | Retrieves a concrete printer for a 'Format'.
toPrinter :: Printer a => Format a -> a
toPrinter (R ps :^ _) = mconcat (intersperse newLine ps)
-- | Produces a formatter for a 'Doc'.
fromDoc :: Printer a => Doc -> Formatter a
fromDoc Empty = ppEmpty
fromDoc (Text txt) = ppText txt
fromDoc (Wrapped txt) = ppWrapped txt
fromDoc (Indent n d) = ppIndent n (fromDoc d)
fromDoc (Above Empty r) = fromDoc r
fromDoc (Above l Empty) = fromDoc l
fromDoc (Above l r) = ppAbove (fromDoc l) (fromDoc r)
fromDoc (Besides Empty r) = fromDoc r
fromDoc (Besides l Empty) = fromDoc l
fromDoc (Besides l r) = ppBesides (fromDoc l) (fromDoc r)
fromDoc (Split l r) = ppSplit (fromDoc l) (fromDoc r)
fromDoc (Join d) = ppJoin (fromDoc d)
fromDoc (Colour _ Empty) = ppEmpty
fromDoc (Colour c d) = ppColour c (fromDoc d)
-- | Produces a printer for the most efficient rendering of a document within
-- a specified amount of horizontal space.
-- Returns 'Nothing' if the document cannot be rendered within the given amount
-- of space.
render :: Printer a => Int -> Doc -> Maybe a
render wmax doc = listToMaybe $ fmap toPrinter $ spwn wmax
where
_ :< spwn = fromDoc doc
-------------------------------------------------------------------------------
-- Utilities
-------------------------------------------------------------------------------
-- | Breaks up a list into its 'init' and 'last' parts.
-- Undefined for empty lists.
unSnoc :: [a] -> ([a], a)
unSnoc = unsnc id
where
unsnc acc [x] = (acc [], x)
unsnc acc (x : xs) = unsnc (acc . (x :)) xs