diff --git a/maud/src/lib.rs b/maud/src/lib.rs index b012cf4..d194135 100644 --- a/maud/src/lib.rs +++ b/maud/src/lib.rs @@ -23,6 +23,17 @@ impl<T: fmt::Display + ?Sized> 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<T>(pub T); diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 713eafc..8371927 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -23,9 +23,6 @@ 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)) } @@ -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(..))) } @@ -172,19 +172,11 @@ impl<'cx, 'i> Parser<'cx, 'i> { 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 <https://github.com/lfairy/maud/issues/23> - 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()); @@ -308,9 +300,9 @@ impl<'cx, 'i> Parser<'cx, 'i> { 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<P<Expr>> { // 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<P<Expr>> { 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 8c52cae..0b2bdf5 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<Expr>) { 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 265cc00..be02f40 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, $"<pinkie>").unwrap(); + html!(s, ^"<pinkie>").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("<pinkie>")).unwrap(); + html!(s, ^PreEscaped("<pinkie>")).unwrap(); assert_eq!(s, "<pinkie>"); } @@ -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; } @@ -225,7 +225,7 @@ mod control { let mut s = String::new(); html!(s, { #if let Some(value) = input { - $value + ^value } #else { "oh noes" } @@ -240,7 +240,7 @@ mod control { let mut s = String::new(); html!(s, { ul #for pony in &ponies { - li $pony + li ^pony } }).unwrap(); assert_eq!(s, concat!( @@ -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, "<p>Hi, Lyra!</p>"); } @@ -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,55 @@ 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 "); +}