packages feed

forms-data-format (empty) → 0.1

raw patch · 5 files changed

+220/−0 lines, 5 filesdep +basedep +bytestringdep +grammatical-parsers

Dependencies added: base, bytestring, grammatical-parsers, monoid-subclasses, parsers, rank2classes, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for forms-data-format++## 0.1.0.0 -- YYYY-mm-dd++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,28 @@+BSD 3-Clause License++Copyright (c) 2023, Mario++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this+   list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+   this list of conditions and the following disclaimer in the documentation+   and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its+   contributors may be used to endorse or promote products derived from+   this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,15 @@+Forms Data Format+=================++The Forms Data Format, or FDF for short, is a horrible data format thought up+by Adobe. Its only redeeming feature is that in practice it's much simpler+than PDF.++This Haskell package is hacked together to parse and re-serialize *some* files+in FDF format. It certainly does not support the full range of possible FDF+files. I tried to follow the specification but gave up.++The main purpose of the package is to allow parsing and serializing [Canadian+tax forms](https://github.com/blamario/canadian-income-tax) in FDF format.+For that purpose this hack seems sufficient. If you find it doesn't work for+some other FDF files, feel free to contribute.
+ forms-data-format.cabal view
@@ -0,0 +1,36 @@+cabal-version:      2.4+name:               forms-data-format+version:            0.1++synopsis: Parse and serialize FDF, the Forms Data Format++description: This is a hacked-together library to parse and serialize FDF,+             Adobe's [Forms Data+             Format](https://helpx.adobe.com/acrobat/kb/acrobat-forms-form-data-web.html). It+             does not follow the specification, but seems to work for simple+             cases.++category:           data, text+license:            BSD-3-Clause+license-files:      LICENSE+copyright:          (c) 2023 Mario Blažević+author:             Mario Blažević+maintainer:         blamario@protonmail.com+bug-reports:        https://github.com/blamario/forms-data-format/issues++extra-source-files: CHANGELOG.md, README.md+source-repository head+  type:     git+  location: https://github.com/blamario/forms-data-format++library+    exposed-modules:  Text.FDF++    -- Modules included in this library but not exported.+    -- other-modules:+    other-extensions: ImportQualifiedPost NamedFieldPuns OverloadedStrings+    build-depends:    base == 4.*, bytestring >=0.9 && < 0.12, text,+                      monoid-subclasses == 1.*, rank2classes >= 1 && < 1.6,+                      parsers < 0.13, grammatical-parsers >= 0.5 && < 0.8+    hs-source-dirs:   src+    default-language: Haskell2010
+ src/Text/FDF.hs view
@@ -0,0 +1,136 @@+{-# 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