Skip to content

Commit

Permalink
Simplify |escape: make it a "normal" filter
Browse files Browse the repository at this point in the history
  • Loading branch information
Kijewski committed Jul 6, 2024
1 parent 65ff402 commit 0aead15
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 211 deletions.
12 changes: 6 additions & 6 deletions rinja/benches/escape.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use criterion::{criterion_group, criterion_main, Criterion};
use rinja::{Html, MarkupDisplay};
use rinja::filters::{escape, Html};

criterion_main!(benches);
criterion_group!(benches, functions);
Expand Down Expand Up @@ -65,10 +65,10 @@ quis lacus at, gravida maximus elit. Duis tristique, nisl nullam.
"#;

b.iter(|| {
format!("{}", MarkupDisplay::new_unsafe(string_long, Html));
format!("{}", MarkupDisplay::new_unsafe(string_short, Html));
format!("{}", MarkupDisplay::new_unsafe(empty, Html));
format!("{}", MarkupDisplay::new_unsafe(no_escape, Html));
format!("{}", MarkupDisplay::new_unsafe(no_escape_long, Html));
format!("{}", escape(string_long, Html).unwrap());
format!("{}", escape(string_short, Html).unwrap());
format!("{}", escape(empty, Html).unwrap());
format!("{}", escape(no_escape, Html).unwrap());
format!("{}", escape(no_escape_long, Html).unwrap());
});
}
11 changes: 3 additions & 8 deletions rinja/benches/to-json.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use criterion::{criterion_group, criterion_main, Criterion};
use rinja::filters::{json, json_pretty};
use rinja::{Html, MarkupDisplay};
use rinja::filters::{escape, json, json_pretty, Html};

criterion_main!(benches);
criterion_group!(benches, functions);
Expand All @@ -9,7 +8,6 @@ fn functions(c: &mut Criterion) {
c.bench_function("escape JSON", escape_json);
c.bench_function("escape JSON (pretty)", escape_json_pretty);
c.bench_function("escape JSON for HTML", escape_json_for_html);
c.bench_function("escape JSON for HTML (pretty)", escape_json_for_html);
c.bench_function("escape JSON for HTML (pretty)", escape_json_for_html_pretty);
}

Expand All @@ -32,18 +30,15 @@ fn escape_json_pretty(b: &mut criterion::Bencher<'_>) {
fn escape_json_for_html(b: &mut criterion::Bencher<'_>) {
b.iter(|| {
for &s in STRINGS {
format!("{}", MarkupDisplay::new_unsafe(json(s).unwrap(), Html));
format!("{}", escape(json(s).unwrap(), Html).unwrap());
}
});
}

fn escape_json_for_html_pretty(b: &mut criterion::Bencher<'_>) {
b.iter(|| {
for &s in STRINGS {
format!(
"{}",
MarkupDisplay::new_unsafe(json_pretty(s, 2).unwrap(), Html),
);
format!("{}", escape(json_pretty(s, 2).unwrap(), Html).unwrap(),);
}
});
}
Expand Down
217 changes: 93 additions & 124 deletions rinja/src/filters/escape.rs
Original file line number Diff line number Diff line change
@@ -1,112 +1,74 @@
use core::fmt::{self, Display, Formatter, Write};
use core::str;

#[derive(Debug)]
pub struct MarkupDisplay<E, T>
where
E: Escaper,
T: Display,
{
value: DisplayValue<T>,
escaper: E,
use std::convert::Infallible;
use std::fmt::{self, Display, Formatter, Write};
use std::str;

/// Marks a string (or other `Display` type) as safe
///
/// Use this is you want to allow markup in an expression, or if you know
/// that the expression's contents don't need to be escaped.
///
/// Rinja will automatically insert the first (`Escaper`) argument,
/// so this filter only takes a single argument of any type that implements
/// `Display`.
#[inline]
pub fn safe(text: impl fmt::Display, escaper: impl Escaper) -> Result<impl Display, Infallible> {
let _ = escaper; // it should not be part of the interface that the `escaper` is unused
Ok(text)
}

impl<E, T> MarkupDisplay<E, T>
where
E: Escaper,
T: Display,
{
pub fn new_unsafe(value: T, escaper: E) -> Self {
Self {
value: DisplayValue::Unsafe(value),
escaper,
/// Escapes strings according to the escape mode.
///
/// Rinja will automatically insert the first (`Escaper`) argument,
/// so this filter only takes a single argument of any type that implements
/// `Display`.
///
/// It is possible to optionally specify an escaper other than the default for
/// 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)
}
}

pub fn new_safe(value: T, escaper: E) -> Self {
Self {
value: DisplayValue::Safe(value),
escaper,
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)
}
}

#[must_use]
pub fn mark_safe(mut self) -> MarkupDisplay<E, T> {
self.value = match self.value {
DisplayValue::Unsafe(t) => DisplayValue::Safe(t),
_ => self.value,
};
self
}
}

impl<E, T> Display for MarkupDisplay<E, T>
where
E: Escaper,
T: Display,
{
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
match self.value {
DisplayValue::Unsafe(ref t) => write!(
EscapeWriter {
fmt,
escaper: &self.escaper
},
"{t}"
),
DisplayValue::Safe(ref t) => t.fmt(fmt),
#[inline]
fn write_char(&mut self, c: char) -> fmt::Result {
self.1.write_escaped_char(&mut self.0, c)
}
}
}

#[derive(Debug)]
pub struct EscapeWriter<'a, E, W> {
fmt: W,
escaper: &'a E,
}

impl<E, W> Write for EscapeWriter<'_, E, W>
where
W: Write,
E: Escaper,
{
fn write_str(&mut self, s: &str) -> fmt::Result {
self.escaper.write_escaped(&mut self.fmt, s)
}
}

pub fn escape<E>(string: &str, escaper: E) -> Escaped<'_, E>
where
E: Escaper,
{
Escaped { string, escaper }
Ok(EscapeDisplay(text, escaper))
}

#[derive(Debug)]
pub struct Escaped<'a, E>
where
E: Escaper,
{
string: &'a str,
escaper: E,
}

impl<E> Display for Escaped<'_, E>
where
E: Escaper,
{
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
self.escaper.write_escaped(fmt, self.string)
}
/// Alias for [`escape()`]
#[inline]
pub fn e(text: impl fmt::Display, escaper: impl Escaper) -> Result<impl Display, Infallible> {
escape(text, escaper)
}

/// Escape characters in a safe way for HTML texts and attributes
///
/// * `<` => `&lt;`
/// * `>` => `&gt;`
/// * `&` => `&amp;`
/// * `"` => `&quot;`
/// * `'` => `&#x27;`
#[derive(Debug, Clone, Copy, Default)]
pub struct Html;

impl Escaper for Html {
fn write_escaped<W>(&self, mut fmt: W, string: &str) -> fmt::Result
where
W: Write,
{
fn write_escaped_str<W: Write>(&self, mut fmt: W, string: &str) -> fmt::Result {
let mut last = 0;
for (index, byte) in string.bytes().enumerate() {
const MIN_CHAR: u8 = b'"';
Expand All @@ -133,48 +95,55 @@ impl Escaper for Html {
}
fmt.write_str(&string[last..])
}

fn write_escaped_char<W: Write>(&self, mut fmt: W, c: char) -> fmt::Result {
fmt.write_str(match (c.is_ascii(), c as u8) {
(true, b'<') => "&lt;",
(true, b'>') => "&gt;",
(true, b'&') => "&amp;",
(true, b'"') => "&quot;",
(true, b'\'') => "&#x27;",
_ => return fmt.write_char(c),
})
}
}

/// Don't escape the input but return in verbatim
#[derive(Debug, Clone, Copy, Default)]
pub struct Text;

impl Escaper for Text {
fn write_escaped<W>(&self, mut fmt: W, string: &str) -> fmt::Result
where
W: Write,
{
#[inline]
fn write_escaped_str<W: Write>(&self, mut fmt: W, string: &str) -> fmt::Result {
fmt.write_str(string)
}
}

#[derive(Debug, PartialEq)]
enum DisplayValue<T>
where
T: Display,
{
Safe(T),
Unsafe(T),
}

pub trait Escaper {
fn write_escaped<W>(&self, fmt: W, string: &str) -> fmt::Result
where
W: Write;
#[inline]
fn write_escaped_char<W: Write>(&self, mut fmt: W, c: char) -> fmt::Result {
fmt.write_char(c)
}
}

#[cfg(test)]
mod tests {
extern crate std;

use std::string::ToString;

use super::*;
pub trait Escaper: Copy {
fn write_escaped_str<W: Write>(&self, fmt: W, string: &str) -> fmt::Result;

#[test]
fn test_escape() {
assert_eq!(escape("", Html).to_string(), "");
assert_eq!(escape("<&>", Html).to_string(), "&lt;&amp;&gt;");
assert_eq!(escape("bla&", Html).to_string(), "bla&amp;");
assert_eq!(escape("<foo", Html).to_string(), "&lt;foo");
assert_eq!(escape("bla&h", Html).to_string(), "bla&amp;h");
#[inline]
fn write_escaped_char<W: Write>(&self, fmt: W, c: char) -> fmt::Result {
self.write_escaped_str(fmt, c.encode_utf8(&mut [0; 4]))
}
}

#[test]
fn test_escape() {
assert_eq!(escape("", Html).unwrap().to_string(), "");
assert_eq!(escape("<&>", Html).unwrap().to_string(), "&lt;&amp;&gt;");
assert_eq!(escape("bla&", Html).unwrap().to_string(), "bla&amp;");
assert_eq!(escape("<foo", Html).unwrap().to_string(), "&lt;foo");
assert_eq!(escape("bla&h", Html).unwrap().to_string(), "bla&amp;h");

assert_eq!(escape("", Text).unwrap().to_string(), "");
assert_eq!(escape("<&>", Text).unwrap().to_string(), "<&>");
assert_eq!(escape("bla&", Text).unwrap().to_string(), "bla&");
assert_eq!(escape("<foo", Text).unwrap().to_string(), "<foo");
assert_eq!(escape("bla&h", Text).unwrap().to_string(), "bla&h");
}
48 changes: 2 additions & 46 deletions rinja/src/filters/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
//! Contains all the built-in filter functions for use in templates.
//! You can define your own filters, as well.

pub mod escape;
mod escape;
#[cfg(feature = "serde_json")]
mod json;

use std::cell::Cell;
use std::convert::Infallible;
use std::fmt::{self, Write};

use escape::{Escaper, MarkupDisplay};
pub use escape::{e, escape, safe, Escaper, Html, Text};
#[cfg(feature = "humansize")]
use humansize::{ISizeFormatter, ToF64, DECIMAL};
#[cfg(feature = "serde_json")]
Expand Down Expand Up @@ -40,50 +40,6 @@ const URLENCODE_SET: &AsciiSet = &URLENCODE_STRICT_SET.remove(b'/');
// MAX_LEN is maximum allowed length for filters.
const MAX_LEN: usize = 10_000;

/// Marks a string (or other `Display` type) as safe
///
/// Use this is you want to allow markup in an expression, or if you know
/// that the expression's contents don't need to be escaped.
///
/// Rinja will automatically insert the first (`Escaper`) argument,
/// so this filter only takes a single argument of any type that implements
/// `Display`.
#[inline]
pub fn safe<E, T>(e: E, v: T) -> Result<MarkupDisplay<E, T>, Infallible>
where
E: Escaper,
T: fmt::Display,
{
Ok(MarkupDisplay::new_safe(v, e))
}

/// Escapes strings according to the escape mode.
///
/// Rinja will automatically insert the first (`Escaper`) argument,
/// so this filter only takes a single argument of any type that implements
/// `Display`.
///
/// It is possible to optionally specify an escaper other than the default for
/// the template's extension, like `{{ val|escape("txt") }}`.
#[inline]
pub fn escape<E, T>(e: E, v: T) -> Result<MarkupDisplay<E, T>, Infallible>
where
E: Escaper,
T: fmt::Display,
{
Ok(MarkupDisplay::new_unsafe(v, e))
}

/// Alias for [`escape()`]
#[inline]
pub fn e<E, T>(e: E, v: T) -> Result<MarkupDisplay<E, T>, Infallible>
where
E: Escaper,
T: fmt::Display,
{
escape(e, v)
}

#[cfg(feature = "humansize")]
/// Returns adequate string representation (in KB, ..) of number of bytes
///
Expand Down
1 change: 0 additions & 1 deletion rinja/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ pub mod helpers;

use std::fmt;

pub use filters::escape::{Html, MarkupDisplay, Text};
pub use rinja_derive::Template;

#[doc(hidden)]
Expand Down
Loading

0 comments on commit 0aead15

Please sign in to comment.