packages feed

nestedtext-0.1.3: src/Data/NestedText/Parse.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeOperators #-}

module Data.NestedText.Parse
  ( ParseError(..)
  , parse
  , parse'
  , parseDocument
  ) where

import Data.NestedText.To (ToItem(..), ToKey(..))
import Data.NestedText.Type
import Data.NestedText.Util

import Control.Monad (unless, when)
import Control.Monad.Trans.Class (MonadTrans(..))
import qualified Control.Monad.Trans.State.Strict as StateT
import qualified Data.Char as C
import qualified Data.Map.Strict as M
import qualified Data.Set as S
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Vector as V
import Data.Void
import Generic.Data
import qualified Pipes as P
import qualified Pipes.Parse as PP
import qualified Pipes.Prelude as P
import qualified Pipes.Text as PT
import Prelude hiding (readList)

data Line
  = Line'Blank
  | Line'Comment Int T.Text
  | Line'StringItem Int T.Text
  | Line'ListItem Int T.Text
  | Line'DictItem Int T.Text (Maybe T.Text)
  | Line'KeyItem Int T.Text
  | Line'InlineList Int T.Text
  | Line'InlineDict Int T.Text
  deriving (Generic, Eq, Ord, Show)

data ValidLine
  = ValidLine'StringItem Int T.Text
  | ValidLine'ListItem Int T.Text
  | ValidLine'DictItem Int T.Text (Maybe T.Text)
  | ValidLine'KeyItem Int T.Text
  | ValidLine'InlineList Int T.Text
  | ValidLine'InlineDict Int T.Text
  deriving (Generic, Eq, Ord, Show)

data ParseError
  = ParseError'BreakingLinesError
  | ParseError'UnrecognizedLine
  | ParseError'InvalidLine
  | ParseError'InvalidIndent
  | ParseError'InvalidChar
  | ParseError'InvalidEndOfLine
  | ParseError'RemainingInlineContent
  | ParseError'DuplicateKey
  | ParseError'RemainingContent
  | ParseError'Empty
  deriving (Generic, Eq, Ord, Show)

toLine :: T.Text -> Either ParseError Line
toLine ts =
  let ts0 = T.unpack ts
      countPrefixSpaces = go 0
       where
        go !n (' ':xs) = go (succ n) xs
        go !n _ = n
      indentLevel = countPrefixSpaces ts0
      spanByFirstColon = go []
       where
        go _ [] = Nothing
        go revks (':':' ':vs) = Just
          ( reverse $ dropWhile isTabOrSpace revks
          , Just vs
          )
        go revks [':'] = Just
          ( reverse $ dropWhile isTabOrSpace revks
          , Nothing
          )
        go revks (x:xs) = go (x:revks) xs
   in case drop indentLevel ts0 of
        [] -> Right Line'Blank
        '#':' ':cs1 -> Right $ Line'Comment indentLevel $ T.pack cs1
        '#':cs1 -> Right $ Line'Comment indentLevel $ T.pack cs1
        ['>'] -> Right $ Line'StringItem indentLevel T.empty
        '>':' ':cs1 -> Right $ Line'StringItem indentLevel $ T.pack cs1
        ['-'] -> Right $ Line'ListItem indentLevel T.empty
        '-':' ':cs1 -> Right $ Line'ListItem indentLevel $ T.pack cs1
        '[':_ -> Right $ Line'InlineList indentLevel ts
        '{':_ -> Right $ Line'InlineDict indentLevel ts
        [':'] -> Right $ Line'KeyItem indentLevel T.empty
        ':':' ':cs1 -> Right $ Line'KeyItem indentLevel $ T.pack cs1
        '\t':_ -> Left ParseError'InvalidIndent
        cs0 -> case spanByFirstColon cs0 of
          Just (k, mv) -> Right $ Line'DictItem indentLevel (T.pack k) (T.pack <$> mv)
          Nothing -> Left ParseError'UnrecognizedLine

toValidLine :: Line -> Maybe ValidLine
toValidLine (Line'StringItem i ts) = Just $ ValidLine'StringItem i ts
toValidLine (Line'ListItem i ts) = Just $ ValidLine'ListItem i ts
toValidLine (Line'DictItem i ts mts) = Just $ ValidLine'DictItem i ts mts
toValidLine (Line'KeyItem i ts) = Just $ ValidLine'KeyItem i ts
toValidLine (Line'InlineList i ts) = Just $ ValidLine'InlineList i ts
toValidLine (Line'InlineDict i ts) = Just $ ValidLine'InlineDict i ts
toValidLine _ = Nothing

validLine'Indent :: ValidLine -> Int
validLine'Indent (ValidLine'StringItem i _) = i
validLine'Indent (ValidLine'ListItem i _) = i
validLine'Indent (ValidLine'DictItem i _ _) = i
validLine'Indent (ValidLine'KeyItem i _) = i
validLine'Indent (ValidLine'InlineList i _) = i
validLine'Indent (ValidLine'InlineDict i _) = i

data WaitingCharList
  = WaitingCharList'ValueOrEnd
  | WaitingCharList'Value
  | WaitingCharList'CommaOrEnd
  deriving (Generic, Eq, Ord, Show)

data WaitingCharDict
  = WaitingCharDict'KeyOrEnd
  | WaitingCharDict'Key
  | WaitingCharDict'Colon Key
  | WaitingCharDict'Value Key
  | WaitingCharDict'CommaOrEnd
  deriving (Generic, Eq, Ord, Show)

parse :: TL.Text -> Either ParseError Item
parse ts0 = StateT.evalStateT parser
  $ PT.folds (<>) T.empty id (splitLines $ PT.fromLazy ts0)
  P.>-> P.mapM toLine
  P.>-> P.mapMaybe toValidLine
 where
  parser = do
    xi <- readItemIndent [EQ] 0
    b <- PP.isEndOfInput
    unless b $ lift $ Left ParseError'RemainingContent
    return xi
  toItemWithoutError :: (ToItem a, ToItemError a ~ Void) => a -> Item
  toItemWithoutError x = case toItem x of
    Right y -> y
    Left _ -> error "toItemWithoutError: unreachable Left"
  toKeyWithoutError :: (ToKey a, ToKeyError a ~ Void) => a -> Key
  toKeyWithoutError x = case toKey x of
    Right y -> y
    Left _ -> error "toKeyWithoutError: unreachable Left"
  removeEnclosingSpace = T.dropWhileEnd C.isSpace . T.dropWhile C.isSpace

  readItemIndent cs i = do
    (item, j) <- readItem
    unless ((i `compare` j) `elem` cs) $ lift $ Left ParseError'InvalidIndent
    return item
  readItem = do
    ml <- PP.peek
    case ml of
      Nothing -> lift $ Left ParseError'Empty
      Just l -> case l of
        ValidLine'StringItem i _ts -> do
          ts <- readString i
          return (toItemWithoutError ts, i)
        ValidLine'ListItem i _ts -> do
          vs <- readList i
          return (Item'List vs, i)
        ValidLine'DictItem i _kts _mvts -> do
          dic <- readDict i
          return (Item'Dictionary dic, i)
        ValidLine'KeyItem i _ts -> do
          dic <- readDict i
          return (Item'Dictionary dic, i)
        ValidLine'InlineList i ts -> do
          _ <- PP.draw
          vs <- parseInlineList ts
          return (Item'List vs, i)
        ValidLine'InlineDict i ts -> do
          _ <- PP.draw
          dic <- parseInlineDict ts
          return (Item'Dictionary dic, i)
  readString i = go []
   where
    go ys = do
      ml <- PP.draw
      case ml of
        Nothing -> return $ T.intercalate osNewline $ reverse ys
        Just x -> case i `compare` validLine'Indent x of
          EQ -> case x of
            ValidLine'StringItem _ ts -> go $ ts : ys
            _ -> lift $ Left ParseError'InvalidLine
          LT -> lift $ Left ParseError'InvalidIndent
          GT -> do
            PP.unDraw x
            return $ T.intercalate osNewline $ reverse ys
  readList i = go []
   where
    go ys = do
      ml <- PP.draw
      case ml of
        Nothing -> return $ V.fromList $ reverse ys
        Just x -> case i `compare` validLine'Indent x of
          EQ -> case x of
            ValidLine'ListItem _ ts -> do
              let yys = toItemWithoutError ts : ys
              ml2 <- PP.peek
              case ml2 of
                Nothing -> go yys
                Just x2 -> case i `compare` validLine'Indent x2 of
                  EQ -> go yys
                  LT -> if not $ T.null ts then go yys else do
                    item <- readItemIndent [LT] i
                    go $ item : ys
                  GT -> return $ V.fromList $ reverse yys
            _ -> lift $ Left ParseError'InvalidLine
          LT -> lift $ Left ParseError'InvalidIndent
          GT -> do
            PP.unDraw x
            return $ V.fromList $ reverse ys
  readDict i = go M.empty
   where
    go m0 = do
      ml <- PP.draw
      case ml of
        Nothing -> return m0
        Just x -> case i `compare` validLine'Indent x of
          EQ -> case x of
            ValidLine'DictItem _ k0 mv ->
              let k = toKeyWithoutError k0 in case mv of
                Just v -> do
                  when (M.member k m0) $ lift $ Left ParseError'DuplicateKey
                  go $ M.insert k (toItemWithoutError v) m0
                Nothing -> do
                  ml2 <- PP.peek
                  case ml2 of
                    Nothing -> do
                      when (M.member k m0) $ lift $ Left ParseError'DuplicateKey
                      go $ M.insert k (toItemWithoutError T.empty) m0
                    Just x2 -> case i `compare` validLine'Indent x2 of
                      EQ -> do
                        when (M.member k m0) $ lift $ Left ParseError'DuplicateKey
                        go $ M.insert k (toItemWithoutError T.empty) m0
                      LT -> do
                        vi <- readItemIndent [LT] i
                        when (M.member k m0) $ lift $ Left ParseError'DuplicateKey
                        go $ M.insert k vi m0
                      GT -> do
                        when (M.member k m0) $ lift $ Left ParseError'DuplicateKey
                        return $ M.insert k (toItemWithoutError T.empty) m0
            ValidLine'KeyItem _ _ts -> do
              PP.unDraw x
              k <- readKey i
              vi <- readItemIndent [LT] i
              when (M.member k m0) $ lift $ Left ParseError'DuplicateKey
              go $ M.insert k vi m0
            _ -> lift $ Left ParseError'InvalidLine
          LT -> lift $ Left ParseError'InvalidIndent
          GT -> do
            PP.unDraw x
            return m0
  readKey i = go []
   where
    go ys = do
      ml <- PP.draw
      case ml of
        Nothing -> return
          $ toKeyWithoutError $ T.intercalate osNewline $ reverse ys
        Just x -> case i `compare` validLine'Indent x of
          EQ -> case x of
            ValidLine'KeyItem _ ts -> go $ ts : ys
            _ -> lift $ Left ParseError'InvalidLine
          _ -> do
            PP.unDraw x
            return $ toKeyWithoutError $ T.intercalate osNewline $ reverse ys

  parseInlineList = lift . StateT.evalStateT go . mapM_ P.yield . T.unpack
   where
    go = do
      vs <- readInlineList0
      readInlineSpace
      b <- PP.isEndOfInput
      unless b $ lift $ Left ParseError'RemainingInlineContent
      return vs
  parseInlineDict = lift . StateT.evalStateT go . mapM_ P.yield . T.unpack
   where
    go = do
      dic <- readInlineDict0
      readInlineSpace
      b <- PP.isEndOfInput
      unless b $ lift $ Left ParseError'RemainingInlineContent
      return dic
  readInlineChar x = do
    mc <- PP.draw
    case mc of
      Nothing -> lift $ Left ParseError'InvalidEndOfLine
      Just c | c == x -> return ()
      Just _ -> lift $ Left ParseError'InvalidChar
  readInlineSpace = do
    mc <- PP.draw
    case mc of
      Nothing -> return ()
      Just c | isTabOrSpace c -> readInlineSpace
      Just c -> do
        PP.unDraw c
        return ()
  readInlineString scs = go []
   where
    go ys = do
      mc <- PP.draw
      case mc of
        Nothing -> return $ removeEnclosingSpace $ T.pack $ reverse ys
        Just c -> if S.notMember c scs then go $ c : ys else do
          PP.unDraw c
          return $ removeEnclosingSpace $ T.pack $ reverse ys
  readInlineList0 = do
    readInlineSpace
    readInlineChar '['
    readInlineList1 WaitingCharList'ValueOrEnd []
  readInlineList1 wcl ys = do
    readInlineSpace
    mc <- PP.draw
    case mc of
      Nothing -> lift $ Left ParseError'InvalidEndOfLine
      Just c -> case wcl of
        WaitingCharList'ValueOrEnd -> case c of
          ']' -> return $ V.fromList $ reverse ys
          '[' -> do
            item <- Item'List <$> readInlineList1 WaitingCharList'ValueOrEnd []
            readInlineList1 WaitingCharList'CommaOrEnd $ item : ys
          '{' -> do
            item <- Item'Dictionary
              <$> readInlineDict1 WaitingCharDict'KeyOrEnd M.empty
            readInlineList1 WaitingCharList'CommaOrEnd $ item : ys
          _ | S.member c specialCharsList ->
            lift $ Left ParseError'InvalidChar
          _ -> do
            PP.unDraw c
            item <- toItemWithoutError <$> readInlineString specialCharsList
            readInlineList1 WaitingCharList'CommaOrEnd $ item : ys
        WaitingCharList'Value -> case c of
          '[' -> do
            item <- Item'List <$> readInlineList1 WaitingCharList'ValueOrEnd []
            readInlineList1 WaitingCharList'CommaOrEnd $ item : ys
          '{' -> do
            item <- Item'Dictionary
              <$> readInlineDict1 WaitingCharDict'KeyOrEnd M.empty
            readInlineList1 WaitingCharList'CommaOrEnd $ item : ys
          _ | S.member c specialCharsList ->
            lift $ Left ParseError'InvalidChar
          _ -> do
            PP.unDraw c
            item <- toItemWithoutError <$> readInlineString specialCharsList
            readInlineList1 WaitingCharList'CommaOrEnd $ item : ys
        WaitingCharList'CommaOrEnd -> case c of
          ',' -> readInlineList1 WaitingCharList'Value ys
          ']' -> return $ V.fromList $ reverse ys
          _ -> lift $ Left ParseError'InvalidChar
  readInlineDict0 = do
    readInlineSpace
    readInlineChar '{'
    readInlineDict1 WaitingCharDict'KeyOrEnd M.empty
  readInlineDict1 wcd m0 = do
    readInlineSpace
    mc <- PP.draw
    case mc of
      Nothing -> lift $ Left ParseError'InvalidEndOfLine
      Just c -> case wcd of
        WaitingCharDict'KeyOrEnd -> case c of
          '}' -> return m0
          _ | S.member c specialCharsDict ->
            lift $ Left ParseError'InvalidChar
          _ -> do
            PP.unDraw c
            key <- toKeyWithoutError <$> readInlineString specialCharsDict
            when (M.member key m0) $ lift $ Left ParseError'DuplicateKey
            readInlineDict1 (WaitingCharDict'Colon key) m0
        WaitingCharDict'Key -> case c of
          _ | S.member c specialCharsDict ->
            lift $ Left ParseError'InvalidChar
          _ -> do
            PP.unDraw c
            key <- toKeyWithoutError <$> readInlineString specialCharsDict
            when (M.member key m0) $ lift $ Left ParseError'DuplicateKey
            readInlineDict1 (WaitingCharDict'Colon key) m0
        WaitingCharDict'Colon k -> case c of
          ':' -> readInlineDict1 (WaitingCharDict'Value k) m0
          _ -> lift $ Left ParseError'InvalidChar
        WaitingCharDict'Value k -> case c of
          '[' -> do
            vi <- Item'List <$> readInlineList1 WaitingCharList'ValueOrEnd []
            readInlineDict1 WaitingCharDict'CommaOrEnd $ M.insert k vi m0
          '{' -> do
            vi <- Item'Dictionary
              <$> readInlineDict1 WaitingCharDict'KeyOrEnd M.empty
            readInlineDict1 WaitingCharDict'CommaOrEnd $ M.insert k vi m0
          _ | S.member c specialCharsDict ->
            lift $ Left ParseError'InvalidChar
          _ -> do
            PP.unDraw c
            vi <- toItemWithoutError <$> readInlineString specialCharsDict
            readInlineDict1 WaitingCharDict'CommaOrEnd $ M.insert k vi m0
        WaitingCharDict'CommaOrEnd -> case c of
          ',' -> readInlineDict1 WaitingCharDict'Key m0
          '}' -> return m0
          _ -> lift $ Left ParseError'InvalidChar

parse' :: T.Text -> Either ParseError Item
parse' = parse . TL.fromStrict

parseDocument :: TL.Text -> Either ParseError Document
parseDocument ts = case parse ts of
  Left ParseError'Empty -> Right Document'Empty
  Right i -> Right $ Document'Item i
  Left err -> Left err