packages feed

dao-0.1.0.1: src/Dao/PPrint.hs

-- "src/Dao/PPrintM.hs"  a pretty-printer designed especially for
-- printing Dao script code.
-- 
-- Copyright (C) 2008-2014  Ramin Honary.
-- This file is part of the Dao System.
--
-- The Dao System is free software: you can redistribute it and/or
-- modify it under the terms of the GNU General Public License as
-- published by the Free Software Foundation, either version 3 of the
-- License, or (at your option) any later version.
-- 
-- The Dao System is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
-- 
-- You should have received a copy of the GNU General Public License
-- along with this program (see the file called "LICENSE"). If not, see
-- <http://www.gnu.org/licenses/agpl.html>.

{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}

module Dao.PPrint where

import           Dao.String
import qualified Dao.Tree as T

import qualified Data.Map as M

import           Control.Monad.State

import           Data.List
import           Data.Char
import           Data.Monoid

----------------------------------------------------------------------------------------------------

-- | This is the function you will probably care about most: take a value of any data type that
-- instantiates 'PPrintable', and a maximum text-wrapping width value, and a tab string, and will
-- convert that value to a 'Prelude.String'.
prettyPrint :: PPrintable a => Int -> String -> a -> String
prettyPrint maxWidth tab = showPPrint maxWidth tab . pPrint

-- | Calls 'prettyPrint' with the default values @80@ for the text-wrapping width, and a tab string
-- consisting of four single-space characters (four ASCII @0x20@ characters).
prettyShow :: PPrintable a => a -> String
prettyShow = prettyPrint 80 "    "

----------------------------------------------------------------------------------------------------

-- | Remove trailing whitespace, I stole the idea from the Perl language.
chomp :: String -> String
chomp = foldl (\ out (spc, str) -> if null str then out else out++spc++str) "" . spcstr where
  spcstr cx = case cx of
    "" -> []
    cx -> (spc, str) : spcstr cx' where
      (spc, more) = span  isSpace cx
      (str, cx' ) = break isSpace more

-- | like 'Prelude.map', but doesn't touch the last item in the list.
mapAlmost :: (a -> a) -> [a] -> [a]
mapAlmost fn ax = case ax of
  [] -> []
  [a] -> [a]
  a:ax -> fn a : mapAlmost fn ax

-- | like 'Prelude.map', but doesn't touch the first item in the list.
mapTail :: (a -> a) -> [a] -> [a]
mapTail fn ax = case ax of
  []   -> []
  a:ax -> a : map fn ax

----------------------------------------------------------------------------------------------------

class PPrintable a where { pPrint :: a -> PPrintM () }
type PPrint = PPrintM ()

-- | Put a new line regardless of whether or not we are aleady on a new line.
pNewLine :: PPrint
pNewLine = modify $ \st ->
  st{ printerCol = 0
    , printerBuf = ""
    , printerOut = printerOut st ++ [printerOutputTripple st]
    , lineCount  = lineCount st + 1
    , printerTab = nextTab st
    }

-- | Like 'pNewLine' but also indicates that there *must* be a new line here (like after a comment)
-- to prevent lines from being joined.
pForceNewLine :: PPrint
pForceNewLine = modify (\st -> st{forcedNewLine=True})

-- | Place a new line unless we are already on a new line.
pEndLine :: PPrint
pEndLine = gets printerCol >>= \col ->
  if col==0 then modify (\st -> st{printerTab=nextTab st}) else pNewLine
    

pIndent :: PPrint -> PPrint
pIndent indentedPrinter = do
  tab <- gets nextTab
  modify (\st -> st{nextTab=tab+1})
  indentedPrinter
  modify (\st -> st{nextTab=tab})

instance PPrintable ()   where { pPrint = return }
instance PPrintable UStr where { pPrint = pUStr }
instance PPrintable Name where { pPrint = pUStr . toUStr }
instance PPrintable t => PPrintable (T.Tree Name t) where
  pPrint t = case t of
    T.Void            -> pString "tree"
    T.Leaf       o    -> leaf o
    T.Branch       ox -> pList (pString "tree") "{ " ", " " }" (branch ox)
    T.LeafBranch o ox -> pList (leaf o) " { " ", " " }" (branch ox)
    where
      leaf o = pWrapIndent [pString "tree(", pPrint o, pString ")"]
      branch = map (\ (lbl, o) -> pMapAssoc (lbl, o)) . M.assocs

instance PPrintable Base16String where { pPrint = pShow }
instance PPrintable Base64String where { pPrint = pShow }

pMapAssoc :: (PPrintable a, PPrintable o) => (a, o) -> PPrint
pMapAssoc (a, o) = pWrapIndent [pPrint a, pString " = ", pPrint o]

-- not for export
appendString :: Int -> String -> PPrint
appendString len str = modify $ \st ->
  st{ printerCol = printerCol st + len
    , printerBuf = printerBuf st ++ str
    , charCount  = charCount  st + len
    }

-- | Print a 'Dao.String.UStr' as a single line.
pUStr :: UStr -> PPrint
pUStr u = if nil==u then return () else appendString (ulength u) (uchars u)

-- | Print a 'Prelude.String' as a single line.
pString :: String -> PPrint
pString s = if null s then return () else appendString (length s) s

-- | Print any value that instantiates 'Prelude.Show'.
pShow :: Show a => a -> PPrint
pShow = pString . show

-- | Shortcut for @('pPrint' . 'Data.List.concat')@
pConcat :: [String] -> PPrint
pConcat = pString . concat

-- | Just keep printing items along the line without wrapping until a 'pNewLine' or 'pEndLine'
-- occurs. Actually, this function simply a synonym for 'Control.Monad.sequence_'.
pNoWrap :: [PPrint] -> PPrint
pNoWrap = sequence_

-- | Try to print with the given function, but if the printed text runs past the 'maxWidth', or if
-- the printed text results in multiple lines of output, end the current line of text before
-- placing the text from the given function.
pWrap :: PPrint -> PPrint
pWrap fn = do
  st <- get
  let trySt = execState fn (subprint st)
  if printerCol st + charCount trySt > maxWidth st then pEndLine else return ()
  appendState trySt

-- | Evaluate the 'PPrintM' printer, and every line of output will be used as an item in a list and
-- printed across a line, wrapping on to the next line if the line goes past the width limit.
pInline :: [PPrint] -> PPrint
pInline = sequence_ . map pWrap

-- | Like 'pInline' but if the line wraps, every line after the first will be indented.
pWrapIndent :: [PPrint] -> PPrint
pWrapIndent px = do
  st <- get
  let trySt = execState (pInline px) (subprint st)
  case printerOut trySt of
    []   -> appendState trySt
    p:px ->
      let ind (tab, len, str) = (tab+1, len, str)
      in  appendState (trySt{printerOut = p : map ind px, printerTab = printerTab trySt + 1})

-- | Will evaluate a 'PPrintM' function to create a block of text, and if the block of text can be
-- fit into a single line, it will be placed inline with the text precceding and succeeding it.
-- If it cannot be placed into a single line, it will be preceeded and succeeded by a 'pEndLine'.
-- Passing 'Prelude.False' as the first parameter means 'pEndLine' will not succeed the block of
-- text, which can come in handy (for example) when you need to follow an item with a closing
-- punctuation mark like a comma or semicolon, and you don't want that closing punctuation on the
-- next line.
pGroup :: Bool -> PPrint -> PPrint
pGroup after fn = do
  st <- get
  let trySt = execState (pEndLine >> fn) (subprint st)
  if charCount trySt > maxWidth st || forcedNewLine trySt
    then  pEndLine >> appendState trySt >> (if after then pEndLine else return ())
    else  appendState (stateJoinLines trySt)

pList :: PPrint -> String -> String -> String -> [PPrint] -> PPrint
pList header open separator close px = do
  let sep = ustr separator
  pGroup False $ do
    header >> pString open >> pEndLine
    pIndent $ pInline $ map (pGroup True) $ mapAlmost (>>(pUStr sep)) px
    pEndLine >> pString close

-- | Like 'pList' but there is no need to pass the first @'PPrintM' ()@ header parameter, this
-- parameter is set to @'Prelude.return' ()@.
pList_ :: String -> String -> String -> [PPrint] -> PPrint
pList_ = pList (return ())

pClosure :: PPrint -> String -> String -> [PPrint] -> PPrint
pClosure header open close px = do
  st <- get
  let content = do
        header >> pString open >> pEndLine
        pIndent (sequence_ $ mapAlmost (>>pEndLine) px)
        pEndLine >> pString close
      trySt = execState content (subprint st)
  if charCount trySt + printerCol st > maxWidth st then pEndLine else return ()
  appendState trySt

-- | A commonly used pattern, like 'pClosure' but the contents of it is always a list of items which
-- can be pretty-printed by the given @(o -> 'PPrintM' ())@ function.
pContainer :: String -> (o -> PPrint) -> [o] -> PPrint
pContainer label prin ox = pList (pString label) "{ " ", " " }" (map prin ox)

----------------------------------------------------------------------------------------------------

type PPrintM a = State Printer a
type POutput = (Int, Int, UStr)

-- not for export
data Printer
  = Printer
    { printerTab    :: Int -- how many indentation marks should preceed this line
    , printerCol    :: Int -- how many non-indentation characters are in the buffer
    , printerOut    :: [POutput] -- all lines before the current line in the buffer
    , printerBuf    :: String -- buffers the current line
    , nextTab       :: Int
    , lineCount     :: Int -- how many lines have been printed
    , charCount     :: Int -- how many characters have been printed
    , maxWidth      :: Int
    , forcedNewLine :: Bool
    }

initPrinter :: Int -> Printer
initPrinter width =
  Printer
  { printerTab    = 0
  , printerCol    = 0
  , printerOut    = []
  , printerBuf    = ""
  , maxWidth      = width
  , lineCount     = 0
  , charCount     = 0
  , nextTab       = 0
  , forcedNewLine = False
  }

printerOutputTripple :: Printer -> (Int, Int, UStr)
printerOutputTripple st = (printerTab st, printerCol st, ustr (printerBuf st))

instance Monoid Printer where
  mempty = initPrinter 80
  mappend origSt st = case printerOut st of
    []                ->
      (combine origSt st)
      { printerBuf = printerBuf origSt ++ printerBuf st
      , printerCol = printerCol origSt +  printerCol st
      }
    (_, col, buf):out ->
      (combine origSt st)
      { printerOut = printerOut origSt ++
          (printerTab origSt, printerCol origSt + col, ustr (printerBuf origSt ++ uchars buf)) : out
      , printerBuf = printerBuf st
      , printerCol = printerCol st
      }
    where
      combine origSt st = 
        origSt
        { charCount     = charCount origSt + charCount st
        , lineCount     = lineCount origSt + lineCount st
        , maxWidth      = maxWidth  origSt
        , printerTab    = printerTab st
        , nextTab       = nextTab st
        , forcedNewLine = forcedNewLine origSt || forcedNewLine st
        }

-- | Force a string into the 'printerBuf' buffer without modifying anything else. This should allow
-- you to put markers into the output without effecting any of the metrics used to control how the
-- output is indented or wrapped.
pDebug :: (Printer -> String) -> PPrint
pDebug fn = do
  st <- get
  let msg = "["++fn st++"]"
  put (st{printerBuf=printerBuf st ++ seq msg msg})

stateJoinLines :: Printer -> Printer
stateJoinLines st =
  st{printerBuf = str ++ printerBuf st, printerCol = len + printerCol st, printerOut=[]} where
    (len, str) = foldl joinln (0, "") (printerOut st)
    joinln (len0, str0) (_, len1, str1) = (len0+len1, str0 ++ uchars str1)

appendState :: Printer -> PPrint
appendState = modify . flip mappend

-- | A kind of pre-conversion, the 'PPrintState' is broken into a list of strings, each string
-- preceeded by it's indentation factor.
linesFromPPrintState :: Int -> PPrint -> [(Int, String)]
linesFromPPrintState maxWidth ps = end (execState ps (initPrinter maxWidth)) where
  end st = flip map (printerOut st ++ [printerOutputTripple st]) $ \ (a, _, b) ->
    (a, dropWhile isSpace (chomp (uchars b)))

printAcross :: [PPrint] -> PPrint
printAcross px = case px of
  []   -> return ()
  p:px -> do
    st <- get
    st <- return (st{printerBuf = printerBuf st})
    let trySt = execState p (subprint st)
    if withinMaxWidth st trySt
      then put (mappend st trySt)
      else pEndLine >> modify (\st -> mappend st trySt)
    printAcross px

withinMaxWidth :: Printer -> Printer -> Bool
withinMaxWidth st trySt = null (printerOut trySt) && printerCol st + printerCol trySt <= maxWidth st

subprint :: Printer -> Printer
subprint st = st{printerBuf="", printerCol=0, printerOut=[], charCount=0, lineCount=0}

tabAll :: Bool -> [POutput] -> [POutput]
tabAll alsoTabFinalLine ax = case ax of
  []                 -> []
  [(tab, len, str)]  -> if alsoTabFinalLine then [(tab+1, len, str)] else [(tab, len, str)]
  (tab, len, str):ax -> (tab+1, len, str) : tabAll alsoTabFinalLine ax

-- | Given a list of strings, each prefixed with an indentation count, and an indentation string,
-- concatenate all strings into a one big string, with each string being indented and on it's own
-- line.
linesToString :: String -> [(Int, String)] -> String
linesToString indentStr = intercalate "\n" .
  map (\ (indentCount, content) -> concat (replicate indentCount indentStr) ++ content)

-- Given an indentation string and a maximum width value, construct a string from the 'PPrintState'.
-- The maximum width value is used to call 'linesFromPPrintState', and the indentation string is
-- used to call 'linesToString'.
showPPrint :: Int -> String -> PPrint -> String
showPPrint maxWidth indentStr ps = linesToString indentStr (linesFromPPrintState maxWidth ps)

----------------------------------------------------------------------------------------------------

-- | Statements like "if" or "while" take a condition, and the Dao languages does not require these
-- conditions be enclosed in parenthases. The question is, should there be a space after the "if" or
-- "while" statement? This function resolves that question by checking if an object expression
-- already is enclosed in parentheses, and if so, does not put a space. Otherwise, a space will be
-- printed between the "if" tag or "while" tag and the condition.
class PrecedeWithSpace a where { precedeWithSpace :: a -> Bool }
instance PrecedeWithSpace Name where { precedeWithSpace _ = True }