diff --git a/maud/src/lib.rs b/maud/src/lib.rs index 9c4a1809..a4d1d31a 100644 --- a/maud/src/lib.rs +++ b/maud/src/lib.rs @@ -14,7 +14,7 @@ extern crate alloc; use alloc::{borrow::Cow, boxed::Box, string::String}; use core::fmt::{self, Arguments, Display, Write}; -pub use maud_macros::html; +pub use maud_macros::{html, html_to}; mod escape; @@ -376,18 +376,42 @@ pub mod macro_private { } } + pub trait AsMutString { + fn as_mut_string(&mut self) -> &mut String; + } + impl ViaRender for &ChooseRenderOrDisplay {} impl ViaDisplay for ChooseRenderOrDisplay {} + impl AsMutString for &mut String { + fn as_mut_string(&mut self) -> &mut String { + self + } + } + + impl AsMutString for String { + fn as_mut_string(&mut self) -> &mut String { + self + } + } + impl ViaRenderTag { - pub fn render_to(self, value: &T, buffer: &mut String) { - value.render_to(buffer); + pub fn render_to(self, value: &T, mut buffer: B) + where + T: Render + ?Sized, + B: AsMutString + { + value.render_to(buffer.as_mut_string()); } } impl ViaDisplayTag { - pub fn render_to(self, value: &T, buffer: &mut String) { - display(value).render_to(buffer); + pub fn render_to(self, value: &T, mut buffer: B) + where + T: Display + ?Sized, + B: AsMutString + { + display(value).render_to(buffer.as_mut_string()); } } } diff --git a/maud/tests/html_to.rs b/maud/tests/html_to.rs new file mode 100644 index 00000000..f274bcf8 --- /dev/null +++ b/maud/tests/html_to.rs @@ -0,0 +1,62 @@ +use maud::{self, html, html_to, Render}; + +#[test] +fn html_render_to_buffer() { + let mut buf = String::new(); + + html_to! { buf, + p { "existing" } + }; + + assert_eq!(buf, "

existing

"); +} + +#[test] +fn html_buffer_reuse() { + let mut buf = String::new(); + html_to! { buf, + p { "existing" } + }; + + html_to! { buf, + p { "reused" } + }; + + assert_eq!(buf, "

existing

reused

"); +} + +#[test] +fn impl_render_to_html_to() { + struct Foo; + impl Render for Foo { + fn render_to(&self, buffer: &mut String) { + html_to! { buffer, + a { "foobar" } + } + } + } + + let rendered = html! { + p { (Foo) } + }.into_string(); + + assert_eq!(rendered, "

foobar

"); +} + +#[test] +fn impl_render_to_html_to_use_render_in_html_to() { + struct Foo; + impl Render for Foo { + fn render_to(&self, buffer: &mut String) { + html_to! { buffer, + a { (42) } + } + } + } + + let rendered = html! { + p { (Foo) } + }.into_string(); + + assert_eq!(rendered, "

42

"); +} \ No newline at end of file diff --git a/maud_macros/src/generate.rs b/maud_macros/src/generate.rs index c9ba9fb6..386f27fc 100644 --- a/maud_macros/src/generate.rs +++ b/maud_macros/src/generate.rs @@ -103,7 +103,7 @@ impl Generator { fn splice(&self, expr: TokenStream, build: &mut Builder) { let output_ident = self.output_ident.clone(); - build.push_tokens(quote!(maud::macro_private::render_to!(&#expr, &mut #output_ident);)); + build.push_tokens(quote!(maud::macro_private::render_to!(&#expr, #output_ident.as_mut_string());)); } fn element(&self, name: TokenStream, attrs: Vec, body: ElementBody, build: &mut Builder) { diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs index b1ccf2df..615f58bb 100644 --- a/maud_macros/src/lib.rs +++ b/maud_macros/src/lib.rs @@ -10,8 +10,8 @@ mod escape; mod generate; mod parse; -use proc_macro2::{Ident, Span, TokenStream, TokenTree}; -use proc_macro_error::proc_macro_error; +use proc_macro2::{TokenStream, TokenTree}; +use proc_macro_error::{proc_macro_error, abort}; use quote::quote; #[proc_macro] @@ -20,18 +20,39 @@ pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream { expand(input.into()).into() } +#[proc_macro] +#[proc_macro_error] +pub fn html_to(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + expand_to(input.into()).into() +} + fn expand(input: TokenStream) -> TokenStream { - let output_ident = TokenTree::Ident(Ident::new("__maud_output", Span::mixed_site())); + quote!({ + extern crate alloc; + extern crate maud; + let mut __maud_output = alloc::string::String::new(); + maud::html_to!(__maud_output, #input); + maud::PreEscaped(__maud_output) + }) +} + +fn expand_to(input: TokenStream) -> TokenStream { // Heuristic: the size of the resulting markup tends to correlate with the // code size of the template itself let size_hint = input.to_string().len(); - let markups = parse::parse(input); - let stmts = generate::generate(markups, output_ident.clone()); + // TODO: Better place for error handling? + let (output_ident, markups) = match parse::parse(input.clone()) { + (Some(ident), markups) => (ident, markups), + _ => abort!( + input, + "expected mutable String buffer" + ) + }; + + let stmts = generate::generate(markups, TokenTree::Ident(output_ident.clone())); quote!({ - extern crate alloc; extern crate maud; - let mut #output_ident = alloc::string::String::with_capacity(#size_hint); + #output_ident.reserve(#size_hint); #stmts - maud::PreEscaped(#output_ident) }) -} +} \ No newline at end of file diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index dee037e4..94460b19 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -6,8 +6,9 @@ use syn::Lit; use crate::ast; -pub fn parse(input: TokenStream) -> Vec { - Parser::new(input).markups() +pub fn parse(input: TokenStream) -> (Option, Vec) { + let mut parser = Parser::new(input); + (parser.buffer_ident(), parser.markups()) } #[derive(Clone)] @@ -82,6 +83,20 @@ impl Parser { result } + /// Try to parse an output buffer ident + fn buffer_ident(&mut self) -> Option { + match self.peek2() { + Some(( + TokenTree::Ident(ident), + Some(TokenTree::Punct(ref punct)), + )) if punct.as_char() == ',' => { + self.advance2(); + Some(ident) + }, + _ => None, + } + } + /// Parses a single block of markup. fn markup(&mut self) -> ast::Markup { let token = match self.peek() {