diff --git a/.travis.yml b/.travis.yml
index 2cb40c2..c3cfbad 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,4 +2,5 @@ language: rust
 rust: nightly
 sudo: false
 script:
+  - ( cd maud && cargo test --verbose )
   - ( cd maud_macros && cargo test --verbose )
diff --git a/maud/src/lib.rs b/maud/src/lib.rs
index 84a7bee..d03986e 100644
--- a/maud/src/lib.rs
+++ b/maud/src/lib.rs
@@ -8,6 +8,51 @@
 use std::fmt;
 use std::io;
 
+/// An adapter that escapes HTML special characters.
+///
+/// # Example
+///
+/// ```
+/// # use maud::Escaper;
+/// use std::fmt::Write;
+/// let mut result = String::new();
+/// write!(Escaper::new(&mut result), "<script>launchMissiles()</script>").unwrap();
+/// assert_eq!(result, "&lt;script&gt;launchMissiles()&lt;/script&gt;");
+/// ```
+pub struct Escaper<'a> {
+    // FIXME: store the writer directly instead of borrowing it
+    // see <https://github.com/rust-lang/rust/pull/28368>
+    inner: &'a mut fmt::Write,
+}
+
+impl<'a> Escaper<'a> {
+    /// Creates an `Escaper` from a `std::fmt::Write`.
+    pub fn new(inner: &'a mut fmt::Write) -> Escaper<'a> {
+        Escaper { inner: inner }
+    }
+
+    /// Extracts the inner writer.
+    pub fn into_inner(self) -> &'a mut fmt::Write {
+        self.inner
+    }
+}
+
+impl<'a> fmt::Write for Escaper<'a> {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        for c in s.chars() {
+            try!(match c {
+                '&' => self.inner.write_str("&amp;"),
+                '<' => self.inner.write_str("&lt;"),
+                '>' => self.inner.write_str("&gt;"),
+                '"' => self.inner.write_str("&quot;"),
+                '\'' => self.inner.write_str("&#39;"),
+                _ => self.inner.write_char(c),
+            });
+        }
+        Ok(())
+    }
+}
+
 /// Wraps a `std::io::Write` in a `std::fmt::Write`.
 ///
 /// Most I/O libraries work with binary data (`[u8]`), but Maud outputs
@@ -37,11 +82,15 @@ impl<W: io::Write> Utf8Writer<W> {
         }
     }
 
+    /// Extracts the inner writer, along with any errors encountered
+    /// along the way.
     pub fn into_inner(self) -> (W, io::Result<()>) {
         let Utf8Writer { inner, result } = self;
         (inner, result)
     }
 
+    /// Drops the inner writer, returning any errors encountered
+    /// along the way.
     pub fn into_result(self) -> io::Result<()> {
         self.result
     }
@@ -68,38 +117,3 @@ impl<W: io::Write> fmt::Write for Utf8Writer<W> {
         }
     }
 }
-
-/// Escapes an HTML value.
-pub fn escape(s: &str) -> String {
-    use std::fmt::Write;
-    let mut buf = String::new();
-    rt::Escaper { inner: &mut buf }.write_str(s).unwrap();
-    buf
-}
-
-/// Internal functions used by the `maud_macros` package. You should
-/// never need to call these directly.
-#[doc(hidden)]
-pub mod rt {
-    use std::fmt;
-
-    pub struct Escaper<'a, 'b: 'a> {
-        pub inner: &'a mut (fmt::Write + 'b),
-    }
-
-    impl<'a, 'b> fmt::Write for Escaper<'a, 'b> {
-        fn write_str(&mut self, s: &str) -> fmt::Result {
-            for c in s.chars() {
-                try!(match c {
-                    '&' => self.inner.write_str("&amp;"),
-                    '<' => self.inner.write_str("&lt;"),
-                    '>' => self.inner.write_str("&gt;"),
-                    '"' => self.inner.write_str("&quot;"),
-                    '\'' => self.inner.write_str("&#39;"),
-                    _ => write!(self.inner, "{}", c),
-                });
-            }
-            Ok(())
-        }
-    }
-}
diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs
index 594fb62..f319fdf 100644
--- a/maud_macros/src/render.rs
+++ b/maud_macros/src/render.rs
@@ -1,10 +1,11 @@
+use std::fmt::Write;
 use syntax::ast::{Expr, Ident, Pat, Stmt, TokenTree, TtToken};
 use syntax::codemap::DUMMY_SP;
 use syntax::ext::base::ExtCtxt;
 use syntax::parse::token;
 use syntax::ptr::P;
 
-use maud;
+use maud::Escaper;
 
 #[derive(Copy, Clone)]
 pub enum Escape {
@@ -119,7 +120,7 @@ impl<'cx> Renderer<'cx> {
         let escaped;
         let s = match escape {
             Escape::PassThru => s,
-            Escape::Escape => { escaped = maud::escape(s); &*escaped },
+            Escape::Escape => { escaped = html_escape(s); &*escaped },
         };
         self.push_str(s);
     }
@@ -133,7 +134,7 @@ impl<'cx> Renderer<'cx> {
             Escape::Escape =>
                 quote_expr!(self.cx,
                     write!(
-                        ::maud::rt::Escaper { inner: $w },
+                        ::maud::Escaper::new($w),
                         "{}",
                         $expr)),
         };
@@ -190,3 +191,10 @@ impl<'cx> Renderer<'cx> {
         self.push(stmt);
     }
 }
+
+fn html_escape(s: &str) -> String {
+    use std::fmt::Write;
+    let mut out = String::new();
+    Escaper::new(&mut out).write_str(s).unwrap();
+    out
+}