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 }) } }