--- * -*- outline-regexp:"--- \\*"; -*-
--- ** doc
-- In Emacs, use TAB on lines beginning with "-- *" to collapse/expand sections.
{-|

A reader for the "timedot" file format.
Example:

@
;DATE
;ACCT  DOTS  # Each dot represents 15m, spaces are ignored
;ACCT  8    # numbers with or without a following h represent hours
;ACCT  5m   # numbers followed by m represent minutes

; on 2/1, 1h was spent on FOSS haskell work, 0.25h on research, etc.
2/1
fos.haskell   .... ..
biz.research  .
inc.client1   .... .... .... .... .... ....

2/2
biz.research  .
inc.client1   .... .... ..

@

-}

--- ** language
{-# LANGUAGE OverloadedStrings #-}

--- ** exports
module Hledger.Read.TimedotReader (
  -- * Reader
  reader,
  -- * Misc other exports
  timedotfilep,
)
where

--- ** imports
import Control.Monad
import Control.Monad.Except (ExceptT, liftEither)
import Control.Monad.State.Strict
import Data.Char (isSpace)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time (Day)
import Text.Megaparsec hiding (parse)
import Text.Megaparsec.Char

import Hledger.Data
import Hledger.Read.Common hiding (emptyorcommentlinep)
import Hledger.Utils

--- ** doctest setup
-- $setup
-- >>> :set -XOverloadedStrings

--- ** reader

reader :: MonadIO m => Reader m
reader :: forall (m :: * -> *). MonadIO m => Reader m
reader = Reader
  {rFormat :: String
rFormat     = String
"timedot"
  ,rExtensions :: [String]
rExtensions = [String
"timedot"]
  ,rReadFn :: InputOpts -> String -> Text -> ExceptT String IO Journal
rReadFn     = InputOpts -> String -> Text -> ExceptT String IO Journal
parse
  ,rParser :: MonadIO m => ErroringJournalParser m Journal
rParser    = forall (m :: * -> *). JournalParser m Journal
timedotp
  }

-- | Parse and post-process a "Journal" from the timedot format, or give an error.
parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
parse :: InputOpts -> String -> Text -> ExceptT String IO Journal
parse InputOpts
iopts String
fp Text
t = ErroringJournalParser IO Journal
-> InputOpts -> String -> Text -> ExceptT String IO Journal
initialiseAndParseJournal forall (m :: * -> *). JournalParser m Journal
timedotp InputOpts
iopts String
fp Text
t
                   forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= forall e (m :: * -> *) a. MonadError e m => Either e a -> m a
liftEither forall b c a. (b -> c) -> (a -> b) -> a -> c
. [AccountAlias] -> Journal -> Either String Journal
journalApplyAliases (InputOpts -> [AccountAlias]
aliasesFromOpts InputOpts
iopts)
                   forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= InputOpts -> String -> Text -> Journal -> ExceptT String IO Journal
journalFinalise InputOpts
iopts String
fp Text
t

--- ** utilities

traceparse, traceparse' :: String -> TextParser m ()
traceparse :: forall (m :: * -> *). String -> TextParser m ()
traceparse  = forall a b. a -> b -> a
const forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *) a. Monad m => a -> m a
return ()
traceparse' :: forall (m :: * -> *). String -> TextParser m ()
traceparse' = forall a b. a -> b -> a
const forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *) a. Monad m => a -> m a
return ()
-- for debugging:
-- traceparse  s = traceParse (s++"?")
-- traceparse' s = trace s $ return ()

--- ** parsers
{-
Rough grammar for timedot format:

timedot:           preamble day*
preamble:          (emptyline | commentline | orgheading)*
orgheading:        orgheadingprefix restofline
day:               dateline entry* (emptyline | commentline)*
dateline:          orgheadingprefix? date description?
orgheadingprefix:  star+ space+
description:       restofline  ; till semicolon?
entry:          orgheadingprefix? space* singlespaced (doublespace quantity?)?
doublespace:       space space+
quantity:          (dot (dot | space)* | number | number unit)

Date lines and item lines can begin with an org heading prefix, which is ignored.
Org headings before the first date line are ignored, regardless of content.
-}

timedotfilep :: JournalParser m Journal
timedotfilep = forall (m :: * -> *). JournalParser m Journal
timedotp -- XXX rename export above

timedotp :: JournalParser m ParsedJournal
timedotp :: forall (m :: * -> *). JournalParser m Journal
timedotp = forall (m :: * -> *). JournalParser m ()
preamblep forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> forall (m :: * -> *) a. MonadPlus m => m a -> m [a]
many forall (m :: * -> *). JournalParser m ()
dayp forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> forall e s (m :: * -> *). MonadParsec e s m => m ()
eof forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> forall s (m :: * -> *). MonadState s m => m s
get

preamblep :: JournalParser m ()
preamblep :: forall (m :: * -> *). JournalParser m ()
preamblep = do
  forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *). String -> TextParser m ()
traceparse String
"preamblep"
  forall (m :: * -> *) a. MonadPlus m => m a -> m [a]
many forall a b. (a -> b) -> a -> b
$ forall e s (m :: * -> *) a. MonadParsec e s m => m a -> m ()
notFollowedBy forall (m :: * -> *). JournalParser m (Day, Text, Text, [Tag])
datelinep forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> (forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *). String -> TextParser m ()
emptyorcommentlinep String
"#;*")
  forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *). String -> TextParser m ()
traceparse' String
"preamblep"

-- | Parse timedot day entries to multi-posting time transactions for that day.
-- @
-- 2020/2/1 optional day description
-- fos.haskell  .... ..
-- biz.research .
-- inc.client1  .... .... .... .... .... ....
-- @
dayp :: JournalParser m ()
dayp :: forall (m :: * -> *). JournalParser m ()
dayp = forall e s (m :: * -> *) a.
MonadParsec e s m =>
String -> m a -> m a
label String
"timedot day entry" forall a b. (a -> b) -> a -> b
$ do
  forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *). String -> TextParser m ()
traceparse String
"dayp"
  SourcePos
pos <- forall s e (m :: * -> *).
(TraversableStream s, MonadParsec e s m) =>
m SourcePos
getSourcePos
  (Day
date,Text
desc,Text
comment,[Tag]
tags) <- forall (m :: * -> *). JournalParser m (Day, Text, Text, [Tag])
datelinep
  forall (m :: * -> *). JournalParser m ()
commentlinesp
  [Posting]
ps <- forall (m :: * -> *) a. MonadPlus m => m a -> m [a]
many forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *). JournalParser m Posting
timedotentryp forall (f :: * -> *) a b. Applicative f => f a -> f b -> f a
<* forall (m :: * -> *). JournalParser m ()
commentlinesp
  SourcePos
endpos <- forall s e (m :: * -> *).
(TraversableStream s, MonadParsec e s m) =>
m SourcePos
getSourcePos
  -- lift $ traceparse' "dayp end"
  let t :: Transaction
t = Transaction -> Transaction
txnTieKnot forall a b. (a -> b) -> a -> b
$ Transaction
nulltransaction{
    tsourcepos :: (SourcePos, SourcePos)
tsourcepos   = (SourcePos
pos, SourcePos
endpos),
    tdate :: Day
tdate        = Day
date,
    tstatus :: Status
tstatus      = Status
Cleared,
    tdescription :: Text
tdescription = Text
desc,
    tcomment :: Text
tcomment     = Text
comment,
    ttags :: [Tag]
ttags        = [Tag]
tags,
    tpostings :: [Posting]
tpostings    = [Posting]
ps
    }
  forall s (m :: * -> *). MonadState s m => (s -> s) -> m ()
modify' forall a b. (a -> b) -> a -> b
$ Transaction -> Journal -> Journal
addTransaction Transaction
t

datelinep :: JournalParser m (Day,Text,Text,[Tag])
datelinep :: forall (m :: * -> *). JournalParser m (Day, Text, Text, [Tag])
datelinep = do
  forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *). String -> TextParser m ()
traceparse String
"datelinep"
  forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall a b. (a -> b) -> a -> b
$ forall (f :: * -> *) a. Alternative f => f a -> f (Maybe a)
optional forall {m :: * -> *}. ParsecT HledgerParseErrorData Text m ()
orgheadingprefixp
  Day
date <- forall (m :: * -> *). JournalParser m Day
datep
  Text
desc <- Text -> Text
T.strip forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall (m :: * -> *). TextParser m Text
descriptionp
  (Text
comment, [Tag]
tags) <- forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall (m :: * -> *). TextParser m (Text, [Tag])
transactioncommentp
  -- lift $ traceparse' "datelinep end"
  forall (m :: * -> *) a. Monad m => a -> m a
return (Day
date, Text
desc, Text
comment, [Tag]
tags)

-- | Zero or more empty lines or hash/semicolon comment lines
-- or org headlines which do not start a new day.
commentlinesp :: JournalParser m ()
commentlinesp :: forall (m :: * -> *). JournalParser m ()
commentlinesp = do
  forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *). String -> TextParser m ()
traceparse String
"commentlinesp"
  forall (f :: * -> *) a. Functor f => f a -> f ()
void forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *) a. MonadPlus m => m a -> m [a]
many forall a b. (a -> b) -> a -> b
$ forall e s (m :: * -> *) a. MonadParsec e s m => m a -> m a
try forall a b. (a -> b) -> a -> b
$ forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *). String -> TextParser m ()
emptyorcommentlinep String
"#;"

-- orgnondatelinep :: JournalParser m ()
-- orgnondatelinep = do
--   lift $ traceparse "orgnondatelinep"
--   lift orgheadingprefixp
--   notFollowedBy datelinep
--   void $ lift restofline
--   lift $ traceparse' "orgnondatelinep"

orgheadingprefixp :: ParsecT HledgerParseErrorData Text m ()
orgheadingprefixp = do
  -- traceparse "orgheadingprefixp"
  forall (m :: * -> *) a. MonadPlus m => m a -> m ()
skipSome (forall e s (m :: * -> *).
(MonadParsec e s m, Token s ~ Char) =>
Token s -> m (Token s)
char Char
'*') forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> forall s (m :: * -> *).
(Stream s, Token s ~ Char) =>
ParsecT HledgerParseErrorData s m ()
skipNonNewlineSpaces1

-- | Parse a single timedot entry to one (dateless) transaction.
-- @
-- fos.haskell  .... ..
-- @
timedotentryp :: JournalParser m Posting
timedotentryp :: forall (m :: * -> *). JournalParser m Posting
timedotentryp = do
  forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *). String -> TextParser m ()
traceparse String
"timedotentryp"
  forall e s (m :: * -> *) a. MonadParsec e s m => m a -> m ()
notFollowedBy forall (m :: * -> *). JournalParser m (Day, Text, Text, [Tag])
datelinep
  forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall a b. (a -> b) -> a -> b
$ forall (f :: * -> *) a. Alternative f => f a -> f (Maybe a)
optional forall a b. (a -> b) -> a -> b
$ forall (f :: * -> *) (m :: * -> *) a.
(Foldable f, Alternative m) =>
f (m a) -> m a
choice [forall {m :: * -> *}. ParsecT HledgerParseErrorData Text m ()
orgheadingprefixp, forall s (m :: * -> *).
(Stream s, Token s ~ Char) =>
ParsecT HledgerParseErrorData s m ()
skipNonNewlineSpaces1]
  Text
a <- forall (m :: * -> *). JournalParser m Text
modifiedaccountnamep
  forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall s (m :: * -> *).
(Stream s, Token s ~ Char) =>
ParsecT HledgerParseErrorData s m ()
skipNonNewlineSpaces
  (Hours
hours, Text
comment, [Tag]
tags) <-
    forall e s (m :: * -> *) a. MonadParsec e s m => m a -> m a
try (do
      (Text
c,[Tag]
ts) <- forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall (m :: * -> *). TextParser m (Text, [Tag])
transactioncommentp  -- or postingp, but let's not bother supporting date:/date2:
      forall (m :: * -> *) a. Monad m => a -> m a
return (Hours
0, Text
c, [Tag]
ts)
    )
    forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> (do
      Hours
h <- forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall (m :: * -> *). TextParser m Hours
durationp
      (Text
c,[Tag]
ts) <- forall e s (m :: * -> *) a. MonadParsec e s m => m a -> m a
try (forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall (m :: * -> *). TextParser m (Text, [Tag])
transactioncommentp) forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> (forall e s (m :: * -> *).
(MonadParsec e s m, Token s ~ Char) =>
m (Token s)
newline forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> forall (m :: * -> *) a. Monad m => a -> m a
return (Text
"",[]))
      forall (m :: * -> *) a. Monad m => a -> m a
return (Hours
h,Text
c,[Tag]
ts)
    )
  Maybe (Text, AmountStyle)
mcs <- forall (m :: * -> *). JournalParser m (Maybe (Text, AmountStyle))
getDefaultCommodityAndStyle
  let 
    (Text
c,AmountStyle
s) = case Maybe (Text, AmountStyle)
mcs of
      Just (Text
defc,AmountStyle
defs) -> (Text
defc, AmountStyle
defs{asprecision :: AmountPrecision
asprecision=forall a. Ord a => a -> a -> a
max (AmountStyle -> AmountPrecision
asprecision AmountStyle
defs) (Word8 -> AmountPrecision
Precision Word8
2)})
      Maybe (Text, AmountStyle)
_ -> (Text
"", AmountStyle
amountstyle{asprecision :: AmountPrecision
asprecision=Word8 -> AmountPrecision
Precision Word8
2})
  -- lift $ traceparse' "timedotentryp end"
  forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$ Posting
nullposting{paccount :: Text
paccount=Text
a
                      ,pamount :: MixedAmount
pamount=Amount -> MixedAmount
mixedAmount forall a b. (a -> b) -> a -> b
$ Amount
nullamt{acommodity :: Text
acommodity=Text
c, aquantity :: Hours
aquantity=Hours
hours, astyle :: AmountStyle
astyle=AmountStyle
s}
                      ,ptype :: PostingType
ptype=PostingType
VirtualPosting
                      ,pcomment :: Text
pcomment=Text
comment
                      ,ptags :: [Tag]
ptags=[Tag]
tags
                      }

type Hours = Quantity

durationp :: TextParser m Hours
durationp :: forall (m :: * -> *). TextParser m Hours
durationp = do
  forall (m :: * -> *). String -> TextParser m ()
traceparse String
"durationp"
  forall e s (m :: * -> *) a. MonadParsec e s m => m a -> m a
try forall (m :: * -> *). TextParser m Hours
numericquantityp forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> forall (m :: * -> *). TextParser m Hours
dotquantityp
    -- <* traceparse' "durationp"

-- | Parse a duration of seconds, minutes, hours, days, weeks, months or years,
-- written as a decimal number followed by s, m, h, d, w, mo or y, assuming h
-- if there is no unit. Returns the duration as hours, assuming
-- 1m = 60s, 1h = 60m, 1d = 24h, 1w = 7d, 1mo = 30d, 1y=365d.
-- @
-- 1.5
-- 1.5h
-- 90m
-- @
numericquantityp :: TextParser m Hours
numericquantityp :: forall (m :: * -> *). TextParser m Hours
numericquantityp = do
  -- lift $ traceparse "numericquantityp"
  (Hours
q, Word8
_, Maybe Char
_, Maybe DigitGroupStyle
_) <- forall (m :: * -> *).
Maybe AmountStyle
-> TextParser m (Hours, Word8, Maybe Char, Maybe DigitGroupStyle)
numberp forall a. Maybe a
Nothing
  Maybe (Tokens Text)
msymbol <- forall (f :: * -> *) a. Alternative f => f a -> f (Maybe a)
optional forall a b. (a -> b) -> a -> b
$ forall (f :: * -> *) (m :: * -> *) a.
(Foldable f, Alternative m) =>
f (m a) -> m a
choice forall a b. (a -> b) -> a -> b
$ forall a b. (a -> b) -> [a] -> [b]
map (forall e s (m :: * -> *).
MonadParsec e s m =>
Tokens s -> m (Tokens s)
string forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. (a, b) -> a
fst) [(Tokens Text, Hours)]
timeUnits
  forall s (m :: * -> *).
(Stream s, Token s ~ Char) =>
ParsecT HledgerParseErrorData s m ()
skipNonNewlineSpaces
  let q' :: Hours
q' =
        case Maybe (Tokens Text)
msymbol of
          Maybe (Tokens Text)
Nothing  -> Hours
q
          Just Tokens Text
sym ->
            case forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup Tokens Text
sym [(Tokens Text, Hours)]
timeUnits of
              Just Hours
mult -> Hours
q forall a. Num a => a -> a -> a
* Hours
mult
              Maybe Hours
Nothing   -> Hours
q  -- shouldn't happen.. ignore
  forall (m :: * -> *) a. Monad m => a -> m a
return Hours
q'

-- (symbol, equivalent in hours).
timeUnits :: [(Tokens Text, Hours)]
timeUnits =
  [(Tokens Text
"s",Hours
2.777777777777778e-4)
  ,(Tokens Text
"mo",Hours
5040) -- before "m"
  ,(Tokens Text
"m",Hours
1.6666666666666666e-2)
  ,(Tokens Text
"h",Hours
1)
  ,(Tokens Text
"d",Hours
24)
  ,(Tokens Text
"w",Hours
168)
  ,(Tokens Text
"y",Hours
61320)
  ]

-- | Parse a quantity written as a line of dots, each representing 0.25.
-- @
-- .... ..
-- @
dotquantityp :: TextParser m Quantity
dotquantityp :: forall (m :: * -> *). TextParser m Hours
dotquantityp = do
  -- lift $ traceparse "dotquantityp"
  String
dots <- forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
notforall b c a. (b -> c) -> (a -> b) -> a -> c
.Char -> Bool
isSpace) forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> forall (m :: * -> *) a. MonadPlus m => m a -> m [a]
many (forall (f :: * -> *) e s (m :: * -> *).
(Foldable f, MonadParsec e s m) =>
f (Token s) -> m (Token s)
oneOf (String
". " :: [Char]))
  forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$ forall a b. (Integral a, Num b) => a -> b
fromIntegral (forall (t :: * -> *) a. Foldable t => t a -> Int
length String
dots) forall a. Fractional a => a -> a -> a
/ Hours
4

-- | XXX new comment line parser, move to Hledger.Read.Common.emptyorcommentlinep
-- Parse empty lines, all-blank lines, and lines beginning with any of the provided
-- comment-beginning characters.
emptyorcommentlinep :: [Char] -> TextParser m ()
emptyorcommentlinep :: forall (m :: * -> *). String -> TextParser m ()
emptyorcommentlinep String
cs =
  forall e s (m :: * -> *) a.
MonadParsec e s m =>
String -> m a -> m a
label (String
"empty line or comment line beginning with "forall a. [a] -> [a] -> [a]
++String
cs) forall a b. (a -> b) -> a -> b
$ do
    forall (m :: * -> *). String -> TextParser m ()
traceparse String
"emptyorcommentlinep" -- XXX possible to combine label and traceparse ?
    forall s (m :: * -> *).
(Stream s, Token s ~ Char) =>
ParsecT HledgerParseErrorData s m ()
skipNonNewlineSpaces
    forall (f :: * -> *) a. Functor f => f a -> f ()
void forall e s (m :: * -> *).
(MonadParsec e s m, Token s ~ Char) =>
m (Token s)
newline forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> forall (f :: * -> *) a. Functor f => f a -> f ()
void ParsecT HledgerParseErrorData Text m (Tokens Text)
commentp
    forall (m :: * -> *). String -> TextParser m ()
traceparse' String
"emptyorcommentlinep"
    where
      commentp :: ParsecT HledgerParseErrorData Text m (Tokens Text)
commentp = do
        forall (f :: * -> *) (m :: * -> *) a.
(Foldable f, Alternative m) =>
f (m a) -> m a
choice (forall a b. (a -> b) -> [a] -> [b]
map (forall (m :: * -> *) a. MonadPlus m => m a -> m [a]
someforall b c a. (b -> c) -> (a -> b) -> a -> c
.forall e s (m :: * -> *).
(MonadParsec e s m, Token s ~ Char) =>
Token s -> m (Token s)
char) String
cs)
        forall e s (m :: * -> *).
MonadParsec e s m =>
Maybe String -> (Token s -> Bool) -> m (Tokens s)
takeWhileP forall a. Maybe a
Nothing (forall a. Eq a => a -> a -> Bool
/=Char
'\n') forall (f :: * -> *) a b. Applicative f => f a -> f b -> f a
<* forall e s (m :: * -> *).
(MonadParsec e s m, Token s ~ Char) =>
m (Token s)
newline