mirror of
https://github.com/askama-rs/askama.git
synced 2025-10-02 07:20:55 +00:00
Add HTML safe types
This PR adds an `HtmlSafeMarker` trait. Types that implement this marker
are know to never generate strings containing the characters `< > & " '`,
so they don't have to be escaped.
All glory goes to \@dtolnay's ["Autoref-based stable specialization"][1]
case study / blog entry.
[1]: <0a9f083f33/autoref-specialization/README.md
>
This commit is contained in:
parent
8c856c51c1
commit
e3948ed436
@ -27,31 +27,32 @@ pub fn safe(text: impl fmt::Display, escaper: impl Escaper) -> Result<impl Displ
|
||||
/// the template's extension, like `{{ val|escape("txt") }}`.
|
||||
#[inline]
|
||||
pub fn escape(text: impl fmt::Display, escaper: impl Escaper) -> Result<impl Display, Infallible> {
|
||||
struct EscapeDisplay<T, E>(T, E);
|
||||
struct EscapeWriter<W, E>(W, E);
|
||||
|
||||
impl<T: fmt::Display, E: Escaper> fmt::Display for EscapeDisplay<T, E> {
|
||||
#[inline]
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(EscapeWriter(fmt, self.1), "{}", &self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write, E: Escaper> Write for EscapeWriter<W, E> {
|
||||
#[inline]
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.1.write_escaped_str(&mut self.0, s)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_char(&mut self, c: char) -> fmt::Result {
|
||||
self.1.write_escaped_char(&mut self.0, c)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(EscapeDisplay(text, escaper))
|
||||
}
|
||||
|
||||
pub struct EscapeDisplay<T, E>(T, E);
|
||||
|
||||
impl<T: fmt::Display, E: Escaper> fmt::Display for EscapeDisplay<T, E> {
|
||||
#[inline]
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
|
||||
struct EscapeWriter<W, E>(W, E);
|
||||
|
||||
impl<W: Write, E: Escaper> Write for EscapeWriter<W, E> {
|
||||
#[inline]
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.1.write_escaped_str(&mut self.0, s)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_char(&mut self, c: char) -> fmt::Result {
|
||||
self.1.write_escaped_char(&mut self.0, c)
|
||||
}
|
||||
}
|
||||
|
||||
write!(EscapeWriter(fmt, self.1), "{}", &self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Alias for [`escape()`]
|
||||
#[inline]
|
||||
pub fn e(text: impl fmt::Display, escaper: impl Escaper) -> Result<impl Display, Infallible> {
|
||||
@ -160,6 +161,107 @@ pub trait Escaper: Copy {
|
||||
}
|
||||
}
|
||||
|
||||
/// Used internally by rinja to select the appropriate escaper
|
||||
pub trait AutoEscape {
|
||||
type Escaped: fmt::Display;
|
||||
type Error: Into<crate::Error>;
|
||||
|
||||
fn rinja_auto_escape(&self) -> Result<Self::Escaped, Self::Error>;
|
||||
}
|
||||
|
||||
/// Used internally by rinja to select the appropriate escaper
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AutoEscaper<'a, T: fmt::Display + ?Sized, E: Escaper> {
|
||||
text: &'a T,
|
||||
escaper: E,
|
||||
}
|
||||
|
||||
impl<'a, T: fmt::Display + ?Sized, E: Escaper> AutoEscaper<'a, T, E> {
|
||||
#[inline]
|
||||
pub fn new(text: &'a T, escaper: E) -> Self {
|
||||
Self { text, escaper }
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the provided escaper
|
||||
impl<'a, T: fmt::Display + ?Sized, E: Escaper> AutoEscape for &&AutoEscaper<'a, T, E> {
|
||||
type Escaped = EscapeDisplay<&'a T, E>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn rinja_auto_escape(&self) -> Result<Self::Escaped, Self::Error> {
|
||||
Ok(EscapeDisplay(self.text, self.escaper))
|
||||
}
|
||||
}
|
||||
|
||||
/// Types that implement this marker trait don't need to be HTML escaped
|
||||
///
|
||||
/// Please note that this trait is only meant as speed-up helper. In some odd circumcises rinja
|
||||
/// might still decide to HTML escape the input, so if this must not happen, then you need to use
|
||||
/// the [`|safe`](super::safe) filter to prevent the auto escaping.
|
||||
///
|
||||
/// If you are unsure if your type generates HTML safe output in all cases, then DON'T mark it.
|
||||
/// Better safe than sorry!
|
||||
pub trait HtmlSafeMarker: fmt::Display {}
|
||||
|
||||
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for &T {}
|
||||
|
||||
/// Don't escape HTML safe types
|
||||
impl<'a, T: HtmlSafeMarker + ?Sized> AutoEscape for &AutoEscaper<'a, T, Html> {
|
||||
type Escaped = &'a T;
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn rinja_auto_escape(&self) -> Result<Self::Escaped, Self::Error> {
|
||||
Ok(self.text)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! mark_html_safe {
|
||||
($($ty:ty),* $(,)?) => {$(
|
||||
impl HtmlSafeMarker for $ty {}
|
||||
)*};
|
||||
}
|
||||
|
||||
mark_html_safe! {
|
||||
bool,
|
||||
f32, f64,
|
||||
i8, i16, i32, i64, i128, isize,
|
||||
u8, u16, u32, u64, u128, usize,
|
||||
std::num::NonZeroI8, std::num::NonZeroI16, std::num::NonZeroI32,
|
||||
std::num::NonZeroI64, std::num::NonZeroI128, std::num::NonZeroIsize,
|
||||
std::num::NonZeroU8, std::num::NonZeroU16, std::num::NonZeroU32,
|
||||
std::num::NonZeroU64, std::num::NonZeroU128, std::num::NonZeroUsize,
|
||||
}
|
||||
|
||||
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for Box<T> {}
|
||||
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for std::cell::Ref<'_, T> {}
|
||||
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for std::cell::RefMut<'_, T> {}
|
||||
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for std::rc::Rc<T> {}
|
||||
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for std::sync::Arc<T> {}
|
||||
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for std::sync::MutexGuard<'_, T> {}
|
||||
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for std::sync::RwLockReadGuard<'_, T> {}
|
||||
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for std::sync::RwLockWriteGuard<'_, T> {}
|
||||
impl<T: HtmlSafeMarker> HtmlSafeMarker for std::num::Wrapping<T> {}
|
||||
|
||||
impl<T> HtmlSafeMarker for std::borrow::Cow<'_, T>
|
||||
where
|
||||
T: HtmlSafeMarker + std::borrow::ToOwned + ?Sized,
|
||||
T::Owned: HtmlSafeMarker,
|
||||
{
|
||||
}
|
||||
|
||||
/// Texts are always safe
|
||||
impl<'a, T: fmt::Display + ?Sized> AutoEscape for &AutoEscaper<'a, T, Text> {
|
||||
type Escaped = &'a T;
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn rinja_auto_escape(&self) -> Result<Self::Escaped, Self::Error> {
|
||||
Ok(self.text)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape() {
|
||||
assert_eq!(escape("", Html).unwrap().to_string(), "");
|
||||
@ -174,3 +276,53 @@ fn test_escape() {
|
||||
assert_eq!(escape("<foo", Text).unwrap().to_string(), "<foo");
|
||||
assert_eq!(escape("bla&h", Text).unwrap().to_string(), "bla&h");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_html_safe_marker() {
|
||||
struct Script1;
|
||||
struct Script2;
|
||||
|
||||
impl fmt::Display for Script1 {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("<script>")
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Script2 {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("<script>")
|
||||
}
|
||||
}
|
||||
|
||||
impl HtmlSafeMarker for Script2 {}
|
||||
|
||||
assert_eq!(
|
||||
(&&AutoEscaper::new(&Script1, Html))
|
||||
.rinja_auto_escape()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"<script>",
|
||||
);
|
||||
assert_eq!(
|
||||
(&&AutoEscaper::new(&Script2, Html))
|
||||
.rinja_auto_escape()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"<script>",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
(&&AutoEscaper::new(&Script1, Text))
|
||||
.rinja_auto_escape()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"<script>",
|
||||
);
|
||||
assert_eq!(
|
||||
(&&AutoEscaper::new(&Script2, Text))
|
||||
.rinja_auto_escape()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"<script>",
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ use std::cell::Cell;
|
||||
use std::convert::Infallible;
|
||||
use std::fmt::{self, Write};
|
||||
|
||||
pub use escape::{e, escape, safe, Escaper, Html, Text};
|
||||
pub use escape::{e, escape, safe, AutoEscape, AutoEscaper, Escaper, Html, HtmlSafeMarker, Text};
|
||||
#[cfg(feature = "humansize")]
|
||||
use humansize::{ISizeFormatter, ToF64, DECIMAL};
|
||||
#[cfg(feature = "serde_json")]
|
||||
|
@ -87,9 +87,11 @@ impl<'a> Generator<'a> {
|
||||
// Implement `Template` for the given context struct.
|
||||
fn impl_template(&mut self, ctx: &Context<'a>, buf: &mut Buffer) -> Result<(), CompileError> {
|
||||
self.write_header(buf, format_args!("{CRATE}::Template"), None);
|
||||
buf.write("fn render_into(&self, writer: &mut (impl ::std::fmt::Write + ?Sized)) -> ");
|
||||
buf.write(CRATE);
|
||||
buf.writeln("::Result<()> {");
|
||||
buf.writeln(format_args!(
|
||||
"fn render_into(&self, writer: &mut (impl ::std::fmt::Write + ?Sized)) \
|
||||
-> {CRATE}::Result<()> {{",
|
||||
));
|
||||
buf.writeln(format_args!("use {CRATE}::filters::AutoEscape as _;"));
|
||||
|
||||
buf.discard = self.buf_writable.discard;
|
||||
// Make sure the compiler understands that the generated code depends on the template files.
|
||||
@ -1166,7 +1168,7 @@ impl<'a> Generator<'a> {
|
||||
let expression = match wrapped {
|
||||
DisplayWrap::Wrapped => expr,
|
||||
DisplayWrap::Unwrapped => format!(
|
||||
"{CRATE}::filters::escape(&({expr}), {})?",
|
||||
"(&&{CRATE}::filters::AutoEscaper::new(&({expr}), {})).rinja_auto_escape()?",
|
||||
self.input.escaper,
|
||||
),
|
||||
};
|
||||
|
@ -27,6 +27,7 @@ struct Foo;"##
|
||||
let expected = format!(
|
||||
r#"impl ::rinja::Template for Foo {{
|
||||
fn render_into(&self, writer: &mut (impl ::std::fmt::Write + ?Sized)) -> ::rinja::Result<()> {{
|
||||
use ::rinja::filters::AutoEscape as _;
|
||||
{new_expected}
|
||||
::rinja::Result::Ok(())
|
||||
}}
|
||||
@ -58,7 +59,7 @@ impl ::std::fmt::Display for Foo {{
|
||||
::std::write!(
|
||||
writer,
|
||||
"{expr0}",
|
||||
expr0 = &::rinja::filters::escape(&(query), ::rinja::filters::Text)?,
|
||||
expr0 = &(&&::rinja::filters::AutoEscaper::new(&(query), ::rinja::filters::Text)).rinja_auto_escape()?,
|
||||
)?;
|
||||
}"#,
|
||||
);
|
||||
@ -71,7 +72,7 @@ impl ::std::fmt::Display for Foo {{
|
||||
::std::write!(
|
||||
writer,
|
||||
"{expr0}",
|
||||
expr0 = &::rinja::filters::escape(&(s), ::rinja::filters::Text)?,
|
||||
expr0 = &(&&::rinja::filters::AutoEscaper::new(&(s), ::rinja::filters::Text)).rinja_auto_escape()?,
|
||||
)?;
|
||||
}"#,
|
||||
);
|
||||
@ -84,7 +85,7 @@ impl ::std::fmt::Display for Foo {{
|
||||
::std::write!(
|
||||
writer,
|
||||
"{expr0}",
|
||||
expr0 = &::rinja::filters::escape(&(s), ::rinja::filters::Text)?,
|
||||
expr0 = &(&&::rinja::filters::AutoEscaper::new(&(s), ::rinja::filters::Text)).rinja_auto_escape()?,
|
||||
)?;
|
||||
}"#,
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user