diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2013 Mikhail Vorozhtsov
+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.
+- Neither the names of the copyright owners nor the names of the 
+  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
+OWNER 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/src/Text/Ascii.hs b/src/Text/Ascii.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Ascii.hs
@@ -0,0 +1,623 @@
+{-# LANGUAGE UnicodeSyntax #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+
+-- | ASCII utility functions.
+module Text.Ascii
+  (
+  -- * ASCII checks
+    IsAscii(..)
+  , isAscii
+  , Ascii
+  , maybeAscii
+  , ascii
+  -- * Character properties
+  , isControl
+  , isPrintable
+  , isWhiteSpace
+  , isSpaceOrTab
+  , isLower
+  , isUpper
+  , toLower
+  , toUpper
+  , isAlpha
+  , isDigit
+  , isNzDigit
+  , isAlphaNum
+  , fromDigit
+  , fromNzDigit
+  , unsafeFromDigit
+  , isOctDigit
+  , isNzOctDigit
+  , fromOctDigit
+  , fromNzOctDigit
+  , unsafeFromOctDigit
+  , isUpHexDigit
+  , isNzUpHexDigit
+  , fromUpHexDigit
+  , fromNzUpHexDigit
+  , unsafeFromUpHexDigit
+  , isLowHexDigit
+  , isNzLowHexDigit
+  , fromLowHexDigit
+  , fromNzLowHexDigit
+  , unsafeFromLowHexDigit
+  , isHexDigit
+  , isNzHexDigit
+  , fromHexDigit
+  , fromNzHexDigit
+  , unsafeFromHexDigit
+  -- * Byte properties
+  , isControl8
+  , isPrintable8
+  , isWhiteSpace8
+  , isSpaceOrTab8
+  , isLower8
+  , isUpper8
+  , toLower8
+  , toUpper8
+  , isAlpha8
+  , isDigit8
+  , isNzDigit8
+  , isAlphaNum8
+  , fromDigit8
+  , fromNzDigit8
+  , unsafeFromDigit8
+  , isOctDigit8
+  , isNzOctDigit8
+  , fromOctDigit8
+  , fromNzOctDigit8
+  , unsafeFromOctDigit8
+  , isUpHexDigit8
+  , isNzUpHexDigit8
+  , fromUpHexDigit8
+  , fromNzUpHexDigit8
+  , unsafeFromUpHexDigit8
+  , isLowHexDigit8
+  , isNzLowHexDigit8
+  , fromLowHexDigit8
+  , fromNzLowHexDigit8
+  , unsafeFromLowHexDigit8
+  , isHexDigit8
+  , isNzHexDigit8
+  , fromHexDigit8
+  , fromNzHexDigit8
+  , unsafeFromHexDigit8
+  ) where
+
+import Data.Checked
+import Data.Char (ord, chr)
+import Data.Word (Word8)
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.Text as TS
+import qualified Data.Text.Lazy as TL
+
+data IsAscii = IsAscii
+
+instance Property IsAscii Word8 where
+  holds _ = (< 128)
+  {-# INLINE holds #-}
+
+instance Property IsAscii BS.ByteString where
+  holds _ = BS.all isAscii
+  {-# INLINE holds #-}
+
+instance Property IsAscii BL.ByteString where
+  holds _ = BL.all isAscii
+  {-# INLINE holds #-}
+
+instance Property IsAscii Char where
+  holds _ = (< 128) . ord
+  {-# INLINE holds #-}
+
+instance Property IsAscii α ⇒ Property IsAscii [α] where
+  holds _ = all isAscii
+  {-# INLINE holds #-}
+
+instance Property IsAscii TS.Text where
+  holds _ = TS.all isAscii
+  {-# INLINE holds #-}
+
+instance Property IsAscii TL.Text where
+  holds _ = TL.all isAscii
+  {-# INLINE holds #-}
+
+isAscii ∷ Property IsAscii v ⇒ v → Bool 
+isAscii = holds IsAscii
+{-# INLINE isAscii #-}
+
+type Ascii α = Checked IsAscii α
+
+-- | Map a character to its ASCII encoding if possible, otherwise
+--   return 'Nothing'.
+maybeAscii ∷ Char → Maybe Word8
+maybeAscii c | isAscii c = Just $ ascii c
+             | otherwise = Nothing
+{-# INLINABLE maybeAscii #-}
+
+-- | Encode an ASCII character. No checks is performed.
+ascii ∷ Char → Word8
+ascii = fromIntegral . ord
+{-# INLINE ascii #-}
+
+-- | Test if a character is an ASCII control character.
+isControl ∷ Char → Bool
+isControl c = w < 32 || w == 127
+  where w = ord c
+{-# INLINE isControl #-}
+
+-- | Test if a character is an ASCII printable character.
+isPrintable ∷ Char → Bool
+isPrintable c = w >= 32 && w < 127
+  where w = ord c
+{-# INLINE isPrintable #-}
+
+-- | Test if a character is an ASCII whitespace character.
+isWhiteSpace ∷ Char → Bool
+isWhiteSpace c = c == ' ' || (w >= 9 && w <= 13)
+  where w = ord c
+{-# INLINE isWhiteSpace #-}
+
+-- | Test if a character is the SPACE or the TAB character.
+isSpaceOrTab ∷ Char → Bool
+isSpaceOrTab c = c == ' ' || c == '\t'
+{-# INLINE isSpaceOrTab #-}
+
+-- | Test if a character is an ASCII lower-case letter.
+isLower ∷ Char → Bool
+isLower c = c >= 'a' && c <= 'z'
+{-# INLINE isLower #-}
+
+-- | Test if a character is an ASCII upper-case letter.
+isUpper ∷ Char → Bool
+isUpper c = c >= 'A' && c <= 'Z'
+{-# INLINE isUpper #-}
+
+-- | Map lower-case ASCII letters to the corresponding upper-case letters,
+--   leaving other characters as is.
+toLower ∷ Char → Char
+toLower c | isUpper c = chr (ord c + 32)
+          | otherwise = c
+{-# INLINABLE toLower #-}
+
+-- | Map upper-case ASCII letters to the corresponding lower-case letters,
+--   leaving other characters as is.
+toUpper ∷ Char → Char
+toUpper c | isLower c = chr (ord c - 32)
+          | otherwise = c
+{-# INLINABLE toUpper #-}
+
+-- | Test if a character is an ASCII letter.
+isAlpha ∷ Char → Bool
+isAlpha c = isUpper c || isLower c
+{-# INLINABLE isAlpha #-}
+
+-- | Test if a character is a decimal digit (/'0' ... '9'/).
+isDigit ∷ Char → Bool
+isDigit c = c >= '0' && c <= '9'
+{-# INLINE isDigit #-}
+
+-- | Test if a character is a non-zero decimal digit (/'1' ... '9'/).
+isNzDigit ∷ Char → Bool
+isNzDigit c = c >= '1' && c <= '9'
+{-# INLINE isNzDigit #-}
+
+-- | Test if a character is either an ASCII letter or a decimal digit.
+isAlphaNum ∷ Char → Bool
+isAlphaNum c = isDigit c || isAlpha c
+{-# INLINABLE isAlphaNum #-}
+
+-- | Map a decimal digit to the corresponding number. Return 'Nothing' on
+--   other inputs.
+fromDigit ∷ Num a ⇒ Char → Maybe a
+fromDigit c | isDigit c = Just $ unsafeFromDigit c
+            | otherwise = Nothing
+{-# INLINABLE fromDigit #-}
+
+-- | Map non-zero decimal digits to the corresponding numbers. Return
+--   'Nothing' on other inputs.
+fromNzDigit ∷ Num a ⇒ Char → Maybe a
+fromNzDigit c | isNzDigit c = Just $ unsafeFromDigit c
+              | otherwise   = Nothing
+{-# INLINABLE fromNzDigit #-}
+
+-- | Map decimal digits to the corresponding numbers. No checks is performed.
+unsafeFromDigit ∷ Num a ⇒ Char → a
+unsafeFromDigit c = fromIntegral (ord c - ord '0')
+{-# INLINE unsafeFromDigit #-}
+
+-- | Test if a character is an octal digit (/'0' ... '7'/).
+isOctDigit ∷ Char → Bool
+isOctDigit c = c >= '0' && c <= '7'
+{-# INLINE isOctDigit #-}
+
+-- | Test if a character is a non-zero octal digit (/'1' ... '7'/).
+isNzOctDigit ∷ Char → Bool
+isNzOctDigit c = c >= '1' && c <= '7'
+{-# INLINE isNzOctDigit #-}
+
+-- | Map octal digits to the corresponding numbers. Return 'Nothing' on
+--   other inputs.
+fromOctDigit ∷ Num a ⇒ Char → Maybe a
+fromOctDigit c | isOctDigit c = Just $ unsafeFromOctDigit c
+               | otherwise    = Nothing
+{-# INLINABLE fromOctDigit #-}
+
+-- | Map non-zero octal digits to the corresponding numbers. Return
+--   'Nothing' on other inputs.
+fromNzOctDigit ∷ Num a ⇒ Char → Maybe a
+fromNzOctDigit c | isNzOctDigit c = Just $ unsafeFromOctDigit c
+                 | otherwise      = Nothing
+{-# INLINABLE fromNzOctDigit #-}
+
+-- | Map octal digits to the corresponding numbers. No checks is performed.
+unsafeFromOctDigit ∷ Num a ⇒ Char → a
+unsafeFromOctDigit = unsafeFromDigit
+{-# INLINE unsafeFromOctDigit #-}
+
+isLowAF ∷ Char → Bool
+isLowAF c = c >= 'a' && c <= 'f'
+{-# INLINE isLowAF #-}
+
+fromLowAF ∷ Num a ⇒ Char → a
+fromLowAF c = fromIntegral (ord c - ord 'a' + 10)
+{-# INLINE fromLowAF #-}
+
+-- | Test if a character is a lower-case hexadecimal digit
+--   (/'0' ... '9'/ or /'a' ... 'f'/).
+isLowHexDigit ∷ Char → Bool
+isLowHexDigit c = isDigit c || isLowAF c
+{-# INLINABLE isLowHexDigit #-}
+
+-- | Test if a character is a non-zero lower-case hexadecimal digit
+--   (/'1' ... '9'/ or /'a' ... 'f'/).
+isNzLowHexDigit ∷ Char → Bool
+isNzLowHexDigit c = isNzDigit c || isLowAF c
+{-# INLINABLE isNzLowHexDigit #-}
+
+-- | Map lower-case hexadecimal digits to the corresponding numbers.
+--   Return 'Nothing' on other inputs.
+fromLowHexDigit ∷ Num a ⇒ Char → Maybe a
+fromLowHexDigit c | isDigit c = Just $ unsafeFromDigit c
+                  | isLowAF c = Just $ fromLowAF c
+                  | otherwise = Nothing
+{-# INLINABLE fromLowHexDigit #-}
+
+-- | Map non-zero lower-case hexadecimal digits to the corresponding numbers.
+--   Return 'Nothing' on other inputs.
+fromNzLowHexDigit ∷ Num a ⇒ Char → Maybe a
+fromNzLowHexDigit c | isNzDigit c = Just $ unsafeFromDigit c
+                    | isLowAF c   = Just $ fromLowAF c
+                    | otherwise   = Nothing
+{-# INLINABLE fromNzLowHexDigit #-}
+
+-- | Map lower-case hexadecimal digits to the corresponding numbers.
+--   No checks is performed.
+unsafeFromLowHexDigit ∷ Num a ⇒ Char → a
+unsafeFromLowHexDigit c | c < 'a'   = unsafeFromDigit c
+                        | otherwise = fromLowAF c
+{-# INLINE unsafeFromLowHexDigit #-}
+
+isUpAF ∷ Char → Bool
+isUpAF c = c >= 'A' && c <= 'F'
+{-# INLINE isUpAF #-}
+
+fromUpAF ∷ Num a ⇒ Char → a
+fromUpAF c = fromIntegral (ord c - ord 'A' + 10)
+{-# INLINE fromUpAF #-}
+
+-- | Test if a character is an upper-case hexadecimal digit
+--   (/'0' ... '9'/ or /'A' ... 'F'/).
+isUpHexDigit ∷ Char → Bool
+isUpHexDigit c = isDigit c || isUpAF c
+{-# INLINABLE isUpHexDigit #-}
+
+-- | Test if a character is a non-zero upper-case hexadecimal digit
+--   (/'1' ... '9'/ or /'A' ... 'F'/).
+isNzUpHexDigit ∷ Char → Bool
+isNzUpHexDigit c = isNzDigit c || isUpAF c
+{-# INLINABLE isNzUpHexDigit #-}
+
+-- | Map upper-case hexadecimal digits to the corresponding numbers.
+--   Return 'Nothing' on other inputs.
+fromUpHexDigit ∷ Num a ⇒ Char → Maybe a
+fromUpHexDigit c | isDigit c = Just $ unsafeFromDigit c
+                 | isUpAF c  = Just $ fromUpAF c
+                 | otherwise = Nothing
+{-# INLINABLE fromUpHexDigit #-}
+
+-- | Map non-zero upper-case hexadecimal digits to the corresponding numbers.
+--   Return 'Nothing' on other inputs.
+fromNzUpHexDigit ∷ Num a ⇒ Char → Maybe a
+fromNzUpHexDigit c | isNzDigit c = Just $ unsafeFromDigit c
+                   | isUpAF c    = Just $ fromUpAF c
+                   | otherwise   = Nothing
+{-# INLINABLE fromNzUpHexDigit #-}
+
+-- | Map upper-case hexadecimal digits to the corresponding numbers.
+--   No checks is performed.
+unsafeFromUpHexDigit ∷ Num a ⇒ Char → a
+unsafeFromUpHexDigit c | c < 'A'   = unsafeFromDigit c
+                       | otherwise = fromUpAF c
+{-# INLINE unsafeFromUpHexDigit #-}
+
+-- | Test if a character is a hexadecimal digit
+--   (/'0' ... '9'/ or /'a' ... 'f'/ or /'A' ... 'F'/).
+isHexDigit ∷ Char → Bool
+isHexDigit c = isDigit c || isUpAF c || isLowAF c
+{-# INLINABLE isHexDigit #-}
+
+-- | Test if a character is a non-zero hexadecimal digit
+--   (/'1' ... '9'/ or /'a' ... 'f'/ or /'A' ... 'F'/).
+isNzHexDigit ∷ Char → Bool
+isNzHexDigit c = isNzDigit c || isUpAF c || isLowAF c
+{-# INLINABLE isNzHexDigit #-}
+
+-- | Map hexadecimal digits to the corresponding numbers.
+--   Return 'Nothing' on other inputs.
+fromHexDigit ∷ Num a ⇒ Char → Maybe a
+fromHexDigit c | isDigit c = Just $ unsafeFromDigit c
+               | isUpAF c  = Just $ fromUpAF c
+               | isLowAF c = Just $ fromLowAF c
+               | otherwise = Nothing
+{-# INLINABLE fromHexDigit #-}
+
+-- | Map non-zero hexadecimal digits to the corresponding numbers.
+--   Return 'Nothing' on other inputs.
+fromNzHexDigit ∷ Num a ⇒ Char → Maybe a
+fromNzHexDigit c | isNzDigit c = Just $ unsafeFromDigit c
+                 | isUpAF c    = Just $ fromUpAF c
+                 | isLowAF c   = Just $ fromLowAF c
+                 | otherwise   = Nothing
+{-# INLINABLE fromNzHexDigit #-}
+
+-- | Map hexadecimal digits to the corresponding numbers. No checks is
+--   performed.
+unsafeFromHexDigit ∷ Num a ⇒ Char → a
+unsafeFromHexDigit c | c < 'A'   = unsafeFromDigit c
+                     | c < 'a'   = fromUpAF c
+                     | otherwise = fromLowAF c
+{-# INLINE unsafeFromHexDigit #-}
+
+-- | Test if a byte is the encoding of an ASCII control character.
+isControl8 ∷ Word8 → Bool
+isControl8 w = w < 32 || w == 127
+{-# INLINE isControl8 #-}
+
+-- | Test if a byte is the encoding of an ASCII printable character.
+isPrintable8 ∷ Word8 → Bool
+isPrintable8 w = w >= 32 && w < 127
+{-# INLINE isPrintable8 #-}
+
+-- | Test if a byte is the encoding of an ASCII whitespace character.
+isWhiteSpace8 ∷ Word8 → Bool
+isWhiteSpace8 w = w == ascii ' ' || w >= 9 && w <= 13
+{-# INLINE isWhiteSpace8 #-}
+
+-- | Test if a byte is the encoding of the SPACE or the TAB character.
+isSpaceOrTab8 ∷ Word8 → Bool
+isSpaceOrTab8 w = w == ascii ' ' || w == ascii '\t'
+{-# INLINE isSpaceOrTab8 #-}
+
+-- | Test if a byte is the encoding of an ASCII lower-case letter.
+isLower8 ∷ Word8 → Bool
+isLower8 w = w >= ascii 'a' && w <= ascii 'z'
+{-# INLINE isLower8 #-}
+
+-- | Test if a byte is the encoding of an ASCII upper-case letter.
+isUpper8 ∷ Word8 → Bool
+isUpper8 w = w >= ascii 'A' && w <= ascii 'Z'
+{-# INLINE isUpper8 #-}
+
+-- | Map the encodings of lower-case ASCII letters to the encodings of
+--   the corresponding upper-case letters, leaving other bytes as is.
+toLower8 ∷ Word8 → Word8
+toLower8 w | isUpper8 w = w + 32
+           | otherwise  = w
+{-# INLINABLE toLower8 #-}
+
+-- | Map the encodings of upper-case ASCII letters to the encodings of
+--   the corresponding lower-case letters, leaving other bytes as is.
+toUpper8 ∷ Word8 → Word8
+toUpper8 w | isLower8 w = w - 32
+           | otherwise  = w
+{-# INLINABLE toUpper8 #-}
+
+-- | Test if a byte is the encoding of an ASCII letter.
+isAlpha8 ∷ Word8 → Bool
+isAlpha8 w = isUpper8 w || isLower8 w
+{-# INLINABLE isAlpha8 #-}
+
+-- | Test if a byte is the encoding of a decimal digit (/'0' ... '9'/).
+isDigit8 ∷ Word8 → Bool
+isDigit8 w = w >= ascii '0' && w <= ascii '9'
+{-# INLINE isDigit8 #-}
+
+-- | Test if a byte is the encoding of a non-zero decimal digit
+--   (/'1' ... '9'/).
+isNzDigit8 ∷ Word8 → Bool
+isNzDigit8 w = w >= ascii '1' && w <= ascii '9'
+{-# INLINE isNzDigit8 #-}
+
+-- | Test if a byte is the encoding of either an ASCII letter
+--   or a decimal digit.
+isAlphaNum8 ∷ Word8 → Bool
+isAlphaNum8 w = isDigit8 w || isAlpha8 w
+{-# INLINABLE isAlphaNum8 #-}
+
+-- | Map the encoding of a decimal digit to the corresponding number.
+--   Return 'Nothing' on other inputs.
+fromDigit8 ∷ Num a ⇒ Word8 → Maybe a
+fromDigit8 w | isDigit8 w = Just $ unsafeFromDigit8 w
+             | otherwise  = Nothing
+{-# INLINABLE fromDigit8 #-}
+
+-- | Map the encoding of a non-zero decimal digit to the corresponding number.
+--   Return 'Nothing' on other inputs.
+fromNzDigit8 ∷ Num a ⇒ Word8 → Maybe a
+fromNzDigit8 w | isNzDigit8 w = Just $ unsafeFromDigit8 w
+               | otherwise    = Nothing
+{-# INLINABLE fromNzDigit8 #-}
+
+-- | Map the encoding of a decimal digit to the corresponding number.
+--   No checks is performed.
+unsafeFromDigit8 ∷ Num a ⇒ Word8 → a
+unsafeFromDigit8 w = fromIntegral (w - ascii '0')
+{-# INLINE unsafeFromDigit8 #-}
+
+-- | Test if a byte is the encoding of an octal digit (/'0' ... '7'/).
+isOctDigit8 ∷ Word8 → Bool
+isOctDigit8 w = w >= ascii '0' && w <= ascii '7'
+{-# INLINE isOctDigit8 #-}
+
+-- | Test if a byte is the encoding of a non-zero octal digit
+--   (/'1' ... '7'/).
+isNzOctDigit8 ∷ Word8 → Bool
+isNzOctDigit8 w = w >= ascii '1' && w <= ascii '7'
+{-# INLINE isNzOctDigit8 #-}
+
+-- | Map the encoding of an octal digit to the corresponding number.
+--   Return 'Nothing' on other inputs.
+fromOctDigit8 ∷ Num a ⇒ Word8 → Maybe a
+fromOctDigit8 w | isOctDigit8 w = Just $ unsafeFromOctDigit8 w
+                | otherwise     = Nothing
+{-# INLINABLE fromOctDigit8 #-}
+
+-- | Map the encoding of a non-zero octal digit to the corresponding number.
+--   Return 'Nothing' on other inputs.
+fromNzOctDigit8 ∷ Num a ⇒ Word8 → Maybe a
+fromNzOctDigit8 w | isNzOctDigit8 w = Just $ unsafeFromOctDigit8 w
+                  | otherwise       = Nothing
+{-# INLINABLE fromNzOctDigit8 #-}
+
+-- | Map the encoding of an octal digit to the corresponding number.
+--   No checks is performed.
+unsafeFromOctDigit8 ∷ Num a ⇒ Word8 → a
+unsafeFromOctDigit8 = unsafeFromDigit8
+{-# INLINE unsafeFromOctDigit8 #-}
+
+isLowAF8 ∷ Word8 → Bool
+isLowAF8 w = w >= ascii 'a' && w <= ascii 'f'
+{-# INLINE isLowAF8 #-}
+
+fromLowAF8 ∷ Num a ⇒ Word8 → a
+fromLowAF8 w = fromIntegral (w - ascii 'a' + 10)
+{-# INLINE fromLowAF8 #-}
+
+-- | Test if a byte is the encoding of a lower-case hexadecimal digit
+--   (/'0' ... '9'/ or /'a' ... 'f'/).
+isLowHexDigit8 ∷ Word8 → Bool
+isLowHexDigit8 w = isDigit8 w || isLowAF8 w
+{-# INLINABLE isLowHexDigit8 #-}
+
+-- | Test if a byte is the encoding of a non-zero lower-case hexadecimal digit
+--   (/'1' ... '9'/ or /'a' ... 'f'/).
+isNzLowHexDigit8 ∷ Word8 → Bool
+isNzLowHexDigit8 w = isNzDigit8 w || isLowAF8 w
+{-# INLINABLE isNzLowHexDigit8 #-}
+
+-- | Map the encoding of a lower-case hexadecimal digit to the corresponding
+--   number. Return 'Nothing' on other inputs.
+fromLowHexDigit8 ∷ Num a ⇒ Word8 → Maybe a
+fromLowHexDigit8 w | isDigit8 w = Just $ unsafeFromDigit8 w
+                   | isLowAF8 w = Just $ fromLowAF8 w
+                   | otherwise  = Nothing
+{-# INLINABLE fromLowHexDigit8 #-}
+
+-- | Map the encoding of a non-zero lower-case hexadecimal digit to
+--   the corresponding number. Return 'Nothing' on other inputs.
+fromNzLowHexDigit8 ∷ Num a ⇒ Word8 → Maybe a
+fromNzLowHexDigit8 w | isNzDigit8 w = Just $ unsafeFromDigit8 w
+                     | isLowAF8 w   = Just $ fromLowAF8 w
+                     | otherwise    = Nothing
+{-# INLINABLE fromNzLowHexDigit8 #-}
+
+-- | Map the encoding of a lower-case hexadecimal digit to the corresponding
+--   number. No checks is performed.
+unsafeFromLowHexDigit8 ∷ Num a ⇒ Word8 → a
+unsafeFromLowHexDigit8 w | w < ascii 'a' = unsafeFromDigit8 w
+                         | otherwise     = fromLowAF8 w
+{-# INLINE unsafeFromLowHexDigit8 #-}
+
+isUpAF8 ∷ Word8 → Bool
+isUpAF8 w = w >= ascii 'A' && w <= ascii 'F'
+{-# INLINE isUpAF8 #-}
+
+fromUpAF8 ∷ Num a ⇒ Word8 → a
+fromUpAF8 w = fromIntegral (w - ascii 'A' + 10)
+{-# INLINE fromUpAF8 #-}
+
+-- | Test if a byte is the encoding of an upper-case hexadecimal digit
+--   (/'0' ... '9'/ or /'A' ... 'F'/).
+isUpHexDigit8 ∷ Word8 → Bool
+isUpHexDigit8 w = isDigit8 w || isUpAF8 w
+{-# INLINABLE isUpHexDigit8 #-}
+
+-- | Test if a byte is the encoding of a non-zero upper-case hexadecimal digit
+--   (/'1' ... '9'/ or /'A' ... 'F'/).
+isNzUpHexDigit8 ∷ Word8 → Bool
+isNzUpHexDigit8 w = isNzDigit8 w || isUpAF8 w
+{-# INLINABLE isNzUpHexDigit8 #-}
+
+-- | Map the encoding of an upper-case hexadecimal digit to the corresponding
+--   number. Return 'Nothing' on other inputs.
+fromUpHexDigit8 ∷ Num a ⇒ Word8 → Maybe a
+fromUpHexDigit8 w | isDigit8 w = Just $ unsafeFromDigit8 w
+                  | isUpAF8 w  = Just $ fromUpAF8 w
+                  | otherwise  = Nothing
+{-# INLINABLE fromUpHexDigit8 #-}
+
+-- | Map the encoding of a non-zero upper-case hexadecimal digit to
+--   the corresponding number. Return 'Nothing' on other inputs.
+fromNzUpHexDigit8 ∷ Num a ⇒ Word8 → Maybe a
+fromNzUpHexDigit8 w | isNzDigit8 w = Just $ unsafeFromDigit8 w
+                    | isUpAF8 w    = Just $ fromUpAF8 w
+                    | otherwise    = Nothing
+{-# INLINABLE fromNzUpHexDigit8 #-}
+
+-- | Map the encoding of an upper-case hexadecimal digit to the corresponding
+--   number. No checks is performed.
+unsafeFromUpHexDigit8 ∷ Num a ⇒ Word8 → a
+unsafeFromUpHexDigit8 w | w < ascii 'A' = unsafeFromDigit8 w
+                        | otherwise     = fromUpAF8 w
+{-# INLINE unsafeFromUpHexDigit8 #-}
+
+-- | Test if a byte is the encoding of a hexadecimal digit
+--   (/'0' ... '9'/ or /'a' ... 'f'/ or /'A' ... 'F'/).
+isHexDigit8 ∷ Word8 → Bool
+isHexDigit8 w = isDigit8 w || isUpAF8 w || isLowAF8 w
+{-# INLINABLE isHexDigit8 #-}
+
+-- | Test if a byte is the encoding of a non-zero hexadecimal digit
+--   (/'1' ... '9'/ or /'a' ... 'f'/ or /'A' ... 'F'/).
+isNzHexDigit8 ∷ Word8 → Bool
+isNzHexDigit8 w = isNzDigit8 w || isUpAF8 w || isLowAF8 w
+{-# INLINABLE isNzHexDigit8 #-}
+
+-- | Map the encoding of a hexadecimal digit to the corresponding
+--   number. Return 'Nothing' on other inputs.
+fromHexDigit8 ∷ Num a ⇒ Word8 → Maybe a
+fromHexDigit8 w | isDigit8 w = Just $ unsafeFromDigit8 w
+                | isUpAF8 w  = Just $ fromUpAF8 w
+                | isLowAF8 w = Just $ fromLowAF8 w
+                | otherwise  = Nothing
+{-# INLINABLE fromHexDigit8 #-}
+
+-- | Map the encoding of a non-zero hexadecimal digit to the corresponding
+--   number. Return 'Nothing' on other inputs.
+fromNzHexDigit8 ∷ Num a ⇒ Word8 → Maybe a
+fromNzHexDigit8 w | isNzDigit8 w = Just $ unsafeFromDigit8 w
+                  | isUpAF8 w    = Just $ fromUpAF8 w
+                  | isLowAF8 w   = Just $ fromLowAF8 w
+                  | otherwise    = Nothing
+{-# INLINABLE fromNzHexDigit8 #-}
+
+-- | Map the encoding of a hexadecimal digit to the corresponding
+--   number. No checks is performed.
+unsafeFromHexDigit8 ∷ Num a ⇒ Word8 → a
+unsafeFromHexDigit8 w | w < ascii 'A' = unsafeFromDigit8 w
+                      | w < ascii 'a' = fromUpAF8 w
+                      | otherwise     = fromLowAF8 w
+{-# INLINE unsafeFromHexDigit8 #-}
+
diff --git a/src/Text/Latin1.hs b/src/Text/Latin1.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Latin1.hs
@@ -0,0 +1,193 @@
+{-# LANGUAGE UnicodeSyntax #-}
+{-# LANGUAGE PatternGuards #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+
+-- | Latin-1 utility functions.
+module Text.Latin1
+  (
+  -- * Latin-1 checks 
+    IsLatin1(..)
+  , isLatin1
+  , Latin1
+  , asciiIsLatin1
+  , maybeLatin1
+  , latin1
+  -- * Character properties
+  , isControl
+  , isPrintable
+  , isWhiteSpace
+  , isLower
+  , isUpper
+  , toLower
+  , toUpper
+  , isAlpha
+  , isAlphaNum
+  -- * Byte properties
+  , isControl8
+  , isPrintable8
+  , isWhiteSpace8
+  , isLower8
+  , isUpper8
+  , toLower8
+  , toUpper8
+  , isAlpha8
+  , isAlphaNum8
+  ) where
+
+import Data.Checked
+import Data.Char (ord, chr)
+import Data.Word (Word8)
+import qualified Data.Text as TS
+import qualified Data.Text.Lazy as TL
+import Text.Ascii (Ascii)
+import qualified Text.Ascii as A
+
+data IsLatin1 = IsLatin1
+
+instance Property IsLatin1 Char where
+  holds _ = (< 256) . ord
+  {-# INLINE holds #-}
+
+instance Property IsLatin1 α ⇒ Property IsLatin1 [α] where
+  holds _ = all isLatin1
+  {-# INLINE holds #-}
+
+instance Property IsLatin1 TS.Text where
+  holds _ = TS.all isLatin1
+  {-# INLINE holds #-}
+
+instance Property IsLatin1 TL.Text where
+  holds _ = TL.all isLatin1
+  {-# INLINE holds #-}
+
+isLatin1 ∷ Property IsLatin1 v ⇒ v → Bool 
+isLatin1 = holds IsLatin1
+{-# INLINE isLatin1 #-}
+
+type Latin1 α = Checked IsLatin1 α
+
+-- | ASCII values are Latin-1 values.
+asciiIsLatin1 ∷ Ascii α → Latin1 α
+asciiIsLatin1 = trustMe . checked
+{-# INLINE asciiIsLatin1 #-}
+
+-- | Map a character to its Latin-1 encoding if possible, otherwise
+--   return 'Nothing'.
+maybeLatin1 ∷ Char → Maybe Word8
+maybeLatin1 c | isLatin1 c = Just $ latin1 c
+              | otherwise  = Nothing
+{-# INLINABLE maybeLatin1 #-}
+
+-- | Encode a Latin-1 character. No checks is performed.
+latin1 ∷ Char → Word8
+latin1 = fromIntegral . ord
+{-# INLINE latin1 #-}
+
+-- | Test if a character is a Latin-1 control character.
+isControl ∷ Char → Bool
+isControl c = w < 32 || (w >= 127 && w <= 159)
+  where w = ord c
+{-# INLINE isControl #-}
+
+-- | Test if a character is a Latin-1 printable character.
+isPrintable ∷ Char → Bool
+isPrintable c = A.isPrintable c || (w >= 160 && w < 256)
+  where w = ord c
+{-# INLINE isPrintable #-}
+
+-- | Test if a character is a Latin-1 whitespace character.
+isWhiteSpace ∷ Char → Bool
+isWhiteSpace c = A.isWhiteSpace c || w == 133 || w == 160
+  where w = ord c
+{-# INLINE isWhiteSpace #-}
+
+-- | Test if a character is a Latin-1 lower-case letter.
+isLower ∷ Char → Bool
+isLower c = A.isLower c || (w >= 223 && w < 256 && w /= 247)
+  where w = ord c
+{-# INLINE isLower #-}
+
+-- | Test if a character is a Latin-1 upper-case letter.
+isUpper ∷ Char → Bool
+isUpper c = A.isUpper c || (w >= 192 && w <= 222 && w /= 215)
+  where w = ord c
+{-# INLINE isUpper #-}
+
+-- | Map lower-case Latin-1 letters to the corresponding upper-case letters,
+--   leaving other characters as is.
+toLower ∷ Char → Char
+toLower c | isUpper c = chr (ord c + 32)
+          | otherwise = c
+{-# INLINABLE toLower #-}
+
+-- | Map upper-case Latin-1 letters to the corresponding lower-case letters,
+--   leaving other characters as is.
+toUpper ∷ Char → Char
+toUpper c | w ← ord c
+          , A.isLower c || (w >= 224 && w <= 254 && w /= 247)
+          = chr (ord c - 32)
+          | otherwise
+          = c
+{-# INLINABLE toUpper #-}
+
+-- | Test if a character is a Latin-1 letter.
+isAlpha ∷ Char → Bool
+isAlpha c = isUpper c || isLower c
+{-# INLINABLE isAlpha #-}
+
+-- | Test if a character is either a Latin-1 letter or a decimal digit.
+isAlphaNum ∷ Char → Bool
+isAlphaNum c = A.isDigit c || isAlpha c
+{-# INLINABLE isAlphaNum #-}
+
+-- | Test if a byte is the encoding of a Latin-1 control character.
+isControl8 ∷ Word8 → Bool
+isControl8 w = w < 32 || (w >= 127 && w <= 159)
+{-# INLINE isControl8 #-}
+
+-- | Test if a byte is the encoding of a Latin-1 printable character.
+isPrintable8 ∷ Word8 → Bool
+isPrintable8 w = A.isPrintable8 w || w >= 160
+{-# INLINE isPrintable8 #-}
+
+-- | Test if a byte is the encoding of a Latin-1 whitespace character.
+isWhiteSpace8 ∷ Word8 → Bool
+isWhiteSpace8 w = A.isWhiteSpace8 w || w == 133 || w == 160
+{-# INLINE isWhiteSpace8 #-}
+
+-- | Test if a byte is the encoding of a Latin-1 lower-case letter.
+isLower8 ∷ Word8 → Bool
+isLower8 w = A.isLower8 w || (w >= 223 && w /= 247)
+{-# INLINE isLower8 #-}
+
+-- | Test if a byte is the encoding of a Latin-1 upper-case letter.
+isUpper8 ∷ Word8 → Bool
+isUpper8 w = A.isUpper8 w || (w >= 192 && w <= 222 && w /= 215)
+{-# INLINE isUpper8 #-}
+
+-- | Map the encodings of lower-case Latin-1 letters to the encodings of
+--   the corresponding upper-case letters, leaving other bytes as is.
+toLower8 ∷ Word8 → Word8
+toLower8 w | isUpper8 w = w + 32
+           | otherwise  = w
+{-# INLINABLE toLower8 #-}
+
+-- | Map the encodings of upper-case Latin-1 letters to the encodings of
+--   the corresponding lower-case letters, leaving other bytes as is.
+toUpper8 ∷ Word8 → Word8
+toUpper8 w | A.isLower8 w || (w >= 224 && w /= 247 && w /= 255) = w - 32
+           | otherwise = w
+{-# INLINABLE toUpper8 #-}
+
+-- | Test if a byte is the encoding of a Latin-1 letter.
+isAlpha8 ∷ Word8 → Bool
+isAlpha8 w = isUpper8 w || isLower8 w
+{-# INLINABLE isAlpha8 #-}
+
+-- | Test if a byte is the encoding of either a Latin-1 letter
+--   or a decimal digit.
+isAlphaNum8 ∷ Word8 → Bool
+isAlphaNum8 w = A.isDigit8 w || isAlpha8 w
+{-# INLINABLE isAlphaNum8 #-}
+
diff --git a/text-latin1.cabal b/text-latin1.cabal
new file mode 100644
--- /dev/null
+++ b/text-latin1.cabal
@@ -0,0 +1,36 @@
+Name: text-latin1
+Version: 0.1
+Category: Text
+Stability: experimental
+Synopsis: Latin-1 (including ASCII) utility functions
+Description:
+  This package provides various functions over the ASCII ang Latin-1
+  portions of the 'Char' and 'Word8' data types.
+
+Homepage: https://github.com/mvv/text-latin1
+Bug-Reports: https://github.com/mvv/text-latin1/issues
+
+Author: Mikhail Vorozhtsov <mikhail.vorozhtsov@gmail.com>
+Maintainer: Mikhail Vorozhtsov <mikhail.vorozhtsov@gmail.com>
+Copyright: 2013 Mikhail Vorozhtsov <mikhail.vorozhtsov@gmail.com>
+License: BSD3
+License-File: LICENSE
+
+Cabal-Version: >= 1.6.0
+Build-Type: Simple
+
+Source-Repository head
+  Type: git
+  Location: https://github.com/mvv/text-latin1.git
+
+Library
+  Build-Depends: base >= 4 && < 5
+               , data-checked
+               , bytestring
+               , text
+  Hs-Source-Dirs: src
+  GHC-Options: -Wall
+  Exposed-Modules:
+    Text.Ascii
+    Text.Latin1
+
