diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+The following license covers this documentation, and the source code, except
+where otherwise indicated.
+
+Copyright 2010, Suite Solutions. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "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 HOLDERS 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.lhs b/Setup.lhs
new file mode 100644
--- /dev/null
+++ b/Setup.lhs
@@ -0,0 +1,7 @@
+#!/usr/bin/env runhaskell
+
+> module Main where
+> import Distribution.Simple
+
+> main :: IO ()
+> main = defaultMain
diff --git a/Text/XML/Enumerator/Parse.hs b/Text/XML/Enumerator/Parse.hs
new file mode 100644
--- /dev/null
+++ b/Text/XML/Enumerator/Parse.hs
@@ -0,0 +1,728 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+-- | This module provides both a native Haskell solution for parsing XML
+-- documents into a stream of events, and a set of parser combinators for
+-- dealing with a stream of events.
+--
+-- The important thing to know about the combinators is that they do /not/ work
+-- on the fully-powered 'Event' datatype; rather, this module defines an
+-- 'SEvent' datatype which only deals with tags, attributes and content. For
+-- most uses, this is sufficient. If you need to parse doctypes, instructions
+-- or contents, you will not be able to use the combinators.
+--
+-- As a simple example, if you have the following XML file:
+--
+-- > <?xml version="1.0" encoding="utf-8"?>
+-- > <people>
+-- >     <person age="25">Michael</person>
+-- >     <person age="2">Eliezer</person>
+-- > </people>
+--
+-- Then this code:
+--
+-- > {-# LANGUAGE OverloadedStrings #-}
+-- > import Text.XML.Enumerator.Parse
+-- > import Data.Text.Lazy (Text, unpack)
+-- > 
+-- > data Person = Person { age :: Int, name :: Text }
+-- >     deriving Show
+-- > 
+-- > parsePerson = tag' "person" (requireAttr "age") $ \age -> do
+-- >     name <- content'
+-- >     return $ Person (read $ unpack age) name
+-- > 
+-- > parsePeople = tag'' "people" $ many parsePerson
+-- > 
+-- > main = parseFile_ "people.xml" (const Nothing) $ force "people required" parsePeople
+--
+-- will produce:
+--
+-- > [Person {age = 25, name = "Michael"},Person {age = 2, name = "Eliezer"}]
+module Text.XML.Enumerator.Parse
+    ( -- * Parsing XML files
+      parseBytes
+    , detectUtf
+      -- * Simplified events
+    , SEvent (..)
+    , simplify
+    , SAttr
+    , parseFile
+    , parseFile_
+      -- * SEvent parsing
+    , tag
+    , tag'
+    , tag''
+    , content
+    , content'
+      -- * Attribute parsing
+    , AttrParser
+    , requireAttr
+    , optionalAttr
+    , requireAttrRaw
+    , optionalAttrRaw
+    , ignoreAttrs
+      -- * Combinators
+    , choose
+    , many
+    , force
+      -- * Exceptions
+    , XmlException (..)
+    ) where
+import Data.Attoparsec hiding (many)
+import qualified Data.Attoparsec as A
+import Data.Attoparsec.Enumerator
+import Data.XML.Types
+import Data.Word (Word8)
+import Control.Applicative ((<|>), (<$>))
+import Data.Text.Encoding (decodeUtf8With)
+import Data.Text.Lazy (fromChunks, pack, Text)
+import qualified Data.Text.Lazy as T
+import Data.Text.Encoding.Error (lenientDecode)
+import Text.XML.Enumerator.Token
+import Prelude hiding (takeWhile)
+import qualified Data.ByteString as S
+import qualified Data.Map as Map
+import Data.Enumerator (Iteratee, Enumeratee, (>>==), Stream (..),
+                        checkDone, yield, ($$), joinI, run, throwError)
+import qualified Data.Enumerator as E
+import qualified Data.Enumerator.Text as E
+import Control.Monad (unless, ap, liftM)
+import qualified Data.Text as TS
+import Data.List (foldl')
+import Control.Applicative (Applicative (..))
+import Data.Typeable (Typeable)
+import Control.Exception (Exception, throwIO, SomeException)
+import Data.Enumerator.IO (enumFile)
+import Control.Monad.IO.Class (liftIO)
+import Data.Char (isSpace)
+
+amp, hash, charx, semicolon, char0, char9, charA, charZ, chara, charz
+   , colon, equal, squote, dquote, lt, gt, qmark, fslash, exmark, dash
+   , lsquare, rsquare
+    :: Word8
+amp = 38
+hash = 35
+charx = 120
+semicolon = 59
+char0 = 48
+char9 = 57
+charA = 65
+charZ = 90
+chara = 97
+charz = 122
+colon = 58
+equal = 61
+squote = 39
+dquote = 34
+lt = 60
+gt = 62
+qmark = 63
+fslash = 47
+exmark = 33
+dash = 45
+lsquare = 91
+rsquare = 93
+
+tokenToEvent :: [NSLevel] -> Token -> ([NSLevel], [Event])
+tokenToEvent n (TokenBeginDocument _) = (n, [])
+tokenToEvent n (TokenInstruction i) = (n, [EventInstruction i])
+tokenToEvent n (TokenBeginElement name as isClosed) =
+    (n', if isClosed then [begin, end] else [begin])
+  where
+    l0 = case n of
+            [] -> NSLevel Nothing Map.empty
+            x:_ -> x
+    (as', l') = foldl' go (id, l0) as
+    go (front, l) a@(TName kpref kname, val)
+        | kpref == Just "xmlns" =
+            (front, l { prefixes = Map.insert kname (contentsToText val)
+                                 $ prefixes l })
+        | kpref == Nothing && kname == "xmlns" =
+            (front, l { defaultNS = if T.null $ contentsToText val
+                                        then Nothing
+                                        else Just $ contentsToText val })
+        | otherwise = (front . (:) a, l)
+    n' = if isClosed then n else l' : n
+    contentsToText = T.concat . map helper
+    helper (ContentText t) = t
+    helper (ContentEntity _) = T.empty -- FIXME
+    fixAttName level (name', val) = Attribute (tnameToName True level name') val
+    begin = EventBeginElement (tnameToName False l' name) $ map (fixAttName l') $ as' []
+    end = EventEndElement $ tnameToName False l' name
+tokenToEvent n (TokenEndElement name) =
+    (n', [EventEndElement $ tnameToName False l name])
+  where
+    (l, n') =
+        case n of
+            [] -> (NSLevel Nothing Map.empty, [])
+            x:xs -> (x, xs)
+tokenToEvent n (TokenContent c) = (n, [EventContent c])
+tokenToEvent n (TokenComment c) = (n, [EventComment c])
+tokenToEvent n (TokenDoctype t eid) = (n, [EventDoctype $ Doctype t eid []])
+
+tnameToName :: Bool -> NSLevel -> TName -> Name
+tnameToName isAttr (NSLevel def _) (TName Nothing name) =
+    Name name (if isAttr then Nothing else def) Nothing
+tnameToName _ (NSLevel _ m) (TName (Just pref) name) =
+    case Map.lookup pref m of
+        Just ns -> Name name (Just ns) (Just pref)
+        Nothing -> Name name Nothing (Just pref) -- FIXME is this correct?
+
+-- | Automatically determine which UTF variant is being used. This function
+-- first checks for BOMs, removing them as necessary, and then check for the
+-- equivalent of <?xml for each of UTF-8, UTF-16LE/BE, and UTF-32LE/BE. It
+-- defaults to assuming UTF-8. The output byte stream is guaranteed to be valid
+-- UTF-8 bytes.
+detectUtf :: Monad m => Enumeratee S.ByteString S.ByteString m a
+detectUtf param = do
+    x <- takeFourBytes S.empty
+    let (toDrop, mcodec) =
+            case S.unpack x of
+                [0x00, 0x00, 0xFE, 0xFF] -> (4, Just E.utf32_be)
+                [0xFF, 0xFE, 0x00, 0x00] -> (4, Just E.utf32_le)
+                0xFE : 0xFF: _           -> (2, Just E.utf16_be)
+                0xFF : 0xFE: _           -> (2, Just E.utf16_le)
+                0xEF : 0xBB: 0xBF : _    -> (3, Nothing)
+                [0x00, 0x00, 0x00, 0x3C] -> (0, Just E.utf32_be)
+                [0x3C, 0x00, 0x00, 0x00] -> (0, Just E.utf32_le)
+                [0x00, 0x3C, 0x00, 0x3F] -> (0, Just E.utf16_be)
+                [0x3C, 0x00, 0x3F, 0x00] -> (0, Just E.utf16_le)
+                _                        -> (0, Nothing) -- Assuming UTF-8
+    unless (toDrop == 4) $ yield () $ Chunks [S.drop toDrop x]
+    iter <-
+      case mcodec of
+        Nothing -> return param
+        Just codec -> (joinI $ E.decode codec $$ joinI $ E.encode E.utf8 param) >>== return
+    E.map id iter
+  where
+    takeFourBytes front = do
+        x <- E.head
+        case x of
+            Nothing -> return front
+            Just y -> do
+                let z = S.append front y
+                if S.length z < 4
+                    then takeFourBytes z
+                    else do
+                        let (a, b) = S.splitAt 4 z
+                        E.yield a $ Chunks [b]
+
+-- | Parses a UTF8-encoded byte stream into 'Event's. This function is
+-- implemented fully in Haskell using attoparsec for parsing. The produced
+-- error messages do not give line/column information, so you may prefer to
+-- stick with the parser provided by libxml-enumerator. However, this has the
+-- advantage of not relying on any C libraries.
+--
+-- If you are uncertain of the character encoding, use the 'detectUtf'
+-- enumeratee.
+parseBytes :: Monad m => Enumeratee S.ByteString Event m a
+parseBytes =
+    checkDone $ \k -> k (Chunks [EventBeginDocument]) >>== loop []
+  where
+    loop levels = checkDone $ go levels
+    go levels k = do
+        mtoken <- iterToken
+        case mtoken of
+            Nothing -> k (Chunks [EventEndDocument]) >>== return
+            Just token ->
+                let (levels', events) = tokenToEvent levels token
+                 in k (Chunks events) >>== loop levels'
+
+iterToken :: Monad m => Iteratee S.ByteString m (Maybe Token)
+iterToken = iterParser ((endOfInput >> return Nothing) <|> fmap Just parseToken)
+
+parseToken :: Parser Token
+parseToken = do
+    (word8 lt >> parseLt) <|> fmap TokenContent (parseContent False False)
+  where
+    parseLt =
+        (word8 qmark >> parseInstr) <|>
+        (word8 exmark >> (parseComment <|> parseCdata <|> parseDoctype)) <|>
+        (word8 fslash >> parseEnd) <|>
+        parseBegin
+    parseInstr = do
+        name <- parseIdent
+        if name == "xml"
+            then do
+                as <- A.many parseAttribute
+                skipSpace
+                word8' qmark
+                word8' gt
+                newline <|> return ()
+                return $ TokenBeginDocument as
+            else do
+                skipSpace
+                x <- toText <$> takeWhile (/= qmark)
+                word8' qmark
+                word8' gt
+                return $ TokenInstruction $ Instruction name x
+    parseComment = do
+        word8' dash
+        word8' dash
+        c <- toText . S.pack <$> manyTill anyWord8 (string "-->") -- FIXME use takeWhile instead
+        return $ TokenComment c
+    parseCdata = do
+        _ <- string "[CDATA["
+        t <- toText . S.pack <$> manyTill anyWord8 (string "]]>") -- FIXME use takeWhile instead
+        return $ TokenContent $ ContentText t
+    parseDoctype = do
+        _ <- string "DOCTYPE"
+        skipSpace
+        i <- parseIdent
+        skipSpace
+        eid <- fmap Just parsePublicID <|>
+               fmap Just parseSystemID <|>
+               return Nothing
+        skipSpace
+        (do
+            word8' lsquare
+            skipWhile (/= rsquare)
+            word8' rsquare
+            skipSpace) <|> return ()
+        word8' gt
+        newline
+        return $ TokenDoctype i eid
+    parsePublicID = do
+        _ <- string "PUBLIC"
+        x <- quotedText
+        y <- quotedText
+        return $ PublicID x y
+    parseSystemID = do
+        _ <- string "SYSTEM"
+        x <- quotedText
+        return $ SystemID x
+    quotedText = do
+        skipSpace
+        toText <$> (between dquote <|> between squote)
+    between c = do
+        word8' c
+        x <- takeWhile (/=c)
+        word8' c
+        return x
+    parseEnd = do
+        skipSpace
+        n <- parseName
+        skipSpace
+        word8' gt
+        return $ TokenEndElement n
+    parseBegin = do
+        skipSpace
+        n <- parseName
+        as <- A.many parseAttribute
+        skipSpace
+        isClose <- (word8 fslash >> skipSpace >> return True) <|> return False
+        word8' gt
+        return $ TokenBeginElement n as isClose
+
+parseAttribute :: Parser TAttribute
+parseAttribute = do
+    skipSpace
+    key <- parseName
+    skipSpace
+    word8' equal
+    skipSpace
+    val <- squoted <|> dquoted
+    return (key, val)
+  where
+    squoted = do
+        word8' squote
+        manyTill (parseContent False True) (word8 squote)
+    dquoted = do
+        word8' dquote
+        manyTill (parseContent True False) (word8 dquote)
+
+parseName :: Parser TName
+parseName = do
+    i1 <- parseIdent
+    mi2 <- (word8 colon >> fmap Just parseIdent) <|> return Nothing
+    return $
+        case mi2 of
+            Nothing -> TName Nothing i1
+            Just i2 -> TName (Just i1) i2
+
+parseIdent :: Parser Text
+parseIdent =
+    toText <$> takeWhile1 valid
+  where
+    valid 38 = False -- amp
+    valid 60 = False -- lt
+    valid 62 = False -- gt
+    valid 58 = False -- colon
+    valid 63 = False -- qmark
+    valid 61 = False -- equal
+    valid 34 = False -- dquote
+    valid 39 = False -- squote
+    valid 47 = False -- fslash
+    valid c  = not $ isSpaceW c
+
+isSpaceW :: Word8 -> Bool
+isSpaceW 0x09 = True
+isSpaceW 0x0A = True
+isSpaceW 0x0D = True
+isSpaceW 0x20 = True
+isSpaceW _    = False
+
+parseContent :: Bool -- break on double quote
+             -> Bool -- break on single quote
+             -> Parser Content
+parseContent breakDouble breakSingle =
+    parseEntity <|> parseText
+  where
+    parseEntity = do
+        word8' amp
+        parseEntityNum <|> parseEntityWord
+    parseEntityNum = do
+        word8' hash
+        w <- parseEntityHex <|> parseEntityDig
+        return $ ContentText $ pack [toEnum w]
+    parseEntityHex = do
+        word8' charx
+        res <- hexadecimal
+        word8' semicolon
+        return res
+    hexadecimal = do
+        x <- hex
+        hexadecimal' $ fromIntegral x
+    hexadecimal' x = (do
+        y <- hex
+        hexadecimal' $ x * 16 + fromIntegral y
+        ) <|> return x
+    hex = satisfyWith hex' (< 16)
+    hex' w
+        | char0 <= w && w <= char9 = w - char0
+        | charA <= w && w <= charZ = w - charA + 10
+        | chara <= w && w <= charz = w - chara + 10
+        | otherwise = 16 -- failing case
+    parseEntityDig = do
+        res <- decimal
+        word8' semicolon
+        return res
+    decimal = do
+        x <- dig
+        decimal' $ fromIntegral x
+    decimal' x = (do
+        y <- dig
+        decimal' $ x * 10 + fromIntegral y
+        ) <|> return x
+    dig = satisfyWith dig' (< 10)
+    dig' w
+        | char0 <= w && w <= char9 = w - char0
+        | otherwise = 10 -- failing case
+    parseEntityWord = do
+        s <- takeWhile1 (/= semicolon)
+        word8' semicolon
+        return $ case s of
+            _
+                | s == "amp"  -> ContentText "&"
+                | s == "gt"   -> ContentText ">"
+                | s == "lt"   -> ContentText "<"
+                | s == "apos" -> ContentText "'"
+                | s == "quot" -> ContentText "\""
+                | otherwise   ->
+                    ContentEntity $ toText s
+    parseText = do
+        bs <- takeWhile1 valid
+        return $ ContentText $ toText bs
+    valid 34 = not breakDouble
+    valid 39 = not breakSingle
+    valid 38 = False -- amp
+    valid 60 = False -- lt
+    valid _  = True
+
+toText :: S.ByteString -> Text
+toText = fromChunks . return
+       . TS.replace "\r\n" "\n" -- FIXME do this more efficiently
+       . decodeUtf8With lenientDecode
+
+skipSpace :: Parser ()
+skipSpace = skipWhile isSpaceW
+
+newline :: Parser ()
+newline = ((word8 13 >> word8 10) <|> word8 10) >> return ()
+
+word8' :: Word8 -> Parser ()
+word8' c = word8 c >> return ()
+
+-- | A simplified attribute, having all entities converted to text.
+type SAttr = (Name, Text)
+
+-- | A greatly simplified XML event datatype. The best way to produce these
+-- values is the 'simplify' enumeratee.
+data SEvent = SBeginElement Name [SAttr]
+            | SEndElement
+            | SContent Text
+    deriving (Show, Eq)
+
+-- | Grabs the next piece of content if available.
+content :: Monad m => Iteratee SEvent m (Maybe Text)
+content = do
+    x <- E.peek
+    case x of
+        Just (SContent t) -> E.drop 1 >> return (Just t)
+        _ -> return Nothing
+
+-- | Grabs the next piece of content. If none if available, returns 'T.empty'.
+content' :: Monad m => Iteratee SEvent m Text
+content' = do
+    x <- content
+    case x of
+        Nothing -> return T.empty
+        Just y -> return y
+
+-- | The most generic way to parse a tag. It takes a predicate for checking if
+-- this is the correct tag name, an 'AttrParser' for handling attributes, and
+-- then a parser for dealing with content.
+--
+-- This function automatically absorbs its balancing closing tag, and will
+-- throw an exception if not all of the attributes or child elements are
+-- consumed. If you want to allow extra attributes, see 'ignoreAttrs'.
+tag :: Monad m
+    => (Name -> Maybe a)
+    -> (a -> AttrParser b)
+    -> (b -> Iteratee SEvent m c)
+    -> Iteratee SEvent m (Maybe c)
+tag checkName attrParser f = do
+    x <- dropWS
+    case x of
+        Just (SBeginElement name as) ->
+            case checkName name of
+                Just y ->
+                    case runAttrParser' (attrParser y) as of
+                        Left e -> throwError e
+                        Right z -> do
+                            E.drop 1
+                            z' <- f z
+                            a <- dropWS
+                            case a of
+                                Just SEndElement -> E.drop 1 >> return (Just z')
+                                _ -> throwError $ SXmlException ("Expected end tag for: " ++ show name) a
+                Nothing -> return Nothing
+        _ -> return Nothing
+  where
+    dropWS = do
+        x <- E.peek
+        case x of
+            Just (SContent t)
+                | T.all isSpace t -> E.drop 1 >> E.peek
+            _ -> return x
+    runAttrParser' p as =
+        case runAttrParser p as of
+            Left e -> Left e
+            Right ([], x) -> Right x
+            Right (attr, _) -> Left $ UnparsedAttributes attr
+
+-- | A simplified version of 'tag' which matches for specific tag names instead
+-- of taking a predicate function. This is often sufficient, and when combined
+-- with OverloadedStrings and the IsString instance of 'Name', can prove to be
+-- very concise.
+tag' :: Monad m
+     => Name
+     -> AttrParser a
+     -> (a -> Iteratee SEvent m b)
+     -> Iteratee SEvent m (Maybe b)
+tag' name attrParser = tag
+    (\x -> if x == name then Just () else Nothing)
+    (const attrParser)
+
+-- | A further simplified tag parser, which requires that no attributes exist.
+tag'' :: Monad m => Name -> Iteratee SEvent m a -> Iteratee SEvent m (Maybe a)
+tag'' name f = tag' name (return ()) $ const f
+
+-- | Get the value of the first parser which returns 'Just'. If none return
+-- 'Just', returns 'Nothing'.
+choose :: Monad m
+       => [Iteratee SEvent m (Maybe a)]
+       -> Iteratee SEvent m (Maybe a)
+choose [] = return Nothing
+choose (i:is) = do
+    x <- i
+    case x of
+        Nothing -> choose is
+        Just a -> return $ Just a
+
+-- | Force an optional parser into a required parser. All of the 'tag'
+-- functions, 'choose' and 'many' deal with 'Maybe' parsers. Use this when you
+-- want to finally force something to happen.
+force :: Monad m
+      => String -- ^ Error message
+      -> Iteratee SEvent m (Maybe a)
+      -> Iteratee SEvent m a
+force msg i = do
+    x <- i
+    case x of
+        Nothing -> throwError $ XmlException msg Nothing
+        Just a -> return a
+
+-- | Convert a stream of 'Event's into a stream 'SEvent's. The first argument
+-- is a function to decode character entity references. Some things to note
+-- about this function:
+--
+-- * It drops events for document begin/end, comments, and instructions.
+--
+-- * It concatenates all pieces of content together. The output of this
+-- function is guaranteed to not have two consecutive 'SContent's.
+--
+-- * It automatically checks that tag beginnings and endings are well balanced,
+-- and throws an exception otherwise.
+--
+-- * It also throws an exception if your supplied entity function does not know
+-- how to deal with a character entity.
+--
+-- Please also note that you do /not/ need to handle the 5 XML-defined
+-- character entity references (lt, gt, amp, quot and apos), nor deal with
+-- numeric entities (decimal and hex).
+simplify :: Monad m => (Text -> Maybe Text) -> Enumeratee Event SEvent m b
+simplify renderEntity =
+    loop []
+  where
+    loop stack = E.checkDone $ go stack
+    sattr (Attribute x y) = do
+        y' <- flip mapM y $ \z ->
+            case z of
+                ContentText t -> return t
+                ContentEntity t ->
+                    case renderEntity t of
+                        Just t' -> return t'
+                        Nothing -> throwError $ InvalidEntity t
+        return (x, T.concat y')
+    go stack k = do
+        x <- E.head
+        case x of
+            Nothing -> k EOF >>== return
+            Just EventBeginDocument -> go stack k
+            Just EventEndDocument ->
+                k EOF >>== return
+            Just EventInstruction{} -> go stack k
+            Just EventDoctype{} -> go stack k
+            Just (EventBeginElement n as) -> do
+                as' <- mapM sattr as
+                k (Chunks [SBeginElement n as']) >>== loop (n : stack)
+            Just (EventEndElement n) ->
+                case stack of
+                    [] -> throwError $ InvalidEndElement n
+                    n':rest
+                        | n == n' -> k (Chunks [SEndElement]) >>== loop rest
+                        | otherwise -> throwError $ InvalidEndElement n
+            Just (EventContent c) -> do
+                t <- contentToText c
+                ts <- takeContents $ (:) t
+                k (Chunks [SContent $ T.concat $ ts []]) >>== loop stack
+            Just EventComment{} -> go stack k
+      where
+        contentToText (ContentEntity e) =
+            case renderEntity e of
+                Nothing -> throwError $ InvalidEntity e
+                Just t -> return t
+        contentToText (ContentText t) = return t
+        takeContents front = do
+            x <- E.peek
+            case x of
+                Nothing -> return front
+                Just EventBeginElement{} -> return front
+                Just EventEndElement{} -> return front
+                Just (EventContent c) -> do
+                    E.drop 1
+                    t <- contentToText c
+                    takeContents $ front . (:) t
+                Just EventBeginDocument -> takeContents front
+                Just EventEndDocument -> takeContents front
+                Just EventInstruction{} -> takeContents front
+                Just EventDoctype{} -> takeContents front
+                Just EventComment{} -> takeContents front
+
+-- | The same as 'parseFile', but throws any exceptions.
+parseFile_ :: String -> (Text -> Maybe Text) -> Iteratee SEvent IO a -> IO a
+parseFile_ fn re p =
+    parseFile fn re p >>= go
+  where
+    go (Left e) = liftIO $ throwIO e
+    go (Right a) = return a
+
+-- | A helper function which reads a file from disk using 'enumFile', detects
+-- character encoding using 'detectUtf', parses the XML using 'parseBytes',
+-- converts to an 'SEvent' stream using 'simplify' and then handing off control
+-- to your supplied parser.
+parseFile :: String -> (Text -> Maybe Text) -> Iteratee SEvent IO a -> IO (Either SomeException a)
+parseFile fn re p =
+    run $ enumFile fn     $$ joinI
+        $ detectUtf       $$ joinI
+        $ parseBytes      $$ joinI
+        $ simplify re     $$ p
+
+data XmlException = XmlException
+    { xmlErrorMessage :: String
+    , xmlBadInput :: Maybe Event
+    }
+                  | InvalidEndElement Name
+                  | InvalidEntity Text
+                  | SXmlException
+    { xmlErrorMessage :: String
+    , sxmlBadInput :: Maybe SEvent
+    }
+                  | UnparsedAttributes [SAttr]
+    deriving (Show, Typeable)
+instance Exception XmlException
+
+-- | A monad for parsing attributes. By default, it requires you to deal with
+-- all attributes present on an element, and will throw an exception if there
+-- are unhandled attributes. Use the 'requireAttr', 'optionalAttr' et al
+-- functions for handling an attribute, and 'ignoreAttrs' if you would like to
+-- skip the rest of the attributes on an element.
+newtype AttrParser a = AttrParser { runAttrParser :: [SAttr] -> Either XmlException ([SAttr], a) }
+
+instance Monad AttrParser where
+    return a = AttrParser $ \as -> Right (as, a)
+    (AttrParser f) >>= g = AttrParser $ \as ->
+        case f as of
+            Left e -> Left e
+            Right (as', f') -> runAttrParser (g f') as'
+instance Functor AttrParser where
+    fmap = liftM
+instance Applicative AttrParser where
+    pure = return
+    (<*>) = ap
+
+optionalAttrRaw :: (SAttr -> Maybe b) -> AttrParser (Maybe b)
+optionalAttrRaw f =
+    AttrParser $ go id
+  where
+    go front [] = Right (front [], Nothing)
+    go front (a:as) =
+        case f a of
+            Nothing -> go (front . (:) a) as
+            Just b -> Right (front as, Just b)
+
+requireAttrRaw :: String -> (SAttr -> Maybe b) -> AttrParser b
+requireAttrRaw msg f = do
+    x <- optionalAttrRaw f
+    case x of
+        Just b -> return b
+        Nothing -> AttrParser $ const $ Left $ XmlException msg Nothing
+
+-- | Require that a certain attribute be present and return its value.
+requireAttr :: Name -> AttrParser Text
+requireAttr n = requireAttrRaw
+    ("Missing attribute: " ++ show n)
+    (\(x, y) -> if x == n then Just y else Nothing)
+
+-- | Return the value for an attribute if present.
+optionalAttr :: Name -> AttrParser (Maybe Text)
+optionalAttr n = optionalAttrRaw
+    (\(x, y) -> if x == n then Just y else Nothing)
+
+-- | Skip the remaining attributes on an element. Since this will clear the
+-- list of attributes, you must call this /after/ any calls to 'requireAttr',
+-- 'optionalAttr', etc.
+ignoreAttrs :: AttrParser ()
+ignoreAttrs = AttrParser $ \_ -> Right ([], ())
+
+-- | Keep parsing elements as long as the parser returns 'Just'.
+many :: Monad m => Iteratee SEvent m (Maybe a) -> Iteratee SEvent m [a]
+many i =
+    go id
+  where
+    go front = do
+        x <- i
+        case x of
+            Nothing -> return $ front []
+            Just y -> go $ front . (:) y
diff --git a/Text/XML/Enumerator/Render.hs b/Text/XML/Enumerator/Render.hs
new file mode 100644
--- /dev/null
+++ b/Text/XML/Enumerator/Render.hs
@@ -0,0 +1,132 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | 'Enumeratee's to render XML 'Event's. Unlike libxml-enumerator and
+-- expat-enumerator, this module does not provide IO and ST variants, since the
+-- underlying rendering operations are pure functions.
+module Text.XML.Enumerator.Render
+    ( renderBuilder
+    , renderBytes
+    ) where
+
+import Data.XML.Types ( Event (..), Content (..), Name (..), Attribute (..)
+                      , Doctype (..))
+import Text.XML.Enumerator.Token
+import qualified Data.Enumerator as E
+import Data.Enumerator ((>>==), ($$))
+import qualified Data.Text.Lazy as T
+import Data.Text.Lazy (Text)
+import Blaze.ByteString.Builder
+import Blaze.ByteString.Builder.Enumerator (builderToByteString)
+import qualified Data.Map as Map
+import Data.Map (Map)
+import Data.Maybe (fromMaybe)
+import Data.ByteString (ByteString)
+import Control.Monad.IO.Class (MonadIO)
+
+-- | Render a stream of 'Event's into a stream of 'ByteString's. This function
+-- wraps around 'renderBuilder' and 'builderToByteString', so it produces
+-- optimally sized 'ByteString's with minimal buffer copying.
+renderBytes :: MonadIO m => E.Enumeratee Event ByteString m b
+renderBytes s = E.joinI $ renderBuilder $$ builderToByteString s
+
+-- | Render a stream of 'Event's into a stream of 'Builder's. Builders are from
+-- the blaze-builder package, and allow the create of optimally sized
+-- 'ByteString's with minimal buffer copying.
+renderBuilder :: Monad m => E.Enumeratee Event Builder m b
+renderBuilder =
+    loop []
+  where
+    loop stack = E.checkDone $ step stack
+    step stack k = do
+        x <- E.head
+        case x of
+            Nothing -> E.yield (E.Continue k) E.EOF
+            Just (EventBeginElement name as) -> do
+                x' <- E.peek
+                if x' == Just (EventEndElement name)
+                    then do
+                        E.drop 1
+                        go $ mkBeginToken True stack name as
+                    else go $ mkBeginToken False stack name as
+            Just e -> go $ eventToToken stack e
+      where
+        go (ts, stack') = k (E.Chunks $ map tokenToBuilder $ ts []) >>== loop stack'
+
+eventToToken :: Stack -> Event -> ([Token] -> [Token], [NSLevel])
+eventToToken s EventBeginDocument =
+    ((:) (TokenBeginDocument
+            [ ("version", [ContentText "1.0"])
+            , ("encoding", [ContentText "UTF-8"])
+            ])
+     , s)
+eventToToken s EventEndDocument = (id, s)
+eventToToken s (EventInstruction i) = ((:) (TokenInstruction i), s)
+eventToToken s (EventDoctype (Doctype n meid _)) =
+    ((:) (TokenDoctype n meid), s)
+eventToToken s (EventBeginElement name attrs) = mkBeginToken False s name attrs
+eventToToken s (EventEndElement name) =
+    ((:) (TokenEndElement $ nameToTName sl name), s')
+  where
+    (sl:s') = s
+eventToToken s (EventContent c) = ((:) (TokenContent c), s)
+eventToToken s (EventComment t) = ((:) (TokenComment t), s)
+
+type Stack = [NSLevel]
+
+nameToTName :: NSLevel -> Name -> TName
+nameToTName _ (Name name _ (Just pref))
+    | pref == "xml" = TName (Just "xml") name
+nameToTName _ (Name name Nothing _) = TName Nothing name -- invariant that this is true
+nameToTName (NSLevel def sl) (Name name (Just ns) _)
+    | def == Just ns = TName Nothing name
+    | otherwise =
+        case Map.lookup ns sl of
+            Nothing -> error "nameToTName"
+            Just pref -> TName (Just pref) name
+
+mkBeginToken :: Bool -> Stack -> Name -> [Attribute]
+             -> ([Token] -> [Token], Stack)
+mkBeginToken isClosed s name attrs =
+    ((:) (TokenBeginElement tname tattrs2 isClosed),
+     if isClosed then s else sl2 : s)
+  where
+    prevsl = case s of
+                [] -> NSLevel Nothing Map.empty
+                sl':_ -> sl'
+    (sl1, tname, tattrs1) = newElemStack prevsl name
+    (sl2, tattrs2) = foldr newAttrStack (sl1, tattrs1) attrs
+
+newElemStack :: NSLevel -> Name -> (NSLevel, TName, [TAttribute])
+newElemStack nsl@(NSLevel def _) (Name local ns _)
+    | def == ns = (nsl, TName Nothing local, [])
+newElemStack (NSLevel _ nsmap) (Name local Nothing _) =
+    (NSLevel Nothing nsmap, TName Nothing local, [(TName Nothing "xmlns", [])])
+newElemStack (NSLevel _ nsmap) (Name local (Just ns) Nothing) =
+    (NSLevel (Just ns) nsmap, TName Nothing local, [(TName Nothing "xmlns", [ContentText ns])])
+newElemStack (NSLevel def nsmap) (Name local (Just ns) (Just pref)) =
+    (NSLevel def nsmap', TName (Just pref) local, [(TName (Just "xmlns") pref, [ContentText ns])])
+  where
+    nsmap' = Map.insert ns pref nsmap
+
+newAttrStack :: Attribute -> (NSLevel, [TAttribute]) -> (NSLevel, [TAttribute])
+newAttrStack (Attribute name value) (NSLevel def nsmap, attrs) =
+    (NSLevel def nsmap', addNS $ (tname, value) : attrs)
+  where
+    (nsmap', tname, addNS) =
+        case name of
+            Name local Nothing _ -> (nsmap, TName Nothing local, id)
+            Name local (Just ns) mpref ->
+                let ppref = fromMaybe "ns" mpref
+                    (pref, addNS') = getPrefix ppref nsmap ns
+                 in (Map.insert ns pref nsmap, TName (Just pref) local, addNS')
+
+getPrefix :: Text -> Map Text Text -> Text -> (Text, [TAttribute] -> [TAttribute])
+getPrefix ppref nsmap ns =
+    case Map.lookup ns nsmap of
+        Just pref -> (pref, id)
+        Nothing ->
+            let pref = findUnused ppref $ Map.elems nsmap
+             in (pref, (:) (TName (Just "xmlns") pref, [ContentText ns]))
+  where
+    findUnused x xs
+        | x `elem` xs = findUnused (x `T.snoc` '_') xs
+        | otherwise = x
diff --git a/Text/XML/Enumerator/Token.hs b/Text/XML/Enumerator/Token.hs
new file mode 100644
--- /dev/null
+++ b/Text/XML/Enumerator/Token.hs
@@ -0,0 +1,126 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Text.XML.Enumerator.Token
+    ( tokenToBuilder
+    , TName (..)
+    , Token (..)
+    , TAttribute
+    , NSLevel (..)
+    ) where
+
+import Data.XML.Types (Instruction (..), Content (..), ExternalID (..))
+import qualified Data.Text.Lazy as T
+import Data.Text.Lazy (Text)
+import Data.String (IsString (fromString))
+import Blaze.ByteString.Builder (Builder, fromByteString, writeByteString)
+import Blaze.ByteString.Builder.Internal.Write (fromWriteList)
+import Blaze.ByteString.Builder.Char.Utf8 (writeChar, fromLazyText)
+import Data.Monoid (mconcat, mempty)
+import Data.ByteString.Char8 ()
+import Data.Map (Map)
+
+data Token = TokenBeginDocument [TAttribute]
+           | TokenInstruction Instruction
+           | TokenBeginElement TName [TAttribute] Bool
+           | TokenEndElement TName
+           | TokenContent Content
+           | TokenComment Text
+           | TokenDoctype Text (Maybe ExternalID)
+    deriving Show
+tokenToBuilder :: Token -> Builder
+tokenToBuilder (TokenBeginDocument attrs) =
+    mconcat $ fromByteString "<?xml" : foldAttrs attrs [fromByteString "?>\n"]
+tokenToBuilder (TokenInstruction (Instruction target data_)) = mconcat
+    [ fromByteString "<?"
+    , fromLazyText target
+    , fromByteString " "
+    , fromLazyText data_
+    , fromByteString "?>"
+    ]
+tokenToBuilder (TokenBeginElement name attrs isEmpty) = mconcat
+    $ fromByteString "<"
+    : tnameToText name
+    : foldAttrs attrs
+    [ if isEmpty then fromByteString "/>" else fromByteString ">"
+    ]
+tokenToBuilder (TokenEndElement name) = mconcat
+    [ fromByteString "</"
+    , tnameToText name
+    , fromByteString ">"
+    ]
+tokenToBuilder (TokenContent c) = contentToText c
+tokenToBuilder (TokenComment t) = mconcat [fromByteString "<!--", fromLazyText t, fromByteString "-->"]
+tokenToBuilder (TokenDoctype name eid) = mconcat
+    [ fromByteString "<!DOCTYPE "
+    , fromLazyText name
+    , go eid
+    , fromByteString ">\n"
+    ]
+  where
+    go Nothing = mempty
+    go (Just (SystemID uri)) = mconcat
+        [ fromByteString " SYSTEM \""
+        , fromLazyText uri
+        , fromByteString "\""
+        ]
+    go (Just (PublicID pid uri)) = mconcat
+        [ fromByteString " PUBLIC \""
+        , fromLazyText pid
+        , fromByteString "\" \""
+        , fromLazyText uri
+        , fromByteString "\""
+        ]
+
+data TName = TName (Maybe Text) Text
+    deriving Show
+
+tnameToText :: TName -> Builder
+tnameToText (TName Nothing name) = fromLazyText name
+tnameToText (TName (Just prefix) name) = mconcat [fromLazyText prefix, fromByteString ":", fromLazyText name]
+
+contentToText :: Content -> Builder
+contentToText (ContentText t) =
+    fromWriteList go $ T.unpack t
+  where
+    go '<' = writeByteString "&lt;"
+    go '>' = writeByteString "&gt;"
+    go '&' = writeByteString "&amp;"
+    -- Not escaping quotes, since this is only called outside of attributes
+    go c   = writeChar c
+contentToText (ContentEntity e) = mconcat
+    [ fromByteString "&"
+    , fromLazyText e
+    , fromByteString ";"
+    ]
+
+type TAttribute = (TName, [Content])
+
+foldAttrs :: [TAttribute] -> [Builder] -> [Builder]
+foldAttrs attrs rest' =
+    foldr go rest' attrs
+  where
+    go (key, val) rest =
+        fromByteString " "
+      : tnameToText key
+      : fromByteString "=\""
+      : foldr go' (fromByteString "\"" : rest) val
+    go' (ContentText t) rest =
+        fromWriteList h (T.unpack t) : rest
+      where
+        h '<' = writeByteString "&lt;"
+        h '>' = writeByteString "&gt;"
+        h '&' = writeByteString "&amp;"
+        h '"' = writeByteString "&quot;"
+        -- Not escaping single quotes, since our attributes are always double
+        -- quoted
+        h c   = writeChar c
+    go' (ContentEntity t) rest =
+        fromByteString "&" : fromLazyText t : fromByteString ";" : rest
+
+instance IsString TName where
+    fromString = TName Nothing . T.pack
+
+data NSLevel = NSLevel
+    { defaultNS :: Maybe Text
+    , prefixes :: Map Text Text
+    }
+    deriving Show
diff --git a/xml-enumerator.cabal b/xml-enumerator.cabal
new file mode 100644
--- /dev/null
+++ b/xml-enumerator.cabal
@@ -0,0 +1,35 @@
+name:            xml-enumerator
+version:         0.0.0
+license:         BSD3
+license-file:    LICENSE
+author:          Michael Snoyman <michaels@suite-sol.com>
+maintainer:      Michael Snoyman <michaels@suite-sol.com>
+synopsis:        Pure-Haskell utilities for dealing with XML with the enumerator package.
+description:
+    Provides the ability to parse and render XML in a streaming manner using the enumerator package.
+category:        Web
+stability:       Stable
+cabal-version:   >= 1.6
+build-type:      Simple
+homepage:        http://github.com/snoyberg/xml-enumerator
+
+library
+    build-depends:   base                      >= 4        && < 5
+                   , enumerator                >= 0.4      && < 0.5
+                   , bytestring                >= 0.9      && < 0.10
+                   , text                      >= 0.7      && < 0.12
+                   , containers                >= 0.2      && < 0.5
+                   , xml-types                 >= 0.1      && < 0.2
+                   , attoparsec-enumerator     >= 0.2.0.3  && < 0.3
+                   , attoparsec                >= 0.8      && < 0.9
+                   , blaze-builder             >= 0.2.1.0  && < 0.3
+                   , blaze-builder-enumerator  >= 0.2      && < 0.3
+                   , transformers              >= 0.2      && < 0.3
+    exposed-modules: Text.XML.Enumerator.Parse
+                     Text.XML.Enumerator.Render
+    other-modules:   Text.XML.Enumerator.Token
+    ghc-options:     -Wall
+
+source-repository head
+  type:     git
+  location: git://github.com/snoyberg/xml-enumerator.git
