Simplify |escape: make it a "normal" filter

This commit is contained in:
René Kijewski 2024-07-07 01:58:02 +02:00
parent 48d69bfb4c
commit 52915f4ce3
8 changed files with 133 additions and 215 deletions

View File

@ -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);
@ -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());
});
}

View File

@ -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);
@ -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);
}
@ -32,7 +30,7 @@ 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());
}
});
}
@ -40,10 +38,7 @@ fn escape_json_for_html(b: &mut criterion::Bencher<'_>) {
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(),);
}
});
}

View File

@ -1,112 +1,74 @@
use core::fmt::{self, Display, Formatter, Write};
use core::str;
use std::convert::Infallible;
use std::fmt::{self, Display, Formatter, Write};
use std::str;
#[derive(Debug)]
pub struct MarkupDisplay<E, T>
where
E: Escaper,
T: Display,
{
value: DisplayValue<T>,
escaper: E,
/// 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)
}
#[inline]
fn write_char(&mut self, c: char) -> fmt::Result {
self.1.write_escaped_char(&mut self.0, c)
}
}
#[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
}
Ok(EscapeDisplay(text, escaper))
}
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),
}
}
}
#[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 }
}
#[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'"';
@ -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;
}
#[cfg(test)]
mod tests {
extern crate std;
use std::string::ToString;
use super::*;
#[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, 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");
}

View File

@ -3,7 +3,7 @@
//! 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;
@ -11,7 +11,7 @@ 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")]
@ -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
///

View File

@ -70,7 +70,6 @@ pub mod helpers;
use std::fmt;
pub use filters::escape::{Html, MarkupDisplay, Text};
pub use rinja_derive::Template;
#[doc(hidden)]

View File

@ -100,8 +100,11 @@ impl<'a> Config<'a> {
escapers.push((str_set(&escaper.extensions), escaper.path.into()));
}
}
for (extensions, path) in DEFAULT_ESCAPERS {
escapers.push((str_set(extensions), format!("{CRATE}{path}").into()));
for (extensions, name) in DEFAULT_ESCAPERS {
escapers.push((
str_set(extensions),
format!("{CRATE}::filters::{name}").into(),
));
}
Ok(Config {
@ -316,9 +319,11 @@ pub(crate) fn get_template_source(
static CONFIG_FILE_NAME: &str = "rinja.toml";
static DEFAULT_SYNTAX_NAME: &str = "default";
static DEFAULT_ESCAPERS: &[(&[&str], &str)] = &[
(&["html", "htm", "svg", "xml"], "::Html"),
(&["md", "none", "txt", "yml", ""], "::Text"),
(&["j2", "jinja", "jinja2"], "::Html"),
(
&["html", "htm", "j2", "jinja", "jinja2", "svg", "xml"],
"Html",
),
(&["md", "none", "txt", "yml", ""], "Text"),
];
#[cfg(test)]
@ -571,7 +576,7 @@ mod tests {
let config = Config::new(
r#"
[[escaper]]
path = "::rinja::Js"
path = "::my_filters::Js"
extensions = ["js"]
"#,
None,
@ -581,16 +586,15 @@ mod tests {
assert_eq!(
config.escapers,
vec![
(str_set(&["js"]), "::rinja::Js".into()),
(str_set(&["js"]), "::my_filters::Js".into()),
(
str_set(&["html", "htm", "svg", "xml"]),
"::rinja::Html".into()
str_set(&["html", "htm", "j2", "jinja", "jinja2", "svg", "xml"]),
"::rinja::filters::Html".into()
),
(
str_set(&["md", "none", "txt", "yml", ""]),
"::rinja::Text".into()
"::rinja::filters::Text".into()
),
(str_set(&["j2", "jinja", "jinja2"]), "::rinja::Html".into()),
]
);
}

View File

@ -1166,8 +1166,8 @@ impl<'a> Generator<'a> {
let expression = match wrapped {
DisplayWrap::Wrapped => expr,
DisplayWrap::Unwrapped => format!(
"{CRATE}::MarkupDisplay::new_unsafe(&({}), {})",
expr, self.input.escaper
"{CRATE}::filters::escape(&({expr}), {})?",
self.input.escaper,
),
};
let id = match expr_cache.entry(expression) {
@ -1393,12 +1393,9 @@ impl<'a> Generator<'a> {
if args.len() != 1 {
return Err(ctx.generate_error("unexpected argument(s) in `safe` filter", node));
}
buf.write(CRATE);
buf.write("::filters::safe(");
buf.write(self.input.escaper);
buf.write(", ");
buf.write(format_args!("{CRATE}::filters::safe("));
self._visit_args(ctx, buf, args)?;
buf.write(")?");
buf.write(format_args!(", {})?", self.input.escaper));
Ok(DisplayWrap::Wrapped)
}
@ -1433,12 +1430,9 @@ impl<'a> Generator<'a> {
.ok_or_else(|| ctx.generate_error("invalid escaper for escape filter", node))?,
None => self.input.escaper,
};
buf.write(CRATE);
buf.write("::filters::escape(");
buf.write(escaper);
buf.write(", ");
buf.write(format_args!("{CRATE}::filters::escape("));
self._visit_args(ctx, buf, &args[..1])?;
buf.write(")?");
buf.write(format_args!(", {escaper})?"));
Ok(DisplayWrap::Wrapped)
}

View File

@ -8,6 +8,7 @@ use crate::build_template;
fn check_if_let() {
// This function makes it much easier to compare expected code by adding the wrapping around
// the code we want to check.
#[track_caller]
fn compare(jinja: &str, expected: &str) {
let jinja = format!(
r##"#[template(source = r#"{jinja}"#, ext = "txt")]
@ -57,7 +58,7 @@ impl ::std::fmt::Display for Foo {{
::std::write!(
writer,
"{expr0}",
expr0 = &::rinja::MarkupDisplay::new_unsafe(&(query), ::rinja::Text),
expr0 = &::rinja::filters::escape(&(query), ::rinja::filters::Text)?,
)?;
}"#,
);
@ -70,7 +71,7 @@ impl ::std::fmt::Display for Foo {{
::std::write!(
writer,
"{expr0}",
expr0 = &::rinja::MarkupDisplay::new_unsafe(&(s), ::rinja::Text),
expr0 = &::rinja::filters::escape(&(s), ::rinja::filters::Text)?,
)?;
}"#,
);
@ -83,7 +84,7 @@ impl ::std::fmt::Display for Foo {{
::std::write!(
writer,
"{expr0}",
expr0 = &::rinja::MarkupDisplay::new_unsafe(&(s), ::rinja::Text),
expr0 = &::rinja::filters::escape(&(s), ::rinja::filters::Text)?,
)?;
}"#,
);