Skip to content

Commit

Permalink
filters: escape arguments to pluralize at compile time
Browse files Browse the repository at this point in the history
  • Loading branch information
Kijewski committed Sep 13, 2024
1 parent 381a16b commit 5b9a623
Show file tree
Hide file tree
Showing 3 changed files with 328 additions and 8 deletions.
20 changes: 20 additions & 0 deletions rinja/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use std::cell::Cell;
use std::fmt;
use std::iter::{Enumerate, Peekable};

use crate::filters::FastWritable;

pub struct TemplateLoop<I>
where
I: Iterator,
Expand Down Expand Up @@ -128,3 +130,21 @@ primitive_type! {
i8, i16, i32, i64, i128, isize,
u8, u16, u32, u64, u128, usize,
}

/// An empty element, so nothing will be written.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Empty;

impl fmt::Display for Empty {
#[inline]
fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
Ok(())
}
}

impl FastWritable for Empty {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, _: &mut W) -> fmt::Result {
Ok(())
}
}
131 changes: 123 additions & 8 deletions rinja_derive/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1564,20 +1564,44 @@ impl<'a> Generator<'a> {
args: &[WithSpan<'_, Expr<'_>>],
node: &WithSpan<'_, T>,
) -> Result<DisplayWrap, CompileError> {
buf.write(format_args!("{CRATE}::filters::pluralize("));
self._visit_args(ctx, buf, args)?;
match args.len() {
1 => buf.write(r#", "", "s""#),
2 => buf.write(r#", "s""#),
3 => {}
const SINGULAR: &WithSpan<'static, Expr<'static>> = &WithSpan::new(
Expr::StrLit(StrLit {
prefix: None,
content: "",
}),
"",
);
const PLURAL: &WithSpan<'static, Expr<'static>> = &WithSpan::new(
Expr::StrLit(StrLit {
prefix: None,
content: "s",
}),
"",
);

let (count, sg, pl) = match args {
[count] => (count, SINGULAR, PLURAL),
[count, sg] => (count, sg, PLURAL),
[count, sg, pl] => (count, sg, pl),
_ => {
return Err(
ctx.generate_error("unexpected argument(s) in `pluralize` filter", node)
);
}
};
if let Some(is_singular) = expr_is_int_lit_plus_minus_one(count) {
let value = if is_singular { sg } else { pl };
self._visit_auto_escaped_arg(ctx, buf, value)?;
} else {
buf.write(format_args!("{CRATE}::filters::pluralize("));
self._visit_arg(ctx, buf, count)?;
for value in [sg, pl] {
buf.write(", ");
self._visit_auto_escaped_arg(ctx, buf, value)?;
}
buf.write(format_args!(")?"));
}
buf.write(")?");
Ok(DisplayWrap::Unwrapped)
Ok(DisplayWrap::Wrapped)
}

fn _visit_linebreaks_filter<T>(
Expand Down Expand Up @@ -1844,6 +1868,28 @@ impl<'a> Generator<'a> {
Ok(())
}

fn _visit_auto_escaped_arg(
&mut self,
ctx: &Context<'_>,
buf: &mut Buffer,
arg: &WithSpan<'_, Expr<'_>>,
) -> Result<(), CompileError> {
if let Some(Writable::Lit(arg)) = compile_time_escape(arg, self.input.escaper) {
if !arg.is_empty() {
buf.write(format_args!("{CRATE}::filters::Safe("));
buf.write_escaped_str(&arg);
buf.write(')');
} else {
buf.write(format_args!("{CRATE}::helpers::Empty"));
}
} else {
buf.write(format_args!("(&&::rinja::filters::AutoEscaper::new("));
self._visit_arg(ctx, buf, arg)?;
buf.write(format_args!(", {}))", self.input.escaper));
}
Ok(())
}

fn visit_attr(
&mut self,
ctx: &Context<'_>,
Expand Down Expand Up @@ -2268,6 +2314,67 @@ impl<'a> Generator<'a> {
}
}

fn expr_is_int_lit_plus_minus_one(expr: &WithSpan<'_, Expr<'_>>) -> Option<bool> {
fn is_signed_singular<T: Eq + Default, E>(
from_str_radix: impl Fn(&str, u32) -> Result<T, E>,
value: &str,
plus_one: T,
minus_one: T,
) -> Option<bool> {
Some([plus_one, minus_one].contains(&from_str_radix(value, 10).ok()?))
}

fn is_unsigned_singular<T: Eq + Default, E>(
from_str_radix: impl Fn(&str, u32) -> Result<T, E>,
value: &str,
plus_one: T,
) -> Option<bool> {
Some(from_str_radix(value, 10).ok()? == plus_one)
}

let Expr::NumLit(_, Num::Int(value, kind)) = **expr else {
return None;
};
match kind {
Some(IntKind::I8) => is_signed_singular(i8::from_str_radix, value, 1, -1),
Some(IntKind::I16) => is_signed_singular(i16::from_str_radix, value, 1, -1),
Some(IntKind::I32) => is_signed_singular(i32::from_str_radix, value, 1, -1),
Some(IntKind::I64) => is_signed_singular(i64::from_str_radix, value, 1, -1),
Some(IntKind::I128) => is_signed_singular(i128::from_str_radix, value, 1, -1),
Some(IntKind::Isize) => {
if cfg!(target_pointer_width = "16") {
is_signed_singular(i16::from_str_radix, value, 1, -1)
} else if cfg!(target_pointer_width = "32") {
is_signed_singular(i32::from_str_radix, value, 1, -1)
} else if cfg!(target_pointer_width = "64") {
is_signed_singular(i64::from_str_radix, value, 1, -1)
} else {
unreachable!("unexpected `cfg!(target_pointer_width)`")
}
}
Some(IntKind::U8) => is_unsigned_singular(u8::from_str_radix, value, 1),
Some(IntKind::U16) => is_unsigned_singular(u16::from_str_radix, value, 1),
Some(IntKind::U32) => is_unsigned_singular(u32::from_str_radix, value, 1),
Some(IntKind::U64) => is_unsigned_singular(u64::from_str_radix, value, 1),
Some(IntKind::U128) => is_unsigned_singular(u128::from_str_radix, value, 1),
Some(IntKind::Usize) => {
if cfg!(target_pointer_width = "16") {
is_unsigned_singular(u16::from_str_radix, value, 1)
} else if cfg!(target_pointer_width = "32") {
is_unsigned_singular(u32::from_str_radix, value, 1)
} else if cfg!(target_pointer_width = "64") {
is_unsigned_singular(u64::from_str_radix, value, 1)
} else {
unreachable!("unexpected `cfg!(target_pointer_width)`")
}
}
None => match value.starts_with('-') {
true => is_signed_singular(i128::from_str_radix, value, 1, -1),
false => is_unsigned_singular(u128::from_str_radix, value, 1),
},
}
}

/// In here, we inspect in the expression if it is a literal, and if it is, whether it
/// can be escaped at compile time.
fn compile_time_escape<'a>(expr: &Expr<'a>, escaper: &str) -> Option<Writable<'a>> {
Expand Down Expand Up @@ -2439,6 +2546,14 @@ impl Buffer {
}
}

fn write_escaped_str(&mut self, s: &str) {
if !self.discard {
self.buf.push('"');
string_escape(&mut self.buf, s);
self.buf.push('"');
}
}

fn write_writer(&mut self, s: &str) -> usize {
const OPEN: &str = r#"writer.write_str(""#;
const CLOSE: &str = r#"")?;"#;
Expand Down
185 changes: 185 additions & 0 deletions rinja_derive/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -731,3 +731,188 @@ fn test_code_in_comment() {
let generated = build_template(&ast).unwrap();
assert!(!generated.contains("compile_error"));
}

#[test]
fn test_pluralize() {
compare(
r#"{{dogs}} dog{{dogs|pluralize}}"#,
r#"
match (
&((&&::rinja::filters::AutoEscaper::new(
&(self.dogs),
::rinja::filters::Text,
))
.rinja_auto_escape()?),
&(::rinja::filters::pluralize(
&(self.dogs),
::rinja::helpers::Empty,
::rinja::filters::Safe("s"),
)?),
) {
(expr0, expr3) => {
(&&::rinja::filters::Writable(expr0)).rinja_write(writer)?;
writer.write_str(" dog")?;
(&&::rinja::filters::Writable(expr3)).rinja_write(writer)?;
}
}"#,
&[("dogs", "i8")],
10,
);
compare(
r#"{{dogs}} dog{{dogs|pluralize("go")}}"#,
r#"
match (
&((&&::rinja::filters::AutoEscaper::new(
&(self.dogs),
::rinja::filters::Text,
))
.rinja_auto_escape()?),
&(::rinja::filters::pluralize(
&(self.dogs),
::rinja::filters::Safe("go"),
::rinja::filters::Safe("s"),
)?),
) {
(expr0, expr3) => {
(&&::rinja::filters::Writable(expr0)).rinja_write(writer)?;
writer.write_str(" dog")?;
(&&::rinja::filters::Writable(expr3)).rinja_write(writer)?;
}
}"#,
&[("dogs", "i8")],
10,
);
compare(
r#"{{mice}} {{mice|pluralize("mouse", "mice")}}"#,
r#"
match (
&((&&::rinja::filters::AutoEscaper::new(
&(self.mice),
::rinja::filters::Text,
))
.rinja_auto_escape()?),
&(::rinja::filters::pluralize(
&(self.mice),
::rinja::filters::Safe("mouse"),
::rinja::filters::Safe("mice"),
)?),
) {
(expr0, expr2) => {
(&&::rinja::filters::Writable(expr0)).rinja_write(writer)?;
writer.write_str(" ")?;
(&&::rinja::filters::Writable(expr2)).rinja_write(writer)?;
}
}"#,
&[("dogs", "i8")],
7,
);

compare(
r#"{{count|pluralize(one, count)}}"#,
r#"
match (
&(::rinja::filters::pluralize(
&(self.count),
(&&::rinja::filters::AutoEscaper::new(
&(self.one),
::rinja::filters::Text,
)),
(&&::rinja::filters::AutoEscaper::new(
&(self.count),
::rinja::filters::Text,
)),
)?),
) {
(expr0,) => {
(&&::rinja::filters::Writable(expr0)).rinja_write(writer)?;
}
}"#,
&[("count", "i8"), ("one", "&'static str")],
3,
);

compare(
r#"{{0|pluralize(sg, pl)}}"#,
r#"
match (
&((&&::rinja::filters::AutoEscaper::new(
&(self.pl),
::rinja::filters::Text,
))),
) {
(expr0,) => {
(&&::rinja::filters::Writable(expr0)).rinja_write(writer)?;
}
}
"#,
&[("sg", "&'static str"), ("pl", "&'static str")],
3,
);
compare(
r#"{{1|pluralize(sg, pl)}}"#,
r#"
match (
&((&&::rinja::filters::AutoEscaper::new(
&(self.sg),
::rinja::filters::Text,
))),
) {
(expr0,) => {
(&&::rinja::filters::Writable(expr0)).rinja_write(writer)?;
}
}
"#,
&[("sg", "&'static str"), ("pl", "&'static str")],
3,
);

compare(
r#"{{0|pluralize("sg", "pl")}}"#,
r#"
match (&(::rinja::filters::Safe("pl")),) {
(expr0,) => {
(&&::rinja::filters::Writable(expr0)).rinja_write(writer)?;
}
}
"#,
&[],
3,
);
compare(
r#"{{1|pluralize("sg", "pl")}}"#,
r#"
match (&(::rinja::filters::Safe("sg")),) {
(expr0,) => {
(&&::rinja::filters::Writable(expr0)).rinja_write(writer)?;
}
}
"#,
&[],
3,
);

compare(
r#"{{0|pluralize}}"#,
r#"
match (&(::rinja::filters::Safe("s")),) {
(expr0,) => {
(&&::rinja::filters::Writable(expr0)).rinja_write(writer)?;
}
}
"#,
&[],
3,
);
compare(
r#"{{1|pluralize}}"#,
r#"
match (&(::rinja::helpers::Empty),) {
(expr0,) => {
(&&::rinja::filters::Writable(expr0)).rinja_write(writer)?;
}
}
"#,
&[],
3,
);
}

0 comments on commit 5b9a623

Please sign in to comment.