diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,23 @@
+## 1.10.1.0
+* Add `document` function to wrap a stream of XML events in an 'EventBeginDocument'/'EventEndDocument' pair.
+* Ensure compatibility with GHC 9.10
+
+## 1.10.0.1
+* Fix doctest on GHC 9.12
+
+## 1.10.0.0
+
+* `rsPretty` is now exported only from the new module `Text.XML.Render.Internal`
+  - Migration path: Import it from its new home.
+  - Since it is now internal, be prepared for it to be changed or removed in some future version.
+* `prettify` is no longer exported at all.
+  - Migration path: Use `rsPretty = True` in your `RenderSettings` when you finally render the stream instead of inserting the `prettify` combinator into your conduit pipeline.
+  - Same caveat as above about `rsPretty` now being internal
+  - If you require having the prettified version of an XML event stream as a conduit before rendering (we hope this use case occurs rarely if ever), things will be more awkward:
+    - The hack of rendering with `rsPretty = True` and re-parsing might work.
+    - Another hack could be to fork this library and add `prettify` to the export list in `Text.XML.Render.Internal`.
+    - Otherwise, you might need to implement pretty-printing yourself. The source code of `Text.XML.Render.Internal` might be helpful in that case.
+
 ## 1.9.1.1
 
 * Entity declarations with tags inside are now correctly handled
diff --git a/src/Text/XML.hs b/src/Text/XML.hs
--- a/src/Text/XML.hs
+++ b/src/Text/XML.hs
@@ -59,7 +59,6 @@
     , P.decodeHtmlEntities
       -- ** Rendering
     , R.RenderSettings
-    , R.rsPretty
     , R.rsNamespaces
     , R.rsAttrOrder
     , R.rsUseCDATA
diff --git a/src/Text/XML/Stream/Parse.hs b/src/Text/XML/Stream/Parse.hs
--- a/src/Text/XML/Stream/Parse.hs
+++ b/src/Text/XML/Stream/Parse.hs
@@ -1301,9 +1301,19 @@
 --
 -- If an opening-tag is consumed but no matching closing-tag is found, an 'XmlException' is thrown.
 --
--- >>> runConduit $ parseLBS def "<a><b></b>" .| void (takeTree "a" ignoreAttrs) .| sinkList
--- *** Exception: InvalidEndElement (Name {nameLocalName = "a", nameNamespace = Nothing, namePrefix = Nothing}) Nothing
---
+{-
+>>> runConduit $ parseLBS def "<a><b></b>" .| void (takeTree "a" ignoreAttrs) .| sinkList
+#if MIN_VERSION_base(4, 21, 0)
+*** Exception: Error while parsing XML event: expected </Name {nameLocalName = "a", nameNamespace = Nothing, namePrefix = Nothing}>, got nothing
+
+^
+HasCallStack backtrace:
+  throwIO, called at libraries/exceptions/src/Control/Monad/Catch.hs:308:12 in exceptions-0.10.9-inplace:Control.Monad.Catch
+  throwM, called at /home/alexfmpe/profiles/alexfmpe/repos/xml/xml-conduit/src/Text/XML/Stream/Parse.hs:1324:71 in main:Text.XML.Stream.Parse
+#else
+*** Exception: InvalidEndElement (Name {nameLocalName = "a", nameNamespace = Nothing, namePrefix = Nothing}) Nothing
+#endif
+-}
 -- This function automatically ignores comments, instructions and whitespace.
 --
 -- Returns @Just ()@ if an element was consumed, 'Nothing' otherwise.
diff --git a/src/Text/XML/Stream/Render.hs b/src/Text/XML/Stream/Render.hs
--- a/src/Text/XML/Stream/Render.hs
+++ b/src/Text/XML/Stream/Render.hs
@@ -1,427 +1,32 @@
-{-# LANGUAGE CPP  #-}
-{-# LANGUAGE FlexibleContexts  #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes        #-}
--- | '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.Stream.Render
-    ( -- * Rendering XML files
-      renderBuilder
-    , renderBuilderFlush
-    , renderBytes
-    , renderText
-    , prettify
-      -- * Renderer settings
-    , RenderSettings
-    , def
-    , rsPretty
-    , rsNamespaces
-    , rsAttrOrder
-    , rsUseCDATA
-    , rsXMLDeclaration
-    , orderAttrs
-      -- * Event rendering
-    , tag
-    , content
-      -- * Attribute rendering
-    , Attributes
-    , attr
-    , optionalAttr
-    ) where
-
-import           Control.Applicative          ((<$>))
-import           Control.Monad.Trans.Resource (MonadThrow)
-import           Data.ByteString              (ByteString)
-import           Data.ByteString.Builder      (Builder)
-import           Conduit
-import           Data.Default           (Default (def))
-import           Data.List                    (foldl')
-import           Data.Map                     (Map)
-import qualified Data.Map                     as Map
-import           Data.Maybe                   (fromMaybe, mapMaybe)
-import           Data.Monoid                  (Monoid, mappend, mempty)
-import qualified Data.Set                     as Set
-import           Data.Text                    (Text)
-import qualified Data.Text                    as T
-import           Data.XML.Types               (Content (..), Event (..),
-                                               Name (..))
-import           Text.XML.Stream.Token
-
--- | 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.
---
--- The output is UTF8 encoded.
-renderBytes :: PrimMonad m => RenderSettings -> ConduitT Event ByteString m ()
-renderBytes rs = renderBuilder rs .| builderToByteString
-
--- | Render a stream of 'Event's into a stream of 'Text's. This function
--- wraps around 'renderBuilder', 'builderToByteString' and 'renderBytes', so it
--- produces optimally sized 'Text's with minimal buffer copying.
-renderText :: (PrimMonad m, MonadThrow m) => RenderSettings -> ConduitT Event Text m ()
-renderText rs = renderBytes rs .| decodeUtf8C
-
-{-# DEPRECATED rsPretty "Will be removed in future versions of `xml-conduit`. See <https://github.com/snoyberg/xml/issues/196>." #-}
-data RenderSettings = RenderSettings
-    { rsPretty     :: Bool
-    , rsNamespaces :: [(Text, Text)]
-      -- ^ Defines some top level namespace definitions to be used, in the form
-      -- of (prefix, namespace). This has absolutely no impact on the meaning
-      -- of your documents, but can increase readability by moving commonly
-      -- used namespace declarations to the top level.
-    , rsAttrOrder  :: Name -> Map.Map Name Text -> [(Name, Text)]
-      -- ^ Specify how to turn the unordered attributes used by the "Text.XML"
-      -- module into an ordered list.
-    , rsUseCDATA   :: Content -> Bool
-      -- ^ Determines if for a given text content the renderer should use a
-      -- CDATA node.
-      --
-      -- Default: @False@
-      --
-      -- @since 1.3.3
-    , rsXMLDeclaration :: Bool
-      -- ^ Determines whether the XML declaration will be output.
-      --
-      -- Default: @True@
-      --
-      -- @since 1.5.1
-    }
-
-instance Default RenderSettings where
-    def = RenderSettings
-        { rsPretty = False
-        , rsNamespaces = []
-        , rsAttrOrder = const Map.toList
-        , rsUseCDATA = const False
-        , rsXMLDeclaration = True
-        }
-
--- | Convenience function to create an ordering function suitable for
--- use as the value of 'rsAttrOrder'. The ordering function is created
--- from an explicit ordering of the attributes, specified as a list of
--- tuples, as follows: In each tuple, the first component is the
--- 'Name' of an element, and the second component is a list of
--- attributes names. When the given element is rendered, the
--- attributes listed, when present, appear first in the given order,
--- followed by any other attributes in arbitrary order. If an element
--- does not appear, all of its attributes are rendered in arbitrary
--- order.
-orderAttrs :: [(Name, [Name])] ->
-              Name -> Map Name Text -> [(Name, Text)]
-orderAttrs orderSpec = order
-  where
-    order elt attrMap =
-      let initialAttrs = fromMaybe [] $ lookup elt orderSpec
-          mkPair attr' = (,) attr' <$> Map.lookup attr' attrMap
-          otherAttrMap =
-            Map.filterWithKey (const . not . (`elem` initialAttrs)) attrMap
-      in mapMaybe mkPair initialAttrs ++ Map.toAscList otherAttrMap
-
--- | 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 => RenderSettings -> ConduitT Event Builder m ()
-renderBuilder settings = mapC Chunk .| renderBuilder' yield' settings
-  where
-    yield' Flush = return ()
-    yield' (Chunk bs) = yield bs
-
--- | Same as 'renderBuilder' but allows you to flush XML stream to ensure that all
--- events at needed point are rendered.
---
--- @since 1.3.5
-renderBuilderFlush :: Monad m => RenderSettings -> ConduitT (Flush Event) (Flush Builder) m ()
-renderBuilderFlush = renderBuilder' yield
-
-renderBuilder'
-  :: Monad m
-  => (Flush Builder -> ConduitT (Flush Event) o m ())
-  -> RenderSettings
-  -> ConduitT (Flush Event) o m ()
-renderBuilder' yield' settings =
-    if rsPretty settings
-    then prettify .| renderEvent'
-    else renderEvent'
-  where
-    renderEvent' = renderEvent yield' settings
-
-renderEvent
-  :: Monad m
-  => (Flush Builder -> ConduitT (Flush Event) o m ())
-  -> RenderSettings
-  -> ConduitT (Flush Event) o m ()
-renderEvent yield' RenderSettings { rsPretty = isPretty, rsNamespaces = namespaces0, rsUseCDATA = useCDATA, rsXMLDeclaration = useXMLDecl } =
-    loop []
-  where
-    loop nslevels = await >>= maybe (return ()) (go nslevels)
-
-    go nslevels Flush = yield' Flush >> loop nslevels
-    go nslevels (Chunk e) =
-        case e of
-            EventBeginElement n1 as -> do
-                mnext <- peekC
-                isClosed <-
-                    case mnext of
-                        Just (Chunk (EventEndElement n2)) | n1 == n2 -> do
-                            dropC 1
-                            return True
-                        _ -> return False
-                let (token, nslevels') = mkBeginToken isPretty isClosed namespaces0 nslevels n1 as
-                yield' $ Chunk token
-                loop nslevels'
-            _ -> do
-                let (token, nslevels') = eventToToken nslevels useCDATA useXMLDecl e
-                yield' $ Chunk token
-                loop nslevels'
-
-eventToToken :: Stack -> (Content -> Bool) -> Bool -> Event -> (Builder, [NSLevel])
-eventToToken s _ True EventBeginDocument =
-    (tokenToBuilder $ TokenXMLDeclaration
-            [ ("version", [ContentText "1.0"])
-            , ("encoding", [ContentText "UTF-8"])
-            ]
-     , s)
-eventToToken s _ False EventBeginDocument = (mempty, s)
-eventToToken s _ _ EventEndDocument = (mempty, s)
-eventToToken s _ _ (EventInstruction i) = (tokenToBuilder $ TokenInstruction i, s)
-eventToToken s _ _ (EventBeginDoctype n meid) = (tokenToBuilder $ TokenDoctype n meid [], s)
-eventToToken s _ _ EventEndDoctype = (mempty, s)
-eventToToken s _ _ (EventCDATA t) = (tokenToBuilder $ TokenCDATA t, s)
-eventToToken s _ _ (EventEndElement name) =
-    (tokenToBuilder $ TokenEndElement $ nameToTName sl name, s')
-  where
-    (sl:s') = s
-eventToToken s useCDATA _ (EventContent c)
-    | useCDATA c =
-        case c of
-          ContentText txt -> (tokenToBuilder $ TokenCDATA txt, s)
-          ContentEntity txt -> (tokenToBuilder $ TokenCDATA txt, s)
-    | otherwise  = (tokenToBuilder $ TokenContent c, s)
-eventToToken s _ _ (EventComment t) = (tokenToBuilder $ TokenComment t, s)
-eventToToken _ _ _ EventBeginElement{} = error "eventToToken on EventBeginElement" -- mkBeginToken False s name attrs
-
-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 -- ^ pretty print attributes?
-             -> Bool -- ^ self closing?
-             -> [(Text, Text)] -- ^ namespaces to apply to top-level
-             -> Stack
-             -> Name
-             -> [(Name, [Content])]
-             -> (Builder, Stack)
-mkBeginToken isPretty isClosed namespaces0 s name attrs =
-    (tokenToBuilder $ TokenBeginElement tname tattrs3 isClosed indent,
-     if isClosed then s else sl3 : s)
-  where
-    indent = if isPretty then 2 + 4 * length s else 0
-    prevsl = case s of
-                [] -> NSLevel Nothing Map.empty
-                sl':_ -> sl'
-    (sl1, tname, tattrs1) = newElemStack prevsl name
-    (sl2, tattrs2) = foldr newAttrStack (sl1, tattrs1) $ nubAttrs attrs
-    (sl3, tattrs3) =
-        case s of
-            [] -> (sl2 { prefixes = Map.union (prefixes sl2) $ Map.fromList namespaceSL }, namespaceAttrs ++ tattrs2)
-            _ -> (sl2, tattrs2)
-
-    (namespaceSL, namespaceAttrs) = unzip $ mapMaybe unused namespaces0
-    unused (k, v) =
-        case lookup k' tattrs2 of
-            Just{} -> Nothing
-            Nothing -> Just ((v, k), (k', v'))
-      where
-        k' = TName (Just "xmlns") k
-        v' = [ContentText v]
-
-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)) =
-    case Map.lookup ns nsmap of
-        Just pref'
-            | pref == pref' ->
-                ( NSLevel def' nsmap
-                , TName (Just pref) local
-                , []
-                )
-        _ -> ( NSLevel def' nsmap'
-             , TName (Just pref) local
-             , [(TName (Just "xmlns") pref, [ContentText ns])]
-             )
-  where
-    nsmap' = Map.insert ns pref nsmap
-
-newAttrStack :: (Name, [Content]) -> (NSLevel, [TAttribute]) -> (NSLevel, [TAttribute])
-newAttrStack (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 _ _ "http://www.w3.org/XML/1998/namespace" = ("xml", id)
-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
-
--- | Convert a stream of 'Event's into a prettified one, adding extra
--- whitespace. Note that this can change the meaning of your XML.
-{-# DEPRECATED prettify "Will be removed in future versions of `xml-conduit`. See <https://github.com/snoyberg/xml/issues/196>." #-}
-prettify :: Monad m => ConduitT (Flush Event) (Flush Event) m ()
-prettify = prettify' 0
-
-prettify' :: Monad m => Int -> ConduitT (Flush Event) (Flush Event) m ()
-prettify' level =
-    await >>= maybe (return ()) goC
-  where
-    yield' = yield . Chunk
-
-    goC Flush = yield Flush >> prettify' level
-    goC (Chunk e) = go e
-
-    go e@EventBeginDocument = do
-        yield' e
-        yield' $ EventContent $ ContentText "\n"
-        prettify' level
-    go e@EventBeginElement{} = do
-        yield' before
-        yield' e
-        mnext <- peekC
-        case mnext of
-            Just (Chunk next@EventEndElement{}) -> do
-                dropC 1
-                yield' next
-                yield' after
-                prettify' level
-            _ -> do
-                yield' after
-                prettify' $ level + 1
-    go e@EventEndElement{} = do
-        let level' = max 0 $ level - 1
-        yield' $ before' level'
-        yield' e
-        yield' after
-        prettify' level'
-    go (EventContent c) = do
-        cs <- takeContents (c:)
-        let cs' = mapMaybe normalize cs
-        case cs' of
-            [] -> return ()
-            _ -> do
-                yield' before
-                mapM_ (yield' . EventContent) cs'
-                yield' after
-        prettify' level
-    go (EventCDATA t) = go $ EventContent $ ContentText t
-    go e@EventInstruction{} = do
-        yield' before
-        yield' e
-        yield' after
-        prettify' level
-    go (EventComment t) = do
-        yield' before
-        yield' $ EventComment $ T.concat
-            [ " "
-            , T.unwords $ T.words t
-            , " "
-            ]
-        yield' after
-        prettify' level
-
-    go e@EventEndDocument = yield' e >> prettify' level
-    go e@EventBeginDoctype{} = yield' e >> prettify' level
-    go e@EventEndDoctype{} = yield' e >> yield' after >> prettify' level
-
-    takeContents front = do
-        me <- peekC
-        case me of
-            Just (Chunk (EventContent c)) -> do
-                dropC 1
-                takeContents $ front . (c:)
-            Just (Chunk (EventCDATA t)) -> do
-                dropC 1
-                takeContents $ front . (ContentText t:)
-            _ -> return $ front []
-
-    normalize (ContentText t)
-        | T.null t' = Nothing
-        | otherwise = Just $ ContentText t'
-      where
-        t' = T.unwords $ T.words t
-    normalize c = Just c
-
-    before = EventContent $ ContentText $ T.replicate level "    "
-    before' l = EventContent $ ContentText $ T.replicate l "    "
-    after = EventContent $ ContentText "\n"
-
-nubAttrs :: [(Name, v)] -> [(Name, v)]
-nubAttrs orig =
-    front []
-  where
-    (front, _) = foldl' go (id, Set.empty) orig
-    go (dlist, used) (k, v)
-        | k `Set.member` used = (dlist, used)
-        | otherwise = (dlist . ((k, v):), Set.insert k used)
-
-
--- | Generate a complete XML 'Element'.
-tag :: (Monad m) => Name -> Attributes -> ConduitT i Event m ()  -- ^ 'Element''s subnodes.
-                                       -> ConduitT i Event m ()
-tag name (Attributes a) content' = do
-  yield $ EventBeginElement name a
-  content'
-  yield $ EventEndElement name
-
--- | Generate a textual 'EventContent'.
-content :: (Monad m) => Text -> ConduitT i Event m ()
-content = yield . EventContent . ContentText
+{- | '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.Stream.Render (
+    -- * Rendering XML files
+    renderBuilder,
+    renderBuilderFlush,
+    renderBytes,
+    renderText,
 
--- | A list of attributes.
-data Attributes = Attributes [(Name, [Content])]
+    -- * Renderer settings
+    RenderSettings,
+    def,
+    rsNamespaces,
+    rsAttrOrder,
+    rsUseCDATA,
+    rsXMLDeclaration,
+    orderAttrs,
 
-instance Monoid Attributes where
-  mempty = Attributes mempty
-#if !MIN_VERSION_base(4,11,0)
-  (Attributes a) `mappend` (Attributes b) = Attributes (a `mappend` b)
-#else
-instance Semigroup Attributes where
-  (Attributes a) <> (Attributes b) = Attributes (a <> b)
-#endif
+    -- * Event rendering
+    document,
+    tag,
+    content,
 
--- | Generate a single attribute.
-attr :: Name        -- ^ Attribute's name
-     -> Text        -- ^ Attribute's value
-     -> Attributes
-attr name value = Attributes [(name, [ContentText value])]
+    -- * Attribute rendering
+    Attributes,
+    attr,
+    optionalAttr,
+) where
 
--- | Helper function that generates a valid attribute if input isn't 'Nothing', or 'mempty' otherwise.
-optionalAttr :: Name -> Maybe Text -> Attributes
-optionalAttr name = maybe mempty (attr name)
+import Text.XML.Stream.Render.Internal
diff --git a/src/Text/XML/Stream/Render/Internal.hs b/src/Text/XML/Stream/Render/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/XML/Stream/Render/Internal.hs
@@ -0,0 +1,440 @@
+{-# LANGUAGE CPP  #-}
+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes        #-}
+-- | '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.Stream.Render.Internal
+    ( -- * Rendering XML files
+      renderBuilder
+    , renderBuilderFlush
+    , renderBytes
+    , renderText
+      -- * Renderer settings
+    , RenderSettings
+    , def
+    , rsPretty
+    , rsNamespaces
+    , rsAttrOrder
+    , rsUseCDATA
+    , rsXMLDeclaration
+    , orderAttrs
+      -- * Event rendering
+    , document
+    , tag
+    , content
+      -- * Attribute rendering
+    , Attributes
+    , attr
+    , optionalAttr
+    ) where
+
+import           Control.Applicative          ((<$>))
+import           Control.Monad.Trans.Resource (MonadThrow)
+import           Data.ByteString              (ByteString)
+import           Data.ByteString.Builder      (Builder)
+import           Conduit
+import           Data.Default           (Default (def))
+import           Data.List                    (foldl')
+import           Data.Map                     (Map)
+import qualified Data.Map                     as Map
+import           Data.Maybe                   (fromMaybe, mapMaybe)
+import           Data.Monoid                  (Monoid, mappend, mempty)
+import qualified Data.Set                     as Set
+import           Data.Text                    (Text)
+import qualified Data.Text                    as T
+import           Data.XML.Types               (Content (..), Event (..),
+                                               Name (..))
+import           Text.XML.Stream.Token
+
+-- | 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.
+--
+-- The output is UTF8 encoded.
+renderBytes :: PrimMonad m => RenderSettings -> ConduitT Event ByteString m ()
+renderBytes rs = renderBuilder rs .| builderToByteString
+
+-- | Render a stream of 'Event's into a stream of 'Text's. This function
+-- wraps around 'renderBuilder', 'builderToByteString' and 'renderBytes', so it
+-- produces optimally sized 'Text's with minimal buffer copying.
+renderText :: (PrimMonad m, MonadThrow m) => RenderSettings -> ConduitT Event Text m ()
+renderText rs = renderBytes rs .| decodeUtf8C
+
+data RenderSettings = RenderSettings
+    { rsPretty     :: Bool
+      -- ^ Pretty-print XML document. Implementation is arbitrary and can change the meaning of your XML (e.g. by adding whitespace and newlines in text content.)
+      --
+      -- See <https://github.com/snoyberg/xml/issues/196>.
+    , rsNamespaces :: [(Text, Text)]
+      -- ^ Defines some top level namespace definitions to be used, in the form
+      -- of (prefix, namespace). This has absolutely no impact on the meaning
+      -- of your documents, but can increase readability by moving commonly
+      -- used namespace declarations to the top level.
+    , rsAttrOrder  :: Name -> Map.Map Name Text -> [(Name, Text)]
+      -- ^ Specify how to turn the unordered attributes used by the "Text.XML"
+      -- module into an ordered list.
+    , rsUseCDATA   :: Content -> Bool
+      -- ^ Determines if for a given text content the renderer should use a
+      -- CDATA node.
+      --
+      -- Default: @False@
+      --
+      -- @since 1.3.3
+    , rsXMLDeclaration :: Bool
+      -- ^ Determines whether the XML declaration will be output. Note that when
+      -- using the streaming API the XML declaration will be output only if this
+      -- is set to true /and/ the stream includes an 'EventBeginDocument' event.
+      -- Apart from yielding it explicitly, this can be achieved by wrapping the
+      -- stream in the 'document' function.
+      --
+      -- Default: @True@
+      --
+      -- @since 1.5.1
+    }
+
+instance Default RenderSettings where
+    def = RenderSettings
+        { rsPretty = False
+        , rsNamespaces = []
+        , rsAttrOrder = const Map.toList
+        , rsUseCDATA = const False
+        , rsXMLDeclaration = True
+        }
+
+-- | Convenience function to create an ordering function suitable for
+-- use as the value of 'rsAttrOrder'. The ordering function is created
+-- from an explicit ordering of the attributes, specified as a list of
+-- tuples, as follows: In each tuple, the first component is the
+-- 'Name' of an element, and the second component is a list of
+-- attributes names. When the given element is rendered, the
+-- attributes listed, when present, appear first in the given order,
+-- followed by any other attributes in arbitrary order. If an element
+-- does not appear, all of its attributes are rendered in arbitrary
+-- order.
+orderAttrs :: [(Name, [Name])] ->
+              Name -> Map Name Text -> [(Name, Text)]
+orderAttrs orderSpec = order
+  where
+    order elt attrMap =
+      let initialAttrs = fromMaybe [] $ lookup elt orderSpec
+          mkPair attr' = (,) attr' <$> Map.lookup attr' attrMap
+          otherAttrMap =
+            Map.filterWithKey (const . not . (`elem` initialAttrs)) attrMap
+      in mapMaybe mkPair initialAttrs ++ Map.toAscList otherAttrMap
+
+-- | 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 => RenderSettings -> ConduitT Event Builder m ()
+renderBuilder settings = mapC Chunk .| renderBuilder' yield' settings
+  where
+    yield' Flush = return ()
+    yield' (Chunk bs) = yield bs
+
+-- | Same as 'renderBuilder' but allows you to flush XML stream to ensure that all
+-- events at needed point are rendered.
+--
+-- @since 1.3.5
+renderBuilderFlush :: Monad m => RenderSettings -> ConduitT (Flush Event) (Flush Builder) m ()
+renderBuilderFlush = renderBuilder' yield
+
+renderBuilder'
+  :: Monad m
+  => (Flush Builder -> ConduitT (Flush Event) o m ())
+  -> RenderSettings
+  -> ConduitT (Flush Event) o m ()
+renderBuilder' yield' settings =
+    if rsPretty settings
+    then prettify .| renderEvent'
+    else renderEvent'
+  where
+    renderEvent' = renderEvent yield' settings
+
+renderEvent
+  :: Monad m
+  => (Flush Builder -> ConduitT (Flush Event) o m ())
+  -> RenderSettings
+  -> ConduitT (Flush Event) o m ()
+renderEvent yield' RenderSettings { rsPretty = isPretty, rsNamespaces = namespaces0, rsUseCDATA = useCDATA, rsXMLDeclaration = useXMLDecl } =
+    loop []
+  where
+    loop nslevels = await >>= maybe (return ()) (go nslevels)
+
+    go nslevels Flush = yield' Flush >> loop nslevels
+    go nslevels (Chunk e) =
+        case e of
+            EventBeginElement n1 as -> do
+                mnext <- peekC
+                isClosed <-
+                    case mnext of
+                        Just (Chunk (EventEndElement n2)) | n1 == n2 -> do
+                            dropC 1
+                            return True
+                        _ -> return False
+                let (token, nslevels') = mkBeginToken isPretty isClosed namespaces0 nslevels n1 as
+                yield' $ Chunk token
+                loop nslevels'
+            _ -> do
+                let (token, nslevels') = eventToToken nslevels useCDATA useXMLDecl e
+                yield' $ Chunk token
+                loop nslevels'
+
+eventToToken :: Stack -> (Content -> Bool) -> Bool -> Event -> (Builder, [NSLevel])
+eventToToken s _ True EventBeginDocument =
+    (tokenToBuilder $ TokenXMLDeclaration
+            [ ("version", [ContentText "1.0"])
+            , ("encoding", [ContentText "UTF-8"])
+            ]
+     , s)
+eventToToken s _ False EventBeginDocument = (mempty, s)
+eventToToken s _ _ EventEndDocument = (mempty, s)
+eventToToken s _ _ (EventInstruction i) = (tokenToBuilder $ TokenInstruction i, s)
+eventToToken s _ _ (EventBeginDoctype n meid) = (tokenToBuilder $ TokenDoctype n meid [], s)
+eventToToken s _ _ EventEndDoctype = (mempty, s)
+eventToToken s _ _ (EventCDATA t) = (tokenToBuilder $ TokenCDATA t, s)
+eventToToken s _ _ (EventEndElement name) =
+    (tokenToBuilder $ TokenEndElement $ nameToTName sl name, s')
+  where
+    (sl:s') = s
+eventToToken s useCDATA _ (EventContent c)
+    | useCDATA c =
+        case c of
+          ContentText txt -> (tokenToBuilder $ TokenCDATA txt, s)
+          ContentEntity txt -> (tokenToBuilder $ TokenCDATA txt, s)
+    | otherwise  = (tokenToBuilder $ TokenContent c, s)
+eventToToken s _ _ (EventComment t) = (tokenToBuilder $ TokenComment t, s)
+eventToToken _ _ _ EventBeginElement{} = error "eventToToken on EventBeginElement" -- mkBeginToken False s name attrs
+
+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 -- ^ pretty print attributes?
+             -> Bool -- ^ self closing?
+             -> [(Text, Text)] -- ^ namespaces to apply to top-level
+             -> Stack
+             -> Name
+             -> [(Name, [Content])]
+             -> (Builder, Stack)
+mkBeginToken isPretty isClosed namespaces0 s name attrs =
+    (tokenToBuilder $ TokenBeginElement tname tattrs3 isClosed indent,
+     if isClosed then s else sl3 : s)
+  where
+    indent = if isPretty then 2 + 4 * length s else 0
+    prevsl = case s of
+                [] -> NSLevel Nothing Map.empty
+                sl':_ -> sl'
+    (sl1, tname, tattrs1) = newElemStack prevsl name
+    (sl2, tattrs2) = foldr newAttrStack (sl1, tattrs1) $ nubAttrs attrs
+    (sl3, tattrs3) =
+        case s of
+            [] -> (sl2 { prefixes = Map.union (prefixes sl2) $ Map.fromList namespaceSL }, namespaceAttrs ++ tattrs2)
+            _ -> (sl2, tattrs2)
+
+    (namespaceSL, namespaceAttrs) = unzip $ mapMaybe unused namespaces0
+    unused (k, v) =
+        case lookup k' tattrs2 of
+            Just{} -> Nothing
+            Nothing -> Just ((v, k), (k', v'))
+      where
+        k' = TName (Just "xmlns") k
+        v' = [ContentText v]
+
+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)) =
+    case Map.lookup ns nsmap of
+        Just pref'
+            | pref == pref' ->
+                ( NSLevel def' nsmap
+                , TName (Just pref) local
+                , []
+                )
+        _ -> ( NSLevel def' nsmap'
+             , TName (Just pref) local
+             , [(TName (Just "xmlns") pref, [ContentText ns])]
+             )
+  where
+    nsmap' = Map.insert ns pref nsmap
+
+newAttrStack :: (Name, [Content]) -> (NSLevel, [TAttribute]) -> (NSLevel, [TAttribute])
+newAttrStack (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 _ _ "http://www.w3.org/XML/1998/namespace" = ("xml", id)
+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
+
+-- | Convert a stream of 'Event's into a prettified one, adding extra
+-- whitespace. Note that this can change the meaning of your XML.
+prettify :: Monad m => ConduitT (Flush Event) (Flush Event) m ()
+prettify = prettify' 0
+
+prettify' :: Monad m => Int -> ConduitT (Flush Event) (Flush Event) m ()
+prettify' level =
+    await >>= maybe (return ()) goC
+  where
+    yield' = yield . Chunk
+
+    goC Flush = yield Flush >> prettify' level
+    goC (Chunk e) = go e
+
+    go e@EventBeginDocument = do
+        yield' e
+        yield' $ EventContent $ ContentText "\n"
+        prettify' level
+    go e@EventBeginElement{} = do
+        yield' before
+        yield' e
+        mnext <- peekC
+        case mnext of
+            Just (Chunk next@EventEndElement{}) -> do
+                dropC 1
+                yield' next
+                yield' after
+                prettify' level
+            _ -> do
+                yield' after
+                prettify' $ level + 1
+    go e@EventEndElement{} = do
+        let level' = max 0 $ level - 1
+        yield' $ before' level'
+        yield' e
+        yield' after
+        prettify' level'
+    go (EventContent c) = do
+        cs <- takeContents (c:)
+        let cs' = mapMaybe normalize cs
+        case cs' of
+            [] -> return ()
+            _ -> do
+                yield' before
+                mapM_ (yield' . EventContent) cs'
+                yield' after
+        prettify' level
+    go (EventCDATA t) = go $ EventContent $ ContentText t
+    go e@EventInstruction{} = do
+        yield' before
+        yield' e
+        yield' after
+        prettify' level
+    go (EventComment t) = do
+        yield' before
+        yield' $ EventComment $ T.concat
+            [ " "
+            , T.unwords $ T.words t
+            , " "
+            ]
+        yield' after
+        prettify' level
+
+    go e@EventEndDocument = yield' e >> prettify' level
+    go e@EventBeginDoctype{} = yield' e >> prettify' level
+    go e@EventEndDoctype{} = yield' e >> yield' after >> prettify' level
+
+    takeContents front = do
+        me <- peekC
+        case me of
+            Just (Chunk (EventContent c)) -> do
+                dropC 1
+                takeContents $ front . (c:)
+            Just (Chunk (EventCDATA t)) -> do
+                dropC 1
+                takeContents $ front . (ContentText t:)
+            _ -> return $ front []
+
+    normalize (ContentText t)
+        | T.null t' = Nothing
+        | otherwise = Just $ ContentText t'
+      where
+        t' = T.unwords $ T.words t
+    normalize c = Just c
+
+    before = EventContent $ ContentText $ T.replicate level "    "
+    before' l = EventContent $ ContentText $ T.replicate l "    "
+    after = EventContent $ ContentText "\n"
+
+nubAttrs :: [(Name, v)] -> [(Name, v)]
+nubAttrs orig =
+    front []
+  where
+    (front, _) = foldl' go (id, Set.empty) orig
+    go (dlist, used) (k, v)
+        | k `Set.member` used = (dlist, used)
+        | otherwise = (dlist . ((k, v):), Set.insert k used)
+
+-- | Wrap the given stream in an 'EventBeginDocument'/'EventEndDocument' pair.
+--
+-- @since 1.10.1.0
+document :: (Monad m) => ConduitT i Event m () -> ConduitT i Event m ()
+document content' = do
+  yield EventBeginDocument
+  content'
+  yield EventEndDocument
+
+-- | Generate a complete XML 'Element'.
+tag :: (Monad m) => Name -> Attributes -> ConduitT i Event m ()  -- ^ 'Element''s subnodes.
+                                       -> ConduitT i Event m ()
+tag name (Attributes a) content' = do
+  yield $ EventBeginElement name a
+  content'
+  yield $ EventEndElement name
+
+-- | Generate a textual 'EventContent'.
+content :: (Monad m) => Text -> ConduitT i Event m ()
+content = yield . EventContent . ContentText
+
+-- | A list of attributes.
+data Attributes = Attributes [(Name, [Content])]
+
+instance Monoid Attributes where
+  mempty = Attributes mempty
+#if !MIN_VERSION_base(4,11,0)
+  (Attributes a) `mappend` (Attributes b) = Attributes (a `mappend` b)
+#else
+instance Semigroup Attributes where
+  (Attributes a) <> (Attributes b) = Attributes (a <> b)
+#endif
+
+-- | Generate a single attribute.
+attr :: Name        -- ^ Attribute's name
+     -> Text        -- ^ Attribute's value
+     -> Attributes
+attr name value = Attributes [(name, [ContentText value])]
+
+-- | Helper function that generates a valid attribute if input isn't 'Nothing', or 'mempty' otherwise.
+optionalAttr :: Name -> Maybe Text -> Attributes
+optionalAttr name = maybe mempty (attr name)
diff --git a/src/Text/XML/Unresolved.hs b/src/Text/XML/Unresolved.hs
--- a/src/Text/XML/Unresolved.hs
+++ b/src/Text/XML/Unresolved.hs
@@ -38,9 +38,8 @@
     , P.psDecodeEntities
     , P.psRetainNamespaces
       -- ** Render
-    , R.RenderSettings
-    , R.rsPretty
-    , R.rsNamespaces
+    , RI.RenderSettings
+    , RI.rsNamespaces
     ) where
 
 import           Conduit
@@ -67,6 +66,7 @@
 import           Text.XML.Stream.Parse        (ParseSettings)
 import qualified Text.XML.Stream.Parse        as P
 import qualified Text.XML.Stream.Render       as R
+import qualified Text.XML.Stream.Render.Internal       as RI
 
 readFile :: P.ParseSettings -> FilePath -> IO Document
 readFile ps fp = runConduitRes $ CB.sourceFile fp .| sinkDoc ps
diff --git a/test/unit.hs b/test/unit.hs
--- a/test/unit.hs
+++ b/test/unit.hs
@@ -15,6 +15,7 @@
 import           Text.XML.Stream.Parse        (def)
 import qualified Text.XML.Stream.Parse        as P
 import qualified Text.XML.Unresolved          as D
+import qualified Text.XML.Stream.Render.Internal as R
 
 import           Control.Monad
 import qualified Data.Set                     as Set
@@ -24,6 +25,7 @@
                                                (&.//), (&/), (&//))
 
 import qualified Control.Monad.Trans.Resource as C
+import           Conduit                      (foldC, sinkList, yieldMany)
 import           Data.Conduit                 ((.|), runConduit,
                                                runConduitRes, ConduitT)
 import           Data.Conduit.Attoparsec      (ParseError(..))
@@ -59,6 +61,8 @@
         it "normalizes line endings" crlfToLfConversion
         it "normalizes \\r at the end of a content" crlfToLfConversionCrAtEnd
         it "normalizes multiple \\rs and \\r\\ns" crlfToLfConversionCrCrCr
+        context "generates events for rendering in a stream" streamRenderGenerateEvents
+        it "renders events from a stream" streamRender
     describe "XML Cursors" $ do
         it "has correct parent" cursorParent
         it "has correct ancestor" cursorAncestor
@@ -143,7 +147,7 @@
 
 documentParsePrettyRender :: IO ()
 documentParsePrettyRender =
-    L.unpack (D.renderLBS def { D.rsPretty = True } (D.parseLBS_ def doc)) @?= L.unpack doc
+    L.unpack (D.renderLBS def { R.rsPretty = True } (D.parseLBS_ def doc)) @?= L.unpack doc
   where
     doc = L.unlines
         [ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
@@ -901,7 +905,7 @@
                 , Res.NodeComment "foo bar\n\r\nbaz    \tbin "
                 , Res.NodeElement $ Res.Element "bar" Map.empty [Res.NodeContent "bar content"]
                 ]
-    pretty @=? S.concat (L.toChunks $ Res.renderLBS def { D.rsPretty = True } doc)
+    pretty @=? S.concat (L.toChunks $ Res.renderLBS def { R.rsPretty = True } doc)
 
 caseTopLevelNamespace :: Assertion
 caseTopLevelNamespace = do
@@ -1107,3 +1111,47 @@
     where
         doc = D.parseLBS_ def "<crlf>\r\r\r\n\r\r\r</crlf>"
         content = [ContentText "\n\n\n\n\n\n"]
+
+streamRenderGenerateEvents :: Spec
+streamRenderGenerateEvents = do
+    it "generates events for a document" $ do
+        emptyDoc <- runConduit $ R.document mempty .| sinkList
+        emptyDoc @?= [EventBeginDocument, EventEndDocument]
+        nonEmptyDoc <- runConduit $
+            R.document (R.tag "foo" mempty $ R.content "...") .| sinkList
+        nonEmptyDoc @?=
+            [ EventBeginDocument
+            , EventBeginElement "foo" []
+            , EventContent $ ContentText "..."
+            , EventEndElement "foo"
+            , EventEndDocument
+            ]
+    it "generates events for a tag" $ do
+        emptyTag <- runConduit $ R.tag "foo" mempty mempty .| sinkList
+        emptyTag @?= [EventBeginElement "foo" [], EventEndElement "foo"]
+        nonEmptyTag <- runConduit $
+            R.tag "foo" (R.attr "bar" "baz") (R.content "...") .| sinkList
+        nonEmptyTag @?=
+            [ EventBeginElement "foo" [("bar", [ContentText "baz"])]
+            , EventContent $ ContentText "..."
+            , EventEndElement "foo"
+            ]
+
+streamRender :: Assertion
+streamRender = do
+    x <- runConduit $ input .| R.renderBytes def .| foldC
+    x @?= output
+  where
+    input = yieldMany
+        [ EventBeginDocument
+        , EventBeginElement "foo" [("bar", [ContentText "baz"])]
+        , EventContent $ ContentText "..."
+        , EventEndElement "foo"
+        , EventEndDocument
+        ]
+    output = S.concat
+        [ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+        , "<foo bar=\"baz\">"
+        , "..."
+        , "</foo>"
+        ]
diff --git a/xml-conduit.cabal b/xml-conduit.cabal
--- a/xml-conduit.cabal
+++ b/xml-conduit.cabal
@@ -1,7 +1,7 @@
 cabal-version:   1.14
 
 name:            xml-conduit
-version:         1.9.1.4
+version:         1.10.1.0
 license:         MIT
 license-file:    LICENSE
 author:          Michael Snoyman <michael@snoyman.com>, Aristid Breitkreuz <aristidb@googlemail.com>
@@ -14,10 +14,10 @@
 homepage:        http://github.com/snoyberg/xml
 extra-source-files: README.md
                     ChangeLog.md
-tested-with:     GHC >=8.0 && <8.12
+tested-with:     GHC >=8.0 && <9.12
 
 custom-setup
-    setup-depends:   base >= 4 && <5, Cabal <4, cabal-doctest >= 1.0.9 && <1.1
+    setup-depends:   base >= 4 && <5, Cabal <4, cabal-doctest >= 1 && <1.1
 
 library
     build-depends:   base                      >= 4.12     && < 5
@@ -36,6 +36,7 @@
                    , deepseq                   >= 1.1.0.0
     exposed-modules: Text.XML.Stream.Parse
                      Text.XML.Stream.Render
+                     Text.XML.Stream.Render.Internal
                      Text.XML.Unresolved
                      Text.XML.Cursor
                      Text.XML.Cursor.Generic
