diff --git a/rinja/src/helpers.rs b/rinja/src/helpers.rs index 719b99f96..5290a9cda 100644 --- a/rinja/src/helpers.rs +++ b/rinja/src/helpers.rs @@ -4,6 +4,8 @@ use std::cell::Cell; use std::fmt; use std::iter::{Enumerate, Peekable}; +use crate::filters::FastWritable; + pub struct TemplateLoop where I: Iterator, @@ -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(&self, _: &mut W) -> fmt::Result { + Ok(()) + } +} diff --git a/rinja_derive/src/generator.rs b/rinja_derive/src/generator.rs index eb9d5556f..7482a8891 100644 --- a/rinja_derive/src/generator.rs +++ b/rinja_derive/src/generator.rs @@ -1564,20 +1564,44 @@ impl<'a> Generator<'a> { args: &[WithSpan<'_, Expr<'_>>], node: &WithSpan<'_, T>, ) -> Result { - 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( @@ -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<'_>, @@ -2268,6 +2314,67 @@ impl<'a> Generator<'a> { } } +fn expr_is_int_lit_plus_minus_one(expr: &WithSpan<'_, Expr<'_>>) -> Option { + fn is_signed_singular( + from_str_radix: impl Fn(&str, u32) -> Result, + value: &str, + plus_one: T, + minus_one: T, + ) -> Option { + Some([plus_one, minus_one].contains(&from_str_radix(value, 10).ok()?)) + } + + fn is_unsigned_singular( + from_str_radix: impl Fn(&str, u32) -> Result, + value: &str, + plus_one: T, + ) -> Option { + 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> { @@ -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#"")?;"#; diff --git a/rinja_derive/src/tests.rs b/rinja_derive/src/tests.rs index 258bc3cfd..6b95e9508 100644 --- a/rinja_derive/src/tests.rs +++ b/rinja_derive/src/tests.rs @@ -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, + ); +}