Skip to content

Commit

Permalink
Remove blanket Render impl for T: Display (#320)
Browse files Browse the repository at this point in the history
Closes #271
  • Loading branch information
lambda-fairy authored Nov 21, 2021
1 parent f6bbece commit 2f3d68c
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 58 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [Unreleased]

- Remove blanket `Render` impl for `T: Display`
[#320](https://github.com/lambda-fairy/maud/pull/320)

## [0.23.0] - 2021-11-10

- Update to support axum 0.2
Expand Down
1 change: 1 addition & 0 deletions docs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 3 additions & 9 deletions docs/content/render-trait.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
# The `Render` trait

By default,
a [`(splice)`](splices-toggles.md) is rendered using the [`std::fmt::Display`][Display] trait,
with any HTML special characters escaped automatically.
Maud uses the [`Render`][Render] trait to convert [`(spliced)`](splices-toggles.md) values to HTML.
This is implemented for many Rust primitive types (`&str`, `i32`) by default, but you can implement it for your own types as well.

To change this behavior,
implement the [`Render`][Render] trait for your type.
Then, when a value of this type is used in a template,
Maud will call your custom code instead.

Below are some examples of using `Render`.
Below are some examples of implementing `Render`.
Feel free to use these snippets in your own project!

## Example: a shorthand for including CSS stylesheets
Expand Down
5 changes: 2 additions & 3 deletions docs/content/splices-toggles.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ html! {

### What can be spliced?

You can splice any value that implements [`std::fmt::Display`][Display].
You can splice any value that implements [`Render`][Render].
Most primitive types (such as `str` and `i32`) implement this trait,
so they should work out of the box.

To change this behavior for some type,
To get this behavior for a custom type,
you can implement the [`Render`][Render] trait by hand.
The [`PreEscaped`][PreEscaped] wrapper type,
which outputs its argument without escaping,
Expand All @@ -116,7 +116,6 @@ html! {
# ;
```

[Display]: http://doc.rust-lang.org/std/fmt/trait.Display.html
[Render]: https://docs.rs/maud/*/maud/trait.Render.html
[PreEscaped]: https://docs.rs/maud/*/maud/struct.PreEscaped.html

Expand Down
1 change: 1 addition & 0 deletions maud/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ actix-web = ["actix-web-dep", "futures-util"]

[dependencies]
maud_macros = { version = "0.23.0", path = "../maud_macros" }
itoa = { version = "0.4.8", default-features = false, features = ["i128"] }
rocket = { version = ">= 0.3, < 0.5", optional = true }
futures-util = { version = "0.3.0", optional = true, default-features = false }
actix-web-dep = { package = "actix-web", version = ">= 2, < 4", optional = true, default-features = false }
Expand Down
108 changes: 67 additions & 41 deletions maud/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

extern crate alloc;

use alloc::string::String;
use core::fmt::{self, Write};
use alloc::{borrow::Cow, boxed::Box, string::String};
use core::fmt::{self, Arguments, Write};

pub use maud_macros::{html, html_debug};

Expand Down Expand Up @@ -59,16 +59,9 @@ impl<'a> fmt::Write for Escaper<'a> {

/// Represents a type that can be rendered as HTML.
///
/// If your type implements [`Display`][1], then it will implement this
/// trait automatically through a blanket impl.
///
/// [1]: https://doc.rust-lang.org/std/fmt/trait.Display.html
///
/// On the other hand, if your type has a custom HTML representation,
/// then you can implement `Render` by hand. To do this, override
/// either the `.render()` or `.render_to()` methods; since each is
/// defined in terms of the other, you only need to implement one of
/// them. See the example below.
/// To implement this for your own type, override either the `.render()`
/// or `.render_to()` methods; since each is defined in terms of the
/// other, you only need to implement one of them. See the example below.
///
/// # Minimal implementation
///
Expand Down Expand Up @@ -115,48 +108,81 @@ pub trait Render {
}
}

impl<T: fmt::Display + ?Sized> Render for T {
impl Render for str {
fn render_to(&self, w: &mut String) {
let _ = write!(Escaper::new(w), "{}", self);
escape::escape_to_string(self, w);
}
}

/// Spicy hack to specialize `Render` for `T: AsRef<str>`.
///
/// The `std::fmt` machinery is rather heavyweight, both in code size and speed.
/// It would be nice to skip this overhead for the common cases of `&str` and
/// `String`. But the obvious solution uses *specialization*, which (as of this
/// writing) requires Nightly. The [*inherent method specialization*][1] trick
/// is less clear but works on Stable.
///
/// This module is an implementation detail and should not be used directly.
///
/// [1]: https://github.com/dtolnay/case-studies/issues/14
#[doc(hidden)]
pub mod render {
use crate::{Escaper, Render};
use alloc::string::String;
use core::fmt::Write;
impl Render for String {
fn render_to(&self, w: &mut String) {
str::render_to(self, w);
}
}

pub trait RenderInternal {
fn __maud_render_to(&self, w: &mut String);
impl<'a> Render for Cow<'a, str> {
fn render_to(&self, w: &mut String) {
str::render_to(self, w);
}
}

pub struct RenderWrapper<'a, T: ?Sized>(pub &'a T);
impl<'a> Render for Arguments<'a> {
fn render_to(&self, w: &mut String) {
let _ = Escaper::new(w).write_fmt(*self);
}
}

impl<'a, T: AsRef<str> + ?Sized> RenderWrapper<'a, T> {
pub fn __maud_render_to(&self, w: &mut String) {
let _ = Escaper::new(w).write_str(self.0.as_ref());
}
impl<'a, T: Render + ?Sized> Render for &'a T {
fn render_to(&self, w: &mut String) {
T::render_to(self, w);
}
}

impl<'a, T: Render + ?Sized> RenderInternal for RenderWrapper<'a, T> {
fn __maud_render_to(&self, w: &mut String) {
self.0.render_to(w);
}
impl<'a, T: Render + ?Sized> Render for &'a mut T {
fn render_to(&self, w: &mut String) {
T::render_to(self, w);
}
}

impl<T: Render + ?Sized> Render for Box<T> {
fn render_to(&self, w: &mut String) {
T::render_to(self, w);
}
}

macro_rules! impl_render_with_display {
($($ty:ty)*) => {
$(
impl Render for $ty {
fn render_to(&self, w: &mut String) {
format_args!("{self}").render_to(w);
}
}
)*
};
}

impl_render_with_display! {
char f32 f64
}

macro_rules! impl_render_with_itoa {
($($ty:ty)*) => {
$(
impl Render for $ty {
fn render_to(&self, w: &mut String) {
let _ = itoa::fmt(w, *self);
}
}
)*
};
}

impl_render_with_itoa! {
i8 i16 i32 i64 i128 isize
u8 u16 u32 u64 u128 usize
}

/// A wrapper that renders the inner value without escaping.
#[derive(Debug, Clone, Copy)]
pub struct PreEscaped<T: AsRef<str>>(pub T);
Expand Down
6 changes: 1 addition & 5 deletions maud_macros/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,7 @@ impl Generator {

fn splice(&self, expr: TokenStream, build: &mut Builder) {
let output_ident = self.output_ident.clone();
let tokens = quote!({
use maud::render::{RenderInternal, RenderWrapper};
RenderWrapper(&#expr).__maud_render_to(&mut #output_ident);
});
build.push_tokens(tokens);
build.push_tokens(quote!(maud::Render::render_to(&#expr, &mut #output_ident);));
}

fn element(&self, name: TokenStream, attrs: Vec<Attr>, body: ElementBody, build: &mut Builder) {
Expand Down

1 comment on commit 2f3d68c

@HookedBehemoth
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this, code like this, using chrono, fails
p { (date.format("%Y-%m-%d %H:%M:%S")) }
with
the trait Renderis not implemented forDelayedFormat<StrftimeItems<'_>>``
Implicitly calling .to_string() resolves this.

Please sign in to comment.