diff --git a/maud/src/lib.rs b/maud/src/lib.rs index b012cf42..d194135b 100644 --- a/maud/src/lib.rs +++ b/maud/src/lib.rs @@ -23,6 +23,17 @@ impl Render for T { } } +/// Represents a type that can be rendered as HTML just once. +pub trait RenderOnce { + fn render_once(self, &mut fmt::Write) -> fmt::Result; +} + +impl<'a, T: Render + ?Sized> RenderOnce for &'a T { + fn render_once(self, w: &mut fmt::Write) -> fmt::Result { + Render::render(self, w) + } +} + /// A wrapper that renders the inner value without escaping. #[derive(Debug)] pub struct PreEscaped(pub T); diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 713eafc3..6e44ac26 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -23,11 +23,8 @@ macro_rules! parse_error { ($self_:expr, $sp:expr, $msg:expr) => (error!($self_.render.cx, $sp, $msg)) } -macro_rules! dollar { - () => (TokenTree::Token(_, Token::Dollar)) -} -macro_rules! pound { - () => (TokenTree::Token(_, Token::Pound)) +macro_rules! at { + () => (TokenTree::Token(_, Token::At)) } macro_rules! dot { () => (TokenTree::Token(_, Token::Dot)) @@ -53,6 +50,9 @@ macro_rules! minus { macro_rules! slash { () => (TokenTree::Token(_, Token::BinOp(BinOpToken::Slash))) } +macro_rules! caret { + () => (TokenTree::Token(_, Token::BinOp(BinOpToken::Caret))) +} macro_rules! literal { () => (TokenTree::Token(_, Token::Literal(..))) } @@ -156,35 +156,27 @@ impl<'cx, 'i> Parser<'cx, 'i> { try!(self.literal(tt, false)) }, // If - [pound!(), keyword!(sp, k), ..] if k.is_keyword(Keyword::If) => { + [at!(), keyword!(sp, k), ..] if k.is_keyword(Keyword::If) => { self.shift(2); try!(self.if_expr(sp)); }, // For - [pound!(), keyword!(sp, k), ..] if k.is_keyword(Keyword::For) => { + [at!(), keyword!(sp, k), ..] if k.is_keyword(Keyword::For) => { self.shift(2); try!(self.for_expr(sp)); }, // Call - [pound!(), ident!(sp, name), ..] if name.name.as_str() == "call" => { + [at!(), ident!(sp, name), ..] if name.name.as_str() == "call" => { self.shift(2); let func = try!(self.splice(sp)); self.render.emit_call(func); }, // Splice - [ref tt @ dollar!(), ..] => { + [ref tt @ caret!(), ..] => { self.shift(1); let expr = try!(self.splice(tt.get_span())); self.render.splice(expr); }, - [substnt!(sp, ident), ..] => { - self.shift(1); - // Parse `SubstNt` as `[Dollar, Ident]` - // See - let prefix = TokenTree::Token(sp, Token::Ident(ident, IdentStyle::Plain)); - let expr = try!(self.splice_with_prefix(prefix)); - self.render.splice(expr); - }, // Element [ident!(sp, _), ..] => { let name = try!(self.name()); @@ -222,9 +214,9 @@ impl<'cx, 'i> Parser<'cx, 'i> { Ok(()) } - /// Parses and renders an `#if` expression. + /// Parses and renders an `@if` expression. /// - /// The leading `#if` should already be consumed. + /// The leading `@if` should already be consumed. fn if_expr(&mut self, sp: Span) -> PResult<()> { // Parse the initial if let mut if_cond = vec![]; @@ -239,11 +231,11 @@ impl<'cx, 'i> Parser<'cx, 'i> { self.shift(1); if_cond.push(tt.clone()); }, - [] => parse_error!(self, sp, "expected body for this #if"), + [] => parse_error!(self, sp, "expected body for this @if"), }} - // Parse the (optional) else + // Parse the (optional) @else let else_body = match self.input { - [pound!(), keyword!(_, k), ..] if k.is_keyword(Keyword::Else) => { + [at!(), keyword!(_, k), ..] if k.is_keyword(Keyword::Else) => { self.shift(2); match self.input { [keyword!(sp, k), ..] if k.is_keyword(Keyword::If) => { @@ -263,7 +255,7 @@ impl<'cx, 'i> Parser<'cx, 'i> { self.shift(1); Some(try!(self.block(sp, &d.tts))) }, - _ => parse_error!(self, sp, "expected body for this #else"), + _ => parse_error!(self, sp, "expected body for this @else"), } }, _ => None, @@ -272,9 +264,9 @@ impl<'cx, 'i> Parser<'cx, 'i> { Ok(()) } - /// Parses and renders a `#for` expression. + /// Parses and renders a `@for` expression. /// - /// The leading `#for` should already be consumed. + /// The leading `@for` should already be consumed. fn for_expr(&mut self, sp: Span) -> PResult<()> { let mut pattern = vec![]; loop { match self.input { @@ -286,7 +278,7 @@ impl<'cx, 'i> Parser<'cx, 'i> { self.shift(1); pattern.push(tt.clone()); }, - _ => parse_error!(self, sp, "invalid #for"), + _ => parse_error!(self, sp, "invalid @for"), }} let pattern = try!(self.with_rust_parser(pattern, RustParser::parse_pat)); let mut iterable = vec![]; @@ -301,16 +293,16 @@ impl<'cx, 'i> Parser<'cx, 'i> { self.shift(1); iterable.push(tt.clone()); }, - _ => parse_error!(self, sp, "invalid #for"), + _ => parse_error!(self, sp, "invalid @for"), }} let iterable = try!(self.with_rust_parser(iterable, RustParser::parse_expr)); self.render.emit_for(pattern, iterable, body); Ok(()) } - /// Parses and renders a `$splice`. + /// Parses and renders a `^splice`. /// - /// The leading `$` should already be consumed. + /// The leading `^` should already be consumed. fn splice(&mut self, sp: Span) -> PResult> { // First, munch a single token tree let prefix = match self.input { @@ -323,24 +315,24 @@ impl<'cx, 'i> Parser<'cx, 'i> { self.splice_with_prefix(prefix) } - /// Parses and renders a `$splice`, given a prefix that we've already + /// Parses and renders a `^splice`, given a prefix that we've already /// consumed. fn splice_with_prefix(&mut self, prefix: TokenTree) -> PResult> { let mut tts = vec![prefix]; loop { match self.input { - // Munch attribute lookups e.g. `$person.address.street` + // Munch attribute lookups e.g. `^person.address.street` [ref dot @ dot!(), ref ident @ ident!(_, _), ..] => { self.shift(2); tts.push(dot.clone()); tts.push(ident.clone()); }, - // Munch tuple attribute lookups e.g. `$person.1.2` + // Munch tuple attribute lookups e.g. `^person.1.2` [ref dot @ dot!(), ref num @ integer!(), ..] => { self.shift(2); tts.push(dot.clone()); tts.push(num.clone()); }, - // Munch path lookups e.g. `$some_mod::Struct` + // Munch path lookups e.g. `^some_mod::Struct` [ref sep @ modsep!(), ref ident @ ident!(_, _), ..] => { self.shift(2); tts.push(sep.clone()); diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs index 8c52caea..0b2bdf57 100644 --- a/maud_macros/src/render.rs +++ b/maud_macros/src/render.rs @@ -118,7 +118,7 @@ impl<'cx> Renderer<'cx> { /// Appends the result of an expression, with the specified escaping method. pub fn splice(&mut self, expr: P) { let w = self.writer; - let expr = quote_expr!(self.cx, ::maud::Render::render(&$expr, &mut *$w)); + let expr = quote_expr!(self.cx, { use ::maud::RenderOnce; $expr.render_once(&mut *$w) }); let stmt = self.wrap_try(expr); self.push(stmt); } diff --git a/maud_macros/tests/tests.rs b/maud_macros/tests/tests.rs index 265cc004..f1667dfc 100644 --- a/maud_macros/tests/tests.rs +++ b/maud_macros/tests/tests.rs @@ -93,7 +93,7 @@ mod splices { #[test] fn literals() { let mut s = String::new(); - html!(s, $"").unwrap(); + html!(s, ^"").unwrap(); assert_eq!(s, "<pinkie>"); } @@ -101,7 +101,7 @@ mod splices { fn raw_literals() { use maud::PreEscaped; let mut s = String::new(); - html!(s, $PreEscaped("")).unwrap(); + html!(s, ^PreEscaped("")).unwrap(); assert_eq!(s, ""); } @@ -109,7 +109,7 @@ mod splices { fn blocks() { let mut s = String::new(); html!(s, { - ${ + ^{ let mut result = 1i32; for i in 2..11 { result *= i; @@ -142,7 +142,7 @@ mod splices { #[test] fn statics() { let mut s = String::new(); - html!(s, $BEST_PONY).unwrap(); + html!(s, ^BEST_PONY).unwrap(); assert_eq!(s, "Pinkie Pie"); } @@ -150,7 +150,7 @@ mod splices { fn closures() { let best_pony = "Pinkie Pie"; let mut s = String::new(); - html!(s, $best_pony).unwrap(); + html!(s, ^best_pony).unwrap(); assert_eq!(s, "Pinkie Pie"); } @@ -177,7 +177,7 @@ mod splices { }; let mut s = String::new(); html!(s, { - "Name: " $pinkie.name ". Rating: " $pinkie.repugnance() + "Name: " ^pinkie.name ". Rating: " ^pinkie.repugnance() }).unwrap(); assert_eq!(s, "Name: Pinkie Pie. Rating: 1"); } @@ -186,7 +186,7 @@ mod splices { fn nested_macro_invocation() { let best_pony = "Pinkie Pie"; let mut s = String::new(); - html!(s, $(format!("{}", best_pony))).unwrap(); + html!(s, ^(format!("{}", best_pony))).unwrap(); assert_eq!(s, "Pinkie Pie"); } } @@ -195,7 +195,7 @@ mod splices { fn issue_13() { let owned = String::from("yay"); let mut s = String::new(); - html!(s, $owned).unwrap(); + html!(s, ^owned).unwrap(); let _ = owned; } @@ -205,13 +205,13 @@ mod control { for (number, &name) in (1..4).zip(["one", "two", "three"].iter()) { let mut s = String::new(); html!(s, { - #if number == 1 { + @if number == 1 { "one" - } #else if number == 2 { + } @else if number == 2 { "two" - } #else if number == 3 { + } @else if number == 3 { "three" - } #else { + } @else { "oh noes" } }).unwrap(); @@ -224,9 +224,9 @@ mod control { for &(input, output) in [(Some("yay"), "yay"), (None, "oh noes")].iter() { let mut s = String::new(); html!(s, { - #if let Some(value) = input { - $value - } #else { + @if let Some(value) = input { + ^value + } @else { "oh noes" } }).unwrap(); @@ -239,8 +239,8 @@ mod control { let ponies = ["Apple Bloom", "Scootaloo", "Sweetie Belle"]; let mut s = String::new(); html!(s, { - ul #for pony in &ponies { - li $pony + ul @for pony in &ponies { + li ^pony } }).unwrap(); assert_eq!(s, concat!( @@ -288,9 +288,9 @@ fn call() { panic!("oh noes") }; html!(s, { - #call ducks - #call (|w: &mut fmt::Write| write!(w, "Geese")) - #call swans(true) + @call ducks + @call (|w: &mut fmt::Write| write!(w, "Geese")) + @call swans(true) }).unwrap(); assert_eq!(s, "DucksGeeseSwans"); } @@ -306,7 +306,7 @@ fn issue_23() { } let name = "Lyra"; - let s = to_string!(p { "Hi, " $name "!" }); + let s = to_string!(p { "Hi, " ^name "!" }); assert_eq!(s, "

Hi, Lyra!

"); } @@ -314,7 +314,7 @@ fn issue_23() { fn tuple_accessors() { let mut s = String::new(); let a = ("ducks", "geese"); - html!(s, { $a.0 }).unwrap(); + html!(s, { ^a.0 }).unwrap(); assert_eq!(s, "ducks"); } @@ -327,6 +327,127 @@ fn splice_with_path() { } let mut s = String::new(); - html!(s, $inner::name()).unwrap(); + html!(s, ^inner::name()).unwrap(); assert_eq!(s, "Maud"); } + +#[test] +fn multirender() { + struct R<'a>(&'a str); + impl<'a> maud::Render for R<'a> { + fn render(&self, w: &mut std::fmt::Write) -> std::fmt::Result { + w.write_str(self.0) + } + } + + let mut s = String::new(); + let r = R("pinkie "); + html!(s, ^r).unwrap(); + html!(s, ^r).unwrap(); + // R is not-Copyable so this shows that it will auto-ref splice arguments that implement Render. + assert_eq!(s, "pinkie pinkie "); +} + +#[test] +fn render_once_by_move() { + struct Once<'a>(&'a str); + impl<'a> maud::RenderOnce for Once<'a> { + fn render_once(self, w: &mut std::fmt::Write) -> std::fmt::Result { + w.write_str(self.0) + } + } + + let mut s = String::new(); + let once = Once("pinkie"); + html!(s, ^once).unwrap(); + assert_eq!(s, "pinkie"); +} + +#[test] +fn render_once_by_move_with_copy() { + #[derive(Clone, Copy)] + struct Once<'a>(&'a str); + impl<'a> maud::RenderOnce for Once<'a> { + fn render_once(self, w: &mut std::fmt::Write) -> std::fmt::Result { + w.write_str(self.0) + } + } + + let mut s = String::new(); + let once = Once("pinkie "); + html!(s, ^once).unwrap(); + html!(s, ^once).unwrap(); + assert_eq!(s, "pinkie pinkie "); +} + +#[test] +fn issue_26() { + macro_rules! to_string { + ($($x:tt)*) => {{ + let mut s = String::new(); + html!(s, $($x)*).unwrap(); + s + }} + } + + let name = "Lyra"; + let s = to_string!(p { "Hi, " ^(name) "!" }); + assert_eq!(s, "

Hi, Lyra!

"); +} + +#[test] +fn issue_26_2() { + macro_rules! to_string { + ($($x:tt)*) => {{ + let mut s = String::new(); + html!(s, $($x)*).unwrap(); + s + }} + } + + let name = "Lyra"; + let s = to_string!(p { "Hi, " ^("person called ".to_string() + name) "!" }); + assert_eq!(s, "

Hi, person called Lyra!

"); +} + +#[test] +fn issue_26_3() { + macro_rules! to_string { + ($($x:tt)*) => {{ + let mut s = String::new(); + html!(s, $($x)*).unwrap(); + s + }} + } + + let name = "Lyra"; + let s = to_string!(p { "Hi, " ^{"person called ".to_string() + name} "!" }); + assert_eq!(s, "

Hi, person called Lyra!

"); +} + +#[test] +fn issue_21() { + macro_rules! greet { + () => ({ + let mut result = String::new(); + let name = "Pinkie Pie"; + html!(result, p { "Hello, " ^name "!" }).map(|()| result) + }) + } + + let s = greet!().unwrap(); + assert_eq!(s, "

Hello, Pinkie Pie!

"); +} + +#[test] +fn issue_21_2() { + macro_rules! greet { + ($name:expr) => ({ + let mut result = String::new(); + html!(result, p { "Hello, " ^$name "!" }).map(|()| result) + }) + } + + let s = greet!("Pinkie Pie").unwrap(); + assert_eq!(s, "

Hello, Pinkie Pie!

"); +}