Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove blanket Render impl for T: Display #320

Merged
merged 6 commits into from
Nov 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -58,16 +58,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 @@ -114,48 +107,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