diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,97 @@
+## Inflections 0.3.0.0
+
+* A more type-safe API forbidding creation of invalid words.
+
+* Made the API use `Text` instead of `String` (which significally improved
+  speed).
+
+* Switched to Megaparsec 5 for parsing.
+
+* Renamed `defaultMap` to `defaultTransliterations`.
+
+* Words now can contain digits (recognized by all parsers).
+
+* `parseSnakeCase` now is not confused when a word happens to have prefix
+  coinciding with an acronym. This is harder to fix for `parseCamelCase`
+  because acronym may contain capital letters, so old behavior is preserved
+  for `parseCamelCase` for now.
+
+* `parseCamelCase` and `parseSnakeCase` take any instance of `Foldable` as a
+  collection of acronyms, not just lists.
+
+* Added the `CHANGELOG.md` file.
+
+* Switched test suite to Hspec.
+
+* The `toUnderscore`, `toDashed`, and `toCamelCased` are not partial
+  anymore. They return parse error in `Left` just like parsing functions,
+  but this result can be lifted into any instance of `MonadThrow` with
+  `betterThrow` helper.
+
+* Improved documentation.
+
+## Inflections 0.2.0.1
+
+* Support for GHC 8.0.
+
+## Inflections 0.2.0.0
+
+* Added `other-modules` to test suite.
+
+## Inflections 0.1.0.10
+
+* Support for GHC 7.10.
+
+## Inflections 0.1.0.9
+
+* Support for GHC 7.8.
+
+## Inflections 0.1.0.8
+
+* Fixed a typo in docs of `humanize`.
+
+* Added `toUnderscore`, `toDashed`, and `toCamelCased`.
+
+## Inflections 0.1.0.7
+
+* Support for `base-4.7`.
+
+* Improved documentation.
+
+## Inflections 0.1.0.6
+
+* Added `titleize` and `humanize`.
+
+* Improved documentation.
+
+## Inflections 0.1.0.5
+
+* Added module documentation for `Text.Inflections`.
+
+## Inflections 0.1.0.4
+
+* Reduced number of public modules to one: `Text.Inflections`.
+
+* Added `ordinal` and `ordinalize`.
+
+* Improved documentation.
+
+## Inflections 0.1.0.3
+
+* Added `camelize`, `camelizeCustom`, `underscore`, and `underscoreCustom`.
+
+* Made the word parser accept empty input.
+
+* Improved documentation.
+
+## Inflections 0.1.0.2
+
+* Added the `transliterate` and `transliterateCustom` functions.
+
+## Inflections 0.1.0.1
+
+* No changes.
+
+## Inflections 0.1.0.0
+
+* Initial release.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,73 @@
+# String Inflections for Haskell
+
+[![License MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT)
+[![Hackage](https://img.shields.io/hackage/v/inflections.svg)](http://hackage.haskell.org/package/inflections)
+[![Stackage Nightly](http://stackage.org/package/inflections/badge/nightly)](http://stackage.org/nightly/package/inflections)
+[![Stackage LTS](http://stackage.org/package/inflections/badge/lts)](http://stackage.org/lts/package/inflections)
+[![Build Status](https://travis-ci.org/stackbuilders/inflections-hs.svg?branch=master)](https://travis-ci.org/stackbuilders/inflections-hs)
+
+This library is a partial port of the
+[String Inflector](http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html)
+from Ruby on Rails. It currently implements methods such as `parameterize`,
+`transliterate`, `camelize`, `underscore` and `dasherize`. Please see the
+haddock documentation for a complete list of the functions implemented by this
+library.
+
+Unlike the ActiveSupport (Rails) and Ember implementations of inflections, this
+library uses a parser to verify the input to functions like `camelize`. This is
+done to ensure that the output strings adhere to the syntax that they are
+supposed to generate. You can read more about the philosophy behind this library
+in the
+[Haddock documentation](http://hackage.haskell.org/package/inflections/docs/Text-Inflections.html).
+
+## Usage
+
+The following examples demonstrate usage of the `parameterize`, `transliterate`
+and `camelize` functions:
+
+```haskell
+λ: parameterize "Hola. ¿Cómo estás?"
+"hola-como-estas"
+
+λ: transliterate "Hola. ¿Cómo estás?"
+"Hola. ?Como estas?"
+
+λ: import Control.Monad (liftM)
+λ: liftM camelize $ parseSnakeCase "hey_there"
+"HeyThere"
+```
+
+## Customization
+
+Part of parameterizing strings is approximating all characters in the input
+encoding to ASCII characters. This library copies the character approximation
+table from the Ruby i18n library. This data structure is provided as
+`defaultCharacterTransliterations`. You can provide your own transliteration map
+by passing a Map structure (from Data.Map) to the `parameterizeCustom` function.
+
+If you want to specify a custom default replacement or approximation table for
+the `transliterate` function, you should instead call the `transliterateCustom`
+function which accepts a String for replacements and a Map for substitution.
+
+## TODO
+
+I'd like this library to implement other functions found in the Rails
+inflections library. If you need one of those functions, please submit a pull
+request!
+
+## Further documentation
+
+For more information, please see the the
+[Haddock docs for this module](http://hackage.haskell.org/package/inflections/docs/Text-Inflections.html).
+
+## Author
+
+Justin Leitgeb <justin@stackbuilders.com>
+
+## Contributing
+
+You may submit pull requests to this repository on GitHub. Tests are appreciated with your contribution.
+
+## License
+
+MIT - see [the LICENSE file](LICENSE).
diff --git a/Text/Inflections.hs b/Text/Inflections.hs
--- a/Text/Inflections.hs
+++ b/Text/Inflections.hs
@@ -1,160 +1,180 @@
-{- |
-Module      :  Text.Inflections
-Description :  Rails-like inflections library for common String transformations.
-Copyright   :  (c) Justin Leitgeb
-License     :  MIT
-
-Maintainer  :  justin@stackbuilders.com
-Stability   :  unstable
-Portability :  portable
-
-This module provides methods for common String transformations, similar to the
-"Inflections" library found in Rails:
-
-<http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html>
-
-While many of the functions in this library are the same as in implementations
-in Rails' ActiveSupport, the philosophy of this library is fundamentally
-different.  Where Rails tries to be as permissive as possible, and return a
-String when given any input, this library tries to output strings that make
-sense according to the function that is called.
-
-When you look closely at many of the functions in Rails' inflections
-library, you will notice that many of them are partial. That is, they only
-have well-defined output for some of the possible inputs to the function allowed
-by the type system. As an example, let's take the @underscore@ function. In
-Rails, it works like this:
-
->>> "fooBar".underscore
-"foo_bar"
-
-Looks ok so far. However, it's also easy to produce less expected results:
-
->>> "foo bar".underscore
-"foo bar"
-
-The output isn't underscored - it contains a space! It turns out that some of
-the functions from Inflections in ActiveSupport are /partial/. Ie., the outputs
-are really only specified for a certain range of the inputs allowed by the
-String type.
-
-In the Haskell inflections library, we aim to deliver more predictable results
-by separating the parsing of strings into tokens from the application of
-transformations. Let's see an example.
-
-First, we tokenize an underscored String using 'parseSnakeCase':
-
->>> parseSnakeCase [] "foo_bar"
-Right [Word "foo",Word "bar"]
-
-We can chain together the tokenization of the input String and the
-transformation to CamelCase by using 'Control.Monad.LiftM':
-
->>> import Control.Monad (liftM)
->>> liftM camelize $ parseSnakeCase "foo_bar"
-
-By separating out the tokenization from the application of inflections, we also
-end up with useful libraries for validating input which can be used
-independently:
-
->>> parseSnakeCase [] "fooBar"
-Left "(unknown)" (line 1, column 4):
-unexpected 'B'
-expecting lowercase letter, "_" or end of input
-
-This library is still a work-in-progress, and contributions are welcome for
-missing pieces and to fix bugs. Please see the Github page to contribute with
-code or bug reports:
-
-<https://github.com/stackbuilders/inflections-hs>
+-- |
+-- Module      :  Text.Inflections
+-- Description :  Rails-like inflections library for common Text transformations.
+-- Copyright   :  (c) Justin Leitgeb
+-- License     :  MIT
+--
+-- Maintainer  :  justin@stackbuilders.com
+-- Stability   :  unstable
+-- Portability :  portable
+--
+-- This module provides methods for common 'Text' transformations, similar
+-- to the Inflections library found in Rails:
+--
+-- <http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html>
+--
+-- While many of the functions in this library are the same as in
+-- implementations in Rails' ActiveSupport, the philosophy of this library
+-- is fundamentally different. Where Rails tries to be as permissive as
+-- possible, and return a String when given any input, this library tries to
+-- output 'Text' that makes sense according to the function that is called.
+--
+-- When you look closely at many of the functions in Rails' inflections
+-- library, you will notice that many of them are partial. That is, they
+-- only have well-defined output for some of the possible inputs to the
+-- function allowed by the type system. As an example, let's take the
+-- @underscore@ function. In Rails, it works like this:
+--
+-- >>> "fooBar".underscore
+-- "foo_bar"
+--
+-- Looks OK so far. However, it's also easy to produce less expected results:
+--
+-- >>> "foo bar".underscore
+-- "foo bar"
+--
+-- The output isn't underscored — it contains a space! It turns out that
+-- some of the functions from Inflections in ActiveSupport are /partial/.
+-- I.e., the outputs are really only specified for a certain range of the
+-- inputs allowed by the String type.
+--
+-- In the Haskell inflections library, we aim to deliver more predictable
+-- results by separating the parsing of strings into tokens from the
+-- application of transformations. Let's see an example.
+--
+-- First, we tokenize an underscored 'Text' using 'parseSnakeCase':
+--
+-- >>> parseSnakeCase [] "foo_bar"
+-- Right [Word "foo",Word "bar"]
+--
+-- We can chain together the tokenization of the input String and the
+-- transformation to CamelCase by using 'fmap':
+--
+-- >>> camelize <$> parseSnakeCase [] "foo_bar"
+-- Right "FooBar"
+--
+-- By separating out the tokenization from the application of inflections,
+-- we also end up with useful libraries for validating input which can be
+-- used independently:
+--
+-- >>> parseSnakeCase [] "fooBar"
+-- 1:4:
+-- unexpected 'B'
+-- expecting '_', end of input, or lowercase letter
+--
+-- As of version 0.3.0.0, we don't permit creation of invalid 'Word's by
+-- using of the smart constructors 'mkWord' and 'mkAcronym'. This is done
+-- because not every 'Text' value is a valid 'Word', as it should not
+-- contain whitespace, for example. Normal words have the type @'Word'
+-- 'Normal'@, while acronyms have the type @'Word' 'Acronym'@. If you need
+-- to have several words\/acronyms in a single list, use the existential
+-- wrapper 'SomeWord'. Parsing functions now produce 'SomeWord's.
+--
+-- This library is still a work-in-progress, and contributions are welcome
+-- for missing pieces and to fix bugs. Please see the Github page to
+-- contribute with code or bug reports:
+--
+-- <https://github.com/stackbuilders/inflections-hs>
 
--}
+{-# LANGUAGE CPP #-}
 
 module Text.Inflections
-    ( camelize
-    , camelizeCustom
-
-    , dasherize
-
-    , humanize
-
-    , underscore
-
-    , titleize
-
-    , defaultMap
-
-    , parameterize
-    , parameterizeCustom
-
-    , transliterate
-    , transliterateCustom
-
-    , ordinal
-    , ordinalize
-
-    , parseSnakeCase
-    , parseCamelCase
-    , Transliterations
+  ( -- * Types and helpers
+    Word
+  , WordType (..)
+  , mkWord
+  , mkAcronym
+  , unWord
+  , SomeWord (..)
+  , unSomeWord
+  , InflectionException (..)
+    -- * Parsing
+  , parseSnakeCase
+  , parseCamelCase
+    -- * Rendering
+  , camelize
+  , camelizeCustom
+  , dasherize
+  , humanize
+  , underscore
+  , titleize
+  , Transliterations
+  , defaultTransliterations
+  , parameterize
+  , parameterizeCustom
+  , transliterate
+  , transliterateCustom
+  , ordinalize
+  , ordinal
     -- * Often used combinators
-    , toUnderscore
-    , toDashed
-    , toCamelCased
-    )
+  , toUnderscore
+  , toDashed
+  , toCamelCased
+  , betterThrow )
 where
 
-
-import Text.Inflections.Data ( defaultMap )
-
-import Text.Inflections.Parameterize ( Transliterations
-                                     , parameterize
-                                     , parameterizeCustom )
-
-import Text.Inflections.Underscore ( underscore )
-
-import Text.Inflections.Camelize ( camelize, camelizeCustom )
-
-import Text.Inflections.Humanize ( humanize )
-
-import Text.Inflections.Titleize ( titleize )
-
-import Text.Inflections.Transliterate ( transliterate, transliterateCustom )
-
-import Text.Inflections.Dasherize ( dasherize )
-
-import Text.Inflections.Ordinal ( ordinal, ordinalize )
-
-import Text.Inflections.Parse.SnakeCase ( parseSnakeCase )
-
-import Text.Inflections.Parse.Types ( mapWord )
-
-import Text.Inflections.Parse.CamelCase ( parseCamelCase )
+import Control.Monad.Catch (MonadThrow (..))
+import Data.Text (Text)
+import Text.Inflections.Camelize (camelize, camelizeCustom)
+import Text.Inflections.Dasherize (dasherize)
+import Text.Inflections.Data (Transliterations, defaultTransliterations)
+import Text.Inflections.Humanize (humanize)
+import Text.Inflections.Ordinal (ordinal, ordinalize)
+import Text.Inflections.Parameterize (parameterize, parameterizeCustom)
+import Text.Inflections.Parse.CamelCase (parseCamelCase)
+import Text.Inflections.Parse.SnakeCase (parseSnakeCase)
+import Text.Inflections.Titleize (titleize)
+import Text.Inflections.Transliterate (transliterate, transliterateCustom)
+import Text.Inflections.Types
+import Text.Inflections.Underscore (underscore)
+import Text.Megaparsec
 
-import Data.Char ( toLower )
+#if MIN_VERSION_base(4,8,0)
+import Prelude hiding (Word)
+#endif
 
--- | Transforms CamelCasedString to
--- snake_cased_string_with_underscores. Throws exception if parsing failed
-toUnderscore :: String -> String
-toUnderscore =
-    underscore
-    . either (error .  ("toUnderscore: " ++) . show) id
-    . parseCamelCase []
+-- | Transforms CamelCasedString to snake_cased_string_with_underscores.
+--
+-- > toUnderscore = fmap underscore . parseCamelCase []
+--
+-- >>> toUnderscore "FooBarBazz"
+-- "foo_bar_bazz"
+toUnderscore :: Text -> Either (ParseError Char Dec) Text
+toUnderscore = fmap underscore . parseCamelCase []
 
--- | Transforms CamelCasedString to snake-cased-string-with-dashes. Throws
--- exception if parsing failed.
-toDashed :: String -> String
-toDashed =
-    dasherize
-    . map (mapWord (map toLower))
-    . either (error . ("toDashed: " ++) . show) id
-    . parseCamelCase []
+-- | Transforms CamelCasedString to snake-cased-string-with-dashes.
+--
+-- > toDashed = fmap dasherize . parseCamelCase []
+--
+-- >>> toDashed "FooBarBazz"
+-- "foo-bar-bazz"
+toDashed :: Text -> Either (ParseError Char Dec) Text
+toDashed = fmap dasherize . parseCamelCase []
 
 -- | Transforms underscored_text to CamelCasedText. If first argument is
 -- 'True' then FirstCharacter in result string will be in upper case. If
--- 'False' then firstCharacter will be in lower case. Throws exception if
--- parsing failed
-toCamelCased :: Bool -> String -> String
-toCamelCased t =
-    camelizeCustom t
-    . either (error . ("toCamelCased: " ++) . show) id
-    . parseSnakeCase []
+-- 'False' then firstCharacter will be in lower case.
+--
+-- > toCamelCased t = fmap (camelizeCustom t) . parseSnakeCase []
+--
+-- >>> toCamelCased True "foo_bar_bazz"
+-- "FooBarBazz"
+-- >>> toCamelCased False "foo_bar_bazz"
+-- "fooBarBazz"
+toCamelCased
+   :: Bool              -- ^ Capitalize the first character
+  -> Text              -- ^ Input
+  -> Either (ParseError Char Dec) Text -- ^ Ouput
+toCamelCased t = fmap (camelizeCustom t) . parseSnakeCase []
+
+-- | Lift something of type @'Either' ('ParseError' 'Char' 'Dec') a@ to
+-- an instance of 'MonadThrow'. Useful when you want to shortcut on parsing
+-- failures and you're in an instance of 'MonadThrow'.
+--
+-- This throws 'InflectionParsingFailed' if given value is inside 'Left'.
+--
+-- /since 0.3.0.0/
+
+betterThrow :: MonadThrow m => Either (ParseError Char Dec) a -> m a
+betterThrow (Left err) = throwM (InflectionParsingFailed err)
+betterThrow (Right  x) = return x
diff --git a/Text/Inflections/Camelize.hs b/Text/Inflections/Camelize.hs
--- a/Text/Inflections/Camelize.hs
+++ b/Text/Inflections/Camelize.hs
@@ -1,32 +1,53 @@
-module Text.Inflections.Camelize ( camelize, camelizeCustom ) where
+-- |
+-- Module      :  Text.Inflections.Camelize
+-- Copyright   :  © 2016 Justin Leitgeb
+-- License     :  MIT
+--
+-- Maintainer  :  Justin Leitgeb <justin@stackbuilders.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Conversion to CamelCased phrases.
 
-import Text.Inflections.Parse.Types (Word(..))
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
 
-import Data.Char (toUpper, toLower)
+module Text.Inflections.Camelize
+  ( camelize
+  , camelizeCustom )
+where
 
-import Prelude (String, Bool(..), concatMap, (.), zip, ($), repeat)
+import Data.Text (Text)
+import Text.Inflections.Types
+import qualified Data.Text as T
 
--- |Turns a an input Word List in into CamelCase. Returns the CamelCase String.
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative
+#endif
+
+-- | Turn an input word list in into CamelCase.
+--
+-- >>> foo  <- SomeWord <$> mkWord "foo"
+-- >>> bar  <- SomeWord <$> mkAcronym "bar"
+-- >>> bazz <- SomeWord <$> mkWord "bazz"
+-- >>> camelize [foo,bar,bazz]
+-- "FoobarBazz"
 camelize
-  :: [Word] -- ^ Input Words to separate with underscores
-  -> String -- ^ The camelized String
+  :: [SomeWord] -- ^ Input words
+  -> Text       -- ^ The camelized 'Text'
 camelize = camelizeCustom True
 
--- |Turns an input Word List into a CamelCase String.
+-- | Turn an input word list into a CamelCase String.
+--
+-- >>> foo  <- SomeWord <$> mkWord "foo"
+-- >>> bar  <- SomeWord <$> mkAcronym "bar"
+-- >>> bazz <- SomeWord <$> mkWord "bazz"
+-- >>> camelizeCustom False [foo,bar,bazz]
+-- "foobarBazz"
 camelizeCustom
-  :: Bool   -- ^ Whether to capitalize the first character in the output String
-  -> [Word] -- ^ The input Words
-  -> String -- ^ The camelized String
-camelizeCustom isFirstCap = concatMap (caseForWord isFirstCap) . isFirstList
-
-
-caseForWord :: Bool -> (Word, Bool) -> String
-caseForWord True (Word (c:cs), True)  = toUpper c : cs
-caseForWord False (Word (c:cs), True) = toLower c : cs
-caseForWord _ (Word (c:cs), _)        = toUpper c : cs
-caseForWord _ (Word [], _)            = []
-caseForWord _ (Acronym s, _)          = s
-
--- |Returns list with Bool indicating if an element is first.
-isFirstList :: [a] -> [(a, Bool)]
-isFirstList xs = zip xs $ True : repeat False
+  :: Bool       -- ^ Whether to capitalize the first character in the output String
+  -> [SomeWord] -- ^ The input Words
+  -> Text       -- ^ The camelized 'Text'
+camelizeCustom _ []     = ""
+camelizeCustom c (x:xs) = T.concat $
+  unSomeWord (if c then T.toTitle else T.toLower) x : (unSomeWord T.toTitle <$> xs)
diff --git a/Text/Inflections/Dasherize.hs b/Text/Inflections/Dasherize.hs
--- a/Text/Inflections/Dasherize.hs
+++ b/Text/Inflections/Dasherize.hs
@@ -1,17 +1,32 @@
-module Text.Inflections.Dasherize ( dasherize ) where
+-- |
+-- Module      :  Text.Inflections.Dasherize
+-- Copyright   :  © 2016 Justin Leitgeb
+-- License     :  MIT
+--
+-- Maintainer  :  Justin Leitgeb <justin@stackbuilders.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Conversion to dasherized phrases.
 
-import Text.Inflections.Parse.Types (Word(..))
+{-# LANGUAGE OverloadedStrings #-}
 
-import Data.List (intercalate)
+module Text.Inflections.Dasherize
+  ( dasherize )
+where
 
-import Prelude (String, (.), map)
+import Data.Text (Text)
+import Text.Inflections.Types
+import qualified Data.Text as T
 
--- | Replaces underscores in a snake_cased string with dashes (hyphens).
+-- | Produce a string with words separated by dashes (hyphens).
+--
+-- >>> foo  <- SomeWord <$> mkWord "foo"
+-- >>> bar  <- SomeWord <$> mkAcronym "bar"
+-- >>> bazz <- SomeWord <$> mkWord "bazz"
+-- >>> dasherize [foo,bar,bazz]
+-- "foo-bar-bazz"
 dasherize
-  :: [Word] -- ^ Input Words to separate with dashes
-  -> String -- ^ The dasherized String
-dasherize = intercalate "-" . map toString
-
-toString :: Word -> String
-toString (Acronym s) = s
-toString (Word s)    = s
+  :: [SomeWord] -- ^ Input words to separate with dashes
+  -> Text       -- ^ The dasherized 'Text'
+dasherize = T.intercalate "-" . fmap (unSomeWord T.toLower)
diff --git a/Text/Inflections/Data.hs b/Text/Inflections/Data.hs
--- a/Text/Inflections/Data.hs
+++ b/Text/Inflections/Data.hs
@@ -1,11 +1,33 @@
-module Text.Inflections.Data where
+-- |
+-- Module      :  Text.Inflections.Data
+-- Copyright   :  © 2016 Justin Leitgeb
+-- License     :  MIT
+--
+-- Maintainer  :  Justin Leitgeb <justin@stackbuilders.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Auxiliary data used in the library.
 
-import Data.Map (Map, fromList)
+module Text.Inflections.Data
+  ( Transliterations
+  , defaultTransliterations )
+where
 
--- |These default transliterations stolen from the Ruby i18n library - see
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as M
+
+-- | A 'HashMap' containing mappings from international characters to
+-- sequences approximating these characters within the ASCII range.
+type Transliterations = HashMap Char String
+
+-- | These default transliterations are stolen from the Ruby i18n library -
+-- see
 -- <https://github.com/svenfuchs/i18n/blob/master/lib/i18n/backend/transliterator.rb#L41:L69>.
-defaultMap :: Map Char String
-defaultMap = fromList [
+--
+-- NOTE: before version 0.3.0.0 this was called @defaultMap@.
+defaultTransliterations :: Transliterations
+defaultTransliterations = M.fromList [
   ('À', "A"), ('Á', "A"), ('Â', "A"), ('Ã', "A"), ('Ä', "A"), ('Å', "A"),
   ('Æ', "AE"), ('Ç', "C"), ('È', "E"), ('É', "E"), ('Ê', "E"), ('Ë', "E"),
   ('Ì', "I"), ('Í', "I"), ('Î', "I"), ('Ï', "I"), ('Ð', "D"), ('Ñ', "N"),
diff --git a/Text/Inflections/Humanize.hs b/Text/Inflections/Humanize.hs
--- a/Text/Inflections/Humanize.hs
+++ b/Text/Inflections/Humanize.hs
@@ -1,24 +1,42 @@
-module Text.Inflections.Humanize (humanize) where
-
-import Text.Inflections.Parse.Types (Word(..))
+-- |
+-- Module      :  Text.Inflections.Humanize
+-- Copyright   :  © 2016 Justin Leitgeb
+-- License     :  MIT
+--
+-- Maintainer  :  Justin Leitgeb <justin@stackbuilders.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Conversion to “humanized” phrases.
 
-import Data.Char (toUpper)
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
 
-import Prelude (String, Bool(..), (.), map, zip, ($), unwords, repeat)
+module Text.Inflections.Humanize
+  ( humanize )
+where
 
--- |Capitalizes the first word and turns underscores into spaces. Like titleize,
--- this is meant for creating pretty output.
-humanize
-  :: [Word] -- ^ List of Words, first of which will be capitalized
-  -> String -- ^ The humanized output
-humanize = unwords . map caseForWord . isFirstList
+import Data.Text (Text)
+import Text.Inflections.Types
+import qualified Data.Text as T
 
--- |Returns list with Bool indicating if an element is first.
-isFirstList :: [a] -> [(a, Bool)]
-isFirstList xs = zip xs $ True : repeat False
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative
+#endif
 
-caseForWord :: (Word, Bool) -> String
-caseForWord (Word (c:cs), True)  = toUpper c : cs
-caseForWord (Word s, False)      = s
-caseForWord (Word [], _)         = []
-caseForWord (Acronym s, _)       = s  -- Acronyms are left intact
+-- | Capitalize the first word and separate words with spaces. Like
+-- 'Text.Inflections.Titleize.titleize', this is meant for creating pretty
+-- output.
+--
+-- >>> foo  <- SomeWord <$> mkWord "foo"
+-- >>> bar  <- SomeWord <$> mkAcronym "bar"
+-- >>> bazz <- SomeWord <$> mkWord "bazz"
+-- >>> humanize [foo,bar,bazz]
+-- "Foo bar bazz"
+humanize
+  :: [SomeWord]  -- ^ List of words, first of which will be capitalized
+  -> Text        -- ^ The humanized output
+humanize xs' =
+  case unSomeWord (T.replace "_" " ") <$> xs' of
+    []     -> ""
+    (x:xs) -> T.unwords $ T.toTitle x : (T.toLower <$> xs)
diff --git a/Text/Inflections/Ordinal.hs b/Text/Inflections/Ordinal.hs
--- a/Text/Inflections/Ordinal.hs
+++ b/Text/Inflections/Ordinal.hs
@@ -1,9 +1,47 @@
-module Text.Inflections.Ordinal (ordinal, ordinalize)
+-- |
+-- Module      :  Text.Inflections.Ordinal
+-- Copyright   :  © 2016 Justin Leitgeb
+-- License     :  MIT
+--
+-- Maintainer  :  Justin Leitgeb <justin@stackbuilders.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Conversion to spelled ordinal numbers.
+
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Inflections.Ordinal
+  ( ordinalize
+  , ordinal )
 where
 
+import Data.Monoid ((<>))
+import Data.Text (Text)
+import qualified Data.Text as T
+
+-- |Turns a number into an ordinal string used to denote the position in an
+-- ordered sequence such as 1st, 2nd, 3rd, 4th.
+--
+-- >>> ordinalize 1
+-- "1st"
+-- >>> ordinalize 2
+-- "2nd"
+-- >>> ordinalize 10
+-- "10th"
+ordinalize :: (Integral a, Show a) => a -> Text
+ordinalize n = T.pack (show n) <> ordinal n
+
 -- |Returns the suffix that should be added to a number to denote the position
 -- in an ordered sequence such as 1st, 2nd, 3rd, 4th.
-ordinal :: Integer -> String
+--
+-- >>> ordinal 1
+-- "st"
+-- >>> ordinal 2
+-- "nd"
+-- >>> ordinal 10
+-- "th"
+ordinal :: Integral a => a -> Text
 ordinal number
         | remainder100 `elem` [11..13] = "th"
         | remainder10 == 1             = "st"
@@ -13,9 +51,3 @@
   where abs_number   = abs number
         remainder10  = abs_number `mod` 10
         remainder100 = abs_number `mod` 100
-
--- |Turns a number into an ordinal string used to denote the position in an
--- ordered sequence such as 1st, 2nd, 3rd, 4th.
-ordinalize :: Integer -> String
-ordinalize n = show n ++ ordinal n
-
diff --git a/Text/Inflections/Parameterize.hs b/Text/Inflections/Parameterize.hs
--- a/Text/Inflections/Parameterize.hs
+++ b/Text/Inflections/Parameterize.hs
@@ -1,90 +1,48 @@
+-- |
+-- Module      :  Text.Inflections.Parametrize
+-- Copyright   :  © 2016 Justin Leitgeb
+-- License     :  MIT
+--
+-- Maintainer  :  Justin Leitgeb <justin@stackbuilders.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Parametrization for strings, useful for transliteration.
+
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
+
 module Text.Inflections.Parameterize
   ( parameterize
-  , parameterizeCustom
-  , Transliterations )
+  , parameterizeCustom )
 where
 
-import qualified Data.Map as Map
-import Control.Monad (guard)
-import Data.Maybe (mapMaybe)
-
-import Data.Char (toLower)
-
-import Data.List (group)
-import qualified Text.Parsec as P
-
-import Text.Inflections.Data (defaultMap)
-import Text.Inflections.Parse.Parameterizable ( PChar(..)
-                                              , parser
-                                              , isValidParamChar )
-
--- |A 'Data.Map.Map' containing mappings from international characters to
--- sequences approximating these characters within the ASCII range.
-type Transliterations = Map.Map Char String
-
--- |Replaces special characters in a string so that it may be used as part of a
--- 'pretty' URL. Uses the default transliterations in this library
-parameterize :: String -> String
-parameterize = parameterizeCustom defaultMap
-
--- |Transliterate a String with a custom transliteration table.
-parameterizeCustom :: Transliterations -> String -> String
-parameterizeCustom ts s =
-    case parsed of
-      Right ast -> (concatMap pCharToC . squeezeSeparators .
-                    trimUnwanted wanted . mapMaybe (parameterizeChar ts))
-                   ast
-
-      -- Note that this should never fail, since we accommodate all Unicode
-      -- characters as valid input.
-      Left err -> fail $ "Parse failed, please report a bug! Error: " ++
-                         show err
-
-    where parsed = P.parse parser  "" s
-          wanted :: [PChar] -- All valid URL chars - we shouldn't trim these.
-          wanted = Underscore :
-                   map (Acceptable . (: [])) (['a'..'z'] ++ ['0'..'9'])
-
--- |Look up character in transliteration list. Accepts a Transliteration map
--- which has Chars as keys and Strings as values for approximating common
--- international Unicode characters within the ASCII range.
-transliteratePCharCustom :: Transliterations -> Char -> Maybe PChar
-transliteratePCharCustom ts c = do
-  -- We may have expanded into multiple characters during
-  -- transliteration, so check validity of all characters in
-  -- result.
-  v <- Map.lookup c ts
-  guard (all isValidParamChar v)
-  return (Acceptable v)
-
--- |Given a Transliteration table and a PChar, returns Maybe PChar indicating
--- how this character should appear in a URL.
-parameterizeChar :: Transliterations -> PChar -> Maybe PChar
-parameterizeChar _  (UCase c)      = Just $ Acceptable [toLower c]
-parameterizeChar _  (Acceptable c) = Just $ Acceptable c
-parameterizeChar _  Separator      = Just Separator
-parameterizeChar _  Underscore     = Just Underscore
-parameterizeChar _  (OtherAscii _) = Just Separator
-parameterizeChar ts (NonAscii c)   = transliteratePCharCustom ts c
+import Data.Char (isAscii, isAlphaNum, isPunctuation, toLower)
+import Data.Text (Text)
+import Text.Inflections.Data
+import qualified Data.HashMap.Strict as M
+import qualified Data.Text           as T
 
--- |Turns PChar tokens into their String representation.
-pCharToC :: PChar -> String
-pCharToC (UCase c)        = [c]
-pCharToC (Acceptable str) = str
-pCharToC Separator        = "-"
-pCharToC Underscore       = "_"
-pCharToC (OtherAscii c)   = [c]
-pCharToC (NonAscii c)     = [c]
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative
+#endif
 
--- |Reduce sequences of separators down to only one separator.
-squeezeSeparators :: [PChar] -> [PChar]
-squeezeSeparators ps = concatMap squashSeparatorGroup $ group ps
-    where squashSeparatorGroup g = case head g of
-                                     Separator -> [Separator] -- only take head
-                                     _         -> g           -- don't change
+-- | Replace special characters in a string so that it may be used as part
+-- of a 'pretty' URL. Uses the 'defaultTransliterations'.
+parameterize :: Text -> Text
+parameterize = parameterizeCustom defaultTransliterations
+{-# INLINE parameterize #-}
 
--- |Trim non-wanted elements from the beginning and end of list.
-trimUnwanted :: Eq a => [a] -> [a] -> [a]
-trimUnwanted wanted = dropWhile notWanted . reverse . dropWhile notWanted
-                      . reverse
-  where notWanted = (`notElem` wanted)
+-- | Transliterate 'Text' with a custom transliteration table.
+parameterizeCustom :: Transliterations -> Text -> Text
+parameterizeCustom m txt = (T.intercalate "-" . T.words) (T.unfoldr f ("", txt))
+  where
+    f ("", t) = uncurry g <$> T.uncons t
+    f (x:xs, t) = Just (x, (xs, t))
+    g x xs
+      | (isAscii x && isAlphaNum x) || x == '_' = (toLower x, ("", xs))
+      | isPunctuation x = (' ', ("", xs))
+      | otherwise =
+          case toLower <$> M.lookupDefault " " x m of
+            ""     -> (' ', ("",xs))
+            (y:ys) -> (y,   (ys,xs))
diff --git a/Text/Inflections/Parse/Acronym.hs b/Text/Inflections/Parse/Acronym.hs
deleted file mode 100644
--- a/Text/Inflections/Parse/Acronym.hs
+++ /dev/null
@@ -1,16 +0,0 @@
-{-# LANGUAGE FlexibleContexts, NoMonomorphismRestriction #-}
-
-module Text.Inflections.Parse.Acronym ( acronym ) where
-
-import qualified Text.ParserCombinators.Parsec.Char as C
-import qualified Text.Parsec as P
-import qualified Text.Parsec.Prim as Prim
-
-import Text.Inflections.Parse.Types
-
-import Control.Applicative ((<$>))
-
-import Prelude (Char, String, (.), map)
-
-acronym :: P.Stream s m Char => [String] -> P.ParsecT s u m Word
-acronym as = Acronym <$> P.choice (map (Prim.try . C.string) as)
diff --git a/Text/Inflections/Parse/CamelCase.hs b/Text/Inflections/Parse/CamelCase.hs
--- a/Text/Inflections/Parse/CamelCase.hs
+++ b/Text/Inflections/Parse/CamelCase.hs
@@ -1,28 +1,75 @@
-{-# LANGUAGE FlexibleContexts, NoMonomorphismRestriction #-}
+-- |
+-- Module      :  Text.Inflections.Parse.CamelCase
+-- Copyright   :  © 2016 Justin Leitgeb
+-- License     :  MIT
+--
+-- Maintainer  :  Justin Leitgeb <justin@stackbuilders.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Parser for camel case “symbols”.
 
-module Text.Inflections.Parse.CamelCase ( parseCamelCase )
-where
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE DataKinds         #-}
+{-# LANGUAGE OverloadedStrings #-}
 
-import Text.Parsec
+module Text.Inflections.Parse.CamelCase
+  ( parseCamelCase )
+where
 
-import Text.Inflections.Parse.Types (Word(..))
-import Text.Inflections.Parse.Acronym (acronym)
+import Control.Applicative
+import Data.Text (Text)
+import Text.Inflections.Types
+import Text.Megaparsec
+import Text.Megaparsec.Text
+import qualified Data.Text as T
 
-import Prelude (Char, String, Either, return, ($))
+#if MIN_VERSION_base(4,8,0)
+import Prelude hiding (Word)
+#else
+import Data.Foldable
+import Prelude hiding (elem)
+#endif
 
-parseCamelCase :: [String] -> String -> Either ParseError [Word]
-parseCamelCase acronyms = parse (parser acronyms) "(unknown)"
+-- | Parse a CamelCase string.
+--
+-- >>> bar <- mkAcronym "bar"
+-- >>> parseCamelCase [bar] "FooBarBazz"
+-- Right [Word "Foo",Acronym "Bar",Word "Bazz"]
+--
+-- >>> parseCamelCase [] "foo_bar_bazz"
+-- 1:4:
+-- unexpected '_'
+-- expecting end of input, lowercase letter, or uppercase letter
+parseCamelCase :: (Foldable f, Functor f)
+  => f (Word 'Acronym) -- ^ Collection of acronyms
+  -> Text              -- ^ Input
+  -> Either (ParseError Char Dec) [SomeWord] -- ^ Result of parsing
+parseCamelCase acronyms = parse (parser acronyms) ""
 
+parser :: (Foldable f, Functor f)
+  => f (Word 'Acronym) -- ^ Collection of acronyms
+  -> Parser [SomeWord] -- ^ CamelCase parser
+parser acronyms = many (a <|> n) <* eof
+  where
+    n = SomeWord <$> word
+    a = SomeWord <$> acronym acronyms
 
--- |Recognizes an input String in CamelCase.
-parser :: Stream s m Char => [String] -> ParsecT s u m [Word]
-parser acronyms = do
-  ws <- many $ choice [ acronym acronyms, word ]
-  eof
-  return ws
+acronym :: (Foldable f, Functor f)
+  => f (Word 'Acronym)
+  -> Parser (Word 'Acronym)
+acronym acronyms = do
+  x <- T.pack <$> choice (string . T.unpack . unWord <$> acronyms)
+  case mkAcronym x of
+    Nothing -> empty -- cannot happen if the system is sound
+    Just acr -> return acr
+{-# INLINE acronym #-}
 
-word :: Stream s m Char => ParsecT s u m Word
+word :: Parser (Word 'Normal)
 word = do
-  firstChar <- upper <|> lower
-  restChars <- many lower
-  return $ Word $ firstChar : restChars
+  firstChar <- upperChar <|> lowerChar
+  restChars <- many $ lowerChar <|> digitChar
+  case (mkWord . T.pack) (firstChar : restChars) of
+    Nothing -> empty -- cannot happen if the system is sound
+    Just wrd -> return wrd
+{-# INLINE word #-}
diff --git a/Text/Inflections/Parse/Parameterizable.hs b/Text/Inflections/Parse/Parameterizable.hs
deleted file mode 100644
--- a/Text/Inflections/Parse/Parameterizable.hs
+++ /dev/null
@@ -1,42 +0,0 @@
-{-# LANGUAGE FlexibleContexts, NoMonomorphismRestriction #-}
-
-module Text.Inflections.Parse.Parameterizable
-  ( parser
-  , isValidParamChar
-  , PChar(..) )
-where
-
-import Data.Char (isAsciiLower, isAsciiUpper, isAscii, isDigit)
-import Control.Applicative
-import qualified Text.Parsec as P
-import qualified Text.ParserCombinators.Parsec.Char as C
-
-data PChar =   UCase Char
-             -- Since some of the transliterating approximations expand from
-             -- one Unicode to two ASCII chars (eg., œ to oe), we represent
-             -- this as a String.
-             | Acceptable String
-             | Separator
-             | Underscore
-             | OtherAscii Char
-             | NonAscii Char
-             deriving (Eq, Show)
-
--- |Matches 'acceptable' characters for parameterization purposes.
-acceptableParser :: P.Stream s m Char => P.ParsecT s u m PChar
-acceptableParser = do
-  c <- C.satisfy isValidParamChar
-  return $ Acceptable [c]
-
-parser :: P.Stream s m Char => P.ParsecT s u m [PChar]
-parser = P.many $ P.choice [
-           acceptableParser
-         , UCase      <$> C.satisfy isAsciiUpper
-         , Separator  <$  C.char '-'
-         , Underscore <$  C.char '_'
-         , OtherAscii <$> C.satisfy isAscii
-         , NonAscii   <$> C.satisfy (not . isAscii)
-         ]
-
-isValidParamChar :: Char -> Bool
-isValidParamChar c = isAsciiLower c || isDigit c
diff --git a/Text/Inflections/Parse/SnakeCase.hs b/Text/Inflections/Parse/SnakeCase.hs
--- a/Text/Inflections/Parse/SnakeCase.hs
+++ b/Text/Inflections/Parse/SnakeCase.hs
@@ -1,25 +1,62 @@
-{-# LANGUAGE FlexibleContexts, NoMonomorphismRestriction #-}
-
-module Text.Inflections.Parse.SnakeCase ( parseSnakeCase )
-where
+-- |
+-- Module      :  Text.Inflections.Parse.SnakeCase
+-- Copyright   :  © 2016 Justin Leitgeb
+-- License     :  MIT
+--
+-- Maintainer  :  Justin Leitgeb <justin@stackbuilders.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Parser for snake case “symbols”.
 
-import Control.Applicative ((<$>))
-import Text.Parsec
+{-# LANGUAGE CPP       #-}
+{-# LANGUAGE DataKinds #-}
 
-import Text.Inflections.Parse.Types (Word(..))
-import Text.Inflections.Parse.Acronym (acronym)
+module Text.Inflections.Parse.SnakeCase
+  ( parseSnakeCase )
+where
 
-import Prelude (Char, String, Either, return)
+import Control.Applicative
+import Data.Text (Text)
+import Text.Inflections.Types
+import Text.Megaparsec
+import Text.Megaparsec.Text
+import qualified Data.Text as T
 
-parseSnakeCase :: [String] -> String -> Either ParseError [Word]
-parseSnakeCase acronyms = parse (parser acronyms) "(unknown)"
+#if MIN_VERSION_base(4,8,0)
+import Prelude hiding (Word)
+#else
+import Data.Foldable
+import Prelude hiding (elem)
+#endif
 
+-- | Parse a snake_case string.
+--
+-- >>> bar <- mkAcronym "bar"
+-- >>> parseSnakeCase [bar] "foo_bar_bazz"
+-- Right [Word "foo",Acronym "bar",Word "bazz"]
+--
+-- >>> parseSnakeCase [] "fooBarBazz"
+-- 1:4:
+-- unexpected 'B'
+-- expecting '_', end of input, or lowercase letter
+parseSnakeCase :: (Foldable f, Functor f)
+  => f (Word 'Acronym) -- ^ Collection of acronyms
+  -> Text              -- ^ Input
+  -> Either (ParseError Char Dec) [SomeWord] -- ^ Result of parsing
+parseSnakeCase acronyms = parse (parser acronyms) ""
 
-parser :: Stream s m Char => [String] -> ParsecT s u m [Word]
-parser acronyms = do
-  ws <- (acronym acronyms <|> word) `sepBy` char '_'
-  eof
-  return ws
+parser :: (Foldable f, Functor f)
+  => f (Word 'Acronym)
+  -> Parser [SomeWord]
+parser acronyms = (pWord acronyms `sepBy` char '_') <* eof
 
-word :: Stream s m Char => ParsecT s u m Word
-word = Word <$> (many1 lower <|> many1 digit)
+pWord :: (Foldable f, Functor f)
+  => f (Word 'Acronym)
+  -> Parser SomeWord
+pWord acronyms = do
+  let acs = unWord <$> acronyms
+  r <- T.pack <$> some alphaNumChar
+  if r `elem` acs
+    then maybe empty (return . SomeWord) (mkAcronym r)
+    else maybe empty (return . SomeWord) (mkWord    r)
diff --git a/Text/Inflections/Parse/Types.hs b/Text/Inflections/Parse/Types.hs
deleted file mode 100644
--- a/Text/Inflections/Parse/Types.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module Text.Inflections.Parse.Types ( Word(..), mapWord ) where
-
-import Prelude (String, Show, Eq, ($))
-
--- | A 'String' that should be kept whole through applied inflections
-data Word
-
-    -- | A word that may be transformed by inflection
-    = Word String
-
-    -- | A word that may not be transformed by inflections
-    | Acronym String
-
-    deriving (Show, Eq)
-
-mapWord :: (String -> String) -> Word -> Word
-mapWord f (Word s) = Word $ f s
-mapWord f (Acronym s) = Acronym $ f s
diff --git a/Text/Inflections/Titleize.hs b/Text/Inflections/Titleize.hs
--- a/Text/Inflections/Titleize.hs
+++ b/Text/Inflections/Titleize.hs
@@ -1,18 +1,30 @@
-module Text.Inflections.Titleize (titleize) where
-
-import Text.Inflections.Parse.Types (Word(..))
+-- |
+-- Module      :  Text.Inflections.Titleize
+-- Copyright   :  © 2016 Justin Leitgeb
+-- License     :  MIT
+--
+-- Maintainer  :  Justin Leitgeb <justin@stackbuilders.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Conversion to titleized phrases.
 
-import Data.Char (toUpper)
+module Text.Inflections.Titleize
+  ( titleize )
+where
 
-import Prelude (String, unwords, map, ($))
+import Data.Text (Text)
+import Text.Inflections.Types
+import qualified Data.Text as T
 
--- | Capitalizes all the Words in the input 'Data.List'.
+-- | Capitalize all the Words in the input list.
+--
+-- >>> foo  <- SomeWord <$> mkWord "foo"
+-- >>> bar  <- SomeWord <$> mkAcronym "bar"
+-- >>> bazz <- SomeWord <$> mkWord "bazz"
+-- >>> titleize [foo,bar,bazz]
+-- "Foo bar Bazz"
 titleize
-  :: [Word] -- ^ List of Words, first of which will be capitalized
-  -> String -- ^ The titleized String
-titleize s = unwords $ map upperCaseWord s
-
-upperCaseWord :: Word -> String
-upperCaseWord (Word (c:cs))  = toUpper c : cs
-upperCaseWord (Word [])      = []
-upperCaseWord (Acronym s)    = s  -- Acronyms are left intact
+  :: [SomeWord] -- ^ List of words, first of which will be capitalized
+  -> Text       -- ^ The titleized 'Text'
+titleize = T.unwords . fmap (unSomeWord T.toTitle)
diff --git a/Text/Inflections/Transliterate.hs b/Text/Inflections/Transliterate.hs
--- a/Text/Inflections/Transliterate.hs
+++ b/Text/Inflections/Transliterate.hs
@@ -1,30 +1,54 @@
+-- |
+-- Module      :  Text.Inflections.Transliterate
+-- Copyright   :  © 2016 Justin Leitgeb
+-- License     :  MIT
+--
+-- Maintainer  :  Justin Leitgeb <justin@stackbuilders.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Support for transliteration.
+
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
+
 module Text.Inflections.Transliterate
     ( transliterate
-    , transliterateCustom
-    )
+    , transliterateCustom )
 where
 
-import Text.Inflections.Parameterize ( Transliterations )
-import Text.Inflections.Data (defaultMap)
-
 import Data.Char (isAscii)
-import Data.Maybe(fromMaybe)
+import Data.Text (Text)
+import Text.Inflections.Data
+import qualified Data.HashMap.Strict as M
+import qualified Data.Text           as T
 
-import qualified Data.Map as Map
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative
+#endif
 
--- |Returns a String after default approximations for changing Unicode characters
--- to a valid ASCII range are applied. If you want to supplement the default
--- approximations with your own, you should use the transliterateCustom
--- function instead of transliterate.
-transliterate :: String -> String
-transliterate = transliterateCustom "?" defaultMap
+-- | Returns a 'Text' after default approximations for changing Unicode
+-- characters to a valid ASCII range are applied. If you want to supplement
+-- the default approximations with your own, you should use the
+-- 'transliterateCustom' function instead of 'transliterate'.
+transliterate :: Text -> Text
+transliterate = transliterateCustom "?" defaultTransliterations
 
--- |Returns a String after default approximations for changing Unicode characters
--- to a valid ASCII range are applied.
-transliterateCustom :: String -> Transliterations -> String -> String
-transliterateCustom replacement ts = concatMap lookupCharTransliteration
-  where lookupCharTransliteration c =
-          if isAscii c then -- Don't bother looking up Chars in ASCII range
-            [c]
-          else
-            fromMaybe replacement (Map.lookup c ts)
+-- | Returns a 'Text' after default approximations for changing Unicode
+-- characters to a valid ASCII range are applied.
+transliterateCustom
+  :: String            -- ^ The default replacement
+  -> Transliterations  -- ^ The table of transliterations
+  -> Text              -- ^ The input
+  -> Text              -- ^ The output
+transliterateCustom replacement m txt = T.unfoldr f ("", txt)
+  where
+    f ("", t) = uncurry g <$> T.uncons t
+    f (x:xs, t) = Just (x, (xs, t))
+    g x xs =
+      if isAscii x
+        then (x, ("", xs))
+        else
+          case M.lookupDefault replacement x m of
+            ""     -> ('?', ("",xs))
+            (y:ys) -> (y,   (ys,xs))
diff --git a/Text/Inflections/Types.hs b/Text/Inflections/Types.hs
new file mode 100644
--- /dev/null
+++ b/Text/Inflections/Types.hs
@@ -0,0 +1,143 @@
+-- |
+-- Module      :  Text.Inflections.Types
+-- Copyright   :  © 2016 Justin Leitgeb
+-- License     :  MIT
+--
+-- Maintainer  :  Justin Leitgeb <justin@stackbuilders.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Types used in the library. Usually you don't need to import this module
+-- and "Text.Inflections" should be enough.
+
+{-# LANGUAGE CPP                #-}
+{-# LANGUAGE DataKinds          #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric      #-}
+{-# LANGUAGE FlexibleContexts   #-}
+{-# LANGUAGE FlexibleInstances  #-}
+{-# LANGUAGE GADTs              #-}
+{-# LANGUAGE KindSignatures     #-}
+
+module Text.Inflections.Types
+  ( Word
+  , WordType (..)
+  , mkWord
+  , mkAcronym
+  , unWord
+  , SomeWord (..)
+  , unSomeWord
+  , InflectionException (..) )
+where
+
+import Control.Monad.Catch
+import Data.Char (isAlphaNum)
+import Data.Data (Data)
+import Data.Text (Text)
+import Data.Typeable (Typeable)
+import GHC.Generics (Generic)
+import Text.Megaparsec
+import qualified Data.Text as T
+
+#if MIN_VERSION_base(4,8,0)
+import Prelude hiding (Word)
+#endif
+
+-- | Create a word from given 'Text'. The input should consist of only
+-- alpha-numeric characters (no white spaces or punctuation)
+-- 'InflectionInvalidWord' will be thrown.
+--
+-- /since 0.3.0.0/
+
+mkWord :: MonadThrow m => Text -> m (Word 'Normal)
+mkWord txt =
+  if T.all isAlphaNum txt
+    then return (Word txt)
+    else throwM (InflectionInvalidWord txt)
+
+-- | Create an acronym from given 'Text'. The input should consist of only
+-- alpha-numeric characters 'InflectionInvalidAcronym' will be thrown.
+-- Acronym is different from normal word by that it may not be transformed
+-- by inflections (also see 'unSomeWord').
+--
+-- /since 0.3.0.0/
+
+mkAcronym :: MonadThrow m => Text -> m (Word 'Acronym)
+mkAcronym txt =
+  if T.all isAlphaNum txt
+    then return (Word txt)
+    else throwM (InflectionInvalidAcronym txt)
+
+-- | A 'Text' value that should be kept whole through applied inflections.
+
+data Word (t :: WordType) = Word Text
+  deriving (Eq, Ord)
+
+instance Show (Word 'Normal) where
+  show (Word x) = "Word " ++ show x
+
+instance Show (Word 'Acronym) where
+  show (Word x) = "Acronym " ++ show x
+
+-- | A type-level tag for words.
+--
+-- /since 0.3.0.0/
+
+data WordType = Normal | Acronym
+
+-- | Get a 'Text' value from 'Word'.
+--
+-- /since 0.3.0.0/
+
+unWord :: Word t -> Text
+unWord (Word s) = s
+
+-- | An existential wrapper that allows to keep words and acronyms in single
+-- list for example. The only thing that receiver of 'SomeWord' can do is to
+-- apply 'unWord' on it, of course. This is faciliated by 'unSomeWord'.
+--
+-- /since 0.3.0.0/
+
+data SomeWord where
+  SomeWord :: (Transformable (Word t), Show (Word t)) => Word t -> SomeWord
+  -- NOTE The constraint is only needed because GHC is not smart enough
+  -- (yet?) to figure out that t cannot be anything other than Normal and
+  -- Acronym and thus all cases are already covered by the instances
+  -- provided below.
+
+instance Show SomeWord where
+  show (SomeWord w) = show w
+
+instance Eq SomeWord where
+  x == y = unSomeWord id x == unSomeWord id y
+
+-- | Extract 'Text' from 'SomeWord' and apply given function only if the
+-- word inside wasn't an acronym.
+--
+-- /since 0.3.0.0/
+
+unSomeWord :: (Text -> Text) -> SomeWord -> Text
+unSomeWord f (SomeWord w) = transform f w
+
+-- | Non public stuff.
+
+class Transformable a where
+  transform :: (Text -> Text) -> a -> Text
+
+instance Transformable (Word 'Normal) where
+  transform f = f . unWord
+
+instance Transformable (Word 'Acronym) where
+  transform _ = unWord
+
+-- | The exceptions that is thrown when parsing of input fails.
+--
+-- /since 0.3.0.0/
+
+data InflectionException
+  = InflectionParsingFailed (ParseError Char Dec)
+  | InflectionInvalidWord Text
+  | InflectionInvalidAcronym Text
+  deriving (Eq, Show, Typeable, Data, Generic)
+
+instance Exception InflectionException
diff --git a/Text/Inflections/Underscore.hs b/Text/Inflections/Underscore.hs
--- a/Text/Inflections/Underscore.hs
+++ b/Text/Inflections/Underscore.hs
@@ -1,19 +1,32 @@
-module Text.Inflections.Underscore ( underscore ) where
+-- |
+-- Module      :  Text.Inflections.Underscore
+-- Copyright   :  © 2016 Justin Leitgeb
+-- License     :  MIT
+--
+-- Maintainer  :  Justin Leitgeb <justin@stackbuilders.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Conversion to phrases separated by underscores.
 
-import Text.Inflections.Parse.Types (Word(..))
+{-# LANGUAGE OverloadedStrings #-}
 
-import Data.Char (toLower)
-import Data.List (intercalate)
+module Text.Inflections.Underscore
+  ( underscore )
+where
 
-import Prelude (String, (.), map)
+import Data.Text (Text)
+import Text.Inflections.Types
+import qualified Data.Text as T
 
--- |Turns a CamelCase string into an underscore_separated String.
+-- | Separate given words by underscores.
+--
+-- >>> foo  <- SomeWord <$> mkWord "foo"
+-- >>> bar  <- SomeWord <$> mkAcronym "bar"
+-- >>> bazz <- SomeWord <$> mkWord "bazz"
+-- >>> underscore [foo,bar,bazz]
+-- "foo_bar_bazz"
 underscore
-  :: [Word] -- ^ Input Words to separate with underscores
-  -> String -- ^ The underscored String
-underscore = intercalate "_" . map toDowncasedString
-
-
-toDowncasedString :: Word -> String
-toDowncasedString (Acronym s) = map toLower s
-toDowncasedString (Word s)    = map toLower s
+  :: [SomeWord] -- ^ Input words to separate with underscores
+  -> Text       -- ^ The underscored String
+underscore = T.intercalate "_" . fmap (unSomeWord T.toLower)
diff --git a/inflections.cabal b/inflections.cabal
--- a/inflections.cabal
+++ b/inflections.cabal
@@ -1,5 +1,5 @@
 name:                inflections
-version:             0.2.0.1
+version:             0.3.0.0
 synopsis:            Inflections library for Haskell
 description:
   Inflections provides methods for singularization, pluralization,
@@ -7,63 +7,77 @@
 
 license:             MIT
 license-file:        LICENSE
-author:              Justin Leitgeb
+author:              Justin Leitgeb <justin@stackbuilders.com>
 homepage:            https://github.com/stackbuilders/inflections-hs
-maintainer:          justin@stackbuilders.com
-copyright:           2014 Justin Leitgeb
+bug-reports:         https://github.com/stackbuilders/inflections-hs/issues
+maintainer:          Justin Leitgeb <justin@stackbuilders.com>
+copyright:           2014–2016 Justin Leitgeb
 category:            Text
 build-type:          Simple
 cabal-version:       >=1.10
+extra-source-files:  CHANGELOG.md
+                   , README.md
 
 source-repository head
   type:            git
   location:        https://github.com/stackbuilders/inflections-hs.git
 
-library
-  exposed-modules:       Text.Inflections
-                       , Text.Inflections.Parse.Types
-
-
-  other-modules:         Text.Inflections.Data
-                       , Text.Inflections.Parameterize
-                       , Text.Inflections.Humanize
-                       , Text.Inflections.Underscore
-                       , Text.Inflections.Camelize
-                       , Text.Inflections.Dasherize
-                       , Text.Inflections.Ordinal
-                       , Text.Inflections.Titleize
-                       , Text.Inflections.Transliterate
+flag dev
+  description:        Turn on development settings.
+  manual:             True
+  default:            False
 
-                       , Text.Inflections.Parse.Acronym
-                       , Text.Inflections.Parse.Parameterizable
+library
+  exposed-modules:     Text.Inflections
 
-                       , Text.Inflections.Parse.SnakeCase
-                       , Text.Inflections.Parse.CamelCase
+  other-modules:       Text.Inflections.Data
+                     , Text.Inflections.Camelize
+                     , Text.Inflections.Dasherize
+                     , Text.Inflections.Humanize
+                     , Text.Inflections.Ordinal
+                     , Text.Inflections.Parameterize
+                     , Text.Inflections.Parse.CamelCase
+                     , Text.Inflections.Parse.SnakeCase
+                     , Text.Inflections.Titleize
+                     , Text.Inflections.Transliterate
+                     , Text.Inflections.Types
+                     , Text.Inflections.Underscore
 
-  ghc-options:         -Wall
-  build-depends:       base >=4.2 && <4.10, parsec, containers
+  if flag(dev)
+    ghc-options:      -Wall -Werror
+  else
+    ghc-options:      -O2 -Wall
+  build-depends:       base         >= 4.6   && < 5.0
+                     , exceptions   >= 0.6   && < 0.9
+                     , megaparsec   >= 5.0   && < 6.0
+                     , text         >= 0.2   && < 1.3
+                     , unordered-containers >= 0.2.7 && < 0.3
   default-language:    Haskell2010
 
 test-suite test
   type: exitcode-stdio-1.0
   hs-source-dirs: test
-  main-is: Suite.hs
-  build-depends:
-      inflections
-      , base >=4.2 && <4.10
-      , test-framework
-      , HUnit
-      , QuickCheck
-      , test-framework-hunit
-      , test-framework-quickcheck2
-      , parsec
-      , containers
-  ghc-options:         -Wall
+  main-is: Spec.hs
+  build-depends:       inflections
+                     , QuickCheck   >= 2.7.6 && < 3.0
+                     , base         >= 4.6   && < 5.0
+                     , hspec        >= 2.0   && < 3.0
+                     , hspec-megaparsec >= 0.3 && < 0.4
+                     , megaparsec   >= 5.1   && < 6.0
+                     , text         >= 0.2   && < 1.3
+  if flag(dev)
+    ghc-options:      -Wall -Werror
+  else
+    ghc-options:      -O2 -Wall
   default-language:    Haskell2010
-  other-modules:
-    Text.InflectionsTest
-    Text.Inflections.HumanizeTest
-    Text.Inflections.OrdinalTest
-    Text.Inflections.Tests
-    Text.Inflections.TitleizeTest
-    Text.Inflections.UnderscoreTest
+  other-modules:       Text.Inflections.DasherizeSpec
+                     , Text.Inflections.HumanizeSpec
+                     , Text.Inflections.OrdinalSpec
+                     , Text.Inflections.ParametrizeSpec
+                     , Text.Inflections.Parse.CamelCaseSpec
+                     , Text.Inflections.Parse.SnakeCaseSpec
+                     , Text.Inflections.TitleizeSpec
+                     , Text.Inflections.TransliterateSpec
+                     , Text.Inflections.TypesSpec
+                     , Text.Inflections.UnderscoreSpec
+                     , Text.InflectionsSpec
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/test/Suite.hs b/test/Suite.hs
deleted file mode 100644
--- a/test/Suite.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module Main where
-
-import  Test.Framework (defaultMain)
-
-import qualified Text.InflectionsTest
-import qualified Text.Inflections.Tests
-import qualified Text.Inflections.UnderscoreTest
-import qualified Text.Inflections.OrdinalTest
-import qualified Text.Inflections.HumanizeTest
-import qualified Text.Inflections.TitleizeTest
-
-main :: IO ()
-main = defaultMain $ Text.InflectionsTest.tests ++
-       Text.Inflections.Tests.tests ++
-       Text.Inflections.UnderscoreTest.tests ++
-       Text.Inflections.OrdinalTest.tests ++
-       Text.Inflections.HumanizeTest.tests ++
-       Text.Inflections.TitleizeTest.tests
diff --git a/test/Text/Inflections/DasherizeSpec.hs b/test/Text/Inflections/DasherizeSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Text/Inflections/DasherizeSpec.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Inflections.DasherizeSpec
+  ( spec )
+where
+
+import Test.Hspec
+
+import Text.Inflections
+
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative
+#endif
+
+spec :: Spec
+spec = describe "dasherize" $
+  it "dasherizes a collection of words sentence" $ do
+    foo <- SomeWord <$> mkWord "foo"
+    bar <- SomeWord <$> mkWord "bar"
+    dasherize [foo,bar] `shouldBe` "foo-bar"
diff --git a/test/Text/Inflections/HumanizeSpec.hs b/test/Text/Inflections/HumanizeSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Text/Inflections/HumanizeSpec.hs
@@ -0,0 +1,27 @@
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Inflections.HumanizeSpec (spec) where
+
+import Test.Hspec
+
+import Text.Inflections
+
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative
+#endif
+
+spec :: Spec
+spec = describe "humazine" $ do
+  it "converts snake case to a human-readable string" $ do
+    employee <- SomeWord <$> mkWord "employee"
+    salary   <- SomeWord <$> mkWord "salary"
+    humanize [employee,salary] `shouldBe` "Employee salary"
+  it "turns underscores into spaces" $ do
+    employee  <- SomeWord <$> mkWord "employee"
+    has       <- SomeWord <$> mkWord "has"
+    salary    <- SomeWord <$> mkWord "salary"
+    humanize [employee, has, salary] `shouldBe` "Employee has salary"
+  it "capitalizes the first word of a sentence" $ do
+    underground <- SomeWord <$> mkWord "underground"
+    humanize [underground] `shouldBe` "Underground"
diff --git a/test/Text/Inflections/HumanizeTest.hs b/test/Text/Inflections/HumanizeTest.hs
deleted file mode 100644
--- a/test/Text/Inflections/HumanizeTest.hs
+++ /dev/null
@@ -1,28 +0,0 @@
-module Text.Inflections.HumanizeTest where
-
-import Test.HUnit hiding (Test)
-
-import Test.Framework.Providers.HUnit (testCase)
-
-import Test.Framework (Test, testGroup)
-
-import Text.Inflections (humanize)
-import Text.Inflections.Parse.Types (Word(..))
-
-{-# ANN module "HLint: ignore Use camelCase" #-}
-
-tests :: [Test]
-tests = [ testGroup "humanize"
-          [ testCase "employee_salary -> Employee salary" test_humanize1
-          , testCase "underground -> underground" test_humanize2
-          ]
-        ]
-
-----------------------------------------------------
-
-test_humanize1 :: Assertion
-test_humanize1 = "Employee salary" @?=
-                 humanize [Word "employee", Word "salary"]
-
-test_humanize2 :: Assertion
-test_humanize2 = "Underground" @?= humanize [Word "underground"]
diff --git a/test/Text/Inflections/OrdinalSpec.hs b/test/Text/Inflections/OrdinalSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Text/Inflections/OrdinalSpec.hs
@@ -0,0 +1,42 @@
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Inflections.OrdinalSpec (spec) where
+
+import Data.Text (Text)
+import Test.Hspec
+import Test.QuickCheck.Property
+import qualified Data.Text as T
+
+import Text.Inflections (ordinal, ordinalize)
+
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative
+#endif
+
+spec :: Spec
+spec = do
+  describe "ordinal" $ do
+    it "returns the ordinal for 1" $
+      ordinal (1 :: Integer) `shouldBe` "st"
+    it "returns the ordinal for 2" $
+      ordinal (2 :: Integer) `shouldBe` "nd"
+    it "returns the ordinal for 1002" $
+      ordinal (1002 :: Integer) `shouldBe` "nd"
+    it "returns the ordinal for 1003" $
+      ordinal (1003 :: Integer) `shouldBe` "rd"
+    it "returns the ordinal for -11" $
+      ordinal (-11 :: Integer) `shouldBe` "th"
+    it "returns the ordinal for -1021" $
+      ordinal (-1021 :: Integer) `shouldBe` "st"
+    it "never returns empty output" $ property $
+      property <$> not . T.null . (ordinal :: Integer -> Text)
+  describe "ordinalize" $ do
+    it "returns the full ordinal for 1" $
+      ordinalize (1 :: Integer) `shouldBe` "1st"
+    it "returns the full ordinal for -1021" $
+      ordinalize (-1021 :: Integer) `shouldBe` "-1021st"
+  it "always returns the number as prefix of the result" $
+    property $ \n ->
+      let s = show (n :: Integer)
+      in T.pack s == T.take (length s) (ordinalize n)
diff --git a/test/Text/Inflections/OrdinalTest.hs b/test/Text/Inflections/OrdinalTest.hs
deleted file mode 100644
--- a/test/Text/Inflections/OrdinalTest.hs
+++ /dev/null
@@ -1,65 +0,0 @@
-module Text.Inflections.OrdinalTest where
-
-import Test.HUnit hiding (Test)
-
-import Test.Framework.Providers.QuickCheck2 (testProperty)
-import Test.Framework.Providers.HUnit (testCase)
-
-import Test.Framework (Test, testGroup)
-
-import Text.Inflections (ordinal, ordinalize)
-
-{-# ANN module "HLint: ignore Use camelCase" #-}
-
-tests :: [Test]
-tests = [ testGroup "ordinal"
-          [ testCase "1 -> st" test_ordinal1
-          , testCase "2 -> nd" test_ordinal2
-          , testCase "1002 -> nd" test_ordinal1002
-          , testCase "1003 -> rd" test_ordinal1003
-          , testCase "-11 -> th" test_ordinalNegative11
-          , testCase "-1021 -> st" test_ordinalNegative1021
-          , testProperty "notEmpty" prop_ordinalReturnsNotEmpty
-          ]
-        , testGroup "ordinalize"
-          [ testCase "1 -> st" test_ordinalize1
-          , testCase "-1021 -> st" test_ordinalizeNegative1021
-          , testProperty "result contains number" prop_ordinalizeSamePrefix
-          ]
-        ]
-
-----------------------------------------------------
-
-test_ordinal1 :: Assertion
-test_ordinal1 = "st" @?= ordinal 1
-
-test_ordinal2 :: Assertion
-test_ordinal2 = "nd" @?= ordinal 2
-
-test_ordinal1002 :: Assertion
-test_ordinal1002 = "nd" @?= ordinal 1002
-
-test_ordinal1003 :: Assertion
-test_ordinal1003 = "rd" @?= ordinal 1003
-
-test_ordinalNegative11 :: Assertion
-test_ordinalNegative11 = "th" @?= ordinal (-11)
-
-test_ordinalNegative1021 :: Assertion
-test_ordinalNegative1021 = "st" @?= ordinal (-1021)
-
-----------------------------------------------------
-
-test_ordinalize1 :: Assertion
-test_ordinalize1 = "1st" @?= ordinalize 1
-
-test_ordinalizeNegative1021 :: Assertion
-test_ordinalizeNegative1021 = "-1021st" @?= ordinalize (-1021)
-
-----------------------------------------------------
-
-prop_ordinalReturnsNotEmpty :: Integer -> Bool
-prop_ordinalReturnsNotEmpty = not . null . ordinal
-
-prop_ordinalizeSamePrefix :: Integer -> Bool
-prop_ordinalizeSamePrefix n = show n == take (length $ show n) (ordinalize n)
diff --git a/test/Text/Inflections/ParametrizeSpec.hs b/test/Text/Inflections/ParametrizeSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Text/Inflections/ParametrizeSpec.hs
@@ -0,0 +1,39 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Inflections.ParametrizeSpec (spec) where
+
+import Data.Char (toLower)
+import Data.List (group)
+import Test.Hspec
+import Test.QuickCheck
+import Text.Inflections
+import qualified Data.Text as T
+
+spec :: Spec
+spec =
+  describe "parameterize" $ do
+    it "returns only valid characters" $ property $ \sf ->
+      T.all (`elem` (alphaNumerics ++ "-_")) $ parameterize (T.pack sf)
+    it "never returns a string beginning with a separator" $ property $ \s ->
+      let parameterized = parameterize (T.pack s)
+      in (not . T.null) parameterized ==> T.head parameterized /= '-'
+    it "never returns a string ending with a separator" $ property $ \s ->
+      let parameterized = parameterize (T.pack s) in
+      (not . T.null) parameterized ==> T.last parameterized /= '-'
+    it "returns every alphanumeric character from the input" $ property $ \s ->
+      let parameterized = parameterize (T.pack s)
+      in all (\c -> c `notElem` alphaNumerics ||
+           c `elem` (alphaNumerics ++ "-") &&
+           c `elem` T.unpack parameterized) $ map toLower s
+    it "never returns a string with a sequence of dashes" $ property $ \s ->
+      let parameterized = parameterize (T.pack s)
+      in longestSequenceOf '-' (T.unpack parameterized) <= 1
+
+longestSequenceOf :: Char -> String -> Int
+longestSequenceOf _ [] = 0
+longestSequenceOf c s =
+    if null subseqLengths then 0 else maximum subseqLengths
+  where subseqLengths = (map length . filter (\str -> head str == c) . group) s
+
+alphaNumerics :: String
+alphaNumerics = ['a'..'z'] ++ ['0'..'9']
diff --git a/test/Text/Inflections/Parse/CamelCaseSpec.hs b/test/Text/Inflections/Parse/CamelCaseSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Text/Inflections/Parse/CamelCaseSpec.hs
@@ -0,0 +1,47 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Inflections.Parse.CamelCaseSpec
+  ( spec )
+where
+
+import Test.Hspec
+import Test.Hspec.Megaparsec
+import Text.Inflections
+
+spec :: Spec
+spec =
+  describe "parseCamelCase" $ do
+    context "when given no acronyms" $ do
+      context "when first word is capitalized" $
+        it "parses CamelCase correctly" $ do
+          r <- mapM (fmap SomeWord . mkWord) ["One","Two","Three"]
+          parseCamelCase [] "OneTwoThree" `shouldParse` r
+      context "when first word is not capitalized" $
+        it "parses camelCase correctly" $ do
+          r <- mapM (fmap SomeWord . mkWord) ["one","Two","Three"]
+          parseCamelCase [] "oneTwoThree" `shouldParse` r
+      context "when there are digits in the words" $
+        it "parses CamelCase correctly" $ do
+          r <- mapM (fmap SomeWord . mkWord) ["one1two","Three3"]
+          parseCamelCase [] "one1twoThree3" `shouldParse` r
+    context "when given some acronyms" $ do
+      context "when first word is capitalized" $
+        it "parses CamelCase correctly" $ do
+          a <- mkAcronym "BOO"
+          r <- mapM (fmap SomeWord . mkWord) ["One","BOO","One"]
+          parseCamelCase [a] "OneBOOOne" `shouldParse` r
+      context "when first word is not capitalized" $
+        it "parses camelCase correctly" $ do
+          a <- mkAcronym "BOO"
+          r <- mapM (fmap SomeWord . mkWord) ["one","Two","BOO"]
+          parseCamelCase [a] "oneTwoBOO" `shouldParse` r
+      context "when there are digits in the words" $
+        it "parses CamelCase correctly" $ do
+          a <- mkAcronym "BOO"
+          r <- mapM (fmap SomeWord . mkWord) ["one1two","Three3"]
+          parseCamelCase [a] "one1twoThree3" `shouldParse` r
+      context "when a word has a suffix coinciding with a acronym" $
+        it "is still parsed correctly as a normala word" $ do
+          a <- mkAcronym "boo"
+          r <- mapM (fmap SomeWord . mkWord) ["fooboo","Bar"]
+          parseCamelCase [a] "foobooBar" `shouldParse` r
diff --git a/test/Text/Inflections/Parse/SnakeCaseSpec.hs b/test/Text/Inflections/Parse/SnakeCaseSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Text/Inflections/Parse/SnakeCaseSpec.hs
@@ -0,0 +1,35 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Inflections.Parse.SnakeCaseSpec
+  ( spec )
+where
+
+import Test.Hspec
+import Test.Hspec.Megaparsec
+import Text.Inflections
+
+spec :: Spec
+spec =
+  describe "parseSnakeCase" $ do
+    context "when given no acronyms" $ do
+      it "parses snake_case correctly" $ do
+        r <- mapM (fmap SomeWord . mkWord) ["OneTwo","Three"]
+        parseSnakeCase [] "OneTwo_Three" `shouldParse` r
+      it "handles digits in the words correctly" $ do
+        r <- mapM (fmap SomeWord . mkWord) ["one4a","two00"]
+        parseSnakeCase [] "one4a_two00" `shouldParse` r
+    context "when given some acronyms" $ do
+      it "parses snake_case correctly" $ do
+        a <- mkAcronym "BOO"
+        r <- mapM (fmap SomeWord . mkWord) ["BOO","one","one"]
+        parseSnakeCase [a] "BOO_one_one" `shouldParse` r
+      context "when acronym happens to be a prefix of other word" $
+        it "parses the word correctly" $ do
+          a <- mkAcronym "BOO"
+          r <- mapM (fmap SomeWord . mkWord) ["one","BOOtwo"]
+          parseSnakeCase [a] "one_BOOtwo" `shouldParse` r
+      context "when acronym happens to be a suffix of other word" $
+        it "parses the word correctly" $ do
+          a <- mkAcronym "BOO"
+          r <- mapM (fmap SomeWord . mkWord) ["oneBOO","two"]
+          parseSnakeCase [a] "oneBOO_two" `shouldParse` r
diff --git a/test/Text/Inflections/Tests.hs b/test/Text/Inflections/Tests.hs
deleted file mode 100644
--- a/test/Text/Inflections/Tests.hs
+++ /dev/null
@@ -1,107 +0,0 @@
-module Text.Inflections.Tests where
-
-import Test.HUnit hiding (Test)
-
-import Test.Framework.Providers.QuickCheck2 (testProperty)
-import Test.Framework.Providers.HUnit (testCase)
-
-import Test.QuickCheck
-
-import Test.Framework (Test, testGroup)
-
-import Data.List (group)
-import Data.Char (toLower)
-
-import Text.Inflections
-import Text.Inflections.Parse.Types (Word(..))
-
-{-# ANN module "HLint: ignore Use camelCase" #-}
-
-tests :: [Test]
-tests = [testGroup "dasherize"
-         [ testCase "foo bar -> foo-bar" test_dasherize1
-         ],
-
-         testGroup "transliterate"
-         [ testCase "Without substitutions" test_correctTransliterationWithoutSubs
-         , testCase "With substitutions" test_correctTransliterationWithSubs
-         , testCase "Missing subs" test_correctTransliterationMissingSubs
-         ],
-
-         testGroup "parameterize"
-         [ testProperty "Contains only valid chars"
-                        prop_parameterize1
-         , testProperty "Does not begin with a separator character"
-                         prop_parameterize2
-         , testProperty "Does not end in a separator character"
-                         prop_parameterize3
-         , testProperty "All alphanumerics in input exist in output"
-                        prop_parameterize4
-         , testProperty "Doesn't have subsequences of more than one hyphen"
-                        prop_parameterize5
-         ]
-        ]
-
-test_correctTransliterationWithoutSubs :: Assertion
-test_correctTransliterationWithoutSubs =
-    transliterate "this is a test" @?= "this is a test"
-
-test_correctTransliterationWithSubs :: Assertion
-test_correctTransliterationWithSubs =
-    transliterate "Feliz año nuevo" @?= "Feliz ano nuevo"
-
-test_correctTransliterationMissingSubs :: Assertion
-test_correctTransliterationMissingSubs =
-    transliterate "Have a ❤ ñ!" @?= "Have a ? n!"
-
-fromRight :: Either a b -> b
-fromRight (Left _)  =
-    error "Either.Unwrap.fromRight: Argument takes form 'Left _'"
-fromRight (Right x) = x
-
-isRight :: Either a b -> Bool
-isRight (Left _)  = False
-isRight (Right _) = True
-
-test_dasherize1 :: Assertion
-test_dasherize1 = "foo-bar" @?= dasherize [Word "foo", Word "bar"]
-
-prop_parameterize1 :: String -> Bool
-prop_parameterize1 sf = all (`elem` (alphaNumerics ++ "-_")) $
-                        parameterize sf
-
-prop_parameterize2 :: String -> Property
-prop_parameterize2 s =
-    (not . null) parameterized ==> head parameterized /= '-'
-    where parameterized = parameterize s
-
-prop_parameterize3 :: String -> Property
-prop_parameterize3 s =
-    (not . null) parameterized ==> last parameterized /= '-'
-    where parameterized = parameterize s
-
-prop_parameterize4 :: String -> Bool
-prop_parameterize4 s = all (\c -> c `notElem` alphaNumerics ||
-                              c `elem` (alphaNumerics ++ "-") &&
-                              c `elem` parameterized) $ map toLower s
-    where parameterized = parameterize s
-
-prop_parameterize5 :: String -> Bool
-prop_parameterize5 s = longestSequenceOf '-' parameterized <= 1
-    where parameterized = parameterize s
-
-
--- Helper functions and shared tests
-
-longestSequenceOf :: Char -> String -> Int
-longestSequenceOf _ [] = 0
-longestSequenceOf c s =
-    if null subseqLengths then 0 else maximum subseqLengths
-
-  where subseqLengths = (map length . filter (\str -> head str == c) . group) s
-
-numMatching :: Eq a => a -> [a] -> Int
-numMatching char str = length $ filter (== char) str
-
-alphaNumerics :: String
-alphaNumerics = ['a'..'z'] ++ ['0'..'9']
diff --git a/test/Text/Inflections/TitleizeSpec.hs b/test/Text/Inflections/TitleizeSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Text/Inflections/TitleizeSpec.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Inflections.TitleizeSpec (spec) where
+
+import Test.Hspec
+import Text.Inflections
+
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative
+#endif
+
+spec :: Spec
+spec = describe "titleize" $ do
+  it "converts two words to title case" $ do
+    employee <- SomeWord <$> mkWord "Employee"
+    salary   <- SomeWord <$> mkWord "Salary"
+    titleize [employee,salary] `shouldBe` "Employee Salary"
+  it "converts one word to title case" $ do
+    underground <- SomeWord <$> mkWord "underground"
+    titleize [underground] `shouldBe` "Underground"
diff --git a/test/Text/Inflections/TitleizeTest.hs b/test/Text/Inflections/TitleizeTest.hs
deleted file mode 100644
--- a/test/Text/Inflections/TitleizeTest.hs
+++ /dev/null
@@ -1,28 +0,0 @@
-module Text.Inflections.TitleizeTest where
-
-import Test.HUnit hiding (Test)
-
-import Test.Framework.Providers.HUnit (testCase)
-
-import Test.Framework (Test, testGroup)
-
-import Text.Inflections (titleize)
-import Text.Inflections.Parse.Types (Word(..))
-
-{-# ANN module "HLint: ignore Use camelCase" #-}
-
-tests :: [Test]
-tests = [ testGroup "titleize"
-          [ testCase "employee_salary -> Employee Salary" test_titleize1
-          , testCase "underground -> Underground" test_titleize2
-          ]
-        ]
-
-----------------------------------------------------
-
-test_titleize1 :: Assertion
-test_titleize1 = "Employee Salary" @?=
-                 titleize [Word "Employee", Word "Salary"]
-
-test_titleize2 :: Assertion
-test_titleize2 = "Underground" @?= titleize [Word "underground"]
diff --git a/test/Text/Inflections/TransliterateSpec.hs b/test/Text/Inflections/TransliterateSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Text/Inflections/TransliterateSpec.hs
@@ -0,0 +1,29 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Inflections.TransliterateSpec
+  ( spec )
+where
+
+import Test.Hspec
+import Text.Inflections (transliterate)
+
+spec :: Spec
+spec = describe "transliterate" $ do
+   withSubstitutions
+   withoutSubstitutions
+   missingSubstitutions
+
+withoutSubstitutions :: Spec
+withoutSubstitutions =
+  it "transliterates without subsitutions" $
+     transliterate "this is a test" `shouldBe` "this is a test"
+
+withSubstitutions :: Spec
+withSubstitutions =
+  it "transliterates with substitution" $
+     transliterate "Feliz año nuevo" `shouldBe` "Feliz ano nuevo"
+
+missingSubstitutions :: Spec
+missingSubstitutions =
+  it "transliterates with missing substitutions" $
+    transliterate "Have a ❤ ñ!" `shouldBe` "Have a ? n!"
diff --git a/test/Text/Inflections/TypesSpec.hs b/test/Text/Inflections/TypesSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Text/Inflections/TypesSpec.hs
@@ -0,0 +1,51 @@
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Inflections.TypesSpec
+  ( spec )
+where
+
+import Test.Hspec
+import Text.Inflections
+import qualified Data.Text as T
+
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative
+#endif
+
+spec :: Spec
+spec = do
+  describe "mkWord" $ do
+    context "when provided a correct Text value" $
+      it "creates a normal word" $ do
+        x <- mkWord "foo"
+        unWord x `shouldBe` "foo"
+    context "when provided an incorrect Text value" $
+      it "throws the correct exception" $ do
+        mkWord "foo bar" `shouldThrow` (== InflectionInvalidWord "foo bar")
+        mkWord "foo_" `shouldThrow` (== InflectionInvalidWord "foo_")
+        mkWord "&$?%" `shouldThrow` (== InflectionInvalidWord "&$?%")
+  describe "mkAcronym" $ do
+    context "when provided a correct Text value" $
+      it "creates an acronym" $ do
+        x <- mkAcronym "foo"
+        unWord x `shouldBe` "foo"
+    context "when providde an incorrect Text value" $
+      it "throws the correct exception" $ do
+        mkAcronym "foo bar" `shouldThrow` (== InflectionInvalidAcronym "foo bar")
+        mkAcronym "foo_" `shouldThrow` (== InflectionInvalidAcronym "foo_")
+        mkAcronym "&$?%" `shouldThrow` (== InflectionInvalidAcronym "&$?%")
+  describe "unWord" $
+    it "extracts the inner Text value" $ do
+      (unWord <$> mkWord "foo")    `shouldReturn` "foo"
+      (unWord <$> mkWord "bar")    `shouldReturn` "bar"
+      (unWord <$> mkAcronym "baz") `shouldReturn` "baz"
+  describe "unSomeWord" $ do
+    context "when inner value is a normal word" $
+      it "Text is extracted and the given function applied" $ do
+        x <- SomeWord <$> mkWord "word"
+        unSomeWord T.toUpper x `shouldBe` "WORD"
+    context "when inner value is an acronym" $
+      it "Text is extracted, but the function is not applied" $ do
+        x <- SomeWord <$> mkAcronym "acronym"
+        unSomeWord T.toUpper x `shouldBe` "acronym"
diff --git a/test/Text/Inflections/UnderscoreSpec.hs b/test/Text/Inflections/UnderscoreSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Text/Inflections/UnderscoreSpec.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Inflections.UnderscoreSpec (spec) where
+
+import Test.Hspec
+import Text.Inflections
+
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative
+#endif
+
+spec :: Spec
+spec = describe "underscore" $
+  it "converts a word list to snake case" $ do
+    test <- SomeWord <$> mkWord "test"
+    this <- SomeWord <$> mkWord "this"
+    underscore [test, this] `shouldBe` "test_this"
diff --git a/test/Text/Inflections/UnderscoreTest.hs b/test/Text/Inflections/UnderscoreTest.hs
deleted file mode 100644
--- a/test/Text/Inflections/UnderscoreTest.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-module Text.Inflections.UnderscoreTest where
-
-import Test.HUnit hiding (Test)
-
-import Test.Framework.Providers.HUnit (testCase)
-
-import Test.Framework (Test, testGroup)
-
-import Text.Inflections (underscore)
-import Text.Inflections.Parse.Types (Word(..))
-
-{-# ANN module "HLint: ignore Use camelCase" #-}
-
-tests :: [Test]
-tests = [testGroup "underscore"
-          [ testCase "testThis -> test_this" test_underscore
-          ]
-        ]
-
-test_underscore :: Assertion
-test_underscore = "test_this" @?= underscore [Word "test", Word "this"]
diff --git a/test/Text/InflectionsSpec.hs b/test/Text/InflectionsSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Text/InflectionsSpec.hs
@@ -0,0 +1,39 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.InflectionsSpec (spec) where
+
+import Test.Hspec
+import Test.QuickCheck
+import Text.Inflections
+
+spec :: Spec
+spec = do
+
+  describe "toUnderscore" $ do
+    it "converts camel case to snake case" $
+      toUnderscore "camelCasedText" `shouldBe` Right "camel_cased_text"
+    it "converts camel case to snake case with numbers" $
+      toUnderscore "ipv4Address" `shouldBe` Right "ipv4_address"
+
+  describe "toDashed" $
+    it "converts camel case to dashed" $
+      toDashed "camelCasedText" `shouldBe` Right "camel-cased-text"
+
+  describe "toCamelCased" $ do
+    context "when the first argument is False" $
+      it "converts snake case to camel case" $
+        toCamelCased False "underscored_text" `shouldBe` Right "underscoredText"
+    context "when the first argument is True" $
+      it "converts snake case to camel case with the first word capitalized" $
+        toCamelCased True "underscored_text" `shouldBe` Right "UnderscoredText"
+
+  describe "betterThrow" $ do
+    context "when given a parse error" $
+      it "throws the correct exception" $
+        property $ \err ->
+          betterThrow (Left err) `shouldThrow`
+            (== InflectionParsingFailed err)
+    context "when given a value in Right" $
+      it "returns the value" $
+        property $ \x ->
+          betterThrow (Right x) `shouldReturn` (x :: Int)
diff --git a/test/Text/InflectionsTest.hs b/test/Text/InflectionsTest.hs
deleted file mode 100644
--- a/test/Text/InflectionsTest.hs
+++ /dev/null
@@ -1,32 +0,0 @@
-module Text.InflectionsTest where
-
-import Test.HUnit hiding (Test)
-
-import Test.Framework.Providers.HUnit (testCase)
-
-import Test.Framework (Test, testGroup)
-
-import Text.Inflections (toUnderscore, toDashed, toCamelCased)
-
-{-# ANN module "HLint: ignore Use camelCase" #-}
-
-tests :: [Test]
-tests = [ testGroup "toUnderscore"
-          [ testCase "camelCasedText -> camel_cased_text" test_to_underscore
-          ]
-        , testGroup "toDashed"
-          [ testCase "camelCasedText -> camel-cased-text" test_to_dashed
-          ]
-        , testGroup "toCamelCased"
-          [ testCase "underscored_text -> camelCasedText" test_to_camel_cased
-          ]
-        ]
-
-test_to_underscore :: Assertion
-test_to_underscore = "camel_cased_text" @?= toUnderscore "camelCasedText"
-
-test_to_dashed :: Assertion
-test_to_dashed = "camel-cased-text" @?= toDashed "camelCasedText"
-
-test_to_camel_cased :: Assertion
-test_to_camel_cased = "underscoredText" @?= toCamelCased False "underscored_text"
