{-# LANGUAGE TupleSections, DeriveGeneric, OverloadedStrings, CPP #-} module Sugar ( Sugar(..) , Wrap(..) , Note , FromSugar(..) , ToSugar(..) , sugarTextMay , readSugarFromFile , readSugarListFromFile , parseSugarFromText , parseSugarListFromText , prettyPrintSugarIO , prettyPrintSugar ) where import Control.Applicative (Alternative(..)) import Data.Void (Void) import Data.Text (Text) import Data.Map (Map) import Data.Maybe (isNothing) import Data.Text.Conversions (ToText(..), fromText, unUTF8, decodeConvertText, UTF8(..)) import Data.String (IsString(..)) import Data.Word (Word8,Word16,Word32,Word64) import Data.Int (Int8,Int16,Int32,Int64) import Data.Char (isSeparator) import GHC.Generics (Generic) import qualified Data.Map as Map import qualified Data.Serialize as Serialize import qualified Data.ByteString as BS import qualified Data.Text as T import qualified Data.Text.IO as TIO import qualified Text.Megaparsec as P import qualified Text.Megaparsec.Char as P import qualified Text.Megaparsec.Char.Lexer as L --- data Sugar = Sugar'Unit Note | Sugar'Text Text Note | Sugar'List [Sugar] Wrap Note | Sugar'Map [(Sugar,Sugar)] Note deriving (Eq, Show, Generic) data Wrap = Wrap'Square | Wrap'Paren deriving (Eq, Show, Generic) type Note = Maybe Sugar -- instance Serialize.Serialize Sugar where get = do tag <- Serialize.getWord8 go tag where go :: Word8 -> Serialize.Get Sugar go 0 = Sugar'Unit <$> Serialize.get go 1 = Sugar'Text <$> getSerializedText <*> Serialize.get go 2 = Sugar'List <$> Serialize.get <*> Serialize.get <*> Serialize.get go 3 = Sugar'Map <$> Serialize.get <*> Serialize.get go _ = fail "No matching Sugar value" getSerializedText :: Serialize.Get Text getSerializedText = do txt <- (decodeConvertText . UTF8) <$> (Serialize.get :: Serialize.Get BS.ByteString) maybe (fail "Cannot deserialize text as UTF8") pure txt put (Sugar'Unit note) = do Serialize.put (0 :: Word8) Serialize.put note put (Sugar'Text txt note) = do Serialize.put (1 :: Word8) Serialize.put (unUTF8 $ fromText txt :: BS.ByteString) Serialize.put note put (Sugar'List xs w note) = do Serialize.put (2 :: Word8) Serialize.put xs Serialize.put w Serialize.put note put (Sugar'Map m note) = do Serialize.put (3 :: Word8) Serialize.put m Serialize.put note instance Serialize.Serialize Wrap where instance IsString Sugar where fromString str = Sugar'Text (toText str) Nothing -- class FromSugar a where parseSugar :: Sugar -> Maybe a instance FromSugar a => FromSugar [a] where parseSugar (Sugar'List xs _ _) = mapM parseSugar xs parseSugar _ = Nothing sugarTextMay :: Sugar -> Maybe Text sugarTextMay (Sugar'Text t _) = Just t sugarTextMay _ = Nothing -- class ToSugar a where toSugar :: a -> Sugar instance ToSugar () where toSugar () = Sugar'Unit Nothing instance ToSugar Text where toSugar t = Sugar'Text t Nothing -- TODO: Review this if it causes problems in the REPL instance ToSugar a => ToSugar [a] where toSugar xs = Sugar'List (map toSugar xs) Wrap'Square Nothing instance (ToSugar a, ToSugar b) => ToSugar (Map a b) where toSugar m = Sugar'Map (map (\(k,v) -> (toSugar k, toSugar v)) $ Map.toList m) Nothing instance (ToSugar a, ToSugar b) => ToSugar (a,b) where toSugar (a,b) = Sugar'List [toSugar a, toSugar b] Wrap'Paren Nothing instance (ToSugar a, ToSugar b, ToSugar c) => ToSugar (a,b,c) where toSugar (a,b,c) = Sugar'List [toSugar a, toSugar b, toSugar c] Wrap'Paren Nothing instance ToSugar Integer where toSugar = sugarShow instance ToSugar Int where toSugar = sugarShow instance ToSugar Int8 where toSugar = sugarShow instance ToSugar Int16 where toSugar = sugarShow instance ToSugar Int32 where toSugar = sugarShow instance ToSugar Int64 where toSugar = sugarShow instance ToSugar Word where toSugar = sugarShow instance ToSugar Word8 where toSugar = sugarShow instance ToSugar Word16 where toSugar = sugarShow instance ToSugar Word32 where toSugar = sugarShow instance ToSugar Word64 where toSugar = sugarShow instance ToSugar Float where toSugar = sugarShow instance ToSugar Double where toSugar = sugarShow sugarShow :: Show a => a -> Sugar sugarShow s = Sugar'Text (T.pack $ show s) Nothing --- data PrettyPrintConfig = PrettyPrintConfig { ppcTabbedSpaces :: Int } deriving (Show, Eq) data PrettyPrintState = PrettyPrintState { ppsNesting :: Int } deriving (Show, Eq) prettyPrintSugarIO :: Sugar -> IO () prettyPrintSugarIO = TIO.putStr . prettyPrintSugar prettyPrintSugar :: Sugar -> Text prettyPrintSugar = prettyPrintSugar' (PrettyPrintConfig 2) prettyPrintSugar' :: PrettyPrintConfig -> Sugar -> Text prettyPrintSugar' ppc = prettyPrintStep ppc (PrettyPrintState 0) prettyPrintNesting :: PrettyPrintConfig -> PrettyPrintState -> Text prettyPrintNesting ppc pps = T.replicate (ppcTabbedSpaces ppc * ppsNesting pps) " " ppsIncrNesting :: PrettyPrintState -> PrettyPrintState ppsIncrNesting pps = pps { ppsNesting = ppsNesting pps + 1 } ppsDecrNesting :: PrettyPrintState -> PrettyPrintState ppsDecrNesting pps = pps { ppsNesting = if n >= 1 then n else 0 } where n = ppsNesting pps - 1 ppNewLine :: PrettyPrintConfig -> PrettyPrintState -> Text ppNewLine ppc pps = "\n" <> prettyPrintNesting ppc pps prettyPrintStep :: PrettyPrintConfig -> PrettyPrintState -> Sugar -> Text prettyPrintStep _ _ (Sugar'Unit note) = "()" <> minifyPrintNote note prettyPrintStep _ _ (Sugar'Text txt note) = sanitizeText txt <> minifyPrintNote note prettyPrintStep ppc pps (Sugar'List xs w note) = open <> T.concat (map (\x -> T.concat [ppNewLine ppc pps, prettyPrintStep ppc (ppsIncrNesting pps) x]) xs) <> ppNewLine ppc (ppsDecrNesting pps) <> close <> minifyPrintNote note where open, close :: Text (open,close) = case w of Wrap'Square -> ("[","]"); Wrap'Paren -> ("(",")") prettyPrintStep ppc pps (Sugar'Map m note) = if ppsNesting pps == 0 && isNothing note then topLevel else nested where topLevel = T.concat (map (\(k,v) -> T.concat [prettyPrintStep ppc nextPps k, " ", prettyPrintStep ppc nextPps v, "\n"]) m) nested = "{" <> T.concat (map (\(k,v) -> T.concat [ppNewLine ppc pps, prettyPrintStep ppc nextPps k, " ", prettyPrintStep ppc nextPps v]) m) <> ppNewLine ppc (ppsDecrNesting pps) <> "}" <> minifyPrintNote note nextPps = ppsIncrNesting pps minifyPrint :: Sugar -> Text minifyPrint (Sugar'Unit note) = "()" <> minifyPrintNote note minifyPrint (Sugar'Text txt note) = sanitizeText txt <> minifyPrintNote note minifyPrint (Sugar'List xs w note) = open <> T.intercalate " " (map minifyPrint xs) <> close <> minifyPrintNote note where open, close :: Text (open,close) = case w of Wrap'Square -> ("[","]"); Wrap'Paren -> ("(",")") minifyPrint (Sugar'Map m note) = "{" <> T.intercalate " " (map minifyPrint xs) <> "}" <> minifyPrintNote note where xs :: [Sugar] xs = (\(k,v) -> [k,v]) =<< m minifyPrintNote :: Note -> Text minifyPrintNote Nothing = "" minifyPrintNote (Just s) = "<" <> minifyPrint s <> ">" sanitizeText :: Text -> Text sanitizeText t | T.length t == 0 = "\"\"" | T.find (\c -> isSeparator c || elem c reservedChars) t /= Nothing = "\"" <> replaceDoubleQuotes t <> "\"" | otherwise = t where replaceDoubleQuotes :: Text -> Text replaceDoubleQuotes = T.replace "\"" "\\\"" reservedChars :: [Char] reservedChars = ['\"','[',']','<','>','(',')','{','}',';'] --- --- readSugarFromFile :: FilePath -> IO (Maybe Sugar) readSugarFromFile path = do content <- TIO.readFile path return $ parseSugarFromText content parseSugarFromText :: Text -> Maybe Sugar parseSugarFromText t = case P.runParser sugarP "" t of Left _ -> Nothing Right s -> Just s readSugarListFromFile :: FilePath -> IO (Maybe Sugar) readSugarListFromFile path = do content <- TIO.readFile path return $ parseSugarListFromText content parseSugarListFromText :: Text -> Maybe Sugar parseSugarListFromText t = case P.runParser sugarNoBracketsListP "" t of Left _ -> Nothing Right s -> Just s --- --- type Parser = P.Parsec Void Text sugarP :: Parser Sugar sugarP = P.choice [P.try noCurlysMapP, sugarP'] sugarNoBracketsListP :: Parser Sugar sugarNoBracketsListP = P.choice [P.try noBracketsListP, sugarP'] sugarP' :: Parser Sugar sugarP' = do c <- P.lookAhead P.anySingle case c of '\"' -> quotedTextP '(' -> P.choice [unitP, parenListP] ')' -> fail "Not valid Sugar" '[' -> squareListP ']' -> fail "Not valid Sugar" '{' -> mapP '}' -> fail "Not valid Sugar" '<' -> fail "Not valid Sugar" '>' -> fail "Not valid Sugar" _ -> unQuotedTextP unitP :: Parser Sugar unitP = P.string "()" *> sc *> (Sugar'Unit <$> noteP) parenListP, squareListP :: Parser Sugar parenListP = (\xs -> Sugar'List xs Wrap'Paren) <$> parensP (P.many sugarP') <*> noteP squareListP = (\xs -> Sugar'List xs Wrap'Square) <$> (squareBracketsP $ sc *> P.many elementP <* sc) <*> noteP where elementP :: Parser Sugar elementP = sc *> sugarP' <* sc noBracketsListP :: Parser Sugar noBracketsListP = (\xs -> Sugar'List xs Wrap'Square) <$> (sc *> P.many elementP <* sc) <*> pure Nothing where elementP :: Parser Sugar elementP = sc *> sugarP' <* sc mapP, noCurlysMapP :: Parser Sugar mapP = Sugar'Map <$> (curlyBracesP $ sc *> P.many mapPairP <* sc) <*> noteP noCurlysMapP = Sugar'Map <$> (sc *> P.many mapPairP <* sc) <*> pure Nothing -- TODO: Instead of `P.space1`, use the same characters for `isSeparator` mapPairP :: Parser (Sugar, Sugar) mapPairP = (,) <$> sugarP' <*> (sc *> sugarP') <* sc noteP :: Parser Note noteP = P.optional $ angleBracketsP sugarP' parensP, angleBracketsP, squareBracketsP, curlyBracesP :: Parser a -> Parser a parensP = P.between (symbol "(") (symbol ")") angleBracketsP = P.between (symbol "<") (symbol ">") squareBracketsP = P.between (symbol "[") (symbol "]") curlyBracesP = P.between (symbol "{") (symbol "}") symbol :: Text -> Parser Text symbol = L.symbol sc quotedTextP, unQuotedTextP :: Parser Sugar quotedTextP = Sugar'Text <$> doubleQuotedTextP_ <*> (sc *> noteP) unQuotedTextP = Sugar'Text <$> notQuotedTextP_ <*> noteP doubleQuotedTextP_, notQuotedTextP_ :: Parser Text doubleQuotedTextP_ = T.pack <$> quotedP where quotedP :: Parser String quotedP = P.between (P.char '\"') (P.char '\"') (many (P.try escaped <|> normalChar)) where escaped = '\"' <$ P.string "\\\"" normalChar = P.satisfy (/='\"') notQuotedTextP_ = P.takeWhileP (Just "Text char") (\c -> not $ isSeparator c || c == '\n' || elem c reservedChars) sc :: Parser () sc = L.space ws (L.skipLineComment ";") -- TODO replace with ';' once issue 88 is fixed (L.skipBlockComment "#|" "|#") ws :: Parser () ws = (P.newline <|> P.separatorChar) *> pure ()