diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs
index e7ba5f4..0aa8a8f 100644
--- a/maud_macros/src/lib.rs
+++ b/maud_macros/src/lib.rs
@@ -14,7 +14,7 @@ mod parse;
 mod render;
 
 fn expand_html<'cx>(cx: &'cx mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'cx> {
-    match parse::parse(cx, &*args) {
+    match parse::parse(cx, args) {
         Some(markups) => {
             let expr = render::render(cx, &*markups);
             MacExpr::new(expr)
diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index b38e1ba..6cf16cd 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -1,12 +1,14 @@
-use syntax::ast::{Expr, Lit, TokenTree, TtToken};
+use syntax::ast::{Expr, Lit, TokenTree, TtDelimited, TtToken};
 use syntax::ext::base::ExtCtxt;
 use syntax::parse;
+use syntax::parse::parser::Parser as RustParser;
 use syntax::parse::token;
 use syntax::ptr::P;
 
 #[derive(Show)]
 pub enum Markup {
-    Element(Vec<(String, Value)>, Vec<Markup>),
+    Element(String, Vec<(String, Value)>, Option<Box<Markup>>),
+    Block(Vec<Markup>),
     Value(Value),
 }
 
@@ -16,22 +18,6 @@ pub struct Value {
     pub escape: Escape,
 }
 
-impl Value {
-    pub fn escape(value: Value_) -> Value {
-        Value {
-            value: value,
-            escape: Escape::Escape,
-        }
-    }
-
-    pub fn no_escape(value: Value_) -> Value {
-        Value {
-            value: value,
-            escape: Escape::NoEscape,
-        }
-    }
-}
-
 #[derive(Show)]
 pub enum Value_ {
     Literal(String),
@@ -44,54 +30,191 @@ pub enum Escape {
     Escape,
 }
 
-pub fn parse(cx: &mut ExtCtxt, mut args: &[TokenTree]) -> Option<Vec<Markup>> {
-    macro_rules! semi {
-        () => (TtToken(_, token::Semi))
-    }
-    macro_rules! minus {
-        () => (TtToken(_, token::BinOp(token::Minus)))
-    }
-    macro_rules! literal {
-        () => (TtToken(_, token::Literal(..)))
+macro_rules! some {
+    ($e:expr) => (
+        match $e {
+            Some(x) => x,
+            None => return None,
+        }
+    )
+}
+
+macro_rules! any {
+    ($self_:expr;) => (None);
+    ($self_:expr; $e:expr) => (any!($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;
+                }
+            },
+        }
+    })
+}
+
+macro_rules! dollar {
+    () => (TtToken(_, token::Dollar))
+}
+macro_rules! eq {
+    () => (TtToken(_, token::Eq))
+}
+macro_rules! semi {
+    () => (TtToken(_, token::Semi))
+}
+macro_rules! minus {
+    () => (TtToken(_, token::BinOp(token::Minus)))
+}
+macro_rules! literal {
+    () => (TtToken(_, token::Literal(..)))
+}
+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()
+}
+
+struct Parser<'cx, 's: 'cx, 'i> {
+    cx: &'cx mut ExtCtxt<'s>,
+    input: &'i [TokenTree],
+}
+
+impl<'cx, 's, 'i> Parser<'cx, 's, 'i> {
+    /// Consume `n` items from the input.
+    fn shift(&mut self, n: uint) {
+        self.input = self.input.slice_from(n);
     }
 
-    let mut result = vec![];
-    loop {
-        match match args {
-            [semi!(), ..] => {
-                args.shift(1);
-                continue
-            },
+    /// 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())
+    }
+
+    fn markups(&mut self) -> Option<Vec<Markup>> {
+        let mut result = vec![];
+        loop {
+            match self.input {
+                [] => return Some(result),
+                [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;
+                        },
+                    }
+                }
+            }
+        }
+    }
+
+    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;
+            self.literal(),
+            self.splice())
+    }
+
+    fn literal(&mut self) -> Option<Value> {
+        let (tt, minus) = match self.input {
             [minus!(), ref tt @ literal!(), ..] => {
-                args.shift(2);
-                parse_literal(cx, tt, true)
+                self.shift(2);
+                (tt, true)
             },
             [ref tt @ literal!(), ..] => {
-                args.shift(1);
-                parse_literal(cx, tt, false)
+                self.shift(1);
+                (tt, false)
+            },
+            _ => return None,
+        };
+        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,
+            })
+    }
+
+    fn splice(&mut self) -> Option<Value> {
+        let (escape, sp) = match self.input {
+            [ref tt @ dollar!(), dollar!(), ..] => {
+                self.shift(2);
+                (Escape::NoEscape, tt.get_span())
+            },
+            [ref tt @ dollar!(), ..] => {
+                self.shift(1);
+                (Escape::Escape, tt.get_span())
+            },
+            _ => return None,
+        };
+        let tt = match self.input {
+            [ref tt, ..] => {
+                self.shift(1);
+                self.new_rust_parser(vec![tt.clone()]).parse_expr()
+            },
+            _ => {
+                self.cx.span_err(sp, "expected expression for this splice");
+                return None;
+            },
+        };
+        Some(Value {
+            value: Value_::Splice(tt),
+            escape: escape,
+        })
+    }
+
+    fn element(&mut self) -> Option<Markup> {
+        let name = match self.input {
+            [ident!(name), ..] => {
+                self.shift(1);
+                name.as_str().to_string()
+            },
+            _ => return None,
+        };
+        let attrs = some!(self.attrs());
+        let body = any!(self; self.markup());
+        Some(Markup::Element(name, attrs, body.map(|body| box body)))
+    }
+
+    fn attrs(&mut self) -> Option<Vec<(String, Value)>> {
+        let mut attrs = vec![];
+        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));
+        }
+        Some(attrs)
+    }
+
+    fn block(&mut self) -> Option<Markup> {
+        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)
             },
             _ => None,
-        } {
-            Some(x) => result.push(x),
-            None => break,
-        }
-    }
-    match args {
-        [] => Some(result),
-        [ref tt, ..] => {
-            cx.span_err(tt.get_span(), "invalid syntax");
-            None
         }
     }
 }
 
-fn parse_literal(cx: &mut ExtCtxt, tt: &TokenTree, minus: bool) -> Option<Markup> {
-    let mut parser = parse::tts_to_parser(cx.parse_sess, vec![tt.clone()], cx.cfg.clone());
-    let lit = parser.parse_lit();
-    lit_to_string(cx, lit, minus)
-        .map(|s| Markup::Value(Value::escape(Value_::Literal(s))))
-}
-
+/// Convert a literal to a string.
 fn lit_to_string(cx: &mut ExtCtxt, lit: Lit, minus: bool) -> Option<String> {
     use syntax::ast::Lit_::*;
     let mut result = String::new();
@@ -111,13 +234,3 @@ fn lit_to_string(cx: &mut ExtCtxt, lit: Lit, minus: bool) -> Option<String> {
     };
     Some(result)
 }
-
-trait Shift {
-    fn shift(&mut self, n: uint);
-}
-
-impl<'a, T> Shift for &'a [T] {
-    fn shift(&mut self, n: uint) {
-        *self = self.slice_from(n);
-    }
-}
diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs
index 4d9d444..7d4fceb 100644
--- a/maud_macros/src/render.rs
+++ b/maud_macros/src/render.rs
@@ -23,6 +23,11 @@ fn render_markup(cx: &mut ExtCtxt, markup: &Markup, w: Ident, out: &mut Vec<P<St
     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));
diff --git a/maud_macros/tests/tests.rs b/maud_macros/tests/tests.rs
index 6ffede0..cd8b576 100644
--- a/maud_macros/tests/tests.rs
+++ b/maud_macros/tests/tests.rs
@@ -16,3 +16,65 @@ fn escaping() {
     let s = maud::render(template);
     assert_eq!(&*s, "&lt;flim&amp;flam&gt;");
 }
+
+#[test]
+fn blocks() {
+    let s = maud::render(html! {
+        "hello"
+        {
+            " ducks";
+            " geese";
+        }
+        " swans"
+    });
+    assert_eq!(&*s, "hello ducks geese swans");
+}
+
+mod splice {
+    use super::maud;  // lol
+
+    #[test]
+    fn literal() {
+        let s = maud::render(html! { $"<pinkie>" });
+        assert_eq!(&*s, "&lt;pinkie&gt;");
+    }
+
+    #[test]
+    fn raw_literal() {
+        let s = maud::render(html! { $$"<pinkie>" });
+        assert_eq!(&*s, "<pinkie>");
+    }
+
+    #[test]
+    fn block() {
+        let s = maud::render(html! {
+            ${
+                let mut result = 1i32;
+                for i in range(2, 11) {
+                    result *= i;
+                }
+                result
+            }
+        });
+        assert_eq!(&*s, "3628800");
+    }
+
+    static BEST_PONY: &'static str = "Pinkie Pie";
+
+    #[test]
+    fn statics() {
+        let s = maud::render(html! { $BEST_PONY });
+        assert_eq!(&*s, "Pinkie Pie");
+    }
+
+    // FIXME: See <https://github.com/rust-lang/rust/issues/15962>
+    // for why this is commented out
+    /*
+    #[test]
+    fn closure() {
+        let best_pony = "Pinkie Pie";
+        let s = maud::render(html! { $best_pony });
+        assert_eq!(&*s, "Pinkie Pie");
+    }
+    */
+}