From 9d90e94c8701ccf420bd76bd94c7e48077ab3eb4 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Thu, 17 May 2018 20:38:44 +1200
Subject: [PATCH] Change macro internals to use an explicit AST (#127)

---
 CONTRIBUTING.md             |   2 +-
 maud/tests/basic_syntax.rs  |   2 +-
 maud_lints/src/doctype.rs   |   3 +-
 maud_lints/src/lib.rs       |   1 +
 maud_lints/src/util.rs      |   2 +-
 maud_macros/Cargo.toml      |   1 +
 maud_macros/src/ast.rs      |  77 ++++++++
 maud_macros/src/build.rs    | 132 --------------
 maud_macros/src/generate.rs | 279 ++++++++++++++++++++++++++++
 maud_macros/src/lib.rs      |   9 +-
 maud_macros/src/parse.rs    | 351 ++++++++++++++++--------------------
 11 files changed, 526 insertions(+), 333 deletions(-)
 create mode 100644 maud_macros/src/ast.rs
 delete mode 100644 maud_macros/src/build.rs
 create mode 100644 maud_macros/src/generate.rs

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7de1b0c..e0f13e9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,7 +9,7 @@ Great to see that you want to help out! Here are some tips for getting started:
 * Most contributions should include tests – see the [existing test code] for how to write these.
 
 * Documentation on the proc macro interface can be found on [docs.rust-lang.org][proc_macro].
-    - As for lints: [Manishearth's site] provides documentation for compiler internals.
+    - The `maud_lints` crate uses internal compiler APIs; the documentation for these can be found on [Manishearth's site].
 
 Have fun! ☺️
 
diff --git a/maud/tests/basic_syntax.rs b/maud/tests/basic_syntax.rs
index 66a7c00..49b5bc6 100644
--- a/maud/tests/basic_syntax.rs
+++ b/maud/tests/basic_syntax.rs
@@ -191,5 +191,5 @@ fn ids_shorthand() {
 #[test]
 fn classes_attrs_ids_mixed_up() {
     let s = html!(p { "Hi, " span.name.here lang="en" #thing { "Lyra" } "!" }).into_string();
-    assert_eq!(s, "<p>Hi, <span lang=\"en\" class=\"name here\" id=\"thing\">Lyra</span>!</p>");
+    assert_eq!(s, r#"<p>Hi, <span class="name here" id="thing" lang="en">Lyra</span>!</p>"#);
 }
diff --git a/maud_lints/src/doctype.rs b/maud_lints/src/doctype.rs
index b4193b8..34150a6 100644
--- a/maud_lints/src/doctype.rs
+++ b/maud_lints/src/doctype.rs
@@ -1,8 +1,9 @@
 use rustc::hir::{Expr, ExprCall, ExprLit, ExprPath};
 use rustc::lint::{LateContext, LateLintPass, LintArray, LintContext, LintPass};
-use super::util::match_def_path;
 use syntax::ast::LitKind;
 
+use util::match_def_path;
+
 declare_lint! {
     pub MAUD_DOCTYPE,
     Warn,
diff --git a/maud_lints/src/lib.rs b/maud_lints/src/lib.rs
index db4efb4..ec2562f 100644
--- a/maud_lints/src/lib.rs
+++ b/maud_lints/src/lib.rs
@@ -11,6 +11,7 @@ extern crate if_chain;
 extern crate rustc;
 extern crate rustc_plugin;
 extern crate syntax;
+extern crate syntax_pos;
 
 use rustc_plugin::Registry;
 
diff --git a/maud_lints/src/util.rs b/maud_lints/src/util.rs
index 125b537..6687f38 100644
--- a/maud_lints/src/util.rs
+++ b/maud_lints/src/util.rs
@@ -5,7 +5,7 @@
 use rustc::hir::def_id::DefId;
 use rustc::lint::LateContext;
 use rustc::ty;
-use syntax::symbol::{LocalInternedString, Symbol};
+use syntax_pos::symbol::{LocalInternedString, Symbol};
 
 /// Check if a `DefId`'s path matches the given absolute type path usage.
 ///
diff --git a/maud_macros/Cargo.toml b/maud_macros/Cargo.toml
index a39bebf..f3fc319 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"] }
+matches = "0.1.6"
 maud_htmlescape = { version = "0.17.0", path = "../maud_htmlescape" }
 
 [lib]
diff --git a/maud_macros/src/ast.rs b/maud_macros/src/ast.rs
new file mode 100644
index 0000000..b689d70
--- /dev/null
+++ b/maud_macros/src/ast.rs
@@ -0,0 +1,77 @@
+use proc_macro::{Span, TokenStream};
+
+#[derive(Debug)]
+pub enum Markup {
+    Block(Block),
+    Literal {
+        content: String,
+        span: Span,
+    },
+    Symbol {
+        symbol: TokenStream,
+    },
+    Splice {
+        expr: TokenStream,
+    },
+    Element {
+        name: TokenStream,
+        attrs: Attrs,
+        body: Option<Box<Markup>>,
+    },
+    Let {
+        tokens: TokenStream,
+    },
+    If {
+        segments: Vec<Special>,
+    },
+    Special(Special),
+    Match {
+        head: TokenStream,
+        arms: Vec<Special>,
+        arms_span: Span,
+    },
+}
+
+#[derive(Debug)]
+pub struct Attrs {
+    pub classes_static: Vec<ClassOrId>,
+    pub classes_toggled: Vec<(ClassOrId, Toggler)>,
+    pub ids: Vec<ClassOrId>,
+    pub attrs: Vec<Attribute>,
+}
+
+pub type ClassOrId = TokenStream;
+
+#[derive(Debug)]
+pub struct Block {
+    pub markups: Vec<Markup>,
+    pub span: Span,
+}
+
+#[derive(Debug)]
+pub struct Special {
+    pub head: TokenStream,
+    pub body: Block,
+}
+
+#[derive(Debug)]
+pub struct Attribute {
+    pub name: TokenStream,
+    pub attr_type: AttrType,
+}
+
+#[derive(Debug)]
+pub enum AttrType {
+    Normal {
+        value: Markup,
+    },
+    Empty {
+        toggler: Option<Toggler>,
+    },
+}
+
+#[derive(Debug)]
+pub struct Toggler {
+    pub cond: TokenStream,
+    pub cond_span: Span,
+}
diff --git a/maud_macros/src/build.rs b/maud_macros/src/build.rs
deleted file mode 100644
index a0a52c2..0000000
--- a/maud_macros/src/build.rs
+++ /dev/null
@@ -1,132 +0,0 @@
-use proc_macro::{Delimiter, Group, Literal, Span, TokenStream, TokenTree};
-use proc_macro::quote;
-
-use maud_htmlescape::Escaper;
-
-pub struct Builder {
-    output_ident: TokenTree,
-    stmts: Vec<TokenStream>,
-    tail: String,
-}
-
-impl Builder {
-    /// Creates a new `Builder`.
-    pub fn new(output_ident: TokenTree) -> Builder {
-        Builder {
-            output_ident,
-            stmts: Vec::new(),
-            tail: String::new(),
-        }
-    }
-
-    /// Flushes the tail buffer, emitting a single `.push_str()` call.
-    fn flush(&mut self) {
-        if !self.tail.is_empty() {
-            let expr = {
-                let output_ident = self.output_ident.clone();
-                let string = TokenTree::Literal(Literal::string(&self.tail));
-                quote!($output_ident.push_str($string);)
-            };
-            self.stmts.push(expr);
-            self.tail.clear();
-        }
-    }
-
-    /// Reifies the `Builder` into a raw list of statements.
-    pub fn build(mut self) -> TokenStream {
-        let Builder { stmts, .. } = { self.flush(); self };
-        stmts.into_iter().collect()
-    }
-
-    /// Pushes a statement, flushing the tail buffer in the process.
-    pub fn push<T>(&mut self, stmt: T) where T: Into<TokenStream> {
-        self.flush();
-        self.stmts.push(stmt.into())
-    }
-
-    /// Pushes a literal string to the tail buffer.
-    fn push_str(&mut self, s: &str) {
-        self.tail.push_str(s);
-    }
-
-    /// Appends a literal string.
-    pub fn string(&mut self, s: &str) {
-        self.push_str(&html_escape(s));
-    }
-
-    /// Appends the result of an expression.
-    pub fn splice(&mut self, expr: TokenStream) {
-        let output_ident = self.output_ident.clone();
-        self.push(quote!({
-            // Create a local trait alias so that autoref works
-            trait Render: maud::Render {
-                fn __maud_render_to(&self, output_ident: &mut String) {
-                    maud::Render::render_to(self, output_ident);
-                }
-            }
-            impl<T: maud::Render> Render for T {}
-            $expr.__maud_render_to(&mut $output_ident);
-        }));
-    }
-
-    pub fn element_open_start(&mut self, name: &str) {
-        self.push_str("<");
-        self.push_str(name);
-    }
-
-    pub fn attribute_start(&mut self, name: &str) {
-        self.push_str(" ");
-        self.push_str(name);
-        self.push_str("=\"");
-    }
-
-    pub fn attribute_empty(&mut self, name: &str) {
-        self.push_str(" ");
-        self.push_str(name);
-    }
-
-    pub fn attribute_end(&mut self) {
-        self.push_str("\"");
-    }
-
-    pub fn element_open_end(&mut self) {
-        self.push_str(">");
-    }
-
-    pub fn element_close(&mut self, name: &str) {
-        self.push_str("</");
-        self.push_str(name);
-        self.push_str(">");
-    }
-
-    /// Emits an `if` expression.
-    ///
-    /// The condition is a token stream (not an expression) so we don't
-    /// need to special-case `if let`.
-    pub fn emit_if(
-        &mut self,
-        mut cond: TokenStream,
-        cond_span: Span,
-        body: TokenStream,
-    ) {
-        // If the condition contains an opening brace `{`,
-        // wrap it in parentheses to avoid parse errors
-        if cond.clone().into_iter().any(|token| match token {
-            TokenTree::Group(group) => group.delimiter() == Delimiter::Brace,
-            _ => false,
-        }) {
-            let mut group = Group::new(Delimiter::Parenthesis, cond);
-            // NOTE: Do we need to do this?
-            group.set_span(cond_span);
-            cond = TokenStream::from(TokenTree::Group(group));
-        }
-        self.push(quote!(if $cond { $body }));
-    }
-}
-
-fn html_escape(s: &str) -> String {
-    use std::fmt::Write;
-    let mut buffer = String::new();
-    Escaper::new(&mut buffer).write_str(s).unwrap();
-    buffer
-}
diff --git a/maud_macros/src/generate.rs b/maud_macros/src/generate.rs
new file mode 100644
index 0000000..d2fbe4b
--- /dev/null
+++ b/maud_macros/src/generate.rs
@@ -0,0 +1,279 @@
+use maud_htmlescape::Escaper;
+use proc_macro::{
+    Delimiter,
+    Group,
+    Literal,
+    quote,
+    Span,
+    Term,
+    TokenStream,
+    TokenTree,
+};
+
+use ast::*;
+
+pub fn generate(markups: Vec<Markup>, output_ident: TokenTree) -> TokenStream {
+    let mut build = Builder::new(output_ident.clone());
+    Generator::new(output_ident).markups(markups, &mut build);
+    build.finish()
+}
+
+struct Generator {
+    output_ident: TokenTree,
+}
+
+impl Generator {
+    fn new(output_ident: TokenTree) -> Generator {
+        Generator { output_ident }
+    }
+
+    fn builder(&self) -> Builder {
+        Builder::new(self.output_ident.clone())
+    }
+
+    fn markups(&self, markups: Vec<Markup>, build: &mut Builder) {
+        for markup in markups {
+            self.markup(markup, build);
+        }
+    }
+
+    fn markup(&self, markup: Markup, build: &mut Builder) {
+        match markup {
+            Markup::Block(Block { markups, span }) => {
+                if markups.iter().any(|markup| matches!(*markup, Markup::Let { .. })) {
+                    build.push_tokens(self.block(Block { markups, span }));
+                } else {
+                    self.markups(markups, build);
+                }
+            },
+            Markup::Literal { content, .. } => build.push_escaped(&content),
+            Markup::Symbol { symbol } => self.name(symbol, build),
+            Markup::Splice { expr } => build.push_tokens(self.splice(expr)),
+            Markup::Element { name, attrs, body } => self.element(name, attrs, body, build),
+            Markup::Let { tokens } => build.push_tokens(tokens),
+            Markup::If { segments } => {
+                for segment in segments {
+                    build.push_tokens(self.special(segment));
+                }
+            },
+            Markup::Special(special) => build.push_tokens(self.special(special)),
+            Markup::Match { head, arms, arms_span } => {
+                build.push_tokens({
+                    let body = arms
+                        .into_iter()
+                        .map(|arm| self.special(arm))
+                        .collect();
+                    let mut body = TokenTree::Group(Group::new(Delimiter::Brace, body));
+                    body.set_span(arms_span);
+                    quote!($head $body)
+                });
+            },
+        }
+    }
+
+    fn block(&self, Block { markups, span }: Block) -> TokenStream {
+        let mut build = self.builder();
+        self.markups(markups, &mut build);
+        let mut block = TokenTree::Group(Group::new(Delimiter::Brace, build.finish()));
+        block.set_span(span);
+        TokenStream::from(block)
+    }
+
+    fn splice(&self, expr: TokenStream) -> TokenStream {
+        let output_ident = self.output_ident.clone();
+        quote!({
+            // Create a local trait alias so that autoref works
+            trait Render: maud::Render {
+                fn __maud_render_to(&self, output_ident: &mut String) {
+                    maud::Render::render_to(self, output_ident);
+                }
+            }
+            impl<T: maud::Render> Render for T {}
+            $expr.__maud_render_to(&mut $output_ident);
+        })
+    }
+
+    fn element(
+        &self,
+        name: TokenStream,
+        attrs: Attrs,
+        body: Option<Box<Markup>>,
+        build: &mut Builder,
+    ) {
+        build.push_str("<");
+        self.name(name.clone(), build);
+        self.attrs(attrs, build);
+        build.push_str(">");
+        if let Some(body) = body {
+            self.markup(*body, build);
+            build.push_str("</");
+            self.name(name, build);
+            build.push_str(">");
+        }
+    }
+
+    fn name(&self, name: TokenStream, build: &mut Builder) {
+        let string = name.into_iter().map(|token| token.to_string()).collect::<String>();
+        build.push_escaped(&string);
+    }
+
+    fn attrs(&self, attrs: Attrs, build: &mut Builder) {
+        for Attribute { name, attr_type } in desugar_attrs(attrs) {
+            match attr_type {
+                AttrType::Normal { value } => {
+                    build.push_str(" ");
+                    self.name(name, build);
+                    build.push_str("=\"");
+                    self.markup(value, build);
+                    build.push_str("\"");
+                },
+                AttrType::Empty { toggler: None } => {
+                    build.push_str(" ");
+                    self.name(name, build);
+                },
+                AttrType::Empty { toggler: Some(toggler) } => {
+                    let head = desugar_toggler(toggler);
+                    build.push_tokens({
+                        let mut build = self.builder();
+                        build.push_str(" ");
+                        self.name(name, &mut build);
+                        let body = build.finish();
+                        quote!($head { $body })
+                    })
+                },
+            }
+        }
+    }
+
+    fn special(&self, Special { head, body }: Special) -> TokenStream {
+        let body = self.block(body);
+        quote!($head $body)
+    }
+}
+
+////////////////////////////////////////////////////////
+
+fn desugar_attrs(Attrs { classes_static, classes_toggled, ids, attrs }: Attrs) -> Vec<Attribute> {
+    let classes = desugar_classes_or_ids("class", classes_static, classes_toggled);
+    let ids = desugar_classes_or_ids("id", ids, vec![]);
+    classes.into_iter().chain(ids).chain(attrs).collect()
+}
+
+fn desugar_classes_or_ids(
+    attr_name: &'static str,
+    values_static: Vec<ClassOrId>,
+    values_toggled: Vec<(ClassOrId, Toggler)>,
+) -> Option<Attribute> {
+    if values_static.is_empty() && values_toggled.is_empty() {
+        return None;
+    }
+    let mut markups = Vec::new();
+    let mut leading_space = false;
+    for symbol in values_static {
+        markups.extend(prepend_leading_space(symbol, &mut leading_space));
+    }
+    for (symbol, toggler) in values_toggled {
+        let body = Block {
+            markups: prepend_leading_space(symbol, &mut leading_space),
+            span: toggler.cond_span,
+        };
+        let head = desugar_toggler(toggler);
+        markups.push(Markup::Special(Special { head, body }));
+    }
+    Some(Attribute {
+        name: TokenStream::from(TokenTree::Term(Term::new(attr_name, Span::call_site()))),
+        attr_type: AttrType::Normal {
+            value: Markup::Block(Block {
+                markups,
+                span: Span::call_site(),
+            }),
+        },
+    })
+}
+
+fn prepend_leading_space(symbol: TokenStream, leading_space: &mut bool) -> Vec<Markup> {
+    let mut markups = Vec::new();
+    if *leading_space {
+        markups.push(Markup::Literal {
+            content: " ".to_owned(),
+            span: span_tokens(symbol.clone()),
+        });
+    }
+    *leading_space = true;
+    markups.push(Markup::Symbol { symbol });
+    markups
+}
+
+fn desugar_toggler(Toggler { mut cond, cond_span }: Toggler) -> TokenStream {
+    // If the expression contains an opening brace `{`,
+    // wrap it in parentheses to avoid parse errors
+    if cond.clone().into_iter().any(|token| match token {
+        TokenTree::Group(ref group) if group.delimiter() == Delimiter::Brace => true,
+        _ => false,
+    }) {
+        let mut wrapped_cond = TokenTree::Group(Group::new(Delimiter::Parenthesis, cond));
+        wrapped_cond.set_span(cond_span);
+        cond = TokenStream::from(wrapped_cond);
+    }
+    quote!(if $cond)
+}
+
+fn span_tokens<I: IntoIterator<Item=TokenTree>>(tokens: I) -> Span {
+    tokens
+        .into_iter()
+        .fold(None, |span: Option<Span>, token| Some(match span {
+            None => token.span(),
+            Some(span) => span.join(token.span()).unwrap_or(span),
+        }))
+        .unwrap_or_else(Span::def_site)
+}
+
+////////////////////////////////////////////////////////
+
+struct Builder {
+    output_ident: TokenTree,
+    tokens: Vec<TokenTree>,
+    tail: String,
+}
+
+impl Builder {
+    fn new(output_ident: TokenTree) -> Builder {
+        Builder {
+            output_ident,
+            tokens: Vec::new(),
+            tail: String::new(),
+        }
+    }
+
+    fn push_str(&mut self, string: &str) {
+        self.tail.push_str(string);
+    }
+
+    fn push_escaped(&mut self, string: &str) {
+        use std::fmt::Write;
+        Escaper::new(&mut self.tail).write_str(string).unwrap();
+    }
+
+    fn push_tokens<T: IntoIterator<Item=TokenTree>>(&mut self, tokens: T) {
+        self.cut();
+        self.tokens.extend(tokens);
+    }
+
+    fn cut(&mut self) {
+        if self.tail.is_empty() {
+            return;
+        }
+        let push_str_expr = {
+            let output_ident = self.output_ident.clone();
+            let string = TokenTree::Literal(Literal::string(&self.tail));
+            quote!($output_ident.push_str($string);)
+        };
+        self.tail.clear();
+        self.tokens.extend(push_str_expr);
+    }
+
+    fn finish(mut self) -> TokenStream {
+        self.cut();
+        self.tokens.into_iter().collect()
+    }
+}
diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs
index e322387..30fa1dc 100644
--- a/maud_macros/src/lib.rs
+++ b/maud_macros/src/lib.rs
@@ -8,11 +8,13 @@
 #![cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))]
 
 extern crate literalext;
+#[macro_use] extern crate matches;
 extern crate maud_htmlescape;
 extern crate proc_macro;
 
+mod ast;
+mod generate;
 mod parse;
-mod build;
 
 use proc_macro::{Literal, Span, Term, TokenStream, TokenTree};
 use proc_macro::quote;
@@ -37,10 +39,11 @@ fn expand(input: TokenStream) -> TokenStream {
     // code size of the template itself
     let size_hint = input.to_string().len();
     let size_hint = TokenTree::Literal(Literal::u64_unsuffixed(size_hint as u64));
-    let stmts = match parse::parse(input, output_ident.clone()) {
-        Ok(stmts) => stmts,
+    let markups = match parse::parse(input) {
+        Ok(markups) => markups,
         Err(e) => panic!(e),
     };
+    let stmts = generate::generate(markups, output_ident.clone());
     quote!({
         extern crate maud;
         let mut $output_ident = String::with_capacity($size_hint);
diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index d1562b5..199f582 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -1,30 +1,24 @@
 use proc_macro::{
     Delimiter,
-    Group,
     Literal,
     Spacing,
     Span,
     TokenStream,
     TokenTree,
 };
-use std::iter;
 use std::mem;
 
 use literalext::LiteralExt;
 
-use super::build::Builder;
-use super::ParseResult;
+use ast;
+use ParseResult;
 
-pub fn parse(input: TokenStream, output_ident: TokenTree) -> ParseResult<TokenStream> {
-    let mut parser = Parser::new(input, output_ident);
-    let mut builder = parser.builder();
-    parser.markups(&mut builder)?;
-    Ok(builder.build())
+pub fn parse(input: TokenStream) -> ParseResult<Vec<ast::Markup>> {
+    Parser::new(input).markups()
 }
 
 #[derive(Clone)]
 struct Parser {
-    output_ident: TokenTree,
     /// Indicates whether we're inside an attribute node.
     in_attr: bool,
     input: <TokenStream as IntoIterator>::IntoIter,
@@ -39,9 +33,8 @@ impl Iterator for Parser {
 }
 
 impl Parser {
-    fn new(input: TokenStream, output_ident: TokenTree) -> Parser {
+    fn new(input: TokenStream) -> Parser {
         Parser {
-            output_ident,
             in_attr: false,
             input: input.into_iter(),
         }
@@ -49,16 +42,11 @@ impl Parser {
 
     fn with_input(&self, input: TokenStream) -> Parser {
         Parser {
-            output_ident: self.output_ident.clone(),
             in_attr: self.in_attr,
             input: input.into_iter(),
         }
     }
 
-    fn builder(&self) -> Builder {
-        Builder::new(self.output_ident.clone())
-    }
-
     /// Returns the next token in the stream without consuming it.
     fn peek(&mut self) -> Option<TokenTree> {
         self.clone().next()
@@ -92,52 +80,54 @@ impl Parser {
     }
 
     /// Parses and renders multiple blocks of markup.
-    fn markups(&mut self, builder: &mut Builder) -> ParseResult<()> {
+    fn markups(&mut self) -> ParseResult<Vec<ast::Markup>> {
+        let mut result = Vec::new();
         loop {
             match self.peek2() {
-                None => return Ok(()),
+                None => break,
                 Some((TokenTree::Op(op), _)) if op.op() == ';' => self.advance(),
-                Some((TokenTree::Op(op), Some(TokenTree::Term(term)))) if op.op() == '@' && term.as_str() == "let" => {
-                    // When emitting a `@let`, wrap the rest of the block in a
-                    // new block to avoid scoping issues
+                Some((
+                    TokenTree::Op(op),
+                    Some(TokenTree::Term(term)),
+                )) if op.op() == '@' && term.as_str() == "let" => {
                     self.advance2();
-                    builder.push({
-                        let mut builder = self.builder();
-                        builder.push(TokenTree::Term(term));
-                        self.let_expr(&mut builder)?;
-                        self.markups(&mut builder)?;
-                        TokenTree::Group(Group::new(Delimiter::Brace, builder.build()))
-                    });
+                    let keyword = TokenTree::Term(term);
+                    result.push(self.let_expr(keyword)?);
                 },
-                _ => self.markup(builder)?,
+                _ => result.push(self.markup()?),
             }
         }
+        Ok(result)
     }
 
     /// Parses and renders a single block of markup.
-    fn markup(&mut self, builder: &mut Builder) -> ParseResult<()> {
+    fn markup(&mut self) -> ParseResult<ast::Markup> {
         let token = match self.peek() {
             Some(token) => token,
             None => return self.error("unexpected end of input"),
         };
-        match token {
+        let markup = match token {
             // Literal
             TokenTree::Literal(lit) => {
                 self.advance();
-                self.literal(lit, builder)?;
+                self.literal(&lit)?
             },
             // Special form
             TokenTree::Op(op) if op.op() == '@' => {
                 self.advance();
                 match self.next() {
                     Some(TokenTree::Term(term)) => {
-                        builder.push(TokenTree::Term(term));
+                        let keyword = TokenTree::Term(term);
                         match term.as_str() {
-                            "if" => self.if_expr(builder)?,
-                            "while" => self.while_expr(builder)?,
-                            "for" => self.for_expr(builder)?,
-                            "match" => self.match_expr(builder)?,
-                            "let" => return self.error("let only works inside a block"),
+                            "if" => {
+                                let mut segments = Vec::new();
+                                self.if_expr(vec![keyword], &mut segments)?;
+                                ast::Markup::If { segments }
+                            },
+                            "while" => self.while_expr(keyword)?,
+                            "for" => self.for_expr(keyword)?,
+                            "match" => self.match_expr(keyword)?,
+                            "let" => return self.error("@let only works inside a block"),
                             other => return self.error(format!("unknown keyword `@{}`", other)),
                         }
                     },
@@ -147,82 +137,91 @@ impl Parser {
             // Element
             TokenTree::Term(_) => {
                 let name = self.namespaced_name()?;
-                self.element(&name, builder)?;
+                self.element(name)?
             },
             // Splice
             TokenTree::Group(ref group) if group.delimiter() == Delimiter::Parenthesis => {
                 self.advance();
-                builder.splice(group.stream());
-            },
+                ast::Markup::Splice { expr: group.stream() }
+            }
             // Block
             TokenTree::Group(ref group) if group.delimiter() == Delimiter::Brace => {
                 self.advance();
-                self.with_input(group.stream()).markups(builder)?;
+                ast::Markup::Block(self.block(group.stream(), group.span())?)
             },
             // ???
             _ => return self.error("invalid syntax"),
-        }
-        Ok(())
+        };
+        Ok(markup)
     }
 
     /// Parses and renders a literal string.
-    fn literal(&mut self, lit: Literal, builder: &mut Builder) -> ParseResult<()> {
+    fn literal(&mut self, lit: &Literal) -> ParseResult<ast::Markup> {
         if let Some(s) = lit.parse_string() {
-            builder.string(&s);
-            Ok(())
+            Ok(ast::Markup::Literal {
+                content: s.to_string(),
+                span: lit.span(),
+            })
         } else {
             self.error("expected string")
         }
     }
 
-    /// Parses and renders an `@if` expression.
+    /// Parses an `@if` expression.
     ///
     /// The leading `@if` should already be consumed.
-    fn if_expr(&mut self, builder: &mut Builder) -> ParseResult<()> {
-        loop {
+    fn if_expr(
+        &mut self,
+        prefix: Vec<TokenTree>,
+        segments: &mut Vec<ast::Special>,
+    ) -> ParseResult<()> {
+        let mut head = prefix;
+        let body = loop {
             match self.next() {
                 Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => {
-                    let block = self.block(block.stream(), block.span())?;
-                    builder.push(block);
-                    break;
+                    break self.block(block.stream(), block.span())?;
                 },
-                Some(token) => builder.push(token),
+                Some(token) => head.push(token),
                 None => return self.error("unexpected end of @if expression"),
             }
-        }
-        self.else_if_expr(builder)
+        };
+        segments.push(ast::Special { head: head.into_iter().collect(), body });
+        self.else_if_expr(segments)
     }
 
-    /// Parses and renders an optional `@else if` or `@else`.
+    /// Parses an optional `@else if` or `@else`.
     ///
     /// The leading `@else if` or `@else` should *not* already be consumed.
-    fn else_if_expr(&mut self, builder: &mut Builder) -> ParseResult<()> {
+    fn else_if_expr(&mut self, segments: &mut Vec<ast::Special>) -> ParseResult<()> {
         match self.peek2() {
             Some((
                 TokenTree::Op(op),
                 Some(TokenTree::Term(else_keyword)),
             )) if op.op() == '@' && else_keyword.as_str() == "else" => {
                 self.advance2();
-                builder.push(TokenTree::Term(else_keyword));
+                let else_keyword = TokenTree::Term(else_keyword);
                 match self.peek() {
                     // `@else if`
                     Some(TokenTree::Term(if_keyword)) if if_keyword.as_str() == "if" => {
                         self.advance();
-                        builder.push(TokenTree::Term(if_keyword));
-                        self.if_expr(builder)?;
+                        let if_keyword = TokenTree::Term(if_keyword);
+                        self.if_expr(vec![else_keyword, if_keyword], segments)
                     },
                     // Just an `@else`
                     _ => {
                         match self.next() {
                             Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Brace => {
-                                let block = self.block(group.stream(), group.span())?;
-                                builder.push(block);
+                                let body = self.block(group.stream(), group.span())?;
+                                segments.push(ast::Special {
+                                    head: vec![else_keyword].into_iter().collect(),
+                                    body,
+                                });
+                                Ok(())
                             },
-                            _ => return self.error("expected body for @else"),
+                            _ => self.error("expected body for @else"),
                         }
                     },
                 }
-                self.else_if_expr(builder)
             },
             // We didn't find an `@else`; stop
             _ => Ok(()),
@@ -232,95 +231,90 @@ impl Parser {
     /// Parses and renders an `@while` expression.
     ///
     /// The leading `@while` should already be consumed.
-    fn while_expr(&mut self, builder: &mut Builder) -> ParseResult<()> {
-        loop {
+    fn while_expr(&mut self, keyword: TokenTree) -> ParseResult<ast::Markup> {
+        let mut head = vec![keyword];
+        let body = loop {
             match self.next() {
                 Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => {
-                    let block = self.block(block.stream(), block.span())?;
-                    builder.push(block);
-                    break;
+                    break self.block(block.stream(), block.span())?;
                 },
-                Some(token) => builder.push(token),
+                Some(token) => head.push(token),
                 None => return self.error("unexpected end of @while expression"),
             }
-        }
-        Ok(())
+        };
+        Ok(ast::Markup::Special(ast::Special { head: head.into_iter().collect(), body }))
     }
 
-    /// Parses and renders a `@for` expression.
+    /// Parses a `@for` expression.
     ///
     /// The leading `@for` should already be consumed.
-    fn for_expr(&mut self, builder: &mut Builder) -> ParseResult<()> {
+    fn for_expr(&mut self, keyword: TokenTree) -> ParseResult<ast::Markup> {
+        let mut head = vec![keyword];
         loop {
             match self.next() {
                 Some(TokenTree::Term(in_keyword)) if in_keyword.as_str() == "in" => {
-                    builder.push(TokenTree::Term(in_keyword));
+                    head.push(TokenTree::Term(in_keyword));
                     break;
                 },
-                Some(token) => builder.push(token),
+                Some(token) => head.push(token),
                 None => return self.error("unexpected end of @for expression"),
             }
         }
-        loop {
+        let body = loop {
             match self.next() {
                 Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => {
-                    let block = self.block(block.stream(), block.span())?;
-                    builder.push(block);
-                    break;
+                    break self.block(block.stream(), block.span())?;
                 },
-                Some(token) => builder.push(token),
+                Some(token) => head.push(token),
                 None => return self.error("unexpected end of @for expression"),
             }
-        }
-        Ok(())
+        };
+        Ok(ast::Markup::Special(ast::Special { head: head.into_iter().collect(), body }))
     }
 
-    /// Parses and renders a `@match` expression.
+    /// Parses a `@match` expression.
     ///
     /// The leading `@match` should already be consumed.
-    fn match_expr(&mut self, builder: &mut Builder) -> ParseResult<()> {
-        loop {
+    fn match_expr(&mut self, keyword: TokenTree) -> ParseResult<ast::Markup> {
+        let mut head = vec![keyword];
+        let (arms, arms_span) = loop {
             match self.next() {
                 Some(TokenTree::Group(ref body)) if body.delimiter() == Delimiter::Brace => {
                     let span = body.span();
-                    let body = self.with_input(body.stream()).match_arms()?;
-                    let mut body = Group::new(Delimiter::Brace, body);
-                    body.set_span(span);
-                    builder.push(TokenTree::Group(body));
-                    break;
+                    break (self.with_input(body.stream()).match_arms()?, span);
                 },
-                Some(token) => builder.push(token),
+                Some(token) => head.push(token),
                 None => return self.error("unexpected end of @match expression"),
             }
-        }
-        Ok(())
+        };
+        Ok(ast::Markup::Match { head: head.into_iter().collect(), arms, arms_span })
     }
 
-    fn match_arms(&mut self) -> ParseResult<TokenStream> {
+    fn match_arms(&mut self) -> ParseResult<Vec<ast::Special>> {
         let mut arms = Vec::new();
         while let Some(arm) = self.match_arm()? {
             arms.push(arm);
         }
-        Ok(arms.into_iter().collect())
+        Ok(arms)
     }
 
-    fn match_arm(&mut self) -> ParseResult<Option<TokenStream>> {
-        let mut pat = Vec::new();
+    fn match_arm(&mut self) -> ParseResult<Option<ast::Special>> {
+        let mut head = Vec::new();
         loop {
             match self.peek2() {
                 Some((TokenTree::Op(eq), Some(TokenTree::Op(gt))))
                 if eq.op() == '=' && gt.op() == '>' && eq.spacing() == Spacing::Joint => {
                     self.advance2();
-                    pat.push(TokenTree::Op(eq));
-                    pat.push(TokenTree::Op(gt));
+                    head.push(TokenTree::Op(eq));
+                    head.push(TokenTree::Op(gt));
                     break;
                 },
                 Some((token, _)) => {
                     self.advance();
-                    pat.push(token);
+                    head.push(token);
                 },
                 None =>
-                    if pat.is_empty() {
+                    if head.is_empty() {
                         return Ok(None);
                     } else {
                         return self.error("unexpected end of @match pattern");
@@ -359,22 +353,23 @@ impl Parser {
             },
             None => return self.error("unexpected end of @match arm"),
         };
-        Ok(Some(pat.into_iter().chain(iter::once(body)).collect()))
+        Ok(Some(ast::Special { head: head.into_iter().collect(), body }))
     }
 
-    /// Parses and renders a `@let` expression.
+    /// Parses a `@let` expression.
     ///
     /// The leading `@let` should already be consumed.
-    fn let_expr(&mut self, builder: &mut Builder) -> ParseResult<()> {
+    fn let_expr(&mut self, keyword: TokenTree) -> ParseResult<ast::Markup> {
+        let mut tokens = vec![keyword];
         loop {
             match self.next() {
                 Some(token) => {
                     match token {
                         TokenTree::Op(ref op) if op.op() == '=' => {
-                            builder.push(token.clone());
+                            tokens.push(token.clone());
                             break;
                         },
-                        _ => builder.push(token),
+                        _ => tokens.push(token),
                     }
                 },
                 None => return self.error("unexpected end of @let expression"),
@@ -385,46 +380,43 @@ impl Parser {
                 Some(token) => {
                     match token {
                         TokenTree::Op(ref op) if op.op() == ';' => {
-                            builder.push(token.clone());
+                            tokens.push(token.clone());
                             break;
                         },
-                        _ => builder.push(token),
+                        _ => tokens.push(token),
                     }
                 },
                 None => return self.error("unexpected end of @let expression"),
             }
         }
-        Ok(())
+        Ok(ast::Markup::Let { tokens: tokens.into_iter().collect() })
     }
 
-    /// Parses and renders an element node.
+    /// Parses an element node.
     ///
     /// The element name should already be consumed.
-    fn element(&mut self, name: &str, builder: &mut Builder) -> ParseResult<()> {
+    fn element(&mut self, name: TokenStream) -> ParseResult<ast::Markup> {
         if self.in_attr {
             return self.error("unexpected element, you silly bumpkin");
         }
-        builder.element_open_start(name);
-        self.attrs(builder)?;
-        builder.element_open_end();
-        match self.peek() {
+        let attrs = self.attrs()?;
+        let body = match self.peek() {
             Some(TokenTree::Op(o)) if o.op() == ';' || o.op() == '/' => {
                 // Void element
                 self.advance();
+                None
             },
-            _ => {
-                self.markup(builder)?;
-                builder.element_close(name);
-            },
-        }
-        Ok(())
+            _ => Some(Box::new(self.markup()?)),
+        };
+        Ok(ast::Markup::Element { name, attrs, body })
     }
 
-    /// Parses and renders the attributes of an element.
-    fn attrs(&mut self, builder: &mut Builder) -> ParseResult<()> {
+    /// Parses the attributes of an element.
+    fn attrs(&mut self) -> ParseResult<ast::Attrs> {
         let mut classes_static = Vec::new();
         let mut classes_toggled = Vec::new();
         let mut ids = Vec::new();
+        let mut attrs = Vec::new();
         loop {
             let mut attempt = self.clone();
             let maybe_name = attempt.namespaced_name();
@@ -433,41 +425,35 @@ impl Parser {
                 // Non-empty attribute
                 (Ok(ref name), Some(TokenTree::Op(ref op))) if op.op() == '=' => {
                     self.commit(attempt);
-                    builder.attribute_start(&name);
+                    let value;
                     {
                         // Parse a value under an attribute context
                         let in_attr = mem::replace(&mut self.in_attr, true);
-                        self.markup(builder)?;
+                        value = self.markup()?;
                         self.in_attr = in_attr;
                     }
-                    builder.attribute_end();
+                    attrs.push(ast::Attribute {
+                        name: name.clone(),
+                        attr_type: ast::AttrType::Normal { value },
+                    });
                 },
                 // Empty attribute
                 (Ok(ref name), Some(TokenTree::Op(ref op))) if op.op() == '?' => {
                     self.commit(attempt);
-                    if let Some((cond, cond_span)) = self.attr_toggler() {
-                        // Toggle the attribute based on a boolean expression
-                        let body = {
-                            let mut builder = self.builder();
-                            builder.attribute_empty(&name);
-                            builder.build()
-                        };
-                        builder.emit_if(cond, cond_span, body);
-                    } else {
-                        // Write the attribute unconditionally
-                        builder.attribute_empty(&name);
-                    }
+                    let toggler = self.attr_toggler();
+                    attrs.push(ast::Attribute {
+                        name: name.clone(),
+                        attr_type: ast::AttrType::Empty { toggler },
+                    });
                 },
                 // Class shorthand
                 (Err(_), Some(TokenTree::Op(op))) if op.op() == '.' => {
                     self.commit(attempt);
-                    let class_name = self.name()?;
-                    if let Some((cond, cond_span)) = self.attr_toggler() {
-                        // Toggle the class based on a boolean expression
-                        classes_toggled.push((cond, cond_span, class_name));
+                    let name = self.name()?;
+                    if let Some(toggler) = self.attr_toggler() {
+                        classes_toggled.push((name, toggler));
                     } else {
-                        // Emit the class unconditionally
-                        classes_static.push(class_name);
+                        classes_static.push(name);
                     }
                 },
                 // ID shorthand
@@ -479,91 +465,68 @@ impl Parser {
                 _ => break,
             }
         }
-        if !classes_static.is_empty() || !classes_toggled.is_empty() {
-            builder.attribute_start("class");
-            builder.string(&classes_static.join(" "));
-            for (i, (cond, cond_span, mut class_name)) in classes_toggled.into_iter().enumerate() {
-                // If a class comes first in the list, then it shouldn't be
-                // prefixed by a space
-                if i > 0 || !classes_static.is_empty() {
-                    class_name = format!(" {}", class_name);
-                }
-                let body = {
-                    let mut builder = self.builder();
-                    builder.string(&class_name);
-                    builder.build()
-                };
-                builder.emit_if(cond, cond_span, body);
-            }
-            builder.attribute_end();
-        }
-        if !ids.is_empty() {
-            builder.attribute_start("id");
-            builder.string(&ids.join(" "));
-            builder.attribute_end();
-        }
-        Ok(())
+        Ok(ast::Attrs { classes_static, classes_toggled, ids, attrs })
     }
 
     /// Parses the `[cond]` syntax after an empty attribute or class shorthand.
-    fn attr_toggler(&mut self) -> Option<(TokenStream, Span)> {
+    fn attr_toggler(&mut self) -> Option<ast::Toggler> {
         match self.peek() {
-            Some(TokenTree::Group(ref grp)) if grp.delimiter() == Delimiter::Bracket => {
+            Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Bracket => {
                 self.advance();
-                Some((grp.stream(), grp.span()))
+                Some(ast::Toggler {
+                    cond: group.stream(),
+                    cond_span: group.span(),
+                })
             },
             _ => None,
         }
     }
 
     /// Parses an identifier, without dealing with namespaces.
-    fn name(&mut self) -> ParseResult<String> {
-        let mut s = if let Some(TokenTree::Term(term)) = self.peek() {
+    fn name(&mut self) -> ParseResult<TokenStream> {
+        let mut result = Vec::new();
+        if let Some(token @ TokenTree::Term(_)) = self.peek() {
             self.advance();
-            String::from(term.as_str())
+            result.push(token);
         } else {
             return self.error("expected identifier");
-        };
+        }
         let mut expect_ident = false;
         loop {
             expect_ident = match self.peek() {
                 Some(TokenTree::Op(op)) if op.op() == '-' => {
                     self.advance();
-                    s.push('-');
+                    result.push(TokenTree::Op(op));
                     true
                 },
                 Some(TokenTree::Term(term)) if expect_ident => {
                     self.advance();
-                    s.push_str(term.as_str());
+                    result.push(TokenTree::Term(term));
                     false
                 },
                 _ => break,
             };
         }
-        Ok(s)
+        Ok(result.into_iter().collect())
     }
 
     /// Parses a HTML element or attribute name, along with a namespace
     /// if necessary.
-    fn namespaced_name(&mut self) -> ParseResult<String> {
-        let mut s = self.name()?;
+    fn namespaced_name(&mut self) -> ParseResult<TokenStream> {
+        let mut result = vec![self.name()?];
         if let Some(TokenTree::Op(op)) = self.peek() {
             if op.op() == ':' {
                 self.advance();
-                s.push(':');
-                s.push_str(&self.name()?);
+                result.push(TokenStream::from(TokenTree::Op(op)));
+                result.push(self.name()?);
             }
         }
-        Ok(s)
+        Ok(result.into_iter().collect())
     }
 
-    /// Parses the given token stream as a Maud expression, returning a block of
-    /// Rust code.
-    fn block(&mut self, body: TokenStream, span: Span) -> ParseResult<TokenTree> {
-        let mut builder = self.builder();
-        self.with_input(body).markups(&mut builder)?;
-        let mut group = Group::new(Delimiter::Brace, builder.build());
-        group.set_span(span);
-        Ok(TokenTree::Group(group))
+    /// Parses the given token stream as a Maud expression.
+    fn block(&mut self, body: TokenStream, span: Span) -> ParseResult<ast::Block> {
+        let markups = self.with_input(body).markups()?;
+        Ok(ast::Block { markups, span })
     }
 }