From d8b52cf82e1886f8e6df432ad66a3ee8be6e0b8f Mon Sep 17 00:00:00 2001 From: nikvoid <77514082+nikvoid@users.noreply.github.com> Date: Wed, 12 Apr 2023 17:09:08 +0300 Subject: [PATCH 1/2] PoC writing to existing buffer with html_to! macro --- maud/src/lib.rs | 2 +- maud/tests/html_to.rs | 44 ++++++++++++++++++++++++++++++++++++++++++ maud_macros/src/lib.rs | 43 ++++++++++++++++++++++++++++++++++------- 3 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 maud/tests/html_to.rs diff --git a/maud/src/lib.rs b/maud/src/lib.rs index 9c4a1809..6a606436 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; diff --git a/maud/tests/html_to.rs b/maud/tests/html_to.rs new file mode 100644 index 00000000..5cc1f53d --- /dev/null +++ b/maud/tests/html_to.rs @@ -0,0 +1,44 @@ +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

"); +} \ No newline at end of file diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs index b1ccf2df..b746c822 100644 --- a/maud_macros/src/lib.rs +++ b/maud_macros/src/lib.rs @@ -11,7 +11,7 @@ mod generate; mod parse; use proc_macro2::{Ident, Span, TokenStream, TokenTree}; -use proc_macro_error::proc_macro_error; +use proc_macro_error::{proc_macro_error, abort}; use quote::quote; #[proc_macro] @@ -20,18 +20,47 @@ 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 { + // TODO: Better/more beatiful way to get ident? + let mut iter = input.clone().into_iter(); + + // TODO: Better place for error handling? + let output_ident = match iter.next() { + Some(ident @ TokenTree::Ident(_)) => ident, + Some(token) => abort!( + token, + "expected mutable String buffer", + ), + None => abort!( + input, + "expected mutable String buffer" + ), + }; + // 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 markups = parse::parse(iter.collect()); let stmts = generate::generate(markups, 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 From 0a3f7a258437844873cc0992c2a6cd55e76903f4 Mon Sep 17 00:00:00 2001 From: nikvoid <77514082+nikvoid@users.noreply.github.com> Date: Wed, 12 Apr 2023 19:10:45 +0300 Subject: [PATCH 2/2] separate buffer ident from remaining tokens with comma (,), add trait to avoid taking &mut String references (fix Splicing exprs into html_to!) --- maud/src/lib.rs | 32 ++++++++++++++++++++++++++++---- maud/tests/html_to.rs | 26 ++++++++++++++++++++++---- maud_macros/src/generate.rs | 2 +- maud_macros/src/lib.rs | 30 +++++++++++------------------- maud_macros/src/parse.rs | 19 +++++++++++++++++-- 5 files changed, 79 insertions(+), 30 deletions(-) diff --git a/maud/src/lib.rs b/maud/src/lib.rs index 6a606436..a4d1d31a 100644 --- a/maud/src/lib.rs +++ b/maud/src/lib.rs @@ -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 index 5cc1f53d..f274bcf8 100644 --- a/maud/tests/html_to.rs +++ b/maud/tests/html_to.rs @@ -4,7 +4,7 @@ use maud::{self, html, html_to, Render}; fn html_render_to_buffer() { let mut buf = String::new(); - html_to! { buf + html_to! { buf, p { "existing" } }; @@ -14,11 +14,11 @@ fn html_render_to_buffer() { #[test] fn html_buffer_reuse() { let mut buf = String::new(); - html_to! { buf + html_to! { buf, p { "existing" } }; - html_to! { buf + html_to! { buf, p { "reused" } }; @@ -30,7 +30,7 @@ fn impl_render_to_html_to() { struct Foo; impl Render for Foo { fn render_to(&self, buffer: &mut String) { - html_to! { buffer + html_to! { buffer, a { "foobar" } } } @@ -41,4 +41,22 @@ fn impl_render_to_html_to() { }.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 b746c822..615f58bb 100644 --- a/maud_macros/src/lib.rs +++ b/maud_macros/src/lib.rs @@ -10,7 +10,7 @@ mod escape; mod generate; mod parse; -use proc_macro2::{Ident, Span, TokenStream, TokenTree}; +use proc_macro2::{TokenStream, TokenTree}; use proc_macro_error::{proc_macro_error, abort}; use quote::quote; @@ -31,33 +31,25 @@ fn expand(input: TokenStream) -> TokenStream { extern crate alloc; extern crate maud; let mut __maud_output = alloc::string::String::new(); - maud::html_to!(__maud_output #input); + maud::html_to!(__maud_output, #input); maud::PreEscaped(__maud_output) }) } fn expand_to(input: TokenStream) -> TokenStream { - // TODO: Better/more beatiful way to get ident? - let mut iter = input.clone().into_iter(); - - // TODO: Better place for error handling? - let output_ident = match iter.next() { - Some(ident @ TokenTree::Ident(_)) => ident, - Some(token) => abort!( - token, - "expected mutable String buffer", - ), - None => abort!( + // 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(); + // 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" - ), + ) }; - // 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(iter.collect()); - let stmts = generate::generate(markups, output_ident.clone()); + let stmts = generate::generate(markups, TokenTree::Ident(output_ident.clone())); quote!({ extern crate maud; #output_ident.reserve(#size_hint); 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() {