diff --git a/Examples/wc.hs b/Examples/wc.hs
--- a/Examples/wc.hs
+++ b/Examples/wc.hs
@@ -9,17 +9,15 @@
 -----------------------------------------------------------------------------
 module Main (main) where
 import Data.Enumerator
-import Data.Enumerator.IO
+import qualified Data.Enumerator.IO as EIO
+import qualified Data.Enumerator.Text as ET
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Char8 as B8
 import qualified Data.Text as T
-import qualified Data.Text.Encoding as T
 
 -- support imports
 import Control.Exception as E
 import Data.List
-import Data.Bits ((.&.))
-import Data.Word (Word8)
 import Control.Monad (unless, forM_)
 import System.IO
 import System.Console.GetOpt
@@ -53,24 +51,14 @@
 --
 -- Since it's possible for the input file's encoding to be non-UTF8, a bit
 -- of error handling is also required.
+--
+-- Note the use of joinI. 'ET.decode' is an enumeratee, which means it returns
+-- an iteratee yielding an inner step. 'joinI' "collapses" an enumeratee's
+-- return value, much as 'join' does to monadic values.
 
 iterChars :: Monad m => Iteratee E.SomeException B.ByteString m Integer
-iterChars = continue (step (B.empty, 0)) where
-	step accT chunk = case chunk of
-		EOF -> let (extra, acc) = accT in yield acc (Chunks [extra])
-		(Chunks xs) -> case foldl' foldStep (Just accT) xs of
-			Just accT' -> continue $ step $! accT'
-			Nothing -> throwError (E.SomeException (E.ErrorCall "Invalid UTF-8"))
-	
-	-- The 'decodeUtf8' function is complicated, and defined later, but
-	-- all it does is decode as much input as possible, then return any
-	-- remaining bytes.
-	
-	foldStep Nothing _ = Nothing
-	foldStep (Just (extra, acc)) bytes = case decodeUtf8 (B.append extra bytes) of
-		Just (text, extra') -> Just (extra', acc + toInteger (T.length text))
-		Nothing -> Nothing
-
+iterChars = joinI (ET.decode ET.utf8 $$ count) where
+	count = liftFoldL' (\acc t -> acc + toInteger (T.length t)) 0
 
 main :: IO ()
 main = do
@@ -88,7 +76,7 @@
 		putStr $ filename ++ ": "
 		
 		-- see cat.hs for commented implementation of 'Data.Enumerator.IO.enumFile'
-		eitherStat <- run (enumFile filename $$ iter)
+		eitherStat <- run (EIO.enumFile filename $$ iter)
 		putStrLn $ case eitherStat of
 			Left err -> "ERROR: " ++ show err
 			Right stat -> show stat
@@ -121,86 +109,3 @@
 		exitFailure
 	
 	return (Prelude.head options, files)
-
--- Incremental UTF-8 validator / decoder
-
-decodeUtf8 :: B.ByteString -> Maybe (T.Text, B.ByteString)
-decodeUtf8 allBytes = loop B.empty allBytes where
-	
-	loop acc bytes | B.null bytes = Just (T.decodeUtf8 acc, bytes)
-	loop acc bytes = do
-		let (x0, x1, x2, x3) = indexes bytes
-		req <- required x0
-		if req > B.length bytes
-			then Just (T.decodeUtf8 acc, bytes)
-			else loop (B.append acc (B.take req bytes)) (B.drop req bytes)
-	
-	required x0
-		| x0 .&. 0x80 == 0x00 = Just 1
-		| x0 .&. 0xE0 == 0xC0 = Just 2
-		| x0 .&. 0xF0 == 0xE0 = Just 3
-		| x0 .&. 0xF8 == 0xF0 = Just 4
-		| otherwise           = Nothing
-	
-	indexes bytes = (x0, x1, x2, x3) where
-		x0 = B.index bytes 0
-		x1 = B.index bytes 1
-		x2 = B.index bytes 2
-		x3 = B.index bytes 3
-
--- UTF8 decoding gunk; mostly copied from Data.Text
-
-between :: Word8                -- ^ byte to check
-        -> Word8                -- ^ lower bound
-        -> Word8                -- ^ upper bound
-        -> Bool
-between x y z = x >= y && x <= z
-{-# INLINE between #-}
-
-validate1    :: Word8 -> Bool
-validate1 x1 = between x1 0x00 0x7F
-{-# INLINE validate1 #-}
-
-validate2       :: Word8 -> Word8 -> Bool
-validate2 x1 x2 = between x1 0xC2 0xDF && between x2 0x80 0xBF
-{-# INLINE validate2 #-}
-
-validate3          :: Word8 -> Word8 -> Word8 -> Bool
-{-# INLINE validate3 #-}
-validate3 x1 x2 x3 = validate3_1 ||
-                     validate3_2 ||
-                     validate3_3 ||
-                     validate3_4
-  where
-    validate3_1 = (x1 == 0xE0) &&
-                  between x2 0xA0 0xBF &&
-                  between x3 0x80 0xBF
-    validate3_2 = between x1 0xE1 0xEC &&
-                  between x2 0x80 0xBF &&
-                  between x3 0x80 0xBF
-    validate3_3 = x1 == 0xED &&
-                  between x2 0x80 0x9F &&
-                  between x3 0x80 0xBF
-    validate3_4 = between x1 0xEE 0xEF &&
-                  between x2 0x80 0xBF &&
-                  between x3 0x80 0xBF
-
-validate4             :: Word8 -> Word8 -> Word8 -> Word8 -> Bool
-{-# INLINE validate4 #-}
-validate4 x1 x2 x3 x4 = validate4_1 ||
-                        validate4_2 ||
-                        validate4_3
-  where 
-    validate4_1 = x1 == 0xF0 &&
-                  between x2 0x90 0xBF &&
-                  between x3 0x80 0xBF &&
-                  between x4 0x80 0xBF
-    validate4_2 = between x1 0xF1 0xF3 &&
-                  between x2 0x80 0xBF &&
-                  between x3 0x80 0xBF &&
-                  between x4 0x80 0xBF
-    validate4_3 = x1 == 0xF4 &&
-                  between x2 0x80 0x8F &&
-                  between x3 0x80 0xBF &&
-                  between x4 0x80 0xBF
-
diff --git a/enumerator.cabal b/enumerator.cabal
--- a/enumerator.cabal
+++ b/enumerator.cabal
@@ -1,5 +1,5 @@
 name: enumerator
-version: 0.1.1
+version: 0.2
 synopsis: Implementation of Oleg Kiselyov's left-fold enumerators
 license: MIT
 license-file: license.txt
@@ -19,6 +19,7 @@
 
 extra-source-files:
   Examples/*.hs
+  readme.txt
 
 source-repository head
   type: darcs
@@ -29,10 +30,22 @@
   hs-source-dirs: hs
 
   build-depends:
-      base >=3 && < 5
-    , transformers >= 0.2 && < 0.3
+      transformers >= 0.2 && < 0.3
     , bytestring >= 0.9 && < 0.10
+    , text >= 0.7 && < 0.8
 
+  if impl(ghc >= 6.10)
+    build-depends:
+        base >=4 && < 5
+  else
+    build-depends:
+        base >=3 && < 4
+      , extensible-exceptions
+
   exposed-modules:
     Data.Enumerator
     Data.Enumerator.IO
+    Data.Enumerator.Text
+
+  other-modules:
+    Data.Enumerator.Util
diff --git a/hs/Data/Enumerator.hs b/hs/Data/Enumerator.hs
--- a/hs/Data/Enumerator.hs
+++ b/hs/Data/Enumerator.hs
@@ -33,12 +33,14 @@
 	, (>==>)
 	, (<==<)
 	  -- ** Iteratees
+	, run
 	, consume
 	, isEOF
 	, liftTrans
 	, liftFoldL
 	, liftFoldL'
 	, liftFoldM
+	, printChunks
 	  -- ** Enumerators
 	, enumEOF
 	, enumList
@@ -61,9 +63,6 @@
 	, Data.Enumerator.dropWhile
 	, span
 	, Data.Enumerator.break
-	  -- * Utility functions
-	, run
-	, printChunks
 	) where
 import Data.List (genericDrop, genericLength, genericSplitAt)
 import Data.Monoid (Monoid, mempty, mappend, mconcat)
@@ -215,6 +214,8 @@
 	case step of
 		Error err -> runIteratee (h err)
 		_ -> return step
+infixl 1 >>==
+
 -- | Equivalent to (>>=), but allows 'Iteratee's with different input types
 -- to be composed.
 (>>==) :: Monad m =>
@@ -223,6 +224,7 @@
 	Iteratee e a' m b'
 i >>== f = Iteratee $ runIteratee i >>= runIteratee . f
 {-# INLINE (>>==) #-}
+infixr 1 ==<<
 
 -- | @(==\<\<) = flip (\>\>==)@
 (==<<):: Monad m =>
@@ -231,6 +233,7 @@
 	Iteratee e a' m b'
 (==<<) = flip (>>==)
 {-# INLINE (==<<) #-}
+infixr 0 $$
 
 -- | @($$) = (==\<\<)@
 --
@@ -242,6 +245,8 @@
 	Iteratee e a' m b'
 ($$) = (==<<)
 {-# INLINE ($$) #-}
+infixr 1 >==>
+
 -- | @(>==>) e1 e2 s = e1 s >>== e2@
 (>==>) :: Monad m =>
 	Enumerator e a m b ->
@@ -250,6 +255,7 @@
 	Iteratee e a' m b'
 (>==>) e1 e2 s = e1 s >>== e2
 {-# INLINE (>==>) #-}
+infixr 1 <==<
 
 -- | @(\<==\<) = flip (>==>)@
 (<==<) :: Monad m =>
@@ -293,6 +299,22 @@
 		Chunks [] -> continue $ step acc
 		Chunks xs -> Iteratee $ liftM (Continue . step) (foldM f acc xs)
 		EOF -> yield acc EOF
+-- | Run an iteratee until it finishes, and return either the final value
+-- (if it succeeded) or the error (if it failed).
+run :: Monad m => Iteratee e a m b -> m (Either e b)
+run i = do
+	mStep <- runIteratee $ enumEOF ==<< i
+	case mStep of
+		Error err -> return $ Left err
+		Yield x _ -> return $ Right x
+		Continue _ -> error "run: divergent iteratee"
+-- | Print chunks as they're received from the enumerator, optionally
+-- printing empty chunks.
+printChunks :: (MIO.MonadIO m, Show a) => Bool -> Iteratee e a m ()
+printChunks printEmpty = continue step where
+	step (Chunks []) | not printEmpty = continue step
+	step (Chunks xs) = MIO.liftIO (print xs) >> continue step
+	step EOF = MIO.liftIO (putStrLn "EOF") >> yield () EOF
 -- | The most primitive enumerator; simply sends 'EOF'. The iteratee must
 -- either yield a value or throw an error continuing receiving 'EOF' will
 -- not terminate with any useful value.
@@ -394,19 +416,3 @@
 -- | @break p = 'span' (not . p)@
 break :: Monad m => (a -> Bool) -> Iteratee e a m [a]
 break p = span $ not . p
--- | Run an iteratee until it finishes, and return either the final value
--- (if it succeeded) or the error (if it failed).
-run :: Monad m => Iteratee e a m b -> m (Either e b)
-run i = do
-	mStep <- runIteratee $ enumEOF ==<< i
-	case mStep of
-		Error err -> return $ Left err
-		Yield x _ -> return $ Right x
-		Continue _ -> error "run: divergent iteratee"
--- | Print chunks as they're received from the enumerator, optionally
--- printing empty chunks.
-printChunks :: (MIO.MonadIO m, Show a) => Bool -> Iteratee e a m ()
-printChunks printEmpty = continue step where
-	step (Chunks []) | not printEmpty = continue step
-	step (Chunks xs) = MIO.liftIO (print xs) >> continue step
-	step EOF = MIO.liftIO (putStrLn "EOF") >> yield () EOF
diff --git a/hs/Data/Enumerator/IO.hs b/hs/Data/Enumerator/IO.hs
--- a/hs/Data/Enumerator/IO.hs
+++ b/hs/Data/Enumerator/IO.hs
@@ -17,7 +17,7 @@
 	, iterHandle
 	) where
 import Data.Enumerator
-import Control.Monad.IO.Class (liftIO)
+import Data.Enumerator.Util
 import qualified Control.Exception as E
 import qualified Data.ByteString as B
 import qualified System.IO as IO
@@ -25,49 +25,51 @@
 -- stream them to an 'Iteratee'. If an exception occurs during file IO,
 -- enumeration will stop and 'Error' will be returned. Exceptions from the
 -- iteratee are not caught.
+--
+-- This enumerator blocks until at least one byte is available from the
+-- handle, and might read less than the maximum buffer size in some
+-- cases.
 enumHandle :: Integer -- ^ Buffer size
            -> IO.Handle
            -> Enumerator E.SomeException B.ByteString IO b
 enumHandle bufferSize h = Iteratee . loop where
-	size' = fromInteger bufferSize
-	loop (Continue k) = read' k
+	loop (Continue k) = withBytes $ \bytes -> if B.null bytes
+		then return $ Continue k
+		else runIteratee (k (Chunks [bytes])) >>= loop
+	
 	loop step = return step
-	read' k = do
-		eitherB <- E.try $ B.hGet h size'
-		case eitherB of
-			Left err -> return $ Error err
-			Right bytes | B.null bytes -> return $ Continue k
-			Right bytes -> do
-				step <- runIteratee (k (Chunks [bytes]))
-				loop step
+	
+	intSize = fromInteger bufferSize
+	withBytes = tryStep $ do
+		hasInput <- E.catch
+			(IO.hWaitForInput h (-1))
+			(\(E.SomeException _) -> return False) 
+		if hasInput
+			then B.hGetNonBlocking h intSize
+			else return B.empty
 -- | Opens a file path in binary mode, and passes the handle to 'enumHandle'.
 -- The file will be closed when the 'Iteratee' finishes.
 enumFile :: FilePath -> Enumerator E.SomeException B.ByteString IO b
-enumFile path s = Iteratee $ do
-	eitherH <- E.try $ IO.openBinaryFile path IO.ReadMode
-	case eitherH of
-		Left err -> return $ Error err
-		Right h -> E.finally
-			(runIteratee (enumHandle 4096 h s))
-			(IO.hClose h)
+enumFile path s = Iteratee io where
+	withHandle = tryStep (IO.openBinaryFile path IO.ReadMode)
+	io = withHandle $ \h -> E.finally
+		(runIteratee (enumHandle 4096 h s))
+		(IO.hClose h)
 -- | Read bytes from a stream and write them to a handle. If an exception
 -- occurs during file IO, enumeration will stop and 'Error' will be
 -- returned.
 iterHandle :: IO.Handle -> Iteratee E.SomeException B.ByteString IO ()
 iterHandle h = continue step where
 	step EOF = yield () EOF
-	step (Chunks bytes) = do
-		eitherErr <- liftIO . E.try $ mapM_ (B.hPut h) bytes
-		case eitherErr of
-			Left err -> throwError err
-			_ -> continue step
+	step (Chunks []) = continue step
+	step (Chunks bytes) = Iteratee io where
+		put = mapM_ (B.hPut h) bytes
+		io = tryStep put (\_ -> return $ Continue step)
 -- | Opens a file path in binary mode, and passes the handle to 'iterHandle'.
 -- The file will be closed when the 'Iteratee' finishes.
 iterFile :: FilePath -> Iteratee E.SomeException B.ByteString IO ()
-iterFile path = Iteratee $ do
-	eitherH <- E.try $ IO.openBinaryFile path IO.WriteMode
-	case eitherH of
-		Left err -> return $ Error err
-		Right h -> E.finally
-			(runIteratee (iterHandle h))
-			(IO.hClose h)
+iterFile path = Iteratee io where
+	withHandle = tryStep (IO.openBinaryFile path IO.WriteMode)
+	io = withHandle $ \h -> E.finally
+		(runIteratee (iterHandle h))
+		(IO.hClose h)
diff --git a/hs/Data/Enumerator/Text.hs b/hs/Data/Enumerator/Text.hs
new file mode 100644
--- /dev/null
+++ b/hs/Data/Enumerator/Text.hs
@@ -0,0 +1,235 @@
+-----------------------------------------------------------------------------
+-- |
+-- Module: Data.Enumerator.Text
+-- Copyright: 2010 John Millikin
+-- License: MIT
+--
+-- Maintainer: jmillikin@gmail.com
+-- Portability: portable
+--
+-- Enumerator-based text IO
+--
+-----------------------------------------------------------------------------
+module Data.Enumerator.Text (
+	  -- * Enumerators and iteratees
+	  enumHandle
+	, enumFile
+	, iterHandle
+	, iterFile
+	  -- * Codecs
+	, Codec
+	, encode
+	, decode
+	, utf8
+	, utf16_le
+	, utf16_be
+	, utf32_le
+	, utf32_be
+	, ascii
+	, iso8859_1
+	) where
+import qualified Control.Exception as E
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+import qualified System.IO as IO
+import Control.Arrow (first)
+import Data.Bits ((.&.))
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Char8 as B8
+import qualified Data.Text.Encoding as TE
+import Data.Bits ((.|.), shiftL)
+import Data.Word (Word16)
+import Prelude as Prelude
+import Numeric (showIntAtBase)
+import Data.Char (toUpper, intToDigit, ord)
+import Data.Word (Word8)
+import System.IO.Unsafe (unsafePerformIO)
+import Data.Enumerator
+import Data.Enumerator.Util
+-- | Read lines of text from the handle, and stream them to an 'Iteratee'.
+-- If an exception occurs during file IO, enumeration will stop and 'Error'
+-- will be returned. Exceptions from the iteratee are not caught.
+enumHandle :: IO.Handle -> Enumerator E.SomeException T.Text IO b
+enumHandle h = Iteratee . loop where
+	loop (Continue k) = withText $ \text -> if T.null text
+		then return $ Continue k
+		else runIteratee (k (Chunks [text])) >>= loop
+	
+	loop step = return step
+	withText = tryStep (T.hGetLine h)
+-- | Opens a file path in text mode, and passes the handle to 'enumHandle'.
+-- The file will be closed when the 'Iteratee' finishes.
+enumFile :: FilePath -> Enumerator E.SomeException T.Text IO b
+enumFile path s = Iteratee io where
+	withHandle = tryStep (IO.openFile path IO.ReadMode)
+	io = withHandle $ \h -> E.finally
+		(runIteratee (enumHandle h s))
+		(IO.hClose h)
+-- | Read text from a stream and write it to a handle. If an exception
+-- occurs during file IO, enumeration will stop and 'Error' will be
+-- returned.
+iterHandle :: IO.Handle -> Iteratee E.SomeException T.Text IO ()
+iterHandle h = continue step where
+	step EOF = yield () EOF
+	step (Chunks []) = continue step
+	step (Chunks chunks) = Iteratee io where
+		put = mapM_ (T.hPutStr h) chunks
+		io = tryStep put (\_ -> return $ Continue step)
+-- | Opens a file path in text mode, and passes the handle to 'iterHandle'.
+-- The file will be closed when the 'Iteratee' finishes.
+iterFile :: FilePath -> Iteratee E.SomeException T.Text IO ()
+iterFile path = Iteratee io where
+	withHandle = tryStep (IO.openFile path IO.WriteMode)
+	io = withHandle $ \h -> E.finally
+		(runIteratee (iterHandle h))
+		(IO.hClose h)
+data Codec = Codec
+	{ codecName :: T.Text
+	, codecEncode :: [T.Text] -> Either E.SomeException [B.ByteString]
+	, codecDecode :: B.ByteString -> Either E.SomeException (T.Text, B.ByteString)
+	}
+
+instance Show Codec where
+	showsPrec d c = showParen (d > 10) $
+		showString "Codec " . shows (codecName c)
+encode :: Monad m => Codec -> Enumeratee E.SomeException T.Text B.ByteString m b
+encode codec = loop where
+	loop = checkDone $ continue . step
+	step k EOF = yield (Continue k) EOF
+	step k (Chunks []) = continue $ step k
+	step k (Chunks xs) = case codecEncode codec xs of
+		Left err -> throwError err
+		Right byteChunks -> k (Chunks byteChunks) >>== loop
+decode :: Monad m => Codec -> Enumeratee E.SomeException B.ByteString T.Text m b
+decode codec = loop B.empty where
+	dec = codecDecode codec
+	
+	loop acc = checkDone $ continue . step acc
+	step acc k EOF = yield (Continue k) $ if B.null acc
+		then EOF
+		else Chunks [acc]
+	step acc k (Chunks []) = continue $ step acc k
+	step acc k (Chunks xs) = case dec (B.concat (acc:xs)) of
+		Left err -> throwError err
+		Right (text, extra) -> if T.null text
+			then continue $ step extra k
+			else k (Chunks [text]) >>== loop extra
+utf8 :: Codec
+utf8 = Codec name enc dec where
+	name = T.pack "UTF-8"
+	enc = Right . Prelude.map TE.encodeUtf8
+	dec = unsafeTryDec . splitBytes
+	splitBytes bytes = loop 0 where
+		required x0
+			| x0 .&. 0x80 == 0x00 = 1
+			| x0 .&. 0xE0 == 0xC0 = 2
+			| x0 .&. 0xF0 == 0xE0 = 3
+			| x0 .&. 0xF8 == 0xF0 = 4
+			
+			-- Invalid input; let Text figure it out
+			| otherwise           = 1
+		maxN = B.length bytes
+		
+		loop n | n == maxN = (TE.decodeUtf8 bytes, B.empty)
+		loop n = let
+			req = required $ B.index bytes n
+			tooLong = first TE.decodeUtf8 $ B.splitAt n bytes
+			decodeMore = loop $! n + req
+			in if req > maxN then tooLong else decodeMore
+utf16_le :: Codec
+utf16_le = Codec name enc dec where
+	name = T.pack "UTF-16-LE"
+	enc = Right . Prelude.map TE.encodeUtf16LE
+	dec = unsafeTryDec . splitBytes
+	splitBytes bytes = loop 0 where
+		maxN = B.length bytes
+		
+		loop n |  n      == maxN = (TE.decodeUtf16LE bytes, B.empty)
+		       | (n + 1) == maxN = decodeTo n
+		loop n = let
+			req = utf16Required (B.index bytes 0) (B.index bytes 1)
+			decodeMore = loop $! n + req
+			in if req > maxN then decodeTo n else decodeMore
+		
+		decodeTo n = first TE.decodeUtf16LE $ B.splitAt n bytes
+utf16_be :: Codec
+utf16_be = Codec name enc dec where
+	name = T.pack "UTF-16-BE"
+	enc = Right . Prelude.map TE.encodeUtf16BE
+	dec = unsafeTryDec . splitBytes
+	splitBytes bytes = loop 0 where
+		maxN = B.length bytes
+		
+		loop n |  n      == maxN = (TE.decodeUtf16BE bytes, B.empty)
+		       | (n + 1) == maxN = decodeTo n
+		loop n = let
+			req = utf16Required (B.index bytes 1) (B.index bytes 0)
+			decodeMore = loop $! n + req
+			in if req > maxN then decodeTo n else decodeMore
+		
+		decodeTo n = first TE.decodeUtf16BE $ B.splitAt n bytes
+utf16Required :: Word8 -> Word8 -> Int
+utf16Required x0 x1 = required where
+	required = if x >= 0xD800 && x <= 0xDBFF
+		then 4
+		else 2
+	x :: Word16
+	x = (fromIntegral x1 `shiftL` 8) .|. fromIntegral x0
+utf32_le :: Codec
+utf32_le = Codec name enc dec where
+	name = T.pack "UTF-32-LE"
+	enc = Right . Prelude.map TE.encodeUtf32LE
+	dec = unsafeTryDec . utf32SplitBytes TE.decodeUtf32LE
+	
+
+utf32_be :: Codec
+utf32_be = Codec name enc dec where
+	name = T.pack "UTF-32-BE"
+	enc = Right . Prelude.map TE.encodeUtf32BE
+	dec = unsafeTryDec . utf32SplitBytes TE.decodeUtf32BE
+utf32SplitBytes :: (B.ByteString -> a) -> B.ByteString -> (a, B.ByteString)
+utf32SplitBytes dec bytes = (dec toDecode, extra) where
+	len = B.length bytes
+	lenExtra = mod len 4
+	lenToDecode = len - lenExtra
+	(toDecode, extra) = if lenExtra == 0
+		then (bytes, B.empty)
+		else B.splitAt lenToDecode bytes
+ascii :: Codec
+ascii = Codec name (mapEither enc) dec where
+	name = T.pack "ASCII"
+	enc t = case T.findBy (\c -> ord c > 0x7F) t of
+		Nothing -> Right . B8.pack . T.unpack $ t
+		Just c -> illegalEnc name c
+	dec bytes = case B.find (\w -> w > 0x7F) bytes of
+		Nothing -> Right (T.pack (B8.unpack bytes), B.empty)
+		Just w -> illegalDec name w
+iso8859_1 :: Codec
+iso8859_1 = Codec name (mapEither enc) dec where
+	name = T.pack "ISO-8859-1"
+	enc t = case T.findBy (\c -> ord c > 0xFF) t of
+		Nothing -> Right . B8.pack . T.unpack $ t
+		Just c -> illegalEnc name c
+	dec bytes = Right (T.pack (B8.unpack bytes), B.empty)
+illegalEnc :: T.Text -> Char -> Either E.SomeException a
+illegalEnc name c = Left . E.SomeException . E.ErrorCall $ msg "" where
+	len = Prelude.length
+	pad str | len str < 4 = replicate (4 - len str) '0' ++ str
+	        | otherwise      = str
+	hex = "U+" ++ pad (showIntAtBase 16 (toUpper . intToDigit) (ord c) "")
+	msg = (s "Codec " . shows name . s " can't encode character " . s hex)
+	s = showString
+illegalDec :: T.Text -> Word8 -> Either E.SomeException a
+illegalDec name w = Left . E.SomeException . E.ErrorCall $ msg "" where
+	len = Prelude.length
+	pad str | len str < 2 = replicate (2 - len str) '0' ++ str
+	        | otherwise      = str
+	hex = "0x" ++ pad (showIntAtBase 16 (toUpper . intToDigit) w "")
+	msg = (s "Codec " . shows name . s " can't decode byte " . s hex)
+	s = showString
+unsafeTryDec :: (a, b) -> Either E.SomeException (a, b)
+unsafeTryDec (a, b) = unsafePerformIO $ do
+	tried <- E.try $ E.evaluate a
+	return $ case tried of
+		Left err -> Left err
+		Right _ -> Right (a, b)
diff --git a/hs/Data/Enumerator/Util.hs b/hs/Data/Enumerator/Util.hs
new file mode 100644
--- /dev/null
+++ b/hs/Data/Enumerator/Util.hs
@@ -0,0 +1,21 @@
+module Data.Enumerator.Util where
+import Data.Enumerator
+import qualified Control.Exception as E
+
+tryStep :: IO t
+       -> (t -> IO (Step E.SomeException a IO b))
+       -> IO (Step E.SomeException a IO b)
+tryStep get io = do
+	tried <- E.try get
+	case tried of
+		Right t -> io t
+		Left err -> return $ Error err
+{-# INLINE tryStep #-}
+
+mapEither :: (a -> Either e b) -> [a] -> Either e [b]
+mapEither f = loop [] where
+	loop acc [] = Right (reverse acc)
+	loop acc (a:as) = case f a of
+		Left err -> Left err
+		Right b -> loop (b:acc) as
+{-# INLINE mapEither #-}
diff --git a/readme.txt b/readme.txt
new file mode 100644
--- /dev/null
+++ b/readme.txt
@@ -0,0 +1,10 @@
+The source code for "enumerator" is literate. To build the library from scratch,
+install the "anansi" application and then run:
+
+    anansi -o hs/ enumerator.anansi
+
+To generate the woven PDF, install NoWeb and then run:
+
+    anansi -w -l latex-noweb -o enumerator.tex enumerator.anansi
+    xelatex enumerator.tex
+    xelatex enumerator.tex
