Skip to content
18 changes: 10 additions & 8 deletions maud/benches/complicated_maud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

extern crate test;

use maud::{Markup, html};
use maud::{Markup, Render, html, html_render};

#[derive(Debug)]
struct Entry {
Expand All @@ -11,7 +11,7 @@ struct Entry {
}

mod btn {
use maud::{Markup, Render, html};
use maud::{Render, html_render};

#[derive(Copy, Clone)]
pub enum RequestMethod {
Expand Down Expand Up @@ -42,24 +42,26 @@ mod btn {
}

impl Render for Button<'_> {
fn render(&self) -> Markup {
fn render_to(&self, buffer: &mut String) {
match self.req_meth {
RequestMethod::Get => {
html! { a.btn href=(self.path) { (self.label) } }
let html = html_render! { a.btn href=(self.path) { (self.label) } };
html.render_to(buffer);
}
RequestMethod::Post => {
html! {
let html = html_render! {
form method="POST" action=(self.path) {
input.btn type="submit" value=(self.label);
}
}
};
html.render_to(buffer);
}
}
}
}
}

fn layout<S: AsRef<str>>(title: S, inner: Markup) -> Markup {
fn layout<S: AsRef<str>>(title: S, inner: impl Render) -> Markup {
html! {
html {
head {
Expand Down Expand Up @@ -97,7 +99,7 @@ fn render_complicated_template(b: &mut test::Bencher) {
use crate::btn::{Button, RequestMethod};
layout(
format!("Homepage of {year}"),
html! {
html_render! {
h1 { "Hello there!" }

@for entry in &teams {
Expand Down
34 changes: 34 additions & 0 deletions maud/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,31 @@ use core::fmt::{self, Arguments, Display, Write};

pub use maud_macros::html;

/// Functionally the same as [`html`] but produces a struct that implements [`Render`].
///
/// You can
///
/// - Call `render()` to render the HTML as [`Markup`]
/// - Call `render_to()` to render the HTML to a pre-existing `&mut String`
/// - Pass this directly into an [`html`] invocation to add it as a component without allocating a new buffer.
///
/// ```
/// use maud::{html, html_render, DOCTYPE};
///
/// let component = html_render! {
/// p { "Wow, such component!" }
/// };
///
/// let full = html! {
/// (DOCTYPE)
/// head {}
/// html {
/// (component)
/// }
/// };
/// ```
pub use maud_macros::html_render;

mod escape;

#[cfg(feature = "json")]
Expand Down Expand Up @@ -454,6 +479,15 @@ pub mod macro_private {
use alloc::string::String;
use core::fmt::Display;

#[repr(transparent)]
pub struct RenderFn<F>(pub F);

impl<F: Fn(&mut String)> Render for RenderFn<F> {
fn render_to(&self, buffer: &mut String) {
(self.0)(buffer)
}
}

#[doc(hidden)]
#[macro_export]
macro_rules! render_to {
Expand Down
42 changes: 41 additions & 1 deletion maud/tests/basic_syntax.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use maud::{Markup, html};
use maud::{Markup, Render, html, html_render};

#[test]
fn literals() {
Expand Down Expand Up @@ -373,3 +373,43 @@ fn div_shorthand_id_with_attrs() {
r#"<div class="awesome-class" id="unique-id" contenteditable dir="rtl"></div>"#
);
}

#[test]
fn html_component() {
let component = html_render! {
p {
"Hi!"
span { "Such component!" }
}
};

let result = html! {
(maud::DOCTYPE)
head {}
html {
h1 { ("Such webpage!") }
main {
(component)
}
}
};

let result_as_render = html_render! {
(maud::DOCTYPE)
head {}
html {
h1 { ("Such webpage!") }
main {
(component)
}
}
}
.render();

assert_eq!(result.0, result_as_render.0);

assert_eq!(
result.into_string(),
r#"<!DOCTYPE html><head></head><html><h1>Such webpage!</h1><main><p>Hi!<span>Such component!</span></p></main></html>"#
);
}
20 changes: 15 additions & 5 deletions maud_macros/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ use syn::{Expr, Local, parse_quote, token::Brace};

use crate::{ast::*, escape};

pub fn generate(markups: Markups<Element>, output_ident: Ident) -> TokenStream {
pub fn generate(markups: Markups<Element>, output_ident: Ident, as_struct: bool) -> TokenStream {
let mut build = Builder::new(output_ident.clone());
Generator::new(output_ident).markups(markups, &mut build);
Generator::new(output_ident, as_struct).markups(markups, &mut build);
build.finish()
}

struct Generator {
output_ident: Ident,
as_struct: bool,
}

impl Generator {
fn new(output_ident: Ident) -> Generator {
Generator { output_ident }
fn new(output_ident: Ident, as_struct: bool) -> Generator {
Generator {
output_ident,
as_struct,
}
}

fn builder(&self) -> Builder {
Expand Down Expand Up @@ -66,7 +70,13 @@ impl Generator {

fn splice(&self, expr: Expr, build: &mut Builder) {
let output_ident = &self.output_ident;
build.push_tokens(quote!(maud::macro_private::render_to!(&(#expr), &mut #output_ident);));
if self.as_struct {
build.push_tokens(quote!(maud::macro_private::render_to!(&(#expr), #output_ident);));
} else {
build.push_tokens(
quote!(maud::macro_private::render_to!(&(#expr), &mut #output_ident);),
);
}
}

fn element(&self, element: Element, build: &mut Builder) {
Expand Down
42 changes: 31 additions & 11 deletions maud_macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ use syn::parse::{ParseStream, Parser};

#[proc_macro]
pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
expand(input.into()).into()
expand(input.into(), false).into()
}

fn expand(input: TokenStream) -> TokenStream {
#[proc_macro]
pub fn html_render(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
expand(input.into(), true).into()
}

fn expand(input: TokenStream, as_struct: bool) -> 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();
Expand All @@ -44,13 +49,28 @@ fn expand(input: TokenStream) -> TokenStream {
let diag_tokens = diagnostics.into_iter().map(Diagnostic::emit_as_expr_tokens);

let output_ident = Ident::new("__maud_output", Span::mixed_site());
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);
#stmts
#(#diag_tokens)*
maud::PreEscaped(#output_ident)
}}
let stmts = generate::generate(markups, output_ident.clone(), as_struct);
if as_struct {
quote! {{
extern crate alloc;
extern crate maud;

maud::macro_private::RenderFn({
|#output_ident: &mut alloc::string::String| {
#output_ident.reserve(#size_hint);
#stmts
#(#diag_tokens)*
}
})
}}
} else {
quote! {{
extern crate alloc;
extern crate maud;
let mut #output_ident = alloc::string::String::with_capacity(#size_hint);
#stmts
#(#diag_tokens)*
maud::PreEscaped(#output_ident)
}}
}
}
Loading