diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,23 @@
+Copyright (c) 2017 Daniel Lovasko
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+   this list of conditions and the following disclaimer.
+ * 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.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/comma.cabal b/comma.cabal
new file mode 100644
--- /dev/null
+++ b/comma.cabal
@@ -0,0 +1,37 @@
+name:                comma
+version:             1.0.0
+synopsis:            CSV Parser & Producer
+description:         Comma is a simple CSV format parser and producer that
+                     closely follows the RFC4810 document.
+homepage:            https://github.com/lovasko/comma
+license:             OtherLicense
+license-file:        LICENSE
+author:              Daniel Lovasko <daniel.lovasko@gmail.com>
+maintainer:          Daniel Lovasko <daniel.lovasko@gmail.com>
+copyright:           2017 Daniel Lovasko
+category:            Text
+build-type:          Simple
+cabal-version:       >=1.10
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Text.Comma
+  build-depends:       base >= 4.7 && < 5
+                     , attoparsec
+                     , text
+  default-language:    Haskell2010
+
+test-suite comma-test
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Prop.hs
+  build-depends:       base >= 4.7 && < 5
+                     , QuickCheck
+                     , comma
+                     , text
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/lovasko/comma
diff --git a/src/Text/Comma.hs b/src/Text/Comma.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Comma.hs
@@ -0,0 +1,50 @@
+{- |
+Module      : Text.Comma
+Description : CSV Parser & Producer
+Copyright   : (c) Daniel Lovasko, 2017
+License     : BSD2
+
+Maintainer  : Daniel Lovasko <daniel.lovasko@gmail.com>
+Stability   : stable
+Portability : portable
+-}
+
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Comma
+( comma
+, uncomma
+) where
+
+import Control.Applicative
+import Data.List
+import qualified Data.Attoparsec.Text as A
+import qualified Data.Text as T
+
+
+-- | Parse a field of a record.
+field :: A.Parser T.Text -- ^ parser
+field = fmap T.concat quoted <|> normal A.<?> "field"
+  where
+    normal  = A.takeWhile (A.notInClass "\n\r,\"")     A.<?> "normal field"
+    quoted  = A.char '"' *> many between <* A.char '"' A.<?> "quoted field"
+    between = A.takeWhile1 (/= '"') <|> (A.string "\"\"" *> pure "\"")
+
+-- | Parse a block of text into a CSV table.
+comma :: T.Text                   -- ^ CSV text
+      -> Either String [[T.Text]] -- ^ error | table
+comma text = A.parseOnly table (T.stripEnd text)
+  where
+    table  = A.sepBy1 record A.endOfLine A.<?> "table"
+    record = A.sepBy1 field (A.char ',') A.<?> "record"
+
+-- | Render a table of texts into a valid CSV output.
+uncomma :: [[T.Text]] -- ^ table
+        -> T.Text     -- ^ CSV text
+uncomma = T.unlines . map (\r -> T.concat $ intersperse "," (map conv r))
+  where
+    isQuoted  = T.any (`elem` ['"', '\n', '\r'])
+    enquote x = T.concat ["\"", x, "\""]
+    conv f
+      | isQuoted f = enquote (T.replace "\"" "\"\"" f)
+      | otherwise  = f
diff --git a/test/Prop.hs b/test/Prop.hs
new file mode 100644
--- /dev/null
+++ b/test/Prop.hs
@@ -0,0 +1,38 @@
+import Control.Monad
+import Text.Comma
+import Test.QuickCheck
+import qualified Data.Text as T
+
+
+newtype CSV = CSV [[T.Text]] deriving (Show)
+
+-- | Generate the content of a field.
+genField :: Gen T.Text
+genField = fmap T.pack (replicateM 5 $ elements alphabet)
+  where alphabet = ['a'..'z'] ++ ['A'..'Z'] ++ "\"\n"
+
+-- | Random-generated instances of CSV.
+instance Arbitrary CSV where
+  arbitrary = do
+    nRows <- elements [1..10]
+    nCols <- elements [1..3]
+    cells <- replicateM (nRows * nCols) genField
+    return $ CSV (init $ chunksOf nCols cells)
+
+-- | Split a list into chunks of a specified length.
+chunksOf :: Int   -- ^ chunk length
+         -> [a]   -- ^ list
+         -> [[a]] -- ^ list of chunks
+chunksOf _ [] = [[]]
+chunksOf n xs = take n xs : chunksOf n (drop n xs)
+
+-- | The 'comma' and 'uncomma' function have to form identity when composed.
+test :: CSV  -- ^ CSV table
+     -> Bool -- ^ result
+test (CSV csv) = case comma (uncomma csv) of
+  Left _     -> False
+  Right csv' -> csv == csv'
+
+-- | Run the identity property test.
+main :: IO ()
+main = quickCheckWith stdArgs {maxSuccess=10000} (property test)
