From e0c7fcf055be89f7fc0dfd2857c1f957bfd58989 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Fri, 7 Jul 2017 22:59:20 +1200
Subject: [PATCH 01/17] Initial stab at porting to new proc macro interface

---
 maud/Cargo.toml                               |   1 +
 maud/src/lib.rs                               |   5 +
 {maud_macros => maud}/tests/basic_syntax.rs   |   5 +-
 .../tests/control_structures.rs               |   5 +-
 {maud_macros => maud}/tests/misc.rs           |   5 +-
 {maud_macros => maud}/tests/splices.rs        |   5 +-
 maud_macros/Cargo.toml                        |   4 +-
 maud_macros/src/lib.rs                        |  49 +-
 maud_macros/src/parse.rs                      | 577 ++++++------------
 maud_macros/src/render.rs                     | 131 ++--
 10 files changed, 300 insertions(+), 487 deletions(-)
 rename {maud_macros => maud}/tests/basic_syntax.rs (98%)
 rename {maud_macros => maud}/tests/control_structures.rs (98%)
 rename {maud_macros => maud}/tests/misc.rs (96%)
 rename {maud_macros => maud}/tests/splices.rs (97%)

diff --git a/maud/Cargo.toml b/maud/Cargo.toml
index 9349900..bfcb7bf 100644
--- a/maud/Cargo.toml
+++ b/maud/Cargo.toml
@@ -13,6 +13,7 @@ description = "Compile-time HTML templates."
 categories = ["template-engine"]
 
 [dependencies]
+maud_macros = { version = "0.16.3", path = "../maud_macros" }
 iron = { version = "0.5.1", optional = true }
 rocket = { version = "0.3", optional = true }
 
diff --git a/maud/src/lib.rs b/maud/src/lib.rs
index 4e9dec2..95b3d71 100644
--- a/maud/src/lib.rs
+++ b/maud/src/lib.rs
@@ -1,3 +1,4 @@
+#![feature(proc_macro)]
 #![feature(specialization)]
 
 //! A macro for writing HTML templates.
@@ -12,8 +13,12 @@
 #[cfg(feature = "iron")] extern crate iron;
 #[cfg(feature = "rocket")] extern crate rocket;
 
+extern crate maud_macros;
+
 use std::fmt::{self, Write};
 
+pub use maud_macros::{html, html_debug};
+
 /// Represents a type that can be rendered as HTML.
 ///
 /// If your type implements [`Display`][1], then it will implement this
diff --git a/maud_macros/tests/basic_syntax.rs b/maud/tests/basic_syntax.rs
similarity index 98%
rename from maud_macros/tests/basic_syntax.rs
rename to maud/tests/basic_syntax.rs
index 30ccaff..a31cd39 100644
--- a/maud_macros/tests/basic_syntax.rs
+++ b/maud/tests/basic_syntax.rs
@@ -1,9 +1,8 @@
-#![feature(plugin)]
-#![plugin(maud_macros)]
+#![feature(proc_macro)]
 
 extern crate maud;
 
-use maud::Markup;
+use maud::{Markup, html};
 
 #[test]
 fn literals() {
diff --git a/maud_macros/tests/control_structures.rs b/maud/tests/control_structures.rs
similarity index 98%
rename from maud_macros/tests/control_structures.rs
rename to maud/tests/control_structures.rs
index 7d574f5..891d10e 100644
--- a/maud_macros/tests/control_structures.rs
+++ b/maud/tests/control_structures.rs
@@ -1,8 +1,9 @@
-#![feature(conservative_impl_trait, plugin)]
-#![plugin(maud_macros)]
+#![feature(conservative_impl_trait, proc_macro)]
 
 extern crate maud;
 
+use maud::html;
+
 #[test]
 fn if_expr() {
     for (number, &name) in (1..4).zip(["one", "two", "three"].iter()) {
diff --git a/maud_macros/tests/misc.rs b/maud/tests/misc.rs
similarity index 96%
rename from maud_macros/tests/misc.rs
rename to maud/tests/misc.rs
index 8928379..c367db3 100644
--- a/maud_macros/tests/misc.rs
+++ b/maud/tests/misc.rs
@@ -1,8 +1,9 @@
-#![feature(plugin)]
-#![plugin(maud_macros)]
+#![feature(proc_macro)]
 
 extern crate maud;
 
+use maud::html;
+
 #[test]
 fn issue_13() {
     let owned = String::from("yay");
diff --git a/maud_macros/tests/splices.rs b/maud/tests/splices.rs
similarity index 97%
rename from maud_macros/tests/splices.rs
rename to maud/tests/splices.rs
index b5dc006..4a221f0 100644
--- a/maud_macros/tests/splices.rs
+++ b/maud/tests/splices.rs
@@ -1,8 +1,9 @@
-#![feature(plugin)]
-#![plugin(maud_macros)]
+#![feature(proc_macro)]
 
 extern crate maud;
 
+use maud::html;
+
 #[test]
 fn literals() {
     let s = html!(("<pinkie>")).into_string();
diff --git a/maud_macros/Cargo.toml b/maud_macros/Cargo.toml
index e4ff337..6c43639 100644
--- a/maud_macros/Cargo.toml
+++ b/maud_macros/Cargo.toml
@@ -13,11 +13,11 @@ description = "Compile-time HTML templates."
 
 [dependencies]
 if_chain = "0.1"
-maud = { path = "../maud", version = "0.16.0" }
+literalext = { git = "https://github.com/mystor/literalext.git", default-features = false, features = ["proc-macro"] }
 
 [lib]
 name = "maud_macros"
-plugin = true
+proc-macro = true
 
 [badges]
 travis-ci = { repository = "lfairy/maud" }
diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs
index b580f47..dcc70c0 100644
--- a/maud_macros/src/lib.rs
+++ b/maud_macros/src/lib.rs
@@ -1,53 +1,46 @@
-#![crate_type = "dylib"]
-#![feature(plugin_registrar, quote)]
-#![feature(slice_patterns)]
-#![feature(rustc_private)]
+#![feature(proc_macro)]
 #![recursion_limit = "1000"]  // if_chain
 
 #![doc(html_root_url = "https://docs.rs/maud_macros/0.16.3")]
 
 #[macro_use]
 extern crate if_chain;
-#[macro_use]
-extern crate rustc;
-extern crate rustc_plugin;
-extern crate syntax;
-extern crate maud;
+extern crate literalext;
+extern crate proc_macro;
 
-use rustc_plugin::Registry;
-use syntax::codemap::Span;
-use syntax::ext::base::{DummyResult, ExtCtxt, MacEager, MacResult};
-use syntax::print::pprust;
-use syntax::tokenstream::TokenTree;
-
-mod lints;
+// TODO move lints into their own `maud_lints` crate
+// mod lints;
 mod parse;
 mod render;
 
-type ParseResult<T> = Result<T, ()>;
+use proc_macro::TokenStream;
 
-fn expand_html<'cx>(cx: &'cx mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'cx> {
-    match parse::parse(cx, sp, args) {
-        Ok(expr) => MacEager::expr(quote_expr!(cx, $expr)),
-        Err(..) => DummyResult::expr(sp),
+type ParseResult<T> = Result<T, String>;
+
+#[proc_macro]
+pub fn html(args: TokenStream) -> TokenStream {
+    match parse::parse(args) {
+        Ok(expr) => expr,
+        Err(e) => panic!(e),
     }
 }
 
-fn expand_html_debug<'cx>(cx: &'cx mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'cx> {
-    match parse::parse(cx, sp, args) {
+#[proc_macro]
+pub fn html_debug(args: TokenStream) -> TokenStream {
+    match parse::parse(args) {
         Ok(expr) => {
-            let expr = quote_expr!(cx, $expr);
-            cx.span_warn(sp, &format!("expansion:\n{}",
-                                      pprust::expr_to_string(&expr)));
-            MacEager::expr(expr)
+            println!("expansion:\n{}", expr);
+            expr
         },
-        Err(..) => DummyResult::expr(sp),
+        Err(e) => panic!(e),
     }
 }
 
+/*
 #[plugin_registrar]
 pub fn plugin_registrar(reg: &mut Registry) {
     reg.register_macro("html", expand_html);
     reg.register_macro("html_debug", expand_html_debug);
     lints::register_lints(reg);
 }
+*/
diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 06accaf..362f066 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -1,484 +1,246 @@
+use proc_macro::{Delimiter, Literal, TokenNode, TokenStream, TokenTree, TokenTreeIter};
 use std::mem;
-use syntax::ast::LitKind;
-use syntax::codemap::Span;
-use syntax::ext::base::ExtCtxt;
-use syntax::parse;
-use syntax::parse::token::{BinOpToken, DelimToken, Token};
-use syntax::print::pprust;
-use syntax::symbol::keywords;
-use syntax::tokenstream::{Delimited, TokenStream, TokenTree};
+
+use literalext::LiteralExt;
 
 use super::render::Renderer;
 use super::ParseResult;
 
-macro_rules! at {
-    () => (TokenTree::Token(_, Token::At))
-}
-macro_rules! dot {
-    () => (TokenTree::Token(_, Token::Dot))
-}
-macro_rules! eq {
-    () => (TokenTree::Token(_, Token::Eq))
-}
-macro_rules! pound {
-    () => (TokenTree::Token(_, Token::Pound))
-}
-macro_rules! question {
-    () => (TokenTree::Token(_, Token::Question))
-}
-macro_rules! semi {
-    () => (TokenTree::Token(_, Token::Semi))
-}
-macro_rules! colon {
-    () => (TokenTree::Token(_, Token::Colon))
-}
-macro_rules! comma {
-    () => (TokenTree::Token(_, Token::Comma))
-}
-macro_rules! fat_arrow {
-    () => (TokenTree::Token(_, Token::FatArrow))
-}
-macro_rules! minus {
-    () => (TokenTree::Token(_, Token::BinOp(BinOpToken::Minus)))
-}
-macro_rules! slash {
-    () => (TokenTree::Token(_, Token::BinOp(BinOpToken::Slash)))
-}
-macro_rules! literal {
-    () => (TokenTree::Token(_, Token::Literal(..)))
-}
-macro_rules! ident {
-    ($sp:pat, $x:pat) => (TokenTree::Token($sp, Token::Ident($x)))
-}
-macro_rules! keyword {
-    ($sp:pat, $x:ident) => (TokenTree::Token($sp, ref $x @ Token::Ident(..)))
-}
-
-pub fn parse(cx: &ExtCtxt, sp: Span, input: &[TokenTree]) -> ParseResult<Vec<TokenTree>> {
-    let mut render = Renderer::new(cx);
-    Parser {
-        cx,
+pub fn parse(input: TokenStream) -> ParseResult<TokenStream> {
+    let mut render = Renderer::new();
+    let _ = Parser {
         in_attr: false,
-        input: input,
-        span: sp,
+        input: Lookahead::new(input.clone()),
+    }.markups(&mut render);
+    /*
+    Parser {
+        in_attr: false,
+        input: Lookahead::new(input.clone()),
     }.markups(&mut render)?;
+    */
     // Heuristic: the size of the resulting markup tends to correlate with the
     // code size of the template itself
-    let size_hint = pprust::tts_to_string(input).len();
-    Ok(render.into_expr(size_hint).into_trees().collect())
+    let size_hint = input.to_string().len();
+    Ok(render.into_expr(size_hint))
 }
 
-struct Parser<'cx, 'a: 'cx, 'i> {
-    cx: &'cx ExtCtxt<'a>,
+struct Parser {
     in_attr: bool,
-    input: &'i [TokenTree],
-    span: Span,
+    input: Lookahead<TokenTree>,
 }
 
-impl<'cx, 'a, 'i> Parser<'cx, 'a, 'i> {
-    /// Consumes `n` items from the input.
-    fn shift(&mut self, n: usize) {
-        self.input = &self.input[n..];
+impl Parser {
+    fn next(&mut self) -> Option<TokenNode> {
+        self.input.next().map(|tt| tt.kind)
+    }
+
+    fn peek(&mut self) -> Option<TokenNode> {
+        self.input.peek().map(|tt| tt.kind)
+    }
+
+    fn advance(&mut self) {
+        self.next();
     }
 
     /// Attaches an error message to the span and returns `Err`.
-    fn error<T>(&self, span: Span, message: &str) -> ParseResult<T> {
-        self.cx.span_err(span, message);
-        Err(())
+    fn error<T, E: Into<String>>(&self, message: E) -> ParseResult<T> {
+        Err(message.into())
     }
 
     /// Parses and renders multiple blocks of markup.
     fn markups(&mut self, render: &mut Renderer) -> ParseResult<()> {
         loop {
-            match *self.input {
-                [] => return Ok(()),
-                [semi!(), ..] => self.shift(1),
-                [_, ..] => self.markup(render)?,
+            match self.peek() {
+                None => return Ok(()),
+                Some(TokenNode::Op(';', _)) => self.advance(),
+                _ => self.markup(render)?,
             }
         }
     }
 
     /// Parses and renders a single block of markup.
     fn markup(&mut self, render: &mut Renderer) -> ParseResult<()> {
-        match *self.input {
+        let token = match self.peek() {
+            Some(token) => token,
+            None => return self.error("unexpected end of input"),
+        };
+        match token {
             // Literal
-            [ref tt @ literal!(), ..] => {
-                self.shift(1);
-                self.literal(tt, render)?;
+            TokenNode::Literal(lit) => {
+                self.advance();
+                self.literal(lit, render)?;
             },
-            // If
-            [at!(), keyword!(sp, k), ..] if k.is_keyword(keywords::If) => {
-                self.shift(2);
-                self.if_expr(sp, render)?;
-            },
-            // While
-            [at!(), keyword!(sp, k), ..] if k.is_keyword(keywords::While) => {
-                self.shift(2);
-                self.while_expr(sp, render)?;
-            },
-            // For
-            [at!(), keyword!(sp, k), ..] if k.is_keyword(keywords::For) => {
-                self.shift(2);
-                self.for_expr(sp, render)?;
-            },
-            // Match
-            [at!(), keyword!(sp, k), ..] if k.is_keyword(keywords::Match) => {
-                self.shift(2);
-                self.match_expr(sp, render)?;
-            },
-            // Let
-            [at!(), keyword!(sp, k), ..] if k.is_keyword(keywords::Let) => {
-                self.shift(2);
-                self.let_expr(sp, render)?;
+            // Special form
+            TokenNode::Op('@', _) => {
+                self.advance();
+                match self.next() {
+                    Some(TokenNode::Term(term)) => match term.as_str() {
+                        "if" => self.if_expr(render)?,
+                        "while" => self.while_expr(render)?,
+                        "for" => self.for_expr(render)?,
+                        "match" => self.match_expr(render)?,
+                        "let" => self.let_expr(render)?,
+                        other => return self.error(format!("unknown keyword `@{}`", other)),
+                    },
+                    _ => return self.error("expected keyword after `@`"),
+                }
             }
             // Element
-            [ident!(sp, _), ..] => {
-                let name = self.namespaced_name().unwrap();
-                self.element(sp, &name, render)?;
+            TokenNode::Term(_) => {
+                let name = self.namespaced_name()?;
+                self.element(&name, render)?;
             },
             // Splice
-            [TokenTree::Delimited(_, ref d), ..] if d.delim == DelimToken::Paren => {
-                self.shift(1);
-                render.splice(d.stream());
+            TokenNode::Group(Delimiter::Parenthesis, expr) => {
+                self.advance();
+                render.splice(expr);
             }
             // Block
-            [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => {
-                self.shift(1);
+            TokenNode::Group(Delimiter::Brace, block) => {
+                self.advance();
                 Parser {
-                    cx: self.cx,
                     in_attr: self.in_attr,
-                    input: &d.stream().into_trees().collect::<Vec<_>>(),
-                    span: sp,
+                    input: Lookahead::new(block),
                 }.markups(render)?;
             },
             // ???
-            _ => {
-                if let [ref tt, ..] = *self.input {
-                    return self.error(tt.span(), "invalid syntax");
-                } else {
-                    return self.error(self.span, "unexpected end of block");
-                }
-            },
+            _ => return self.error("invalid syntax"),
         }
         Ok(())
     }
 
     /// Parses and renders a literal string.
-    fn literal(&mut self, tt: &TokenTree, render: &mut Renderer) -> ParseResult<()> {
-        let mut rust_parser = parse::stream_to_parser(self.cx.parse_sess, tt.clone().into());
-        let lit = rust_parser.parse_lit().map_err(|mut e| e.emit())?;
-        if let LitKind::Str(s, _) = lit.node {
-            render.string(&s.as_str());
+    fn literal(&mut self, lit: Literal, render: &mut Renderer) -> ParseResult<()> {
+        if let Some(s) = lit.parse_string() {
+            render.string(&s);
             Ok(())
         } else {
-            return self.error(lit.span, "literal strings must be surrounded by quotes (\"like this\")")
+            self.error("expected string")
         }
     }
 
     /// Parses and renders an `@if` expression.
     ///
     /// The leading `@if` should already be consumed.
-    fn if_expr(&mut self, sp: Span, render: &mut Renderer) -> ParseResult<()> {
-        // Parse the initial if
-        let mut if_cond = vec![];
-        let if_body;
-        loop { match *self.input {
-            [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => {
-                self.shift(1);
-                if_body = self.block(sp, d.stream(), render)?;
-                break;
-            },
-            [ref tt, ..] => {
-                self.shift(1);
-                if_cond.push(tt.clone());
-            },
-            [] => return self.error(sp, "expected body for this @if"),
-        }}
-        // Parse the (optional) @else
-        let else_body = match *self.input {
-            [at!(), keyword!(_, k), ..] if k.is_keyword(keywords::Else) => {
-                self.shift(2);
-                match *self.input {
-                    [keyword!(sp, k), ..] if k.is_keyword(keywords::If) => {
-                        self.shift(1);
-                        let mut render = render.fork();
-                        self.if_expr(sp, &mut render)?;
-                        Some(render.into_stmts())
-                    },
-                    [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => {
-                        self.shift(1);
-                        Some(self.block(sp, d.stream(), render)?)
-                    },
-                    _ => return self.error(sp, "expected body for this @else"),
-                }
-            },
-            _ => None,
-        };
-        render.emit_if(if_cond.into_iter().collect(), if_body, else_body);
-        Ok(())
+    fn if_expr(&mut self, render: &mut Renderer) -> ParseResult<()> {
+        self.error("unimplemented")
     }
 
     /// Parses and renders an `@while` expression.
     ///
     /// The leading `@while` should already be consumed.
-    fn while_expr(&mut self, sp: Span, render: &mut Renderer) -> ParseResult<()> {
-        let mut cond = vec![];
-        let body;
-        loop { match *self.input {
-            [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => {
-                self.shift(1);
-                body = self.block(sp, d.stream(), render)?;
-                break;
-            },
-            [ref tt, ..] => {
-                self.shift(1);
-                cond.push(tt.clone());
-            },
-            [] => return self.error(sp, "expected body for this @while"),
-        }}
-        render.emit_while(cond.into_iter().collect(), body);
-        Ok(())
+    fn while_expr(&mut self, render: &mut Renderer) -> ParseResult<()> {
+        self.error("unimplemented")
     }
 
     /// Parses and renders a `@for` expression.
     ///
     /// The leading `@for` should already be consumed.
-    fn for_expr(&mut self, sp: Span, render: &mut Renderer) -> ParseResult<()> {
-        let mut pattern = vec![];
-        loop { match *self.input {
-            [keyword!(_, k), ..] if k.is_keyword(keywords::In) => {
-                self.shift(1);
-                break;
-            },
-            [ref tt, ..] => {
-                self.shift(1);
-                pattern.push(tt.clone());
-            },
-            _ => return self.error(sp, "invalid @for"),
-        }}
-        let mut iterable = vec![];
-        let body;
-        loop { match *self.input {
-            [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => {
-                self.shift(1);
-                body = self.block(sp, d.stream(), render)?;
-                break;
-            },
-            [ref tt, ..] => {
-                self.shift(1);
-                iterable.push(tt.clone());
-            },
-            _ => return self.error(sp, "invalid @for"),
-        }}
-        render.emit_for(pattern.into_iter().collect(), iterable.into_iter().collect(), body);
-        Ok(())
+    fn for_expr(&mut self, render: &mut Renderer) -> ParseResult<()> {
+        self.error("unimplemented")
     }
 
     /// Parses and renders a `@match` expression.
     ///
     /// The leading `@match` should already be consumed.
-    fn match_expr(&mut self, sp: Span, render: &mut Renderer) -> ParseResult<()> {
-        // Parse the initial match
-        let mut match_var = vec![];
-        let match_bodies;
-        loop { match *self.input {
-            [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => {
-                self.shift(1);
-                match_bodies = Parser {
-                    cx: self.cx,
-                    in_attr: self.in_attr,
-                    input: &d.stream().into_trees().collect::<Vec<_>>(),
-                    span: sp,
-                }.match_bodies(render)?;
-                break;
-            },
-            [ref tt, ..] => {
-                self.shift(1);
-                match_var.push(tt.clone());
-            },
-            [] => return self.error(sp, "expected body for this @match"),
-        }}
-        render.emit_match(match_var.into_iter().collect(), match_bodies.into_iter().collect());
-        Ok(())
+    fn match_expr(&mut self, render: &mut Renderer) -> ParseResult<()> {
+        self.error("unimplemented")
     }
 
     fn match_bodies(&mut self, render: &mut Renderer) -> ParseResult<Vec<TokenTree>> {
-        let mut bodies = Vec::new();
-        loop { match *self.input {
-            [] => break,
-            [ref tt @ comma!(), ..] => {
-                self.shift(1);
-                bodies.push(tt.clone());
-            },
-            [ref tt, ..] => bodies.append(&mut self.match_body(tt.span(), render)?),
-        }}
-        Ok(bodies)
+        self.error("unimplemented")
     }
 
-    fn match_body(&mut self, sp: Span, render: &mut Renderer) -> ParseResult<Vec<TokenTree>> {
-        let mut body = vec![];
-        loop { match *self.input {
-            [ref tt @ fat_arrow!(), ..] => {
-                self.shift(1);
-                body.push(tt.clone());
-                break;
-            },
-            [ref tt, ..] => {
-                self.shift(1);
-                body.push(tt.clone());
-            },
-            _ => return self.error(sp, "invalid @match pattern"),
-        }}
-        let mut expr = Vec::new();
-        loop { match *self.input {
-            [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => {
-                if expr.is_empty() {
-                    self.shift(1);
-                    expr = self.block(sp, d.stream(), render)?.into_trees().collect();
-                    break;
-                } else {
-                    self.shift(1);
-                    expr.push(TokenTree::Delimited(sp, d.clone()));
-                }
-            },
-            [comma!(), ..] | [] => {
-                if expr.is_empty() {
-                    return self.error(sp, "expected body for this @match arm");
-                } else {
-                    expr = self.block(sp, expr.into_iter().collect(), render)?.into_trees().collect();
-                    break;
-                }
-            },
-            [ref tt, ..] => {
-                self.shift(1);
-                expr.push(tt.clone());
-            },
-        }}
-        body.push(TokenTree::Delimited(sp, Delimited {
-            delim: DelimToken::Brace,
-            tts: expr.into_iter().collect::<TokenStream>().into(),
-        }));
-        Ok(body)
+    fn match_body(&mut self, render: &mut Renderer) -> ParseResult<Vec<TokenTree>> {
+        self.error("unimplemented")
     }
 
     /// Parses and renders a `@let` expression.
     ///
     /// The leading `@let` should already be consumed.
-    fn let_expr(&mut self, sp: Span, render: &mut Renderer) -> ParseResult<()> {
-        let mut pattern = vec![];
-        loop { match *self.input {
-            [eq!(), ..] => {
-                self.shift(1);
-                break;
-            },
-            [ref tt, ..] => {
-                self.shift(1);
-                pattern.push(tt.clone());
-            },
-            _ => return self.error(sp, "invalid @let"),
-        }}
-        let mut rhs = vec![];
-        let body;
-        loop { match *self.input {
-            [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => {
-                self.shift(1);
-                body = self.block(sp, d.stream(), render)?;
-                break;
-            },
-            [ref tt, ..] => {
-                self.shift(1);
-                rhs.push(tt.clone());
-            },
-            _ => return self.error(sp, "invalid @let"),
-        }}
-        render.emit_let(pattern.into_iter().collect(), rhs.into_iter().collect(), body);
-        Ok(())
+    fn let_expr(&mut self, render: &mut Renderer) -> ParseResult<()> {
+        self.error("unimplemented")
     }
 
     /// Parses and renders an element node.
     ///
     /// The element name should already be consumed.
-    fn element(&mut self, sp: Span, name: &str, render: &mut Renderer) -> ParseResult<()> {
+    fn element(&mut self, name: &str, render: &mut Renderer) -> ParseResult<()> {
         if self.in_attr {
-            return self.error(sp, "unexpected element, you silly bumpkin");
+            return self.error("unexpected element, you silly bumpkin");
         }
         render.element_open_start(name);
         self.attrs(render)?;
         render.element_open_end();
-        if let [slash!(), ..] = *self.input {
-            self.shift(1);
-        } else {
-            self.markup(render)?;
-            render.element_close(name);
+        match self.peek() {
+            Some(TokenNode::Op('/', _)) => {
+                // Void element
+                self.advance();
+            },
+            _ => {
+                self.markup(render)?;
+                render.element_close(name);
+            },
         }
         Ok(())
     }
 
     /// Parses and renders the attributes of an element.
     fn attrs(&mut self, render: &mut Renderer) -> ParseResult<()> {
-        let mut classes_static = Vec::new();
-        let mut classes_toggled = Vec::new();
-        let mut ids = Vec::new();
+        let mut classes_static: Vec<String> = Vec::new();
+        let mut classes_toggled: Vec<(TokenStream, String)> = Vec::new();
+        let mut ids: Vec<String> = Vec::new();
         loop {
-            let old_input = self.input;
+            let start_position = self.input.save();
             let maybe_name = self.namespaced_name();
-            match (maybe_name, self.input) {
-                (Ok(name), &[eq!(), ..]) => {
-                    // Non-empty attribute
-                    self.shift(1);
+            let token_after = self.next();
+            match (maybe_name, token_after) {
+                // Non-empty attribute
+                (Ok(name), Some(TokenNode::Op('=', _))) => {
                     render.attribute_start(&name);
                     {
                         // Parse a value under an attribute context
-                        let mut in_attr = true;
-                        mem::swap(&mut self.in_attr, &mut in_attr);
+                        let in_attr = mem::replace(&mut self.in_attr, true);
                         self.markup(render)?;
-                        mem::swap(&mut self.in_attr, &mut in_attr);
+                        self.in_attr = in_attr;
                     }
                     render.attribute_end();
                 },
-                (Ok(name), &[question!(), ..]) => {
-                    // Empty attribute
-                    self.shift(1);
-                    match *self.input {
-                        [TokenTree::Delimited(_, ref d), ..] if d.delim == DelimToken::Bracket => {
-                            // Toggle the attribute based on a boolean expression
-                            self.shift(1);
-                            let cond = d.stream();
-                            let body = {
-                                let mut render = render.fork();
-                                render.attribute_empty(&name);
-                                render.into_stmts()
-                            };
-                            render.emit_if(cond, body, None);
-                        },
-                        _ => {
-                            // Write the attribute unconditionally
+                // Empty attribute
+                (Ok(name), Some(TokenNode::Op('?', _))) => match self.peek() {
+                    // Toggle the attribute based on a boolean expression
+                    Some(TokenNode::Group(Delimiter::Bracket, cond)) => {
+                        self.advance();
+                        let body = {
+                            let mut render = render.fork();
                             render.attribute_empty(&name);
-                        },
-                    }
+                            render.into_stmts()
+                        };
+                        render.emit_if(cond, body, None);
+                    },
+                    // Write the attribute unconditionally
+                    _ => render.attribute_empty(&name),
                 },
-                (Err(_), &[dot!(), ident!(_, _), ..]) => {
-                    // Class shorthand
-                    self.shift(1);
-                    let class_name = self.name().unwrap();
-                    match *self.input {
-                        [TokenTree::Delimited(_, ref d), ..] if d.delim == DelimToken::Bracket => {
-                            // Toggle the class based on a boolean expression
-                            self.shift(1);
-                            let cond = d.stream();
+                // Class shorthand
+                (Err(_), Some(TokenNode::Op('.', _))) => {
+                    let class_name = self.name()?;
+                    match self.peek() {
+                        // Toggle the class based on a boolean expression
+                        Some(TokenNode::Group(Delimiter::Bracket, cond)) => {
+                            self.advance();
                             classes_toggled.push((cond, class_name));
                         },
                         // Emit the class unconditionally
                         _ => classes_static.push(class_name),
                     }
                 },
-                (Err(_), &[pound!(), ident!(_, _), ..]) => {
-                    // ID shorthand
-                    self.shift(1);
-                    ids.push(self.name().unwrap());
+                // ID shorthand
+                (Err(_), Some(TokenNode::Op('#', _))) => {
+                    ids.push(self.name()?);
                 },
+                // If it's not a valid attribute, backtrack and bail out
                 _ => {
-                    self.input = old_input;
+                    self.input.restore(start_position);
                     break;
                 },
             }
@@ -511,24 +273,23 @@ impl<'cx, 'a, 'i> Parser<'cx, 'a, 'i> {
 
     /// Parses an identifier, without dealing with namespaces.
     fn name(&mut self) -> ParseResult<String> {
-        let mut s = match *self.input {
-            [ident!(_, name), ..] => {
-                self.shift(1);
-                String::from(&name.name.as_str() as &str)
-            },
-            _ => return Err(()),
+        let mut s = if let Some(TokenNode::Term(term)) = self.peek() {
+            self.advance();
+            String::from(term.as_str())
+        } else {
+            return self.error("expected identifier");
         };
         let mut expect_ident = false;
         loop {
-            expect_ident = match *self.input {
-                [minus!(), ..] => {
-                    self.shift(1);
+            expect_ident = match self.peek() {
+                Some(TokenNode::Op('-', _)) => {
+                    self.advance();
                     s.push('-');
                     true
                 },
-                [ident!(_, name), ..] if expect_ident => {
-                    self.shift(1);
-                    s.push_str(&name.name.as_str());
+                Some(TokenNode::Term(term)) if expect_ident => {
+                    self.advance();
+                    s.push_str(term.as_str());
                     false
                 },
                 _ => break,
@@ -541,24 +302,68 @@ impl<'cx, 'a, 'i> Parser<'cx, 'a, 'i> {
     /// if necessary.
     fn namespaced_name(&mut self) -> ParseResult<String> {
         let mut s = self.name()?;
-        if let [colon!(), ident!(_, _), ..] = *self.input {
-            self.shift(1);
+        if let Some(TokenNode::Op(':', _)) = self.peek() {
+            self.advance();
             s.push(':');
-            s.push_str(&self.name().unwrap());
+            s.push_str(&self.name()?);
         }
         Ok(s)
     }
 
     /// Parses the given token tree, returning a vector of statements.
-    fn block(&mut self, sp: Span, tts: TokenStream, render: &mut Renderer) -> ParseResult<TokenStream> {
+    fn block(&mut self, body: TokenStream, render: &mut Renderer) -> ParseResult<TokenStream> {
         let mut render = render.fork();
         let mut parse = Parser {
-            cx: self.cx,
             in_attr: self.in_attr,
-            input: &tts.into_trees().collect::<Vec<_>>(),
-            span: sp,
+            input: Lookahead::new(body),
         };
         parse.markups(&mut render)?;
         Ok(render.into_stmts())
     }
 }
+
+struct Lookahead<T> {
+    buffer: Vec<T>,
+    index: usize,
+}
+
+impl<T> Lookahead<T> {
+    fn new<I: IntoIterator<Item=T>>(items: I) -> Self {
+        Lookahead {
+            buffer: items.into_iter().collect(),
+            index: 0,
+        }
+    }
+
+    fn save(&self) -> Position {
+        Position { index: self.index }
+    }
+
+    fn restore(&mut self, Position { index }: Position) {
+        self.index = index;
+    }
+}
+
+impl<T> Lookahead<T> where T: Clone {
+    fn peek(&mut self) -> Option<T> {
+        let position = self.save();
+        let result = self.next();
+        self.restore(position);
+        result
+    }
+}
+
+impl<T> Iterator for Lookahead<T> where T: Clone {
+    type Item = T;
+    fn next(&mut self) -> Option<T> {
+        let result = self.buffer.get(self.index).cloned();
+        if result.is_some() {
+            self.index += 1;
+        }
+        result
+    }
+}
+
+struct Position {
+    index: usize,
+}
diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs
index 7d296c8..04b5100 100644
--- a/maud_macros/src/render.rs
+++ b/maud_macros/src/render.rs
@@ -1,38 +1,28 @@
-use syntax::ast::Ident;
-use syntax::ext::base::ExtCtxt;
-use syntax::symbol::Symbol;
-use syntax::tokenstream::{TokenStream, TokenTree};
+use proc_macro::{Literal, Term, TokenNode, TokenStream};
+use proc_macro::quote;
+use std::fmt;
 
-use maud::Escaper;
-
-// FIXME(rust-lang/rust#40939):
-// * Use `TokenStreamBuilder` instead of `Vec<TokenStream>`
-// * Use `quote!()` instead of `quote_tokens!()`
-
-pub struct Renderer<'cx, 'a: 'cx> {
-    cx: &'cx ExtCtxt<'a>,
-    writer: Ident,
+pub struct Renderer {
+    output: TokenNode,
     stmts: Vec<TokenStream>,
     tail: String,
 }
 
-impl<'cx, 'a> Renderer<'cx, 'a> {
-    /// Creates a new `Renderer` using the given extension context.
-    pub fn new(cx: &'cx ExtCtxt<'a>) -> Renderer<'cx, 'a> {
-        let writer = Ident::with_empty_ctxt(Symbol::gensym("__maud_output"));
+impl Renderer {
+    /// Creates a new `Renderer`.
+    pub fn new() -> Renderer {
+        let output = TokenNode::Term(Term::intern("__maud_output"));
         Renderer {
-            cx: cx,
-            writer: writer,
+            output: output,
             stmts: Vec::new(),
             tail: String::new(),
         }
     }
 
     /// Creates a new `Renderer` under the same context as `self`.
-    pub fn fork(&self) -> Renderer<'cx, 'a> {
+    pub fn fork(&self) -> Renderer {
         Renderer {
-            cx: self.cx,
-            writer: self.writer,
+            output: self.output.clone(),
             stmts: Vec::new(),
             tail: String::new(),
         }
@@ -42,36 +32,38 @@ impl<'cx, 'a> Renderer<'cx, 'a> {
     fn flush(&mut self) {
         if !self.tail.is_empty() {
             let expr = {
-                let w = self.writer;
-                let s = &*self.tail;
-                quote_tokens!(self.cx, $w.push_str($s);)
+                let output = self.output.clone();
+                let string = TokenNode::Literal(Literal::string(&self.tail));
+                quote!($output.push_str($string);)
             };
-            self.stmts.push(expr.into_iter().collect());
+            self.stmts.push(expr);
             self.tail.clear();
         }
     }
 
     /// Reifies the `Renderer` into a block of markup.
     pub fn into_expr(mut self, size_hint: usize) -> TokenStream {
-        let Renderer { cx, writer, stmts, .. } = { self.flush(); self };
-        let stmts: Vec<TokenTree> = TokenStream::concat(stmts).into_trees().collect();
-        quote_tokens!(cx, {
-            let mut $writer = ::std::string::String::with_capacity($size_hint);
+        let Renderer { output, stmts, .. } = { self.flush(); self };
+        let size_hint = TokenNode::Literal(Literal::u64(size_hint as u64));
+        let stmts = stmts.into_iter().collect::<TokenStream>();
+        quote!({
+            extern crate maud;
+            let mut $output = String::with_capacity($size_hint as usize);
             $stmts
-            ::maud::PreEscaped($writer)
-        }).into_iter().collect()
+            maud::PreEscaped($output)
+        })
     }
 
     /// Reifies the `Renderer` into a raw list of statements.
     pub fn into_stmts(mut self) -> TokenStream {
         let Renderer { stmts, .. } = { self.flush(); self };
-        TokenStream::concat(stmts)
+        stmts.into_iter().collect()
     }
 
     /// Pushes a statement, flushing the tail buffer in the process.
-    fn push<T>(&mut self, stmt: T) where T: IntoIterator<Item=TokenTree> {
+    fn push<T>(&mut self, stmt: T) where T: Into<TokenStream> {
         self.flush();
-        self.stmts.push(stmt.into_iter().collect())
+        self.stmts.push(stmt.into())
     }
 
     /// Pushes a literal string to the tail buffer.
@@ -86,12 +78,17 @@ impl<'cx, 'a> Renderer<'cx, 'a> {
 
     /// Appends the result of an expression.
     pub fn splice(&mut self, expr: TokenStream) {
-        let w = self.writer;
-        let expr: Vec<TokenTree> = expr.into_trees().collect();
-        self.push(quote_tokens!(self.cx, {
-            #[allow(unused_imports)]
-            use ::maud::Render as __maud_Render;
-            $expr.render_to(&mut $w);
+        let output = self.output.clone();
+        self.push(quote!({
+            extern crate maud;
+            // Create a local trait alias so that autoref works
+            trait Render: maud::Render {
+                fn render_to(&self, output: &mut String) {
+                    maud::Render::render_to(self, output);
+                }
+            }
+            impl<T: maud::Render> Render for T {}
+            $expr.render_to(&mut $output);
         }));
     }
 
@@ -131,14 +128,9 @@ impl<'cx, 'a> Renderer<'cx, 'a> {
     /// need to special-case `if let`.
     pub fn emit_if(&mut self, if_cond: TokenStream, if_body: TokenStream,
                    else_body: Option<TokenStream>) {
-        let if_cond: Vec<TokenTree> = if_cond.into_trees().collect();
-        let if_body: Vec<TokenTree> = if_body.into_trees().collect();
         let stmt = match else_body {
-            None => quote_tokens!(self.cx, if $if_cond { $if_body }),
-            Some(else_body) => {
-                let else_body: Vec<TokenTree> = else_body.into_trees().collect();
-                quote_tokens!(self.cx, if $if_cond { $if_body } else { $else_body })
-            },
+            None => quote!(if $if_cond { $if_body }),
+            Some(else_body) => quote!(if $if_cond { $if_body } else { $else_body }),
         };
         self.push(stmt);
     }
@@ -148,32 +140,22 @@ impl<'cx, 'a> Renderer<'cx, 'a> {
     /// The condition is a token tree (not an expression) so we don't
     /// need to special-case `while let`.
     pub fn emit_while(&mut self, cond: TokenStream, body: TokenStream) {
-        let cond: Vec<TokenTree> = cond.into_trees().collect();
-        let body: Vec<TokenTree> = body.into_trees().collect();
-        let stmt = quote_tokens!(self.cx, while $cond { $body });
+        let stmt = quote!(while $cond { $body });
         self.push(stmt);
     }
 
     pub fn emit_for(&mut self, pattern: TokenStream, iterable: TokenStream, body: TokenStream) {
-        let pattern: Vec<TokenTree> = pattern.into_trees().collect();
-        let iterable: Vec<TokenTree> = iterable.into_trees().collect();
-        let body: Vec<TokenTree> = body.into_trees().collect();
-        let stmt = quote_tokens!(self.cx, for $pattern in $iterable { $body });
+        let stmt = quote!(for $pattern in $iterable { $body });
         self.push(stmt);
     }
 
     pub fn emit_match(&mut self, match_var: TokenStream, match_body: TokenStream) {
-        let match_var: Vec<TokenTree> = match_var.into_trees().collect();
-        let match_body: Vec<TokenTree> = match_body.into_trees().collect();
-        let stmt = quote_tokens!(self.cx, match $match_var { $match_body });
+        let stmt = quote!(match $match_var { $match_body });
         self.push(stmt);
     }
 
     pub fn emit_let(&mut self, pattern: TokenStream, rhs: TokenStream, body: TokenStream) {
-        let pattern: Vec<TokenTree> = pattern.into_trees().collect();
-        let rhs: Vec<TokenTree> = rhs.into_trees().collect();
-        let body: Vec<TokenTree> = body.into_trees().collect();
-        let stmt = quote_tokens!(self.cx, { let $pattern = $rhs; $body });
+        let stmt = quote!({ let $pattern = $rhs; $body });
         self.push(stmt);
     }
 }
@@ -184,3 +166,28 @@ fn html_escape(s: &str) -> String {
     Escaper::new(&mut buffer).write_str(s).unwrap();
     buffer
 }
+
+// TODO move this into a common `maud_htmlescape` crate
+struct Escaper<'a>(&'a mut String);
+
+impl<'a> Escaper<'a> {
+    /// Creates an `Escaper` from a `String`.
+    pub fn new(buffer: &'a mut String) -> Escaper<'a> {
+        Escaper(buffer)
+    }
+}
+
+impl<'a> fmt::Write for Escaper<'a> {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        for b in s.bytes() {
+            match b {
+                b'&' => self.0.push_str("&amp;"),
+                b'<' => self.0.push_str("&lt;"),
+                b'>' => self.0.push_str("&gt;"),
+                b'"' => self.0.push_str("&quot;"),
+                _ => unsafe { self.0.as_mut_vec().push(b) },
+            }
+        }
+        Ok(())
+    }
+}

From 6a593388ae63faa37b72920b9a4c85612fc64794 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sat, 8 Jul 2017 20:27:14 +1200
Subject: [PATCH 02/17] Fix maud_extras

---
 maud_extras/Cargo.toml |  1 -
 maud_extras/lib.rs     | 29 ++++++++++++++---------------
 2 files changed, 14 insertions(+), 16 deletions(-)

diff --git a/maud_extras/Cargo.toml b/maud_extras/Cargo.toml
index 01f883b..a733309 100644
--- a/maud_extras/Cargo.toml
+++ b/maud_extras/Cargo.toml
@@ -10,7 +10,6 @@ description = "Extra bits and pieces for use in Maud templates."
 
 [dependencies]
 maud = { path = "../maud", features = ["iron"] }
-maud_macros = { path = "../maud_macros" }
 
 [lib]
 path = "lib.rs"
diff --git a/maud_extras/lib.rs b/maud_extras/lib.rs
index 1f2aec7..2756d68 100644
--- a/maud_extras/lib.rs
+++ b/maud_extras/lib.rs
@@ -1,19 +1,18 @@
-#![feature(plugin)]
-#![plugin(maud_macros)]
+#![feature(proc_macro)]
 
 extern crate maud;
 
-use maud::{Markup, Render};
+use maud::{Markup, Render, html};
 
 /// Links to an external stylesheet.
 ///
 /// # Example
 ///
 /// ```rust
-/// # #![feature(plugin)]
-/// # #![plugin(maud_macros)]
+/// # #![feature(proc_macro)]
 /// # extern crate maud;
 /// # extern crate maud_extras;
+/// # use maud::html;
 /// # use maud_extras::*;
 /// # fn main() {
 /// let markup = html! { (Css("styles.css")) };
@@ -36,10 +35,10 @@ impl<T: AsRef<str>> Render for Css<T> {
 /// # Example
 ///
 /// ```rust
-/// # #![feature(plugin)]
-/// # #![plugin(maud_macros)]
+/// # #![feature(proc_macro)]
 /// # extern crate maud;
 /// # extern crate maud_extras;
+/// # use maud::html;
 /// # use maud_extras::*;
 /// # fn main() {
 /// let markup = html! { (Js("app.js")) };
@@ -62,10 +61,10 @@ impl<T: AsRef<str>> Render for Js<T> {
 /// # Example
 ///
 /// ```rust
-/// # #![feature(plugin)]
-/// # #![plugin(maud_macros)]
+/// # #![feature(proc_macro)]
 /// # extern crate maud;
 /// # extern crate maud_extras;
+/// # use maud::html;
 /// # use maud_extras::*;
 /// # fn main() {
 /// let markup = html! { (Meta("description", "test description")) };
@@ -88,10 +87,10 @@ impl<T: AsRef<str>, U: AsRef<str>> Render for Meta<T, U> {
 /// # Example
 ///
 /// ```rust
-/// # #![feature(plugin)]
-/// # #![plugin(maud_macros)]
+/// # #![feature(proc_macro)]
 /// # extern crate maud;
 /// # extern crate maud_extras;
+/// # use maud::html;
 /// # use maud_extras::*;
 /// # fn main() {
 /// let markup = html! { (Title("Maud")) };
@@ -114,10 +113,10 @@ impl<T: AsRef<str>> Render for Title<T> {
 /// # Example
 ///
 /// ```rust
-/// # #![feature(plugin)]
-/// # #![plugin(maud_macros)]
+/// # #![feature(proc_macro)]
 /// # extern crate maud;
 /// # extern crate maud_extras;
+/// # use maud::html;
 /// # use maud_extras::*;
 /// # fn main() {
 /// let markup = html! { (Charset("utf-8")) };
@@ -140,10 +139,10 @@ impl<T: AsRef<str>> Render for Charset<T> {
 /// # Example
 ///
 /// ```rust
-/// # #![feature(plugin)]
-/// # #![plugin(maud_macros)]
+/// # #![feature(proc_macro)]
 /// # extern crate maud;
 /// # extern crate maud_extras;
+/// # use maud::html;
 /// # use maud_extras::*;
 /// # fn main() {
 /// let markup = html! { (MetaProperty("og:description", "test description")) };

From e61385645e27682be9cc4acc9cfde5be32d31d45 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sun, 16 Jul 2017 22:11:10 +1200
Subject: [PATCH 03/17] Don't throw away spans

---
 maud_macros/src/parse.rs | 44 ++++++++++++++++++++--------------------
 1 file changed, 22 insertions(+), 22 deletions(-)

diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 362f066..f00f8fb 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -30,12 +30,12 @@ struct Parser {
 }
 
 impl Parser {
-    fn next(&mut self) -> Option<TokenNode> {
-        self.input.next().map(|tt| tt.kind)
+    fn next(&mut self) -> Option<TokenTree> {
+        self.input.next()
     }
 
-    fn peek(&mut self) -> Option<TokenNode> {
-        self.input.peek().map(|tt| tt.kind)
+    fn peek(&mut self) -> Option<TokenTree> {
+        self.input.peek()
     }
 
     fn advance(&mut self) {
@@ -52,7 +52,7 @@ impl Parser {
         loop {
             match self.peek() {
                 None => return Ok(()),
-                Some(TokenNode::Op(';', _)) => self.advance(),
+                Some(TokenTree { kind: TokenNode::Op(';', _), .. }) => self.advance(),
                 _ => self.markup(render)?,
             }
         }
@@ -66,15 +66,15 @@ impl Parser {
         };
         match token {
             // Literal
-            TokenNode::Literal(lit) => {
+            TokenTree { kind: TokenNode::Literal(lit), .. } => {
                 self.advance();
                 self.literal(lit, render)?;
             },
             // Special form
-            TokenNode::Op('@', _) => {
+            TokenTree { kind: TokenNode::Op('@', _), .. } => {
                 self.advance();
                 match self.next() {
-                    Some(TokenNode::Term(term)) => match term.as_str() {
+                    Some(TokenTree { kind: TokenNode::Term(term), .. }) => match term.as_str() {
                         "if" => self.if_expr(render)?,
                         "while" => self.while_expr(render)?,
                         "for" => self.for_expr(render)?,
@@ -86,17 +86,17 @@ impl Parser {
                 }
             }
             // Element
-            TokenNode::Term(_) => {
+            TokenTree { kind: TokenNode::Term(_), .. } => {
                 let name = self.namespaced_name()?;
                 self.element(&name, render)?;
             },
             // Splice
-            TokenNode::Group(Delimiter::Parenthesis, expr) => {
+            TokenTree { kind: TokenNode::Group(Delimiter::Parenthesis, expr), .. } => {
                 self.advance();
                 render.splice(expr);
             }
             // Block
-            TokenNode::Group(Delimiter::Brace, block) => {
+            TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. } => {
                 self.advance();
                 Parser {
                     in_attr: self.in_attr,
@@ -173,7 +173,7 @@ impl Parser {
         self.attrs(render)?;
         render.element_open_end();
         match self.peek() {
-            Some(TokenNode::Op('/', _)) => {
+            Some(TokenTree { kind: TokenNode::Op('/', _), .. }) => {
                 // Void element
                 self.advance();
             },
@@ -196,7 +196,7 @@ impl Parser {
             let token_after = self.next();
             match (maybe_name, token_after) {
                 // Non-empty attribute
-                (Ok(name), Some(TokenNode::Op('=', _))) => {
+                (Ok(name), Some(TokenTree { kind: TokenNode::Op('=', _), .. })) => {
                     render.attribute_start(&name);
                     {
                         // Parse a value under an attribute context
@@ -207,9 +207,9 @@ impl Parser {
                     render.attribute_end();
                 },
                 // Empty attribute
-                (Ok(name), Some(TokenNode::Op('?', _))) => match self.peek() {
+                (Ok(name), Some(TokenTree { kind: TokenNode::Op('?', _), .. })) => match self.peek() {
                     // Toggle the attribute based on a boolean expression
-                    Some(TokenNode::Group(Delimiter::Bracket, cond)) => {
+                    Some(TokenTree { kind: TokenNode::Group(Delimiter::Bracket, cond), .. }) => {
                         self.advance();
                         let body = {
                             let mut render = render.fork();
@@ -222,11 +222,11 @@ impl Parser {
                     _ => render.attribute_empty(&name),
                 },
                 // Class shorthand
-                (Err(_), Some(TokenNode::Op('.', _))) => {
+                (Err(_), Some(TokenTree { kind: TokenNode::Op('.', _), .. })) => {
                     let class_name = self.name()?;
                     match self.peek() {
                         // Toggle the class based on a boolean expression
-                        Some(TokenNode::Group(Delimiter::Bracket, cond)) => {
+                        Some(TokenTree { kind: TokenNode::Group(Delimiter::Bracket, cond), .. }) => {
                             self.advance();
                             classes_toggled.push((cond, class_name));
                         },
@@ -235,7 +235,7 @@ impl Parser {
                     }
                 },
                 // ID shorthand
-                (Err(_), Some(TokenNode::Op('#', _))) => {
+                (Err(_), Some(TokenTree { kind: TokenNode::Op('#', _), .. })) => {
                     ids.push(self.name()?);
                 },
                 // If it's not a valid attribute, backtrack and bail out
@@ -273,7 +273,7 @@ impl Parser {
 
     /// Parses an identifier, without dealing with namespaces.
     fn name(&mut self) -> ParseResult<String> {
-        let mut s = if let Some(TokenNode::Term(term)) = self.peek() {
+        let mut s = if let Some(TokenTree { kind: TokenNode::Term(term), .. }) = self.peek() {
             self.advance();
             String::from(term.as_str())
         } else {
@@ -282,12 +282,12 @@ impl Parser {
         let mut expect_ident = false;
         loop {
             expect_ident = match self.peek() {
-                Some(TokenNode::Op('-', _)) => {
+                Some(TokenTree { kind: TokenNode::Op('-', _), .. }) => {
                     self.advance();
                     s.push('-');
                     true
                 },
-                Some(TokenNode::Term(term)) if expect_ident => {
+                Some(TokenTree { kind: TokenNode::Term(term), .. }) if expect_ident => {
                     self.advance();
                     s.push_str(term.as_str());
                     false
@@ -302,7 +302,7 @@ impl Parser {
     /// if necessary.
     fn namespaced_name(&mut self) -> ParseResult<String> {
         let mut s = self.name()?;
-        if let Some(TokenNode::Op(':', _)) = self.peek() {
+        if let Some(TokenTree { kind: TokenNode::Op(':', _), .. }) = self.peek() {
             self.advance();
             s.push(':');
             s.push_str(&self.name()?);

From 3d1e82b20ea784c2abb1440e73d53ddb58c7ea5d Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sun, 16 Jul 2017 22:13:26 +1200
Subject: [PATCH 04/17] Implement let expressions

---
 maud_macros/src/parse.rs | 29 +++++++++++++++++++++++++----
 1 file changed, 25 insertions(+), 4 deletions(-)

diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index f00f8fb..f2aa746 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -159,7 +159,28 @@ impl Parser {
     ///
     /// The leading `@let` should already be consumed.
     fn let_expr(&mut self, render: &mut Renderer) -> ParseResult<()> {
-        self.error("unimplemented")
+        let mut pat = Vec::new();
+        loop {
+            match self.next() {
+                Some(TokenTree { kind: TokenNode::Op('=', _), .. }) => break,
+                Some(token) => pat.push(token),
+                None => return self.error("unexpected end of @let expression"),
+            }
+        }
+        let mut expr = Vec::new();
+        let body;
+        loop {
+            match self.next() {
+                Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. }) => {
+                    body = self.block(block, render)?;
+                    break;
+                },
+                Some(token) => expr.push(token),
+                None => return self.error("unexpected end of @let expression"),
+            }
+        }
+        render.emit_let(pat.into_iter().collect(), expr.into_iter().collect(), body);
+        Ok(())
     }
 
     /// Parses and renders an element node.
@@ -187,9 +208,9 @@ impl Parser {
 
     /// Parses and renders the attributes of an element.
     fn attrs(&mut self, render: &mut Renderer) -> ParseResult<()> {
-        let mut classes_static: Vec<String> = Vec::new();
-        let mut classes_toggled: Vec<(TokenStream, String)> = Vec::new();
-        let mut ids: Vec<String> = Vec::new();
+        let mut classes_static = Vec::new();
+        let mut classes_toggled = Vec::new();
+        let mut ids = Vec::new();
         loop {
             let start_position = self.input.save();
             let maybe_name = self.namespaced_name();

From 88c8a7e8c988c6f38b33c689c008e5805819ea85 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Mon, 17 Jul 2017 21:13:01 +1200
Subject: [PATCH 05/17] Revert "Silence `collapsible_if` warnings (#93)"

This reverts commit a72877cb5524af7d8e56d0ecb70de13980503639.
---
 .travis.yml | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 9f9b00f..e190a34 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,7 +15,6 @@ script:
   - ( cd maud && cargo test --all-features )
   - if command -v cargo-clippy > /dev/null; then ( cd maud && cargo clippy -- -D warnings ); fi
   - ( cd maud_macros && cargo test --all-features )
-  # Silence `collapsible_if` warnings for now -- see https://github.com/lfairy/maud/issues/93
-  - if command -v cargo-clippy > /dev/null; then ( cd maud_macros && cargo clippy -- -D warnings -A collapsible_if ); fi
+  - if command -v cargo-clippy > /dev/null; then ( cd maud_macros && cargo clippy -- -D warnings ); fi
   - ( cd maud_extras && cargo test --all-features )
   - if command -v cargo-clippy > /dev/null; then ( cd maud_extras && cargo clippy -- -D warnings ); fi

From 748ce69f4d3bad2b58b59e866aff041722d9d21c Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sun, 23 Jul 2017 13:27:18 +1200
Subject: [PATCH 06/17] Use loop-break-value

It's stable now!
---
 maud_macros/src/parse.rs | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index f2aa746..9130fc8 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -168,17 +168,15 @@ impl Parser {
             }
         }
         let mut expr = Vec::new();
-        let body;
-        loop {
+        let body = loop {
             match self.next() {
                 Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. }) => {
-                    body = self.block(block, render)?;
-                    break;
+                    break self.block(block, render)?;
                 },
                 Some(token) => expr.push(token),
                 None => return self.error("unexpected end of @let expression"),
             }
-        }
+        };
         render.emit_let(pat.into_iter().collect(), expr.into_iter().collect(), body);
         Ok(())
     }
@@ -343,6 +341,7 @@ impl Parser {
     }
 }
 
+// TODO use rust-lang/rust#43280 instead
 struct Lookahead<T> {
     buffer: Vec<T>,
     index: usize,

From af57ac41ce9fc9595c902c452d069c9d49960e5a Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sun, 23 Jul 2017 13:49:02 +1200
Subject: [PATCH 07/17] Refactor backtracking mechanism

---
 maud_macros/src/parse.rs | 119 +++++++++++++++------------------------
 1 file changed, 45 insertions(+), 74 deletions(-)

diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 9130fc8..91be5cc 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -10,12 +10,14 @@ pub fn parse(input: TokenStream) -> ParseResult<TokenStream> {
     let mut render = Renderer::new();
     let _ = Parser {
         in_attr: false,
-        input: Lookahead::new(input.clone()),
+        input: input.clone().into_iter().collect(),
+        index: 0,
     }.markups(&mut render);
     /*
     Parser {
         in_attr: false,
-        input: Lookahead::new(input.clone()),
+        input: input.clone().into_iter().collect(),
+        index: 0,
     }.markups(&mut render)?;
     */
     // Heuristic: the size of the resulting markup tends to correlate with the
@@ -24,24 +26,35 @@ pub fn parse(input: TokenStream) -> ParseResult<TokenStream> {
     Ok(render.into_expr(size_hint))
 }
 
+#[derive(Clone)]
 struct Parser {
     in_attr: bool,
-    input: Lookahead<TokenTree>,
+    // FIXME(rust-lang/rust#43280) use TokenTreeIter instead of tracking indices manually
+    input: Vec<TokenTree>,
+    index: usize,
 }
 
 impl Parser {
     fn next(&mut self) -> Option<TokenTree> {
-        self.input.next()
+        let result = self.input.get(self.index).cloned();
+        if result.is_some() {
+            self.index += 1;
+        }
+        result
     }
 
     fn peek(&mut self) -> Option<TokenTree> {
-        self.input.peek()
+        self.input.get(self.index).cloned()
     }
 
     fn advance(&mut self) {
         self.next();
     }
 
+    fn commit(&mut self, attempt: Parser) {
+        *self = attempt;
+    }
+
     /// Attaches an error message to the span and returns `Err`.
     fn error<T, E: Into<String>>(&self, message: E) -> ParseResult<T> {
         Err(message.into())
@@ -100,7 +113,8 @@ impl Parser {
                 self.advance();
                 Parser {
                     in_attr: self.in_attr,
-                    input: Lookahead::new(block),
+                    input: block.into_iter().collect(),
+                    index: 0,
                 }.markups(render)?;
             },
             // ???
@@ -210,12 +224,13 @@ impl Parser {
         let mut classes_toggled = Vec::new();
         let mut ids = Vec::new();
         loop {
-            let start_position = self.input.save();
-            let maybe_name = self.namespaced_name();
-            let token_after = self.next();
+            let mut attempt = self.clone();
+            let maybe_name = attempt.namespaced_name();
+            let token_after = attempt.next();
             match (maybe_name, token_after) {
                 // Non-empty attribute
                 (Ok(name), Some(TokenTree { kind: TokenNode::Op('=', _), .. })) => {
+                    self.commit(attempt);
                     render.attribute_start(&name);
                     {
                         // Parse a value under an attribute context
@@ -226,22 +241,26 @@ impl Parser {
                     render.attribute_end();
                 },
                 // Empty attribute
-                (Ok(name), Some(TokenTree { kind: TokenNode::Op('?', _), .. })) => match self.peek() {
-                    // Toggle the attribute based on a boolean expression
-                    Some(TokenTree { kind: TokenNode::Group(Delimiter::Bracket, cond), .. }) => {
-                        self.advance();
-                        let body = {
-                            let mut render = render.fork();
-                            render.attribute_empty(&name);
-                            render.into_stmts()
-                        };
-                        render.emit_if(cond, body, None);
-                    },
-                    // Write the attribute unconditionally
-                    _ => render.attribute_empty(&name),
+                (Ok(name), Some(TokenTree { kind: TokenNode::Op('?', _), .. })) => {
+                    self.commit(attempt);
+                    match self.peek() {
+                        // Toggle the attribute based on a boolean expression
+                        Some(TokenTree { kind: TokenNode::Group(Delimiter::Bracket, cond), .. }) => {
+                            self.advance();
+                            let body = {
+                                let mut render = render.fork();
+                                render.attribute_empty(&name);
+                                render.into_stmts()
+                            };
+                            render.emit_if(cond, body, None);
+                        },
+                        // Write the attribute unconditionally
+                        _ => render.attribute_empty(&name),
+                    }
                 },
                 // Class shorthand
                 (Err(_), Some(TokenTree { kind: TokenNode::Op('.', _), .. })) => {
+                    self.commit(attempt);
                     let class_name = self.name()?;
                     match self.peek() {
                         // Toggle the class based on a boolean expression
@@ -255,13 +274,11 @@ impl Parser {
                 },
                 // ID shorthand
                 (Err(_), Some(TokenTree { kind: TokenNode::Op('#', _), .. })) => {
+                    self.commit(attempt);
                     ids.push(self.name()?);
                 },
                 // If it's not a valid attribute, backtrack and bail out
-                _ => {
-                    self.input.restore(start_position);
-                    break;
-                },
+                _ => break,
             }
         }
         if !classes_static.is_empty() || !classes_toggled.is_empty() {
@@ -334,56 +351,10 @@ impl Parser {
         let mut render = render.fork();
         let mut parse = Parser {
             in_attr: self.in_attr,
-            input: Lookahead::new(body),
+            input: body.into_iter().collect(),
+            index: 0,
         };
         parse.markups(&mut render)?;
         Ok(render.into_stmts())
     }
 }
-
-// TODO use rust-lang/rust#43280 instead
-struct Lookahead<T> {
-    buffer: Vec<T>,
-    index: usize,
-}
-
-impl<T> Lookahead<T> {
-    fn new<I: IntoIterator<Item=T>>(items: I) -> Self {
-        Lookahead {
-            buffer: items.into_iter().collect(),
-            index: 0,
-        }
-    }
-
-    fn save(&self) -> Position {
-        Position { index: self.index }
-    }
-
-    fn restore(&mut self, Position { index }: Position) {
-        self.index = index;
-    }
-}
-
-impl<T> Lookahead<T> where T: Clone {
-    fn peek(&mut self) -> Option<T> {
-        let position = self.save();
-        let result = self.next();
-        self.restore(position);
-        result
-    }
-}
-
-impl<T> Iterator for Lookahead<T> where T: Clone {
-    type Item = T;
-    fn next(&mut self) -> Option<T> {
-        let result = self.buffer.get(self.index).cloned();
-        if result.is_some() {
-            self.index += 1;
-        }
-        result
-    }
-}
-
-struct Position {
-    index: usize,
-}

From 798f9baee889d3f0a4de5539a0a8e6c994adeb38 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sun, 23 Jul 2017 13:56:09 +1200
Subject: [PATCH 08/17] Implement if expressions

---
 maud_macros/src/parse.rs | 46 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 45 insertions(+), 1 deletion(-)

diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 91be5cc..3bb009b 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -137,7 +137,51 @@ impl Parser {
     ///
     /// The leading `@if` should already be consumed.
     fn if_expr(&mut self, render: &mut Renderer) -> ParseResult<()> {
-        self.error("unimplemented")
+        let mut if_cond = Vec::new();
+        let if_body = loop {
+            match self.next() {
+                Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. }) => {
+                    break self.block(block, render)?;
+                },
+                Some(token) => if_cond.push(token),
+                None => return self.error("unexpected end of @if expression"),
+            }
+        };
+        let mut attempt = self.clone();
+        let else_body = if_chain! {
+            // Try to match an `@else` after this
+            if let Some(TokenTree { kind: TokenNode::Op('@', _), .. }) = attempt.next();
+            if let Some(TokenTree { kind: TokenNode::Term(else_keyword), .. }) = attempt.next();
+            if else_keyword.as_str() == "else";
+            then {
+                self.commit(attempt);
+                if_chain! {
+                    // `@else if`
+                    if let Some(TokenTree { kind: TokenNode::Term(if_keyword), .. }) = self.peek();
+                    if if_keyword.as_str() == "if";
+                    then {
+                        self.advance();
+                        let mut render = render.fork();
+                        self.if_expr(&mut render)?;
+                        Some(render.into_stmts())
+                    }
+                    // Just an `@else`
+                    else {
+                        if let Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. }) = self.next() {
+                            Some(self.block(block, render)?)
+                        } else {
+                            return self.error("expected body for @else");
+                        }
+                    }
+                }
+            }
+            else {
+                // We didn't find an `@else`; backtrack
+                None
+            }
+        };
+        render.emit_if(if_cond.into_iter().collect(), if_body, else_body);
+        Ok(())
     }
 
     /// Parses and renders an `@while` expression.

From 62bdd4b06afd7fd7f89d1330eedd223846d3df98 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sun, 23 Jul 2017 14:00:49 +1200
Subject: [PATCH 09/17] Use literalext from crates.io

---
 maud_macros/Cargo.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/maud_macros/Cargo.toml b/maud_macros/Cargo.toml
index 6c43639..37e2379 100644
--- a/maud_macros/Cargo.toml
+++ b/maud_macros/Cargo.toml
@@ -13,7 +13,7 @@ description = "Compile-time HTML templates."
 
 [dependencies]
 if_chain = "0.1"
-literalext = { git = "https://github.com/mystor/literalext.git", default-features = false, features = ["proc-macro"] }
+literalext = { version = "0.1", default-features = false, features = ["proc-macro"] }
 
 [lib]
 name = "maud_macros"

From cd113be71b8f832d360145227bc8c9b35e47d57a Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sun, 23 Jul 2017 14:03:23 +1200
Subject: [PATCH 10/17] Implement while expressions

---
 maud_macros/src/parse.rs | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 3bb009b..4c53292 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -188,7 +188,18 @@ impl Parser {
     ///
     /// The leading `@while` should already be consumed.
     fn while_expr(&mut self, render: &mut Renderer) -> ParseResult<()> {
-        self.error("unimplemented")
+        let mut cond = Vec::new();
+        let body = loop {
+            match self.next() {
+                Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. }) => {
+                    break self.block(block, render)?;
+                },
+                Some(token) => cond.push(token),
+                None => return self.error("unexpected end of @while expression"),
+            }
+        };
+        render.emit_while(cond.into_iter().collect(), body);
+        Ok(())
     }
 
     /// Parses and renders a `@for` expression.

From 513d7f3d73b8820843ce888c25dfcb9a51a25632 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sun, 23 Jul 2017 14:08:19 +1200
Subject: [PATCH 11/17] Implement for expressions

---
 maud_macros/src/parse.rs | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 4c53292..5b6acde 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -206,7 +206,26 @@ impl Parser {
     ///
     /// The leading `@for` should already be consumed.
     fn for_expr(&mut self, render: &mut Renderer) -> ParseResult<()> {
-        self.error("unimplemented")
+        let mut pat = Vec::new();
+        loop {
+            match self.next() {
+                Some(TokenTree { kind: TokenNode::Term(in_keyword), .. }) if in_keyword.as_str() == "in" => break,
+                Some(token) => pat.push(token),
+                None => return self.error("unexpected end of @for expression"),
+            }
+        }
+        let mut expr = Vec::new();
+        let body = loop {
+            match self.next() {
+                Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. }) => {
+                    break self.block(block, render)?;
+                },
+                Some(token) => expr.push(token),
+                None => return self.error("unexpected end of @for expression"),
+            }
+        };
+        render.emit_for(pat.into_iter().collect(), expr.into_iter().collect(), body);
+        Ok(())
     }
 
     /// Parses and renders a `@match` expression.

From fd7e000cda1a900e7d96b2a05d13f3d5538b4edb Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sat, 29 Jul 2017 13:24:40 +1200
Subject: [PATCH 12/17] TokenTreeIter values can be cloned now

---
 maud_macros/Cargo.toml   |  1 -
 maud_macros/src/lib.rs   |  2 --
 maud_macros/src/parse.rs | 66 +++++++++++++++++++---------------------
 3 files changed, 32 insertions(+), 37 deletions(-)

diff --git a/maud_macros/Cargo.toml b/maud_macros/Cargo.toml
index 37e2379..5765624 100644
--- a/maud_macros/Cargo.toml
+++ b/maud_macros/Cargo.toml
@@ -12,7 +12,6 @@ repository = "https://github.com/lfairy/maud"
 description = "Compile-time HTML templates."
 
 [dependencies]
-if_chain = "0.1"
 literalext = { version = "0.1", default-features = false, features = ["proc-macro"] }
 
 [lib]
diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs
index dcc70c0..cbdbb18 100644
--- a/maud_macros/src/lib.rs
+++ b/maud_macros/src/lib.rs
@@ -3,8 +3,6 @@
 
 #![doc(html_root_url = "https://docs.rs/maud_macros/0.16.3")]
 
-#[macro_use]
-extern crate if_chain;
 extern crate literalext;
 extern crate proc_macro;
 
diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 5b6acde..1f7066a 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -10,14 +10,12 @@ pub fn parse(input: TokenStream) -> ParseResult<TokenStream> {
     let mut render = Renderer::new();
     let _ = Parser {
         in_attr: false,
-        input: input.clone().into_iter().collect(),
-        index: 0,
+        input: input.clone().into_iter(),
     }.markups(&mut render);
     /*
     Parser {
         in_attr: false,
-        input: input.clone().into_iter().collect(),
-        index: 0,
+        input: input.clone().into_iter(),
     }.markups(&mut render)?;
     */
     // Heuristic: the size of the resulting markup tends to correlate with the
@@ -29,28 +27,32 @@ pub fn parse(input: TokenStream) -> ParseResult<TokenStream> {
 #[derive(Clone)]
 struct Parser {
     in_attr: bool,
-    // FIXME(rust-lang/rust#43280) use TokenTreeIter instead of tracking indices manually
-    input: Vec<TokenTree>,
-    index: usize,
+    input: TokenTreeIter,
 }
 
 impl Parser {
     fn next(&mut self) -> Option<TokenTree> {
-        let result = self.input.get(self.index).cloned();
-        if result.is_some() {
-            self.index += 1;
-        }
-        result
+        self.input.next()
     }
 
     fn peek(&mut self) -> Option<TokenTree> {
-        self.input.get(self.index).cloned()
+        self.clone().next()
+    }
+
+    fn peek2(&mut self) -> Option<(TokenTree, Option<TokenTree>)> {
+        let mut clone = self.clone();
+        clone.next().map(|first| (first, clone.next()))
     }
 
     fn advance(&mut self) {
         self.next();
     }
 
+    fn advance2(&mut self) {
+        self.next();
+        self.next();
+    }
+
     fn commit(&mut self, attempt: Parser) {
         *self = attempt;
     }
@@ -113,8 +115,7 @@ impl Parser {
                 self.advance();
                 Parser {
                     in_attr: self.in_attr,
-                    input: block.into_iter().collect(),
-                    index: 0,
+                    input: block.into_iter(),
                 }.markups(render)?;
             },
             // ???
@@ -147,38 +148,36 @@ impl Parser {
                 None => return self.error("unexpected end of @if expression"),
             }
         };
-        let mut attempt = self.clone();
-        let else_body = if_chain! {
+        let else_body = match self.peek2() {
             // Try to match an `@else` after this
-            if let Some(TokenTree { kind: TokenNode::Op('@', _), .. }) = attempt.next();
-            if let Some(TokenTree { kind: TokenNode::Term(else_keyword), .. }) = attempt.next();
-            if else_keyword.as_str() == "else";
-            then {
-                self.commit(attempt);
-                if_chain! {
+            Some((
+                TokenTree { kind: TokenNode::Op('@', _), .. },
+                Some(TokenTree { kind: TokenNode::Term(else_keyword), .. }),
+            )) if else_keyword.as_str() == "else" => {
+                self.advance2();
+                match self.peek() {
                     // `@else if`
-                    if let Some(TokenTree { kind: TokenNode::Term(if_keyword), .. }) = self.peek();
-                    if if_keyword.as_str() == "if";
-                    then {
+                    Some(TokenTree { kind: TokenNode::Term(if_keyword), .. })
+                    if if_keyword.as_str() == "if" => {
                         self.advance();
                         let mut render = render.fork();
                         self.if_expr(&mut render)?;
                         Some(render.into_stmts())
-                    }
+                    },
                     // Just an `@else`
-                    else {
+                    _ => {
                         if let Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. }) = self.next() {
                             Some(self.block(block, render)?)
                         } else {
                             return self.error("expected body for @else");
                         }
-                    }
+                    },
                 }
-            }
-            else {
+            },
+            _ => {
                 // We didn't find an `@else`; backtrack
                 None
-            }
+            },
         };
         render.emit_if(if_cond.into_iter().collect(), if_body, else_body);
         Ok(())
@@ -425,8 +424,7 @@ impl Parser {
         let mut render = render.fork();
         let mut parse = Parser {
             in_attr: self.in_attr,
-            input: body.into_iter().collect(),
-            index: 0,
+            input: body.into_iter(),
         };
         parse.markups(&mut render)?;
         Ok(render.into_stmts())

From 37419d9781f3257a35025868b9c8ad23d950f9a8 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sat, 29 Jul 2017 14:30:52 +1200
Subject: [PATCH 13/17] Implement match expressions and remove debugging stuff

---
 maud_macros/src/parse.rs | 97 ++++++++++++++++++++++++++++++++++------
 1 file changed, 84 insertions(+), 13 deletions(-)

diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 1f7066a..023de7e 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -1,4 +1,4 @@
-use proc_macro::{Delimiter, Literal, TokenNode, TokenStream, TokenTree, TokenTreeIter};
+use proc_macro::{Delimiter, Literal, Spacing, TokenNode, TokenStream, TokenTree, TokenTreeIter, quote};
 use std::mem;
 
 use literalext::LiteralExt;
@@ -11,13 +11,7 @@ pub fn parse(input: TokenStream) -> ParseResult<TokenStream> {
     let _ = Parser {
         in_attr: false,
         input: input.clone().into_iter(),
-    }.markups(&mut render);
-    /*
-    Parser {
-        in_attr: false,
-        input: input.clone().into_iter(),
     }.markups(&mut render)?;
-    */
     // Heuristic: the size of the resulting markup tends to correlate with the
     // code size of the template itself
     let size_hint = input.to_string().len();
@@ -57,7 +51,7 @@ impl Parser {
         *self = attempt;
     }
 
-    /// Attaches an error message to the span and returns `Err`.
+    /// Returns an `Err` with the given message.
     fn error<T, E: Into<String>>(&self, message: E) -> ParseResult<T> {
         Err(message.into())
     }
@@ -231,15 +225,92 @@ impl Parser {
     ///
     /// The leading `@match` should already be consumed.
     fn match_expr(&mut self, render: &mut Renderer) -> ParseResult<()> {
-        self.error("unimplemented")
+        let mut head = Vec::new();
+        let body = loop {
+            match self.next() {
+                Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, body), .. }) => {
+                    let mut parse = Parser {
+                        in_attr: self.in_attr,
+                        input: body.into_iter(),
+                    };
+                    break parse.match_arms(render)?;
+                },
+                Some(token) => head.push(token),
+                None => return self.error("unexpected end of @match expression"),
+            }
+        };
+        render.emit_match(head.into_iter().collect(), body);
+        Ok(())
     }
 
-    fn match_bodies(&mut self, render: &mut Renderer) -> ParseResult<Vec<TokenTree>> {
-        self.error("unimplemented")
+    fn match_arms(&mut self, render: &mut Renderer) -> ParseResult<TokenStream> {
+        let mut arms = Vec::new();
+        while let Some(arm) = self.match_arm(render)? {
+            arms.push(arm);
+        }
+        Ok(arms.into_iter().collect())
     }
 
-    fn match_body(&mut self, render: &mut Renderer) -> ParseResult<Vec<TokenTree>> {
-        self.error("unimplemented")
+    fn match_arm(&mut self, render: &mut Renderer) -> ParseResult<Option<TokenStream>> {
+        let mut pat = Vec::new();
+        loop {
+            match self.peek2() {
+                Some((
+                    eq @ TokenTree { kind: TokenNode::Op('=', Spacing::Joint), .. },
+                    Some(gt @ TokenTree { kind: TokenNode::Op('>', _), .. }),
+                )) => {
+                    self.advance2();
+                    pat.push(eq);
+                    pat.push(gt);
+                    break;
+                },
+                Some((token, _)) => {
+                    self.advance();
+                    pat.push(token);
+                },
+                None =>
+                    if pat.is_empty() {
+                        return Ok(None);
+                    } else {
+                        return self.error("unexpected end of @match pattern");
+                    },
+            }
+        }
+        let body = match self.next() {
+            // $pat => { $stmts }
+            Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, body), span }) => {
+                let body = self.block(body, render)?;
+                // Trailing commas are optional if the match arm is a braced block
+                if let Some(TokenTree { kind: TokenNode::Op(',', _), .. }) = self.peek() {
+                    self.advance();
+                }
+                // Re-use the span from the original block
+                TokenTree {
+                    kind: TokenNode::Group(Delimiter::Brace, body),
+                    span,
+                }.into()
+            },
+            // $pat => $expr
+            Some(first_token) => {
+                let mut body = vec![first_token];
+                loop {
+                    match self.next() {
+                        Some(TokenTree { kind: TokenNode::Op(',', _), .. }) => break,
+                        Some(token) => {
+                            body.push(token);
+                        },
+                        None => return self.error("unexpected end of @match arm"),
+                    }
+                }
+                let body = self.block(body.into_iter().collect(), render)?;
+                // The generated code may have multiple statements, unlike the
+                // original expression. So wrap the whole thing in a block just
+                // in case.
+                quote!({ $body })
+            },
+            None => return self.error("unexpected end of @match arm"),
+        };
+        Ok(Some(pat.into_iter().chain(body.into_iter()).collect()))
     }
 
     /// Parses and renders a `@let` expression.

From 2213c0dc32ca86ebceb1e80ec307477406778237 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sat, 29 Jul 2017 19:20:33 +1200
Subject: [PATCH 14/17] Move escaping routines into a shared maud_htmlescape
 crate

---
 .travis.yml                |  2 ++
 Cargo.toml                 |  3 +-
 maud/Cargo.toml            |  1 +
 maud/src/lib.rs            | 48 ++-------------------------
 maud_htmlescape/Cargo.toml | 15 +++++++++
 maud_htmlescape/lib.rs     | 68 ++++++++++++++++++++++++++++++++++++++
 maud_macros/Cargo.toml     |  1 +
 maud_macros/src/lib.rs     |  1 +
 maud_macros/src/render.rs  | 28 ++--------------
 9 files changed, 94 insertions(+), 73 deletions(-)
 create mode 100644 maud_htmlescape/Cargo.toml
 create mode 100644 maud_htmlescape/lib.rs

diff --git a/.travis.yml b/.travis.yml
index e190a34..93cbc23 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,6 +12,8 @@ script:
       printf 'Checking for tabs in %s\n' "$TRAVIS_COMMIT_RANGE"
       ! git diff --name-only --diff-filter=ACMR "$TRAVIS_COMMIT_RANGE" | xargs grep $'\t'
     fi
+  - ( cd maud_htmlescape && cargo test --all-features )
+  - if command -v cargo-clippy > /dev/null; then ( cd maud_htmlescape && cargo clippy -- -D warnings ); fi
   - ( cd maud && cargo test --all-features )
   - if command -v cargo-clippy > /dev/null; then ( cd maud && cargo clippy -- -D warnings ); fi
   - ( cd maud_macros && cargo test --all-features )
diff --git a/Cargo.toml b/Cargo.toml
index 123dc41..87876cd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
 [workspace]
 members = [
-    "maud",
+    "maud_htmlescape",
     "maud_macros",
+    "maud",
     "maud_extras",
 ]
diff --git a/maud/Cargo.toml b/maud/Cargo.toml
index bfcb7bf..3001991 100644
--- a/maud/Cargo.toml
+++ b/maud/Cargo.toml
@@ -13,6 +13,7 @@ description = "Compile-time HTML templates."
 categories = ["template-engine"]
 
 [dependencies]
+maud_htmlescape = { version = "0.17.0", path = "../maud_htmlescape" }
 maud_macros = { version = "0.16.3", path = "../maud_macros" }
 iron = { version = "0.5.1", optional = true }
 rocket = { version = "0.3", optional = true }
diff --git a/maud/src/lib.rs b/maud/src/lib.rs
index 95b3d71..115d7df 100644
--- a/maud/src/lib.rs
+++ b/maud/src/lib.rs
@@ -13,6 +13,7 @@
 #[cfg(feature = "iron")] extern crate iron;
 #[cfg(feature = "rocket")] extern crate rocket;
 
+extern crate maud_htmlescape;
 extern crate maud_macros;
 
 use std::fmt::{self, Write};
@@ -121,52 +122,7 @@ impl<T: AsRef<str> + Into<String>> Into<String> for PreEscaped<T> {
     }
 }
 
-/// An adapter that escapes HTML special characters.
-///
-/// The following characters are escaped:
-///
-/// * `&` is escaped as `&amp;`
-/// * `<` is escaped as `&lt;`
-/// * `>` is escaped as `&gt;`
-/// * `"` is escaped as `&quot;`
-///
-/// All other characters are passed through unchanged.
-///
-/// **Note:** In versions prior to 0.13, the single quote (`'`) was
-/// escaped as well.
-///
-/// # Example
-///
-/// ```
-/// # use maud::Escaper;
-/// use std::fmt::Write;
-/// let mut s = String::new();
-/// write!(Escaper::new(&mut s), "<script>launchMissiles()</script>").unwrap();
-/// assert_eq!(s, "&lt;script&gt;launchMissiles()&lt;/script&gt;");
-/// ```
-pub struct Escaper<'a>(&'a mut String);
-
-impl<'a> Escaper<'a> {
-    /// Creates an `Escaper` from a `String`.
-    pub fn new(buffer: &'a mut String) -> Escaper<'a> {
-        Escaper(buffer)
-    }
-}
-
-impl<'a> fmt::Write for Escaper<'a> {
-    fn write_str(&mut self, s: &str) -> fmt::Result {
-        for b in s.bytes() {
-            match b {
-                b'&' => self.0.push_str("&amp;"),
-                b'<' => self.0.push_str("&lt;"),
-                b'>' => self.0.push_str("&gt;"),
-                b'"' => self.0.push_str("&quot;"),
-                _ => unsafe { self.0.as_mut_vec().push(b) },
-            }
-        }
-        Ok(())
-    }
-}
+pub use maud_htmlescape::Escaper;
 
 /// The literal string `<!DOCTYPE html>`.
 ///
diff --git a/maud_htmlescape/Cargo.toml b/maud_htmlescape/Cargo.toml
new file mode 100644
index 0000000..a5caae0
--- /dev/null
+++ b/maud_htmlescape/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+
+name = "maud_htmlescape"
+# When releasing a new version, please update html_root_url in lib.rs
+version = "0.17.0"
+authors = ["Chris Wong <lambda.fairy@gmail.com>"]
+
+license = "MIT/Apache-2.0"
+documentation = "https://docs.rs/maud_htmlescape/"
+homepage = "https://maud.lambda.xyz/"
+repository = "https://github.com/lfairy/maud"
+description = "Internal support code used by Maud."
+
+[lib]
+path = "lib.rs"
diff --git a/maud_htmlescape/lib.rs b/maud_htmlescape/lib.rs
new file mode 100644
index 0000000..873df13
--- /dev/null
+++ b/maud_htmlescape/lib.rs
@@ -0,0 +1,68 @@
+//! Internal support code used by the [Maud] template engine.
+//!
+//! You should not need to depend on this crate directly.
+//!
+//! [Maud]: https://maud.lambda.xyz
+
+#![doc(html_root_url = "https://docs.rs/maud_htmlescape/0.17.0")]
+
+use std::fmt;
+
+/// An adapter that escapes HTML special characters.
+///
+/// The following characters are escaped:
+///
+/// * `&` is escaped as `&amp;`
+/// * `<` is escaped as `&lt;`
+/// * `>` is escaped as `&gt;`
+/// * `"` is escaped as `&quot;`
+///
+/// All other characters are passed through unchanged.
+///
+/// **Note:** In versions prior to 0.13, the single quote (`'`) was
+/// escaped as well.
+///
+/// # Example
+///
+/// ```rust,ignore
+/// use std::fmt::Write;
+/// let mut s = String::new();
+/// write!(Escaper::new(&mut s), "<script>launchMissiles()</script>").unwrap();
+/// assert_eq!(s, "&lt;script&gt;launchMissiles()&lt;/script&gt;");
+/// ```
+pub struct Escaper<'a>(&'a mut String);
+
+impl<'a> Escaper<'a> {
+    /// Creates an `Escaper` from a `String`.
+    pub fn new(buffer: &'a mut String) -> Escaper<'a> {
+        Escaper(buffer)
+    }
+}
+
+impl<'a> fmt::Write for Escaper<'a> {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        for b in s.bytes() {
+            match b {
+                b'&' => self.0.push_str("&amp;"),
+                b'<' => self.0.push_str("&lt;"),
+                b'>' => self.0.push_str("&gt;"),
+                b'"' => self.0.push_str("&quot;"),
+                _ => unsafe { self.0.as_mut_vec().push(b) },
+            }
+        }
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use std::fmt::Write;
+    use Escaper;
+
+    #[test]
+    fn it_works() {
+        let mut s = String::new();
+        write!(Escaper::new(&mut s), "<script>launchMissiles()</script>").unwrap();
+        assert_eq!(s, "&lt;script&gt;launchMissiles()&lt;/script&gt;");
+    }
+}
diff --git a/maud_macros/Cargo.toml b/maud_macros/Cargo.toml
index 5765624..40907eb 100644
--- a/maud_macros/Cargo.toml
+++ b/maud_macros/Cargo.toml
@@ -13,6 +13,7 @@ description = "Compile-time HTML templates."
 
 [dependencies]
 literalext = { version = "0.1", default-features = false, features = ["proc-macro"] }
+maud_htmlescape = { version = "0.17.0", path = "../maud_htmlescape" }
 
 [lib]
 name = "maud_macros"
diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs
index cbdbb18..409c7ab 100644
--- a/maud_macros/src/lib.rs
+++ b/maud_macros/src/lib.rs
@@ -4,6 +4,7 @@
 #![doc(html_root_url = "https://docs.rs/maud_macros/0.16.3")]
 
 extern crate literalext;
+extern crate maud_htmlescape;
 extern crate proc_macro;
 
 // TODO move lints into their own `maud_lints` crate
diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs
index 04b5100..b286502 100644
--- a/maud_macros/src/render.rs
+++ b/maud_macros/src/render.rs
@@ -1,6 +1,7 @@
 use proc_macro::{Literal, Term, TokenNode, TokenStream};
 use proc_macro::quote;
-use std::fmt;
+
+use maud_htmlescape::Escaper;
 
 pub struct Renderer {
     output: TokenNode,
@@ -166,28 +167,3 @@ fn html_escape(s: &str) -> String {
     Escaper::new(&mut buffer).write_str(s).unwrap();
     buffer
 }
-
-// TODO move this into a common `maud_htmlescape` crate
-struct Escaper<'a>(&'a mut String);
-
-impl<'a> Escaper<'a> {
-    /// Creates an `Escaper` from a `String`.
-    pub fn new(buffer: &'a mut String) -> Escaper<'a> {
-        Escaper(buffer)
-    }
-}
-
-impl<'a> fmt::Write for Escaper<'a> {
-    fn write_str(&mut self, s: &str) -> fmt::Result {
-        for b in s.bytes() {
-            match b {
-                b'&' => self.0.push_str("&amp;"),
-                b'<' => self.0.push_str("&lt;"),
-                b'>' => self.0.push_str("&gt;"),
-                b'"' => self.0.push_str("&quot;"),
-                _ => unsafe { self.0.as_mut_vec().push(b) },
-            }
-        }
-        Ok(())
-    }
-}

From 7dcc7d7063dcaa5fe46a79a554ab22d18a53517d Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sat, 29 Jul 2017 19:41:22 +1200
Subject: [PATCH 15/17] Fix Clippy warnings

---
 maud_macros/src/parse.rs | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 023de7e..73a1bb6 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -7,14 +7,14 @@ use super::render::Renderer;
 use super::ParseResult;
 
 pub fn parse(input: TokenStream) -> ParseResult<TokenStream> {
-    let mut render = Renderer::new();
-    let _ = Parser {
-        in_attr: false,
-        input: input.clone().into_iter(),
-    }.markups(&mut render)?;
     // Heuristic: the size of the resulting markup tends to correlate with the
     // code size of the template itself
     let size_hint = input.to_string().len();
+    let mut render = Renderer::new();
+    Parser {
+        in_attr: false,
+        input: input.into_iter(),
+    }.markups(&mut render)?;
     Ok(render.into_expr(size_hint))
 }
 
@@ -24,11 +24,15 @@ struct Parser {
     input: TokenTreeIter,
 }
 
-impl Parser {
+impl Iterator for Parser {
+    type Item = TokenTree;
+
     fn next(&mut self) -> Option<TokenTree> {
         self.input.next()
     }
+}
 
+impl Parser {
     fn peek(&mut self) -> Option<TokenTree> {
         self.clone().next()
     }

From f1fbea181ad22b5e92541b24021f109f8d414de2 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sat, 29 Jul 2017 20:01:14 +1200
Subject: [PATCH 16/17] Reduce duplication in Travis config

---
 .travis.yml | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 93cbc23..e9f1b18 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,11 +12,12 @@ script:
       printf 'Checking for tabs in %s\n' "$TRAVIS_COMMIT_RANGE"
       ! git diff --name-only --diff-filter=ACMR "$TRAVIS_COMMIT_RANGE" | xargs grep $'\t'
     fi
-  - ( cd maud_htmlescape && cargo test --all-features )
-  - if command -v cargo-clippy > /dev/null; then ( cd maud_htmlescape && cargo clippy -- -D warnings ); fi
-  - ( cd maud && cargo test --all-features )
-  - if command -v cargo-clippy > /dev/null; then ( cd maud && cargo clippy -- -D warnings ); fi
-  - ( cd maud_macros && cargo test --all-features )
-  - if command -v cargo-clippy > /dev/null; then ( cd maud_macros && cargo clippy -- -D warnings ); fi
-  - ( cd maud_extras && cargo test --all-features )
-  - if command -v cargo-clippy > /dev/null; then ( cd maud_extras && cargo clippy -- -D warnings ); fi
+  - cargo test --all --all-features
+  - |
+    if command -v cargo-clippy > /dev/null; then
+      CLIPPY_STATUS=0
+      for package in maud_htmlescape maud_macros maud maud_extras; do
+        cargo clippy --manifest-path $package/Cargo.toml -- -D warnings || CLIPPY_STATUS=$?
+      done
+      (exit $CLIPPY_STATUS)
+    fi

From 4f7189375e8a248f1f33de780de241de6dc1e580 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sun, 30 Jul 2017 13:59:35 +1200
Subject: [PATCH 17/17] Avoid using `quote!` in parser

---
 maud_macros/src/parse.rs | 21 +++++++++++++++++----
 1 file changed, 17 insertions(+), 4 deletions(-)

diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 73a1bb6..fb82f29 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -1,4 +1,14 @@
-use proc_macro::{Delimiter, Literal, Spacing, TokenNode, TokenStream, TokenTree, TokenTreeIter, quote};
+use proc_macro::{
+    Delimiter,
+    Literal,
+    Spacing,
+    Span,
+    TokenNode,
+    TokenStream,
+    TokenTree,
+    TokenTreeIter,
+};
+use std::iter;
 use std::mem;
 
 use literalext::LiteralExt;
@@ -292,7 +302,7 @@ impl Parser {
                 TokenTree {
                     kind: TokenNode::Group(Delimiter::Brace, body),
                     span,
-                }.into()
+                }
             },
             // $pat => $expr
             Some(first_token) => {
@@ -310,11 +320,14 @@ impl Parser {
                 // The generated code may have multiple statements, unlike the
                 // original expression. So wrap the whole thing in a block just
                 // in case.
-                quote!({ $body })
+                TokenTree {
+                    kind: TokenNode::Group(Delimiter::Brace, body),
+                    span: Span::default(),
+                }
             },
             None => return self.error("unexpected end of @match arm"),
         };
-        Ok(Some(pat.into_iter().chain(body.into_iter()).collect()))
+        Ok(Some(pat.into_iter().chain(iter::once(body)).collect()))
     }
 
     /// Parses and renders a `@let` expression.