diff --git a/maud/src/lib.rs b/maud/src/lib.rs
index 60ecf0e..8eeeea7 100644
--- a/maud/src/lib.rs
+++ b/maud/src/lib.rs
@@ -1,6 +1,9 @@
 //! Super fast HTML template engine.
 
-use std::io::IoResult;
+use std::fmt;
+use std::fmt::Writer as FmtWriter;
+
+pub type FmtResult<T> = Result<T, fmt::Error>;
 
 /// Utilities for escaping HTML5 markup.
 ///
@@ -9,27 +12,19 @@ use std::io::IoResult;
 ///
 /// [1]: http://www.w3.org/TR/html51/syntax.html#escapingString
 pub mod escape {
+    use std::fmt::Writer as FmtWriter;
+
     use super::render;
     use super::rt;
 
     /// Escape a double-quoted attribute value, as per HTML5 rules.
     pub fn attribute(s: &str) -> String {
-        render(|w| {
-            for c in s.chars() {
-                try!(rt::escape_attribute(c, w));
-            }
-            Ok(())
-        })
+        render(|w| rt::escape_attribute(w, |w| w.write_str(s)))
     }
 
     /// Escape non-attribute text content, as per HTML5 rules.
     pub fn non_attribute(s: &str) -> String {
-        render(|w| {
-            for c in s.chars() {
-                try!(rt::escape_non_attribute(c, w));
-            }
-            Ok(())
-        })
+        render(|w| rt::escape_non_attribute(w, |w| w.write_str(s)))
     }
 }
 
@@ -38,33 +33,66 @@ pub mod escape {
 #[experimental = "These functions should not be called directly.
 Use the macros in `maud_macros` instead."]
 pub mod rt {
-    use std::io::IoResult;
+    use std::fmt::Writer as FmtWriter;
+    use super::FmtResult;
 
-    #[inline]
-    pub fn escape_attribute(c: char, w: &mut Writer) -> IoResult<()> {
-        match c {
-            '&' => w.write_str("&amp;"),
-            '\u{A0}' => w.write_str("&nbsp;"),
-            '"' => w.write_str("&quot;"),
-            _ => w.write_char(c),
+    struct AttrEscaper<'a, 'b: 'a> {
+        inner: &'a mut (FmtWriter + 'b),
+    }
+
+    impl<'a, 'b> FmtWriter for AttrEscaper<'a, 'b> {
+        fn write_str(&mut self, s: &str) -> FmtResult<()> {
+            for c in s.chars() {
+                try!(match c {
+                    '&' => self.inner.write_str("&amp;"),
+                    '\u{A0}' => self.inner.write_str("&nbsp;"),
+                    '"' => self.inner.write_str("&quot;"),
+                    _ => write!(self.inner, "{}", c),
+                });
+            }
+            Ok(())
+        }
+    }
+
+    struct NonAttrEscaper<'a, 'b: 'a> {
+        inner: &'a mut (FmtWriter + 'b),
+    }
+
+    impl<'a, 'b> FmtWriter for NonAttrEscaper<'a, 'b> {
+        fn write_str(&mut self, s: &str) -> FmtResult<()> {
+            for c in s.chars() {
+                try!(match c {
+                    '&' => self.inner.write_str("&amp;"),
+                    '\u{A0}' => self.inner.write_str("&nbsp;"),
+                    '<' => self.inner.write_str("&lt;"),
+                    '>' => self.inner.write_str("&gt;"),
+                    _ => write!(self.inner, "{}", c),
+                });
+            }
+            Ok(())
         }
     }
 
     #[inline]
-    pub fn escape_non_attribute(c: char, w: &mut Writer) -> IoResult<()> {
-        match c {
-            '&' => w.write_str("&amp;"),
-            '\u{A0}' => w.write_str("&nbsp;"),
-            '<' => w.write_str("&lt;"),
-            '>' => w.write_str("&gt;"),
-            _ => w.write_char(c),
-        }
+    pub fn escape_attribute<F>(w: &mut FmtWriter, f: F) -> FmtResult<()> where
+        F: FnOnce(&mut FmtWriter) -> FmtResult<()>
+    {
+        f(&mut AttrEscaper { inner: w })
+    }
+
+    #[inline]
+    pub fn escape_non_attribute<F>(w: &mut FmtWriter, f: F) -> FmtResult<()> where
+        F: FnOnce(&mut FmtWriter) -> FmtResult<()>
+    {
+        f(&mut NonAttrEscaper { inner: w })
     }
 }
 
 /// Render a template into a `String`.
-pub fn render<F: FnOnce(&mut Writer) -> IoResult<()>>(template: F) -> String {
-    let mut buf = vec![];
+pub fn render<F>(template: F) -> String where
+    F: FnOnce(&mut FmtWriter) -> FmtResult<()>
+{
+    let mut buf = String::new();
     template(&mut buf).unwrap();
-    String::from_utf8(buf).unwrap()
+    buf
 }
diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs
index 5dac7eb..d9bdccd 100644
--- a/maud_macros/src/lib.rs
+++ b/maud_macros/src/lib.rs
@@ -1,5 +1,6 @@
 #![crate_type = "dylib"]
-#![feature(plugin_registrar, quote, slicing_syntax)]
+#![feature(plugin_registrar, quote)]
+#![allow(unstable)]
 
 extern crate syntax;
 extern crate rustc;
@@ -15,10 +16,7 @@ mod render;
 
 fn expand_html<'cx>(cx: &'cx mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'cx> {
     match parse::parse(cx, args) {
-        Some(markups) => {
-            let expr = render::render(cx, markups[]);
-            MacExpr::new(expr)
-        },
+        Some(expr) => MacExpr::new(expr),
         None => DummyResult::any(sp),
     }
 }
diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 6ff0dab..da183f4 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -5,56 +5,34 @@ use syntax::parse::parser::Parser as RustParser;
 use syntax::parse::token;
 use syntax::ptr::P;
 
-#[derive(Show)]
-pub enum Markup {
-    Element(String, Vec<(String, Value)>, Option<Box<Markup>>),
-    Block(Vec<Markup>),
-    Value(Value),
-}
-
-#[derive(Show)]
-pub struct Value {
-    pub value: Value_,
-    pub escape: Escape,
-}
-
-#[derive(Show)]
-pub enum Value_ {
-    Literal(String),
-    Splice(P<Expr>),
-}
+use super::render::Renderer;
 
 #[derive(Copy, PartialEq, Show)]
 pub enum Escape {
-    NoEscape,
-    Escape,
+    None,
+    Attr,
+    Body,
 }
 
-macro_rules! some {
-    ($e:expr) => (
-        match $e {
-            Some(x) => x,
-            None => return None,
-        }
-    )
+macro_rules! guard {
+    ($e:expr) => (if !$e { return false; })
 }
 
-macro_rules! any {
-    ($self_:expr;) => (None);
-    ($self_:expr; $e:expr) => (any!($self_; $e,));
+macro_rules! branch {
+    ($self_:expr;) => (return false);
+    ($self_:expr; $e:expr) => (branch!($self_; $e,));
     ($self_:expr; $e:expr, $($es:expr),*) => ({
         let start_ptr = $self_.input.as_ptr();
-        match $e {
-            Some(x) => Some(x),
-            None => {
-                if $self_.input.as_ptr() == start_ptr {
-                    // Parsing failed, but did not consume input.
-                    // Keep going.
-                    any!($self_; $($es),*)
-                } else {
-                    return None;
-                }
-            },
+        if $e {
+            true
+        } else {
+            if $self_.input.as_ptr() == start_ptr {
+                // Parsing failed, but did not consume input.
+                // Keep going.
+                branch!($self_; $($es),*)
+            } else {
+                return false;
+            }
         }
     })
 }
@@ -78,59 +56,72 @@ macro_rules! ident {
     ($x:pat) => (TtToken(_, token::Ident($x, token::IdentStyle::Plain)))
 }
 
-pub fn parse(cx: &mut ExtCtxt, input: &[TokenTree]) -> Option<Vec<Markup>> {
-    Parser { cx: cx, input: input }.markups()
+pub fn parse(cx: &mut ExtCtxt, input: &[TokenTree]) -> Option<P<Expr>> {
+    let mut success = true;
+    let expr = Renderer::with(cx, |render| {
+        let mut parser = Parser {
+            in_attr: false,
+            input: input,
+            render: render,
+        };
+        success = parser.markups();
+    });
+    if success {
+        Some(expr)
+    } else {
+        None
+    }
 }
 
-struct Parser<'cx, 's: 'cx, 'i> {
-    cx: &'cx mut ExtCtxt<'s>,
+struct Parser<'cx: 'r, 's: 'cx, 'i, 'r, 'o: 'r> {
+    in_attr: bool,
     input: &'i [TokenTree],
+    render: &'r mut Renderer<'cx, 's, 'o>,
 }
 
-impl<'cx, 's, 'i> Parser<'cx, 's, 'i> {
+impl<'cx: 'r, 's: 'cx, 'i, 'r, 'o: 'r> Parser<'cx, 's, 'i, 'r, 'o> {
     /// Consume `n` items from the input.
-    fn shift(&mut self, n: uint) {
+    fn shift(&mut self, n: usize) {
         self.input = self.input.slice_from(n);
     }
 
+    fn choose_escape(&self) -> Escape {
+        if self.in_attr {
+            Escape::Attr
+        } else {
+            Escape::Body
+        }
+    }
+
     /// Construct a Rust AST parser from the given token tree.
     fn new_rust_parser(&self, tts: Vec<TokenTree>) -> RustParser<'s> {
-        parse::tts_to_parser(self.cx.parse_sess, tts, self.cx.cfg.clone())
+        parse::tts_to_parser(self.render.cx.parse_sess, tts, self.render.cx.cfg.clone())
     }
 
-    fn markups(&mut self) -> Option<Vec<Markup>> {
-        let mut result = vec![];
+    fn markups(&mut self) -> bool {
         loop {
             match self.input {
-                [] => return Some(result),
+                [] => return true,
                 [semi!(), ..] => self.shift(1),
                 [ref tt, ..] => {
-                    match self.markup() {
-                        Some(markup) => result.push(markup),
-                        None => {
-                            self.cx.span_err(tt.get_span(), "invalid syntax");
-                            return None;
-                        },
+                    if !self.markup() {
+                        self.render.cx.span_err(tt.get_span(), "invalid syntax");
+                        return false;
                     }
                 }
             }
         }
     }
 
-    fn markup(&mut self) -> Option<Markup> {
-        any!(self;
-            self.value().map(Markup::Value),
-            self.block(),
-            self.element())
-    }
-
-    fn value(&mut self) -> Option<Value> {
-        any!(self;
+    fn markup(&mut self) -> bool {
+        branch!(self;
             self.literal(),
-            self.splice())
+            self.splice(),
+            self.block(),
+            !self.in_attr && self.element())
     }
 
-    fn literal(&mut self) -> Option<Value> {
+    fn literal(&mut self) -> bool {
         let (tt, minus) = match self.input {
             [minus!(), ref tt @ literal!(), ..] => {
                 self.shift(2);
@@ -140,27 +131,30 @@ impl<'cx, 's, 'i> Parser<'cx, 's, 'i> {
                 self.shift(1);
                 (tt, false)
             },
-            _ => return None,
+            _ => return false,
         };
         let lit = self.new_rust_parser(vec![tt.clone()]).parse_lit();
-        lit_to_string(self.cx, lit, minus)
-            .map(|s| Value {
-                value: Value_::Literal(s),
-                escape: Escape::Escape,
-            })
+        match lit_to_string(self.render.cx, lit, minus) {
+            Some(s) => {
+                let escape = self.choose_escape();
+                self.render.string(s.as_slice(), escape);
+            },
+            None => return false,
+        }
+        true
     }
 
-    fn splice(&mut self) -> Option<Value> {
+    fn splice(&mut self) -> bool {
         let (escape, sp) = match self.input {
             [ref tt @ dollar!(), dollar!(), ..] => {
                 self.shift(2);
-                (Escape::NoEscape, tt.get_span())
+                (Escape::None, tt.get_span())
             },
             [ref tt @ dollar!(), ..] => {
                 self.shift(1);
-                (Escape::Escape, tt.get_span())
+                (self.choose_escape(), tt.get_span())
             },
-            _ => return None,
+            _ => return false,
         };
         let tt = match self.input {
             [ref tt, ..] => {
@@ -168,48 +162,57 @@ impl<'cx, 's, 'i> Parser<'cx, 's, 'i> {
                 self.new_rust_parser(vec![tt.clone()]).parse_expr()
             },
             _ => {
-                self.cx.span_err(sp, "expected expression for this splice");
-                return None;
+                self.render.cx.span_err(sp, "expected expression for this splice");
+                return false;
             },
         };
-        Some(Value {
-            value: Value_::Splice(tt),
-            escape: escape,
-        })
+        self.render.splice(tt, escape);
+        true
     }
 
-    fn element(&mut self) -> Option<Markup> {
+    fn element(&mut self) -> bool {
         let name = match self.input {
             [ident!(name), ..] => {
                 self.shift(1);
                 name.as_str().to_string()
             },
-            _ => return None,
+            _ => return false,
         };
-        let attrs = some!(self.attrs());
-        let body = any!(self; self.markup());
-        Some(Markup::Element(name, attrs, body.map(|body| box body)))
+        let name = name.as_slice();
+        self.render.element_open_start(name);
+        guard!(self.attrs());
+        self.render.element_open_end();
+        guard!(self.markup());
+        self.render.element_close(name);
+        true
     }
 
-    fn attrs(&mut self) -> Option<Vec<(String, Value)>> {
-        let mut attrs = vec![];
+    fn attrs(&mut self) -> bool {
         while let [ident!(name), eq!(), ..] = self.input {
             self.shift(2);
-            let name = name.as_str().to_string();
-            let value = some!(self.value());
-            attrs.push((name, value));
+            self.render.attribute_start(name.as_str());
+            {
+                let old_in_attr = self.in_attr;
+                self.in_attr = true;
+                guard!(self.markup());
+                self.in_attr = old_in_attr;
+            }
+            self.render.attribute_end();
         }
-        Some(attrs)
+        true
     }
 
-    fn block(&mut self) -> Option<Markup> {
+    fn block(&mut self) -> bool {
         match self.input {
             [TtDelimited(_, ref d), ..] if d.delim == token::DelimToken::Brace => {
                 self.shift(1);
-                Parser { cx: self.cx, input: d.tts[] }.markups()
-                    .map(Markup::Block)
+                Parser {
+                    in_attr: self.in_attr,
+                    input: d.tts.as_slice(),
+                    render: self.render,
+                }.markups()
             },
-            _ => None,
+            _ => false,
         }
     }
 }
@@ -228,7 +231,7 @@ fn lit_to_string(cx: &mut ExtCtxt, lit: Lit, minus: bool) -> Option<String> {
             return None;
         },
         LitChar(c) => result.push(c),
-        LitInt(x, _) => result.push_str(x.to_string()[]),
+        LitInt(x, _) => result.push_str(x.to_string().as_slice()),
         LitFloat(s, _) | LitFloatUnsuffixed(s) => result.push_str(s.get()),
         LitBool(b) => result.push_str(if b { "true" } else { "false" }),
     };
diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs
index ed6db94..22225f4 100644
--- a/maud_macros/src/render.rs
+++ b/maud_macros/src/render.rs
@@ -4,70 +4,88 @@ use syntax::ext::base::ExtCtxt;
 use syntax::parse::token;
 use syntax::ptr::P;
 
-use super::parse::{Markup, Value};
+use super::parse::Escape;
 use maud::escape;
 
-pub fn render(cx: &mut ExtCtxt, markups: &[Markup]) -> P<Expr> {
-    let w = Ident::new(token::intern("w"));
-    let mut stmts = vec![];
-    for markup in markups.iter() {
-        render_markup(cx, markup, w, &mut stmts);
-    }
-    quote_expr!(cx, |&: $w: &mut ::std::io::Writer| -> ::std::io::IoResult<()> {
-        $stmts
-        Ok(())
-    })
+pub struct Renderer<'cx, 's: 'cx, 'o> {
+    pub cx: &'cx mut ExtCtxt<'s>,
+    stmts: &'o mut Vec<P<Stmt>>,
+    w: Ident,
 }
 
-fn render_markup(cx: &mut ExtCtxt, markup: &Markup, w: Ident, out: &mut Vec<P<Stmt>>) {
-    use super::parse::Markup::*;
-    match *markup {
-        Element(..) => unimplemented!(),
-        Block(ref markups) => {
-            for markup in markups.iter() {
-                render_markup(cx, markup, w, out);
-            }
-        },
-        Value(ref value) => {
-            let expr = render_value(cx, value, w, false);
-            out.push(quote_stmt!(cx, $expr));
-        },
-    }
-}
-
-fn render_value(cx: &mut ExtCtxt, value: &Value, w: Ident, is_attr: bool) -> P<Expr> {
-    use super::parse::Escape::*;
-    use super::parse::Value_::*;
-    let &Value { ref value, escape } = value;
-    match *value {
-        Literal(ref s) => {
-            let s = match escape {
-                NoEscape => s[].into_cow(),
-                Escape => if is_attr {
-                    escape::attribute(s[]).into_cow()
-                } else {
-                    escape::non_attribute(s[]).into_cow()
-                },
+impl<'cx, 's: 'cx, 'o> Renderer<'cx, 's, 'o> {
+    pub fn with<F>(cx: &'cx mut ExtCtxt<'s>, f: F) -> P<Expr> where
+        F: for<'o_> FnOnce(&mut Renderer<'cx, 's, 'o_>)
+    {
+        let mut stmts = vec![];
+        let w = Ident::new(token::intern("w"));
+        let cx = {
+            let mut render = Renderer {
+                cx: cx,
+                stmts: &mut stmts,
+                w: w,
             };
-            let s = s[];
-            quote_expr!(cx, {
-                try!($w.write_str($s))
-            })
-        },
-        Splice(ref expr) => match escape {
-            NoEscape => quote_expr!(cx, {
-                try!(write!($w, "{}", $expr));
-            }),
-            Escape => quote_expr!(cx, {
-                let s = $expr.to_string();
-                for c in s.chars() {
-                    try!(if $is_attr {
-                            ::maud::rt::escape_attribute(c, $w)
-                        } else {
-                            ::maud::rt::escape_non_attribute(c, $w)
-                        });
-                }
-            }),
-        },
+            f(&mut render);
+            render.cx
+        };
+        quote_expr!(cx, |&: $w: &mut ::std::fmt::Writer| -> ::std::result::Result<(), ::std::fmt::Error> {
+            $stmts
+            Ok(())
+        })
+    }
+
+    /// Append a literal pre-escaped string.
+    pub fn write(&mut self, s: &str) {
+        let w = self.w;
+        self.stmts.push(quote_stmt!(self.cx, try!($w.write_str($s))));
+    }
+
+    /// Append a literal string, with the specified escaping method.
+    pub fn string(&mut self, s: &str, escape: Escape) {
+        let s = match escape {
+            Escape::None => s.into_cow(),
+            Escape::Attr => escape::attribute(s).into_cow(),
+            Escape::Body => escape::non_attribute(s).into_cow(),
+        };
+        self.write(s.as_slice());
+    }
+
+    /// Append the result of an expression, with the specified escaping method.
+    pub fn splice(&mut self, expr: P<Expr>, escape: Escape) {
+        let w = self.w;
+        self.stmts.push(match escape {
+            Escape::None => quote_stmt!(self.cx, try!(write!($w, "{}", $expr))),
+            Escape::Attr =>
+                quote_stmt!(self.cx,
+                    try!(::maud::rt::escape_attribute($w, |w| write!(w, "{}", $expr)))),
+            Escape::Body =>
+                quote_stmt!(self.cx,
+                    try!(::maud::rt::escape_non_attribute($w, |w| write!(w, "{}", $expr)))),
+        });
+    }
+
+    pub fn element_open_start(&mut self, name: &str) {
+        self.write("<");
+        self.write(name);
+    }
+
+    pub fn attribute_start(&mut self, name: &str) {
+        self.write(" ");
+        self.write(name);
+        self.write("=\"");
+    }
+
+    pub fn attribute_end(&mut self) {
+        self.write("\"");
+    }
+
+    pub fn element_open_end(&mut self) {
+        self.write(">");
+    }
+
+    pub fn element_close(&mut self, name: &str) {
+        self.write("</");
+        self.write(name);
+        self.write(">");
     }
 }
diff --git a/maud_macros/tests/tests.rs b/maud_macros/tests/tests.rs
index 7ff4d7e..62f9e7b 100644
--- a/maud_macros/tests/tests.rs
+++ b/maud_macros/tests/tests.rs
@@ -1,4 +1,5 @@
 #![feature(plugin)]
+#![allow(unstable)]
 
 extern crate maud;
 #[plugin] #[no_link] extern crate maud_macros;