Skip to content

Commit

Permalink
Merge pull request #53 from Kijewski/pr-simplify-escape
Browse files Browse the repository at this point in the history
Remove `rinja_escape` and make `|escape` a "normal" filter
  • Loading branch information
Kijewski authored Jul 7, 2024
2 parents 8c12f7e + 9beca5e commit d401de0
Show file tree
Hide file tree
Showing 20 changed files with 266 additions and 320 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
strategy:
matrix:
package: [
rinja, rinja_actix, rinja_axum, rinja_derive, rinja_derive_standalone, rinja_escape,
rinja, rinja_actix, rinja_axum, rinja_derive, rinja_derive_standalone,
rinja_parser, rinja_rocket, rinja_warp, testing, examples/actix-web-app,
]
runs-on: ubuntu-latest
Expand Down
2 changes: 0 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ members = [
"rinja_axum",
"rinja_derive",
"rinja_derive_standalone",
"rinja_escape",
"rinja_parser",
"rinja_rocket",
"rinja_warp",
Expand All @@ -16,7 +15,6 @@ resolver = "2"
default-members = [
"rinja",
"rinja_derive",
"rinja_escape",
"rinja_parser",
"testing",
]
5 changes: 4 additions & 1 deletion rinja/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ with-warp = ["rinja_derive/with-warp"]

[dependencies]
rinja_derive = { version = "0.13", path = "../rinja_derive" }
rinja_escape = { version = "0.11", path = "../rinja_escape" }
humansize = { version = "2", optional = true }
num-traits = { version = "0.2.6", optional = true }
percent-encoding = { version = "2.1.0", optional = true }
Expand All @@ -48,6 +47,10 @@ name = "to-json"
harness = false
required-features = ["serde_json"]

[[bench]]
name = "escape"
harness = false

[package.metadata.docs.rs]
features = ["default", "serde_json"]
rustdoc-args = ["--generate-link-to-definition", "--cfg=docsrs"]
17 changes: 7 additions & 10 deletions rinja_escape/benches/all.rs → rinja/benches/escape.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
#[macro_use]
extern crate criterion;

use criterion::Criterion;
use rinja_escape::{Html, MarkupDisplay};
use criterion::{criterion_group, criterion_main, Criterion};
use rinja::filters::{escape, Html};

criterion_main!(benches);
criterion_group!(benches, functions);
Expand Down Expand Up @@ -68,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_escape::{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
149 changes: 149 additions & 0 deletions rinja/src/filters/escape.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
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 if 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)
}

/// 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)
}
}

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))
}

/// 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_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'"';
const MAX_CHAR: u8 = b'>';
const TABLE: [Option<&&str>; (MAX_CHAR - MIN_CHAR + 1) as usize] = {
let mut table = [None; (MAX_CHAR - MIN_CHAR + 1) as usize];
table[(b'<' - MIN_CHAR) as usize] = Some(&"&lt;");
table[(b'>' - MIN_CHAR) as usize] = Some(&"&gt;");
table[(b'&' - MIN_CHAR) as usize] = Some(&"&amp;");
table[(b'"' - MIN_CHAR) as usize] = Some(&"&quot;");
table[(b'\'' - MIN_CHAR) as usize] = Some(&"&#x27;");
table
};

let escaped = match byte {
MIN_CHAR..=MAX_CHAR => TABLE[(byte - MIN_CHAR) as usize],
_ => None,
};
if let Some(escaped) = escaped {
fmt.write_str(&string[last..index])?;
fmt.write_str(escaped)?;
last = index + 1;
}
}
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 {
#[inline]
fn write_escaped_str<W: Write>(&self, mut fmt: W, string: &str) -> fmt::Result {
fmt.write_str(string)
}

#[inline]
fn write_escaped_char<W: Write>(&self, mut fmt: W, c: char) -> fmt::Result {
fmt.write_char(c)
}
}

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

#[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");
}
56 changes: 7 additions & 49 deletions rinja/src/filters/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@
//! Contains all the built-in filter functions for use in templates.
//! You can define your own filters, as well.
mod escape;
#[cfg(feature = "serde_json")]
mod json;

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

#[cfg(feature = "serde_json")]
mod json;
pub use escape::{e, escape, safe, Escaper, Html, Text};
#[cfg(feature = "humansize")]
use humansize::{ISizeFormatter, ToF64, DECIMAL};
#[cfg(feature = "serde_json")]
pub use json::{json, json_pretty, AsIndent};
#[cfg(feature = "num-traits")]
use num_traits::{cast::NumCast, Signed};
#[cfg(feature = "urlencode")]
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use rinja_escape::{Escaper, MarkupDisplay};

#[cfg(feature = "serde_json")]
pub use self::json::{json, json_pretty, AsIndent};
use crate::{Error, Result};

#[cfg(feature = "urlencode")]
Expand All @@ -38,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 @@ -71,7 +71,6 @@ pub mod helpers;
use std::fmt;

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

#[doc(hidden)]
pub use crate as shared;
Expand Down
Loading

0 comments on commit d401de0

Please sign in to comment.