{-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} module Text.FDF (FDF (FDF, body), Field (Field, name, value, kids), mapWithKey, mapFieldWithKey, foldMapWithKey, foldMapFieldWithKey, traverseWithKey, traverseFieldWithKey, parse, serialize) where import Control.Applicative ((<*), (<*>), (<|>), many, some, optional) import Data.Bifunctor (bimap) import Data.ByteString (ByteString) import Data.Char (isSpace) import Data.Monoid.Instances.ByteString.UTF8 (ByteStringUTF8 (ByteStringUTF8)) import Data.Monoid.Textual (toString, toText) import Data.Text (Text) import Data.Text qualified as Text import Data.Text.Encoding (encodeUtf8) import Rank2 qualified import Text.Grampa import Text.Grampa.Combinators import Text.Parser.Combinators (manyTill) import Text.Grampa.PEG.Backtrack qualified as PEG type Parser = PEG.Parser (Rank2.Only FDF) data FDF = FDF { header :: ByteString, body :: Field, trailer :: ByteString} deriving (Show) data Field = Field { name :: Text, value :: Maybe Text, kids :: [Field]} deriving (Show) mapWithKey :: ([Text] -> Text -> Text) -> FDF -> FDF mapWithKey f x@FDF{body} = x{body = mapFieldWithKey f body} mapFieldWithKey :: ([Text] -> Text -> Text) -> Field -> Field mapFieldWithKey f x@Field{name, value, kids} = x{value = f [name] <$> value, kids = mapFieldWithKey (f . (name:)) <$> kids} foldMapWithKey :: Monoid a => ([Text] -> Text -> a) -> FDF -> a foldMapWithKey f x@FDF{body} = foldMapFieldWithKey f body foldMapFieldWithKey :: Monoid a => ([Text] -> Text -> a) -> Field -> a foldMapFieldWithKey f x@Field{name, value, kids} = foldMap (f [name]) value <> foldMap (foldMapFieldWithKey $ f . (name:)) kids traverseWithKey :: Applicative f => ([Text] -> Text -> f Text) -> FDF -> f FDF traverseWithKey f x@FDF{body} = (\body'-> x{body = body'}) <$> traverseFieldWithKey f body traverseFieldWithKey :: Applicative f => ([Text] -> Text -> f Text) -> Field -> f Field traverseFieldWithKey f x@Field{name, value, kids} = Field name <$> traverse (f [name]) value <*> traverse (traverseFieldWithKey $ f . (name:)) kids serialize :: FDF -> ByteString serialize FDF{header, body, trailer} = "%FDF-1.2\n" <> header <> "<<\n" <> "/FDF\n" <> "<<\n" <> "/Fields [\n" <> encodeUtf8 (serializeField body) <> "\n" <> "]\n" <> ">>\n" <> ">>\n" <> trailer <> "%%EOF\n" serializeField :: Field -> Text serializeField Field{name, value, kids} = "<<\n" <> "/T (" <> name <> ")\n" <> foldMap (\v-> "/V (" <> v <> ")\n") value <> (if null kids then "" else "/Kids [\n" <> Text.intercalate "\n" (serializeField <$> kids) <> "]\n") <> ">>" parse :: ByteString -> Either String FDF parse input = bimap (\failure-> toString (const "") $ failureDescription s failure 4) id $ simply parseComplete parser s where s = ByteStringUTF8 input parser :: Parser ByteStringUTF8 FDF parser = FDF <$ (string "%FDF-1.2" <* lineEnd "first line") <*> extract ((takeWhile1 (`notElem` ["\r", "\n"]) "bytes") <> lineEnd <> (mconcat <$> manyTill line begin) "header") <* (string "/FDF" <* takeCharsWhile (== ' ') <* lineEnd "end header") <* begin <* (string "/Fields [" <* takeCharsWhile (== ' ') <* lineEnd "fields") <*> field <* (string "]" <* takeCharsWhile (== ' ') <* lineEnd "end the fields") <* (end "end the body") <*> extract ((end "end the object") <> string "endobj" <> lineEnd <> takeCharsWhile isSpace <> string "trailer" <> lineEnd <> (mconcat <$> manyTill line (string "%%EOF" "last line")) "trailer") <* optional lineEnd field :: Parser ByteStringUTF8 Field field = Field <$ begin <*> strictText (string "/T (" *> takeCharsWhile (`notElem` [')', '\r', '\n']) <* string ")" <* lineEnd "name") <*> optional (strictText $ admit (string "/V (" *> commit (takeCharsWhile (`notElem` [')', '\r', '\n']) <* string ")" <* lineEnd) <|> string "/V /" *> commit (takeCharsWhile (`notElem` ['\r', '\n']) <* lineEnd) "value")) <*> admit (string "/Kids [" *> commit (lineEnd *> takeSome field <* string "]" <* lineEnd "kids") <|> commit mempty) <* end begin :: Parser ByteStringUTF8 ByteStringUTF8 begin = string "<<" *> lineEnd "<<" end :: Parser ByteStringUTF8 ByteStringUTF8 end = string ">>" *> takeCharsWhile (== ' ') *> moptional lineEnd ">>" line :: Parser ByteStringUTF8 ByteStringUTF8 line = takeCharsWhile (`notElem` ['\r', '\n']) <> lineEnd "line" lineEnd :: Parser ByteStringUTF8 ByteStringUTF8 lineEnd = string "\r\n" <|> string "\r" <|> string "\n" strictText :: Parser ByteStringUTF8 ByteStringUTF8 -> Parser ByteStringUTF8 Text strictText = fmap $ toText (error . ("Invalid UTF-8 sequence: " ++) . show) extract :: Parser ByteStringUTF8 ByteStringUTF8 -> Parser ByteStringUTF8 ByteString extract = fmap $ \(ByteStringUTF8 bs) -> bs