Ajout : Makefile
[comptalang.git] / lib / Hcompta / Format / Ledger / Write.hs
index 82c6226f6b9e2934e2fc5c42037e054a3848cb46..f1fa742c5ad1779249d3f94844fc696d045b0e4d 100644 (file)
@@ -1,24 +1,28 @@
-{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE NamedFieldPuns #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 module Hcompta.Format.Ledger.Write where
 
+import           Control.Applicative ((<$>), (<*))
 import           Control.Arrow ((***))
 import           Data.Decimal (DecimalRaw(..))
 import qualified Data.Char (isSpace)
 import           Data.Fixed (showFixed)
+import qualified Data.Foldable
 import qualified Data.List
+import qualified Data.List.NonEmpty
 import qualified Data.Map.Strict as Data.Map
 import           Data.Maybe (fromMaybe)
 import qualified Data.Text.Lazy as TL
 import qualified Data.Text as Text
-import           Data.Text (Text)
 import qualified Data.Time.Calendar  as Time (toGregorian)
 import qualified Data.Time.LocalTime as Time (LocalTime(..), TimeOfDay(..), TimeZone(..), timeZoneOffsetString, ZonedTime(..))
-import qualified Text.PrettyPrint.Leijen.Text as P
-import           Text.PrettyPrint.Leijen.Text (Doc, (<>))
+import qualified Hcompta.Lib.Leijen as W
+import           Hcompta.Lib.Leijen (Doc, (<>))
 import           System.IO (Handle)
+import qualified Text.Parsec as R
+import           Text.Parsec (Stream, ParsecT)
 
 import qualified Hcompta.Model.Account as Account
 import           Hcompta.Model.Account (Account)
@@ -42,34 +46,8 @@ import           Hcompta.Model.Journal (Journal)
 import           Hcompta.Model.Date (Date)
 -- import           Hcompta.Format.Ledger.Journal as Journal
 import qualified Hcompta.Format.Ledger.Read as Read
+import qualified Hcompta.Lib.Parsec as R
 
--- * Utilities
-
--- ** Rendering
-
-show :: Doc -> TL.Text
-show = P.displayT . P.renderPretty 1.0 maxBound
-
-showIO :: Handle -> Doc -> IO ()
-showIO handle = P.displayIO handle . P.renderPretty 1.0 maxBound
-
--- ** Combinators
-
--- | Return a 'Doc' from a strict 'Text'
-text :: Text -> Doc
-text = P.text . TL.fromStrict
-
--- | Return a 'Doc' concatenating converted values of a 'Map'
---   separated by a given 'Doc'
-map_concat
- :: Doc -> (a -> Doc)
- -> Data.Map.Map k a -> Doc
-map_concat sep f =
-       snd . Data.Map.foldl
-        (\(first, doc) x -> case first of
-               True  -> (False, f x)
-               False -> (False, doc <> sep <> f x))
-        (True, P.empty) -- NOTE: public API gives no way to test for P.empty
 
 -- * Printing 'Account'
 
@@ -78,31 +56,32 @@ account type_ =
        case type_ of
         Posting.Type_Regular -> account_
         Posting.Type_Virtual -> \acct ->
-               P.char Read.posting_type_virtual_begin <> do
+               W.char Read.posting_type_virtual_begin <> do
                account_ acct <> do
-               P.char Read.posting_type_virtual_end
+               W.char Read.posting_type_virtual_end
         Posting.Type_Virtual_Balanced -> \acct ->
-               P.char Read.posting_type_virtual_balanced_begin <> do
+               W.char Read.posting_type_virtual_balanced_begin <> do
                account_ acct <> do
-               P.char Read.posting_type_virtual_balanced_end
+               W.char Read.posting_type_virtual_balanced_end
        where
                account_ :: Account -> Doc
                account_ acct =
-                       P.align $ P.hcat $
-                               Data.List.intersperse
-                                (P.char Read.account_name_sep)
-                                (Data.List.map account_name acct)
+                       W.align $ W.hcat $
+                               Data.List.NonEmpty.toList $
+                               Data.List.NonEmpty.intersperse
+                                (W.bold $ W.yellow $ W.char Read.account_name_sep)
+                                (Data.List.NonEmpty.map account_name acct)
 
 account_name :: Account.Name -> Doc
-account_name = text
+account_name = W.strict_text
 
 -- ** Mesuring 'Account'
 
 account_length :: Posting.Type -> Account -> Int
 account_length type_ acct =
-       Data.List.foldl
+       Data.Foldable.foldl
         (\acc -> (1 +) . (acc +) . Text.length)
-        (if acct == [] then 0 else (- 1)) acct +
+        (- 1) acct +
        case type_ of
         Posting.Type_Regular -> 0
         Posting.Type_Virtual -> 2
@@ -122,20 +101,20 @@ amount Amount.Amount
        case unit_side of
         Just Style.Side_Left ->
                (unit unit_)
-               <> (case unit_spaced of { Just True | unit_ /= "" -> P.space; _ -> P.empty })
-        _ -> P.empty
+               <> (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
+        _ -> W.empty
        <> quantity style qty
        <> case unit_side of
         (Just Style.Side_Right) ->
-               (case unit_spaced of { Just True | unit_ /= "" -> P.space; _ -> P.empty })
+               (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
                <> unit unit_
         Nothing ->
-               (case unit_spaced of { Just True | unit_ /= "" -> P.space; _ -> P.empty })
+               (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
                <> unit unit_
-        _ -> P.empty
+        _ -> W.empty
 
 unit :: Unit -> Doc
-unit = text
+unit = W.yellow . W.strict_text
 
 quantity :: Style -> Quantity -> Doc
 quantity Style.Style
@@ -146,9 +125,9 @@ quantity Style.Style
  } qty = do
        let Decimal e n = Quantity.round precision qty
        let num = Prelude.show $ abs $ n
-       let sign = text (if n < 0 then "-" else "")
+       let sign = W.bold $ W.yellow $ W.strict_text (if n < 0 then "-" else "")
        case e == 0 || precision == 0 of
-        True  -> sign <> (text $ Text.pack num)
+        True  -> sign <> do W.bold $ W.blue $ (W.strict_text $ Text.pack num)
         False -> do
                let num_len = length num
                let padded =
@@ -164,16 +143,17 @@ quantity Style.Style
                        del_grouping_sep grouping_fractional $
                        ['.', ',']
                sign <> do
-               P.text (TL.pack $ maybe id (\g -> reverse . group g . reverse) grouping_integral $ int) <> do
-               P.char (fromMaybe default_fractioning fractioning) <> do
-               P.text (TL.pack $ maybe id group grouping_fractional frac)
+               W.bold $ W.blue $ do
+                       W.text (TL.pack $ maybe id (\g -> reverse . group g . reverse) grouping_integral $ int) <> do
+                       (W.yellow $ W.char (fromMaybe default_fractioning fractioning)) <> do
+                       W.text (TL.pack $ maybe id group grouping_fractional frac)
        where
                group :: Style.Grouping -> [Char] -> [Char]
                group (Style.Grouping sep sizes_) =
                        Data.List.concat . reverse .
                        Data.List.map reverse . fst .
                        Data.List.foldl
-                        (flip (\digit -> \case
+                        (flip (\digit -> \x -> case x of
                                 ([], sizes) -> ([[digit]], sizes)
                                 (digits:groups, []) -> ((digit:digits):groups, [])
                                 (digits:groups, curr_sizes@(size:sizes)) ->
@@ -208,7 +188,7 @@ amounts_length amts =
        else
                Data.Map.foldr
                 (\n -> (3 +) . (+) (amount_length n))
-                0 amts
+                (-3) amts
 
 quantity_length :: Style -> Quantity -> Int
 quantity_length Style.Style
@@ -240,7 +220,7 @@ quantity_length Style.Style
                        where
                                loop :: Int -> Int -> [Int] -> Int
                                loop pad len =
-                                       \case
+                                       \x -> case x of
                                         [] -> 0
                                         sizes@[size] ->
                                                let l = len - size in
@@ -258,47 +238,87 @@ date (Time.ZonedTime
  (Time.LocalTime day tod)
  tz@(Time.TimeZone tz_min _ tz_name)) = do
        let (y, mo, d) = Time.toGregorian day
-       (if y == 0 then P.empty else P.integer y <> P.char '/') <> do
+       (if y == 0 then W.empty else W.integer y <> sep '/') <> do
        int2 mo <> do
-       P.char '/' <> int2 d <> do
+       sep '/' <> int2 d <> do
        (case tod of
-        Time.TimeOfDay 0 0 0 -> P.empty
+        Time.TimeOfDay 0 0 0 -> W.empty
         Time.TimeOfDay h m s ->
-               P.space <> int2 h <> do
-               P.char ':' <> int2 m <> do
+               W.space <> int2 h <> do
+               sep ':' <> int2 m <> do
                (case s of
-                0 -> P.empty
-                _ -> P.char ':' <> do
-                       (if s < 10 then P.char '0' else P.empty) <> do
-                       text $ Text.pack $ showFixed True s)) <> do
+                0 -> W.empty
+                _ -> sep ':' <> do
+                       (if s < 10 then W.char '0' else W.empty) <> do
+                       W.strict_text $ Text.pack $ showFixed True s)) <> do
        (case tz_min of
-        0 -> P.empty
-        _ | tz_name /= "" -> P.space <> do text $ Text.pack tz_name
-        _ -> P.space <> do text $ Text.pack $ Time.timeZoneOffsetString tz)
+        0 -> W.empty
+        _ | tz_name /= "" -> W.space <> do W.strict_text $ Text.pack tz_name
+        _ -> W.space <> do W.strict_text $ Text.pack $ Time.timeZoneOffsetString tz)
        where
                int2 :: Int -> Doc
-               int2 i = if i < 10 then P.char '0' <> P.int i else P.int i
+               int2 i = if i < 10 then W.char '0' <> W.int i else W.int i
+               sep :: Char -> Doc
+               sep = W.bold . W.dullblack . W.char
 
 -- * Printing 'Comment'
 
 comment :: Comment -> Doc
 comment com =
-       P.char Read.comment_begin
-       <> (case Text.uncons com of
-        Just (c, _) | not $ Data.Char.isSpace c -> P.space
-        _ -> P.empty)
-       <> text com
+       W.cyan $ do
+               W.char Read.comment_begin
+               <> (case Text.uncons com of
+                Just (c, _) | not $ Data.Char.isSpace c -> W.space
+                _ -> W.empty)
+               <> do W.if_color colorize (W.strict_text com)
+       where
+               colorize :: Doc
+               colorize =
+                       case R.runParser (do
+                               pre <- R.many $ R.try $ do
+                                       ns <- R.many $ R.satisfy (\c -> c /= Read.tag_value_sep && not (Data.Char.isSpace c))
+                                       sh <- R.space_horizontal
+                                       return (ns ++ [sh])
+                               ((W.text $ TL.pack $ concat pre) <>) <$> tags <* R.eof)
+                        () "" com of
+                        Left _ -> W.strict_text com
+                        Right doc -> doc
+               tags :: Stream s m Char => ParsecT s u m Doc
+               tags = do
+                       x <- tag_
+                       xs <- W.hcat <$> R.many (R.try (tag_sep >>= (\s -> (s <>) <$> tag_)))
+                       return $ x <> xs
+                       where
+                               tag_sep :: Stream s m Char => ParsecT s u m Doc
+                               tag_sep = do
+                                       s <- R.char Read.tag_sep
+                                       sh <- R.many R.space_horizontal
+                                       return $
+                                               do W.bold $ W.dullblack $ W.char s
+                                               <> do W.text $ TL.pack sh
+                               tag_ :: Stream s m Char => ParsecT s u m Doc
+                               tag_ = do
+                                       n <- Read.tag_name
+                                       s <- R.char Read.tag_value_sep
+                                       v <- Read.tag_value
+                                       return $
+                                               (W.yellow $ W.strict_text n)
+                                               <> (W.bold $ W.dullblack $ W.char s)
+                                               <> (W.red $ W.strict_text v)
 
 comments :: Doc -> [Comment] -> Doc
 comments prefix =
-       P.align . P.hcat .
-       Data.List.intersperse P.line .
+       W.hcat .
+       Data.List.intersperse W.line .
        Data.List.map (\c -> prefix <> comment c)
 
 -- * Printing 'Tag'
 
 tag :: Tag -> Doc
-tag (n, v) = text n <> P.char Read.tag_value_sep <> text v
+tag (n, v) =
+       (W.dullyellow $ W.strict_text n)
+       <> W.char Read.tag_value_sep
+       <> (W.dullred $ W.strict_text v)
 
 -- * Printing 'Posting'
 
@@ -316,30 +336,28 @@ posting
  , Posting.status=status_
  -- , Posting.tags
  } =
-       P.char '\t' <> do
-       P.align $ do
-               status status_ <> do
-               (case Data.Map.null amounts of
+       W.char '\t' <> do
+       status status_ <> do
+               case Data.Map.null amounts of
                 True -> account type_ acct
                 False ->
-                       P.fill (max_account_length + 2)
+                       W.fill (max_account_length + 2)
                         (account type_ acct) <> do
-                       P.fill (max 0 (max_amount_length - amounts_length amounts)) P.empty <> do
-                        -- NOTE: AFAICS Text.PrettyPrint.Leijen gives no way
-                        -- to get the column size of a Doc
-                        -- before printing it, hence the call to amounts_length here again.
-                       map_concat
-                        (P.space <> P.char Read.amount_sep <> P.space)
-                        amount amounts)
+                       W.fill (max 0
+                        ( max_amount_length
+                        - (fromIntegral $ amounts_length amounts) )) W.empty <> do
+                       W.intercalate
+                        (W.space <> (W.bold $ W.yellow $ W.char Read.amount_sep) <> W.space)
+                        amount amounts
        <> (case cmts of
-        [] -> P.empty
-        [c] -> P.space <> comment c
-        _ -> P.line <> do comments (P.text "\t\t") cmts)
+        [] -> W.empty
+        [c] -> W.space <> comment c
+        _ -> W.line <> do comments (W.text "\t\t") cmts)
 
 status :: Transaction.Status -> Doc
-status = \case
-        True  -> P.char '!'
-        False -> P.empty
+status = \x -> case x of
+        True  -> W.char '!'
+        False -> W.empty
 
 -- ** Mesuring 'Posting'
 
@@ -377,28 +395,28 @@ transaction_with_lengths
  -- , Transaction.tags
  } = do
        (case comments_before of
-        [] -> P.empty
-        _  -> comments (P.text "\t") comments_before <> P.line) <> do
-       (P.hcat $
+        [] -> W.empty
+        _  -> comments (W.text "\t") comments_before <> W.line) <> do
+       (W.hcat $
                Data.List.intersperse
-                (P.char Read.date_sep)
+                (W.char Read.date_sep)
                 (Data.List.map date (first_date:dates))) <> do
        (case status_ of
-        True -> P.space <> status status_
-        False -> P.empty) <> do
+        True -> W.space <> status status_
+        False -> W.empty) <> do
        code code_ <> do
        (case description of
-        "" -> P.empty
-        _  -> P.space <> text description) <> do
-       P.line <> do
+        "" -> W.empty
+        _  -> W.space <> (W.dullmagenta $ W.strict_text description)) <> do
+       W.line <> do
        (case comments_after of
-        [] -> P.empty
-        _  -> comments (P.text "\t") comments_after <> P.line) <> do
-       P.vsep $ Data.List.map
+        [] -> W.empty
+        _  -> comments (W.text "\t") comments_after <> W.line) <> do
+       W.vsep $ Data.List.map
         (\(type_, ps) ->
-               map_concat P.line
-                (map_concat P.line
-                        (P.vsep . Data.List.map
+               W.intercalate W.line
+                (W.intercalate W.line
+                        (W.vsep . Data.List.map
                                 (posting posting_lengths_ type_)))
                 (Posting.by_signs_and_account ps))
         [ (Posting.Type_Regular, postings)
@@ -407,9 +425,9 @@ transaction_with_lengths
         ]
 
 code :: Transaction.Code -> Doc
-code = \case
-        "" -> P.empty
-        t  -> P.space <> P.char '(' <> text t <> P.char ')'
+code = \x -> case x of
+        "" -> W.empty
+        t  -> W.space <> W.char '(' <> W.strict_text t <> W.char ')'
 
 -- ** Mesuring 'Transaction'
 
@@ -444,11 +462,18 @@ journal Journal.Journal
                 (Data.List.foldl (flip transaction_lengths))
                 nil_Transaction_Lengths
                 transactions
-       snd $ Data.Map.foldl
-        (Data.List.foldl (\(first, doc) t ->
-               ( False
-               , (if first then P.empty else doc <> P.line)
-                       <> transaction_with_lengths transaction_lengths_ t <> P.line
-               )))
-        (True, P.empty)
+       Data.Map.foldl
+        (Data.List.foldl (\doc t ->
+               (if W.is_empty doc then W.empty else doc <> W.line)
+                       <> transaction_with_lengths transaction_lengths_ t <> W.line
+               ))
+        W.empty
         transactions
+
+-- * Rendering
+
+show :: Bool -> Doc -> TL.Text
+show with_color = W.displayT . W.renderPretty with_color 1.0 maxBound
+
+put :: Bool -> Handle -> Doc -> IO ()
+put with_color handle = W.displayIO handle . W.renderPretty with_color 1.0 maxBound