diff --git a/htmlthing_macros/src/lib.rs b/htmlthing_macros/src/lib.rs
index 8aef544..5a570f4 100644
--- a/htmlthing_macros/src/lib.rs
+++ b/htmlthing_macros/src/lib.rs
@@ -1,29 +1,29 @@
 #![crate_type = "dylib"]
-#![feature(plugin_registrar)]
+#![feature(globs, plugin_registrar, quote, macro_rules)]
 
 extern crate syntax;
 extern crate rustc;
 
+use syntax::ast::{Ident, TokenTree};
 use syntax::codemap::Span;
+use syntax::ext::base::{DummyResult, ExtCtxt, IdentTT, MacItems, MacResult};
 use syntax::parse::token;
-use syntax::ast::{TokenTree, TtToken};
-use syntax::ext::base::{ExtCtxt, MacResult, DummyResult, MacExpr};
-use syntax::ext::build::AstBuilder;
 use rustc::plugin::Registry;
 
-fn expand_html(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'static> {
-    let s = match args {
-        [TtToken(_, token::Ident(s, _))] => token::get_ident(s),
-        _ => {
-            cx.span_err(sp, "argument should be a single identifier");
-            return DummyResult::any(sp);
-        },
-    };
+mod parse;
+mod render;
 
-    MacExpr::new(cx.expr_str(sp, s))
+fn expand_html<'cx>(cx: &'cx mut ExtCtxt, sp: Span, ident: Ident, args: Vec<TokenTree>) -> Box<MacResult + 'cx> {
+    match parse::parse(cx, &*args) {
+        Some(markups) => {
+            let item = render::render(cx, ident, &*markups);
+            MacItems::new(item.into_iter())
+        },
+        None => DummyResult::any(sp),
+    }
 }
 
 #[plugin_registrar]
 pub fn plugin_registrar(reg: &mut Registry) {
-    reg.register_macro("html", expand_html);
+    reg.register_syntax_extension(token::intern("html"), IdentTT(box expand_html, None));
 }
diff --git a/htmlthing_macros/src/parse.rs b/htmlthing_macros/src/parse.rs
new file mode 100644
index 0000000..2d06acb
--- /dev/null
+++ b/htmlthing_macros/src/parse.rs
@@ -0,0 +1,101 @@
+use syntax::ast::{Expr, Lit, TokenTree, TtToken};
+use syntax::ext::base::ExtCtxt;
+use syntax::parse;
+use syntax::parse::token;
+use syntax::ptr::P;
+
+#[deriving(Show)]
+pub enum Markup {
+    Empty,
+    Element(Vec<(String, Value)>, Vec<Markup>),
+    Value(Value),
+}
+
+#[deriving(Show)]
+pub enum Value {
+    Literal(String),
+    Splice(P<Expr>),
+}
+
+pub fn parse(cx: &mut ExtCtxt, mut args: &[TokenTree]) -> Option<Vec<Markup>> {
+    use self::Markup::*;
+    let mut result = vec![];
+    loop {
+        match parse_markup(cx, &mut args) {
+            Empty => break,
+            markup => result.push(markup),
+        }
+    }
+    // If not all tokens were consumed, then there must have been an
+    // error somewhere
+    match args {
+        [] => Some(result),
+        _ => None,
+    }
+}
+
+fn parse_markup(cx: &mut ExtCtxt, args: &mut &[TokenTree]) -> Markup {
+    use self::Markup::*;
+    use self::Value::*;
+    if let Some(s) = parse_literal(cx, args) {
+        Value(Literal(s))
+    } else {
+        match *args {
+            [] => Empty,
+            [ref tt, ..] => {
+                cx.span_err(tt.get_span(), "invalid syntax");
+                Empty
+            },
+        }
+    }
+}
+
+fn parse_literal(cx: &mut ExtCtxt, args: &mut &[TokenTree]) -> Option<String> {
+    let minus = match *args {
+        [TtToken(_, token::BinOp(token::Minus)), ..] => {
+            args.shift(1);
+            true
+        },
+        _ => false,
+    };
+
+    match *args {
+        [ref tt @ TtToken(_, token::Literal(..)), ..] => {
+            args.shift(1);
+            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)
+        },
+        _ => None,
+    }
+}
+
+fn lit_to_string(cx: &mut ExtCtxt, lit: Lit, minus: bool) -> Option<String> {
+    use syntax::ast::Lit_::*;
+    let mut result = String::new();
+    if minus {
+        result.push('-');
+    }
+    match lit.node {
+        LitStr(s, _) => result.push_str(s.get()),
+        LitBinary(..) | LitByte(..) => {
+            cx.span_err(lit.span, "cannot splice binary data");
+            return None;
+        },
+        LitChar(c) => result.push(c),
+        LitInt(x, _) => result.push_str(&*x.to_string()),
+        LitFloat(s, _) | LitFloatUnsuffixed(s) => result.push_str(s.get()),
+        LitBool(b) => result.push_str(if b { "true" } else { "false" }),
+    };
+    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/htmlthing_macros/src/render.rs b/htmlthing_macros/src/render.rs
new file mode 100644
index 0000000..fb330e0
--- /dev/null
+++ b/htmlthing_macros/src/render.rs
@@ -0,0 +1,33 @@
+use syntax::ast::{Ident, Item, Stmt};
+use syntax::ext::base::ExtCtxt;
+use syntax::parse::token;
+use syntax::ptr::P;
+
+use super::parse::Markup;
+
+pub fn render(cx: &mut ExtCtxt, ident: Ident, markups: &[Markup]) -> Option<P<Item>> {
+    let w = Ident::new(token::intern("w"));
+    let mut stmts = vec![];
+    for markup in markups.iter() {
+        render_markup(cx, markup, w, &mut stmts);
+    }
+    quote_item!(cx,
+        fn $ident<W: ::std::io::Writer>($w: &mut W) -> ::std::io::IoResult<()> {
+            $stmts;
+            Ok(())
+        }
+    )
+}
+
+fn render_markup(cx: &mut ExtCtxt, markup: &Markup, w: Ident, out: &mut Vec<P<Stmt>>) {
+    use super::parse::Markup::*;
+    use super::parse::Value::*;
+    match *markup {
+        Empty => {},
+        Element(..) => unimplemented!(),
+        Value(Literal(ref s)) => {
+            out.push(quote_stmt!(cx, try!($w.write_str($s))));
+        },
+        Value(Splice(_)) => unimplemented!(),
+    }
+}
diff --git a/htmlthing_macros/tests/tests.rs b/htmlthing_macros/tests/tests.rs
index 14c6071..5c10bac 100644
--- a/htmlthing_macros/tests/tests.rs
+++ b/htmlthing_macros/tests/tests.rs
@@ -5,6 +5,9 @@ extern crate htmlthing;
 
 #[test]
 fn it_works() {
-    let s = html!(ducks);
-    assert_eq!(s, "ducks");
+    let mut buf = vec![];
+    html! test_template("du\tcks" -23 3.14 '\n' "geese");
+    test_template(&mut buf).unwrap();
+    let s = String::from_utf8(buf).unwrap();
+    assert_eq!(&*s, "du\tcks-233.14\ngeese");
 }